commit 0d67aafcd9e0f8ec1d47db6bf228863799187ba9 Author: kongkx Date: Sat Aug 3 22:20:37 2024 +0800 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83a2e45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv +output diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bee79d --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Font Split + +Split the preset variations in a Variable Font into static fonts + +## Usage + +```bash +python3 -m venv venv +source venv/bin/activate + +pip install -r requirements.txt + +python main.py path/to/font.ttf path/to/output/dir +``` diff --git a/main.py b/main.py new file mode 100644 index 0000000..7bed4bd --- /dev/null +++ b/main.py @@ -0,0 +1,178 @@ +import sys +import os +import argparse +from typing import Tuple, List +from fontTools import ttLib +from fontTools.ttLib import TTFont +from fontTools.varLib.mutator import instantiateVariableFont +from fontTools.ttLib.tables._f_v_a_r import NamedInstance +from fontTools.ttLib.tables._n_a_m_e import NameRecord + + +def camelString(text: str): + return text.replace(' ', '') + +def sortNamingTable(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 getFirstDecodedName(nameID: int, names: List[NameRecord]) -> str: + """ + Parameters: + names (List[NameRecord]): Naming table + Returns: + The first decoded name. + """ + + names = sortNamingTable(names) + + for name in names: + if name.nameID != nameID: + continue + try: + unistr = name.toUnicode() + except UnicodeDecodeError: + continue + + return unistr + +def splitNamedInstance(font: TTFont, output_dir: str) -> List[Tuple[str, str]]: + """ + Parameters: + font (TTFont): A font + Returns: + An dictionnary. The key is the font name and the value an NamedInstance + The dictionnary support the R/B/I/BI family model: https://learn.microsoft.com/en-us/typography/opentype/spec/stat#alternate-font-family-models + """ + + if "fvar" not in font: + raise Exception("The font is not an Variable Font.") + + # Read the note about the R/B/I/BI model: https://learn.microsoft.com/en-us/typography/opentype/spec/stat#alternate-font-family-models + # INVALID_STYLE = ("regular", "bold", "italic", "oblique") + + family_name = getFirstDecodedName(16, font['name'].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 = getFirstDecodedName(1, font['name'].names) + + instances_info: List[Tuple[str, str]] = [] + for instance in font["fvar"].instances: + + instance_name = getFirstDecodedName(instance.subfamilyNameID, font['name'].names) + + font_name = f"{family_name} {instance_name}" + postscript_name = camelString(family_name) + '-' + camelString(instance_name) + + # instance_font = instantiateVariableFont(font, instance.coordinates) + + # name_table = instance_font['name'] + + # langID: int + # platformID: int + # platEncID: int + # # Take "Arial Black" as reference. + # for record in name_table.names: + # if record.nameID == 1: # family name + # name_table.setName(font_name, record.nameID, record.platformID, record.platEncID, record.langID) + # platEncID = record.platEncID + # platformID = record.platformID + # langID = record.langID + # if record.nameID == 2: # style name + # name_table.setName('Regular', record.nameID, record.platformID, record.platEncID, record.langID) + # if record.nameID == 4: # full name + # name_table.setName(font_name, record.nameID, record.platformID, record.platEncID, record.langID) + # if record.nameID == 6: # PostScript Name + # name_table.setName(postscript_name, record.nameID, record.platformID, record.platEncID, record.langID) + + + # name_table.setName(family_name, 16, platformID, platEncID, langID) + # name_table.setName(instance_name, 17, platformID, platEncID, langID) + + # output_path = os.path.join(output_dir, f"{font_name}.ttf") + # instance_font.save(output_path) + + # print(font_name + ' saved') + instances_info.append((font_name, postscript_name)) + + return instances_info + +def generateSampleHtml(instances_info: List[Tuple[str, str]], output_dir: str): + html_content = """ + + + + + + +""" + + for font_name, postscript_name in instances_info: + html_content += f'
{font_name}
\n' + + html_content += """ + + +""" + html_path = os.path.join(output_dir, "sample.html") + with open(html_path, "w", encoding="utf-8") as html_file: + html_file.write(html_content) + + print("Sample HTML generated at", html_path) + + +def main(): + + parser = argparse.ArgumentParser(description='Process a variable font.') + parser.add_argument('font_path', type=str, help='The path to the variable font file') + parser.add_argument('output_dir', type=str, help='The directory to save the output font files') + + args = parser.parse_args() + + font_path = args.font_path + output_dir = args.output_dir + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + font = ttLib.TTFont(font_path) + + names = splitNamedInstance(font, output_dir) + + generateSampleHtml(names, output_dir) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..55c1000 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +fontTools>=4.28.0