1from __future__ import annotations
2
3import logging
4import os
5import traceback
6from io import BytesIO, StringIO, UnsupportedOperation
7from typing import TYPE_CHECKING, TypedDict, TypeVar, overload
8
9from fontTools.config import Config
10from fontTools.misc import xmlWriter
11from fontTools.misc.configTools import AbstractConfig
12from fontTools.misc.loggingTools import deprecateArgument
13from fontTools.misc.textTools import Tag, byteord, tostr
14from fontTools.ttLib import TTLibError
15from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
16from fontTools.ttLib.ttGlyphSet import (
17 _TTGlyph, # noqa: F401
18 _TTGlyphSet,
19 _TTGlyphSetCFF,
20 _TTGlyphSetGlyf,
21 _TTGlyphSetVARC,
22)
23
24if TYPE_CHECKING:
25 from collections.abc import Mapping, MutableMapping
26 from types import ModuleType, TracebackType
27 from typing import Any, BinaryIO, Literal, Sequence, TextIO
28
29 from typing_extensions import Self, Unpack
30
31 from fontTools.ttLib.tables import (
32 B_A_S_E_,
33 C_B_D_T_,
34 C_B_L_C_,
35 C_F_F_,
36 C_F_F__2,
37 C_O_L_R_,
38 C_P_A_L_,
39 D_S_I_G_,
40 E_B_D_T_,
41 E_B_L_C_,
42 F_F_T_M_,
43 G_D_E_F_,
44 G_M_A_P_,
45 G_P_K_G_,
46 G_P_O_S_,
47 G_S_U_B_,
48 G_V_A_R_,
49 H_V_A_R_,
50 J_S_T_F_,
51 L_T_S_H_,
52 M_A_T_H_,
53 M_E_T_A_,
54 M_V_A_R_,
55 S_I_N_G_,
56 S_T_A_T_,
57 S_V_G_,
58 T_S_I__0,
59 T_S_I__1,
60 T_S_I__2,
61 T_S_I__3,
62 T_S_I__5,
63 T_S_I_B_,
64 T_S_I_C_,
65 T_S_I_D_,
66 T_S_I_J_,
67 T_S_I_P_,
68 T_S_I_S_,
69 T_S_I_V_,
70 T_T_F_A_,
71 V_A_R_C_,
72 V_D_M_X_,
73 V_O_R_G_,
74 V_V_A_R_,
75 D__e_b_g,
76 F__e_a_t,
77 G__l_a_t,
78 G__l_o_c,
79 O_S_2f_2,
80 S__i_l_f,
81 S__i_l_l,
82 _a_n_k_r,
83 _a_v_a_r,
84 _b_s_l_n,
85 _c_i_d_g,
86 _c_m_a_p,
87 _c_v_a_r,
88 _c_v_t,
89 _f_e_a_t,
90 _f_p_g_m,
91 _f_v_a_r,
92 _g_a_s_p,
93 _g_c_i_d,
94 _g_l_y_f,
95 _g_v_a_r,
96 _h_d_m_x,
97 _h_e_a_d,
98 _h_h_e_a,
99 _h_m_t_x,
100 _k_e_r_n,
101 _l_c_a_r,
102 _l_o_c_a,
103 _l_t_a_g,
104 _m_a_x_p,
105 _m_e_t_a,
106 _m_o_r_t,
107 _m_o_r_x,
108 _n_a_m_e,
109 _o_p_b_d,
110 _p_o_s_t,
111 _p_r_e_p,
112 _p_r_o_p,
113 _s_b_i_x,
114 _t_r_a_k,
115 _v_h_e_a,
116 _v_m_t_x,
117 )
118 from fontTools.ttLib.tables.DefaultTable import DefaultTable
119
120 _VT_co = TypeVar("_VT_co", covariant=True) # Value type covariant containers.
121
122log = logging.getLogger(__name__)
123
124
125_NumberT = TypeVar("_NumberT", bound=float)
126
127
128class TTFont(object):
129 """Represents a TrueType font.
130
131 The object manages file input and output, and offers a convenient way of
132 accessing tables. Tables will be only decompiled when necessary, ie. when
133 they're actually accessed. This means that simple operations can be extremely fast.
134
135 Example usage:
136
137 .. code-block:: pycon
138
139 >>>
140 >> from fontTools import ttLib
141 >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file
142 >> tt['maxp'].numGlyphs
143 242
144 >> tt['OS/2'].achVendID
145 'B&H\000'
146 >> tt['head'].unitsPerEm
147 2048
148
149 For details of the objects returned when accessing each table, see the
150 :doc:`tables </ttLib/tables>` documentation.
151 To add a table to the font, use the :py:func:`newTable` function:
152
153 .. code-block:: pycon
154
155 >>>
156 >> os2 = newTable("OS/2")
157 >> os2.version = 4
158 >> # set other attributes
159 >> font["OS/2"] = os2
160
161 TrueType fonts can also be serialized to and from XML format (see also the
162 :doc:`ttx </ttx>` binary):
163
164 .. code-block:: pycon
165
166 >>
167 >> tt.saveXML("afont.ttx")
168 Dumping 'LTSH' table...
169 Dumping 'OS/2' table...
170 [...]
171
172 >> tt2 = ttLib.TTFont() # Create a new font object
173 >> tt2.importXML("afont.ttx")
174 >> tt2['maxp'].numGlyphs
175 242
176
177 The TTFont object may be used as a context manager; this will cause the file
178 reader to be closed after the context ``with`` block is exited::
179
180 with TTFont(filename) as f:
181 # Do stuff
182
183 Args:
184 file: When reading a font from disk, either a pathname pointing to a file,
185 or a readable file object.
186 res_name_or_index: If running on a Macintosh, either a sfnt resource name or
187 an sfnt resource index number. If the index number is zero, TTLib will
188 autodetect whether the file is a flat file or a suitcase. (If it is a suitcase,
189 only the first 'sfnt' resource will be read.)
190 sfntVersion (str): When constructing a font object from scratch, sets the four-byte
191 sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create
192 an OpenType file, use ``OTTO``.
193 flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2
194 file.
195 checkChecksums (int): How checksum data should be treated. Default is 0
196 (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to
197 raise an exception if any wrong checksums are found.
198 recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``,
199 ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save.
200 Also compiles the glyphs on importing, which saves memory consumption and
201 time.
202 ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation
203 will be ignored, and the binary data will be returned for those tables instead.
204 recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in
205 the ``head`` table on save.
206 fontNumber (int): The index of the font in a TrueType Collection file.
207 lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon
208 access only. If it is set to False, many data structures are loaded immediately.
209 The default is ``lazy=None`` which is somewhere in between.
210 """
211
212 tables: dict[Tag, DefaultTable | GlyphOrder]
213 reader: SFNTReader | None
214 sfntVersion: str
215 flavor: str | None
216 flavorData: Any | None
217 lazy: bool | None
218 recalcBBoxes: bool
219 recalcTimestamp: bool
220 ignoreDecompileErrors: bool
221 cfg: AbstractConfig
222 glyphOrder: list[str]
223 _reverseGlyphOrderDict: dict[str, int]
224 _tableCache: MutableMapping[tuple[Tag, bytes], DefaultTable] | None
225 disassembleInstructions: bool
226 bitmapGlyphDataFormat: str
227 # Deprecated attributes
228 verbose: bool | None
229 quiet: bool | None
230
231 def __init__(
232 self,
233 file: str | os.PathLike[str] | BinaryIO | None = None,
234 res_name_or_index: str | int | None = None,
235 sfntVersion: str = "\000\001\000\000",
236 flavor: str | None = None,
237 checkChecksums: int = 0,
238 verbose: bool | None = None, # Deprecated
239 recalcBBoxes: bool = True,
240 allowVID: Any = NotImplemented, # Deprecated/Unused
241 ignoreDecompileErrors: bool = False,
242 recalcTimestamp: bool = True,
243 fontNumber: int = -1,
244 lazy: bool | None = None,
245 quiet: bool | None = None, # Deprecated
246 _tableCache: MutableMapping[tuple[Tag, bytes], DefaultTable] | None = None,
247 cfg: Mapping[str, Any] | AbstractConfig = {},
248 ) -> None:
249 # Set deprecated attributes
250 for name in ("verbose", "quiet"):
251 val = locals().get(name)
252 if val is not None:
253 deprecateArgument(name, "configure logging instead")
254 setattr(self, name, val)
255
256 self.lazy = lazy
257 self.recalcBBoxes = recalcBBoxes
258 self.recalcTimestamp = recalcTimestamp
259 self.tables = {}
260 self.reader = None
261 self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg)
262 self.ignoreDecompileErrors = ignoreDecompileErrors
263
264 if not file:
265 self.sfntVersion = sfntVersion
266 self.flavor = flavor
267 self.flavorData = None
268 return
269 seekable = True
270 if not hasattr(file, "read"):
271 if not isinstance(file, (str, os.PathLike)):
272 raise TypeError(
273 "fileOrPath must be a file path (str or PathLike) if it isn't an object with a `read` method."
274 )
275 closeStream = True
276 # assume file is a string
277 if res_name_or_index is not None:
278 # see if it contains 'sfnt' resources in the resource or data fork
279 from . import macUtils
280
281 if res_name_or_index == 0:
282 if macUtils.getSFNTResIndices(file):
283 # get the first available sfnt font.
284 file = macUtils.SFNTResourceReader(file, 1)
285 else:
286 file = open(file, "rb")
287 else:
288 file = macUtils.SFNTResourceReader(file, res_name_or_index)
289 else:
290 file = open(file, "rb")
291 else:
292 # assume "file" is a readable file object
293 assert not isinstance(file, (str, os.PathLike))
294 closeStream = False
295 # SFNTReader wants the input file to be seekable.
296 # SpooledTemporaryFile has no seekable() on < 3.11, but still can seek:
297 # https://github.com/fonttools/fonttools/issues/3052
298 if hasattr(file, "seekable"):
299 seekable = file.seekable()
300 elif hasattr(file, "seek"):
301 try:
302 file.seek(0)
303 except UnsupportedOperation:
304 seekable = False
305
306 if not self.lazy:
307 # read input file in memory and wrap a stream around it to allow overwriting
308 if seekable:
309 file.seek(0)
310 tmp = BytesIO(file.read())
311 if hasattr(file, "name"):
312 # save reference to input file name
313 tmp.name = file.name
314 if closeStream:
315 file.close()
316 file = tmp
317 elif not seekable:
318 raise TTLibError("Input file must be seekable when lazy=True")
319 self._tableCache = _tableCache
320 self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber)
321 self.sfntVersion = self.reader.sfntVersion
322 self.flavor = self.reader.flavor
323 self.flavorData = self.reader.flavorData
324
325 def __enter__(self) -> Self:
326 return self
327
328 def __exit__(
329 self,
330 exc_type: type[BaseException] | None,
331 exc_value: BaseException | None,
332 traceback: TracebackType | None,
333 ) -> None:
334 self.close()
335
336 def close(self) -> None:
337 """If we still have a reader object, close it."""
338 if self.reader is not None:
339 self.reader.close()
340 self.reader = None
341
342 def save(
343 self, file: str | os.PathLike[str] | BinaryIO, reorderTables: bool | None = True
344 ) -> None:
345 """Save the font to disk.
346
347 Args:
348 file: Similarly to the constructor, can be either a pathname or a writable
349 binary file object.
350 reorderTables (Option[bool]): If true (the default), reorder the tables,
351 sorting them by tag (recommended by the OpenType specification). If
352 false, retain the original font order. If None, reorder by table
353 dependency (fastest).
354 """
355 if not hasattr(file, "write"):
356 if self.lazy and self.reader.file.name == file:
357 raise TTLibError("Can't overwrite TTFont when 'lazy' attribute is True")
358 createStream = True
359 else:
360 # assume "file" is a writable file object
361 createStream = False
362
363 tmp = BytesIO()
364
365 writer_reordersTables = self._save(tmp)
366
367 if not (
368 reorderTables is None
369 or writer_reordersTables
370 or (reorderTables is False and self.reader is None)
371 ):
372 if reorderTables is False:
373 # sort tables using the original font's order
374 if self.reader is None:
375 raise TTLibError(
376 "The original table order is unavailable because there isn't a font to read it from."
377 )
378 tableOrder = list(self.reader.keys())
379 else:
380 # use the recommended order from the OpenType specification
381 tableOrder = None
382 tmp.flush()
383 tmp2 = BytesIO()
384 reorderFontTables(tmp, tmp2, tableOrder)
385 tmp.close()
386 tmp = tmp2
387
388 if createStream:
389 # "file" is a path
390 assert isinstance(file, (str, os.PathLike))
391 with open(file, "wb") as file:
392 file.write(tmp.getvalue())
393 else:
394 assert not isinstance(file, (str, os.PathLike))
395 file.write(tmp.getvalue())
396
397 tmp.close()
398
399 def _save(
400 self,
401 file: BinaryIO,
402 tableCache: MutableMapping[tuple[Tag, bytes], Any] | None = None,
403 ) -> bool:
404 """Internal function, to be shared by save() and TTCollection.save()"""
405
406 if self.recalcTimestamp and "head" in self:
407 # make sure 'head' is loaded so the recalculation is actually done
408 self["head"]
409
410 tags = self.keys()
411 tags.pop(0) # skip GlyphOrder tag
412 numTables = len(tags)
413 # write to a temporary stream to allow saving to unseekable streams
414 writer = SFNTWriter(
415 file, numTables, self.sfntVersion, self.flavor, self.flavorData
416 )
417
418 done = []
419 for tag in tags:
420 self._writeTable(tag, writer, done, tableCache)
421
422 writer.close()
423
424 return writer.reordersTables()
425
426 class XMLSavingOptions(TypedDict):
427 writeVersion: bool
428 quiet: bool | None
429 tables: Sequence[str | bytes] | None
430 skipTables: Sequence[str] | None
431 splitTables: bool
432 splitGlyphs: bool
433 disassembleInstructions: bool
434 bitmapGlyphDataFormat: str
435
436 def saveXML(
437 self,
438 fileOrPath: str | os.PathLike[str] | BinaryIO | TextIO,
439 newlinestr: str = "\n",
440 **kwargs: Unpack[XMLSavingOptions],
441 ) -> None:
442 """Export the font as TTX (an XML-based text file), or as a series of text
443 files when splitTables is true. In the latter case, the 'fileOrPath'
444 argument should be a path to a directory.
445 The 'tables' argument must either be false (dump all tables) or a
446 list of tables to dump. The 'skipTables' argument may be a list of tables
447 to skip, but only when the 'tables' argument is false.
448 """
449
450 writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
451 self._saveXML(writer, **kwargs)
452 writer.close()
453
454 def _saveXML(
455 self,
456 writer: xmlWriter.XMLWriter,
457 writeVersion: bool = True,
458 quiet: bool | None = None, # Deprecated
459 tables: Sequence[str | bytes] | None = None,
460 skipTables: Sequence[str] | None = None,
461 splitTables: bool = False,
462 splitGlyphs: bool = False,
463 disassembleInstructions: bool = True,
464 bitmapGlyphDataFormat: str = "raw",
465 ) -> None:
466 if quiet is not None:
467 deprecateArgument("quiet", "configure logging instead")
468
469 self.disassembleInstructions = disassembleInstructions
470 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat
471 if not tables:
472 tables = self.keys()
473 if skipTables:
474 tables = [tag for tag in tables if tag not in skipTables]
475
476 if writeVersion:
477 from fontTools import version
478
479 version = ".".join(version.split(".")[:2])
480 writer.begintag(
481 "ttFont",
482 sfntVersion=repr(tostr(self.sfntVersion))[1:-1],
483 ttLibVersion=version,
484 )
485 else:
486 writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1])
487 writer.newline()
488
489 # always splitTables if splitGlyphs is enabled
490 splitTables = splitTables or splitGlyphs
491
492 if not splitTables:
493 writer.newline()
494 else:
495 if writer.filename is None:
496 raise TTLibError(
497 "splitTables requires the file name to be a file system path, not a stream."
498 )
499 path, ext = os.path.splitext(writer.filename)
500
501 for tag in tables:
502 if splitTables:
503 tablePath = path + "." + tagToIdentifier(tag) + ext
504 tableWriter = xmlWriter.XMLWriter(
505 tablePath, newlinestr=writer.newlinestr
506 )
507 tableWriter.begintag("ttFont", ttLibVersion=version)
508 tableWriter.newline()
509 tableWriter.newline()
510 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
511 writer.newline()
512 else:
513 tableWriter = writer
514 self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs)
515 if splitTables:
516 tableWriter.endtag("ttFont")
517 tableWriter.newline()
518 tableWriter.close()
519 writer.endtag("ttFont")
520 writer.newline()
521
522 def _tableToXML(
523 self,
524 writer: xmlWriter.XMLWriter,
525 tag: str | bytes,
526 quiet: bool | None = None,
527 splitGlyphs: bool = False,
528 ) -> None:
529 if quiet is not None:
530 deprecateArgument("quiet", "configure logging instead")
531 if tag in self:
532 table = self[tag]
533 report = "Dumping '%s' table..." % tag
534 else:
535 report = "No '%s' table found." % tag
536 log.info(report)
537 if tag not in self:
538 return
539 xmlTag = tagToXML(tag)
540 attrs: dict[str, Any] = {}
541 if hasattr(table, "ERROR"):
542 attrs["ERROR"] = "decompilation error"
543 from .tables.DefaultTable import DefaultTable
544
545 if table.__class__ == DefaultTable:
546 attrs["raw"] = True
547 writer.begintag(xmlTag, **attrs)
548 writer.newline()
549 if tag == "glyf":
550 table.toXML(writer, self, splitGlyphs=splitGlyphs)
551 else:
552 table.toXML(writer, self)
553 writer.endtag(xmlTag)
554 writer.newline()
555 writer.newline()
556
557 def importXML(
558 self, fileOrPath: str | os.PathLike[str] | BinaryIO, quiet: bool | None = None
559 ) -> None:
560 """Import a TTX file (an XML-based text format), so as to recreate
561 a font object.
562 """
563 if quiet is not None:
564 deprecateArgument("quiet", "configure logging instead")
565
566 if "maxp" in self and "post" in self:
567 # Make sure the glyph order is loaded, as it otherwise gets
568 # lost if the XML doesn't contain the glyph order, yet does
569 # contain the table which was originally used to extract the
570 # glyph names from (ie. 'post', 'cmap' or 'CFF ').
571 self.getGlyphOrder()
572
573 from fontTools.misc import xmlReader
574
575 reader = xmlReader.XMLReader(fileOrPath, self)
576 reader.read()
577
578 def isLoaded(self, tag: str | bytes) -> bool:
579 """Return true if the table identified by ``tag`` has been
580 decompiled and loaded into memory."""
581 return tag in self.tables
582
583 def has_key(self, tag: str | bytes) -> bool:
584 """Test if the table identified by ``tag`` is present in the font.
585
586 As well as this method, ``tag in font`` can also be used to determine the
587 presence of the table."""
588 if self.isLoaded(tag):
589 return True
590 elif self.reader and tag in self.reader:
591 return True
592 elif tag == "GlyphOrder":
593 return True
594 else:
595 return False
596
597 __contains__ = has_key
598
599 def keys(self) -> list[str]:
600 """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table."""
601 keys = list(self.tables.keys())
602 if self.reader:
603 for key in list(self.reader.keys()):
604 if key not in keys:
605 keys.append(key)
606
607 if "GlyphOrder" in keys:
608 keys.remove("GlyphOrder")
609 keys = sortedTagList(keys)
610 return ["GlyphOrder"] + keys
611
612 def ensureDecompiled(self, recurse: bool | None = None) -> None:
613 """Decompile all the tables, even if a TTFont was opened in 'lazy' mode."""
614 for tag in self.keys():
615 table = self[tag]
616 if recurse is None:
617 recurse = self.lazy is not False
618 if recurse and hasattr(table, "ensureDecompiled"):
619 table.ensureDecompiled(recurse=recurse)
620 self.lazy = False
621
622 def __len__(self) -> int:
623 return len(list(self.keys()))
624
625 @overload
626 def __getitem__(self, tag: Literal["BASE"]) -> B_A_S_E_.table_B_A_S_E_: ...
627 @overload
628 def __getitem__(self, tag: Literal["CBDT"]) -> C_B_D_T_.table_C_B_D_T_: ...
629 @overload
630 def __getitem__(self, tag: Literal["CBLC"]) -> C_B_L_C_.table_C_B_L_C_: ...
631 @overload
632 def __getitem__(self, tag: Literal["CFF "]) -> C_F_F_.table_C_F_F_: ...
633 @overload
634 def __getitem__(self, tag: Literal["CFF2"]) -> C_F_F__2.table_C_F_F__2: ...
635 @overload
636 def __getitem__(self, tag: Literal["COLR"]) -> C_O_L_R_.table_C_O_L_R_: ...
637 @overload
638 def __getitem__(self, tag: Literal["CPAL"]) -> C_P_A_L_.table_C_P_A_L_: ...
639 @overload
640 def __getitem__(self, tag: Literal["DSIG"]) -> D_S_I_G_.table_D_S_I_G_: ...
641 @overload
642 def __getitem__(self, tag: Literal["EBDT"]) -> E_B_D_T_.table_E_B_D_T_: ...
643 @overload
644 def __getitem__(self, tag: Literal["EBLC"]) -> E_B_L_C_.table_E_B_L_C_: ...
645 @overload
646 def __getitem__(self, tag: Literal["FFTM"]) -> F_F_T_M_.table_F_F_T_M_: ...
647 @overload
648 def __getitem__(self, tag: Literal["GDEF"]) -> G_D_E_F_.table_G_D_E_F_: ...
649 @overload
650 def __getitem__(self, tag: Literal["GMAP"]) -> G_M_A_P_.table_G_M_A_P_: ...
651 @overload
652 def __getitem__(self, tag: Literal["GPKG"]) -> G_P_K_G_.table_G_P_K_G_: ...
653 @overload
654 def __getitem__(self, tag: Literal["GPOS"]) -> G_P_O_S_.table_G_P_O_S_: ...
655 @overload
656 def __getitem__(self, tag: Literal["GSUB"]) -> G_S_U_B_.table_G_S_U_B_: ...
657 @overload
658 def __getitem__(self, tag: Literal["GVAR"]) -> G_V_A_R_.table_G_V_A_R_: ...
659 @overload
660 def __getitem__(self, tag: Literal["HVAR"]) -> H_V_A_R_.table_H_V_A_R_: ...
661 @overload
662 def __getitem__(self, tag: Literal["JSTF"]) -> J_S_T_F_.table_J_S_T_F_: ...
663 @overload
664 def __getitem__(self, tag: Literal["LTSH"]) -> L_T_S_H_.table_L_T_S_H_: ...
665 @overload
666 def __getitem__(self, tag: Literal["MATH"]) -> M_A_T_H_.table_M_A_T_H_: ...
667 @overload
668 def __getitem__(self, tag: Literal["META"]) -> M_E_T_A_.table_M_E_T_A_: ...
669 @overload
670 def __getitem__(self, tag: Literal["MVAR"]) -> M_V_A_R_.table_M_V_A_R_: ...
671 @overload
672 def __getitem__(self, tag: Literal["SING"]) -> S_I_N_G_.table_S_I_N_G_: ...
673 @overload
674 def __getitem__(self, tag: Literal["STAT"]) -> S_T_A_T_.table_S_T_A_T_: ...
675 @overload
676 def __getitem__(self, tag: Literal["SVG "]) -> S_V_G_.table_S_V_G_: ...
677 @overload
678 def __getitem__(self, tag: Literal["TSI0"]) -> T_S_I__0.table_T_S_I__0: ...
679 @overload
680 def __getitem__(self, tag: Literal["TSI1"]) -> T_S_I__1.table_T_S_I__1: ...
681 @overload
682 def __getitem__(self, tag: Literal["TSI2"]) -> T_S_I__2.table_T_S_I__2: ...
683 @overload
684 def __getitem__(self, tag: Literal["TSI3"]) -> T_S_I__3.table_T_S_I__3: ...
685 @overload
686 def __getitem__(self, tag: Literal["TSI5"]) -> T_S_I__5.table_T_S_I__5: ...
687 @overload
688 def __getitem__(self, tag: Literal["TSIB"]) -> T_S_I_B_.table_T_S_I_B_: ...
689 @overload
690 def __getitem__(self, tag: Literal["TSIC"]) -> T_S_I_C_.table_T_S_I_C_: ...
691 @overload
692 def __getitem__(self, tag: Literal["TSID"]) -> T_S_I_D_.table_T_S_I_D_: ...
693 @overload
694 def __getitem__(self, tag: Literal["TSIJ"]) -> T_S_I_J_.table_T_S_I_J_: ...
695 @overload
696 def __getitem__(self, tag: Literal["TSIP"]) -> T_S_I_P_.table_T_S_I_P_: ...
697 @overload
698 def __getitem__(self, tag: Literal["TSIS"]) -> T_S_I_S_.table_T_S_I_S_: ...
699 @overload
700 def __getitem__(self, tag: Literal["TSIV"]) -> T_S_I_V_.table_T_S_I_V_: ...
701 @overload
702 def __getitem__(self, tag: Literal["TTFA"]) -> T_T_F_A_.table_T_T_F_A_: ...
703 @overload
704 def __getitem__(self, tag: Literal["VARC"]) -> V_A_R_C_.table_V_A_R_C_: ...
705 @overload
706 def __getitem__(self, tag: Literal["VDMX"]) -> V_D_M_X_.table_V_D_M_X_: ...
707 @overload
708 def __getitem__(self, tag: Literal["VORG"]) -> V_O_R_G_.table_V_O_R_G_: ...
709 @overload
710 def __getitem__(self, tag: Literal["VVAR"]) -> V_V_A_R_.table_V_V_A_R_: ...
711 @overload
712 def __getitem__(self, tag: Literal["Debg"]) -> D__e_b_g.table_D__e_b_g: ...
713 @overload
714 def __getitem__(self, tag: Literal["Feat"]) -> F__e_a_t.table_F__e_a_t: ...
715 @overload
716 def __getitem__(self, tag: Literal["Glat"]) -> G__l_a_t.table_G__l_a_t: ...
717 @overload
718 def __getitem__(self, tag: Literal["Gloc"]) -> G__l_o_c.table_G__l_o_c: ...
719 @overload
720 def __getitem__(self, tag: Literal["OS/2"]) -> O_S_2f_2.table_O_S_2f_2: ...
721 @overload
722 def __getitem__(self, tag: Literal["Silf"]) -> S__i_l_f.table_S__i_l_f: ...
723 @overload
724 def __getitem__(self, tag: Literal["Sill"]) -> S__i_l_l.table_S__i_l_l: ...
725 @overload
726 def __getitem__(self, tag: Literal["ankr"]) -> _a_n_k_r.table__a_n_k_r: ...
727 @overload
728 def __getitem__(self, tag: Literal["avar"]) -> _a_v_a_r.table__a_v_a_r: ...
729 @overload
730 def __getitem__(self, tag: Literal["bsln"]) -> _b_s_l_n.table__b_s_l_n: ...
731 @overload
732 def __getitem__(self, tag: Literal["cidg"]) -> _c_i_d_g.table__c_i_d_g: ...
733 @overload
734 def __getitem__(self, tag: Literal["cmap"]) -> _c_m_a_p.table__c_m_a_p: ...
735 @overload
736 def __getitem__(self, tag: Literal["cvar"]) -> _c_v_a_r.table__c_v_a_r: ...
737 @overload
738 def __getitem__(self, tag: Literal["cvt "]) -> _c_v_t.table__c_v_t: ...
739 @overload
740 def __getitem__(self, tag: Literal["feat"]) -> _f_e_a_t.table__f_e_a_t: ...
741 @overload
742 def __getitem__(self, tag: Literal["fpgm"]) -> _f_p_g_m.table__f_p_g_m: ...
743 @overload
744 def __getitem__(self, tag: Literal["fvar"]) -> _f_v_a_r.table__f_v_a_r: ...
745 @overload
746 def __getitem__(self, tag: Literal["gasp"]) -> _g_a_s_p.table__g_a_s_p: ...
747 @overload
748 def __getitem__(self, tag: Literal["gcid"]) -> _g_c_i_d.table__g_c_i_d: ...
749 @overload
750 def __getitem__(self, tag: Literal["glyf"]) -> _g_l_y_f.table__g_l_y_f: ...
751 @overload
752 def __getitem__(self, tag: Literal["gvar"]) -> _g_v_a_r.table__g_v_a_r: ...
753 @overload
754 def __getitem__(self, tag: Literal["hdmx"]) -> _h_d_m_x.table__h_d_m_x: ...
755 @overload
756 def __getitem__(self, tag: Literal["head"]) -> _h_e_a_d.table__h_e_a_d: ...
757 @overload
758 def __getitem__(self, tag: Literal["hhea"]) -> _h_h_e_a.table__h_h_e_a: ...
759 @overload
760 def __getitem__(self, tag: Literal["hmtx"]) -> _h_m_t_x.table__h_m_t_x: ...
761 @overload
762 def __getitem__(self, tag: Literal["kern"]) -> _k_e_r_n.table__k_e_r_n: ...
763 @overload
764 def __getitem__(self, tag: Literal["lcar"]) -> _l_c_a_r.table__l_c_a_r: ...
765 @overload
766 def __getitem__(self, tag: Literal["loca"]) -> _l_o_c_a.table__l_o_c_a: ...
767 @overload
768 def __getitem__(self, tag: Literal["ltag"]) -> _l_t_a_g.table__l_t_a_g: ...
769 @overload
770 def __getitem__(self, tag: Literal["maxp"]) -> _m_a_x_p.table__m_a_x_p: ...
771 @overload
772 def __getitem__(self, tag: Literal["meta"]) -> _m_e_t_a.table__m_e_t_a: ...
773 @overload
774 def __getitem__(self, tag: Literal["mort"]) -> _m_o_r_t.table__m_o_r_t: ...
775 @overload
776 def __getitem__(self, tag: Literal["morx"]) -> _m_o_r_x.table__m_o_r_x: ...
777 @overload
778 def __getitem__(self, tag: Literal["name"]) -> _n_a_m_e.table__n_a_m_e: ...
779 @overload
780 def __getitem__(self, tag: Literal["opbd"]) -> _o_p_b_d.table__o_p_b_d: ...
781 @overload
782 def __getitem__(self, tag: Literal["post"]) -> _p_o_s_t.table__p_o_s_t: ...
783 @overload
784 def __getitem__(self, tag: Literal["prep"]) -> _p_r_e_p.table__p_r_e_p: ...
785 @overload
786 def __getitem__(self, tag: Literal["prop"]) -> _p_r_o_p.table__p_r_o_p: ...
787 @overload
788 def __getitem__(self, tag: Literal["sbix"]) -> _s_b_i_x.table__s_b_i_x: ...
789 @overload
790 def __getitem__(self, tag: Literal["trak"]) -> _t_r_a_k.table__t_r_a_k: ...
791 @overload
792 def __getitem__(self, tag: Literal["vhea"]) -> _v_h_e_a.table__v_h_e_a: ...
793 @overload
794 def __getitem__(self, tag: Literal["vmtx"]) -> _v_m_t_x.table__v_m_t_x: ...
795 @overload
796 def __getitem__(self, tag: Literal["GlyphOrder"]) -> GlyphOrder: ...
797 @overload
798 def __getitem__(self, tag: str | bytes) -> DefaultTable | GlyphOrder: ...
799
800 def __getitem__(self, tag: str | bytes) -> DefaultTable | GlyphOrder:
801 tag = Tag(tag)
802 table = self.tables.get(tag)
803 if table is None:
804 if tag == "GlyphOrder":
805 table = GlyphOrder(tag)
806 self.tables[tag] = table
807 elif self.reader is not None:
808 table = self._readTable(tag)
809 else:
810 raise KeyError("'%s' table not found" % tag)
811 return table
812
813 def _readTable(self, tag: Tag) -> DefaultTable:
814 log.debug("Reading '%s' table from disk", tag)
815 assert self.reader is not None
816 data = self.reader[tag]
817 if self._tableCache is not None:
818 table = self._tableCache.get((tag, data))
819 if table is not None:
820 return table
821 tableClass = getTableClass(tag)
822 table = tableClass(tag)
823 self.tables[tag] = table
824 log.debug("Decompiling '%s' table", tag)
825 try:
826 table.decompile(data, self)
827 except Exception:
828 if not self.ignoreDecompileErrors:
829 raise
830 # fall back to DefaultTable, retaining the binary table data
831 log.exception(
832 "An exception occurred during the decompilation of the '%s' table", tag
833 )
834 from .tables.DefaultTable import DefaultTable
835
836 file = StringIO()
837 traceback.print_exc(file=file)
838 table = DefaultTable(tag)
839 table.ERROR = file.getvalue()
840 self.tables[tag] = table
841 table.decompile(data, self)
842 if self._tableCache is not None:
843 self._tableCache[(tag, data)] = table
844 return table
845
846 def __setitem__(self, tag: str | bytes, table: DefaultTable) -> None:
847 self.tables[Tag(tag)] = table
848
849 def __delitem__(self, tag: str | bytes) -> None:
850 if tag not in self:
851 raise KeyError("'%s' table not found" % tag)
852 if tag in self.tables:
853 del self.tables[tag]
854 if self.reader and tag in self.reader:
855 del self.reader[tag]
856
857 @overload
858 def get(self, tag: Literal["BASE"]) -> B_A_S_E_.table_B_A_S_E_ | None: ...
859 @overload
860 def get(self, tag: Literal["CBDT"]) -> C_B_D_T_.table_C_B_D_T_ | None: ...
861 @overload
862 def get(self, tag: Literal["CBLC"]) -> C_B_L_C_.table_C_B_L_C_ | None: ...
863 @overload
864 def get(self, tag: Literal["CFF "]) -> C_F_F_.table_C_F_F_ | None: ...
865 @overload
866 def get(self, tag: Literal["CFF2"]) -> C_F_F__2.table_C_F_F__2 | None: ...
867 @overload
868 def get(self, tag: Literal["COLR"]) -> C_O_L_R_.table_C_O_L_R_ | None: ...
869 @overload
870 def get(self, tag: Literal["CPAL"]) -> C_P_A_L_.table_C_P_A_L_ | None: ...
871 @overload
872 def get(self, tag: Literal["DSIG"]) -> D_S_I_G_.table_D_S_I_G_ | None: ...
873 @overload
874 def get(self, tag: Literal["EBDT"]) -> E_B_D_T_.table_E_B_D_T_ | None: ...
875 @overload
876 def get(self, tag: Literal["EBLC"]) -> E_B_L_C_.table_E_B_L_C_ | None: ...
877 @overload
878 def get(self, tag: Literal["FFTM"]) -> F_F_T_M_.table_F_F_T_M_ | None: ...
879 @overload
880 def get(self, tag: Literal["GDEF"]) -> G_D_E_F_.table_G_D_E_F_ | None: ...
881 @overload
882 def get(self, tag: Literal["GMAP"]) -> G_M_A_P_.table_G_M_A_P_ | None: ...
883 @overload
884 def get(self, tag: Literal["GPKG"]) -> G_P_K_G_.table_G_P_K_G_ | None: ...
885 @overload
886 def get(self, tag: Literal["GPOS"]) -> G_P_O_S_.table_G_P_O_S_ | None: ...
887 @overload
888 def get(self, tag: Literal["GSUB"]) -> G_S_U_B_.table_G_S_U_B_ | None: ...
889 @overload
890 def get(self, tag: Literal["GVAR"]) -> G_V_A_R_.table_G_V_A_R_ | None: ...
891 @overload
892 def get(self, tag: Literal["HVAR"]) -> H_V_A_R_.table_H_V_A_R_ | None: ...
893 @overload
894 def get(self, tag: Literal["JSTF"]) -> J_S_T_F_.table_J_S_T_F_ | None: ...
895 @overload
896 def get(self, tag: Literal["LTSH"]) -> L_T_S_H_.table_L_T_S_H_ | None: ...
897 @overload
898 def get(self, tag: Literal["MATH"]) -> M_A_T_H_.table_M_A_T_H_ | None: ...
899 @overload
900 def get(self, tag: Literal["META"]) -> M_E_T_A_.table_M_E_T_A_ | None: ...
901 @overload
902 def get(self, tag: Literal["MVAR"]) -> M_V_A_R_.table_M_V_A_R_ | None: ...
903 @overload
904 def get(self, tag: Literal["SING"]) -> S_I_N_G_.table_S_I_N_G_ | None: ...
905 @overload
906 def get(self, tag: Literal["STAT"]) -> S_T_A_T_.table_S_T_A_T_ | None: ...
907 @overload
908 def get(self, tag: Literal["SVG "]) -> S_V_G_.table_S_V_G_ | None: ...
909 @overload
910 def get(self, tag: Literal["TSI0"]) -> T_S_I__0.table_T_S_I__0 | None: ...
911 @overload
912 def get(self, tag: Literal["TSI1"]) -> T_S_I__1.table_T_S_I__1 | None: ...
913 @overload
914 def get(self, tag: Literal["TSI2"]) -> T_S_I__2.table_T_S_I__2 | None: ...
915 @overload
916 def get(self, tag: Literal["TSI3"]) -> T_S_I__3.table_T_S_I__3 | None: ...
917 @overload
918 def get(self, tag: Literal["TSI5"]) -> T_S_I__5.table_T_S_I__5 | None: ...
919 @overload
920 def get(self, tag: Literal["TSIB"]) -> T_S_I_B_.table_T_S_I_B_ | None: ...
921 @overload
922 def get(self, tag: Literal["TSIC"]) -> T_S_I_C_.table_T_S_I_C_ | None: ...
923 @overload
924 def get(self, tag: Literal["TSID"]) -> T_S_I_D_.table_T_S_I_D_ | None: ...
925 @overload
926 def get(self, tag: Literal["TSIJ"]) -> T_S_I_J_.table_T_S_I_J_ | None: ...
927 @overload
928 def get(self, tag: Literal["TSIP"]) -> T_S_I_P_.table_T_S_I_P_ | None: ...
929 @overload
930 def get(self, tag: Literal["TSIS"]) -> T_S_I_S_.table_T_S_I_S_ | None: ...
931 @overload
932 def get(self, tag: Literal["TSIV"]) -> T_S_I_V_.table_T_S_I_V_ | None: ...
933 @overload
934 def get(self, tag: Literal["TTFA"]) -> T_T_F_A_.table_T_T_F_A_ | None: ...
935 @overload
936 def get(self, tag: Literal["VARC"]) -> V_A_R_C_.table_V_A_R_C_ | None: ...
937 @overload
938 def get(self, tag: Literal["VDMX"]) -> V_D_M_X_.table_V_D_M_X_ | None: ...
939 @overload
940 def get(self, tag: Literal["VORG"]) -> V_O_R_G_.table_V_O_R_G_ | None: ...
941 @overload
942 def get(self, tag: Literal["VVAR"]) -> V_V_A_R_.table_V_V_A_R_ | None: ...
943 @overload
944 def get(self, tag: Literal["Debg"]) -> D__e_b_g.table_D__e_b_g | None: ...
945 @overload
946 def get(self, tag: Literal["Feat"]) -> F__e_a_t.table_F__e_a_t | None: ...
947 @overload
948 def get(self, tag: Literal["Glat"]) -> G__l_a_t.table_G__l_a_t | None: ...
949 @overload
950 def get(self, tag: Literal["Gloc"]) -> G__l_o_c.table_G__l_o_c | None: ...
951 @overload
952 def get(self, tag: Literal["OS/2"]) -> O_S_2f_2.table_O_S_2f_2 | None: ...
953 @overload
954 def get(self, tag: Literal["Silf"]) -> S__i_l_f.table_S__i_l_f | None: ...
955 @overload
956 def get(self, tag: Literal["Sill"]) -> S__i_l_l.table_S__i_l_l | None: ...
957 @overload
958 def get(self, tag: Literal["ankr"]) -> _a_n_k_r.table__a_n_k_r | None: ...
959 @overload
960 def get(self, tag: Literal["avar"]) -> _a_v_a_r.table__a_v_a_r | None: ...
961 @overload
962 def get(self, tag: Literal["bsln"]) -> _b_s_l_n.table__b_s_l_n | None: ...
963 @overload
964 def get(self, tag: Literal["cidg"]) -> _c_i_d_g.table__c_i_d_g | None: ...
965 @overload
966 def get(self, tag: Literal["cmap"]) -> _c_m_a_p.table__c_m_a_p | None: ...
967 @overload
968 def get(self, tag: Literal["cvar"]) -> _c_v_a_r.table__c_v_a_r | None: ...
969 @overload
970 def get(self, tag: Literal["cvt "]) -> _c_v_t.table__c_v_t | None: ...
971 @overload
972 def get(self, tag: Literal["feat"]) -> _f_e_a_t.table__f_e_a_t | None: ...
973 @overload
974 def get(self, tag: Literal["fpgm"]) -> _f_p_g_m.table__f_p_g_m | None: ...
975 @overload
976 def get(self, tag: Literal["fvar"]) -> _f_v_a_r.table__f_v_a_r | None: ...
977 @overload
978 def get(self, tag: Literal["gasp"]) -> _g_a_s_p.table__g_a_s_p | None: ...
979 @overload
980 def get(self, tag: Literal["gcid"]) -> _g_c_i_d.table__g_c_i_d | None: ...
981 @overload
982 def get(self, tag: Literal["glyf"]) -> _g_l_y_f.table__g_l_y_f | None: ...
983 @overload
984 def get(self, tag: Literal["gvar"]) -> _g_v_a_r.table__g_v_a_r | None: ...
985 @overload
986 def get(self, tag: Literal["hdmx"]) -> _h_d_m_x.table__h_d_m_x | None: ...
987 @overload
988 def get(self, tag: Literal["head"]) -> _h_e_a_d.table__h_e_a_d | None: ...
989 @overload
990 def get(self, tag: Literal["hhea"]) -> _h_h_e_a.table__h_h_e_a | None: ...
991 @overload
992 def get(self, tag: Literal["hmtx"]) -> _h_m_t_x.table__h_m_t_x | None: ...
993 @overload
994 def get(self, tag: Literal["kern"]) -> _k_e_r_n.table__k_e_r_n | None: ...
995 @overload
996 def get(self, tag: Literal["lcar"]) -> _l_c_a_r.table__l_c_a_r | None: ...
997 @overload
998 def get(self, tag: Literal["loca"]) -> _l_o_c_a.table__l_o_c_a | None: ...
999 @overload
1000 def get(self, tag: Literal["ltag"]) -> _l_t_a_g.table__l_t_a_g | None: ...
1001 @overload
1002 def get(self, tag: Literal["maxp"]) -> _m_a_x_p.table__m_a_x_p | None: ...
1003 @overload
1004 def get(self, tag: Literal["meta"]) -> _m_e_t_a.table__m_e_t_a | None: ...
1005 @overload
1006 def get(self, tag: Literal["mort"]) -> _m_o_r_t.table__m_o_r_t | None: ...
1007 @overload
1008 def get(self, tag: Literal["morx"]) -> _m_o_r_x.table__m_o_r_x | None: ...
1009 @overload
1010 def get(self, tag: Literal["name"]) -> _n_a_m_e.table__n_a_m_e | None: ...
1011 @overload
1012 def get(self, tag: Literal["opbd"]) -> _o_p_b_d.table__o_p_b_d | None: ...
1013 @overload
1014 def get(self, tag: Literal["post"]) -> _p_o_s_t.table__p_o_s_t | None: ...
1015 @overload
1016 def get(self, tag: Literal["prep"]) -> _p_r_e_p.table__p_r_e_p | None: ...
1017 @overload
1018 def get(self, tag: Literal["prop"]) -> _p_r_o_p.table__p_r_o_p | None: ...
1019 @overload
1020 def get(self, tag: Literal["sbix"]) -> _s_b_i_x.table__s_b_i_x | None: ...
1021 @overload
1022 def get(self, tag: Literal["trak"]) -> _t_r_a_k.table__t_r_a_k | None: ...
1023 @overload
1024 def get(self, tag: Literal["vhea"]) -> _v_h_e_a.table__v_h_e_a | None: ...
1025 @overload
1026 def get(self, tag: Literal["vmtx"]) -> _v_m_t_x.table__v_m_t_x | None: ...
1027 @overload
1028 def get(self, tag: Literal["GlyphOrder"]) -> GlyphOrder: ...
1029 @overload
1030 def get(self, tag: str | bytes) -> DefaultTable | GlyphOrder | Any | None: ...
1031 @overload
1032 def get(
1033 self, tag: str | bytes, default: _VT_co
1034 ) -> DefaultTable | GlyphOrder | Any | _VT_co: ...
1035
1036 def get(
1037 self, tag: str | bytes, default: Any | None = None
1038 ) -> DefaultTable | GlyphOrder | Any | None:
1039 """Returns the table if it exists or (optionally) a default if it doesn't."""
1040 try:
1041 return self[tag]
1042 except KeyError:
1043 return default
1044
1045 def setGlyphOrder(self, glyphOrder: list[str]) -> None:
1046 """Set the glyph order
1047
1048 Args:
1049 glyphOrder ([str]): List of glyph names in order.
1050 """
1051 self.glyphOrder = glyphOrder
1052 if hasattr(self, "_reverseGlyphOrderDict"):
1053 del self._reverseGlyphOrderDict
1054 if self.isLoaded("glyf"):
1055 self["glyf"].setGlyphOrder(glyphOrder)
1056
1057 def getGlyphOrder(self) -> list[str]:
1058 """Returns a list of glyph names ordered by their position in the font."""
1059 try:
1060 return self.glyphOrder
1061 except AttributeError:
1062 pass
1063 if "CFF " in self:
1064 cff = self["CFF "]
1065 self.glyphOrder = cff.getGlyphOrder()
1066 elif "post" in self:
1067 # TrueType font
1068 glyphOrder = self["post"].getGlyphOrder()
1069 if glyphOrder is None:
1070 #
1071 # No names found in the 'post' table.
1072 # Try to create glyph names from the unicode cmap (if available)
1073 # in combination with the Adobe Glyph List (AGL).
1074 #
1075 self._getGlyphNamesFromCmap()
1076 elif len(glyphOrder) < self["maxp"].numGlyphs:
1077 #
1078 # Not enough names found in the 'post' table.
1079 # Can happen when 'post' format 1 is improperly used on a font that
1080 # has more than 258 glyphs (the length of 'standardGlyphOrder').
1081 #
1082 log.warning(
1083 "Not enough names found in the 'post' table, generating them from cmap instead"
1084 )
1085 self._getGlyphNamesFromCmap()
1086 else:
1087 self.glyphOrder = glyphOrder
1088 else:
1089 self._getGlyphNamesFromCmap()
1090 return self.glyphOrder
1091
1092 def _getGlyphNamesFromCmap(self) -> None:
1093 #
1094 # This is rather convoluted, but then again, it's an interesting problem:
1095 # - we need to use the unicode values found in the cmap table to
1096 # build glyph names (eg. because there is only a minimal post table,
1097 # or none at all).
1098 # - but the cmap parser also needs glyph names to work with...
1099 # So here's what we do:
1100 # - make up glyph names based on glyphID
1101 # - load a temporary cmap table based on those names
1102 # - extract the unicode values, build the "real" glyph names
1103 # - unload the temporary cmap table
1104 #
1105 if self.isLoaded("cmap"):
1106 # Bootstrapping: we're getting called by the cmap parser
1107 # itself. This means self.tables['cmap'] contains a partially
1108 # loaded cmap, making it impossible to get at a unicode
1109 # subtable here. We remove the partially loaded cmap and
1110 # restore it later.
1111 # This only happens if the cmap table is loaded before any
1112 # other table that does f.getGlyphOrder() or f.getGlyphName().
1113 cmapLoading = self.tables["cmap"]
1114 del self.tables["cmap"]
1115 else:
1116 cmapLoading = None
1117 # Make up glyph names based on glyphID, which will be used by the
1118 # temporary cmap and by the real cmap in case we don't find a unicode
1119 # cmap.
1120 numGlyphs = int(self["maxp"].numGlyphs)
1121 glyphOrder = ["glyph%.5d" % i for i in range(numGlyphs)]
1122 glyphOrder[0] = ".notdef"
1123 # Set the glyph order, so the cmap parser has something
1124 # to work with (so we don't get called recursively).
1125 self.glyphOrder = glyphOrder
1126
1127 # Make up glyph names based on the reversed cmap table. Because some
1128 # glyphs (eg. ligatures or alternates) may not be reachable via cmap,
1129 # this naming table will usually not cover all glyphs in the font.
1130 # If the font has no Unicode cmap table, reversecmap will be empty.
1131 if "cmap" in self:
1132 reversecmap = self["cmap"].buildReversedMin()
1133 else:
1134 reversecmap = {}
1135 useCount = {}
1136 for i, tempName in enumerate(glyphOrder):
1137 if tempName in reversecmap:
1138 # If a font maps both U+0041 LATIN CAPITAL LETTER A and
1139 # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph,
1140 # we prefer naming the glyph as "A".
1141 glyphName = self._makeGlyphName(reversecmap[tempName])
1142 numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1
1143 if numUses > 1:
1144 glyphName = "%s.alt%d" % (glyphName, numUses - 1)
1145 glyphOrder[i] = glyphName
1146
1147 if "cmap" in self:
1148 # Delete the temporary cmap table from the cache, so it can
1149 # be parsed again with the right names.
1150 del self.tables["cmap"]
1151 self.glyphOrder = glyphOrder
1152 if cmapLoading:
1153 # restore partially loaded cmap, so it can continue loading
1154 # using the proper names.
1155 self.tables["cmap"] = cmapLoading
1156
1157 @staticmethod
1158 def _makeGlyphName(codepoint: int) -> str:
1159 from fontTools import agl # Adobe Glyph List
1160
1161 if codepoint in agl.UV2AGL:
1162 return agl.UV2AGL[codepoint]
1163 elif codepoint <= 0xFFFF:
1164 return "uni%04X" % codepoint
1165 else:
1166 return "u%X" % codepoint
1167
1168 def getGlyphNames(self) -> list[str]:
1169 """Get a list of glyph names, sorted alphabetically."""
1170 glyphNames = sorted(self.getGlyphOrder())
1171 return glyphNames
1172
1173 def getGlyphNames2(self) -> list[str]:
1174 """Get a list of glyph names, sorted alphabetically,
1175 but not case sensitive.
1176 """
1177 from fontTools.misc import textTools
1178
1179 return textTools.caselessSort(self.getGlyphOrder())
1180
1181 def getGlyphName(self, glyphID: int) -> str:
1182 """Returns the name for the glyph with the given ID.
1183
1184 If no name is available, synthesises one with the form ``glyphXXXXX``` where
1185 ```XXXXX`` is the zero-padded glyph ID.
1186 """
1187 try:
1188 return self.getGlyphOrder()[glyphID]
1189 except IndexError:
1190 return "glyph%.5d" % glyphID
1191
1192 def getGlyphNameMany(self, lst: Sequence[int]) -> list[str]:
1193 """Converts a list of glyph IDs into a list of glyph names."""
1194 glyphOrder = self.getGlyphOrder()
1195 cnt = len(glyphOrder)
1196 return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid for gid in lst]
1197
1198 def getGlyphID(self, glyphName: str) -> int:
1199 """Returns the ID of the glyph with the given name."""
1200 try:
1201 return self.getReverseGlyphMap()[glyphName]
1202 except KeyError:
1203 if glyphName[:5] == "glyph":
1204 try:
1205 return int(glyphName[5:])
1206 except (NameError, ValueError):
1207 raise KeyError(glyphName)
1208 raise
1209
1210 def getGlyphIDMany(self, lst: Sequence[str]) -> list[int]:
1211 """Converts a list of glyph names into a list of glyph IDs."""
1212 d = self.getReverseGlyphMap()
1213 try:
1214 return [d[glyphName] for glyphName in lst]
1215 except KeyError:
1216 getGlyphID = self.getGlyphID
1217 return [getGlyphID(glyphName) for glyphName in lst]
1218
1219 def getReverseGlyphMap(self, rebuild: bool = False) -> dict[str, int]:
1220 """Returns a mapping of glyph names to glyph IDs."""
1221 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
1222 self._buildReverseGlyphOrderDict()
1223 return self._reverseGlyphOrderDict
1224
1225 def _buildReverseGlyphOrderDict(self) -> dict[str, int]:
1226 self._reverseGlyphOrderDict = d = {}
1227 for glyphID, glyphName in enumerate(self.getGlyphOrder()):
1228 d[glyphName] = glyphID
1229 return d
1230
1231 def _writeTable(
1232 self,
1233 tag: str | bytes,
1234 writer: SFNTWriter,
1235 done: list[str | bytes], # Use list as original
1236 tableCache: MutableMapping[tuple[Tag, bytes], DefaultTable] | None = None,
1237 ) -> None:
1238 """Internal helper function for self.save(). Keeps track of
1239 inter-table dependencies.
1240 """
1241 if tag in done:
1242 return
1243 tableClass = getTableClass(tag)
1244 for masterTable in tableClass.dependencies:
1245 if masterTable not in done:
1246 if masterTable in self:
1247 self._writeTable(masterTable, writer, done, tableCache)
1248 else:
1249 done.append(masterTable)
1250 done.append(tag)
1251 tabledata = self.getTableData(tag)
1252 if tableCache is not None:
1253 entry = tableCache.get((Tag(tag), tabledata))
1254 if entry is not None:
1255 log.debug("reusing '%s' table", tag)
1256 writer.setEntry(tag, entry)
1257 return
1258 log.debug("Writing '%s' table to disk", tag)
1259 writer[tag] = tabledata
1260 if tableCache is not None:
1261 tableCache[(Tag(tag), tabledata)] = writer[tag]
1262
1263 def getTableData(self, tag: str | bytes) -> bytes:
1264 """Returns the binary representation of a table.
1265
1266 If the table is currently loaded and in memory, the data is compiled to
1267 binary and returned; if it is not currently loaded, the binary data is
1268 read from the font file and returned.
1269 """
1270 tag = Tag(tag)
1271 if self.isLoaded(tag):
1272 log.debug("Compiling '%s' table", tag)
1273 return self.tables[tag].compile(self)
1274 elif self.reader and tag in self.reader:
1275 log.debug("Reading '%s' table from disk", tag)
1276 return self.reader[tag]
1277 else:
1278 raise KeyError(tag)
1279
1280 def getGlyphSet(
1281 self,
1282 preferCFF: bool = True,
1283 location: Mapping[str, _NumberT] | None = None,
1284 normalized: bool = False,
1285 recalcBounds: bool = True,
1286 ) -> _TTGlyphSet:
1287 """Return a generic GlyphSet, which is a dict-like object
1288 mapping glyph names to glyph objects. The returned glyph objects
1289 have a ``.draw()`` method that supports the Pen protocol, and will
1290 have an attribute named 'width'.
1291
1292 If the font is CFF-based, the outlines will be taken from the ``CFF ``
1293 or ``CFF2`` tables. Otherwise the outlines will be taken from the
1294 ``glyf`` table.
1295
1296 If the font contains both a ``CFF ``/``CFF2`` and a ``glyf`` table, you
1297 can use the ``preferCFF`` argument to specify which one should be taken.
1298 If the font contains both a ``CFF `` and a ``CFF2`` table, the latter is
1299 taken.
1300
1301 If the ``location`` parameter is set, it should be a dictionary mapping
1302 four-letter variation tags to their float values, and the returned
1303 glyph-set will represent an instance of a variable font at that
1304 location.
1305
1306 If the ``normalized`` variable is set to True, that location is
1307 interpreted as in the normalized (-1..+1) space, otherwise it is in the
1308 font's defined axes space.
1309 """
1310 if location and "fvar" not in self:
1311 location = None
1312 if location and not normalized:
1313 location = self.normalizeLocation(location)
1314 glyphSet = None
1315 if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self):
1316 glyphSet = _TTGlyphSetCFF(self, location)
1317 elif "glyf" in self:
1318 glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds)
1319 else:
1320 raise TTLibError("Font contains no outlines")
1321 if "VARC" in self:
1322 glyphSet = _TTGlyphSetVARC(self, location, glyphSet)
1323 return glyphSet
1324
1325 def normalizeLocation(self, location: Mapping[str, float]) -> dict[str, float]:
1326 """Normalize a ``location`` from the font's defined axes space (also
1327 known as user space) into the normalized (-1..+1) space. It applies
1328 ``avar`` mapping if the font contains an ``avar`` table.
1329
1330 The ``location`` parameter should be a dictionary mapping four-letter
1331 variation tags to their float values.
1332
1333 Raises ``TTLibError`` if the font is not a variable font.
1334 """
1335 from fontTools.varLib.models import normalizeLocation
1336
1337 if "fvar" not in self:
1338 raise TTLibError("Not a variable font")
1339
1340 axes = self["fvar"].getAxes()
1341 location = normalizeLocation(location, axes)
1342 if "avar" in self:
1343 location = self["avar"].renormalizeLocation(location, self)
1344 return location
1345
1346 def getBestCmap(
1347 self,
1348 cmapPreferences: Sequence[tuple[int, int]] = (
1349 (3, 10),
1350 (0, 6),
1351 (0, 4),
1352 (3, 1),
1353 (0, 3),
1354 (0, 2),
1355 (0, 1),
1356 (0, 0),
1357 ),
1358 ) -> dict[int, str] | None:
1359 """Returns the 'best' Unicode cmap dictionary available in the font
1360 or ``None``, if no Unicode cmap subtable is available.
1361
1362 By default it will search for the following (platformID, platEncID)
1363 pairs in order::
1364
1365 (3, 10), # Windows Unicode full repertoire
1366 (0, 6), # Unicode full repertoire (format 13 subtable)
1367 (0, 4), # Unicode 2.0 full repertoire
1368 (3, 1), # Windows Unicode BMP
1369 (0, 3), # Unicode 2.0 BMP
1370 (0, 2), # Unicode ISO/IEC 10646
1371 (0, 1), # Unicode 1.1
1372 (0, 0) # Unicode 1.0
1373
1374 This particular order matches what HarfBuzz uses to choose what
1375 subtable to use by default. This order prefers the largest-repertoire
1376 subtable, and among those, prefers the Windows-platform over the
1377 Unicode-platform as the former has wider support.
1378
1379 This order can be customized via the ``cmapPreferences`` argument.
1380 """
1381 return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
1382
1383 def reorderGlyphs(self, new_glyph_order: list[str]) -> None:
1384 from .reorderGlyphs import reorderGlyphs
1385
1386 reorderGlyphs(self, new_glyph_order)
1387
1388
1389class GlyphOrder(object):
1390 """A pseudo table. The glyph order isn't in the font as a separate
1391 table, but it's nice to present it as such in the TTX format.
1392 """
1393
1394 def __init__(self, tag: str | None = None) -> None:
1395 pass
1396
1397 def toXML(self, writer: xmlWriter.XMLWriter, ttFont: TTFont) -> None:
1398 glyphOrder = ttFont.getGlyphOrder()
1399 writer.comment(
1400 "The 'id' attribute is only for humans; it is ignored when parsed."
1401 )
1402 writer.newline()
1403 for i, glyphName in enumerate(glyphOrder):
1404 writer.simpletag("GlyphID", id=i, name=glyphName)
1405 writer.newline()
1406
1407 def fromXML(
1408 self, name: str, attrs: dict[str, str], content: list[Any], ttFont: TTFont
1409 ) -> None:
1410 if not hasattr(self, "glyphOrder"):
1411 self.glyphOrder = []
1412 if name == "GlyphID":
1413 self.glyphOrder.append(attrs["name"])
1414 ttFont.setGlyphOrder(self.glyphOrder)
1415
1416
1417def getTableModule(tag: str | bytes) -> ModuleType | None:
1418 """Fetch the packer/unpacker module for a table.
1419 Return None when no module is found.
1420 """
1421 from . import tables
1422
1423 pyTag = tagToIdentifier(tag)
1424 try:
1425 __import__("fontTools.ttLib.tables." + pyTag)
1426 except ImportError as err:
1427 # If pyTag is found in the ImportError message,
1428 # means table is not implemented. If it's not
1429 # there, then some other module is missing, don't
1430 # suppress the error.
1431 if str(err).find(pyTag) >= 0:
1432 return None
1433 else:
1434 raise err
1435 else:
1436 return getattr(tables, pyTag)
1437
1438
1439# Registry for custom table packer/unpacker classes. Keys are table
1440# tags, values are (moduleName, className) tuples.
1441# See registerCustomTableClass() and getCustomTableClass()
1442_customTableRegistry: dict[str | bytes, tuple[str, str]] = {}
1443
1444
1445def registerCustomTableClass(
1446 tag: str | bytes, moduleName: str, className: str | None = None
1447) -> None:
1448 """Register a custom packer/unpacker class for a table.
1449
1450 The 'moduleName' must be an importable module. If no 'className'
1451 is given, it is derived from the tag, for example it will be
1452 ``table_C_U_S_T_`` for a 'CUST' tag.
1453
1454 The registered table class should be a subclass of
1455 :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable`
1456 """
1457 if className is None:
1458 className = "table_" + tagToIdentifier(tag)
1459 _customTableRegistry[tag] = (moduleName, className)
1460
1461
1462def unregisterCustomTableClass(tag: str | bytes) -> None:
1463 """Unregister the custom packer/unpacker class for a table."""
1464 del _customTableRegistry[tag]
1465
1466
1467def getCustomTableClass(tag: str | bytes) -> type[DefaultTable] | None:
1468 """Return the custom table class for tag, if one has been registered
1469 with 'registerCustomTableClass()'. Else return None.
1470 """
1471 if tag not in _customTableRegistry:
1472 return None
1473 import importlib
1474
1475 moduleName, className = _customTableRegistry[tag]
1476 module = importlib.import_module(moduleName)
1477 return getattr(module, className)
1478
1479
1480def getTableClass(tag: str | bytes) -> type[DefaultTable]:
1481 """Fetch the packer/unpacker class for a table."""
1482 tableClass = getCustomTableClass(tag)
1483 if tableClass is not None:
1484 return tableClass
1485 module = getTableModule(tag)
1486 if module is None:
1487 from .tables.DefaultTable import DefaultTable
1488
1489 return DefaultTable
1490 pyTag = tagToIdentifier(tag)
1491 tableClass = getattr(module, "table_" + pyTag)
1492 return tableClass
1493
1494
1495def getClassTag(klass: type[DefaultTable]) -> str | bytes:
1496 """Fetch the table tag for a class object."""
1497 name = klass.__name__
1498 assert name[:6] == "table_"
1499 name = name[6:] # Chop 'table_'
1500 return identifierToTag(name)
1501
1502
1503def newTable(tag: str | bytes) -> DefaultTable:
1504 """Return a new instance of a table."""
1505 tableClass = getTableClass(tag)
1506 return tableClass(tag)
1507
1508
1509def _escapechar(c: str) -> str:
1510 """Helper function for tagToIdentifier()"""
1511 import re
1512
1513 if re.match("[a-z0-9]", c):
1514 return "_" + c
1515 elif re.match("[A-Z]", c):
1516 return c + "_"
1517 else:
1518 return hex(byteord(c))[2:]
1519
1520
1521def tagToIdentifier(tag: str | bytes) -> str:
1522 """Convert a table tag to a valid (but UGLY) python identifier,
1523 as well as a filename that's guaranteed to be unique even on a
1524 caseless file system. Each character is mapped to two characters.
1525 Lowercase letters get an underscore before the letter, uppercase
1526 letters get an underscore after the letter. Trailing spaces are
1527 trimmed. Illegal characters are escaped as two hex bytes. If the
1528 result starts with a number (as the result of a hex escape), an
1529 extra underscore is prepended. Examples:
1530 .. code-block:: pycon
1531
1532 >>>
1533 >> tagToIdentifier('glyf')
1534 '_g_l_y_f'
1535 >> tagToIdentifier('cvt ')
1536 '_c_v_t'
1537 >> tagToIdentifier('OS/2')
1538 'O_S_2f_2'
1539 """
1540 import re
1541
1542 tag = Tag(tag)
1543 if tag == "GlyphOrder":
1544 return tag
1545 assert len(tag) == 4, "tag should be 4 characters long"
1546 while len(tag) > 1 and tag[-1] == " ":
1547 tag = tag[:-1]
1548 ident = ""
1549 for c in tag:
1550 ident = ident + _escapechar(c)
1551 if re.match("[0-9]", ident):
1552 ident = "_" + ident
1553 return ident
1554
1555
1556def identifierToTag(ident: str) -> str:
1557 """the opposite of tagToIdentifier()"""
1558 if ident == "GlyphOrder":
1559 return ident
1560 if len(ident) % 2 and ident[0] == "_":
1561 ident = ident[1:]
1562 assert not (len(ident) % 2)
1563 tag = ""
1564 for i in range(0, len(ident), 2):
1565 if ident[i] == "_":
1566 tag = tag + ident[i + 1]
1567 elif ident[i + 1] == "_":
1568 tag = tag + ident[i]
1569 else:
1570 # assume hex
1571 tag = tag + chr(int(ident[i : i + 2], 16))
1572 # append trailing spaces
1573 tag = tag + (4 - len(tag)) * " "
1574 return Tag(tag)
1575
1576
1577def tagToXML(tag: str | bytes) -> str:
1578 """Similarly to tagToIdentifier(), this converts a TT tag
1579 to a valid XML element name. Since XML element names are
1580 case sensitive, this is a fairly simple/readable translation.
1581 """
1582 import re
1583
1584 tag = Tag(tag)
1585 if tag == "OS/2":
1586 return "OS_2"
1587 elif tag == "GlyphOrder":
1588 return tag
1589 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
1590 return tag.strip()
1591 else:
1592 return tagToIdentifier(tag)
1593
1594
1595def xmlToTag(tag: str) -> str:
1596 """The opposite of tagToXML()"""
1597 if tag == "OS_2":
1598 return Tag("OS/2")
1599 if len(tag) == 8:
1600 return identifierToTag(tag)
1601 else:
1602 return Tag(tag + " " * (4 - len(tag)))
1603
1604
1605# Table order as recommended in the OpenType specification 1.4
1606TTFTableOrder = [
1607 "head",
1608 "hhea",
1609 "maxp",
1610 "OS/2",
1611 "hmtx",
1612 "LTSH",
1613 "VDMX",
1614 "hdmx",
1615 "cmap",
1616 "fpgm",
1617 "prep",
1618 "cvt ",
1619 "loca",
1620 "glyf",
1621 "kern",
1622 "name",
1623 "post",
1624 "gasp",
1625 "PCLT",
1626]
1627
1628OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "]
1629
1630
1631def sortedTagList(
1632 tagList: Sequence[str], tableOrder: Sequence[str] | None = None
1633) -> list[str]:
1634 """Return a sorted copy of tagList, sorted according to the OpenType
1635 specification, or according to a custom tableOrder. If given and not
1636 None, tableOrder needs to be a list of tag names.
1637 """
1638 tagList = sorted(tagList)
1639 if tableOrder is None:
1640 if "DSIG" in tagList:
1641 # DSIG should be last (XXX spec reference?)
1642 tagList.remove("DSIG")
1643 tagList.append("DSIG")
1644 if "CFF " in tagList:
1645 tableOrder = OTFTableOrder
1646 else:
1647 tableOrder = TTFTableOrder
1648 orderedTables = []
1649 for tag in tableOrder:
1650 if tag in tagList:
1651 orderedTables.append(tag)
1652 tagList.remove(tag)
1653 orderedTables.extend(tagList)
1654 return orderedTables
1655
1656
1657def reorderFontTables(
1658 inFile: BinaryIO, # Takes file-like object as per original
1659 outFile: BinaryIO, # Takes file-like object
1660 tableOrder: Sequence[str] | None = None,
1661 checkChecksums: bool = False, # Keep param even if reader handles it
1662) -> None:
1663 """Rewrite a font file, ordering the tables as recommended by the
1664 OpenType specification 1.4.
1665 """
1666 inFile.seek(0)
1667 outFile.seek(0)
1668 reader = SFNTReader(inFile, checkChecksums=checkChecksums)
1669 writer = SFNTWriter(
1670 outFile,
1671 len(reader.tables),
1672 reader.sfntVersion,
1673 reader.flavor,
1674 reader.flavorData,
1675 )
1676 tables = list(reader.keys())
1677 for tag in sortedTagList(tables, tableOrder):
1678 writer[tag] = reader[tag]
1679 writer.close()
1680
1681
1682def maxPowerOfTwo(x: int) -> int:
1683 """Return the highest exponent of two, so that
1684 (2 ** exponent) <= x. Return 0 if x is 0.
1685 """
1686 exponent = 0
1687 while x:
1688 x = x >> 1
1689 exponent = exponent + 1
1690 return max(exponent - 1, 0)
1691
1692
1693def getSearchRange(n: int, itemSize: int = 16) -> tuple[int, int, int]:
1694 """Calculate searchRange, entrySelector, rangeShift."""
1695 # itemSize defaults to 16, for backward compatibility
1696 # with upstream fonttools.
1697 exponent = maxPowerOfTwo(n)
1698 searchRange = (2**exponent) * itemSize
1699 entrySelector = exponent
1700 rangeShift = max(0, n * itemSize - searchRange)
1701 return searchRange, entrySelector, rangeShift