summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2023-06-04 22:30:42 +0300
committerRasmus Andersson <rasmus@notion.se>2023-06-04 22:30:42 +0300
commit48f11a2f15bac1db033ed1008827fa5905071c8d (patch)
tree660adc01ea5e819dc2867fc6c3ce1cc3ee60dd60
parentd35aa9f085511f2fc0c678c428357f166e0b7c9e (diff)
downloadinter-48f11a2f15bac1db033ed1008827fa5905071c8d.tar.xz
build STAT table (version 4) manually for variable fonts, re #577
-rw-r--r--Makefile28
-rw-r--r--misc/tools/bake-vf.py288
2 files changed, 295 insertions, 21 deletions
diff --git a/Makefile b/Makefile
index 33e4fc141..26021b640 100644
--- a/Makefile
+++ b/Makefile
@@ -190,6 +190,13 @@ $(FONTDIR)/var/.%.var.otf: $(UFODIR)/%.var.designspace build/features_data | $(F
. $(VENV) ; misc/tools/woff2 compress -o "$@" "$<"
+$(FONTDIR)/var/Inter-Variable.ttf: $(FONTDIR)/var/.Inter-Roman.var.ttf
+ . $(VENV) ; python misc/tools/bake-vf.py $^ -o $@
+
+$(FONTDIR)/var/Inter-Variable-Italic.ttf: $(FONTDIR)/var/.Inter-Italic.var.ttf
+ . $(VENV) ; python misc/tools/bake-vf.py $^ -o $@
+
+
$(FONTDIR)/static:
mkdir -p $@
$(FONTDIR)/static-hinted:
@@ -199,27 +206,6 @@ $(FONTDIR)/var:
$(UFODIR):
mkdir -p $@
-# Inter Variable
-$(FONTDIR)/var/.Inter-Variable.stamp: \
- $(FONTDIR)/var/.Inter-Roman.var.ttf \
- $(FONTDIR)/var/.Inter-Italic.var.ttf \
- | venv
- rm -rf $(FONTDIR)/var/gen-stat
- mkdir $(FONTDIR)/var/gen-stat
- . $(VENV) ; gftools gen-stat --out $(FONTDIR)/var/gen-stat $^
- . $(VENV) ; python misc/tools/rename.py --scrub-display --family "Inter Variable" \
- -o $(FONTDIR)/var/Inter-Variable.ttf \
- $(FONTDIR)/var/gen-stat/.Inter-Roman.var.ttf
- . $(VENV) ; python misc/tools/rename.py --scrub-display --family "Inter Variable" \
- -o $(FONTDIR)/var/Inter-Variable-Italic.ttf \
- $(FONTDIR)/var/gen-stat/.Inter-Italic.var.ttf
- rm -rf $(FONTDIR)/var/gen-stat
- touch $@
-
-$(FONTDIR)/var/Inter-Variable.ttf: $(FONTDIR)/var/.Inter-Variable.stamp
- touch $@
-$(FONTDIR)/var/Inter-Variable-Italic.ttf: $(FONTDIR)/var/.Inter-Variable.stamp
- touch $@
var: \
$(FONTDIR)/var/Inter-Variable.ttf \
diff --git a/misc/tools/bake-vf.py b/misc/tools/bake-vf.py
new file mode 100644
index 000000000..dab74a5d8
--- /dev/null
+++ b/misc/tools/bake-vf.py
@@ -0,0 +1,288 @@
+"""
+This script "bakes" the final Inter variable fonts.
+
+This script performs the following:
+ 1. Renames the family to "Inter Variable"
+ 2. Updates style names to scrub away "Display"
+ 3. Builds a STAT table
+
+How to debug/develop this script:
+
+1. create a working dir and build the initial fonts:
+
+ mkdir -p build/bake
+ make -j var
+
+2. after making changes, run script and inspect with ttx:
+
+ ( for a in build/fonts/var/.Inter-*.var.ttf; do
+ python misc/tools/bake-vf.py "$a" -o build/bake/"$(basename "${a/.Inter/Inter}")"
+ done && ttx -t STAT -i -f -s build/bake/Inter-*.ttf )
+
+"""
+import sys, os, os.path, re, argparse
+from fontTools.ttLib import TTFont
+from fontTools.otlLib.builder import buildStatTable
+
+
+STAT_AXES = [
+ { "name": "Optical Size", "tag": "opsz" },
+ { "name": "Weight", "tag": "wght" },
+ { "name": "Italic", "tag": "ital" }
+]
+
+
+def stat_locations(ital):
+ return [
+ { "name": "Thin", "location": { "wght": 100, "ital": ital } },
+ { "name": "ExtraLight", "location": { "wght": 200, "ital": ital } },
+ { "name": "Light", "location": { "wght": 300, "ital": ital } },
+ { "name": "Regular", "location": { "wght": 400, "ital": ital }, "flags": 0x2 },
+ { "name": "Medium", "location": { "wght": 500, "ital": ital } },
+ { "name": "SemiBold", "location": { "wght": 600, "ital": ital } },
+ { "name": "Bold", "location": { "wght": 700, "ital": ital } },
+ { "name": "Heavy", "location": { "wght": 800, "ital": ital } },
+ { "name": "Black", "location": { "wght": 900, "ital": ital } },
+ ]
+ # "flags": 0x2
+ # Indicates that the axis value represents the “normal” value
+ # for the axis and may be omitted when composing name strings.
+
+
+WINDOWS_ENGLISH_IDS = 3, 1, 0x409
+MAC_ROMAN_IDS = 1, 0, 0
+
+LEGACY_FAMILY = 1
+SUBFAMILY_NAME = 2
+TRUETYPE_UNIQUE_ID = 3
+FULL_NAME = 4
+POSTSCRIPT_NAME = 6
+PREFERRED_FAMILY = 16
+TYPO_SUBFAMILY_NAME = 17
+WWS_FAMILY = 21
+VAR_PS_NAME_PREFIX = 25
+
+
+FAMILY_RELATED_IDS = set([
+ LEGACY_FAMILY,
+ TRUETYPE_UNIQUE_ID,
+ FULL_NAME,
+ POSTSCRIPT_NAME,
+ PREFERRED_FAMILY,
+ WWS_FAMILY,
+ VAR_PS_NAME_PREFIX,
+])
+
+WHITESPACE_RE = re.compile(r'\s+')
+
+
+def remove_whitespace(s):
+ return WHITESPACE_RE.sub('', s)
+
+
+def normalize_whitespace(s):
+ return WHITESPACE_RE.sub(' ', s)
+
+
+def remove_substring(s, substr):
+ # examples of remove_substring(s, "Display"):
+ # "Inter Display" => "Inter"
+ # "Display Lol" => "Lol"
+ # "Foo Display Lol" => "Foo Lol"
+ # " Foo Bar Lol " => "Foo Bar Lol"
+ return normalize_whitespace(s.strip().replace(substr, '')).strip()
+
+
+def font_is_italic(ttfont):
+ """Check if the font has the word "Italic" in its stylename"""
+ stylename = ttfont["name"].getName(2, 3, 1, 0x409).toUnicode()
+ return True if "Italic" in stylename else False
+
+
+def set_full_name(font, fullName, fullNamePs):
+ nameTable = font["name"]
+ nameTable.setName(fullName, FULL_NAME, 1, 0, 0) # mac
+ nameTable.setName(fullName, FULL_NAME, 3, 1, 0x409) # windows
+ nameTable.setName(fullNamePs, POSTSCRIPT_NAME, 1, 0, 0) # mac
+ nameTable.setName(fullNamePs, POSTSCRIPT_NAME, 3, 1, 0x409) # windows
+
+
+def getFamilyName(font):
+ nameTable = font["name"]
+ r = None
+ for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS):
+ for name_id in (PREFERRED_FAMILY, LEGACY_FAMILY):
+ r = nameTable.getName(nameID=name_id, platformID=plat_id, platEncID=enc_id, langID=lang_id)
+ if r is not None:
+ break
+ if r is not None:
+ break
+ if not r:
+ raise ValueError("family name not found")
+ return r.toUnicode()
+
+
+def getFamilyNames(font):
+ nameTable = font["name"]
+ r = None
+ names = dict() # dict in Py >=3.7 maintains insertion order
+ for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS):
+ for name_id in (PREFERRED_FAMILY, LEGACY_FAMILY):
+ r = nameTable.getName(
+ nameID=name_id, platformID=plat_id, platEncID=enc_id, langID=lang_id)
+ if r:
+ names[r.toUnicode()] = True
+ if len(names) == 0:
+ raise ValueError("family name not found")
+ names = list(names.keys())
+ names.sort()
+ names.reverse() # longest first
+ return names
+
+
+def getStyleName(font):
+ nameTable = font["name"]
+ for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS):
+ for name_id in (TYPO_SUBFAMILY_NAME, SUBFAMILY_NAME):
+ r = nameTable.getName(
+ nameID=name_id, platformID=plat_id, platEncID=enc_id, langID=lang_id)
+ if r is not None:
+ return r.toUnicode()
+ raise ValueError("style name not found")
+
+
+def setStyleName(font, newStyleName):
+ newFullName = getFamilyName(font).strip()
+ if newStyleName != 'Regular':
+ newFullName += " " + newStyleName
+ newFullNamePs = remove_whitespace(newFullName)
+ set_full_name(font, newFullName, newFullNamePs)
+
+ nameTable = font["name"]
+ for rec in nameTable.names:
+ rid = rec.nameID
+ if rid in (SUBFAMILY_NAME, TYPO_SUBFAMILY_NAME):
+ rec.string = newStyleName
+
+
+def setFamilyName(font, nextFamilyName):
+ prevFamilyNames = getFamilyNames(font)
+ # if prevFamilyNames[0] == nextFamilyName:
+ # return
+ # # raise Exception("identical family name")
+
+ def renameRecord(nameRecord, prevFamilyNames, nextFamilyName):
+ # replaces prevFamilyNames with nextFamilyName in nameRecord
+ s = nameRecord.toUnicode()
+ for prevFamilyName in prevFamilyNames:
+ start = s.find(prevFamilyName)
+ if start == -1:
+ continue
+ end = start + len(prevFamilyName)
+ nextFamilyName = s[:start] + nextFamilyName + s[end:]
+ nameRecord.string = nextFamilyName
+ break
+ return s, nextFamilyName
+
+ # postcript name can't contain spaces
+ psPrevFamilyNames = []
+ for s in prevFamilyNames:
+ s = s.strip()
+ if s.find(' ') == -1:
+ psPrevFamilyNames.append(s)
+ else:
+ # Foo Bar Baz -> FooBarBaz
+ psPrevFamilyNames.append(s.replace(" ", ""))
+ # # Foo Bar Baz -> FooBar-Baz
+ p = s.rfind(' ')
+ s = s[:p] + '-' + s[p+1:]
+ psPrevFamilyNames.append(s)
+
+ psNextFamilyName = nextFamilyName.replace(" ", "")
+ found_VAR_PS_NAME_PREFIX = False
+ nameTable = font["name"]
+
+ for rec in nameTable.names:
+ name_id = rec.nameID
+ if name_id not in FAMILY_RELATED_IDS:
+ # leave uninteresting records unmodified
+ continue
+ if name_id == POSTSCRIPT_NAME:
+ old, new = renameRecord(rec, psPrevFamilyNames, psNextFamilyName)
+ elif name_id == TRUETYPE_UNIQUE_ID:
+ # The Truetype Unique ID rec may contain either the PostScript Name
+ # or the Full Name
+ prev_psname = None
+ for s in psPrevFamilyNames:
+ if s in rec.toUnicode():
+ prev_psname = s
+ break
+ if prev_psname is not None:
+ # Note: This is flawed -- a font called "Foo" renamed to "Bar Lol";
+ # if this record is not a PS record, it will incorrectly be rename "BarLol".
+ # However, in practice this is not a big deal since it's just an ID.
+ old, new = renameRecord(rec, [prev_psname], psNextFamilyName)
+ else:
+ old, new = renameRecord(rec, prevFamilyNames, nextFamilyName)
+ elif name_id == VAR_PS_NAME_PREFIX:
+ # Variations PostScript Name Prefix.
+ # If present in a variable font, it may be used as the family prefix in the
+ # PostScript Name Generation for Variation Fonts algorithm.
+ # The character set is restricted to ASCII-range uppercase Latin letters,
+ # lowercase Latin letters, and digits.
+ found_VAR_PS_NAME_PREFIX = True
+ old, new = renameRecord(rec, prevFamilyNames, nextFamilyName)
+ else:
+ old, new = renameRecord(rec, prevFamilyNames, nextFamilyName)
+ # print(" %r: '%s' -> '%s'" % (rec, old, new))
+
+ # HACK! FIXME!
+ # add name ID 25 "Variations PostScript Name Prefix" if not found
+ if not found_VAR_PS_NAME_PREFIX and nextFamilyName.find('Variable') != -1:
+ varPSNamePrefix = remove_whitespace(nextFamilyName)
+ nameTable.setName(varPSNamePrefix, VAR_PS_NAME_PREFIX, 1, 0, 0) # mac
+ nameTable.setName(varPSNamePrefix, VAR_PS_NAME_PREFIX, 3, 1, 0x409) # windows
+
+
+def gen_stat(ttfont):
+ ital = 1 if font_is_italic(ttfont) else 0
+ locations = stat_locations(ital)
+ buildStatTable(ttfont, STAT_AXES, locations=locations)
+
+
+def main():
+ argparser = argparse.ArgumentParser(
+ description='Generate STAT table for variable font family')
+ a = lambda *args, **kwargs: argparser.add_argument(*args, **kwargs)
+ a('--family', metavar='<name>',
+ help='Rename family to <name> instead of "Inter Display"')
+ a('-o', '--output', metavar='<file>',
+ help='Output font file. Defaults to input file (overwrite)')
+ a('input', metavar='<file>', help='Input font file')
+
+ args = argparser.parse_args()
+
+ # load font
+ font = TTFont(args.input, recalcBBoxes=False, recalcTimestamp=False)
+
+ # set family name
+ if not args.family:
+ args.family = "Inter Display"
+ setFamilyName(font, args.family)
+
+ # set style name
+ stylename = remove_substring(getStyleName(font), "Display")
+ if stylename == '':
+ stylename = 'Regular'
+ setStyleName(font, stylename)
+
+ # build STAT table
+ gen_stat(font)
+
+ # save font
+ outfile = args.output or args.input
+ font.save(outfile)
+
+
+if __name__ == '__main__':
+ main()