ttFont.py 61 KB

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