Skip to content

Instantly share code, notes, and snippets.

@moi15moi
Last active April 5, 2023 00:40
Show Gist options
  • Save moi15moi/3f7704696aa16c64a2224888c032277b to your computer and use it in GitHub Desktop.
Save moi15moi/3f7704696aa16c64a2224888c032277b to your computer and use it in GitHub Desktop.
Create Font Name for NamedInstance. Warning. It only consider the Axis value table, format 1
import sys
from dataclasses import dataclass
from fontTools import ttLib
from fontTools.ttLib import ttFont
from fontTools.ttLib.tables._f_v_a_r import NamedInstance
from fontTools.ttLib.tables._n_a_m_e import NameRecord
from typing import Dict, List
@dataclass
class FontInfo:
name: str
coordinates: Dict[str, float]
@dataclass
class AxisRecord:
"""
https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-records
"""
AxisTag: str
AxisNameID: int
AxisOrdering: int
@dataclass
class AxisValueFormat1:
"""
https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-1
"""
OLDER_SIBLING_FONT_ATTRIBUTE = 0x0001
ELIDABLE_AXIS_VALUE_NAME = 0x0002
axisIndex: int
flags: int
valueNameID: int
value: float
def sort_naming_table(names: List[NameRecord]) -> List[NameRecord]:
"""
Parameters:
names (List[NameRecord]): Naming table
Returns:
The sorted naming table.
Based on FontConfig:
- https://gitlab.freedesktop.org/fontconfig/fontconfig/-/blob/d863f6778915f7dd224c98c814247ec292904e30/src/fcfreetype.c#L1127-1140
"""
def isEnglish(name: NameRecord) -> bool:
# From: https://gitlab.freedesktop.org/fontconfig/fontconfig/-/blob/d863f6778915f7dd224c98c814247ec292904e30/src/fcfreetype.c#L1111-1125
return (name.platformID, name.langID) in ((1, 0), (3, 0x409))
# From: https://github.com/freetype/freetype/blob/b98dd169a1823485e35b3007ce707a6712dcd525/include/freetype/ttnameid.h#L86-L91
PLATFORM_ID_APPLE_UNICODE = 0
PLATFORM_ID_MACINTOSH = 1
PLATFORM_ID_ISO = 2
PLATFORM_ID_MICROSOFT = 3
# From: https://gitlab.freedesktop.org/fontconfig/fontconfig/-/blob/d863f6778915f7dd224c98c814247ec292904e30/src/fcfreetype.c#L1078
PLATFORM_ID_ORDER = [
PLATFORM_ID_MICROSOFT,
PLATFORM_ID_APPLE_UNICODE,
PLATFORM_ID_MACINTOSH,
PLATFORM_ID_ISO,
]
return sorted(
names,
key=lambda name: (
PLATFORM_ID_ORDER.index(name.platformID),
name.nameID,
name.platEncID,
-isEnglish(name),
name.langID,
),
)
def get_first_decoded_name(nameID: int, names: List[NameRecord]) -> str:
"""
Parameters:
names (List[NameRecord]): Naming table
Returns:
The first decoded name.
"""
for name in names:
if name.nameID != nameID:
continue
try:
unistr = name.toUnicode()
except UnicodeDecodeError:
continue
return unistr
def get_family(names: List[NameRecord]):
"""
Parameters:
names (List[NameRecord]): Naming table
Returns:
The family of an variation font.
"""
family_name = get_first_decoded_name(16, names)
# If nameID 16 is None, try nameID 1https://learn.microsoft.com/en-us/typography/opentype/spec/otvaroverview#terminology
if family_name is None:
family_name = get_first_decoded_name(1, names)
return family_name
def get_axis_records(stat_table) -> List[AxisRecord]:
"""
Parameters:
stat_table: The STAT table: https://learn.microsoft.com/en-us/typography/opentype/spec/stat
Returns:
A list of each AxisRecord: https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-records
"""
axis_records: List[AxisRecord] = []
if stat_table.DesignAxisRecord:
for axis in stat_table.DesignAxisRecord.Axis:
axis_records.append(
AxisRecord(str(axis.AxisTag), axis.AxisNameID, axis.AxisOrdering)
)
return axis_records
def get_axis_values_format_1(stat_table) -> List[AxisValueFormat1]:
"""
Parameters:
stat_table: The STAT table: https://learn.microsoft.com/en-us/typography/opentype/spec/stat
Returns:
A list of each AxisValue from table format 1: https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-1
"""
axis_values_format_1_table: List[AxisValueFormat1] = []
if stat_table.AxisValueArray:
for value in stat_table.AxisValueArray.AxisValue:
if value.Format == 1:
axis_values_format_1_table.append(
AxisValueFormat1(
value.AxisIndex, value.Flags, value.ValueNameID, value.Value
)
)
return axis_values_format_1_table
def get_named_instance_and_axis_value(
font: ttFont.TTFont,
axis_values_format_1_table: List[AxisValueFormat1],
axis_records: List[AxisRecord],
) -> Dict[NamedInstance, List[AxisValueFormat1]]:
"""
Parameters:
font (ttFont.TTFont): Fonttools font object
axis_values_format_1_table (List[AxisValueFormat1]): List of each AxisValue from table format 1: https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-1
axis_records (List[AxisRecord]): List of each AxisRecord: https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-records
Returns:
An dictionnary that map an NamedInstance for an AxisValue
"""
named_instances: Dict[NamedInstance, AxisValueFormat1] = {}
for named_instance in font["fvar"].instances:
axis_value_for_named_instance: List[AxisValueFormat1] = []
for (
named_instance_tag,
named_instance_value,
) in named_instance.coordinates.items():
axis_value_for_coordinate: AxisValueFormat1 = None
for axis_value in axis_values_format_1_table:
if named_instance_tag == axis_records[axis_value.axisIndex].AxisTag:
if axis_value_for_coordinate is None:
axis_value_for_coordinate = axis_value
elif abs(named_instance_value - axis_value.value) < abs(
named_instance_value - axis_value_for_coordinate.value
) or (
abs(named_instance_value - axis_value.value)
== abs(named_instance_value - axis_value_for_coordinate.value)
and axis_value_for_coordinate.value < axis_value.value
):
axis_value_for_coordinate = axis_value
if axis_value_for_coordinate is not None:
axis_value_for_named_instance.append(axis_value_for_coordinate)
if len(axis_value_for_named_instance) != 0:
named_instances[named_instance] = axis_value_for_named_instance
return named_instances
def get_default_instance(
font: ttFont.TTFont, stat_table, names: List[NameRecord], family: str
):
"""
Parameters:
font (ttFont.TTFont): Fonttools font object
stat_table: The STAT table: https://learn.microsoft.com/en-us/typography/opentype/spec/stat
names (List[NameRecord]): Naming table
family (str): The family of the font
Returns:
The default instance. It is created from each VariationAxisRecord.DefaultValue
"""
default_name = get_first_decoded_name(stat_table.ElidedFallbackNameID, names)
coordinates = {}
for variation_axis_record in font["fvar"].axes:
coordinates[variation_axis_record.axisTag] = variation_axis_record.defaultValue
return FontInfo(f"{family} {default_name}".strip(), coordinates)
def get_font_infos(font: ttFont.TTFont) -> List[FontInfo]:
"""
Parameters:
font (ttFont.TTFont): Fonttools font object
Returns:
A list of FontInfo.
"""
font_infos: List[FontInfo] = []
stat_table = font["STAT"].table
axis_records: List[AxisRecord] = get_axis_records(stat_table)
axis_value_format_1_table: List[AxisValueFormat1] = get_axis_values_format_1(
stat_table
)
named_instances = get_named_instance_and_axis_value(
font, axis_value_format_1_table, axis_records
)
names = sort_naming_table(font["name"].names)
family = get_family(names)
for named_instance, axis_values in named_instances.items():
# [[AxisOrdering, AxisValue], [AxisOrdering, AxisValue]]
styles = []
for axis_value in axis_values:
styles.append([axis_records[axis_value.axisIndex].AxisOrdering, axis_value])
styles.sort(key=lambda style: style[0])
styles_str = ""
for style in styles:
axis_value = style[1]
if axis_value.flags != AxisValueFormat1.ELIDABLE_AXIS_VALUE_NAME:
styles_str += (
get_first_decoded_name(axis_value.valueNameID, names) + " "
)
styles_str = styles_str.strip()
font_infos.append(
FontInfo(f"{family} {styles_str}".strip(), named_instance.coordinates)
)
font_infos.append(get_default_instance(font, stat_table, names, family))
return font_infos
def main():
font_path = r"bahnschrift (original).ttf"
font = ttLib.TTFont(font_path)
font_infos = get_font_infos(font)
print(font_infos)
if __name__ == "__main__":
sys.exit(main())
@moi15moi
Copy link
Author

moi15moi commented Apr 5, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment