Remove slnt/ital VF axis
This removes the slant/italic variable axis and breaks up the font in two: roman and italic. This change will allow diverging designs for italic (for example single-storey a). It also addresses the fact that most software, including web browsers, doesn't handle VFs with slnt or ital well.
This commit is contained in:
parent
17875920ea
commit
3f174fcef6
7700 changed files with 955523 additions and 1364688 deletions
332
misc/tools/postprocess-vf2.py
Executable file
332
misc/tools/postprocess-vf2.py
Executable file
|
|
@ -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()
|
||||
Reference in a new issue