Skip to content

Instantly share code, notes, and snippets.

@moi15moi
Last active April 5, 2023 00:40
Show Gist options
  • Save moi15moi/dd0fd510c03c7d80a274d69bf2edfb29 to your computer and use it in GitHub Desktop.
Save moi15moi/dd0fd510c03c7d80a274d69bf2edfb29 to your computer and use it in GitHub Desktop.
Get font name and properties like GDI
from fontTools.ttLib.ttFont import TTFont
from fontTools.varLib.instancer.names import (
ELIDABLE_AXIS_VALUE_NAME,
)
from typing import Any, Dict, List, Tuple
from font_collector.font_parser import FontParser
from font_collector import NameNotFoundException
DEFAULT_WEIGHT = 400
DEFAULT_ITALIC = False
class FontFace:
family_name: str
full_name: str
weight: int
italic: bool
named_instance_coordinates: Dict[str, float] = {}
def __init__(
self,
family_name: str,
full_name: str,
weight: int,
italic: bool,
named_instance_coordinates: Dict[str, float] = {},
):
self.family_name = family_name
self.full_name = full_name
self.weight = weight
self.italic = italic
self.named_instance_coordinates = named_instance_coordinates
def __repr__(self):
return f'Family_name: "{self.family_name}"\nFull_name: "{self.full_name}"\nWeight: "{self.weight}"\nItalic: "{self.italic}\nnamed_instance_coordinates: "{self.named_instance_coordinates}"'
@property
def is_variable_font(self):
return len(self.named_instance_coordinates) > 0
def open_normal_font(ttfont: TTFont) -> FontFace:
"""
Open the font like an "normal" font (it doesn't deal with ttc font, but the goal of this file is to demonstrate how GDI open variable font)
Parameters:
ttfont (TTFont): An fontTools object
Returns:
An FontFace that represent the font
"""
family_name = FontParser.get_name_by_id(1, ttfont["name"].names)
if is_truetype(ttfont):
full_name = FontParser.get_name_by_id(4, ttfont["name"].names)
else:
full_name = FontParser.get_name_by_id(6, ttfont["name"].names)
weight = ttfont["OS/2"].usWeightClass
is_italic = bool(ttfont["OS/2"].fsSelection & 1)
return FontFace(family_name, full_name, weight, is_italic)
def is_truetype(ttfont: TTFont) -> bool:
return "glyf" in ttfont
def is_valid_variable_font(ttfont: TTFont) -> bool:
"""
Parameters:
ttfont (TTFont): An fontTools object
Returns:
An boolean that indicate if the font is an variable font or not.
"""
if "fvar" not in ttfont or "STAT" not in ttfont:
return False
if ttfont["STAT"].table is None:
return False
for axe in ttfont["fvar"].axes:
if not (axe.minValue <= axe.defaultValue <= axe.maxValue):
return False
if ttfont["STAT"].table.AxisValueArray is not None:
for axis_value in ttfont["STAT"].table.AxisValueArray.AxisValue:
if axis_value.Format in (1, 2, 3, 4):
if axis_value.Format == 4 and len(axis_value.AxisValueRecord) == 0:
return False
else:
return False
return True
def get_distance_between_axis_value_and_coordinates(
ttfont: TTFont, coordinates: Dict[str, float], axis_value: Any, axis_format: int
) -> float:
"""
Parameters:
ttfont (TTFont): An fontTools object
coordinates (Dict[str, float]): The coordinates of an NamedInstance in the fvar table.
axis_value (Any): An AxisValue
axis_format (int): The AxisValue Format.
Since the AxisValue from AxisValueRecord of an AxisValue Format 4 doesn't contain an Format attribute, this parameter is needed.
Returns:
The distance between_axis_value_and_coordinates
"""
axis_tag = ttfont["STAT"].table.DesignAxisRecord.Axis[axis_value.AxisIndex].AxisTag
instance_value = coordinates.get(axis_tag, 0)
if axis_format == 2:
clamped_axis_value = max(
min(instance_value, axis_value.RangeMaxValue),
axis_value.RangeMinValue,
)
else:
clamped_axis_value = axis_value.Value
delta = clamped_axis_value - instance_value
delta_square = delta**2
if delta < 0:
adjust = 1
else:
adjust = 0
distance = delta_square * 2 + adjust
return distance
def get_axis_value_from_coordinates(
ttfont: TTFont, coordinates: Dict[str, float]
) -> List[Any]:
"""
Parameters:
ttfont (TTFont): An fontTools object
coordinates (Dict[str, float]): The coordinates of an NamedInstance in the fvar table.
Returns:
An list who contain all the AxisValue linked to the coordinates.
"""
distances_for_axis_values: List[Tuple[float, Any]] = []
if ttfont["STAT"].table.AxisValueArray is None:
return distances_for_axis_values
for axis_value in ttfont["STAT"].table.AxisValueArray.AxisValue:
if axis_value.Format == 4:
distance = 0
for axis_value_format_4 in axis_value.AxisValueRecord:
distance += get_distance_between_axis_value_and_coordinates(
ttfont, coordinates, axis_value_format_4, axis_value.Format
)
distances_for_axis_values.append((distance, axis_value))
else:
distance = get_distance_between_axis_value_and_coordinates(
ttfont, coordinates, axis_value, axis_value.Format
)
distances_for_axis_values.append((distance, axis_value))
# Sort by ASC
distances_for_axis_values.sort(key=lambda distance: distance[0])
axis_values_coordinate_matches: List[Any] = []
is_axis_useds: List[bool] = [False] * len(
ttfont["STAT"].table.DesignAxisRecord.Axis
)
for distance, axis_value in distances_for_axis_values:
if axis_value.Format == 4:
# The AxisValueRecord can have "internal" duplicate axis, but it cannot have duplicate Axis with the other AxisValue
is_any_duplicate_axis = False
for axis_value_format_4 in axis_value.AxisValueRecord:
if is_axis_useds[axis_value_format_4.AxisIndex]:
is_any_duplicate_axis = True
break
if not is_any_duplicate_axis:
for axis_value_format_4 in axis_value.AxisValueRecord:
is_axis_useds[axis_value_format_4.AxisIndex] = True
axis_values_coordinate_matches.append(axis_value)
else:
if not is_axis_useds[axis_value.AxisIndex]:
is_axis_useds[axis_value.AxisIndex] = True
axis_values_coordinate_matches.append(axis_value)
return axis_values_coordinate_matches
def get_axis_value_table_property(
ttfont: TTFont, axis_values: List[Any], family_name_prefix: str
) -> Tuple[str, str, float, bool]:
"""
Parameters:
ttfont (TTFont): An fontTools object
axis_values (List[Any]): An list of AxisValue.
family_name_prefix (str): The variable family name prefix.
Ex: For the name "Alegreya Italic", "Alegreya" is the family name prefix.
Returns:
The family_name, full_name, weight, italic.
"""
axis_values.sort(
key=lambda axis_value: ttfont["STAT"]
.table.DesignAxisRecord.Axis[
min(
axis_value.AxisValueRecord,
key=lambda axis_value_format_4: ttfont["STAT"]
.table.DesignAxisRecord.Axis[axis_value_format_4.AxisIndex]
.AxisOrdering,
).AxisIndex
]
.AxisOrdering
if axis_value.Format == 4
else ttfont["STAT"]
.table.DesignAxisRecord.Axis[axis_value.AxisIndex]
.AxisOrdering
)
family_axis_value = []
fullname_axis_value = []
weight = DEFAULT_WEIGHT
italic = DEFAULT_ITALIC
for axis_value in axis_values:
# If the Format 4 only contain only 1 AxisValueRecord, it will treat it as an single AxisValue like the Format 1, 2 or 3.
if axis_value.Format == 4 and len(axis_value.AxisValueRecord) > 1:
if not axis_value.Flags & ELIDABLE_AXIS_VALUE_NAME:
family_axis_value.append(axis_value)
fullname_axis_value.append(axis_value)
else:
if axis_value.Format == 2:
value = axis_value.NominalValue
axis_index = axis_value.AxisIndex
elif axis_value.Format in (1, 3):
value = axis_value.Value
axis_index = axis_value.AxisIndex
elif axis_value.Format == 4:
value = axis_value.AxisValueRecord[0].Value
axis_index = axis_value.AxisValueRecord[0].AxisIndex
if ttfont["STAT"].table.DesignAxisRecord.Axis[axis_index].AxisTag == "wght":
weight = value
elif (
ttfont["STAT"].table.DesignAxisRecord.Axis[axis_index].AxisTag == "ital"
):
italic = value == 1
if not (axis_value.Flags & ELIDABLE_AXIS_VALUE_NAME):
fullname_axis_value.append(axis_value)
use_in_family_name = True
if (
ttfont["STAT"].table.DesignAxisRecord.Axis[axis_index].AxisTag
== "wght"
):
use_in_family_name = value not in (400, 700)
elif (
ttfont["STAT"].table.DesignAxisRecord.Axis[axis_index].AxisTag
== "ital"
):
use_in_family_name = value not in (0, 1)
if use_in_family_name:
family_axis_value.append(axis_value)
try:
family_name = f'{family_name_prefix} {" ".join(FontParser.get_name_by_id(axis_value.ValueNameID, ttfont["name"].names) for axis_value in family_axis_value)}'
except NameNotFoundException:
family_name = family_name_prefix
if len(fullname_axis_value) == 0:
try:
full_name = f"{family_name_prefix} {FontParser.get_name_by_id(ttfont['STAT'].table.ElidedFallbackNameID, ttfont['name'].names)}"
except NameNotFoundException:
weight = DEFAULT_WEIGHT
italic = DEFAULT_ITALIC
full_name = f"{family_name_prefix} Regular"
else:
try:
full_name = f'{family_name_prefix} {" ".join(FontParser.get_name_by_id(axis_value.ValueNameID, ttfont["name"].names) for axis_value in fullname_axis_value)}'
except NameNotFoundException:
weight = DEFAULT_WEIGHT
italic = DEFAULT_ITALIC
try:
full_name = f"{family_name_prefix} {FontParser.get_name_by_id(ttfont['STAT'].table.ElidedFallbackNameID, ttfont['name'].names)}"
except NameNotFoundException:
full_name = f"{family_name_prefix} Regular"
return family_name, full_name, weight, italic
def create_font_face_from_named_instance(ttfont: TTFont) -> List[FontFace]:
"""
Parameters:
ttfont (TTFont): An fontTools object
Returns:
An list who contain all the Named instance FontFaces.
"""
fonts: List[FontFace] = []
family_name_prefix = FontParser.get_var_font_family_prefix(ttfont)
axis_values_coordinates: List[Tuple[Any, Dict[str, float]]] = []
for instance in ttfont["fvar"].instances:
axis_value_table = get_axis_value_from_coordinates(ttfont, instance.coordinates)
instance_coordinates = instance.coordinates
for axis_value_coordinates in axis_values_coordinates:
if axis_value_coordinates[0] == axis_value_table:
instance_coordinates = axis_value_coordinates[1]
break
axis_values_coordinates.append((axis_value_table, instance.coordinates))
family_name, full_name, weight, italic = get_axis_value_table_property(
ttfont, axis_value_table, family_name_prefix
)
font_face = FontFace(
family_name, full_name, weight, italic, instance_coordinates
)
fonts.append(font_face)
return fonts
def main():
ttfont = TTFont("Inconsolata-VF.ttf")
if is_valid_variable_font(ttfont):
print("Font face from named instance")
for font in create_font_face_from_named_instance(ttfont):
print(f"Family name: {font.family_name}")
print(f"Full name: {font.full_name}")
print(f"Weight: {font.weight}")
print(f"Italic: {font.italic}")
print()
else:
print('The font is a "normal" font')
font = open_normal_font(ttfont)
print(f"Family name: {font.family_name}")
print(f"Full name: {font.full_name}")
print(f"Weight: {font.weight}")
print(f"Italic: {font.italic}")
if __name__ == "__main__":
exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment