summaryrefslogtreecommitdiff
path: root/misc/tools/postprocess-vf2.py
diff options
context:
space:
mode:
Diffstat (limited to 'misc/tools/postprocess-vf2.py')
-rwxr-xr-xmisc/tools/postprocess-vf2.py332
1 files changed, 332 insertions, 0 deletions
diff --git a/misc/tools/postprocess-vf2.py b/misc/tools/postprocess-vf2.py
new file mode 100755
index 000000000..4eb29f981
--- /dev/null
+++ b/misc/tools/postprocess-vf2.py
@@ -0,0 +1,332 @@
+#!/usr/bin/env python3
+#
+# from gftools
+# https://github.com/googlefonts/gftools/blob/
+# 8b53f595a08d1b3f86f86eb97e07b15b1f52f671/bin/gftools-fix-vf-meta.py
+#
+"""
+Add a STAT table to a weight only variable font.
+
+This script can also add STAT tables to a variable font family which
+consists of two fonts, one for Roman, the other for Italic.
+Both of these fonts must also only contain a weight axis.
+
+For variable fonts with multiple axes, write a python script which
+uses fontTools.otlLib.builder.buildStatTable e.g
+https://github.com/googlefonts/literata/blob/main/sources/gen_stat.py
+
+The generated STAT tables use format 2 Axis Values. These are needed in
+order for Indesign to work.
+
+Special mention to Thomas Linard for reviewing the output of this script.
+
+
+Usage:
+
+Single family:
+gftools fix-vf-meta FontFamily[wght].ttf
+
+Roman + Italic family:
+gftools fix-vf-meta FontFamily[wght].ttf FontFamily-Italic[wght].ttf
+"""
+from fontTools.otlLib.builder import buildStatTable
+from fontTools.ttLib import TTFont
+# from gftools.utils import font_is_italic
+import argparse, re
+
+
+whitespace_re = re.compile(r'\s+')
+
+
+def remove_whitespace(s):
+ return whitespace_re.sub("", s)
+
+
+def font_is_italic(ttfont):
+ if ttfont['head'].macStyle & 0b10:
+ return True
+ return False
+ # # 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 font_has_mac_names(ttfont):
+ for record in ttfont['name'].names:
+ if record.platformID == 1:
+ return True
+ return False
+
+
+def build_stat(roman_font, italic_font=None):
+ roman_wght_axis = dict(
+ tag="wght",
+ name="Weight",
+ values=build_wght_axis_values(roman_font),
+ )
+ roman_opsz_axis = dict(
+ tag="opsz",
+ name="Optical size",
+ values=build_opsz_axis_values(roman_font),
+ )
+ roman_axes = [roman_wght_axis, roman_opsz_axis]
+
+ if italic_font:
+ # We need to create a new Italic axis in the Roman font
+ roman_axes.append(dict(
+ tag="ital",
+ name="Italic",
+ values=[
+ dict(
+ name="Roman",
+ flags=2,
+ value=0.0,
+ linkedValue=1.0,
+ )
+ ]
+ ))
+ italic_wght_axis = dict(
+ tag="wght",
+ name="Weight",
+ values=build_wght_axis_values(italic_font),
+ )
+ italic_opsz_axis = dict(
+ tag="opsz",
+ name="Optical size",
+ values=build_opsz_axis_values(italic_font),
+ )
+ italic_ital_axis = dict(
+ tag="ital",
+ name="Italic",
+ values=[
+ dict(
+ name="Italic",
+ value=1.0,
+ )
+ ]
+ )
+ italic_axes = [italic_wght_axis, italic_opsz_axis, italic_ital_axis]
+ #print("buildStatTable(italic_font)", italic_axes)
+ buildStatTable(italic_font, italic_axes)
+ #print("buildStatTable(roman_font)", roman_axes)
+ buildStatTable(roman_font, roman_axes)
+
+
+def build_stat_v2(roman_font, italic_font=None):
+ roman_wght_axis = dict(
+ tag="wght",
+ name="Weight",
+ )
+ roman_opsz_axis = dict(
+ tag="opsz",
+ name="Optical size",
+ )
+ roman_axes = [roman_wght_axis, roman_opsz_axis]
+ locations = [
+ dict(name='Regular', location=dict(wght=400, opsz=14)),
+ dict(name='Regular Display', location=dict(wght=400, opsz=32)),
+ dict(name='Bold', location=dict(wght=700, opsz=14)),
+ dict(name='Bold Display', location=dict(wght=700, opsz=32)),
+ ]
+ buildStatTable(roman_font, roman_axes, locations)
+
+
+def build_opsz_axis_values(ttfont):
+ nametable = ttfont['name']
+ instances = ttfont['fvar'].instances
+
+ val_min = 0.0
+ val_max = 0.0
+ for instance in instances:
+ opsz_val = instance.coordinates["opsz"]
+ if val_min == 0.0 or opsz_val < val_min:
+ val_min = opsz_val
+ if val_max == 0.0 or opsz_val > val_max:
+ val_max = opsz_val
+
+ return [
+ {
+ "name": "Regular",
+ "value": val_min,
+ "linkedValue": val_max,
+ "flags": 2,
+ },
+ {
+ "name": "Display",
+ "value": val_max,
+ },
+ ]
+
+ # results = []
+ # for instance in instances:
+ # opsz_val = instance.coordinates["opsz"]
+ # name = nametable.getName(instance.subfamilyNameID, 3, 1, 1033).toUnicode()
+ # name = name.replace("Italic", "").strip()
+ # if name == "":
+ # name = "Regular"
+ # inst = {
+ # "name": name,
+ # "value": opsz_val,
+ # }
+ # if int(opsz_val) == val_min:
+ # inst["flags"] = 0
+ # inst["linkedValue"] = val_max
+ # else:
+ # inst["linkedValue"] = val_min
+ # results.append(inst)
+
+ # return results
+
+
+def build_wght_axis_values(ttfont):
+ results = []
+ nametable = ttfont['name']
+ instances = ttfont['fvar'].instances
+ has_bold = any([True for i in instances if i.coordinates['wght'] == 700])
+ for instance in instances:
+ wght_val = instance.coordinates["wght"]
+ name = nametable.getName(instance.subfamilyNameID, 3, 1, 1033).toUnicode()
+ #print(nametable.getName(instance.postscriptNameID, 3, 1, 1033).toUnicode())
+ name = name.replace("Italic", "").strip()
+ if name == "":
+ name = "Regular"
+ inst = {
+ "name": name,
+ "nominalValue": wght_val,
+ }
+ if inst["nominalValue"] == 400:
+ inst["flags"] = 0x2
+ results.append(inst)
+
+ # Dynamically generate rangeMinValues and rangeMaxValues
+ entries = [results[0]["nominalValue"]] + \
+ [i["nominalValue"] for i in results] + \
+ [results[-1]["nominalValue"]]
+ for i, entry in enumerate(results):
+ entry["rangeMinValue"] = (entries[i] + entries[i+1]) / 2
+ entry["rangeMaxValue"] = (entries[i+1] + entries[i+2]) / 2
+
+ # Format 2 doesn't support linkedValues so we have to append another
+ # Axis Value (format 3) for Reg which does support linkedValues
+ if has_bold:
+ inst = {
+ "name": "Regular",
+ "value": 400,
+ "flags": 0x2,
+ "linkedValue": 700
+ }
+ results.append(inst)
+ return results
+
+
+def update_nametable(ttfont):
+ """
+ - Add nameID 25
+ - Update fvar instance names and add fvar instance postscript names
+ """
+ is_italic = font_is_italic(ttfont)
+ has_mac_names = font_has_mac_names(ttfont)
+
+ # Add nameID 25
+ # https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
+ vf_ps_name = _add_nameid_25(ttfont, is_italic, has_mac_names)
+
+ nametable = ttfont['name']
+ instances = ttfont["fvar"].instances
+
+ print("%d instances of %s:" % (len(instances), "Italic" if is_italic else "Roman"))
+
+ # find opsz max
+ opsz_val_max = 0.0
+ for instance in instances:
+ opsz_val = instance.coordinates["opsz"]
+ if opsz_val_max == 0.0 or opsz_val > opsz_val_max:
+ opsz_val_max = opsz_val
+
+ # Update fvar instances
+ i = 0
+ for inst in instances:
+ inst_name = nametable.getName(inst.subfamilyNameID, 3, 1, 1033).toUnicode()
+ print("instance %2d" % i, inst_name)
+ i += 1
+
+ # Update instance subfamilyNameID
+ if is_italic:
+ inst_name = inst_name.strip()
+ inst_name = inst_name.replace("Regular Italic", "Italic")
+ inst_name = inst_name.replace("Italic", "").strip()
+ inst_name = inst_name + " Italic"
+ ttfont['name'].setName(inst_name, inst.subfamilyNameID, 3, 1, 0x409)
+ if has_mac_names:
+ ttfont['name'].setName(inst_name, inst.subfamilyNameID, 1, 0, 0)
+
+ # Add instance psName
+ ps_name = vf_ps_name + remove_whitespace(inst_name)
+ ps_name_id = ttfont['name'].addName(ps_name)
+ inst.postscriptNameID = ps_name_id
+
+
+def _add_nameid_25(ttfont, is_italic, has_mac_names):
+ name = ttfont['name'].getName(16, 3, 1, 1033) or \
+ ttfont['name'].getName(1, 3, 1, 1033).toUnicode()
+ # if is_italic:
+ # name = f"{name}Italic"
+ # else:
+ # name = f"{name}Roman"
+ # ttfont['name'].setName(name, 25, 3, 1, 1033)
+ if is_italic:
+ name = f"{name}Italic"
+ ttfont['name'].setName(name, 25, 3, 1, 1033)
+ if has_mac_names:
+ ttfont['name'].setName(name, 25, 1, 0, 0)
+ return name
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=__doc__
+ )
+ parser.add_argument("fonts", nargs="+", help=(
+ "Paths to font files. Fonts must be part of the same family."
+ )
+ )
+ args = parser.parse_args()
+ paths = args.fonts
+
+ # This monstrosity exists so we don't break the v1 api.
+ italic_font = None
+ if len(paths) > 2:
+ raise Exception(
+ "Can only add STAT tables to a max of two fonts. "
+ "Run gftools fix-vf-meta --help for usage instructions"
+ )
+ elif len(paths) == 2:
+ if "Italic" in paths[0]:
+ tmp = paths[0]
+ paths[0] = paths[1]
+ paths[1] = tmp
+ elif "Italic" not in paths[1]:
+ raise Exception("No Italic font found!")
+ roman_font = TTFont(paths[0])
+ italic_font = TTFont(paths[1])
+ else:
+ roman_font = TTFont(paths[0])
+
+ update_nametable(roman_font)
+ if italic_font:
+ update_nametable(italic_font)
+
+ build_stat(roman_font, italic_font)
+
+ roman_font.save(paths[0] + "-fixed.ttf")
+ if italic_font:
+ italic_font.save(paths[1] + "-fixed.ttf")
+
+ # roman_font.save(paths[0])
+ # if italic_font:
+ # italic_font.save(paths[1])
+
+
+if __name__ == "__main__":
+ main()