Fixup STAT tables of single-axis variable fonts to aid desktop apps (style linking). Related to #142
This commit is contained in:
parent
35a23627a5
commit
70f3df7882
3 changed files with 340 additions and 18 deletions
31
Makefile
31
Makefile
|
|
@ -49,9 +49,9 @@ var: \
|
|||
$(FONTDIR)/var/Inter.var.ttf
|
||||
all_var: \
|
||||
$(FONTDIR)/var/Inter.var.woff2 \
|
||||
$(FONTDIR)/var/Inter.var.ttf \
|
||||
$(FONTDIR)/var/Inter-upright.var.woff2 \
|
||||
$(FONTDIR)/var/Inter-italic.var.woff2 \
|
||||
$(FONTDIR)/var/Inter.var.ttf \
|
||||
$(FONTDIR)/var/Inter-upright.var.ttf \
|
||||
$(FONTDIR)/var/Inter-italic.var.ttf
|
||||
|
||||
|
|
@ -87,11 +87,11 @@ build/%.woff: build/%.ttf
|
|||
.PRECIOUS: build/%.ttf
|
||||
|
||||
|
||||
|
||||
# Master UFOs -> variable TTF
|
||||
$(FONTDIR)/var/%.var.ttf: src/%.designspace $(all_ufo_masters)
|
||||
misc/fontbuild compile-var -o $@ $<
|
||||
|
||||
|
||||
# Instance UFO -> OTF, TTF (note: masters' rules in generated.make)
|
||||
$(FONTDIR)/const/Inter-%.otf: build/ufo/Inter-%.ufo
|
||||
misc/fontbuild compile -o $@ $<
|
||||
|
|
@ -156,7 +156,12 @@ test: all_check_const all_check_var
|
|||
# check does the same thing as test, but without any dependency checks, meaning
|
||||
# it will check whatever font files are already built.
|
||||
check:
|
||||
misc/fontbuild checkfont $(FONTDIR)/const/*.* $(FONTDIR)/var/*.*
|
||||
misc/fontbuild checkfont \
|
||||
$(FONTDIR)/const/*.ttf \
|
||||
$(FONTDIR)/const/*.otf \
|
||||
$(FONTDIR)/const/*.woff2 \
|
||||
$(FONTDIR)/var/*.ttf \
|
||||
$(FONTDIR)/var/*.woff2
|
||||
@echo "check: all ok"
|
||||
|
||||
.PHONY: test check
|
||||
|
|
@ -177,8 +182,6 @@ $(FONTDIR)/samples:
|
|||
mkdir -p $@
|
||||
|
||||
|
||||
|
||||
|
||||
ZD = build/tmp/zip
|
||||
# intermediate zip target that creates a zip file at build/tmp/a.zip
|
||||
build/tmp/a.zip:
|
||||
|
|
@ -210,6 +213,14 @@ build/tmp/a.zip:
|
|||
cp -a misc/dist/*.txt "$(ZD)/"
|
||||
cp -a LICENSE.txt "$(ZD)/"
|
||||
@#
|
||||
@# Fix VF metadata
|
||||
misc/tools/fix-vf-meta.py \
|
||||
"$(ZD)/Inter (web)/Inter-upright.var.woff2" \
|
||||
"$(ZD)/Inter (web)/Inter-italic.var.woff2"
|
||||
misc/tools/fix-vf-meta.py \
|
||||
"$(ZD)/Inter (TTF variable)/Inter-upright.var.ttf" \
|
||||
"$(ZD)/Inter (TTF variable)/Inter-italic.var.ttf"
|
||||
@#
|
||||
@# Add "beta" to Light and Thin filenames.
|
||||
@# Requires "rename" tool in PATH (`brew install rename` on macOS)
|
||||
rename 's/(Light.*|Thin.*)\./$$1-BETA./' "$(ZD)/Inter"*/*.*
|
||||
|
|
@ -235,14 +246,14 @@ zip: all
|
|||
$(MAKE) check
|
||||
$(MAKE) build/release/Inter-${VERSION}-$(shell git rev-parse --short=10 HEAD).zip
|
||||
|
||||
zip_dist: pre_dist all
|
||||
zip_dist: pre_dist
|
||||
$(MAKE) check
|
||||
$(MAKE) ${ZIP_FILE_DIST}
|
||||
|
||||
.PHONY: zip zip_dist
|
||||
|
||||
# distribution
|
||||
pre_dist:
|
||||
pre_dist: all
|
||||
@echo "Creating distribution for version ${VERSION}"
|
||||
@if [ -f "${ZIP_FILE_DIST}" ]; \
|
||||
then echo "${ZIP_FILE_DIST} already exists. Bump version or remove the zip file to continue." >&2; \
|
||||
|
|
@ -295,24 +306,20 @@ build/UnicodeData.txt:
|
|||
@echo fetch http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
|
||||
@curl '-#' -o "$@" http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
|
||||
|
||||
|
||||
# install targets
|
||||
install_ttf: all_ttf_const
|
||||
$(MAKE) all_web -j
|
||||
@echo "Installing TTF files locally at ~/Library/Fonts/Inter"
|
||||
rm -rf ~/'Library/Fonts/Inter'
|
||||
mkdir -p ~/'Library/Fonts/Inter'
|
||||
cp -va $(FONTDIR)/const/*.ttf ~/'Library/Fonts/Inter'
|
||||
|
||||
install_ttf_hinted: all_ttf
|
||||
$(MAKE) all_web -j
|
||||
install_ttf_hinted: all_ttf_hinted
|
||||
@echo "Installing autohinted TTF files locally at ~/Library/Fonts/Inter"
|
||||
rm -rf ~/'Library/Fonts/Inter'
|
||||
mkdir -p ~/'Library/Fonts/Inter'
|
||||
cp -va $(FONTDIR)/const-hinted/*.ttf ~/'Library/Fonts/Inter'
|
||||
|
||||
install_otf: all_otf
|
||||
$(MAKE) all_web -j
|
||||
@echo "Installing OTF files locally at ~/Library/Fonts/Inter"
|
||||
rm -rf ~/'Library/Fonts/Inter'
|
||||
mkdir -p ~/'Library/Fonts/Inter'
|
||||
|
|
|
|||
312
misc/tools/fix-vf-meta.py
Executable file
312
misc/tools/fix-vf-meta.py
Executable file
|
|
@ -0,0 +1,312 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# from gftools
|
||||
# https://github.com/googlefonts/gftools/blob/master/LICENSE
|
||||
#
|
||||
"""
|
||||
Fontmake can only generate a single variable font. It cannot generate a
|
||||
family of variable fonts, that are related to one another.
|
||||
|
||||
This script will update the nametables and STAT tables so a family
|
||||
which has more than one variable font will work correctly in desktop
|
||||
applications.
|
||||
|
||||
It will also work on single font VF families by creating a better STAT
|
||||
table.
|
||||
|
||||
TODO make script work on VFs which have multiple axises. We'll need to
|
||||
change the axis array format to v4 (we're using v1),
|
||||
https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4
|
||||
|
||||
Atm, the script will work well for single axis fonts and families which
|
||||
have a single vf for Roman and another for Italic/Condensed, both using the wght
|
||||
axis (covers 95% of GF cases).
|
||||
"""
|
||||
from argparse import ArgumentParser
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.ttLib.tables import otTables
|
||||
import os
|
||||
import sys
|
||||
if sys.version_info.major == 3:
|
||||
unicode = str
|
||||
|
||||
OS_2_WEIGHT_CLASS = {
|
||||
'Thin': 100,
|
||||
'ExtraLight': 200,
|
||||
'Light': 300,
|
||||
'Regular': 400,
|
||||
'': 400,
|
||||
'Medium': 500,
|
||||
'SemiBold': 600,
|
||||
'Bold': 700,
|
||||
'ExtraBold': 800,
|
||||
'Black': 900,
|
||||
}
|
||||
|
||||
|
||||
def _parse_styles(stylename):
|
||||
bold, italic = False, False
|
||||
if 'Italic' in stylename:
|
||||
italic = True
|
||||
bold = False
|
||||
if 'Bold' == stylename or 'Bold Italic' == stylename:
|
||||
bold = True
|
||||
return bold, italic
|
||||
|
||||
|
||||
def set_fsselection(style, fsselection,):
|
||||
bold, italic = _parse_styles(style)
|
||||
|
||||
mask = 0b1100001
|
||||
fsselection = (fsselection | mask) ^ mask
|
||||
|
||||
if bold:
|
||||
fsselection |= 0b100000
|
||||
else:
|
||||
fsselection |= 0b1000000
|
||||
if italic:
|
||||
# unset Reg bit
|
||||
fsselection = (fsselection | 0b1000000) ^ 0b1000000
|
||||
fsselection |= 0b1
|
||||
return fsselection
|
||||
|
||||
|
||||
def set_mac_style(stylename, macstyle):
|
||||
bold, italic = _parse_styles(stylename)
|
||||
|
||||
mask = ~0b11
|
||||
bold_bit = 0b1 if bold else 0b0
|
||||
italic_bit = 0b10 if italic else 0b0
|
||||
|
||||
macstyle = (macstyle | mask) ^ mask
|
||||
macstyle |= (bold_bit + italic_bit)
|
||||
return macstyle
|
||||
|
||||
|
||||
def set_weight_class(stylename):
|
||||
weight = stylename.replace('Italic', '').replace(' ', '')
|
||||
return OS_2_WEIGHT_CLASS[weight]
|
||||
|
||||
|
||||
def fonts_are_same_family(ttfonts):
|
||||
"""Check fonts have the same preferred family name or family name"""
|
||||
family_names = []
|
||||
for ttfont in ttfonts:
|
||||
pref_family_name = ttfont['name'].getName(16, 3, 1, 1033)
|
||||
family_name = ttfont['name'].getName(1, 3, 1, 1033)
|
||||
name = pref_family_name if pref_family_name else family_name
|
||||
family_names.append(name.toUnicode())
|
||||
if len(set(family_names)) != 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def fix_bits(ttfont):
|
||||
"""Set fsSelection, macStyle and usWeightClass to correct values.
|
||||
|
||||
The values must be derived from the default style. By default, the
|
||||
Regular instance's values are used"""
|
||||
dflt_style = _get_vf_default_style(ttfont)
|
||||
ttfont['OS/2'].fsSelection = set_fsselection(
|
||||
dflt_style, ttfont['OS/2'].fsSelection
|
||||
)
|
||||
ttfont['OS/2'].usWeightClass = set_weight_class(dflt_style)
|
||||
ttfont['head'].macStyle = set_mac_style(
|
||||
dflt_style, ttfont['head'].macStyle
|
||||
)
|
||||
|
||||
|
||||
def create_stat_table(ttfont):
|
||||
"""Atm, Fontmake is only able to produce a basic stat table. Because of
|
||||
this, we'll create a STAT using the font's fvar table."""
|
||||
stat = newTable('STAT')
|
||||
stat.table = otTables.STAT()
|
||||
stat.table.Version = 0x00010001
|
||||
|
||||
# # Build DesignAxisRecords from fvar
|
||||
stat.table.DesignAxisRecord = otTables.AxisRecordArray()
|
||||
stat.table.DesignAxisRecord.Axis = []
|
||||
|
||||
stat_axises = stat.table.DesignAxisRecord.Axis
|
||||
|
||||
# TODO (M Foley) add support for fonts which have multiple
|
||||
# axises e.g Barlow
|
||||
if len(ttfont['fvar'].axes) > 1:
|
||||
raise Exception('VFs with more than one axis are currently '
|
||||
'not supported.')
|
||||
|
||||
for idx, axis in enumerate(ttfont['fvar'].axes):
|
||||
append_stat_axis(stat, axis.axisTag, axis.axisNameID)
|
||||
|
||||
# Build AxisValueArrays for each namedInstance from fvar namedInstances
|
||||
stat.table.AxisValueArray = otTables.AxisValueArray()
|
||||
stat.table.AxisValueArray.AxisValue = []
|
||||
|
||||
for idx, instance in enumerate(ttfont['fvar'].instances):
|
||||
append_stat_record(stat, 0, list(instance.coordinates.values())[0], instance.subfamilyNameID)
|
||||
|
||||
# Set ElidedFallbackNameID
|
||||
stat.table.ElidedFallbackNameID = 2
|
||||
ttfont['STAT'] = stat
|
||||
|
||||
|
||||
def _get_vf_types(ttfonts):
|
||||
styles = []
|
||||
for ttfont in ttfonts:
|
||||
styles.append(_get_vf_type(ttfont))
|
||||
return styles
|
||||
|
||||
|
||||
def _get_vf_type(ttfont):
|
||||
style = ttfont['name'].getName(2, 3, 1, 1033).toUnicode()
|
||||
return 'Italic' if 'Italic' in style else 'Roman'
|
||||
|
||||
|
||||
def _get_vf_default_style(ttfont):
|
||||
"""Return the name record string of the default style"""
|
||||
default_fvar_val = ttfont['fvar'].axes[0].defaultValue
|
||||
|
||||
name_id = None
|
||||
for inst in ttfont['fvar'].instances:
|
||||
if inst.coordinates['wght'] == default_fvar_val:
|
||||
name_id = inst.subfamilyNameID
|
||||
return ttfont['name'].getName(name_id, 3, 1, 1033).toUnicode()
|
||||
|
||||
|
||||
def add_other_vf_styles_to_nametable(ttfont, text_records):
|
||||
"""Each nametable in a font must reference every font in the family.
|
||||
Since fontmake doesn't append the other families to the nametable,
|
||||
we'll do this ourselves. Skip this step if these records already
|
||||
exist."""
|
||||
found = set()
|
||||
for name in ttfont['name'].names[:-len(text_records)-1:-1]:
|
||||
found.add(name.toUnicode())
|
||||
leftover = set(text_records) - found
|
||||
|
||||
if leftover:
|
||||
nameid = ttfont['name'].names[-1].nameID + 1
|
||||
for record in leftover:
|
||||
ttfont['name'].setName(unicode(record), nameid, 3, 1, 1033)
|
||||
nameid += 1
|
||||
|
||||
|
||||
def get_custom_name_record(ttfont, text):
|
||||
"""Return a name record by text. Record ID must be greater than 255"""
|
||||
for record in ttfont['name'].names[::-1]:
|
||||
if record.nameID > 255:
|
||||
rec_text = record.toUnicode()
|
||||
if rec_text == text:
|
||||
return record
|
||||
return None
|
||||
|
||||
|
||||
def append_stat_axis(stat, tag, namerecord_id):
|
||||
"""Add a STAT axis if the tag does not exist already."""
|
||||
has_tags = []
|
||||
axises = stat.table.DesignAxisRecord.Axis
|
||||
for axis in axises:
|
||||
has_tags.append(axis.AxisTag)
|
||||
|
||||
if tag in has_tags:
|
||||
raise Exception('{} has already been declared in the STAT table')
|
||||
|
||||
axis_record = otTables.AxisRecord()
|
||||
axis_record.AxisTag = tag
|
||||
axis_record.AxisNameID = namerecord_id
|
||||
axis_record.AxisOrdering = len(axises)
|
||||
axises.append(axis_record)
|
||||
|
||||
|
||||
def append_stat_record(stat, axis_index, value, namerecord_id, linked_value=None):
|
||||
records = stat.table.AxisValueArray.AxisValue
|
||||
axis_record = otTables.AxisValue()
|
||||
axis_record.Format = 1
|
||||
axis_record.ValueNameID = namerecord_id
|
||||
axis_record.Value = value
|
||||
axis_record.AxisIndex = axis_index
|
||||
|
||||
axis_record.Flags = 0
|
||||
if linked_value:
|
||||
axis_record.Format = 3
|
||||
axis_record.LinkedValue = linked_value
|
||||
records.append(axis_record)
|
||||
|
||||
|
||||
def get_stat_axis_index(ttfont, axis_name):
|
||||
axises = ttfont['STAT'].table.DesignAxisRecord.Axis
|
||||
available_axises = [a.AxisTag for a in axises]
|
||||
for idx, axis in enumerate(axises):
|
||||
if axis.AxisTag == axis_name:
|
||||
return idx
|
||||
raise Exception('{} is not a valid axis. Font has [{}] axises'.format(
|
||||
axis_name, available_axises)
|
||||
)
|
||||
|
||||
|
||||
def set_stat_for_font_in_family(ttfont, family_styles):
|
||||
"""Based on examples from:
|
||||
https://docs.microsoft.com/en-us/typography/opentype/spec/stat"""
|
||||
font_type = _get_vf_type(ttfont)
|
||||
# See example 5
|
||||
if font_type == 'Roman' and 'Italic' in family_styles:
|
||||
name_record = get_custom_name_record(ttfont, 'Italic')
|
||||
append_stat_axis(ttfont['STAT'], 'ital', name_record.nameID)
|
||||
|
||||
name_record = get_custom_name_record(ttfont, 'Roman')
|
||||
axis_idx = get_stat_axis_index(ttfont, 'ital')
|
||||
append_stat_record(ttfont['STAT'], axis_idx, 0, name_record.nameID, linked_value=1.0)
|
||||
|
||||
elif font_type == 'Italic' and 'Roman' in family_styles:
|
||||
name_record = get_custom_name_record(ttfont, 'Italic')
|
||||
append_stat_axis(ttfont['STAT'], 'ital', name_record.nameID)
|
||||
|
||||
name_record = get_custom_name_record(ttfont, 'Italic')
|
||||
axis_idx = get_stat_axis_index(ttfont, 'ital')
|
||||
append_stat_record(ttfont['STAT'], axis_idx, 1.0, name_record.nameID)
|
||||
|
||||
|
||||
def harmonize_vf_families(ttfonts):
|
||||
"""Make sure the fonts which are part of a vf family reference each other
|
||||
in both the nametable and STAT table. For examples see:
|
||||
https://docs.microsoft.com/en-us/typography/opentype/spec/stat
|
||||
|
||||
"""
|
||||
family_styles = _get_vf_types(ttfonts)
|
||||
for ttfont in ttfonts:
|
||||
add_other_vf_styles_to_nametable(ttfont, family_styles)
|
||||
set_stat_for_font_in_family(ttfont, family_styles)
|
||||
|
||||
|
||||
# def fixupFonts(font_paths):
|
||||
# ttfonts = [TTFont(p) for p in font_paths]
|
||||
# ttfonts = [f for f in ttfonts if len(f['fvar'].axes) == 1]
|
||||
# for f in ttfonts:
|
||||
# fix_bits(f)
|
||||
# create_stat_table(f)
|
||||
# harmonize_vf_families(ttfonts)
|
||||
# for path, f in zip(font_paths, ttfonts):
|
||||
# f.save(path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('fonts', nargs='+',
|
||||
help='All fonts within a font family must be included')
|
||||
args = parser.parse_args()
|
||||
font_paths = args.fonts
|
||||
ttfonts = [TTFont(p) for p in font_paths]
|
||||
if not fonts_are_same_family(ttfonts):
|
||||
raise Exception('Fonts have different family_names: [{}]'.format(
|
||||
', '.join(map(os.path.basename, font_paths))
|
||||
))
|
||||
|
||||
for ttfont in ttfonts:
|
||||
fix_bits(ttfont)
|
||||
create_stat_table(ttfont)
|
||||
harmonize_vf_families(ttfonts)
|
||||
|
||||
for path, ttfont in zip(font_paths, ttfonts):
|
||||
ttfont.save(path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -8,3 +8,6 @@ fs==2.3.0
|
|||
numpy==1.16.1
|
||||
scipy==1.2.0
|
||||
munkres==1.0.12
|
||||
|
||||
# for woff2
|
||||
brotli==1.0.7
|
||||
|
|
|
|||
Reference in a new issue