PK!$statmake/__init__.py__version__ = "0.1.1" PK!}__statmake/__main__.pyimport sys import statmake.cli if __name__ == "__main__": sys.exit(statmake.cli.main()) PK!+QQstatmake/classes.pyimport enum import functools import os from typing import Any, List, Mapping, Optional, Tuple, Union import attr import cattr import fontTools.misc.plistlib class AxisValueFlag(enum.Flag): OlderSiblingFontAttribute = 0x0001 ElidableAxisValueName = 0x0002 @attr.s(auto_attribs=True, frozen=True, slots=True) class FlagList: """Represent a list of AxisValueFlags so I can implement a value property.""" flags: List[AxisValueFlag] = attr.ib(factory=list) @property def value(self) -> int: """Return the value of all flags ORed together.""" if not self.flags: return 0 return functools.reduce(lambda x, y: x | y, self.flags).value @attr.s(auto_attribs=True, frozen=True, slots=True) class NameRecord: """Represent a IETF BCP 47 language code to name string mapping for the `name` table.""" mapping: Mapping[str, str] def __getitem__(self, key): return self.mapping.__getitem__(key) @property def default(self): return self.mapping["en"] @classmethod def from_string(cls, name: str): return cls(mapping={"en": name}) @classmethod def from_dict(cls, dictionary: Mapping): return cls(mapping=dictionary) @classmethod def structure(cls, data): if isinstance(data, str): return cls.from_string(data) if isinstance(data, dict): return cls.from_dict(data) raise ValueError(f"Don't know how to construct NameRecord from '{data}'.") @attr.s(auto_attribs=True, frozen=True, slots=True) class LocationFormat1: name: NameRecord value: float flags: FlagList = attr.ib(factory=FlagList) def fill_in_AxisValue(self, axis_value: Any, axis_index: int, name_id: int): """Fill in a supplied fontTools AxisValue object.""" axis_value.Format = 1 axis_value.AxisIndex = axis_index axis_value.ValueNameID = name_id axis_value.Value = self.value axis_value.Flags = self.flags.value return axis_value @attr.s(auto_attribs=True, frozen=True, slots=True) class LocationFormat2: name: NameRecord value: float range: Tuple[float, float] flags: FlagList = attr.ib(factory=FlagList) def __attrs_post_init__(self): if len(self.range) != 2: raise ValueError("Range must be a value pair of (min, max).") def fill_in_AxisValue(self, axis_value: Any, axis_index: int, name_id: int): """Fill in a supplied fontTools AxisValue object.""" axis_value.Format = 2 axis_value.AxisIndex = axis_index axis_value.ValueNameID = name_id axis_value.NominalValue = self.value axis_value.RangeMinValue, axis_value.RangeMaxValue = self.range axis_value.Flags = self.flags.value return axis_value @attr.s(auto_attribs=True, frozen=True, slots=True) class LocationFormat3: name: NameRecord value: float linked_value: float flags: FlagList = attr.ib(factory=FlagList) def fill_in_AxisValue(self, axis_value: Any, axis_index: int, name_id: int): """Fill in a supplied fontTools AxisValue object.""" axis_value.Format = 3 axis_value.AxisIndex = axis_index axis_value.ValueNameID = name_id axis_value.Value = self.value axis_value.LinkedValue = self.linked_value axis_value.Flags = self.flags.value return axis_value @attr.s(auto_attribs=True, frozen=True, slots=True) class LocationFormat4: name: NameRecord axis_values: Mapping[str, float] flags: FlagList = attr.ib(factory=FlagList) def fill_in_AxisValue( self, axis_value: Any, axis_name_to_index: Mapping[str, int], name_id: int, axis_value_record_type: Any, ): """Fill in a supplied fontTools AxisValue object.""" axis_value.Format = 4 axis_value.ValueNameID = name_id axis_value.Flags = self.flags.value axis_value.AxisValueRecord = [] for name, value in self.axis_values.items(): record = axis_value_record_type() record.AxisIndex = axis_name_to_index[name] record.Value = value axis_value.AxisValueRecord.append(record) return axis_value @attr.s(auto_attribs=True, frozen=True, slots=True) class Axis: name: NameRecord tag: str locations: List[Union[LocationFormat1, LocationFormat2, LocationFormat3]] = attr.ib( factory=list ) ordering: Optional[int] = None @attr.s(auto_attribs=True, frozen=True, slots=True) class Stylespace: axes: List[Axis] locations: List[LocationFormat4] = attr.ib(factory=list) elided_fallback_name_id: int = 2 def __attrs_post_init__(self): """Fill in a default ordering unless the user specified at least one custom one. This works around the frozen state with `object.__setattr__`. """ if all(axis.ordering is None for axis in self.axes): for index, axis in enumerate(self.axes): object.__setattr__(axis, "ordering", index) elif not all( isinstance(axis.ordering, int) and axis.ordering >= 0 for axis in self.axes ): raise ValueError( "If you specify the ordering for one axis, you must specify all of " "them and they must be >= 0." ) @classmethod def from_bytes(cls, stylespace_content: bytes): stylespace_content_parsed = fontTools.misc.plistlib.loads(stylespace_content) converter = cattr.Converter() converter.register_structure_hook( FlagList, lambda list_of_str_flags, cls: cls( [getattr(AxisValueFlag, f) for f in list_of_str_flags] ), ) converter.register_structure_hook( NameRecord, lambda data, cls: cls.structure(data) ) stylespace = converter.structure(stylespace_content_parsed, cls) return stylespace @classmethod def from_file(cls, stylespace_path: os.PathLike): with open(stylespace_path, "rb") as fp: stylespace = cls.from_bytes(fp.read()) return stylespace PK!8statmake/cli.pyimport argparse import sys from pathlib import Path import fontTools.designspaceLib import fontTools.ttLib import statmake.classes import statmake.lib def main(args=None): if not args: args = sys.argv[1:] parser = argparse.ArgumentParser() parser.add_argument( "stylespace_file", type=Path, help="The path to the Stylespace file." ) parser.add_argument( "designspace_file", type=Path, help="The path to the Designspace file used to generate the variable font.", ) parser.add_argument( "variable_font", type=Path, help="The path to the variable font file." ) parsed_args = parser.parse_args(args) stylespace = statmake.classes.Stylespace.from_file(parsed_args.stylespace_file) designspace = fontTools.designspaceLib.DesignSpaceDocument.fromfile( parsed_args.designspace_file ) additional_locations = designspace.lib.get("org.statmake.additionalLocations", {}) font = fontTools.ttLib.TTFont(parsed_args.variable_font) statmake.lib.apply_stylespace_to_variable_font( stylespace, font, additional_locations ) font.save(parsed_args.variable_font) PK!G..statmake/lib.pyimport collections import copy from typing import Dict, Mapping, Optional, Set import fontTools.misc.py23 import fontTools.ttLib import fontTools.ttLib.tables.otTables as otTables import statmake.classes def apply_stylespace_to_variable_font( stylespace: statmake.classes.Stylespace, varfont: fontTools.ttLib.TTFont, additional_locations: Mapping[str, float], ): """Generate and apply a STAT table to a variable font. additional_locations: used in subset Designspaces to express where on which other axes not defined by an element the varfont stands. The primary use-case is defining a complete STAT table for variable fonts that do not include all axes of a family (either because they intentionally contain just a subset of axes or because the designs are incompatible). """ name_table, stat_table = generate_name_and_STAT_variable( stylespace, varfont, additional_locations ) varfont["name"] = name_table varfont["STAT"] = stat_table def generate_name_and_STAT_variable( stylespace: statmake.classes.Stylespace, varfont: fontTools.ttLib.TTFont, additional_locations: Mapping[str, float], ): """Generate a new name and STAT table ready for insertion.""" if "fvar" not in varfont: raise ValueError( "Need a variable font with the fvar table to determine which instances " "are present." ) stylespace_name_to_axis = {a.name.default: a for a in stylespace.axes} fvar_name_to_axis = {} name_to_tag: Dict[str, str] = {} name_to_index: Dict[str, int] = {} index = 0 for index, fvar_axis in enumerate(varfont["fvar"].axes): fvar_axis_name = _default_name_string(varfont, fvar_axis.axisNameID) try: stylespace_axis = stylespace_name_to_axis[fvar_axis_name] except KeyError: raise ValueError( f"No stylespace entry found for axis name '{fvar_axis_name}'." ) if fvar_axis.axisTag != stylespace_axis.tag: raise ValueError( f"fvar axis '{fvar_axis_name}' tag is '{fvar_axis.axisTag}', but " f"Stylespace tag is '{stylespace_axis.tag}'." ) fvar_name_to_axis[fvar_axis_name] = fvar_axis name_to_tag[fvar_axis_name] = fvar_axis.axisTag name_to_index[fvar_axis_name] = index for axis_name in additional_locations: try: stylespace_axis = stylespace_name_to_axis[axis_name] except KeyError: raise ValueError(f"No stylespace entry found for axis name '{axis_name}'.") name_to_tag[stylespace_axis.name.default] = stylespace_axis.tag index += 1 name_to_index[stylespace_axis.name.default] = index # First, determine which stops are used on which axes. The STAT table must contain # a name for each stop that is used on each axis, so each stop must have an entry # in the Stylespace. Also include locations in additional_locations that can refer # to axes not present in the current varfont. stylespace_stops: Dict[str, Set[float]] = {} for axis in stylespace.axes: stylespace_stops[axis.tag] = {l.value for l in axis.locations} for named_location in stylespace.locations: for name, value in named_location.axis_values.items(): stylespace_stops[name_to_tag[name]].add(value) axis_stops: Mapping[str, Set[float]] = collections.defaultdict(set) # tag to stops for instance in varfont["fvar"].instances: for k, v in instance.coordinates.items(): if v not in stylespace_stops[k]: raise ValueError( f"There is no Stylespace entry for stop {v} on axis {k}." ) axis_stops[k].add(v) for k, v in additional_locations.items(): axis_tag = name_to_tag[k] if v not in stylespace_stops[axis_tag]: raise ValueError( f"There is no Stylespace entry for stop {v} on axis {k} (from " "additional locations)." ) axis_stops[axis_tag].add(v) # Construct temporary name and STAT tables for returning at the end. name_table = copy.deepcopy(varfont["name"]) stat_table = _new_empty_STAT_table() # Generate axis records. Reuse an axis' name ID if it exists, else make a new one. for axis_name, axis_tag in name_to_tag.items(): stylespace_axis = stylespace_name_to_axis[axis_name] if axis_name in fvar_name_to_axis: axis_name_id = fvar_name_to_axis[axis_name].axisNameID else: axis_name_id = name_table.addMultilingualName( stylespace_axis.name.mapping, mac=False ) axis_record = _new_axis_record( tag=axis_tag, name_id=axis_name_id, ordering=stylespace_axis.ordering ) stat_table.table.DesignAxisRecord.Axis.append(axis_record) # Generate formats 1, 2 and 3. for axis in stylespace.axes: for location in axis.locations: if location.value not in axis_stops[axis.tag]: continue axis_value = otTables.AxisValue() name_id = name_table.addMultilingualName(location.name.mapping, mac=False) location.fill_in_AxisValue( axis_value, axis_index=name_to_index[axis.name.default], name_id=name_id ) stat_table.table.AxisValueArray.AxisValue.append(axis_value) # Generate format 4. for named_location in stylespace.locations: if all( name_to_tag[k] in axis_stops and v in axis_stops[name_to_tag[k]] for k, v in named_location.axis_values.items() ): stat_table.table.Version = 0x00010002 axis_value = otTables.AxisValue() name_id = name_table.addMultilingualName( named_location.name.mapping, mac=False ) named_location.fill_in_AxisValue( axis_value, axis_name_to_index=name_to_index, name_id=name_id, axis_value_record_type=otTables.AxisValueRecord, ) stat_table.table.AxisValueArray.AxisValue.append(axis_value) stat_table.table.ElidedFallbackNameID = stylespace.elided_fallback_name_id return name_table, stat_table def _default_name_string(otfont: fontTools.ttLib.TTFont, name_id: int) -> str: """Return first name table match for name_id for language 'en'.""" name = otfont["name"].getName(name_id, 3, 1, 0x0409).toUnicode() if name is not None: return name raise ValueError(f"No default Windows record for id {name_id}.") def _new_empty_STAT_table(): stat_table = fontTools.ttLib.newTable("STAT") stat_table.table = otTables.STAT() stat_table.table.Version = 0x00010001 stat_table.table.DesignAxisRecord = otTables.AxisRecordArray() stat_table.table.DesignAxisRecord.Axis = [] stat_table.table.AxisValueArray = otTables.AxisValueArray() stat_table.table.AxisValueArray.AxisValue = [] return stat_table def _new_axis_record(tag: str, name_id: int, ordering: Optional[int]): if ordering is None: raise ValueError("ordering must be an integer.") axis_record = otTables.AxisRecord() axis_record.AxisTag = fontTools.misc.py23.Tag(tag) axis_record.AxisNameID = name_id axis_record.AxisOrdering = ordering return axis_record PK!H >).)statmake-0.1.1.dist-info/entry_points.txtN+I/N.,()*.I,MN1s2r3PK!s,, statmake-0.1.1.dist-info/LICENSEMIT License Copyright (c) 2019 Dalton Maag Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUstatmake-0.1.1.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H7)9!statmake-0.1.1.dist-info/METADATATko8_@$MqzbCQDHbw(4ZQ{I/u$,kyky~o,&؝u-mET R:7@ʭIwFL`}/ˊ)7ڋSR7. v)RS2Yyk)ѾKv-֗YݙJnzŪbr-Uj<䊮VYvï軷SjpRtN劑j☾g55*mjMae]+]ХE ɻ7lp>KOc=h,?8'pAMoL徵S1}w4Bu?8˘1NycJ]jUl5|Dp5wY(zqkQ3ΕDp{7&.Ȓ5[~M 4TU(ZL,FJv-9ރ%jA4A"~$]_;hEA7V%>|Y߳%LK1 BH[yY+ =\֪͙+7 wh~)iIj|A kY7gf9uhB)Rm*Bx8Z g6">EVF&5LkUޣ0"ű#ZwA°\X}ܰۛ׍Uف/{y 3gpJ#F{e>lXMʼnC:vZMΛgoKAG;EJ+?v+Dv4/Sc-xIc~PK!H$ostatmake-0.1.1.dist-info/RECORDu9@| 0(D.0@[fT?W7:/F8M\eQ[hO:ŀ~L8*.>&rJݨÂ3r_F3x^_E51lP8OgZ&{նfƝUQh"uP$Z('X(N;u2bY0ewYIծ@p +;@h/tK2#LU (J^N 7_-L Р@Tao gxיs").);statmake-0.1.1.dist-info/entry_points.txtPK!s,, ;statmake-0.1.1.dist-info/LICENSEPK!HڽTUZ@statmake-0.1.1.dist-info/WHEELPK!H7)9!@statmake-0.1.1.dist-info/METADATAPK!H$obDstatmake-0.1.1.dist-info/RECORDPK zF