diff options
| author | robot-piglet <[email protected]> | 2025-12-28 13:20:09 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2025-12-28 13:37:00 +0300 |
| commit | 9cb602af1d0734ea260ccc8c9914fd1717e46209 (patch) | |
| tree | 1d387785e5e9fc5adfeee11b5cab4d32e530cf9e /contrib/python/fonttools | |
| parent | 1f49530d91149641b10db4d1771db584eecc34b3 (diff) | |
Intermediate changes
commit_hash:ae3fb6278831e776f34d8ad9de96961a6369f05c
Diffstat (limited to 'contrib/python/fonttools')
14 files changed, 788 insertions, 178 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA index 49fac2b52f5..fcaae0d0409 100644 --- a/contrib/python/fonttools/.dist-info/METADATA +++ b/contrib/python/fonttools/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: fonttools -Version: 4.61.0 +Version: 4.61.1 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Text Processing :: Fonts Classifier: Topic :: Multimedia :: Graphics @@ -391,11 +392,28 @@ Have fun! Changelog ~~~~~~~~~ +4.61.1 (released 2025-12-12) +---------------------------- + +- [otlLib] buildCoverage: return empty Coverage instead of None (#4003, #4004). +- [instancer] bug fix in ``avar2`` full instancing (#4002). +- [designspaceLib] Preserve empty conditionsets when serializing to XML (#4001). +- [fontBu ilder] Fix FontBuilder ``setupOS2()`` default params globally polluted (#3996, #3997). +- [ttFont] Add more typing annotations to ttFont, xmlWriter, sfnt, varLib.models and others (#3952, #3826). +- Explicitly test and declare support for Python 3.14, even though we were already shipping pre-built wheels for it (#3990). + +4.60.2 (released 2025-12-09) +---------------------------- + +- **Backport release** Same as 4.61.0 but without "Drop support for EOL Python 3.9" change to allow + downstream projects still on Python 3.9 to avail of the security fix for CVE-2025-66034 (#3994, #3999). + 4.61.0 (released 2025-11-28) ---------------------------- - [varLib.main]: **SECURITY** Only use basename(vf.filename) to prevent path traversal attacks when - running `fonttools varLib` command. Fixes CVE-2025-66034, see: + running ``fonttools varLib`` command, or code which invokes ``fonttools.varLib.main()``. + Fixes CVE-2025-66034, see: https://github.com/fonttools/fonttools/security/advisories/GHSA-768j-98cg-p3fv. - [feaLib] Sort BaseLangSysRecords by tag (#3986). - Drop support for EOL Python 3.9 (#3982). diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py index 19dc53b5b65..953cda045ee 100644 --- a/contrib/python/fonttools/fontTools/__init__.py +++ b/contrib/python/fonttools/fontTools/__init__.py @@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger log = logging.getLogger(__name__) -version = __version__ = "4.61.0" +version = __version__ = "4.61.1" __all__ = ["version", "log", "configLogger"] diff --git a/contrib/python/fonttools/fontTools/designspaceLib/__init__.py b/contrib/python/fonttools/fontTools/designspaceLib/__init__.py index 0996e7b69e9..4782041b43a 100644 --- a/contrib/python/fonttools/fontTools/designspaceLib/__init__.py +++ b/contrib/python/fonttools/fontTools/designspaceLib/__init__.py @@ -1559,7 +1559,6 @@ class BaseDocWriter(object): return ("%f" % num).rstrip("0").rstrip(".") def _addRule(self, ruleObject): - # if none of the conditions have minimum or maximum values, do not add the rule. ruleElement = ET.Element("rule") if ruleObject.name is not None: ruleElement.attrib["name"] = ruleObject.name @@ -1580,8 +1579,9 @@ class BaseDocWriter(object): cond.get("maximum") ) conditionsetElement.append(conditionElement) - if len(conditionsetElement): - ruleElement.append(conditionsetElement) + # Serialize the conditionset even if it is empty, as this is the + # canonical way of defining a rule that is always true. + ruleElement.append(conditionsetElement) for sub in ruleObject.subs: subElement = ET.Element("sub") subElement.attrib["name"] = sub[0] diff --git a/contrib/python/fonttools/fontTools/fontBuilder.py b/contrib/python/fonttools/fontTools/fontBuilder.py index f8da717babb..b2b93f6f66f 100644 --- a/contrib/python/fonttools/fontTools/fontBuilder.py +++ b/contrib/python/fonttools/fontTools/fontBuilder.py @@ -264,49 +264,49 @@ _nameIDs = dict( # to insert in setupNameTable doc string: # print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1]))) -_panoseDefaults = Panose() -_OS2Defaults = dict( - version=3, - xAvgCharWidth=0, - usWeightClass=400, - usWidthClass=5, - fsType=0x0004, # default: Preview & Print embedding - ySubscriptXSize=0, - ySubscriptYSize=0, - ySubscriptXOffset=0, - ySubscriptYOffset=0, - ySuperscriptXSize=0, - ySuperscriptYSize=0, - ySuperscriptXOffset=0, - ySuperscriptYOffset=0, - yStrikeoutSize=0, - yStrikeoutPosition=0, - sFamilyClass=0, - panose=_panoseDefaults, - ulUnicodeRange1=0, - ulUnicodeRange2=0, - ulUnicodeRange3=0, - ulUnicodeRange4=0, - achVendID="????", - fsSelection=0, - usFirstCharIndex=0, - usLastCharIndex=0, - sTypoAscender=0, - sTypoDescender=0, - sTypoLineGap=0, - usWinAscent=0, - usWinDescent=0, - ulCodePageRange1=0, - ulCodePageRange2=0, - sxHeight=0, - sCapHeight=0, - usDefaultChar=0, # .notdef - usBreakChar=32, # space - usMaxContext=0, - usLowerOpticalPointSize=0, - usUpperOpticalPointSize=0, -) +def _getOS2Defaults(): + return dict( + version=3, + xAvgCharWidth=0, + usWeightClass=400, + usWidthClass=5, + fsType=0x0004, # default: Preview & Print embedding + ySubscriptXSize=0, + ySubscriptYSize=0, + ySubscriptXOffset=0, + ySubscriptYOffset=0, + ySuperscriptXSize=0, + ySuperscriptYSize=0, + ySuperscriptXOffset=0, + ySuperscriptYOffset=0, + yStrikeoutSize=0, + yStrikeoutPosition=0, + sFamilyClass=0, + panose=Panose(), + ulUnicodeRange1=0, + ulUnicodeRange2=0, + ulUnicodeRange3=0, + ulUnicodeRange4=0, + achVendID="????", + fsSelection=0, + usFirstCharIndex=0, + usLastCharIndex=0, + sTypoAscender=0, + sTypoDescender=0, + sTypoLineGap=0, + usWinAscent=0, + usWinDescent=0, + ulCodePageRange1=0, + ulCodePageRange2=0, + sxHeight=0, + sCapHeight=0, + usDefaultChar=0, # .notdef + usBreakChar=32, # space + usMaxContext=0, + usLowerOpticalPointSize=0, + usUpperOpticalPointSize=0, + ) class FontBuilder(object): @@ -493,7 +493,7 @@ class FontBuilder(object): """Create a new `OS/2` table and initialize it with default values, which can be overridden by keyword arguments. """ - self._initTableWithValues("OS/2", _OS2Defaults, values) + self._initTableWithValues("OS/2", _getOS2Defaults(), values) if "xAvgCharWidth" not in values: assert ( "hmtx" in self.font diff --git a/contrib/python/fonttools/fontTools/misc/configTools.py b/contrib/python/fonttools/fontTools/misc/configTools.py index 7eb1854fdf3..c928840a515 100644 --- a/contrib/python/fonttools/fontTools/misc/configTools.py +++ b/contrib/python/fonttools/fontTools/misc/configTools.py @@ -224,7 +224,9 @@ class AbstractConfig(MutableMapping): def __init__( self, - values: Union[AbstractConfig, Dict[Union[Option, str], Any]] = {}, + values: Union[ + AbstractConfig, Dict[Union[Option, str], Any], Mapping[str, Any] + ] = {}, parse_values: bool = False, skip_unknown: bool = False, ): diff --git a/contrib/python/fonttools/fontTools/misc/fixedTools.py b/contrib/python/fonttools/fontTools/misc/fixedTools.py index 330042871c5..b7086c8ac1e 100644 --- a/contrib/python/fonttools/fontTools/misc/fixedTools.py +++ b/contrib/python/fonttools/fontTools/misc/fixedTools.py @@ -39,7 +39,7 @@ __all__ = [ MAX_F2DOT14 = 0x7FFF / (1 << 14) -def fixedToFloat(value, precisionBits): +def fixedToFloat(value: float, precisionBits: int) -> float: """Converts a fixed-point number to a float given the number of precision bits. diff --git a/contrib/python/fonttools/fontTools/misc/xmlWriter.py b/contrib/python/fonttools/fontTools/misc/xmlWriter.py index 240d762aec2..0553e885e74 100644 --- a/contrib/python/fonttools/fontTools/misc/xmlWriter.py +++ b/contrib/python/fonttools/fontTools/misc/xmlWriter.py @@ -1,5 +1,8 @@ """xmlWriter.py -- Simple XML authoring class""" +from __future__ import annotations + +from typing import BinaryIO, Callable, TextIO from fontTools.misc.textTools import byteord, strjoin, tobytes, tostr import sys import os @@ -25,17 +28,22 @@ ILLEGAL_XML_CHARS = dict.fromkeys( class XMLWriter(object): def __init__( self, - fileOrPath, - indentwhite=INDENT, - idlefunc=None, - encoding="utf_8", - newlinestr="\n", - ): + fileOrPath: str | os.PathLike[str] | BinaryIO | TextIO, + indentwhite: str = INDENT, + idlefunc: Callable[[], None] | None = None, + encoding: str = "utf_8", + newlinestr: str | bytes = "\n", + ) -> None: if encoding.lower().replace("-", "").replace("_", "") != "utf8": raise Exception("Only UTF-8 encoding is supported.") if fileOrPath == "-": fileOrPath = sys.stdout + self.filename: str | os.PathLike[str] | None if not hasattr(fileOrPath, "write"): + if not isinstance(fileOrPath, (str, os.PathLike)): + raise TypeError( + "fileOrPath must be a file path (str or PathLike) if it isn't an object with a `write` method." + ) self.filename = fileOrPath self.file = open(fileOrPath, "wb") self._closeStream = True @@ -74,8 +82,9 @@ class XMLWriter(object): def __exit__(self, exception_type, exception_value, traceback): self.close() - def close(self): + def close(self) -> None: if self._closeStream: + assert not isinstance(self.file, (str, os.PathLike)) self.file.close() def write(self, string, indent=True): @@ -196,7 +205,7 @@ def escape(data): if len(data) > maxLen: preview = repr(data[:maxLen])[1:-1] + "..." TTX_LOG.warning( - "Illegal XML character(s) found; replacing offending " "string %r with %r", + "Illegal XML character(s) found; replacing offending string %r with %r", preview, REPLACEMENT, ) diff --git a/contrib/python/fonttools/fontTools/otlLib/builder.py b/contrib/python/fonttools/fontTools/otlLib/builder.py index c4b482f02b4..7e9b707bb33 100644 --- a/contrib/python/fonttools/fontTools/otlLib/builder.py +++ b/contrib/python/fonttools/fontTools/otlLib/builder.py @@ -53,18 +53,21 @@ def buildCoverage(glyphs, glyphMap): ``font.getReverseGlyphMap()``. Returns: - An ``otTables.Coverage`` object or ``None`` if there are no glyphs - supplied. + An ``otTables.Coverage`` object (empty if no glyphs supplied). """ - - if not glyphs: - return None + # Per the OpenType spec: "For cases in which subtable offset fields are not + # documented as permitting NULL values, font compilers must include a subtable + # of the indicated format, even if it is a header stub without further data + # (for example, a coverage table with no glyph IDs)." + # https://github.com/fonttools/fonttools/issues/4003 self = ot.Coverage() - try: - self.glyphs = sorted(set(glyphs), key=glyphMap.__getitem__) - except KeyError as e: - raise ValueError(f"Could not find glyph {e} in font") from e - + if glyphs: + try: + self.glyphs = sorted(set(glyphs), key=glyphMap.__getitem__) + except KeyError as e: + raise ValueError(f"Could not find glyph {e} in font") from e + else: + self.glyphs = [] return self diff --git a/contrib/python/fonttools/fontTools/ttLib/sfnt.py b/contrib/python/fonttools/fontTools/ttLib/sfnt.py index a9d07e615de..d0ed17ae294 100644 --- a/contrib/python/fonttools/fontTools/ttLib/sfnt.py +++ b/contrib/python/fonttools/fontTools/ttLib/sfnt.py @@ -13,6 +13,9 @@ classes, since whenever the number of tables changes or whenever a table's length changes you need to rewrite the whole file anyway. """ +from __future__ import annotations + +from collections.abc import KeysView from io import BytesIO from types import SimpleNamespace from fontTools.misc.textTools import Tag @@ -84,7 +87,7 @@ class SFNTReader(object): if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") - tables = {} + tables: dict[Tag, DirectoryEntry] = {} for i in range(self.numTables): entry = self.DirectoryEntry() entry.fromFile(self.file) @@ -96,15 +99,15 @@ class SFNTReader(object): if self.flavor == "woff": self.flavorData = WOFFFlavorData(self) - def has_key(self, tag): + def has_key(self, tag: str | bytes) -> bool: return tag in self.tables __contains__ = has_key - def keys(self): + def keys(self) -> KeysView[Tag]: return self.tables.keys() - def __getitem__(self, tag): + def __getitem__(self, tag: str | bytes) -> bytes: """Fetch the raw table data.""" entry = self.tables[Tag(tag)] data = entry.loadData(self.file) @@ -122,10 +125,10 @@ class SFNTReader(object): log.warning("bad checksum for '%s' table", tag) return data - def __delitem__(self, tag): + def __delitem__(self, tag: str | bytes) -> None: del self.tables[Tag(tag)] - def close(self): + def close(self) -> None: self.file.close() # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py b/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py index 8b54b47d7e6..cb4593712c9 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from typing import Mapping, TYPE_CHECKING from fontTools.misc import sstruct from fontTools.misc.fixedTools import ( fixedToFloat as fi2fl, @@ -20,6 +23,9 @@ log = logging.getLogger(__name__) from .otBase import BaseTTXConverter +if TYPE_CHECKING: + from ..ttFont import TTFont + class table__a_v_a_r(BaseTTXConverter): """Axis Variations table @@ -143,8 +149,9 @@ class table__a_v_a_r(BaseTTXConverter): else: super().fromXML(name, attrs, content, ttFont) - def renormalizeLocation(self, location, font, dropZeroes=True): - + def renormalizeLocation( + self, location: Mapping[str, float], font: TTFont, dropZeroes: bool = True + ) -> dict[str, float]: majorVersion = getattr(self, "majorVersion", 1) if majorVersion not in (1, 2): diff --git a/contrib/python/fonttools/fontTools/ttLib/ttFont.py b/contrib/python/fonttools/fontTools/ttLib/ttFont.py index 2b3338e5784..4407e567a36 100644 --- a/contrib/python/fonttools/fontTools/ttLib/ttFont.py +++ b/contrib/python/fonttools/fontTools/ttLib/ttFont.py @@ -1,24 +1,130 @@ +from __future__ import annotations + +import logging +import os +import traceback +from io import BytesIO, StringIO, UnsupportedOperation +from typing import TYPE_CHECKING, TypedDict, TypeVar, overload + from fontTools.config import Config from fontTools.misc import xmlWriter from fontTools.misc.configTools import AbstractConfig -from fontTools.misc.textTools import Tag, byteord, tostr from fontTools.misc.loggingTools import deprecateArgument +from fontTools.misc.textTools import Tag, byteord, tostr from fontTools.ttLib import TTLibError +from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter from fontTools.ttLib.ttGlyphSet import ( - _TTGlyph, + _TTGlyph, # noqa: F401 + _TTGlyphSet, _TTGlyphSetCFF, _TTGlyphSetGlyf, _TTGlyphSetVARC, ) -from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter -from io import BytesIO, StringIO, UnsupportedOperation -import os -import logging -import traceback + +if TYPE_CHECKING: + from collections.abc import Mapping, MutableMapping + from types import ModuleType, TracebackType + from typing import Any, BinaryIO, Literal, Sequence, TextIO + + from typing_extensions import Self, Unpack + + from fontTools.ttLib.tables import ( + B_A_S_E_, + C_B_D_T_, + C_B_L_C_, + C_F_F_, + C_F_F__2, + C_O_L_R_, + C_P_A_L_, + D_S_I_G_, + E_B_D_T_, + E_B_L_C_, + F_F_T_M_, + G_D_E_F_, + G_M_A_P_, + G_P_K_G_, + G_P_O_S_, + G_S_U_B_, + G_V_A_R_, + H_V_A_R_, + J_S_T_F_, + L_T_S_H_, + M_A_T_H_, + M_E_T_A_, + M_V_A_R_, + S_I_N_G_, + S_T_A_T_, + S_V_G_, + T_S_I__0, + T_S_I__1, + T_S_I__2, + T_S_I__3, + T_S_I__5, + T_S_I_B_, + T_S_I_C_, + T_S_I_D_, + T_S_I_J_, + T_S_I_P_, + T_S_I_S_, + T_S_I_V_, + T_T_F_A_, + V_A_R_C_, + V_D_M_X_, + V_O_R_G_, + V_V_A_R_, + D__e_b_g, + F__e_a_t, + G__l_a_t, + G__l_o_c, + O_S_2f_2, + S__i_l_f, + S__i_l_l, + _a_n_k_r, + _a_v_a_r, + _b_s_l_n, + _c_i_d_g, + _c_m_a_p, + _c_v_a_r, + _c_v_t, + _f_e_a_t, + _f_p_g_m, + _f_v_a_r, + _g_a_s_p, + _g_c_i_d, + _g_l_y_f, + _g_v_a_r, + _h_d_m_x, + _h_e_a_d, + _h_h_e_a, + _h_m_t_x, + _k_e_r_n, + _l_c_a_r, + _l_o_c_a, + _l_t_a_g, + _m_a_x_p, + _m_e_t_a, + _m_o_r_t, + _m_o_r_x, + _n_a_m_e, + _o_p_b_d, + _p_o_s_t, + _p_r_e_p, + _p_r_o_p, + _s_b_i_x, + _t_r_a_k, + _v_h_e_a, + _v_m_t_x, + ) + from fontTools.ttLib.tables.DefaultTable import DefaultTable + + _VT_co = TypeVar("_VT_co", covariant=True) # Value type covariant containers. log = logging.getLogger(__name__) +_NumberT = TypeVar("_NumberT", bound=float) + + class TTFont(object): """Represents a TrueType font. @@ -103,24 +209,44 @@ class TTFont(object): The default is ``lazy=None`` which is somewhere in between. """ + tables: dict[Tag, DefaultTable | GlyphOrder] + reader: SFNTReader | None + sfntVersion: str + flavor: str | None + flavorData: Any | None + lazy: bool | None + recalcBBoxes: bool + recalcTimestamp: bool + ignoreDecompileErrors: bool + cfg: AbstractConfig + glyphOrder: list[str] + _reverseGlyphOrderDict: dict[str, int] + _tableCache: MutableMapping[tuple[Tag, bytes], DefaultTable] | None + disassembleInstructions: bool + bitmapGlyphDataFormat: str + # Deprecated attributes + verbose: bool | None + quiet: bool | None + def __init__( self, - file=None, - res_name_or_index=None, - sfntVersion="\000\001\000\000", - flavor=None, - checkChecksums=0, - verbose=None, - recalcBBoxes=True, - allowVID=NotImplemented, - ignoreDecompileErrors=False, - recalcTimestamp=True, - fontNumber=-1, - lazy=None, - quiet=None, - _tableCache=None, - cfg={}, - ): + file: str | os.PathLike[str] | BinaryIO | None = None, + res_name_or_index: str | int | None = None, + sfntVersion: str = "\000\001\000\000", + flavor: str | None = None, + checkChecksums: int = 0, + verbose: bool | None = None, # Deprecated + recalcBBoxes: bool = True, + allowVID: Any = NotImplemented, # Deprecated/Unused + ignoreDecompileErrors: bool = False, + recalcTimestamp: bool = True, + fontNumber: int = -1, + lazy: bool | None = None, + quiet: bool | None = None, # Deprecated + _tableCache: MutableMapping[tuple[Tag, bytes], DefaultTable] | None = None, + cfg: Mapping[str, Any] | AbstractConfig = {}, + ) -> None: + # Set deprecated attributes for name in ("verbose", "quiet"): val = locals().get(name) if val is not None: @@ -142,6 +268,10 @@ class TTFont(object): return seekable = True if not hasattr(file, "read"): + if not isinstance(file, (str, os.PathLike)): + raise TypeError( + "fileOrPath must be a file path (str or PathLike) if it isn't an object with a `read` method." + ) closeStream = True # assume file is a string if res_name_or_index is not None: @@ -160,6 +290,7 @@ class TTFont(object): file = open(file, "rb") else: # assume "file" is a readable file object + assert not isinstance(file, (str, os.PathLike)) closeStream = False # SFNTReader wants the input file to be seekable. # SpooledTemporaryFile has no seekable() on < 3.11, but still can seek: @@ -191,23 +322,31 @@ class TTFont(object): self.flavor = self.reader.flavor self.flavorData = self.reader.flavorData - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, type, value, traceback): + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: self.close() - def close(self): + def close(self) -> None: """If we still have a reader object, close it.""" if self.reader is not None: self.reader.close() + self.reader = None - def save(self, file, reorderTables=True): + def save( + self, file: str | os.PathLike[str] | BinaryIO, reorderTables: bool | None = True + ) -> None: """Save the font to disk. Args: file: Similarly to the constructor, can be either a pathname or a writable - file object. + binary file object. reorderTables (Option[bool]): If true (the default), reorder the tables, sorting them by tag (recommended by the OpenType specification). If false, retain the original font order. If None, reorder by table @@ -232,6 +371,10 @@ class TTFont(object): ): if reorderTables is False: # sort tables using the original font's order + if self.reader is None: + raise TTLibError( + "The original table order is unavailable because there isn't a font to read it from." + ) tableOrder = list(self.reader.keys()) else: # use the recommended order from the OpenType specification @@ -244,20 +387,25 @@ class TTFont(object): if createStream: # "file" is a path + assert isinstance(file, (str, os.PathLike)) with open(file, "wb") as file: file.write(tmp.getvalue()) else: + assert not isinstance(file, (str, os.PathLike)) file.write(tmp.getvalue()) tmp.close() - def _save(self, file, tableCache=None): + def _save( + self, + file: BinaryIO, + tableCache: MutableMapping[tuple[Tag, bytes], Any] | None = None, + ) -> bool: """Internal function, to be shared by save() and TTCollection.save()""" if self.recalcTimestamp and "head" in self: - self[ - "head" - ] # make sure 'head' is loaded so the recalculation is actually done + # make sure 'head' is loaded so the recalculation is actually done + self["head"] tags = self.keys() tags.pop(0) # skip GlyphOrder tag @@ -275,7 +423,22 @@ class TTFont(object): return writer.reordersTables() - def saveXML(self, fileOrPath, newlinestr="\n", **kwargs): + class XMLSavingOptions(TypedDict): + writeVersion: bool + quiet: bool | None + tables: Sequence[str | bytes] | None + skipTables: Sequence[str] | None + splitTables: bool + splitGlyphs: bool + disassembleInstructions: bool + bitmapGlyphDataFormat: str + + def saveXML( + self, + fileOrPath: str | os.PathLike[str] | BinaryIO | TextIO, + newlinestr: str = "\n", + **kwargs: Unpack[XMLSavingOptions], + ) -> None: """Export the font as TTX (an XML-based text file), or as a series of text files when splitTables is true. In the latter case, the 'fileOrPath' argument should be a path to a directory. @@ -290,16 +453,16 @@ class TTFont(object): def _saveXML( self, - writer, - writeVersion=True, - quiet=None, - tables=None, - skipTables=None, - splitTables=False, - splitGlyphs=False, - disassembleInstructions=True, - bitmapGlyphDataFormat="raw", - ): + writer: xmlWriter.XMLWriter, + writeVersion: bool = True, + quiet: bool | None = None, # Deprecated + tables: Sequence[str | bytes] | None = None, + skipTables: Sequence[str] | None = None, + splitTables: bool = False, + splitGlyphs: bool = False, + disassembleInstructions: bool = True, + bitmapGlyphDataFormat: str = "raw", + ) -> None: if quiet is not None: deprecateArgument("quiet", "configure logging instead") @@ -329,6 +492,10 @@ class TTFont(object): if not splitTables: writer.newline() else: + if writer.filename is None: + raise TTLibError( + "splitTables requires the file name to be a file system path, not a stream." + ) path, ext = os.path.splitext(writer.filename) for tag in tables: @@ -352,7 +519,13 @@ class TTFont(object): writer.endtag("ttFont") writer.newline() - def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False): + def _tableToXML( + self, + writer: xmlWriter.XMLWriter, + tag: str | bytes, + quiet: bool | None = None, + splitGlyphs: bool = False, + ) -> None: if quiet is not None: deprecateArgument("quiet", "configure logging instead") if tag in self: @@ -364,7 +537,7 @@ class TTFont(object): if tag not in self: return xmlTag = tagToXML(tag) - attrs = dict() + attrs: dict[str, Any] = {} if hasattr(table, "ERROR"): attrs["ERROR"] = "decompilation error" from .tables.DefaultTable import DefaultTable @@ -381,7 +554,9 @@ class TTFont(object): writer.newline() writer.newline() - def importXML(self, fileOrPath, quiet=None): + def importXML( + self, fileOrPath: str | os.PathLike[str] | BinaryIO, quiet: bool | None = None + ) -> None: """Import a TTX file (an XML-based text format), so as to recreate a font object. """ @@ -400,12 +575,12 @@ class TTFont(object): reader = xmlReader.XMLReader(fileOrPath, self) reader.read() - def isLoaded(self, tag): + def isLoaded(self, tag: str | bytes) -> bool: """Return true if the table identified by ``tag`` has been decompiled and loaded into memory.""" return tag in self.tables - def has_key(self, tag): + def has_key(self, tag: str | bytes) -> bool: """Test if the table identified by ``tag`` is present in the font. As well as this method, ``tag in font`` can also be used to determine the @@ -421,7 +596,7 @@ class TTFont(object): __contains__ = has_key - def keys(self): + def keys(self) -> list[str]: """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table.""" keys = list(self.tables.keys()) if self.reader: @@ -434,7 +609,7 @@ class TTFont(object): keys = sortedTagList(keys) return ["GlyphOrder"] + keys - def ensureDecompiled(self, recurse=None): + def ensureDecompiled(self, recurse: bool | None = None) -> None: """Decompile all the tables, even if a TTFont was opened in 'lazy' mode.""" for tag in self.keys(): table = self[tag] @@ -444,10 +619,185 @@ class TTFont(object): table.ensureDecompiled(recurse=recurse) self.lazy = False - def __len__(self): + def __len__(self) -> int: return len(list(self.keys())) - def __getitem__(self, tag): + @overload + def __getitem__(self, tag: Literal["BASE"]) -> B_A_S_E_.table_B_A_S_E_: ... + @overload + def __getitem__(self, tag: Literal["CBDT"]) -> C_B_D_T_.table_C_B_D_T_: ... + @overload + def __getitem__(self, tag: Literal["CBLC"]) -> C_B_L_C_.table_C_B_L_C_: ... + @overload + def __getitem__(self, tag: Literal["CFF "]) -> C_F_F_.table_C_F_F_: ... + @overload + def __getitem__(self, tag: Literal["CFF2"]) -> C_F_F__2.table_C_F_F__2: ... + @overload + def __getitem__(self, tag: Literal["COLR"]) -> C_O_L_R_.table_C_O_L_R_: ... + @overload + def __getitem__(self, tag: Literal["CPAL"]) -> C_P_A_L_.table_C_P_A_L_: ... + @overload + def __getitem__(self, tag: Literal["DSIG"]) -> D_S_I_G_.table_D_S_I_G_: ... + @overload + def __getitem__(self, tag: Literal["EBDT"]) -> E_B_D_T_.table_E_B_D_T_: ... + @overload + def __getitem__(self, tag: Literal["EBLC"]) -> E_B_L_C_.table_E_B_L_C_: ... + @overload + def __getitem__(self, tag: Literal["FFTM"]) -> F_F_T_M_.table_F_F_T_M_: ... + @overload + def __getitem__(self, tag: Literal["GDEF"]) -> G_D_E_F_.table_G_D_E_F_: ... + @overload + def __getitem__(self, tag: Literal["GMAP"]) -> G_M_A_P_.table_G_M_A_P_: ... + @overload + def __getitem__(self, tag: Literal["GPKG"]) -> G_P_K_G_.table_G_P_K_G_: ... + @overload + def __getitem__(self, tag: Literal["GPOS"]) -> G_P_O_S_.table_G_P_O_S_: ... + @overload + def __getitem__(self, tag: Literal["GSUB"]) -> G_S_U_B_.table_G_S_U_B_: ... + @overload + def __getitem__(self, tag: Literal["GVAR"]) -> G_V_A_R_.table_G_V_A_R_: ... + @overload + def __getitem__(self, tag: Literal["HVAR"]) -> H_V_A_R_.table_H_V_A_R_: ... + @overload + def __getitem__(self, tag: Literal["JSTF"]) -> J_S_T_F_.table_J_S_T_F_: ... + @overload + def __getitem__(self, tag: Literal["LTSH"]) -> L_T_S_H_.table_L_T_S_H_: ... + @overload + def __getitem__(self, tag: Literal["MATH"]) -> M_A_T_H_.table_M_A_T_H_: ... + @overload + def __getitem__(self, tag: Literal["META"]) -> M_E_T_A_.table_M_E_T_A_: ... + @overload + def __getitem__(self, tag: Literal["MVAR"]) -> M_V_A_R_.table_M_V_A_R_: ... + @overload + def __getitem__(self, tag: Literal["SING"]) -> S_I_N_G_.table_S_I_N_G_: ... + @overload + def __getitem__(self, tag: Literal["STAT"]) -> S_T_A_T_.table_S_T_A_T_: ... + @overload + def __getitem__(self, tag: Literal["SVG "]) -> S_V_G_.table_S_V_G_: ... + @overload + def __getitem__(self, tag: Literal["TSI0"]) -> T_S_I__0.table_T_S_I__0: ... + @overload + def __getitem__(self, tag: Literal["TSI1"]) -> T_S_I__1.table_T_S_I__1: ... + @overload + def __getitem__(self, tag: Literal["TSI2"]) -> T_S_I__2.table_T_S_I__2: ... + @overload + def __getitem__(self, tag: Literal["TSI3"]) -> T_S_I__3.table_T_S_I__3: ... + @overload + def __getitem__(self, tag: Literal["TSI5"]) -> T_S_I__5.table_T_S_I__5: ... + @overload + def __getitem__(self, tag: Literal["TSIB"]) -> T_S_I_B_.table_T_S_I_B_: ... + @overload + def __getitem__(self, tag: Literal["TSIC"]) -> T_S_I_C_.table_T_S_I_C_: ... + @overload + def __getitem__(self, tag: Literal["TSID"]) -> T_S_I_D_.table_T_S_I_D_: ... + @overload + def __getitem__(self, tag: Literal["TSIJ"]) -> T_S_I_J_.table_T_S_I_J_: ... + @overload + def __getitem__(self, tag: Literal["TSIP"]) -> T_S_I_P_.table_T_S_I_P_: ... + @overload + def __getitem__(self, tag: Literal["TSIS"]) -> T_S_I_S_.table_T_S_I_S_: ... + @overload + def __getitem__(self, tag: Literal["TSIV"]) -> T_S_I_V_.table_T_S_I_V_: ... + @overload + def __getitem__(self, tag: Literal["TTFA"]) -> T_T_F_A_.table_T_T_F_A_: ... + @overload + def __getitem__(self, tag: Literal["VARC"]) -> V_A_R_C_.table_V_A_R_C_: ... + @overload + def __getitem__(self, tag: Literal["VDMX"]) -> V_D_M_X_.table_V_D_M_X_: ... + @overload + def __getitem__(self, tag: Literal["VORG"]) -> V_O_R_G_.table_V_O_R_G_: ... + @overload + def __getitem__(self, tag: Literal["VVAR"]) -> V_V_A_R_.table_V_V_A_R_: ... + @overload + def __getitem__(self, tag: Literal["Debg"]) -> D__e_b_g.table_D__e_b_g: ... + @overload + def __getitem__(self, tag: Literal["Feat"]) -> F__e_a_t.table_F__e_a_t: ... + @overload + def __getitem__(self, tag: Literal["Glat"]) -> G__l_a_t.table_G__l_a_t: ... + @overload + def __getitem__(self, tag: Literal["Gloc"]) -> G__l_o_c.table_G__l_o_c: ... + @overload + def __getitem__(self, tag: Literal["OS/2"]) -> O_S_2f_2.table_O_S_2f_2: ... + @overload + def __getitem__(self, tag: Literal["Silf"]) -> S__i_l_f.table_S__i_l_f: ... + @overload + def __getitem__(self, tag: Literal["Sill"]) -> S__i_l_l.table_S__i_l_l: ... + @overload + def __getitem__(self, tag: Literal["ankr"]) -> _a_n_k_r.table__a_n_k_r: ... + @overload + def __getitem__(self, tag: Literal["avar"]) -> _a_v_a_r.table__a_v_a_r: ... + @overload + def __getitem__(self, tag: Literal["bsln"]) -> _b_s_l_n.table__b_s_l_n: ... + @overload + def __getitem__(self, tag: Literal["cidg"]) -> _c_i_d_g.table__c_i_d_g: ... + @overload + def __getitem__(self, tag: Literal["cmap"]) -> _c_m_a_p.table__c_m_a_p: ... + @overload + def __getitem__(self, tag: Literal["cvar"]) -> _c_v_a_r.table__c_v_a_r: ... + @overload + def __getitem__(self, tag: Literal["cvt "]) -> _c_v_t.table__c_v_t: ... + @overload + def __getitem__(self, tag: Literal["feat"]) -> _f_e_a_t.table__f_e_a_t: ... + @overload + def __getitem__(self, tag: Literal["fpgm"]) -> _f_p_g_m.table__f_p_g_m: ... + @overload + def __getitem__(self, tag: Literal["fvar"]) -> _f_v_a_r.table__f_v_a_r: ... + @overload + def __getitem__(self, tag: Literal["gasp"]) -> _g_a_s_p.table__g_a_s_p: ... + @overload + def __getitem__(self, tag: Literal["gcid"]) -> _g_c_i_d.table__g_c_i_d: ... + @overload + def __getitem__(self, tag: Literal["glyf"]) -> _g_l_y_f.table__g_l_y_f: ... + @overload + def __getitem__(self, tag: Literal["gvar"]) -> _g_v_a_r.table__g_v_a_r: ... + @overload + def __getitem__(self, tag: Literal["hdmx"]) -> _h_d_m_x.table__h_d_m_x: ... + @overload + def __getitem__(self, tag: Literal["head"]) -> _h_e_a_d.table__h_e_a_d: ... + @overload + def __getitem__(self, tag: Literal["hhea"]) -> _h_h_e_a.table__h_h_e_a: ... + @overload + def __getitem__(self, tag: Literal["hmtx"]) -> _h_m_t_x.table__h_m_t_x: ... + @overload + def __getitem__(self, tag: Literal["kern"]) -> _k_e_r_n.table__k_e_r_n: ... + @overload + def __getitem__(self, tag: Literal["lcar"]) -> _l_c_a_r.table__l_c_a_r: ... + @overload + def __getitem__(self, tag: Literal["loca"]) -> _l_o_c_a.table__l_o_c_a: ... + @overload + def __getitem__(self, tag: Literal["ltag"]) -> _l_t_a_g.table__l_t_a_g: ... + @overload + def __getitem__(self, tag: Literal["maxp"]) -> _m_a_x_p.table__m_a_x_p: ... + @overload + def __getitem__(self, tag: Literal["meta"]) -> _m_e_t_a.table__m_e_t_a: ... + @overload + def __getitem__(self, tag: Literal["mort"]) -> _m_o_r_t.table__m_o_r_t: ... + @overload + def __getitem__(self, tag: Literal["morx"]) -> _m_o_r_x.table__m_o_r_x: ... + @overload + def __getitem__(self, tag: Literal["name"]) -> _n_a_m_e.table__n_a_m_e: ... + @overload + def __getitem__(self, tag: Literal["opbd"]) -> _o_p_b_d.table__o_p_b_d: ... + @overload + def __getitem__(self, tag: Literal["post"]) -> _p_o_s_t.table__p_o_s_t: ... + @overload + def __getitem__(self, tag: Literal["prep"]) -> _p_r_e_p.table__p_r_e_p: ... + @overload + def __getitem__(self, tag: Literal["prop"]) -> _p_r_o_p.table__p_r_o_p: ... + @overload + def __getitem__(self, tag: Literal["sbix"]) -> _s_b_i_x.table__s_b_i_x: ... + @overload + def __getitem__(self, tag: Literal["trak"]) -> _t_r_a_k.table__t_r_a_k: ... + @overload + def __getitem__(self, tag: Literal["vhea"]) -> _v_h_e_a.table__v_h_e_a: ... + @overload + def __getitem__(self, tag: Literal["vmtx"]) -> _v_m_t_x.table__v_m_t_x: ... + @overload + def __getitem__(self, tag: Literal["GlyphOrder"]) -> GlyphOrder: ... + @overload + def __getitem__(self, tag: str | bytes) -> DefaultTable | GlyphOrder: ... + + def __getitem__(self, tag: str | bytes) -> DefaultTable | GlyphOrder: tag = Tag(tag) table = self.tables.get(tag) if table is None: @@ -460,8 +810,9 @@ class TTFont(object): raise KeyError("'%s' table not found" % tag) return table - def _readTable(self, tag): + def _readTable(self, tag: Tag) -> DefaultTable: log.debug("Reading '%s' table from disk", tag) + assert self.reader is not None data = self.reader[tag] if self._tableCache is not None: table = self._tableCache.get((tag, data)) @@ -492,10 +843,10 @@ class TTFont(object): self._tableCache[(tag, data)] = table return table - def __setitem__(self, tag, table): + def __setitem__(self, tag: str | bytes, table: DefaultTable) -> None: self.tables[Tag(tag)] = table - def __delitem__(self, tag): + def __delitem__(self, tag: str | bytes) -> None: if tag not in self: raise KeyError("'%s' table not found" % tag) if tag in self.tables: @@ -503,14 +854,195 @@ class TTFont(object): if self.reader and tag in self.reader: del self.reader[tag] - def get(self, tag, default=None): + @overload + def get(self, tag: Literal["BASE"]) -> B_A_S_E_.table_B_A_S_E_ | None: ... + @overload + def get(self, tag: Literal["CBDT"]) -> C_B_D_T_.table_C_B_D_T_ | None: ... + @overload + def get(self, tag: Literal["CBLC"]) -> C_B_L_C_.table_C_B_L_C_ | None: ... + @overload + def get(self, tag: Literal["CFF "]) -> C_F_F_.table_C_F_F_ | None: ... + @overload + def get(self, tag: Literal["CFF2"]) -> C_F_F__2.table_C_F_F__2 | None: ... + @overload + def get(self, tag: Literal["COLR"]) -> C_O_L_R_.table_C_O_L_R_ | None: ... + @overload + def get(self, tag: Literal["CPAL"]) -> C_P_A_L_.table_C_P_A_L_ | None: ... + @overload + def get(self, tag: Literal["DSIG"]) -> D_S_I_G_.table_D_S_I_G_ | None: ... + @overload + def get(self, tag: Literal["EBDT"]) -> E_B_D_T_.table_E_B_D_T_ | None: ... + @overload + def get(self, tag: Literal["EBLC"]) -> E_B_L_C_.table_E_B_L_C_ | None: ... + @overload + def get(self, tag: Literal["FFTM"]) -> F_F_T_M_.table_F_F_T_M_ | None: ... + @overload + def get(self, tag: Literal["GDEF"]) -> G_D_E_F_.table_G_D_E_F_ | None: ... + @overload + def get(self, tag: Literal["GMAP"]) -> G_M_A_P_.table_G_M_A_P_ | None: ... + @overload + def get(self, tag: Literal["GPKG"]) -> G_P_K_G_.table_G_P_K_G_ | None: ... + @overload + def get(self, tag: Literal["GPOS"]) -> G_P_O_S_.table_G_P_O_S_ | None: ... + @overload + def get(self, tag: Literal["GSUB"]) -> G_S_U_B_.table_G_S_U_B_ | None: ... + @overload + def get(self, tag: Literal["GVAR"]) -> G_V_A_R_.table_G_V_A_R_ | None: ... + @overload + def get(self, tag: Literal["HVAR"]) -> H_V_A_R_.table_H_V_A_R_ | None: ... + @overload + def get(self, tag: Literal["JSTF"]) -> J_S_T_F_.table_J_S_T_F_ | None: ... + @overload + def get(self, tag: Literal["LTSH"]) -> L_T_S_H_.table_L_T_S_H_ | None: ... + @overload + def get(self, tag: Literal["MATH"]) -> M_A_T_H_.table_M_A_T_H_ | None: ... + @overload + def get(self, tag: Literal["META"]) -> M_E_T_A_.table_M_E_T_A_ | None: ... + @overload + def get(self, tag: Literal["MVAR"]) -> M_V_A_R_.table_M_V_A_R_ | None: ... + @overload + def get(self, tag: Literal["SING"]) -> S_I_N_G_.table_S_I_N_G_ | None: ... + @overload + def get(self, tag: Literal["STAT"]) -> S_T_A_T_.table_S_T_A_T_ | None: ... + @overload + def get(self, tag: Literal["SVG "]) -> S_V_G_.table_S_V_G_ | None: ... + @overload + def get(self, tag: Literal["TSI0"]) -> T_S_I__0.table_T_S_I__0 | None: ... + @overload + def get(self, tag: Literal["TSI1"]) -> T_S_I__1.table_T_S_I__1 | None: ... + @overload + def get(self, tag: Literal["TSI2"]) -> T_S_I__2.table_T_S_I__2 | None: ... + @overload + def get(self, tag: Literal["TSI3"]) -> T_S_I__3.table_T_S_I__3 | None: ... + @overload + def get(self, tag: Literal["TSI5"]) -> T_S_I__5.table_T_S_I__5 | None: ... + @overload + def get(self, tag: Literal["TSIB"]) -> T_S_I_B_.table_T_S_I_B_ | None: ... + @overload + def get(self, tag: Literal["TSIC"]) -> T_S_I_C_.table_T_S_I_C_ | None: ... + @overload + def get(self, tag: Literal["TSID"]) -> T_S_I_D_.table_T_S_I_D_ | None: ... + @overload + def get(self, tag: Literal["TSIJ"]) -> T_S_I_J_.table_T_S_I_J_ | None: ... + @overload + def get(self, tag: Literal["TSIP"]) -> T_S_I_P_.table_T_S_I_P_ | None: ... + @overload + def get(self, tag: Literal["TSIS"]) -> T_S_I_S_.table_T_S_I_S_ | None: ... + @overload + def get(self, tag: Literal["TSIV"]) -> T_S_I_V_.table_T_S_I_V_ | None: ... + @overload + def get(self, tag: Literal["TTFA"]) -> T_T_F_A_.table_T_T_F_A_ | None: ... + @overload + def get(self, tag: Literal["VARC"]) -> V_A_R_C_.table_V_A_R_C_ | None: ... + @overload + def get(self, tag: Literal["VDMX"]) -> V_D_M_X_.table_V_D_M_X_ | None: ... + @overload + def get(self, tag: Literal["VORG"]) -> V_O_R_G_.table_V_O_R_G_ | None: ... + @overload + def get(self, tag: Literal["VVAR"]) -> V_V_A_R_.table_V_V_A_R_ | None: ... + @overload + def get(self, tag: Literal["Debg"]) -> D__e_b_g.table_D__e_b_g | None: ... + @overload + def get(self, tag: Literal["Feat"]) -> F__e_a_t.table_F__e_a_t | None: ... + @overload + def get(self, tag: Literal["Glat"]) -> G__l_a_t.table_G__l_a_t | None: ... + @overload + def get(self, tag: Literal["Gloc"]) -> G__l_o_c.table_G__l_o_c | None: ... + @overload + def get(self, tag: Literal["OS/2"]) -> O_S_2f_2.table_O_S_2f_2 | None: ... + @overload + def get(self, tag: Literal["Silf"]) -> S__i_l_f.table_S__i_l_f | None: ... + @overload + def get(self, tag: Literal["Sill"]) -> S__i_l_l.table_S__i_l_l | None: ... + @overload + def get(self, tag: Literal["ankr"]) -> _a_n_k_r.table__a_n_k_r | None: ... + @overload + def get(self, tag: Literal["avar"]) -> _a_v_a_r.table__a_v_a_r | None: ... + @overload + def get(self, tag: Literal["bsln"]) -> _b_s_l_n.table__b_s_l_n | None: ... + @overload + def get(self, tag: Literal["cidg"]) -> _c_i_d_g.table__c_i_d_g | None: ... + @overload + def get(self, tag: Literal["cmap"]) -> _c_m_a_p.table__c_m_a_p | None: ... + @overload + def get(self, tag: Literal["cvar"]) -> _c_v_a_r.table__c_v_a_r | None: ... + @overload + def get(self, tag: Literal["cvt "]) -> _c_v_t.table__c_v_t | None: ... + @overload + def get(self, tag: Literal["feat"]) -> _f_e_a_t.table__f_e_a_t | None: ... + @overload + def get(self, tag: Literal["fpgm"]) -> _f_p_g_m.table__f_p_g_m | None: ... + @overload + def get(self, tag: Literal["fvar"]) -> _f_v_a_r.table__f_v_a_r | None: ... + @overload + def get(self, tag: Literal["gasp"]) -> _g_a_s_p.table__g_a_s_p | None: ... + @overload + def get(self, tag: Literal["gcid"]) -> _g_c_i_d.table__g_c_i_d | None: ... + @overload + def get(self, tag: Literal["glyf"]) -> _g_l_y_f.table__g_l_y_f | None: ... + @overload + def get(self, tag: Literal["gvar"]) -> _g_v_a_r.table__g_v_a_r | None: ... + @overload + def get(self, tag: Literal["hdmx"]) -> _h_d_m_x.table__h_d_m_x | None: ... + @overload + def get(self, tag: Literal["head"]) -> _h_e_a_d.table__h_e_a_d | None: ... + @overload + def get(self, tag: Literal["hhea"]) -> _h_h_e_a.table__h_h_e_a | None: ... + @overload + def get(self, tag: Literal["hmtx"]) -> _h_m_t_x.table__h_m_t_x | None: ... + @overload + def get(self, tag: Literal["kern"]) -> _k_e_r_n.table__k_e_r_n | None: ... + @overload + def get(self, tag: Literal["lcar"]) -> _l_c_a_r.table__l_c_a_r | None: ... + @overload + def get(self, tag: Literal["loca"]) -> _l_o_c_a.table__l_o_c_a | None: ... + @overload + def get(self, tag: Literal["ltag"]) -> _l_t_a_g.table__l_t_a_g | None: ... + @overload + def get(self, tag: Literal["maxp"]) -> _m_a_x_p.table__m_a_x_p | None: ... + @overload + def get(self, tag: Literal["meta"]) -> _m_e_t_a.table__m_e_t_a | None: ... + @overload + def get(self, tag: Literal["mort"]) -> _m_o_r_t.table__m_o_r_t | None: ... + @overload + def get(self, tag: Literal["morx"]) -> _m_o_r_x.table__m_o_r_x | None: ... + @overload + def get(self, tag: Literal["name"]) -> _n_a_m_e.table__n_a_m_e | None: ... + @overload + def get(self, tag: Literal["opbd"]) -> _o_p_b_d.table__o_p_b_d | None: ... + @overload + def get(self, tag: Literal["post"]) -> _p_o_s_t.table__p_o_s_t | None: ... + @overload + def get(self, tag: Literal["prep"]) -> _p_r_e_p.table__p_r_e_p | None: ... + @overload + def get(self, tag: Literal["prop"]) -> _p_r_o_p.table__p_r_o_p | None: ... + @overload + def get(self, tag: Literal["sbix"]) -> _s_b_i_x.table__s_b_i_x | None: ... + @overload + def get(self, tag: Literal["trak"]) -> _t_r_a_k.table__t_r_a_k | None: ... + @overload + def get(self, tag: Literal["vhea"]) -> _v_h_e_a.table__v_h_e_a | None: ... + @overload + def get(self, tag: Literal["vmtx"]) -> _v_m_t_x.table__v_m_t_x | None: ... + @overload + def get(self, tag: Literal["GlyphOrder"]) -> GlyphOrder: ... + @overload + def get(self, tag: str | bytes) -> DefaultTable | GlyphOrder | Any | None: ... + @overload + def get( + self, tag: str | bytes, default: _VT_co + ) -> DefaultTable | GlyphOrder | Any | _VT_co: ... + + def get( + self, tag: str | bytes, default: Any | None = None + ) -> DefaultTable | GlyphOrder | Any | None: """Returns the table if it exists or (optionally) a default if it doesn't.""" try: return self[tag] except KeyError: return default - def setGlyphOrder(self, glyphOrder): + def setGlyphOrder(self, glyphOrder: list[str]) -> None: """Set the glyph order Args: @@ -522,7 +1054,7 @@ class TTFont(object): if self.isLoaded("glyf"): self["glyf"].setGlyphOrder(glyphOrder) - def getGlyphOrder(self): + def getGlyphOrder(self) -> list[str]: """Returns a list of glyph names ordered by their position in the font.""" try: return self.glyphOrder @@ -557,7 +1089,7 @@ class TTFont(object): self._getGlyphNamesFromCmap() return self.glyphOrder - def _getGlyphNamesFromCmap(self): + def _getGlyphNamesFromCmap(self) -> None: # # This is rather convoluted, but then again, it's an interesting problem: # - we need to use the unicode values found in the cmap table to @@ -623,7 +1155,7 @@ class TTFont(object): self.tables["cmap"] = cmapLoading @staticmethod - def _makeGlyphName(codepoint): + def _makeGlyphName(codepoint: int) -> str: from fontTools import agl # Adobe Glyph List if codepoint in agl.UV2AGL: @@ -633,12 +1165,12 @@ class TTFont(object): else: return "u%X" % codepoint - def getGlyphNames(self): + def getGlyphNames(self) -> list[str]: """Get a list of glyph names, sorted alphabetically.""" glyphNames = sorted(self.getGlyphOrder()) return glyphNames - def getGlyphNames2(self): + def getGlyphNames2(self) -> list[str]: """Get a list of glyph names, sorted alphabetically, but not case sensitive. """ @@ -646,7 +1178,7 @@ class TTFont(object): return textTools.caselessSort(self.getGlyphOrder()) - def getGlyphName(self, glyphID): + def getGlyphName(self, glyphID: int) -> str: """Returns the name for the glyph with the given ID. If no name is available, synthesises one with the form ``glyphXXXXX``` where @@ -657,13 +1189,13 @@ class TTFont(object): except IndexError: return "glyph%.5d" % glyphID - def getGlyphNameMany(self, lst): + def getGlyphNameMany(self, lst: Sequence[int]) -> list[str]: """Converts a list of glyph IDs into a list of glyph names.""" glyphOrder = self.getGlyphOrder() cnt = len(glyphOrder) return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid for gid in lst] - def getGlyphID(self, glyphName): + def getGlyphID(self, glyphName: str) -> int: """Returns the ID of the glyph with the given name.""" try: return self.getReverseGlyphMap()[glyphName] @@ -675,7 +1207,7 @@ class TTFont(object): raise KeyError(glyphName) raise - def getGlyphIDMany(self, lst): + def getGlyphIDMany(self, lst: Sequence[str]) -> list[int]: """Converts a list of glyph names into a list of glyph IDs.""" d = self.getReverseGlyphMap() try: @@ -684,19 +1216,25 @@ class TTFont(object): getGlyphID = self.getGlyphID return [getGlyphID(glyphName) for glyphName in lst] - def getReverseGlyphMap(self, rebuild=False): + def getReverseGlyphMap(self, rebuild: bool = False) -> dict[str, int]: """Returns a mapping of glyph names to glyph IDs.""" if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): self._buildReverseGlyphOrderDict() return self._reverseGlyphOrderDict - def _buildReverseGlyphOrderDict(self): + def _buildReverseGlyphOrderDict(self) -> dict[str, int]: self._reverseGlyphOrderDict = d = {} for glyphID, glyphName in enumerate(self.getGlyphOrder()): d[glyphName] = glyphID return d - def _writeTable(self, tag, writer, done, tableCache=None): + def _writeTable( + self, + tag: str | bytes, + writer: SFNTWriter, + done: list[str | bytes], # Use list as original + tableCache: MutableMapping[tuple[Tag, bytes], DefaultTable] | None = None, + ) -> None: """Internal helper function for self.save(). Keeps track of inter-table dependencies. """ @@ -722,7 +1260,7 @@ class TTFont(object): if tableCache is not None: tableCache[(Tag(tag), tabledata)] = writer[tag] - def getTableData(self, tag): + def getTableData(self, tag: str | bytes) -> bytes: """Returns the binary representation of a table. If the table is currently loaded and in memory, the data is compiled to @@ -740,8 +1278,12 @@ class TTFont(object): raise KeyError(tag) def getGlyphSet( - self, preferCFF=True, location=None, normalized=False, recalcBounds=True - ): + self, + preferCFF: bool = True, + location: Mapping[str, _NumberT] | None = None, + normalized: bool = False, + recalcBounds: bool = True, + ) -> _TTGlyphSet: """Return a generic GlyphSet, which is a dict-like object mapping glyph names to glyph objects. The returned glyph objects have a ``.draw()`` method that supports the Pen protocol, and will @@ -780,7 +1322,7 @@ class TTFont(object): glyphSet = _TTGlyphSetVARC(self, location, glyphSet) return glyphSet - def normalizeLocation(self, location): + def normalizeLocation(self, location: Mapping[str, float]) -> dict[str, float]: """Normalize a ``location`` from the font's defined axes space (also known as user space) into the normalized (-1..+1) space. It applies ``avar`` mapping if the font contains an ``avar`` table. @@ -803,7 +1345,7 @@ class TTFont(object): def getBestCmap( self, - cmapPreferences=( + cmapPreferences: Sequence[tuple[int, int]] = ( (3, 10), (0, 6), (0, 4), @@ -813,7 +1355,7 @@ class TTFont(object): (0, 1), (0, 0), ), - ): + ) -> dict[int, str] | None: """Returns the 'best' Unicode cmap dictionary available in the font or ``None``, if no Unicode cmap subtable is available. @@ -838,7 +1380,7 @@ class TTFont(object): """ return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) - def reorderGlyphs(self, new_glyph_order): + def reorderGlyphs(self, new_glyph_order: list[str]) -> None: from .reorderGlyphs import reorderGlyphs reorderGlyphs(self, new_glyph_order) @@ -849,20 +1391,22 @@ class GlyphOrder(object): table, but it's nice to present it as such in the TTX format. """ - def __init__(self, tag=None): + def __init__(self, tag: str | None = None) -> None: pass - def toXML(self, writer, ttFont): + def toXML(self, writer: xmlWriter.XMLWriter, ttFont: TTFont) -> None: glyphOrder = ttFont.getGlyphOrder() writer.comment( - "The 'id' attribute is only for humans; " "it is ignored when parsed." + "The 'id' attribute is only for humans; it is ignored when parsed." ) writer.newline() for i, glyphName in enumerate(glyphOrder): writer.simpletag("GlyphID", id=i, name=glyphName) writer.newline() - def fromXML(self, name, attrs, content, ttFont): + def fromXML( + self, name: str, attrs: dict[str, str], content: list[Any], ttFont: TTFont + ) -> None: if not hasattr(self, "glyphOrder"): self.glyphOrder = [] if name == "GlyphID": @@ -870,7 +1414,7 @@ class GlyphOrder(object): ttFont.setGlyphOrder(self.glyphOrder) -def getTableModule(tag): +def getTableModule(tag: str | bytes) -> ModuleType | None: """Fetch the packer/unpacker module for a table. Return None when no module is found. """ @@ -895,10 +1439,12 @@ def getTableModule(tag): # Registry for custom table packer/unpacker classes. Keys are table # tags, values are (moduleName, className) tuples. # See registerCustomTableClass() and getCustomTableClass() -_customTableRegistry = {} +_customTableRegistry: dict[str | bytes, tuple[str, str]] = {} -def registerCustomTableClass(tag, moduleName, className=None): +def registerCustomTableClass( + tag: str | bytes, moduleName: str, className: str | None = None +) -> None: """Register a custom packer/unpacker class for a table. The 'moduleName' must be an importable module. If no 'className' @@ -913,12 +1459,12 @@ def registerCustomTableClass(tag, moduleName, className=None): _customTableRegistry[tag] = (moduleName, className) -def unregisterCustomTableClass(tag): +def unregisterCustomTableClass(tag: str | bytes) -> None: """Unregister the custom packer/unpacker class for a table.""" del _customTableRegistry[tag] -def getCustomTableClass(tag): +def getCustomTableClass(tag: str | bytes) -> type[DefaultTable] | None: """Return the custom table class for tag, if one has been registered with 'registerCustomTableClass()'. Else return None. """ @@ -931,7 +1477,7 @@ def getCustomTableClass(tag): return getattr(module, className) -def getTableClass(tag): +def getTableClass(tag: str | bytes) -> type[DefaultTable]: """Fetch the packer/unpacker class for a table.""" tableClass = getCustomTableClass(tag) if tableClass is not None: @@ -946,7 +1492,7 @@ def getTableClass(tag): return tableClass -def getClassTag(klass): +def getClassTag(klass: type[DefaultTable]) -> str | bytes: """Fetch the table tag for a class object.""" name = klass.__name__ assert name[:6] == "table_" @@ -954,13 +1500,13 @@ def getClassTag(klass): return identifierToTag(name) -def newTable(tag): +def newTable(tag: str | bytes) -> DefaultTable: """Return a new instance of a table.""" tableClass = getTableClass(tag) return tableClass(tag) -def _escapechar(c): +def _escapechar(c: str) -> str: """Helper function for tagToIdentifier()""" import re @@ -972,7 +1518,7 @@ def _escapechar(c): return hex(byteord(c))[2:] -def tagToIdentifier(tag): +def tagToIdentifier(tag: str | bytes) -> str: """Convert a table tag to a valid (but UGLY) python identifier, as well as a filename that's guaranteed to be unique even on a caseless file system. Each character is mapped to two characters. @@ -1007,7 +1553,7 @@ def tagToIdentifier(tag): return ident -def identifierToTag(ident): +def identifierToTag(ident: str) -> str: """the opposite of tagToIdentifier()""" if ident == "GlyphOrder": return ident @@ -1028,7 +1574,7 @@ def identifierToTag(ident): return Tag(tag) -def tagToXML(tag): +def tagToXML(tag: str | bytes) -> str: """Similarly to tagToIdentifier(), this converts a TT tag to a valid XML element name. Since XML element names are case sensitive, this is a fairly simple/readable translation. @@ -1046,7 +1592,7 @@ def tagToXML(tag): return tagToIdentifier(tag) -def xmlToTag(tag): +def xmlToTag(tag: str) -> str: """The opposite of tagToXML()""" if tag == "OS_2": return Tag("OS/2") @@ -1082,7 +1628,9 @@ TTFTableOrder = [ OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "] -def sortedTagList(tagList, tableOrder=None): +def sortedTagList( + tagList: Sequence[str], tableOrder: Sequence[str] | None = None +) -> list[str]: """Return a sorted copy of tagList, sorted according to the OpenType specification, or according to a custom tableOrder. If given and not None, tableOrder needs to be a list of tag names. @@ -1106,7 +1654,12 @@ def sortedTagList(tagList, tableOrder=None): return orderedTables -def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): +def reorderFontTables( + inFile: BinaryIO, # Takes file-like object as per original + outFile: BinaryIO, # Takes file-like object + tableOrder: Sequence[str] | None = None, + checkChecksums: bool = False, # Keep param even if reader handles it +) -> None: """Rewrite a font file, ordering the tables as recommended by the OpenType specification 1.4. """ @@ -1126,7 +1679,7 @@ def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): writer.close() -def maxPowerOfTwo(x): +def maxPowerOfTwo(x: int) -> int: """Return the highest exponent of two, so that (2 ** exponent) <= x. Return 0 if x is 0. """ @@ -1137,7 +1690,7 @@ def maxPowerOfTwo(x): return max(exponent - 1, 0) -def getSearchRange(n, itemSize=16): +def getSearchRange(n: int, itemSize: int = 16) -> tuple[int, int, int]: """Calculate searchRange, entrySelector, rangeShift.""" # itemSize defaults to 16, for backward compatibility # with upstream fonttools. diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py index fa84e3a57e5..2993bf38bfa 100644 --- a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py +++ b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py @@ -448,7 +448,7 @@ class AxisLimits(_BaseAxisLimits): # Full instancing of avar2 font. Use avar table to normalize location and return. location = self.pinnedLocation() location = { - tag: normalize(value, axes[tag], avarSegments.get(tag, None)) + tag: normalize(value, axes[tag], None) for tag, value in location.items() } return NormalizedAxisLimits( diff --git a/contrib/python/fonttools/fontTools/varLib/models.py b/contrib/python/fonttools/fontTools/varLib/models.py index 52433a66a82..1d836aeb163 100644 --- a/contrib/python/fonttools/fontTools/varLib/models.py +++ b/contrib/python/fonttools/fontTools/varLib/models.py @@ -1,5 +1,7 @@ """Variation fonts interpolation models.""" +from __future__ import annotations + __all__ = [ "normalizeValue", "normalizeLocation", @@ -8,10 +10,15 @@ __all__ = [ "VariationModel", ] +from typing import TYPE_CHECKING from fontTools.misc.roundTools import noRound from .errors import VariationModelError +if TYPE_CHECKING: + from typing import Mapping, Sequence + + def nonNone(lst): return [l for l in lst if l is not None] @@ -44,7 +51,9 @@ def subList(truth, lst): return [l for l, t in zip(lst, truth) if t] -def normalizeValue(v, triple, extrapolate=False): +def normalizeValue( + v: float, triple: Sequence[float], extrapolate: bool = False +) -> float: """Normalizes value based on a min/default/max triple. >>> normalizeValue(400, (100, 400, 900)) @@ -75,7 +84,13 @@ def normalizeValue(v, triple, extrapolate=False): return (v - default) / (upper - default) -def normalizeLocation(location, axes, extrapolate=False, *, validate=False): +def normalizeLocation( + location: Mapping[str, float], + axes: Mapping[str, tuple[float, float, float]], + extrapolate: bool = False, + *, + validate: bool = False, +) -> dict[str, float]: """Normalizes location based on axis min/default/max values from axes. >>> axes = {"wght": (100, 400, 900)} diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make index d34ce34755c..3f8f9d4e88a 100644 --- a/contrib/python/fonttools/ya.make +++ b/contrib/python/fonttools/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.61.0) +VERSION(4.61.1) LICENSE(MIT) |
