Spaces:
Running
Running
| 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.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, # noqa: F401 | |
| _TTGlyphSet, | |
| _TTGlyphSetCFF, | |
| _TTGlyphSetGlyf, | |
| _TTGlyphSetVARC, | |
| ) | |
| 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. | |
| The object manages file input and output, and offers a convenient way of | |
| accessing tables. Tables will be only decompiled when necessary, ie. when | |
| they're actually accessed. This means that simple operations can be extremely fast. | |
| Example usage: | |
| .. code-block:: pycon | |
| >>> | |
| >> from fontTools import ttLib | |
| >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file | |
| >> tt['maxp'].numGlyphs | |
| 242 | |
| >> tt['OS/2'].achVendID | |
| 'B&H\000' | |
| >> tt['head'].unitsPerEm | |
| 2048 | |
| For details of the objects returned when accessing each table, see the | |
| :doc:`tables </ttLib/tables>` documentation. | |
| To add a table to the font, use the :py:func:`newTable` function: | |
| .. code-block:: pycon | |
| >>> | |
| >> os2 = newTable("OS/2") | |
| >> os2.version = 4 | |
| >> # set other attributes | |
| >> font["OS/2"] = os2 | |
| TrueType fonts can also be serialized to and from XML format (see also the | |
| :doc:`ttx </ttx>` binary): | |
| .. code-block:: pycon | |
| >> | |
| >> tt.saveXML("afont.ttx") | |
| Dumping 'LTSH' table... | |
| Dumping 'OS/2' table... | |
| [...] | |
| >> tt2 = ttLib.TTFont() # Create a new font object | |
| >> tt2.importXML("afont.ttx") | |
| >> tt2['maxp'].numGlyphs | |
| 242 | |
| The TTFont object may be used as a context manager; this will cause the file | |
| reader to be closed after the context ``with`` block is exited:: | |
| with TTFont(filename) as f: | |
| # Do stuff | |
| Args: | |
| file: When reading a font from disk, either a pathname pointing to a file, | |
| or a readable file object. | |
| res_name_or_index: If running on a Macintosh, either a sfnt resource name or | |
| an sfnt resource index number. If the index number is zero, TTLib will | |
| autodetect whether the file is a flat file or a suitcase. (If it is a suitcase, | |
| only the first 'sfnt' resource will be read.) | |
| sfntVersion (str): When constructing a font object from scratch, sets the four-byte | |
| sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create | |
| an OpenType file, use ``OTTO``. | |
| flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2 | |
| file. | |
| checkChecksums (int): How checksum data should be treated. Default is 0 | |
| (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to | |
| raise an exception if any wrong checksums are found. | |
| recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``, | |
| ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save. | |
| Also compiles the glyphs on importing, which saves memory consumption and | |
| time. | |
| ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation | |
| will be ignored, and the binary data will be returned for those tables instead. | |
| recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in | |
| the ``head`` table on save. | |
| fontNumber (int): The index of the font in a TrueType Collection file. | |
| lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon | |
| access only. If it is set to False, many data structures are loaded immediately. | |
| 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: 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: | |
| deprecateArgument(name, "configure logging instead") | |
| setattr(self, name, val) | |
| self.lazy = lazy | |
| self.recalcBBoxes = recalcBBoxes | |
| self.recalcTimestamp = recalcTimestamp | |
| self.tables = {} | |
| self.reader = None | |
| self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg) | |
| self.ignoreDecompileErrors = ignoreDecompileErrors | |
| if not file: | |
| self.sfntVersion = sfntVersion | |
| self.flavor = flavor | |
| self.flavorData = None | |
| 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: | |
| # see if it contains 'sfnt' resources in the resource or data fork | |
| from . import macUtils | |
| if res_name_or_index == 0: | |
| if macUtils.getSFNTResIndices(file): | |
| # get the first available sfnt font. | |
| file = macUtils.SFNTResourceReader(file, 1) | |
| else: | |
| file = open(file, "rb") | |
| else: | |
| file = macUtils.SFNTResourceReader(file, res_name_or_index) | |
| else: | |
| 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: | |
| # https://github.com/fonttools/fonttools/issues/3052 | |
| if hasattr(file, "seekable"): | |
| seekable = file.seekable() | |
| elif hasattr(file, "seek"): | |
| try: | |
| file.seek(0) | |
| except UnsupportedOperation: | |
| seekable = False | |
| if not self.lazy: | |
| # read input file in memory and wrap a stream around it to allow overwriting | |
| if seekable: | |
| file.seek(0) | |
| tmp = BytesIO(file.read()) | |
| if hasattr(file, "name"): | |
| # save reference to input file name | |
| tmp.name = file.name | |
| if closeStream: | |
| file.close() | |
| file = tmp | |
| elif not seekable: | |
| raise TTLibError("Input file must be seekable when lazy=True") | |
| self._tableCache = _tableCache | |
| self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber) | |
| self.sfntVersion = self.reader.sfntVersion | |
| self.flavor = self.reader.flavor | |
| self.flavorData = self.reader.flavorData | |
| def __enter__(self) -> Self: | |
| return self | |
| def __exit__( | |
| self, | |
| exc_type: type[BaseException] | None, | |
| exc_value: BaseException | None, | |
| traceback: TracebackType | None, | |
| ) -> None: | |
| self.close() | |
| 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: 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 | |
| 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 | |
| dependency (fastest). | |
| """ | |
| if not hasattr(file, "write"): | |
| if self.lazy and self.reader.file.name == file: | |
| raise TTLibError("Can't overwrite TTFont when 'lazy' attribute is True") | |
| createStream = True | |
| else: | |
| # assume "file" is a writable file object | |
| createStream = False | |
| tmp = BytesIO() | |
| writer_reordersTables = self._save(tmp) | |
| if not ( | |
| reorderTables is None | |
| or writer_reordersTables | |
| or (reorderTables is False and self.reader is None) | |
| ): | |
| 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 | |
| tableOrder = None | |
| tmp.flush() | |
| tmp2 = BytesIO() | |
| reorderFontTables(tmp, tmp2, tableOrder) | |
| tmp.close() | |
| tmp = tmp2 | |
| 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: 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: | |
| # make sure 'head' is loaded so the recalculation is actually done | |
| self["head"] | |
| tags = self.keys() | |
| tags.pop(0) # skip GlyphOrder tag | |
| numTables = len(tags) | |
| # write to a temporary stream to allow saving to unseekable streams | |
| writer = SFNTWriter( | |
| file, numTables, self.sfntVersion, self.flavor, self.flavorData | |
| ) | |
| done = [] | |
| for tag in tags: | |
| self._writeTable(tag, writer, done, tableCache) | |
| writer.close() | |
| return writer.reordersTables() | |
| 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. | |
| The 'tables' argument must either be false (dump all tables) or a | |
| list of tables to dump. The 'skipTables' argument may be a list of tables | |
| to skip, but only when the 'tables' argument is false. | |
| """ | |
| writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) | |
| self._saveXML(writer, **kwargs) | |
| writer.close() | |
| def _saveXML( | |
| self, | |
| 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") | |
| self.disassembleInstructions = disassembleInstructions | |
| self.bitmapGlyphDataFormat = bitmapGlyphDataFormat | |
| if not tables: | |
| tables = self.keys() | |
| if skipTables: | |
| tables = [tag for tag in tables if tag not in skipTables] | |
| if writeVersion: | |
| from fontTools import version | |
| version = ".".join(version.split(".")[:2]) | |
| writer.begintag( | |
| "ttFont", | |
| sfntVersion=repr(tostr(self.sfntVersion))[1:-1], | |
| ttLibVersion=version, | |
| ) | |
| else: | |
| writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1]) | |
| writer.newline() | |
| # always splitTables if splitGlyphs is enabled | |
| splitTables = splitTables or splitGlyphs | |
| 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: | |
| if splitTables: | |
| tablePath = path + "." + tagToIdentifier(tag) + ext | |
| tableWriter = xmlWriter.XMLWriter( | |
| tablePath, newlinestr=writer.newlinestr | |
| ) | |
| tableWriter.begintag("ttFont", ttLibVersion=version) | |
| tableWriter.newline() | |
| tableWriter.newline() | |
| writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) | |
| writer.newline() | |
| else: | |
| tableWriter = writer | |
| self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs) | |
| if splitTables: | |
| tableWriter.endtag("ttFont") | |
| tableWriter.newline() | |
| tableWriter.close() | |
| writer.endtag("ttFont") | |
| writer.newline() | |
| 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: | |
| table = self[tag] | |
| report = "Dumping '%s' table..." % tag | |
| else: | |
| report = "No '%s' table found." % tag | |
| log.info(report) | |
| if tag not in self: | |
| return | |
| xmlTag = tagToXML(tag) | |
| attrs: dict[str, Any] = {} | |
| if hasattr(table, "ERROR"): | |
| attrs["ERROR"] = "decompilation error" | |
| from .tables.DefaultTable import DefaultTable | |
| if table.__class__ == DefaultTable: | |
| attrs["raw"] = True | |
| writer.begintag(xmlTag, **attrs) | |
| writer.newline() | |
| if tag == "glyf": | |
| table.toXML(writer, self, splitGlyphs=splitGlyphs) | |
| else: | |
| table.toXML(writer, self) | |
| writer.endtag(xmlTag) | |
| writer.newline() | |
| writer.newline() | |
| 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. | |
| """ | |
| if quiet is not None: | |
| deprecateArgument("quiet", "configure logging instead") | |
| if "maxp" in self and "post" in self: | |
| # Make sure the glyph order is loaded, as it otherwise gets | |
| # lost if the XML doesn't contain the glyph order, yet does | |
| # contain the table which was originally used to extract the | |
| # glyph names from (ie. 'post', 'cmap' or 'CFF '). | |
| self.getGlyphOrder() | |
| from fontTools.misc import xmlReader | |
| reader = xmlReader.XMLReader(fileOrPath, self) | |
| reader.read() | |
| 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: 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 | |
| presence of the table.""" | |
| if self.isLoaded(tag): | |
| return True | |
| elif self.reader and tag in self.reader: | |
| return True | |
| elif tag == "GlyphOrder": | |
| return True | |
| else: | |
| return False | |
| __contains__ = has_key | |
| 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: | |
| for key in list(self.reader.keys()): | |
| if key not in keys: | |
| keys.append(key) | |
| if "GlyphOrder" in keys: | |
| keys.remove("GlyphOrder") | |
| keys = sortedTagList(keys) | |
| return ["GlyphOrder"] + keys | |
| 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] | |
| if recurse is None: | |
| recurse = self.lazy is not False | |
| if recurse and hasattr(table, "ensureDecompiled"): | |
| table.ensureDecompiled(recurse=recurse) | |
| self.lazy = False | |
| def __len__(self) -> int: | |
| return len(list(self.keys())) | |
| def __getitem__(self, tag: Literal["BASE"]) -> B_A_S_E_.table_B_A_S_E_: ... | |
| def __getitem__(self, tag: Literal["CBDT"]) -> C_B_D_T_.table_C_B_D_T_: ... | |
| def __getitem__(self, tag: Literal["CBLC"]) -> C_B_L_C_.table_C_B_L_C_: ... | |
| def __getitem__(self, tag: Literal["CFF "]) -> C_F_F_.table_C_F_F_: ... | |
| def __getitem__(self, tag: Literal["CFF2"]) -> C_F_F__2.table_C_F_F__2: ... | |
| def __getitem__(self, tag: Literal["COLR"]) -> C_O_L_R_.table_C_O_L_R_: ... | |
| def __getitem__(self, tag: Literal["CPAL"]) -> C_P_A_L_.table_C_P_A_L_: ... | |
| def __getitem__(self, tag: Literal["DSIG"]) -> D_S_I_G_.table_D_S_I_G_: ... | |
| def __getitem__(self, tag: Literal["EBDT"]) -> E_B_D_T_.table_E_B_D_T_: ... | |
| def __getitem__(self, tag: Literal["EBLC"]) -> E_B_L_C_.table_E_B_L_C_: ... | |
| def __getitem__(self, tag: Literal["FFTM"]) -> F_F_T_M_.table_F_F_T_M_: ... | |
| def __getitem__(self, tag: Literal["GDEF"]) -> G_D_E_F_.table_G_D_E_F_: ... | |
| def __getitem__(self, tag: Literal["GMAP"]) -> G_M_A_P_.table_G_M_A_P_: ... | |
| def __getitem__(self, tag: Literal["GPKG"]) -> G_P_K_G_.table_G_P_K_G_: ... | |
| def __getitem__(self, tag: Literal["GPOS"]) -> G_P_O_S_.table_G_P_O_S_: ... | |
| def __getitem__(self, tag: Literal["GSUB"]) -> G_S_U_B_.table_G_S_U_B_: ... | |
| def __getitem__(self, tag: Literal["GVAR"]) -> G_V_A_R_.table_G_V_A_R_: ... | |
| def __getitem__(self, tag: Literal["HVAR"]) -> H_V_A_R_.table_H_V_A_R_: ... | |
| def __getitem__(self, tag: Literal["JSTF"]) -> J_S_T_F_.table_J_S_T_F_: ... | |
| def __getitem__(self, tag: Literal["LTSH"]) -> L_T_S_H_.table_L_T_S_H_: ... | |
| def __getitem__(self, tag: Literal["MATH"]) -> M_A_T_H_.table_M_A_T_H_: ... | |
| def __getitem__(self, tag: Literal["META"]) -> M_E_T_A_.table_M_E_T_A_: ... | |
| def __getitem__(self, tag: Literal["MVAR"]) -> M_V_A_R_.table_M_V_A_R_: ... | |
| def __getitem__(self, tag: Literal["SING"]) -> S_I_N_G_.table_S_I_N_G_: ... | |
| def __getitem__(self, tag: Literal["STAT"]) -> S_T_A_T_.table_S_T_A_T_: ... | |
| def __getitem__(self, tag: Literal["SVG "]) -> S_V_G_.table_S_V_G_: ... | |
| def __getitem__(self, tag: Literal["TSI0"]) -> T_S_I__0.table_T_S_I__0: ... | |
| def __getitem__(self, tag: Literal["TSI1"]) -> T_S_I__1.table_T_S_I__1: ... | |
| def __getitem__(self, tag: Literal["TSI2"]) -> T_S_I__2.table_T_S_I__2: ... | |
| def __getitem__(self, tag: Literal["TSI3"]) -> T_S_I__3.table_T_S_I__3: ... | |
| def __getitem__(self, tag: Literal["TSI5"]) -> T_S_I__5.table_T_S_I__5: ... | |
| def __getitem__(self, tag: Literal["TSIB"]) -> T_S_I_B_.table_T_S_I_B_: ... | |
| def __getitem__(self, tag: Literal["TSIC"]) -> T_S_I_C_.table_T_S_I_C_: ... | |
| def __getitem__(self, tag: Literal["TSID"]) -> T_S_I_D_.table_T_S_I_D_: ... | |
| def __getitem__(self, tag: Literal["TSIJ"]) -> T_S_I_J_.table_T_S_I_J_: ... | |
| def __getitem__(self, tag: Literal["TSIP"]) -> T_S_I_P_.table_T_S_I_P_: ... | |
| def __getitem__(self, tag: Literal["TSIS"]) -> T_S_I_S_.table_T_S_I_S_: ... | |
| def __getitem__(self, tag: Literal["TSIV"]) -> T_S_I_V_.table_T_S_I_V_: ... | |
| def __getitem__(self, tag: Literal["TTFA"]) -> T_T_F_A_.table_T_T_F_A_: ... | |
| def __getitem__(self, tag: Literal["VARC"]) -> V_A_R_C_.table_V_A_R_C_: ... | |
| def __getitem__(self, tag: Literal["VDMX"]) -> V_D_M_X_.table_V_D_M_X_: ... | |
| def __getitem__(self, tag: Literal["VORG"]) -> V_O_R_G_.table_V_O_R_G_: ... | |
| def __getitem__(self, tag: Literal["VVAR"]) -> V_V_A_R_.table_V_V_A_R_: ... | |
| def __getitem__(self, tag: Literal["Debg"]) -> D__e_b_g.table_D__e_b_g: ... | |
| def __getitem__(self, tag: Literal["Feat"]) -> F__e_a_t.table_F__e_a_t: ... | |
| def __getitem__(self, tag: Literal["Glat"]) -> G__l_a_t.table_G__l_a_t: ... | |
| def __getitem__(self, tag: Literal["Gloc"]) -> G__l_o_c.table_G__l_o_c: ... | |
| def __getitem__(self, tag: Literal["OS/2"]) -> O_S_2f_2.table_O_S_2f_2: ... | |
| def __getitem__(self, tag: Literal["Silf"]) -> S__i_l_f.table_S__i_l_f: ... | |
| def __getitem__(self, tag: Literal["Sill"]) -> S__i_l_l.table_S__i_l_l: ... | |
| def __getitem__(self, tag: Literal["ankr"]) -> _a_n_k_r.table__a_n_k_r: ... | |
| def __getitem__(self, tag: Literal["avar"]) -> _a_v_a_r.table__a_v_a_r: ... | |
| def __getitem__(self, tag: Literal["bsln"]) -> _b_s_l_n.table__b_s_l_n: ... | |
| def __getitem__(self, tag: Literal["cidg"]) -> _c_i_d_g.table__c_i_d_g: ... | |
| def __getitem__(self, tag: Literal["cmap"]) -> _c_m_a_p.table__c_m_a_p: ... | |
| def __getitem__(self, tag: Literal["cvar"]) -> _c_v_a_r.table__c_v_a_r: ... | |
| def __getitem__(self, tag: Literal["cvt "]) -> _c_v_t.table__c_v_t: ... | |
| def __getitem__(self, tag: Literal["feat"]) -> _f_e_a_t.table__f_e_a_t: ... | |
| def __getitem__(self, tag: Literal["fpgm"]) -> _f_p_g_m.table__f_p_g_m: ... | |
| def __getitem__(self, tag: Literal["fvar"]) -> _f_v_a_r.table__f_v_a_r: ... | |
| def __getitem__(self, tag: Literal["gasp"]) -> _g_a_s_p.table__g_a_s_p: ... | |
| def __getitem__(self, tag: Literal["gcid"]) -> _g_c_i_d.table__g_c_i_d: ... | |
| def __getitem__(self, tag: Literal["glyf"]) -> _g_l_y_f.table__g_l_y_f: ... | |
| def __getitem__(self, tag: Literal["gvar"]) -> _g_v_a_r.table__g_v_a_r: ... | |
| def __getitem__(self, tag: Literal["hdmx"]) -> _h_d_m_x.table__h_d_m_x: ... | |
| def __getitem__(self, tag: Literal["head"]) -> _h_e_a_d.table__h_e_a_d: ... | |
| def __getitem__(self, tag: Literal["hhea"]) -> _h_h_e_a.table__h_h_e_a: ... | |
| def __getitem__(self, tag: Literal["hmtx"]) -> _h_m_t_x.table__h_m_t_x: ... | |
| def __getitem__(self, tag: Literal["kern"]) -> _k_e_r_n.table__k_e_r_n: ... | |
| def __getitem__(self, tag: Literal["lcar"]) -> _l_c_a_r.table__l_c_a_r: ... | |
| def __getitem__(self, tag: Literal["loca"]) -> _l_o_c_a.table__l_o_c_a: ... | |
| def __getitem__(self, tag: Literal["ltag"]) -> _l_t_a_g.table__l_t_a_g: ... | |
| def __getitem__(self, tag: Literal["maxp"]) -> _m_a_x_p.table__m_a_x_p: ... | |
| def __getitem__(self, tag: Literal["meta"]) -> _m_e_t_a.table__m_e_t_a: ... | |
| def __getitem__(self, tag: Literal["mort"]) -> _m_o_r_t.table__m_o_r_t: ... | |
| def __getitem__(self, tag: Literal["morx"]) -> _m_o_r_x.table__m_o_r_x: ... | |
| def __getitem__(self, tag: Literal["name"]) -> _n_a_m_e.table__n_a_m_e: ... | |
| def __getitem__(self, tag: Literal["opbd"]) -> _o_p_b_d.table__o_p_b_d: ... | |
| def __getitem__(self, tag: Literal["post"]) -> _p_o_s_t.table__p_o_s_t: ... | |
| def __getitem__(self, tag: Literal["prep"]) -> _p_r_e_p.table__p_r_e_p: ... | |
| def __getitem__(self, tag: Literal["prop"]) -> _p_r_o_p.table__p_r_o_p: ... | |
| def __getitem__(self, tag: Literal["sbix"]) -> _s_b_i_x.table__s_b_i_x: ... | |
| def __getitem__(self, tag: Literal["trak"]) -> _t_r_a_k.table__t_r_a_k: ... | |
| def __getitem__(self, tag: Literal["vhea"]) -> _v_h_e_a.table__v_h_e_a: ... | |
| def __getitem__(self, tag: Literal["vmtx"]) -> _v_m_t_x.table__v_m_t_x: ... | |
| def __getitem__(self, tag: Literal["GlyphOrder"]) -> GlyphOrder: ... | |
| 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: | |
| if tag == "GlyphOrder": | |
| table = GlyphOrder(tag) | |
| self.tables[tag] = table | |
| elif self.reader is not None: | |
| table = self._readTable(tag) | |
| else: | |
| raise KeyError("'%s' table not found" % tag) | |
| return table | |
| 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)) | |
| if table is not None: | |
| return table | |
| tableClass = getTableClass(tag) | |
| table = tableClass(tag) | |
| self.tables[tag] = table | |
| log.debug("Decompiling '%s' table", tag) | |
| try: | |
| table.decompile(data, self) | |
| except Exception: | |
| if not self.ignoreDecompileErrors: | |
| raise | |
| # fall back to DefaultTable, retaining the binary table data | |
| log.exception( | |
| "An exception occurred during the decompilation of the '%s' table", tag | |
| ) | |
| from .tables.DefaultTable import DefaultTable | |
| file = StringIO() | |
| traceback.print_exc(file=file) | |
| table = DefaultTable(tag) | |
| table.ERROR = file.getvalue() | |
| self.tables[tag] = table | |
| table.decompile(data, self) | |
| if self._tableCache is not None: | |
| self._tableCache[(tag, data)] = table | |
| return table | |
| def __setitem__(self, tag: str | bytes, table: DefaultTable) -> None: | |
| self.tables[Tag(tag)] = table | |
| def __delitem__(self, tag: str | bytes) -> None: | |
| if tag not in self: | |
| raise KeyError("'%s' table not found" % tag) | |
| if tag in self.tables: | |
| del self.tables[tag] | |
| if self.reader and tag in self.reader: | |
| del self.reader[tag] | |
| def get(self, tag: Literal["BASE"]) -> B_A_S_E_.table_B_A_S_E_ | None: ... | |
| def get(self, tag: Literal["CBDT"]) -> C_B_D_T_.table_C_B_D_T_ | None: ... | |
| def get(self, tag: Literal["CBLC"]) -> C_B_L_C_.table_C_B_L_C_ | None: ... | |
| def get(self, tag: Literal["CFF "]) -> C_F_F_.table_C_F_F_ | None: ... | |
| def get(self, tag: Literal["CFF2"]) -> C_F_F__2.table_C_F_F__2 | None: ... | |
| def get(self, tag: Literal["COLR"]) -> C_O_L_R_.table_C_O_L_R_ | None: ... | |
| def get(self, tag: Literal["CPAL"]) -> C_P_A_L_.table_C_P_A_L_ | None: ... | |
| def get(self, tag: Literal["DSIG"]) -> D_S_I_G_.table_D_S_I_G_ | None: ... | |
| def get(self, tag: Literal["EBDT"]) -> E_B_D_T_.table_E_B_D_T_ | None: ... | |
| def get(self, tag: Literal["EBLC"]) -> E_B_L_C_.table_E_B_L_C_ | None: ... | |
| def get(self, tag: Literal["FFTM"]) -> F_F_T_M_.table_F_F_T_M_ | None: ... | |
| def get(self, tag: Literal["GDEF"]) -> G_D_E_F_.table_G_D_E_F_ | None: ... | |
| def get(self, tag: Literal["GMAP"]) -> G_M_A_P_.table_G_M_A_P_ | None: ... | |
| def get(self, tag: Literal["GPKG"]) -> G_P_K_G_.table_G_P_K_G_ | None: ... | |
| def get(self, tag: Literal["GPOS"]) -> G_P_O_S_.table_G_P_O_S_ | None: ... | |
| def get(self, tag: Literal["GSUB"]) -> G_S_U_B_.table_G_S_U_B_ | None: ... | |
| def get(self, tag: Literal["GVAR"]) -> G_V_A_R_.table_G_V_A_R_ | None: ... | |
| def get(self, tag: Literal["HVAR"]) -> H_V_A_R_.table_H_V_A_R_ | None: ... | |
| def get(self, tag: Literal["JSTF"]) -> J_S_T_F_.table_J_S_T_F_ | None: ... | |
| def get(self, tag: Literal["LTSH"]) -> L_T_S_H_.table_L_T_S_H_ | None: ... | |
| def get(self, tag: Literal["MATH"]) -> M_A_T_H_.table_M_A_T_H_ | None: ... | |
| def get(self, tag: Literal["META"]) -> M_E_T_A_.table_M_E_T_A_ | None: ... | |
| def get(self, tag: Literal["MVAR"]) -> M_V_A_R_.table_M_V_A_R_ | None: ... | |
| def get(self, tag: Literal["SING"]) -> S_I_N_G_.table_S_I_N_G_ | None: ... | |
| def get(self, tag: Literal["STAT"]) -> S_T_A_T_.table_S_T_A_T_ | None: ... | |
| def get(self, tag: Literal["SVG "]) -> S_V_G_.table_S_V_G_ | None: ... | |
| def get(self, tag: Literal["TSI0"]) -> T_S_I__0.table_T_S_I__0 | None: ... | |
| def get(self, tag: Literal["TSI1"]) -> T_S_I__1.table_T_S_I__1 | None: ... | |
| def get(self, tag: Literal["TSI2"]) -> T_S_I__2.table_T_S_I__2 | None: ... | |
| def get(self, tag: Literal["TSI3"]) -> T_S_I__3.table_T_S_I__3 | None: ... | |
| def get(self, tag: Literal["TSI5"]) -> T_S_I__5.table_T_S_I__5 | None: ... | |
| def get(self, tag: Literal["TSIB"]) -> T_S_I_B_.table_T_S_I_B_ | None: ... | |
| def get(self, tag: Literal["TSIC"]) -> T_S_I_C_.table_T_S_I_C_ | None: ... | |
| def get(self, tag: Literal["TSID"]) -> T_S_I_D_.table_T_S_I_D_ | None: ... | |
| def get(self, tag: Literal["TSIJ"]) -> T_S_I_J_.table_T_S_I_J_ | None: ... | |
| def get(self, tag: Literal["TSIP"]) -> T_S_I_P_.table_T_S_I_P_ | None: ... | |
| def get(self, tag: Literal["TSIS"]) -> T_S_I_S_.table_T_S_I_S_ | None: ... | |
| def get(self, tag: Literal["TSIV"]) -> T_S_I_V_.table_T_S_I_V_ | None: ... | |
| def get(self, tag: Literal["TTFA"]) -> T_T_F_A_.table_T_T_F_A_ | None: ... | |
| def get(self, tag: Literal["VARC"]) -> V_A_R_C_.table_V_A_R_C_ | None: ... | |
| def get(self, tag: Literal["VDMX"]) -> V_D_M_X_.table_V_D_M_X_ | None: ... | |
| def get(self, tag: Literal["VORG"]) -> V_O_R_G_.table_V_O_R_G_ | None: ... | |
| def get(self, tag: Literal["VVAR"]) -> V_V_A_R_.table_V_V_A_R_ | None: ... | |
| def get(self, tag: Literal["Debg"]) -> D__e_b_g.table_D__e_b_g | None: ... | |
| def get(self, tag: Literal["Feat"]) -> F__e_a_t.table_F__e_a_t | None: ... | |
| def get(self, tag: Literal["Glat"]) -> G__l_a_t.table_G__l_a_t | None: ... | |
| def get(self, tag: Literal["Gloc"]) -> G__l_o_c.table_G__l_o_c | None: ... | |
| def get(self, tag: Literal["OS/2"]) -> O_S_2f_2.table_O_S_2f_2 | None: ... | |
| def get(self, tag: Literal["Silf"]) -> S__i_l_f.table_S__i_l_f | None: ... | |
| def get(self, tag: Literal["Sill"]) -> S__i_l_l.table_S__i_l_l | None: ... | |
| def get(self, tag: Literal["ankr"]) -> _a_n_k_r.table__a_n_k_r | None: ... | |
| def get(self, tag: Literal["avar"]) -> _a_v_a_r.table__a_v_a_r | None: ... | |
| def get(self, tag: Literal["bsln"]) -> _b_s_l_n.table__b_s_l_n | None: ... | |
| def get(self, tag: Literal["cidg"]) -> _c_i_d_g.table__c_i_d_g | None: ... | |
| def get(self, tag: Literal["cmap"]) -> _c_m_a_p.table__c_m_a_p | None: ... | |
| def get(self, tag: Literal["cvar"]) -> _c_v_a_r.table__c_v_a_r | None: ... | |
| def get(self, tag: Literal["cvt "]) -> _c_v_t.table__c_v_t | None: ... | |
| def get(self, tag: Literal["feat"]) -> _f_e_a_t.table__f_e_a_t | None: ... | |
| def get(self, tag: Literal["fpgm"]) -> _f_p_g_m.table__f_p_g_m | None: ... | |
| def get(self, tag: Literal["fvar"]) -> _f_v_a_r.table__f_v_a_r | None: ... | |
| def get(self, tag: Literal["gasp"]) -> _g_a_s_p.table__g_a_s_p | None: ... | |
| def get(self, tag: Literal["gcid"]) -> _g_c_i_d.table__g_c_i_d | None: ... | |
| def get(self, tag: Literal["glyf"]) -> _g_l_y_f.table__g_l_y_f | None: ... | |
| def get(self, tag: Literal["gvar"]) -> _g_v_a_r.table__g_v_a_r | None: ... | |
| def get(self, tag: Literal["hdmx"]) -> _h_d_m_x.table__h_d_m_x | None: ... | |
| def get(self, tag: Literal["head"]) -> _h_e_a_d.table__h_e_a_d | None: ... | |
| def get(self, tag: Literal["hhea"]) -> _h_h_e_a.table__h_h_e_a | None: ... | |
| def get(self, tag: Literal["hmtx"]) -> _h_m_t_x.table__h_m_t_x | None: ... | |
| def get(self, tag: Literal["kern"]) -> _k_e_r_n.table__k_e_r_n | None: ... | |
| def get(self, tag: Literal["lcar"]) -> _l_c_a_r.table__l_c_a_r | None: ... | |
| def get(self, tag: Literal["loca"]) -> _l_o_c_a.table__l_o_c_a | None: ... | |
| def get(self, tag: Literal["ltag"]) -> _l_t_a_g.table__l_t_a_g | None: ... | |
| def get(self, tag: Literal["maxp"]) -> _m_a_x_p.table__m_a_x_p | None: ... | |
| def get(self, tag: Literal["meta"]) -> _m_e_t_a.table__m_e_t_a | None: ... | |
| def get(self, tag: Literal["mort"]) -> _m_o_r_t.table__m_o_r_t | None: ... | |
| def get(self, tag: Literal["morx"]) -> _m_o_r_x.table__m_o_r_x | None: ... | |
| def get(self, tag: Literal["name"]) -> _n_a_m_e.table__n_a_m_e | None: ... | |
| def get(self, tag: Literal["opbd"]) -> _o_p_b_d.table__o_p_b_d | None: ... | |
| def get(self, tag: Literal["post"]) -> _p_o_s_t.table__p_o_s_t | None: ... | |
| def get(self, tag: Literal["prep"]) -> _p_r_e_p.table__p_r_e_p | None: ... | |
| def get(self, tag: Literal["prop"]) -> _p_r_o_p.table__p_r_o_p | None: ... | |
| def get(self, tag: Literal["sbix"]) -> _s_b_i_x.table__s_b_i_x | None: ... | |
| def get(self, tag: Literal["trak"]) -> _t_r_a_k.table__t_r_a_k | None: ... | |
| def get(self, tag: Literal["vhea"]) -> _v_h_e_a.table__v_h_e_a | None: ... | |
| def get(self, tag: Literal["vmtx"]) -> _v_m_t_x.table__v_m_t_x | None: ... | |
| def get(self, tag: Literal["GlyphOrder"]) -> GlyphOrder: ... | |
| def get(self, tag: str | bytes) -> DefaultTable | GlyphOrder | Any | None: ... | |
| 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: list[str]) -> None: | |
| """Set the glyph order | |
| Args: | |
| glyphOrder ([str]): List of glyph names in order. | |
| """ | |
| self.glyphOrder = glyphOrder | |
| if hasattr(self, "_reverseGlyphOrderDict"): | |
| del self._reverseGlyphOrderDict | |
| if self.isLoaded("glyf"): | |
| self["glyf"].setGlyphOrder(glyphOrder) | |
| def getGlyphOrder(self) -> list[str]: | |
| """Returns a list of glyph names ordered by their position in the font.""" | |
| try: | |
| return self.glyphOrder | |
| except AttributeError: | |
| pass | |
| if "CFF " in self: | |
| cff = self["CFF "] | |
| self.glyphOrder = cff.getGlyphOrder() | |
| elif "post" in self: | |
| # TrueType font | |
| glyphOrder = self["post"].getGlyphOrder() | |
| if glyphOrder is None: | |
| # | |
| # No names found in the 'post' table. | |
| # Try to create glyph names from the unicode cmap (if available) | |
| # in combination with the Adobe Glyph List (AGL). | |
| # | |
| self._getGlyphNamesFromCmap() | |
| elif len(glyphOrder) < self["maxp"].numGlyphs: | |
| # | |
| # Not enough names found in the 'post' table. | |
| # Can happen when 'post' format 1 is improperly used on a font that | |
| # has more than 258 glyphs (the length of 'standardGlyphOrder'). | |
| # | |
| log.warning( | |
| "Not enough names found in the 'post' table, generating them from cmap instead" | |
| ) | |
| self._getGlyphNamesFromCmap() | |
| else: | |
| self.glyphOrder = glyphOrder | |
| else: | |
| self._getGlyphNamesFromCmap() | |
| return self.glyphOrder | |
| 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 | |
| # build glyph names (eg. because there is only a minimal post table, | |
| # or none at all). | |
| # - but the cmap parser also needs glyph names to work with... | |
| # So here's what we do: | |
| # - make up glyph names based on glyphID | |
| # - load a temporary cmap table based on those names | |
| # - extract the unicode values, build the "real" glyph names | |
| # - unload the temporary cmap table | |
| # | |
| if self.isLoaded("cmap"): | |
| # Bootstrapping: we're getting called by the cmap parser | |
| # itself. This means self.tables['cmap'] contains a partially | |
| # loaded cmap, making it impossible to get at a unicode | |
| # subtable here. We remove the partially loaded cmap and | |
| # restore it later. | |
| # This only happens if the cmap table is loaded before any | |
| # other table that does f.getGlyphOrder() or f.getGlyphName(). | |
| cmapLoading = self.tables["cmap"] | |
| del self.tables["cmap"] | |
| else: | |
| cmapLoading = None | |
| # Make up glyph names based on glyphID, which will be used by the | |
| # temporary cmap and by the real cmap in case we don't find a unicode | |
| # cmap. | |
| numGlyphs = int(self["maxp"].numGlyphs) | |
| glyphOrder = ["glyph%.5d" % i for i in range(numGlyphs)] | |
| glyphOrder[0] = ".notdef" | |
| # Set the glyph order, so the cmap parser has something | |
| # to work with (so we don't get called recursively). | |
| self.glyphOrder = glyphOrder | |
| # Make up glyph names based on the reversed cmap table. Because some | |
| # glyphs (eg. ligatures or alternates) may not be reachable via cmap, | |
| # this naming table will usually not cover all glyphs in the font. | |
| # If the font has no Unicode cmap table, reversecmap will be empty. | |
| if "cmap" in self: | |
| reversecmap = self["cmap"].buildReversedMin() | |
| else: | |
| reversecmap = {} | |
| useCount = {} | |
| for i, tempName in enumerate(glyphOrder): | |
| if tempName in reversecmap: | |
| # If a font maps both U+0041 LATIN CAPITAL LETTER A and | |
| # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, | |
| # we prefer naming the glyph as "A". | |
| glyphName = self._makeGlyphName(reversecmap[tempName]) | |
| numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 | |
| if numUses > 1: | |
| glyphName = "%s.alt%d" % (glyphName, numUses - 1) | |
| glyphOrder[i] = glyphName | |
| if "cmap" in self: | |
| # Delete the temporary cmap table from the cache, so it can | |
| # be parsed again with the right names. | |
| del self.tables["cmap"] | |
| self.glyphOrder = glyphOrder | |
| if cmapLoading: | |
| # restore partially loaded cmap, so it can continue loading | |
| # using the proper names. | |
| self.tables["cmap"] = cmapLoading | |
| def _makeGlyphName(codepoint: int) -> str: | |
| from fontTools import agl # Adobe Glyph List | |
| if codepoint in agl.UV2AGL: | |
| return agl.UV2AGL[codepoint] | |
| elif codepoint <= 0xFFFF: | |
| return "uni%04X" % codepoint | |
| else: | |
| return "u%X" % codepoint | |
| def getGlyphNames(self) -> list[str]: | |
| """Get a list of glyph names, sorted alphabetically.""" | |
| glyphNames = sorted(self.getGlyphOrder()) | |
| return glyphNames | |
| def getGlyphNames2(self) -> list[str]: | |
| """Get a list of glyph names, sorted alphabetically, | |
| but not case sensitive. | |
| """ | |
| from fontTools.misc import textTools | |
| return textTools.caselessSort(self.getGlyphOrder()) | |
| 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 | |
| ```XXXXX`` is the zero-padded glyph ID. | |
| """ | |
| try: | |
| return self.getGlyphOrder()[glyphID] | |
| except IndexError: | |
| return "glyph%.5d" % glyphID | |
| 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: str) -> int: | |
| """Returns the ID of the glyph with the given name.""" | |
| try: | |
| return self.getReverseGlyphMap()[glyphName] | |
| except KeyError: | |
| if glyphName[:5] == "glyph": | |
| try: | |
| return int(glyphName[5:]) | |
| except (NameError, ValueError): | |
| raise KeyError(glyphName) | |
| raise | |
| def getGlyphIDMany(self, lst: Sequence[str]) -> list[int]: | |
| """Converts a list of glyph names into a list of glyph IDs.""" | |
| d = self.getReverseGlyphMap() | |
| try: | |
| return [d[glyphName] for glyphName in lst] | |
| except KeyError: | |
| getGlyphID = self.getGlyphID | |
| return [getGlyphID(glyphName) for glyphName in lst] | |
| 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) -> dict[str, int]: | |
| self._reverseGlyphOrderDict = d = {} | |
| for glyphID, glyphName in enumerate(self.getGlyphOrder()): | |
| d[glyphName] = glyphID | |
| return d | |
| 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. | |
| """ | |
| if tag in done: | |
| return | |
| tableClass = getTableClass(tag) | |
| for masterTable in tableClass.dependencies: | |
| if masterTable not in done: | |
| if masterTable in self: | |
| self._writeTable(masterTable, writer, done, tableCache) | |
| else: | |
| done.append(masterTable) | |
| done.append(tag) | |
| tabledata = self.getTableData(tag) | |
| if tableCache is not None: | |
| entry = tableCache.get((Tag(tag), tabledata)) | |
| if entry is not None: | |
| log.debug("reusing '%s' table", tag) | |
| writer.setEntry(tag, entry) | |
| return | |
| log.debug("Writing '%s' table to disk", tag) | |
| writer[tag] = tabledata | |
| if tableCache is not None: | |
| tableCache[(Tag(tag), tabledata)] = writer[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 | |
| binary and returned; if it is not currently loaded, the binary data is | |
| read from the font file and returned. | |
| """ | |
| tag = Tag(tag) | |
| if self.isLoaded(tag): | |
| log.debug("Compiling '%s' table", tag) | |
| return self.tables[tag].compile(self) | |
| elif self.reader and tag in self.reader: | |
| log.debug("Reading '%s' table from disk", tag) | |
| return self.reader[tag] | |
| else: | |
| raise KeyError(tag) | |
| def getGlyphSet( | |
| 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 | |
| have an attribute named 'width'. | |
| If the font is CFF-based, the outlines will be taken from the ``CFF `` | |
| or ``CFF2`` tables. Otherwise the outlines will be taken from the | |
| ``glyf`` table. | |
| If the font contains both a ``CFF ``/``CFF2`` and a ``glyf`` table, you | |
| can use the ``preferCFF`` argument to specify which one should be taken. | |
| If the font contains both a ``CFF `` and a ``CFF2`` table, the latter is | |
| taken. | |
| If the ``location`` parameter is set, it should be a dictionary mapping | |
| four-letter variation tags to their float values, and the returned | |
| glyph-set will represent an instance of a variable font at that | |
| location. | |
| If the ``normalized`` variable is set to True, that location is | |
| interpreted as in the normalized (-1..+1) space, otherwise it is in the | |
| font's defined axes space. | |
| """ | |
| if location and "fvar" not in self: | |
| location = None | |
| if location and not normalized: | |
| location = self.normalizeLocation(location) | |
| glyphSet = None | |
| if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self): | |
| glyphSet = _TTGlyphSetCFF(self, location) | |
| elif "glyf" in self: | |
| glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) | |
| else: | |
| raise TTLibError("Font contains no outlines") | |
| if "VARC" in self: | |
| glyphSet = _TTGlyphSetVARC(self, location, glyphSet) | |
| return glyphSet | |
| 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. | |
| The ``location`` parameter should be a dictionary mapping four-letter | |
| variation tags to their float values. | |
| Raises ``TTLibError`` if the font is not a variable font. | |
| """ | |
| from fontTools.varLib.models import normalizeLocation | |
| if "fvar" not in self: | |
| raise TTLibError("Not a variable font") | |
| axes = self["fvar"].getAxes() | |
| location = normalizeLocation(location, axes) | |
| if "avar" in self: | |
| location = self["avar"].renormalizeLocation(location, self) | |
| return location | |
| def getBestCmap( | |
| self, | |
| cmapPreferences: Sequence[tuple[int, int]] = ( | |
| (3, 10), | |
| (0, 6), | |
| (0, 4), | |
| (3, 1), | |
| (0, 3), | |
| (0, 2), | |
| (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. | |
| By default it will search for the following (platformID, platEncID) | |
| pairs in order:: | |
| (3, 10), # Windows Unicode full repertoire | |
| (0, 6), # Unicode full repertoire (format 13 subtable) | |
| (0, 4), # Unicode 2.0 full repertoire | |
| (3, 1), # Windows Unicode BMP | |
| (0, 3), # Unicode 2.0 BMP | |
| (0, 2), # Unicode ISO/IEC 10646 | |
| (0, 1), # Unicode 1.1 | |
| (0, 0) # Unicode 1.0 | |
| This particular order matches what HarfBuzz uses to choose what | |
| subtable to use by default. This order prefers the largest-repertoire | |
| subtable, and among those, prefers the Windows-platform over the | |
| Unicode-platform as the former has wider support. | |
| This order can be customized via the ``cmapPreferences`` argument. | |
| """ | |
| return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) | |
| def reorderGlyphs(self, new_glyph_order: list[str]) -> None: | |
| from .reorderGlyphs import reorderGlyphs | |
| reorderGlyphs(self, new_glyph_order) | |
| class GlyphOrder(object): | |
| """A pseudo table. The glyph order isn't in the font as a separate | |
| table, but it's nice to present it as such in the TTX format. | |
| """ | |
| def __init__(self, tag: str | None = None) -> None: | |
| pass | |
| 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." | |
| ) | |
| writer.newline() | |
| for i, glyphName in enumerate(glyphOrder): | |
| writer.simpletag("GlyphID", id=i, name=glyphName) | |
| writer.newline() | |
| 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": | |
| self.glyphOrder.append(attrs["name"]) | |
| ttFont.setGlyphOrder(self.glyphOrder) | |
| def getTableModule(tag: str | bytes) -> ModuleType | None: | |
| """Fetch the packer/unpacker module for a table. | |
| Return None when no module is found. | |
| """ | |
| from . import tables | |
| pyTag = tagToIdentifier(tag) | |
| try: | |
| __import__("fontTools.ttLib.tables." + pyTag) | |
| except ImportError as err: | |
| # If pyTag is found in the ImportError message, | |
| # means table is not implemented. If it's not | |
| # there, then some other module is missing, don't | |
| # suppress the error. | |
| if str(err).find(pyTag) >= 0: | |
| return None | |
| else: | |
| raise err | |
| else: | |
| return getattr(tables, pyTag) | |
| # Registry for custom table packer/unpacker classes. Keys are table | |
| # tags, values are (moduleName, className) tuples. | |
| # See registerCustomTableClass() and getCustomTableClass() | |
| _customTableRegistry: dict[str | bytes, tuple[str, str]] = {} | |
| 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' | |
| is given, it is derived from the tag, for example it will be | |
| ``table_C_U_S_T_`` for a 'CUST' tag. | |
| The registered table class should be a subclass of | |
| :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable` | |
| """ | |
| if className is None: | |
| className = "table_" + tagToIdentifier(tag) | |
| _customTableRegistry[tag] = (moduleName, className) | |
| def unregisterCustomTableClass(tag: str | bytes) -> None: | |
| """Unregister the custom packer/unpacker class for a table.""" | |
| del _customTableRegistry[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. | |
| """ | |
| if tag not in _customTableRegistry: | |
| return None | |
| import importlib | |
| moduleName, className = _customTableRegistry[tag] | |
| module = importlib.import_module(moduleName) | |
| return getattr(module, className) | |
| def getTableClass(tag: str | bytes) -> type[DefaultTable]: | |
| """Fetch the packer/unpacker class for a table.""" | |
| tableClass = getCustomTableClass(tag) | |
| if tableClass is not None: | |
| return tableClass | |
| module = getTableModule(tag) | |
| if module is None: | |
| from .tables.DefaultTable import DefaultTable | |
| return DefaultTable | |
| pyTag = tagToIdentifier(tag) | |
| tableClass = getattr(module, "table_" + pyTag) | |
| return tableClass | |
| def getClassTag(klass: type[DefaultTable]) -> str | bytes: | |
| """Fetch the table tag for a class object.""" | |
| name = klass.__name__ | |
| assert name[:6] == "table_" | |
| name = name[6:] # Chop 'table_' | |
| return identifierToTag(name) | |
| def newTable(tag: str | bytes) -> DefaultTable: | |
| """Return a new instance of a table.""" | |
| tableClass = getTableClass(tag) | |
| return tableClass(tag) | |
| def _escapechar(c: str) -> str: | |
| """Helper function for tagToIdentifier()""" | |
| import re | |
| if re.match("[a-z0-9]", c): | |
| return "_" + c | |
| elif re.match("[A-Z]", c): | |
| return c + "_" | |
| else: | |
| return hex(byteord(c))[2:] | |
| 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. | |
| Lowercase letters get an underscore before the letter, uppercase | |
| letters get an underscore after the letter. Trailing spaces are | |
| trimmed. Illegal characters are escaped as two hex bytes. If the | |
| result starts with a number (as the result of a hex escape), an | |
| extra underscore is prepended. Examples: | |
| .. code-block:: pycon | |
| >>> | |
| >> tagToIdentifier('glyf') | |
| '_g_l_y_f' | |
| >> tagToIdentifier('cvt ') | |
| '_c_v_t' | |
| >> tagToIdentifier('OS/2') | |
| 'O_S_2f_2' | |
| """ | |
| import re | |
| tag = Tag(tag) | |
| if tag == "GlyphOrder": | |
| return tag | |
| assert len(tag) == 4, "tag should be 4 characters long" | |
| while len(tag) > 1 and tag[-1] == " ": | |
| tag = tag[:-1] | |
| ident = "" | |
| for c in tag: | |
| ident = ident + _escapechar(c) | |
| if re.match("[0-9]", ident): | |
| ident = "_" + ident | |
| return ident | |
| def identifierToTag(ident: str) -> str: | |
| """the opposite of tagToIdentifier()""" | |
| if ident == "GlyphOrder": | |
| return ident | |
| if len(ident) % 2 and ident[0] == "_": | |
| ident = ident[1:] | |
| assert not (len(ident) % 2) | |
| tag = "" | |
| for i in range(0, len(ident), 2): | |
| if ident[i] == "_": | |
| tag = tag + ident[i + 1] | |
| elif ident[i + 1] == "_": | |
| tag = tag + ident[i] | |
| else: | |
| # assume hex | |
| tag = tag + chr(int(ident[i : i + 2], 16)) | |
| # append trailing spaces | |
| tag = tag + (4 - len(tag)) * " " | |
| return Tag(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. | |
| """ | |
| import re | |
| tag = Tag(tag) | |
| if tag == "OS/2": | |
| return "OS_2" | |
| elif tag == "GlyphOrder": | |
| return tag | |
| if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): | |
| return tag.strip() | |
| else: | |
| return tagToIdentifier(tag) | |
| def xmlToTag(tag: str) -> str: | |
| """The opposite of tagToXML()""" | |
| if tag == "OS_2": | |
| return Tag("OS/2") | |
| if len(tag) == 8: | |
| return identifierToTag(tag) | |
| else: | |
| return Tag(tag + " " * (4 - len(tag))) | |
| # Table order as recommended in the OpenType specification 1.4 | |
| TTFTableOrder = [ | |
| "head", | |
| "hhea", | |
| "maxp", | |
| "OS/2", | |
| "hmtx", | |
| "LTSH", | |
| "VDMX", | |
| "hdmx", | |
| "cmap", | |
| "fpgm", | |
| "prep", | |
| "cvt ", | |
| "loca", | |
| "glyf", | |
| "kern", | |
| "name", | |
| "post", | |
| "gasp", | |
| "PCLT", | |
| ] | |
| OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "] | |
| 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. | |
| """ | |
| tagList = sorted(tagList) | |
| if tableOrder is None: | |
| if "DSIG" in tagList: | |
| # DSIG should be last (XXX spec reference?) | |
| tagList.remove("DSIG") | |
| tagList.append("DSIG") | |
| if "CFF " in tagList: | |
| tableOrder = OTFTableOrder | |
| else: | |
| tableOrder = TTFTableOrder | |
| orderedTables = [] | |
| for tag in tableOrder: | |
| if tag in tagList: | |
| orderedTables.append(tag) | |
| tagList.remove(tag) | |
| orderedTables.extend(tagList) | |
| return orderedTables | |
| 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. | |
| """ | |
| inFile.seek(0) | |
| outFile.seek(0) | |
| reader = SFNTReader(inFile, checkChecksums=checkChecksums) | |
| writer = SFNTWriter( | |
| outFile, | |
| len(reader.tables), | |
| reader.sfntVersion, | |
| reader.flavor, | |
| reader.flavorData, | |
| ) | |
| tables = list(reader.keys()) | |
| for tag in sortedTagList(tables, tableOrder): | |
| writer[tag] = reader[tag] | |
| writer.close() | |
| def maxPowerOfTwo(x: int) -> int: | |
| """Return the highest exponent of two, so that | |
| (2 ** exponent) <= x. Return 0 if x is 0. | |
| """ | |
| exponent = 0 | |
| while x: | |
| x = x >> 1 | |
| exponent = exponent + 1 | |
| return max(exponent - 1, 0) | |
| 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. | |
| exponent = maxPowerOfTwo(n) | |
| searchRange = (2**exponent) * itemSize | |
| entrySelector = exponent | |
| rangeShift = max(0, n * itemSize - searchRange) | |
| return searchRange, entrySelector, rangeShift | |