123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- from copy import deepcopy
- from fontTools.ttLib import TTFont
- from fontTools.misc.testTools import getXML
- from fontTools.otlLib.builder import buildStatTable
- from fontTools.varLib.instancer.names import _updateUniqueIdNameRecord, NameID
- from fontTools.ttLib.tables._f_v_a_r import NamedInstance
- from pkg_resources import resource_filename
- from google.protobuf import text_format
- from collections import OrderedDict
- from axisregistry.axes_pb2 import AxisProto
- from collections import defaultdict
- from itertools import chain
- import logging
- from glob import glob
- import os
- try:
- from ._version import version as __version__
- except ImportError:
- __version__ = "0.0.0+unknown"
- log = logging.getLogger(__file__)
- LINKED_VALUES = {
- "wght": {400.0: 700.0},
- "ital": {0.0: 1.0},
- }
- GF_STATIC_STYLES = OrderedDict(
- [
- ("Thin", 100),
- ("ExtraLight", 200),
- ("Light", 300),
- ("Regular", 400),
- ("Medium", 500),
- ("SemiBold", 600),
- ("Bold", 700),
- ("ExtraBold", 800),
- ("Black", 900),
- ("Thin Italic", 100),
- ("ExtraLight Italic", 200),
- ("Light Italic", 300),
- ("Italic", 400),
- ("Medium Italic", 500),
- ("SemiBold Italic", 600),
- ("Bold Italic", 700),
- ("ExtraBold Italic", 800),
- ("Black Italic", 900),
- ]
- )
- def load_protobuf(klass, path):
- message = klass()
- with open(path, "rb") as text_data:
- text_format.Merge(text_data.read(), message)
- return message
- class AxisRegistry:
- def __init__(self, fp=resource_filename("axisregistry", "data")):
- axis_fps = [fp for fp in glob(os.path.join(fp, "*.textproto"))]
- self._data = {}
- for fp in axis_fps:
- axis = load_protobuf(AxisProto, fp)
- self._data[axis.tag] = axis
- def __getitem__(self, k):
- return self._data[k]
- def __iter__(self):
- for i in self._data.keys():
- yield i
- def keys(self):
- return self._data.keys()
- def items(self):
- return self._data.items()
- def get_fallback(self, name):
- for a in self:
- for fallback in self[a].fallback:
- if name == fallback.name:
- return a, fallback
- return None, None
- def fallbacks_in_fvar(self, ttFont):
- res = defaultdict(list)
- axes_in_font = {
- a.axisTag: {"min": a.minValue, "max": a.maxValue}
- for a in ttFont["fvar"].axes
- }
- for axis in axes_in_font:
- if axis not in self.keys():
- log.warn(f"Axis {axis} not found in GF Axis Registry!")
- continue
- for fallback in self[axis].fallback:
- if (
- fallback.value < axes_in_font[axis]["min"]
- or fallback.value > axes_in_font[axis]["max"]
- ):
- continue
- res[axis].append(fallback)
- return res
- def fallbacks_in_name_table(self, ttFont):
- res = []
- name_table = ttFont["name"]
- tokens = (
- name_table.getBestFamilyName().split()[1:]
- + name_table.getBestSubFamilyName().split()
- )
- fvar_axes_in_font = [a.axisTag for a in ttFont["fvar"].axes]
- for token in tokens:
- axis, fallback = axis_registry.get_fallback(token)
- if any([not axis, axis in fvar_axes_in_font, fallback in res]):
- continue
- res.append((axis, fallback))
- return res
- def fallback_for_value(self, axis_tag, value):
- if axis_tag in axis_registry:
- return next(
- (f for f in axis_registry[axis_tag].fallback if f.value == value),
- None,
- )
- return None
- axis_registry = AxisRegistry()
- AXIS_ORDER = sorted([i for i in axis_registry if i.isupper()]) + [
- "opsz",
- "wdth",
- "wght",
- "ital",
- "slnt",
- ]
- def is_variable(ttFont):
- return "fvar" in ttFont
- def _fvar_dflts(ttFont):
- res = OrderedDict()
- for a in ttFont["fvar"].axes:
- fallback = axis_registry.fallback_for_value(a.axisTag, a.defaultValue)
- if fallback:
- name = fallback.name
- elided = fallback.value == axis_registry[
- a.axisTag
- ].default_value and name not in ["Regular", "Italic"]
- else:
- name = None
- elided = True
- res[a.axisTag] = {"value": a.defaultValue, "name": name, "elided": elided}
- return res
- def build_stat(ttFont, sibling_ttFonts=[]):
- log.info("Building STAT table")
- assert is_variable(ttFont), "not a VF!"
- fallbacks_in_fvar = axis_registry.fallbacks_in_fvar(ttFont)
- fallbacks_in_siblings = list(
- chain.from_iterable(
- axis_registry.fallbacks_in_name_table(f) for f in sibling_ttFonts
- )
- )
- fallbacks_in_names = axis_registry.fallbacks_in_name_table(ttFont)
- nametable = ttFont["name"]
- fvar = ttFont["fvar"]
-
- fvar_nameids = set(i.subfamilyNameID for i in fvar.instances)
- if "STAT" in ttFont:
- stat = ttFont["STAT"]
- if stat.table.AxisValueCount > 0:
- axis_values = stat.table.AxisValueArray.AxisValue
- for ax in axis_values:
- if ax.ValueNameID not in fvar_nameids:
- nametable.removeNames(nameID=ax.ValueNameID)
- if stat.table.DesignAxisCount > 0:
- axes = stat.table.DesignAxisRecord.Axis
- for ax in axes:
- if ax.AxisNameID not in fvar_nameids:
- nametable.removeNames(nameID=ax.AxisNameID)
- del ttFont["STAT"]
- res = []
-
-
- seen_axes = set()
- for axis, fallbacks in fallbacks_in_fvar.items():
- seen_axes.add(axis)
- a = {"tag": axis, "name": axis_registry[axis].display_name, "values": []}
- for fallback in fallbacks:
- a["values"].append(
- {
- "name": fallback.name,
- "value": fallback.value,
-
- "flags": 0x2
- if fallback.value == axis_registry[axis].default_value
- else 0x0,
- }
- )
- if axis in LINKED_VALUES and fallback.value in LINKED_VALUES[axis]:
- a["values"][-1]["linkedValue"] = LINKED_VALUES[axis][fallback.value]
- res.append(a)
- for axis, fallback in fallbacks_in_names:
- if axis in seen_axes:
- continue
- a = {
- "tag": axis,
- "name": axis_registry[axis].display_name,
- "values": [{"name": fallback.name, "value": fallback.value, "flags": 0x0}],
- }
- if axis in LINKED_VALUES and fallback.value in LINKED_VALUES[axis]:
- a["values"][0]["linkedValue"] = LINKED_VALUES[axis][fallback.value]
- res.append(a)
- for axis, fallback in fallbacks_in_siblings:
- if axis in seen_axes:
- continue
- value = 0.0
- a = {
- "tag": axis,
- "name": axis_registry[axis].display_name,
- "values": [{"name": "Normal", "value": value, "flags": 0x2}],
- }
- if axis in LINKED_VALUES and value in LINKED_VALUES[axis]:
- a["values"][0]["linkedValue"] = LINKED_VALUES[axis][value]
- res.append(a)
- buildStatTable(ttFont, res, macNames=False)
- def build_name_table(ttFont, family_name=None, style_name=None, siblings=[]):
- log.info("Building name table")
- name_table = ttFont["name"]
- family_name = family_name if family_name else name_table.getBestFamilyName()
- style_name = style_name if style_name else name_table.getBestSubFamilyName()
- if is_variable(ttFont):
- return build_vf_name_table(ttFont, family_name, siblings=siblings)
- return build_static_name_table_v1(ttFont, family_name, style_name)
- def _fvar_instance_collisions(ttFont, siblings=[]):
- """Check if a font family is going to have colliding fvar instances.
- Collision occur when a family has has 2+ roman styles or 2+ italic
- styles."""
- def is_italic(font):
- return font["post"].italicAngle != 0.0
- family_styles = [is_italic(f) for f in siblings + [ttFont]]
- return len(family_styles) != len(set(family_styles))
- def build_vf_name_table(ttFont, family_name, siblings=[]):
-
- assert is_variable(ttFont), "Not a VF!"
- style_name = _vf_style_name(ttFont, family_name)
- if _fvar_instance_collisions(ttFont, siblings):
- build_static_name_table_v1(ttFont, family_name, style_name)
- else:
- build_static_name_table(ttFont, family_name, style_name)
- build_variations_ps_name(ttFont, family_name)
- def build_variations_ps_name(ttFont, family_name=None):
- assert is_variable(ttFont), "Not a VF!"
- if not family_name:
- family_name = ttFont["name"].getBestFamilyName()
- font_styles = axis_registry.fallbacks_in_name_table(ttFont)
- if font_styles:
- vf_ps = family_name.replace(" ", "") + "".join(
- [
- fallback.name
- for _, fallback in font_styles
- if fallback.name not in family_name
- ]
- )
- else:
- vf_ps = family_name.replace(" ", "")
- ttFont["name"].setName(vf_ps, NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX, 3, 1, 0x409)
- def _vf_style_name(ttFont, family_name):
- fvar_dflts = _fvar_dflts(ttFont)
- res = []
- for axis_name in AXIS_ORDER:
- if axis_name not in fvar_dflts:
- continue
- value = fvar_dflts[axis_name]
- if not value["elided"]:
- res.append(value["name"])
- family_name_tokens = family_name.split()
- font_styles = axis_registry.fallbacks_in_name_table(ttFont)
- for _, fallback in font_styles:
- if fallback.name not in res and fallback.name not in family_name_tokens:
- res.append(fallback.name)
- name = " ".join(res).replace("Regular Italic", "Italic")
- log.debug(f"Built VF style name: '{name}'")
- return name
- def build_fvar_instances(ttFont, axis_dflts={}):
- """Replace a variable font's fvar instances with a set of new instances
- which conform to the Google Fonts instance spec:
- https://github.com/googlefonts/gf-docs/tree/main/Spec#fvar-instances
- """
- assert is_variable(ttFont), "Not a VF!"
- log.info("Building fvar instances")
- fvar = ttFont["fvar"]
- name_table = ttFont["name"]
- style_name = name_table.getBestSubFamilyName()
-
- stat_nameids = []
- if "STAT" in ttFont:
- if ttFont["STAT"].table.AxisValueCount > 0:
- stat_nameids = [
- av.ValueNameID for av in ttFont["STAT"].table.AxisValueArray.AxisValue
- ]
-
- for inst in fvar.instances:
- if inst.subfamilyNameID not in [2, 17] + stat_nameids:
- name_table.removeNames(nameID=inst.subfamilyNameID)
- if inst.postscriptNameID not in [65535, 6]:
- name_table.removeNames(nameID=inst.postscriptNameID)
- fvar_dflts = _fvar_dflts(ttFont)
- if not axis_dflts:
- axis_dflts = {k: v["value"] for k, v in fvar_dflts.items()}
- is_italic = "Italic" in style_name
- is_roman_and_italic = any(a for a in ("slnt", "ital") if a in fvar_dflts)
- fallbacks = axis_registry.fallbacks_in_fvar(ttFont)
-
-
-
- if "wght" not in fvar_dflts:
- fallback = next(
- (f for f in axis_registry["wght"].fallback if f.value == 400.0), None
- )
- fallbacks["wght"] = [fallback]
- wght_fallbacks = fallbacks["wght"]
- ital_axis = next((a for a in fvar.axes if a.axisTag == "ital"), None)
- slnt_axis = next((a for a in fvar.axes if a.axisTag == "slnt"), None)
- def gen_instances(is_italic):
- results = []
- for fallback in wght_fallbacks:
- name = fallback.name if not is_italic else f"{fallback.name} Italic".strip()
- name = name.replace("Regular Italic", "Italic")
- coordinates = {k: v for k, v in axis_dflts.items()}
- if "wght" in fvar_dflts:
- coordinates["wght"] = fallback.value
- if is_italic:
- if ital_axis:
- coordinates["ital"] = ital_axis.minValue
- elif slnt_axis:
- coordinates["slnt"] = slnt_axis.minValue
- inst = NamedInstance()
- inst.subfamilyNameID = name_table.addName(name)
- inst.coordinates = coordinates
- log.debug(f"Adding fvar instance: {name}: {coordinates}")
- results.append(inst)
- return results
- instances = []
- if is_roman_and_italic:
- for bool_ in (False, True):
- instances += gen_instances(is_italic=bool_)
- elif is_italic:
- instances += gen_instances(is_italic=True)
- else:
- instances += gen_instances(is_italic=False)
- fvar.instances = instances
- def build_static_name_table(ttFont, family_name, style_name):
-
- name_table = ttFont["name"]
- name_table.removeNames(platformID=1)
- existing_name = ttFont["name"].getBestFamilyName()
- names = {}
- is_ribbi = (
- True if style_name in ("Regular", "Italic", "Bold", "Bold Italic") else False
- )
- if is_ribbi:
- full_name = f"{family_name} {style_name}"
- ps_name = f"{family_name}-{style_name}".replace(" ", "")
- names[(NameID.FAMILY_NAME, 3, 1, 0x409)] = family_name
- names[(NameID.SUBFAMILY_NAME, 3, 1, 0x409)] = style_name
- names[(NameID.FULL_FONT_NAME, 3, 1, 0x409)] = full_name
- names[(NameID.POSTSCRIPT_NAME, 3, 1, 0x409)] = ps_name
- for name_id in (
- NameID.TYPOGRAPHIC_FAMILY_NAME,
- NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
- 21,
- 22,
- ):
- name_table.removeNames(nameID=name_id)
- else:
- style_tokens = style_name.split()
- new_family_name = family_name.split()
- is_italic = "Italic" in style_tokens
- for t in style_tokens:
- if t in ["Regular", "Italic"] or t in new_family_name:
- continue
- new_family_name.append(t)
- new_family_name = " ".join(new_family_name)
- new_style_name = "Italic" if is_italic else "Regular"
- full_name = f"{family_name} {style_name}"
- ps_name = f"{family_name}-{style_name}".replace(" ", "")
- names[(NameID.FAMILY_NAME, 3, 1, 0x409)] = new_family_name
- names[(NameID.SUBFAMILY_NAME, 3, 1, 0x409)] = new_style_name
- names[(NameID.FULL_FONT_NAME, 3, 1, 0x409)] = full_name
- names[(NameID.POSTSCRIPT_NAME, 3, 1, 0x409)] = ps_name
- names[(NameID.TYPOGRAPHIC_FAMILY_NAME, 3, 1, 0x409)] = family_name
- names[(NameID.TYPOGRAPHIC_SUBFAMILY_NAME, 3, 1, 0x409)] = style_name
-
- for name_id in (21, 22):
- name_table.removeNames(nameID=name_id)
- names[(NameID.UNIQUE_FONT_IDENTIFIER, 3, 1, 0x409)] = _updateUniqueIdNameRecord(
- ttFont, {k[0]: v for k, v in names.items()}, (3, 1, 0x409)
- )
- for k, v in names.items():
- log.debug(f"Adding name record {k}: {v}")
- name_table.setName(v, *k)
-
- skip_ids = [i.numerator for i in NameID]
- for r in ttFont["name"].names:
- if r.nameID in skip_ids:
- continue
- current = r.toUnicode()
- if existing_name not in current:
- continue
- if " " not in current:
- replacement = current.replace(existing_name, family_name).replace(" ", "")
- else:
- replacement = current.replace(existing_name, family_name)
- ttFont["name"].setName(
- replacement, r.nameID, r.platformID, r.platEncID, r.langID
- )
- def build_static_name_table_v1(ttFont, family_name, style_name):
- """Pre VF name tables, this version can only accept wght + ital"""
- non_weight_tokens = []
- v1_tokens = []
- tokens = style_name.split()
- for t in tokens:
- if t not in GF_STATIC_STYLES:
- non_weight_tokens.append(t)
- else:
- v1_tokens.append(t)
- family_tokens = family_name.split()
- new_family_name = []
- for t in family_tokens:
- if t in non_weight_tokens or t in new_family_name:
- continue
- new_family_name.append(t)
- for t in non_weight_tokens:
- new_family_name.append(t)
- family_name = " ".join(new_family_name)
- style_name = " ".join(v1_tokens).replace("Regular Italic", "Italic").strip()
- style_name = style_name or "Regular"
- log.debug(f"New family name: {family_name}")
- log.debug(f"New style name: {style_name}")
- build_static_name_table(ttFont, family_name, style_name)
- def build_filename(ttFont):
- name_table = ttFont["name"]
- family_name = name_table.getBestFamilyName()
- style_name = name_table.getBestSubFamilyName()
- _, ext = os.path.splitext(ttFont.reader.file.name)
- if is_variable(ttFont):
- is_italic = "Italic" in style_name
- axes = _fvar_dflts(ttFont).keys()
- axes = sorted([a for a in axes if a.isupper()]) + sorted(
- [a for a in axes if a.islower()]
- )
- if is_italic:
- return f"{family_name}-Italic[{','.join(axes)}]{ext}".replace(" ", "")
- return f"{family_name}[{','.join(axes)}]{ext}".replace(" ", "")
- return f"{family_name}-{style_name}{ext}".replace(" ", "")
- def dump(table, ttFont=None):
- return "\n".join(getXML(table.toXML, ttFont))
|