Last active
April 5, 2023 00:40
-
-
Save moi15moi/dd0fd510c03c7d80a274d69bf2edfb29 to your computer and use it in GitHub Desktop.
Get font name and properties like GDI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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