Fixup STAT tables of single-axis variable fonts to aid desktop apps (style linking). Related to #142

This commit is contained in:
Rasmus Andersson 2019-03-27 11:17:29 -07:00
parent 35a23627a5
commit 70f3df7882
3 changed files with 340 additions and 18 deletions

View file

@ -49,9 +49,9 @@ var: \
$(FONTDIR)/var/Inter.var.ttf $(FONTDIR)/var/Inter.var.ttf
all_var: \ all_var: \
$(FONTDIR)/var/Inter.var.woff2 \ $(FONTDIR)/var/Inter.var.woff2 \
$(FONTDIR)/var/Inter.var.ttf \
$(FONTDIR)/var/Inter-upright.var.woff2 \ $(FONTDIR)/var/Inter-upright.var.woff2 \
$(FONTDIR)/var/Inter-italic.var.woff2 \ $(FONTDIR)/var/Inter-italic.var.woff2 \
$(FONTDIR)/var/Inter.var.ttf \
$(FONTDIR)/var/Inter-upright.var.ttf \ $(FONTDIR)/var/Inter-upright.var.ttf \
$(FONTDIR)/var/Inter-italic.var.ttf $(FONTDIR)/var/Inter-italic.var.ttf
@ -87,11 +87,11 @@ build/%.woff: build/%.ttf
.PRECIOUS: build/%.ttf .PRECIOUS: build/%.ttf
# Master UFOs -> variable TTF # Master UFOs -> variable TTF
$(FONTDIR)/var/%.var.ttf: src/%.designspace $(all_ufo_masters) $(FONTDIR)/var/%.var.ttf: src/%.designspace $(all_ufo_masters)
misc/fontbuild compile-var -o $@ $< misc/fontbuild compile-var -o $@ $<
# Instance UFO -> OTF, TTF (note: masters' rules in generated.make) # Instance UFO -> OTF, TTF (note: masters' rules in generated.make)
$(FONTDIR)/const/Inter-%.otf: build/ufo/Inter-%.ufo $(FONTDIR)/const/Inter-%.otf: build/ufo/Inter-%.ufo
misc/fontbuild compile -o $@ $< 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 # check does the same thing as test, but without any dependency checks, meaning
# it will check whatever font files are already built. # it will check whatever font files are already built.
check: 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" @echo "check: all ok"
.PHONY: test check .PHONY: test check
@ -177,8 +182,6 @@ $(FONTDIR)/samples:
mkdir -p $@ mkdir -p $@
ZD = build/tmp/zip ZD = build/tmp/zip
# intermediate zip target that creates a zip file at build/tmp/a.zip # intermediate zip target that creates a zip file at build/tmp/a.zip
build/tmp/a.zip: build/tmp/a.zip:
@ -210,6 +213,14 @@ build/tmp/a.zip:
cp -a misc/dist/*.txt "$(ZD)/" cp -a misc/dist/*.txt "$(ZD)/"
cp -a LICENSE.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. @# Add "beta" to Light and Thin filenames.
@# Requires "rename" tool in PATH (`brew install rename` on macOS) @# Requires "rename" tool in PATH (`brew install rename` on macOS)
rename 's/(Light.*|Thin.*)\./$$1-BETA./' "$(ZD)/Inter"*/*.* rename 's/(Light.*|Thin.*)\./$$1-BETA./' "$(ZD)/Inter"*/*.*
@ -235,14 +246,14 @@ zip: all
$(MAKE) check $(MAKE) check
$(MAKE) build/release/Inter-${VERSION}-$(shell git rev-parse --short=10 HEAD).zip $(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) check
$(MAKE) ${ZIP_FILE_DIST} $(MAKE) ${ZIP_FILE_DIST}
.PHONY: zip zip_dist .PHONY: zip zip_dist
# distribution # distribution
pre_dist: pre_dist: all
@echo "Creating distribution for version ${VERSION}" @echo "Creating distribution for version ${VERSION}"
@if [ -f "${ZIP_FILE_DIST}" ]; \ @if [ -f "${ZIP_FILE_DIST}" ]; \
then echo "${ZIP_FILE_DIST} already exists. Bump version or remove the zip file to continue." >&2; \ 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 @echo fetch http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
@curl '-#' -o "$@" 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 targets
install_ttf: all_ttf_const install_ttf: all_ttf_const
$(MAKE) all_web -j
@echo "Installing TTF files locally at ~/Library/Fonts/Inter" @echo "Installing TTF files locally at ~/Library/Fonts/Inter"
rm -rf ~/'Library/Fonts/Inter' rm -rf ~/'Library/Fonts/Inter'
mkdir -p ~/'Library/Fonts/Inter' mkdir -p ~/'Library/Fonts/Inter'
cp -va $(FONTDIR)/const/*.ttf ~/'Library/Fonts/Inter' cp -va $(FONTDIR)/const/*.ttf ~/'Library/Fonts/Inter'
install_ttf_hinted: all_ttf install_ttf_hinted: all_ttf_hinted
$(MAKE) all_web -j
@echo "Installing autohinted TTF files locally at ~/Library/Fonts/Inter" @echo "Installing autohinted TTF files locally at ~/Library/Fonts/Inter"
rm -rf ~/'Library/Fonts/Inter' rm -rf ~/'Library/Fonts/Inter'
mkdir -p ~/'Library/Fonts/Inter' mkdir -p ~/'Library/Fonts/Inter'
cp -va $(FONTDIR)/const-hinted/*.ttf ~/'Library/Fonts/Inter' cp -va $(FONTDIR)/const-hinted/*.ttf ~/'Library/Fonts/Inter'
install_otf: all_otf install_otf: all_otf
$(MAKE) all_web -j
@echo "Installing OTF files locally at ~/Library/Fonts/Inter" @echo "Installing OTF files locally at ~/Library/Fonts/Inter"
rm -rf ~/'Library/Fonts/Inter' rm -rf ~/'Library/Fonts/Inter'
mkdir -p ~/'Library/Fonts/Inter' mkdir -p ~/'Library/Fonts/Inter'

312
misc/tools/fix-vf-meta.py Executable file
View 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()

View file

@ -8,3 +8,6 @@ fs==2.3.0
numpy==1.16.1 numpy==1.16.1
scipy==1.2.0 scipy==1.2.0
munkres==1.0.12 munkres==1.0.12
# for woff2
brotli==1.0.7