Instructions to use dl1wjd2/Logistics_AX with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- ultralytics
How to use dl1wjd2/Logistics_AX with ultralytics:
from huggingface_hub import hf_hub_download from ultralytics import YOLO # pick the weights file from this repo's "Files and versions" tab weights = hf_hub_download("dl1wjd2/Logistics_AX", "<weights>.pt") model = YOLO(weights) source = 'http://images.cocodataset.org/val2017/000000039769.jpg' model.predict(source=source, save=True) - Notebooks
- Google Colab
- Kaggle
Download hug/lib/python3.12/site-packages/fontTools/ttLib/beyond64k.py from dl1wjd2/Logistics_AX: direct link, hf CLI and curl.
- Browser
- Download file 35 kB
-
https://huggingface.co/dl1wjd2/Logistics_AX/resolve/main/hug/lib/python3.12/site-packages/fontTools/ttLib/beyond64k.py
- Command line
-
hf download hf://dl1wjd2/Logistics_AX/hug/lib/python3.12/site-packages/fontTools/ttLib/beyond64k.py
-
curl -L -o beyond64k.py https://huggingface.co/dl1wjd2/Logistics_AX/resolve/main/hug/lib/python3.12/site-packages/fontTools/ttLib/beyond64k.py
35 kB
| """Convert between Open Font Format beyond-64k companion tables. | |
| The Open Font Format beyond-64k specification adds uppercase companion tables and | |
| extended Layout formats that can address glyph IDs above 65535. This module | |
| provides helpers to convert a :class:`fontTools.ttLib.TTFont` in place between | |
| the compact lowercase table family and the extended uppercase table family. | |
| The high-level APIs are :func:`upper_tables` and :func:`lower_tables`. By | |
| default they convert every known table present in the font and validate that | |
| the result does not mix lowercase and uppercase companion tables. Both helpers | |
| accept an optional ``tables`` iterable; each entry may use either the compact or | |
| extended spelling, e.g. ``"glyf"`` and ``"GLYF"`` both identify the glyph table | |
| family. | |
| The module is also available as a command-line tool:: | |
| fonttools ttLib.beyond64k upper input.ttf -o upper.ttf | |
| fonttools ttLib.beyond64k lower upper.ttf -o lower.ttf | |
| fonttools ttLib.beyond64k upper input.ttf GSUB GPOS glyf | |
| Lowering validates lossy cases such as ``MAXP.numGlyphs`` values above 65535, | |
| ``GLYF`` glyphs with cubic outlines, and component glyph IDs that do not fit in | |
| the compact ``glyf`` table. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| from collections.abc import Iterable | |
| from contextlib import contextmanager | |
| from dataclasses import dataclass | |
| from typing import Callable | |
| from fontTools.misc.cliTools import makeOutputFileName | |
| from fontTools.ttLib import TTFont, TTLibError, newTable | |
| from fontTools.ttLib.tables import otTables | |
| from fontTools.ttLib.tables.otTraverse import dfs_base_table | |
| _TABLE_PAIRS = { | |
| "glyf": "GLYF", | |
| "loca": "LOCA", | |
| "maxp": "MAXP", | |
| "hhea": "HHEA", | |
| "hmtx": "HMTX", | |
| "vhea": "VHEA", | |
| "vmtx": "VMTX", | |
| "gvar": "GVAR", | |
| } | |
| _TABLE_IDENTITIES = { | |
| tag: lower for lower, upper in _TABLE_PAIRS.items() for tag in (lower, upper) | |
| } | |
| _TABLE_IDENTITIES.update( | |
| { | |
| "BASE": "BASE", | |
| "COLR": "COLR", | |
| "GDEF": "GDEF", | |
| "GPOS": "GPOS", | |
| "GSUB": "GSUB", | |
| "JSTF": "JSTF", | |
| "VORG": "VORG", | |
| } | |
| ) | |
| class _TableConversion: | |
| destination: str | |
| transform: Callable | None = None | |
| _EXPLICIT_LAYOUT_FORMATS = { | |
| (otTables.BaseCoord, 2): (4, {}), | |
| (otTables.SinglePos, 1): (3, {}), | |
| (otTables.SinglePos, 2): (4, {}), | |
| (otTables.PairPos, 1): ( | |
| 3, | |
| { | |
| otTables.PairSet: otTables.PairSet2, | |
| otTables.PairValueRecord: otTables.PairValue2, | |
| }, | |
| ), | |
| (otTables.PairPos, 2): (4, {}), | |
| (otTables.CursivePos, 1): ( | |
| 2, | |
| {otTables.EntryExitRecord: otTables.EntryExit2}, | |
| ), | |
| (otTables.MarkBasePos, 1): ( | |
| 2, | |
| { | |
| otTables.MarkArray: otTables.MarkArray2, | |
| otTables.MarkRecord: otTables.MarkRecord2, | |
| otTables.BaseArray: otTables.BaseArray2, | |
| otTables.BaseRecord: otTables.BaseRecord2, | |
| }, | |
| ), | |
| (otTables.MarkLigPos, 1): ( | |
| 2, | |
| { | |
| otTables.MarkArray: otTables.MarkArray2, | |
| otTables.MarkRecord: otTables.MarkRecord2, | |
| otTables.LigatureArray: otTables.LigatureArray2, | |
| otTables.LigatureAttach: otTables.LigatureAttach2, | |
| otTables.ComponentRecord: otTables.ComponentRecord2, | |
| }, | |
| ), | |
| (otTables.MarkMarkPos, 1): ( | |
| 2, | |
| { | |
| otTables.MarkArray: otTables.MarkArray2, | |
| otTables.MarkRecord: otTables.MarkRecord2, | |
| otTables.Mark2Array: otTables.Mark2Array2, | |
| otTables.Mark2Record: otTables.Mark2Record2, | |
| }, | |
| ), | |
| (otTables.ReverseChainSingleSubst, 1): (2, {}), | |
| } | |
| def _contextual_layout_conversion( | |
| table_type, compact_format, lookup_prefix, chaining=False | |
| ): | |
| compact_prefix = ("Chain" if chaining else "") + lookup_prefix[:3] | |
| class_based = compact_format == 2 | |
| compact_rule = compact_prefix + ("ClassRule" if class_based else "Rule") | |
| compact_rule_set = compact_prefix + ("ClassSet" if class_based else "RuleSet") | |
| extended_rule = ("Chained" if chaining else "") + ( | |
| "ClassSeqRule" if class_based else "SeqRule" | |
| ) | |
| extended_rule_set = extended_rule + "Set" | |
| lookup_count = lookup_prefix + "Count" | |
| lookup_record = lookup_prefix + "LookupRecord" | |
| replacements = {getattr(otTables, lookup_record): (otTables.SeqLookup, {})} | |
| outer_renames = { | |
| lookup_count: "SeqLookupCount", | |
| lookup_record: "SeqLookupRecord", | |
| } | |
| if compact_format in (1, 2): | |
| replacements[getattr(otTables, compact_rule_set)] = ( | |
| getattr(otTables, extended_rule_set + "2"), | |
| { | |
| compact_rule + "Count": extended_rule + "Count", | |
| compact_rule: extended_rule, | |
| }, | |
| ) | |
| rule_renames = { | |
| lookup_count: "SeqLookupCount", | |
| lookup_record: "SeqLookupRecord", | |
| } | |
| if chaining: | |
| rule_renames.update( | |
| { | |
| "Backtrack": "BacktrackSequence", | |
| "Input": "InputSequence", | |
| "LookAhead": "LookAheadSequence", | |
| } | |
| ) | |
| else: | |
| rule_renames["Class" if class_based else "Input"] = "InputSequence" | |
| replacements[getattr(otTables, compact_rule)] = ( | |
| getattr(otTables, extended_rule + ("" if class_based else "2")), | |
| rule_renames, | |
| ) | |
| outer_renames = { | |
| compact_rule_set + "Count": extended_rule_set + "Count", | |
| compact_rule_set: extended_rule_set, | |
| } | |
| return table_type, compact_format, compact_format + 3, outer_renames, replacements | |
| _CONTEXTUAL_LAYOUT_FORMATS = { | |
| (table_type, compact_format): (extended_format, outer_renames, replacements) | |
| for ( | |
| table_type, | |
| compact_format, | |
| extended_format, | |
| outer_renames, | |
| replacements, | |
| ) in ( | |
| *( | |
| _contextual_layout_conversion(otTables.ContextSubst, format, "Subst") | |
| for format in (1, 2, 3) | |
| ), | |
| *( | |
| _contextual_layout_conversion(otTables.ContextPos, format, "Pos") | |
| for format in (1, 2, 3) | |
| ), | |
| *( | |
| _contextual_layout_conversion( | |
| otTables.ChainContextSubst, format, "Subst", chaining=True | |
| ) | |
| for format in (1, 2) | |
| ), | |
| *( | |
| _contextual_layout_conversion( | |
| otTables.ChainContextPos, format, "Pos", chaining=True | |
| ) | |
| for format in (1, 2) | |
| ), | |
| ) | |
| } | |
| def _rename_layout_attributes(table, renames): | |
| for source, destination in renames.items(): | |
| if hasattr(table, source): | |
| setattr(table, destination, getattr(table, source)) | |
| delattr(table, source) | |
| def _replace_layout_subtree(table, replacements): | |
| for path in reversed(list(dfs_base_table(table))): | |
| subtable = path[-1].value | |
| replacement_type, renames = replacements.get( | |
| type(subtable), (type(subtable), {}) | |
| ) | |
| _rename_layout_attributes(subtable, renames) | |
| if replacement_type is type(subtable): | |
| continue | |
| replacement = replacement_type() | |
| replacement.__dict__.update(subtable.__dict__) | |
| parent = path[-2].value | |
| entry = path[-1] | |
| if entry.index is None: | |
| setattr(parent, entry.name, replacement) | |
| else: | |
| getattr(parent, entry.name)[entry.index] = replacement | |
| def _replace_layout_classes(table, replacements): | |
| for path in reversed(list(dfs_base_table(table))): | |
| subtable = path[-1].value | |
| replacement_type = replacements.get(type(subtable)) | |
| if replacement_type is None: | |
| continue | |
| replacement = replacement_type() | |
| replacement.__dict__.update(subtable.__dict__) | |
| parent = path[-2].value | |
| entry = path[-1] | |
| if entry.index is None: | |
| setattr(parent, entry.name, replacement) | |
| else: | |
| getattr(parent, entry.name)[entry.index] = replacement | |
| def _convert_contextual_layout_formats(table, extended): | |
| conversions = _CONTEXTUAL_LAYOUT_FORMATS | |
| if not extended: | |
| conversions = { | |
| (table_type, extended_format): ( | |
| compact_format, | |
| {destination: source for source, destination in outer_renames.items()}, | |
| { | |
| extended_type: ( | |
| compact_type, | |
| { | |
| destination: source | |
| for source, destination in renames.items() | |
| }, | |
| ) | |
| for compact_type, (extended_type, renames) in replacements.items() | |
| }, | |
| ) | |
| for (table_type, compact_format), ( | |
| extended_format, | |
| outer_renames, | |
| replacements, | |
| ) in conversions.items() | |
| } | |
| changed = False | |
| for path in dfs_base_table(table): | |
| subtable = path[-1].value | |
| conversion = conversions.get( | |
| (type(subtable), getattr(subtable, "Format", None)) | |
| ) | |
| if conversion is None: | |
| continue | |
| format, outer_renames, replacements = conversion | |
| _replace_layout_subtree(subtable, replacements) | |
| _rename_layout_attributes(subtable, outer_renames) | |
| subtable.Format = format | |
| changed = True | |
| return changed | |
| def _convert_explicit_layout_formats(table, extended): | |
| conversions = _EXPLICIT_LAYOUT_FORMATS | |
| if not extended: | |
| conversions = { | |
| (table_type, extended_format): ( | |
| compact_format, | |
| {extended: compact for compact, extended in replacements.items()}, | |
| ) | |
| for (table_type, compact_format), ( | |
| extended_format, | |
| replacements, | |
| ) in conversions.items() | |
| } | |
| changed = False | |
| for path in dfs_base_table(table): | |
| subtable = path[-1].value | |
| conversion = conversions.get( | |
| (type(subtable), getattr(subtable, "Format", None)) | |
| ) | |
| if conversion is None: | |
| continue | |
| subtable.Format, replacements = conversion | |
| _replace_layout_classes(subtable, replacements) | |
| changed = True | |
| return changed | |
| def _convert_layout_formats(table, extended): | |
| contextual = _convert_contextual_layout_formats(table, extended) | |
| explicit = _convert_explicit_layout_formats(table, extended) | |
| return contextual or explicit | |
| def _convert_table_object(table, table_type): | |
| table.ensureDecompiled() | |
| converted = table_type() | |
| converted.__dict__.update(table.__dict__) | |
| return converted | |
| def _move_fields(table, names, extended, overwrite): | |
| for name in names: | |
| compact_name = name | |
| extended_name = name + "2" | |
| source_name, destination_name = ( | |
| (compact_name, extended_name) if extended else (extended_name, compact_name) | |
| ) | |
| source = getattr(table, source_name, None) | |
| destination = getattr(table, destination_name, None) | |
| if source is not None and destination is not None and not overwrite: | |
| raise ValueError( | |
| f"Both {source_name!r} and {destination_name!r} exist; " | |
| "set overwrite=True to replace the destination" | |
| ) | |
| if source is not None: | |
| setattr(table, destination_name, source) | |
| setattr(table, source_name, None) | |
| def _upper_gdef(font, table, overwrite): | |
| table = table.table | |
| _move_fields( | |
| table, | |
| ( | |
| "GlyphClassDef", | |
| "AttachList", | |
| "LigCaretList", | |
| "MarkAttachClassDef", | |
| "MarkGlyphSetsDef", | |
| ), | |
| True, | |
| overwrite, | |
| ) | |
| if getattr(table, "LigCaretList2", None) is not None: | |
| table.LigCaretList2 = _convert_table_object( | |
| table.LigCaretList2, otTables.LigCaretList2 | |
| ) | |
| table.Version = max(table.Version, 0x00010004) | |
| _convert_layout_formats(table, True) | |
| def _lower_gdef(font, table, overwrite): | |
| table = table.table | |
| _move_fields( | |
| table, | |
| ( | |
| "GlyphClassDef", | |
| "AttachList", | |
| "LigCaretList", | |
| "MarkAttachClassDef", | |
| "MarkGlyphSetsDef", | |
| ), | |
| False, | |
| overwrite, | |
| ) | |
| if getattr(table, "LigCaretList", None) is not None: | |
| table.LigCaretList = _convert_table_object( | |
| table.LigCaretList, otTables.LigCaretList | |
| ) | |
| if getattr(table, "VarStore", None) is not None: | |
| table.Version = 0x00010003 | |
| elif getattr(table, "MarkGlyphSetsDef", None) is not None: | |
| table.Version = 0x00010002 | |
| else: | |
| table.Version = 0x00010000 | |
| _convert_layout_formats(table, False) | |
| def _upper_layout_formats(font, table, overwrite): | |
| _convert_layout_formats(table.table, True) | |
| def _lower_layout_formats(font, table, overwrite): | |
| _convert_layout_formats(table.table, False) | |
| def _upper_jstf(font, table, overwrite): | |
| table = table.table | |
| _replace_layout_subtree( | |
| table, | |
| { | |
| otTables.JstfScriptRecord: (otTables.JstfScriptRecord2, {}), | |
| otTables.JstfScript: (otTables.JstfScript2, {}), | |
| otTables.ExtenderGlyph: (otTables.ExtenderGlyph2, {}), | |
| }, | |
| ) | |
| _rename_layout_attributes( | |
| table, | |
| { | |
| "JstfScriptCount": "JstfScriptCount2", | |
| "JstfScriptRecord": "JstfScriptRecord2", | |
| }, | |
| ) | |
| table.Version = max(table.Version, 0x00010001) | |
| def _lower_jstf(font, table, overwrite): | |
| table = table.table | |
| _replace_layout_subtree( | |
| table, | |
| { | |
| otTables.JstfScriptRecord2: (otTables.JstfScriptRecord, {}), | |
| otTables.JstfScript2: (otTables.JstfScript, {}), | |
| otTables.ExtenderGlyph2: (otTables.ExtenderGlyph, {}), | |
| }, | |
| ) | |
| _rename_layout_attributes( | |
| table, | |
| { | |
| "JstfScriptCount2": "JstfScriptCount", | |
| "JstfScriptRecord2": "JstfScriptRecord", | |
| }, | |
| ) | |
| table.Version = 0x00010000 | |
| def _upper_layout_header(font, table, overwrite): | |
| table = table.table | |
| _move_fields(table, ("ScriptList", "FeatureList", "LookupList"), True, overwrite) | |
| if getattr(table, "LookupList2", None) is not None: | |
| table.LookupList2 = _convert_table_object( | |
| table.LookupList2, otTables.LookupList2 | |
| ) | |
| table.Version = max(table.Version, 0x00010002) | |
| _convert_layout_formats(table, True) | |
| def _lower_layout_header(font, table, overwrite): | |
| table = table.table | |
| _move_fields(table, ("ScriptList", "FeatureList", "LookupList"), False, overwrite) | |
| if getattr(table, "LookupList", None) is not None: | |
| table.LookupList = _convert_table_object(table.LookupList, otTables.LookupList) | |
| table.Version = ( | |
| 0x00010001 if getattr(table, "FeatureVariations", None) else 0x00010000 | |
| ) | |
| _convert_layout_formats(table, False) | |
| def _has_extended_layout_formats(table): | |
| # The dispatch formats live on lookup subtables, including those wrapped | |
| # by extension lookups. Avoid descending through every rule/value record | |
| # merely to discover that an ordinary compact font needs no conversion. | |
| lookup_list = getattr(table, "LookupList", None) | |
| if lookup_list is None: | |
| return False | |
| extended_formats = { | |
| (table_type, conversion[0]) | |
| for conversions in (_CONTEXTUAL_LAYOUT_FORMATS, _EXPLICIT_LAYOUT_FORMATS) | |
| for (table_type, _), conversion in conversions.items() | |
| } | |
| for lookup in lookup_list.Lookup: | |
| for subtable in lookup.SubTable: | |
| if isinstance(subtable, (otTables.ExtensionSubst, otTables.ExtensionPos)): | |
| subtable = subtable.ExtSubTable | |
| if (type(subtable), getattr(subtable, "Format", None)) in extended_formats: | |
| return True | |
| return False | |
| def _compact_layout_tables(font, *, restore=True): | |
| # Use compact in-memory layouts for algorithms that dispatch on them. | |
| # No compact serialization occurs here: glyph IDs and counts stay intact. | |
| restore_actions = [] | |
| try: | |
| for tag, version, lower, upper in ( | |
| ("GDEF", 0x00010004, _lower_gdef, _upper_gdef), | |
| ("GSUB", 0x00010002, _lower_layout_header, _upper_layout_header), | |
| ("GPOS", 0x00010002, _lower_layout_header, _upper_layout_header), | |
| ): | |
| if tag not in font: | |
| continue | |
| table = font[tag] | |
| if table.table.Version >= version: | |
| restore_actions.append((tag, upper)) | |
| lower(font, table, True) | |
| elif _has_extended_layout_formats(table.table): | |
| _convert_layout_formats(table.table, False) | |
| restore_actions.append((tag, None)) | |
| yield | |
| finally: | |
| for tag, upper in restore_actions if restore else (): | |
| if tag not in font: | |
| continue | |
| if upper is not None: | |
| upper(font, font[tag], True) | |
| else: | |
| _convert_layout_formats(font[tag].table, True) | |
| def _lower_glyf(font, table, overwrite): | |
| from fontTools.ttLib.tables._g_l_y_f import flagCubic | |
| # Validation has rejected active or hinted CUBIC bits. Explicit lowering | |
| # may discard the remaining inert bits on unhinted on-curve points. | |
| for glyph_name in font.getGlyphOrder(): | |
| glyph = table[glyph_name] | |
| if hasattr(glyph, "flags"): | |
| for i, flag in enumerate(glyph.flags): | |
| glyph.flags[i] = flag & ~flagCubic | |
| def _upper_vorg(font, table, overwrite): | |
| table.majorVersion = 2 | |
| table.minorVersion = 0 | |
| def _lower_vorg(font, table, overwrite): | |
| table.majorVersion = 1 | |
| table.minorVersion = 0 | |
| def _iter_colr_paints(table): | |
| if not hasattr(table, "table"): | |
| return | |
| for path in dfs_base_table(table.table): | |
| value = path[-1].value | |
| if isinstance(value, otTables.Paint): | |
| yield value | |
| def _upper_colr(font, table, overwrite): | |
| for paint in _iter_colr_paints(table): | |
| if paint.Format == otTables.PaintFormat.PaintGlyph: | |
| paint.Format = otTables.PaintFormat.PaintGlyph2 | |
| def _lower_colr(font, table, overwrite): | |
| for paint in _iter_colr_paints(table): | |
| if paint.Format == otTables.PaintFormat.PaintGlyph2: | |
| paint.Format = otTables.PaintFormat.PaintGlyph | |
| _UPPER_TABLES = { | |
| **{ | |
| source: _TableConversion(destination) | |
| for source, destination in _TABLE_PAIRS.items() | |
| }, | |
| "BASE": _TableConversion("BASE", _upper_layout_formats), | |
| "COLR": _TableConversion("COLR", _upper_colr), | |
| "GDEF": _TableConversion("GDEF", _upper_gdef), | |
| "GSUB": _TableConversion("GSUB", _upper_layout_header), | |
| "GPOS": _TableConversion("GPOS", _upper_layout_header), | |
| "JSTF": _TableConversion("JSTF", _upper_jstf), | |
| "VORG": _TableConversion("VORG", _upper_vorg), | |
| } | |
| _LOWER_TABLES = { | |
| **{upper: _TableConversion(lower) for lower, upper in _TABLE_PAIRS.items()}, | |
| "GLYF": _TableConversion("glyf", _lower_glyf), | |
| "BASE": _TableConversion("BASE", _lower_layout_formats), | |
| "COLR": _TableConversion("COLR", _lower_colr), | |
| "GDEF": _TableConversion("GDEF", _lower_gdef), | |
| "GSUB": _TableConversion("GSUB", _lower_layout_header), | |
| "GPOS": _TableConversion("GPOS", _lower_layout_header), | |
| "JSTF": _TableConversion("JSTF", _lower_jstf), | |
| "VORG": _TableConversion("VORG", _lower_vorg), | |
| } | |
| def _normalize_tables(tables: Iterable[str] | None) -> set[str]: | |
| if tables is None: | |
| return set(_TABLE_IDENTITIES.values()) | |
| normalized = set() | |
| for tag in tables: | |
| try: | |
| normalized.add(_TABLE_IDENTITIES[tag]) | |
| except KeyError: | |
| raise ValueError(f"Unsupported beyond-64k table: {tag!r}") from None | |
| return normalized | |
| def _validate_family(font: TTFont) -> None: | |
| lower = sorted(tag for tag in _TABLE_PAIRS if tag in font) | |
| upper = sorted(tag for tag in _TABLE_PAIRS.values() if tag in font) | |
| if lower and upper: | |
| raise ValueError( | |
| "Mixed beyond-64k table family: " | |
| f"lowercase {', '.join(lower)}; uppercase {', '.join(upper)}" | |
| ) | |
| def _validate_lowering(font: TTFont, conversions: dict[str, _TableConversion]) -> None: | |
| if "GLYF" in conversions and "GLYF" in font: | |
| from fontTools.ttLib.tables._g_l_y_f import flagCubic, flagOnCurve | |
| glyf = font["GLYF"] | |
| for glyph_name in font.getGlyphOrder(): | |
| glyph = glyf[glyph_name] | |
| if glyph.isComposite(): | |
| for component in glyph.components: | |
| if font.getGlyphID(component.glyphName) > 0xFFFF: | |
| raise ValueError( | |
| f"GLYF component {component.glyphName!r} does not fit in glyf" | |
| ) | |
| else: | |
| cubic_flags = [ | |
| flag for flag in getattr(glyph, "flags", ()) if flag & flagCubic | |
| ] | |
| program = getattr(glyph, "program", None) | |
| # Hinting can flip an on-curve point and activate its CUBIC bit. | |
| if cubic_flags and ( | |
| any(not flag & flagOnCurve for flag in cubic_flags) | |
| or (program and program.getBytecode()) | |
| ): | |
| raise ValueError( | |
| f"GLYF glyph {glyph_name!r} has cubic outlines or hinted CUBIC flags" | |
| ) | |
| if "MAXP" in conversions and "MAXP" in font and font["MAXP"].numGlyphs > 0xFFFF: | |
| raise ValueError("MAXP.numGlyphs does not fit in maxp") | |
| if "HHEA" in conversions and "HHEA" in font: | |
| if font["HHEA"].numberOfHMetrics > 0xFFFF: | |
| raise ValueError("HHEA.numberOfHMetrics does not fit in hhea") | |
| if "VHEA" in conversions and "VHEA" in font: | |
| if font["VHEA"].numberOfVMetrics > 0xFFFF: | |
| raise ValueError("VHEA.numberOfVMetrics does not fit in vhea") | |
| if "VORG" in conversions and "VORG" in font: | |
| vorg = font["VORG"] | |
| if len(vorg.VOriginRecords) > 0xFFFF: | |
| raise ValueError("VORG record count does not fit in version 1") | |
| for glyph_name in vorg.VOriginRecords: | |
| if font.getGlyphID(glyph_name) > 0xFFFF: | |
| raise ValueError(f"VORG glyph {glyph_name!r} does not fit in version 1") | |
| if "COLR" in conversions and "COLR" in font: | |
| for paint in _iter_colr_paints(font["COLR"]): | |
| if ( | |
| paint.Format == otTables.PaintFormat.PaintGlyph2 | |
| and font.getGlyphID(paint.Glyph) > 0xFFFF | |
| ): | |
| raise ValueError( | |
| f"COLR PaintGlyph2 glyph {paint.Glyph!r} does not fit in PaintGlyph" | |
| ) | |
| _validate_cmap_uvs_lowering(font) | |
| def _cmap_subtable_key(subtable) -> tuple[int, int, int]: | |
| return subtable.platformID, subtable.platEncID, subtable.language | |
| def _merge_uvs_dict(destination, source) -> None: | |
| for selector, entries in source.items(): | |
| merged = dict(destination.get(selector, ())) | |
| merged.update(entries) | |
| destination[selector] = sorted(merged.items()) | |
| def _copy_uvs_subtable(format: int, source): | |
| from fontTools.ttLib.tables._c_m_a_p import CmapSubtable | |
| subtable = CmapSubtable.newSubtable(format) | |
| subtable.platformID = source.platformID | |
| subtable.platEncID = source.platEncID | |
| subtable.language = source.language | |
| subtable.cmap = {} | |
| subtable.uvsDict = { | |
| selector: list(entries) for selector, entries in source.uvsDict.items() | |
| } | |
| return subtable | |
| def _uvs_has_high_glyph_id(font: TTFont, subtable) -> bool: | |
| subtable.ensureDecompiled() | |
| for entries in subtable.uvsDict.values(): | |
| for _, glyph_name in entries: | |
| if glyph_name is not None and font.getGlyphID(glyph_name) > 0xFFFF: | |
| return True | |
| return False | |
| def _character_map_tables(font: TTFont): | |
| for tag in ("cmap", "DMAP"): | |
| if tag in font: | |
| yield tag, font[tag] | |
| def _validate_cmap_uvs_lowering(font: TTFont) -> None: | |
| for tag, character_map in _character_map_tables(font): | |
| for subtable in character_map.tables: | |
| if subtable.format == 15 and _uvs_has_high_glyph_id(font, subtable): | |
| raise ValueError(f"{tag} format 15 glyph IDs do not fit in format 14") | |
| def _upper_beyond64k_cmap_uvs(font: TTFont) -> None: | |
| for _, character_map in _character_map_tables(font): | |
| format15_by_key = { | |
| _cmap_subtable_key(subtable): subtable | |
| for subtable in character_map.tables | |
| if subtable.format == 15 | |
| } | |
| tables = [] | |
| for subtable in character_map.tables: | |
| if subtable.format == 14 and _uvs_has_high_glyph_id(font, subtable): | |
| key = _cmap_subtable_key(subtable) | |
| format15 = format15_by_key.get(key) | |
| if format15 is None: | |
| format15 = _copy_uvs_subtable(15, subtable) | |
| format15_by_key[key] = format15 | |
| tables.append(format15) | |
| else: | |
| format15.ensureDecompiled() | |
| _merge_uvs_dict(format15.uvsDict, subtable.uvsDict) | |
| continue | |
| tables.append(subtable) | |
| character_map.tables = tables | |
| def _lower_beyond64k_cmap_uvs(font: TTFont) -> None: | |
| for tag, character_map in _character_map_tables(font): | |
| format14_by_key = { | |
| _cmap_subtable_key(subtable): subtable | |
| for subtable in character_map.tables | |
| if subtable.format == 14 | |
| } | |
| tables = [] | |
| for subtable in character_map.tables: | |
| if subtable.format == 15: | |
| if _uvs_has_high_glyph_id(font, subtable): | |
| raise ValueError( | |
| f"{tag} format 15 glyph IDs do not fit in format 14" | |
| ) | |
| key = _cmap_subtable_key(subtable) | |
| format14 = format14_by_key.get(key) | |
| if format14 is None: | |
| format14 = _copy_uvs_subtable(14, subtable) | |
| format14_by_key[key] = format14 | |
| tables.append(format14) | |
| else: | |
| format14.ensureDecompiled() | |
| _merge_uvs_dict(format14.uvsDict, subtable.uvsDict) | |
| continue | |
| tables.append(subtable) | |
| character_map.tables = tables | |
| def _drop_beyond64k_cmap_format4(font: TTFont) -> None: | |
| if ( | |
| not any(tag in font for tag in ("cmap", "DMAP")) | |
| or len(font.getGlyphOrder()) <= 0x10000 | |
| ): | |
| return | |
| for _, character_map in _character_map_tables(font): | |
| unicode_bmp_tables = [ | |
| subtable | |
| for subtable in character_map.tables | |
| if subtable.format == 4 and subtable.isUnicode() and not subtable.isSymbol() | |
| ] | |
| if not unicode_bmp_tables: | |
| continue | |
| if not any(subtable.format == 12 for subtable in character_map.tables): | |
| cmap12 = {} | |
| for subtable in unicode_bmp_tables: | |
| cmap12.update(subtable.cmap) | |
| if cmap12: | |
| from fontTools.ttLib.tables._c_m_a_p import CmapSubtable | |
| subtable = CmapSubtable.newSubtable(12) | |
| subtable.platformID = 3 | |
| subtable.platEncID = 10 | |
| subtable.language = 0 | |
| subtable.cmap = cmap12 | |
| character_map.tables.append(subtable) | |
| character_map.tables = [ | |
| subtable | |
| for subtable in character_map.tables | |
| if subtable not in unicode_bmp_tables | |
| ] | |
| def _drop_beyond64k_post_glyph_names(font: TTFont) -> None: | |
| # post 2.0 numberOfGlyphs is uint16: drop above 0xFFFF, not 0x10000. | |
| if "post" not in font or len(font.getGlyphOrder()) <= 0xFFFF: | |
| return | |
| post = font["post"] | |
| if post.formatType == 2.0: | |
| post.formatType = 3.0 | |
| post.extraNames = [] | |
| post.mapping = {} | |
| def _convert_table( | |
| font: TTFont, | |
| source, | |
| source_tag: str, | |
| conversion: _TableConversion, | |
| overwrite: bool, | |
| ) -> None: | |
| destination_tag = conversion.destination | |
| if conversion.transform is not None: | |
| conversion.transform(font, source, overwrite) | |
| if source_tag == destination_tag: | |
| return | |
| destination = newTable(destination_tag) | |
| table_tag = destination.tableTag | |
| destination.__dict__.update(source.__dict__) | |
| destination.tableTag = table_tag | |
| font[destination_tag] = destination | |
| del font[source_tag] | |
| def _convert_tables( | |
| font: TTFont, | |
| *, | |
| tables: Iterable[str] | None, | |
| table_conversions: dict[str, _TableConversion], | |
| validate: bool, | |
| overwrite: bool, | |
| ignore_missing: bool, | |
| validate_conversion=None, | |
| ) -> None: | |
| tables = _normalize_tables(tables) | |
| conversions = { | |
| source: conversion | |
| for source, conversion in table_conversions.items() | |
| if _TABLE_IDENTITIES[source] in tables | |
| } | |
| if validate_conversion is not None: | |
| validate_conversion(font, conversions) | |
| pending = [] | |
| for source_tag, conversion in conversions.items(): | |
| destination_tag = conversion.destination | |
| source_exists = source_tag in font | |
| destination_exists = destination_tag in font | |
| if ( | |
| source_tag != destination_tag | |
| and source_exists | |
| and destination_exists | |
| and not overwrite | |
| ): | |
| raise ValueError( | |
| f"Both {source_tag!r} and {destination_tag!r} exist; " | |
| "set overwrite=True to replace the destination" | |
| ) | |
| if not source_exists: | |
| if destination_exists or ignore_missing: | |
| continue | |
| raise KeyError(source_tag) | |
| pending.append((source_tag, conversion)) | |
| if validate: | |
| result_tags = set(font.keys()) | |
| for source_tag, conversion in pending: | |
| destination_tag = conversion.destination | |
| result_tags.discard(source_tag) | |
| result_tags.add(destination_tag) | |
| lower = sorted(tag for tag in _TABLE_PAIRS if tag in result_tags) | |
| upper = sorted(tag for tag in _TABLE_PAIRS.values() if tag in result_tags) | |
| if lower and upper: | |
| raise ValueError( | |
| "Conversion would create a mixed beyond-64k table family: " | |
| f"lowercase {', '.join(lower)}; uppercase {', '.join(upper)}" | |
| ) | |
| sources = {source_tag: font[source_tag] for source_tag, _ in pending} | |
| for source_tag, conversion in pending: | |
| _convert_table(font, sources[source_tag], source_tag, conversion, overwrite) | |
| if validate: | |
| _validate_family(font) | |
| def upper_tables( | |
| font: TTFont, | |
| *, | |
| tables: Iterable[str] | None = None, | |
| validate: bool = True, | |
| overwrite: bool = False, | |
| ignore_missing: bool = True, | |
| ) -> None: | |
| """Convert selected tables to their beyond-64k companion forms. | |
| Args: | |
| font: Font to modify in place. | |
| tables: Optional iterable of table tags to convert. If omitted, all | |
| supported tables present in ``font`` are converted. Tags may use | |
| either spelling of a companion table family, such as ``"glyf"`` or | |
| ``"GLYF"``. | |
| validate: If true, reject conversions that leave the font with a mixed | |
| lowercase/uppercase companion table family. | |
| overwrite: If true, replace an existing destination table. Otherwise an | |
| existing destination raises ``ValueError``. | |
| ignore_missing: If true, requested source tables that are not present | |
| are ignored unless their destination is already present. Otherwise | |
| missing source tables raise ``KeyError``. | |
| """ | |
| _convert_tables( | |
| font, | |
| tables=tables, | |
| table_conversions=_UPPER_TABLES, | |
| validate=validate, | |
| overwrite=overwrite, | |
| ignore_missing=ignore_missing, | |
| ) | |
| _upper_beyond64k_cmap_uvs(font) | |
| _drop_beyond64k_cmap_format4(font) | |
| _drop_beyond64k_post_glyph_names(font) | |
| def lower_tables( | |
| font: TTFont, | |
| *, | |
| tables: Iterable[str] | None = None, | |
| validate: bool = True, | |
| overwrite: bool = False, | |
| ignore_missing: bool = True, | |
| ) -> None: | |
| """Convert selected tables from their beyond-64k companion forms. | |
| Args are the same as :func:`upper_tables`. In addition to mixed-family | |
| validation, lowering rejects data that cannot be represented in compact | |
| tables, including glyph counts above 65535, cubic ``GLYF`` outlines, and | |
| component glyph IDs that do not fit in ``glyf``. | |
| """ | |
| _convert_tables( | |
| font, | |
| tables=tables, | |
| table_conversions=_LOWER_TABLES, | |
| validate=validate, | |
| overwrite=overwrite, | |
| ignore_missing=ignore_missing, | |
| validate_conversion=_validate_lowering, | |
| ) | |
| _lower_beyond64k_cmap_uvs(font) | |
| def main(args=None): | |
| parser = argparse.ArgumentParser( | |
| "fonttools ttLib.beyond64k", | |
| description=__doc__, | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| ) | |
| parser.add_argument( | |
| "direction", | |
| choices=("upper", "lower"), | |
| help="'upper' converts to beyond-64k companions; 'lower' converts back", | |
| ) | |
| parser.add_argument("input", metavar="INPUT", help="input font path") | |
| parser.add_argument( | |
| "tables", | |
| metavar="TABLE", | |
| nargs="*", | |
| help="optional table tags to convert, e.g. GSUB GPOS glyf", | |
| ) | |
| parser.add_argument("-o", "--output", metavar="OUTPUT", help="output font path") | |
| parser.add_argument( | |
| "--no-validate", | |
| action="store_true", | |
| help="skip mixed-family and lossy-lowering validation", | |
| ) | |
| parser.add_argument( | |
| "--overwrite", | |
| action="store_true", | |
| help="replace destination tables if they already exist", | |
| ) | |
| parser.add_argument( | |
| "--no-ignore-missing", | |
| dest="ignore_missing", | |
| action="store_false", | |
| help="raise an error when a requested source table is missing", | |
| ) | |
| options = parser.parse_args(args) | |
| output = options.output or makeOutputFileName( | |
| options.input, overWrite=True, suffix=f"-{options.direction}" | |
| ) | |
| with TTFont(options.input) as font: | |
| convert = upper_tables if options.direction == "upper" else lower_tables | |
| try: | |
| convert( | |
| font, | |
| tables=options.tables or None, | |
| validate=not options.no_validate, | |
| overwrite=options.overwrite, | |
| ignore_missing=options.ignore_missing, | |
| ) | |
| except (KeyError, ValueError) as error: | |
| raise TTLibError(str(error)) from error | |
| font.save(output) | |
| if __name__ == "__main__": | |
| main() | |