ttFont.py 62 KB

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