1from fontTools.config import Config
2from fontTools.misc import xmlWriter
3from fontTools.misc.configTools import AbstractConfig
4from fontTools.misc.textTools import Tag, byteord, tostr
5from fontTools.misc.loggingTools import deprecateArgument
6from fontTools.ttLib import TTLibError
7from fontTools.ttLib.ttGlyphSet import (
8 _TTGlyph,
9 _TTGlyphSetCFF,
10 _TTGlyphSetGlyf,
11 _TTGlyphSetVARC,
12)
13from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
14from io import BytesIO, StringIO, UnsupportedOperation
15import os
16import logging
17import traceback
18
19log = logging.getLogger(__name__)
20
21
22class TTFont(object):
23 """Represents a TrueType font.
24
25 The object manages file input and output, and offers a convenient way of
26 accessing tables. Tables will be only decompiled when necessary, ie. when
27 they're actually accessed. This means that simple operations can be extremely fast.
28
29 Example usage:
30
31 .. code-block:: pycon
32
33 >>>
34 >> from fontTools import ttLib
35 >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file
36 >> tt['maxp'].numGlyphs
37 242
38 >> tt['OS/2'].achVendID
39 'B&H\000'
40 >> tt['head'].unitsPerEm
41 2048
42
43 For details of the objects returned when accessing each table, see the
44 :doc:`tables </ttLib/tables>` documentation.
45 To add a table to the font, use the :py:func:`newTable` function:
46
47 .. code-block:: pycon
48
49 >>>
50 >> os2 = newTable("OS/2")
51 >> os2.version = 4
52 >> # set other attributes
53 >> font["OS/2"] = os2
54
55 TrueType fonts can also be serialized to and from XML format (see also the
56 :doc:`ttx </ttx>` binary):
57
58 .. code-block:: pycon
59
60 >>
61 >> tt.saveXML("afont.ttx")
62 Dumping 'LTSH' table...
63 Dumping 'OS/2' table...
64 [...]
65
66 >> tt2 = ttLib.TTFont() # Create a new font object
67 >> tt2.importXML("afont.ttx")
68 >> tt2['maxp'].numGlyphs
69 242
70
71 The TTFont object may be used as a context manager; this will cause the file
72 reader to be closed after the context ``with`` block is exited::
73
74 with TTFont(filename) as f:
75 # Do stuff
76
77 Args:
78 file: When reading a font from disk, either a pathname pointing to a file,
79 or a readable file object.
80 res_name_or_index: If running on a Macintosh, either a sfnt resource name or
81 an sfnt resource index number. If the index number is zero, TTLib will
82 autodetect whether the file is a flat file or a suitcase. (If it is a suitcase,
83 only the first 'sfnt' resource will be read.)
84 sfntVersion (str): When constructing a font object from scratch, sets the four-byte
85 sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create
86 an OpenType file, use ``OTTO``.
87 flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2
88 file.
89 checkChecksums (int): How checksum data should be treated. Default is 0
90 (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to
91 raise an exception if any wrong checksums are found.
92 recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``,
93 ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save.
94 Also compiles the glyphs on importing, which saves memory consumption and
95 time.
96 ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation
97 will be ignored, and the binary data will be returned for those tables instead.
98 recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in
99 the ``head`` table on save.
100 fontNumber (int): The index of the font in a TrueType Collection file.
101 lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon
102 access only. If it is set to False, many data structures are loaded immediately.
103 The default is ``lazy=None`` which is somewhere in between.
104 """
105
106 def __init__(
107 self,
108 file=None,
109 res_name_or_index=None,
110 sfntVersion="\000\001\000\000",
111 flavor=None,
112 checkChecksums=0,
113 verbose=None,
114 recalcBBoxes=True,
115 allowVID=NotImplemented,
116 ignoreDecompileErrors=False,
117 recalcTimestamp=True,
118 fontNumber=-1,
119 lazy=None,
120 quiet=None,
121 _tableCache=None,
122 cfg={},
123 ):
124 for name in ("verbose", "quiet"):
125 val = locals().get(name)
126 if val is not None:
127 deprecateArgument(name, "configure logging instead")
128 setattr(self, name, val)
129
130 self.lazy = lazy
131 self.recalcBBoxes = recalcBBoxes
132 self.recalcTimestamp = recalcTimestamp
133 self.tables = {}
134 self.reader = None
135 self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg)
136 self.ignoreDecompileErrors = ignoreDecompileErrors
137
138 if not file:
139 self.sfntVersion = sfntVersion
140 self.flavor = flavor
141 self.flavorData = None
142 return
143 seekable = True
144 if not hasattr(file, "read"):
145 closeStream = True
146 # assume file is a string
147 if res_name_or_index is not None:
148 # see if it contains 'sfnt' resources in the resource or data fork
149 from . import macUtils
150
151 if res_name_or_index == 0:
152 if macUtils.getSFNTResIndices(file):
153 # get the first available sfnt font.
154 file = macUtils.SFNTResourceReader(file, 1)
155 else:
156 file = open(file, "rb")
157 else:
158 file = macUtils.SFNTResourceReader(file, res_name_or_index)
159 else:
160 file = open(file, "rb")
161 else:
162 # assume "file" is a readable file object
163 closeStream = False
164 # SFNTReader wants the input file to be seekable.
165 # SpooledTemporaryFile has no seekable() on < 3.11, but still can seek:
166 # https://github.com/fonttools/fonttools/issues/3052
167 if hasattr(file, "seekable"):
168 seekable = file.seekable()
169 elif hasattr(file, "seek"):
170 try:
171 file.seek(0)
172 except UnsupportedOperation:
173 seekable = False
174
175 if not self.lazy:
176 # read input file in memory and wrap a stream around it to allow overwriting
177 if seekable:
178 file.seek(0)
179 tmp = BytesIO(file.read())
180 if hasattr(file, "name"):
181 # save reference to input file name
182 tmp.name = file.name
183 if closeStream:
184 file.close()
185 file = tmp
186 elif not seekable:
187 raise TTLibError("Input file must be seekable when lazy=True")
188 self._tableCache = _tableCache
189 self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber)
190 self.sfntVersion = self.reader.sfntVersion
191 self.flavor = self.reader.flavor
192 self.flavorData = self.reader.flavorData
193
194 def __enter__(self):
195 return self
196
197 def __exit__(self, type, value, traceback):
198 self.close()
199
200 def close(self):
201 """If we still have a reader object, close it."""
202 if self.reader is not None:
203 self.reader.close()
204
205 def save(self, file, reorderTables=True):
206 """Save the font to disk.
207
208 Args:
209 file: Similarly to the constructor, can be either a pathname or a writable
210 file object.
211 reorderTables (Option[bool]): If true (the default), reorder the tables,
212 sorting them by tag (recommended by the OpenType specification). If
213 false, retain the original font order. If None, reorder by table
214 dependency (fastest).
215 """
216 if not hasattr(file, "write"):
217 if self.lazy and self.reader.file.name == file:
218 raise TTLibError("Can't overwrite TTFont when 'lazy' attribute is True")
219 createStream = True
220 else:
221 # assume "file" is a writable file object
222 createStream = False
223
224 tmp = BytesIO()
225
226 writer_reordersTables = self._save(tmp)
227
228 if not (
229 reorderTables is None
230 or writer_reordersTables
231 or (reorderTables is False and self.reader is None)
232 ):
233 if reorderTables is False:
234 # sort tables using the original font's order
235 tableOrder = list(self.reader.keys())
236 else:
237 # use the recommended order from the OpenType specification
238 tableOrder = None
239 tmp.flush()
240 tmp2 = BytesIO()
241 reorderFontTables(tmp, tmp2, tableOrder)
242 tmp.close()
243 tmp = tmp2
244
245 if createStream:
246 # "file" is a path
247 with open(file, "wb") as file:
248 file.write(tmp.getvalue())
249 else:
250 file.write(tmp.getvalue())
251
252 tmp.close()
253
254 def _save(self, file, tableCache=None):
255 """Internal function, to be shared by save() and TTCollection.save()"""
256
257 if self.recalcTimestamp and "head" in self:
258 self[
259 "head"
260 ] # make sure 'head' is loaded so the recalculation is actually done
261
262 tags = list(self.keys())
263 if "GlyphOrder" in tags:
264 tags.remove("GlyphOrder")
265 numTables = len(tags)
266 # write to a temporary stream to allow saving to unseekable streams
267 writer = SFNTWriter(
268 file, numTables, self.sfntVersion, self.flavor, self.flavorData
269 )
270
271 done = []
272 for tag in tags:
273 self._writeTable(tag, writer, done, tableCache)
274
275 writer.close()
276
277 return writer.reordersTables()
278
279 def saveXML(self, fileOrPath, newlinestr="\n", **kwargs):
280 """Export the font as TTX (an XML-based text file), or as a series of text
281 files when splitTables is true. In the latter case, the 'fileOrPath'
282 argument should be a path to a directory.
283 The 'tables' argument must either be false (dump all tables) or a
284 list of tables to dump. The 'skipTables' argument may be a list of tables
285 to skip, but only when the 'tables' argument is false.
286 """
287
288 writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
289 self._saveXML(writer, **kwargs)
290 writer.close()
291
292 def _saveXML(
293 self,
294 writer,
295 writeVersion=True,
296 quiet=None,
297 tables=None,
298 skipTables=None,
299 splitTables=False,
300 splitGlyphs=False,
301 disassembleInstructions=True,
302 bitmapGlyphDataFormat="raw",
303 ):
304 if quiet is not None:
305 deprecateArgument("quiet", "configure logging instead")
306
307 self.disassembleInstructions = disassembleInstructions
308 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat
309 if not tables:
310 tables = list(self.keys())
311 if "GlyphOrder" not in tables:
312 tables = ["GlyphOrder"] + tables
313 if skipTables:
314 for tag in skipTables:
315 if tag in tables:
316 tables.remove(tag)
317 numTables = len(tables)
318
319 if writeVersion:
320 from fontTools import version
321
322 version = ".".join(version.split(".")[:2])
323 writer.begintag(
324 "ttFont",
325 sfntVersion=repr(tostr(self.sfntVersion))[1:-1],
326 ttLibVersion=version,
327 )
328 else:
329 writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1])
330 writer.newline()
331
332 # always splitTables if splitGlyphs is enabled
333 splitTables = splitTables or splitGlyphs
334
335 if not splitTables:
336 writer.newline()
337 else:
338 path, ext = os.path.splitext(writer.filename)
339
340 for i in range(numTables):
341 tag = tables[i]
342 if splitTables:
343 tablePath = path + "." + tagToIdentifier(tag) + ext
344 tableWriter = xmlWriter.XMLWriter(
345 tablePath, newlinestr=writer.newlinestr
346 )
347 tableWriter.begintag("ttFont", ttLibVersion=version)
348 tableWriter.newline()
349 tableWriter.newline()
350 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
351 writer.newline()
352 else:
353 tableWriter = writer
354 self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs)
355 if splitTables:
356 tableWriter.endtag("ttFont")
357 tableWriter.newline()
358 tableWriter.close()
359 writer.endtag("ttFont")
360 writer.newline()
361
362 def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False):
363 if quiet is not None:
364 deprecateArgument("quiet", "configure logging instead")
365 if tag in self:
366 table = self[tag]
367 report = "Dumping '%s' table..." % tag
368 else:
369 report = "No '%s' table found." % tag
370 log.info(report)
371 if tag not in self:
372 return
373 xmlTag = tagToXML(tag)
374 attrs = dict()
375 if hasattr(table, "ERROR"):
376 attrs["ERROR"] = "decompilation error"
377 from .tables.DefaultTable import DefaultTable
378
379 if table.__class__ == DefaultTable:
380 attrs["raw"] = True
381 writer.begintag(xmlTag, **attrs)
382 writer.newline()
383 if tag == "glyf":
384 table.toXML(writer, self, splitGlyphs=splitGlyphs)
385 else:
386 table.toXML(writer, self)
387 writer.endtag(xmlTag)
388 writer.newline()
389 writer.newline()
390
391 def importXML(self, fileOrPath, quiet=None):
392 """Import a TTX file (an XML-based text format), so as to recreate
393 a font object.
394 """
395 if quiet is not None:
396 deprecateArgument("quiet", "configure logging instead")
397
398 if "maxp" in self and "post" in self:
399 # Make sure the glyph order is loaded, as it otherwise gets
400 # lost if the XML doesn't contain the glyph order, yet does
401 # contain the table which was originally used to extract the
402 # glyph names from (ie. 'post', 'cmap' or 'CFF ').
403 self.getGlyphOrder()
404
405 from fontTools.misc import xmlReader
406
407 reader = xmlReader.XMLReader(fileOrPath, self)
408 reader.read()
409
410 def isLoaded(self, tag):
411 """Return true if the table identified by ``tag`` has been
412 decompiled and loaded into memory."""
413 return tag in self.tables
414
415 def has_key(self, tag):
416 """Test if the table identified by ``tag`` is present in the font.
417
418 As well as this method, ``tag in font`` can also be used to determine the
419 presence of the table."""
420 if self.isLoaded(tag):
421 return True
422 elif self.reader and tag in self.reader:
423 return True
424 elif tag == "GlyphOrder":
425 return True
426 else:
427 return False
428
429 __contains__ = has_key
430
431 def keys(self):
432 """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table."""
433 keys = list(self.tables.keys())
434 if self.reader:
435 for key in list(self.reader.keys()):
436 if key not in keys:
437 keys.append(key)
438
439 if "GlyphOrder" in keys:
440 keys.remove("GlyphOrder")
441 keys = sortedTagList(keys)
442 return ["GlyphOrder"] + keys
443
444 def ensureDecompiled(self, recurse=None):
445 """Decompile all the tables, even if a TTFont was opened in 'lazy' mode."""
446 for tag in self.keys():
447 table = self[tag]
448 if recurse is None:
449 recurse = self.lazy is not False
450 if recurse and hasattr(table, "ensureDecompiled"):
451 table.ensureDecompiled(recurse=recurse)
452 self.lazy = False
453
454 def __len__(self):
455 return len(list(self.keys()))
456
457 def __getitem__(self, tag):
458 tag = Tag(tag)
459 table = self.tables.get(tag)
460 if table is None:
461 if tag == "GlyphOrder":
462 table = GlyphOrder(tag)
463 self.tables[tag] = table
464 elif self.reader is not None:
465 table = self._readTable(tag)
466 else:
467 raise KeyError("'%s' table not found" % tag)
468 return table
469
470 def _readTable(self, tag):
471 log.debug("Reading '%s' table from disk", tag)
472 data = self.reader[tag]
473 if self._tableCache is not None:
474 table = self._tableCache.get((tag, data))
475 if table is not None:
476 return table
477 tableClass = getTableClass(tag)
478 table = tableClass(tag)
479 self.tables[tag] = table
480 log.debug("Decompiling '%s' table", tag)
481 try:
482 table.decompile(data, self)
483 except Exception:
484 if not self.ignoreDecompileErrors:
485 raise
486 # fall back to DefaultTable, retaining the binary table data
487 log.exception(
488 "An exception occurred during the decompilation of the '%s' table", tag
489 )
490 from .tables.DefaultTable import DefaultTable
491
492 file = StringIO()
493 traceback.print_exc(file=file)
494 table = DefaultTable(tag)
495 table.ERROR = file.getvalue()
496 self.tables[tag] = table
497 table.decompile(data, self)
498 if self._tableCache is not None:
499 self._tableCache[(tag, data)] = table
500 return table
501
502 def __setitem__(self, tag, table):
503 self.tables[Tag(tag)] = table
504
505 def __delitem__(self, tag):
506 if tag not in self:
507 raise KeyError("'%s' table not found" % tag)
508 if tag in self.tables:
509 del self.tables[tag]
510 if self.reader and tag in self.reader:
511 del self.reader[tag]
512
513 def get(self, tag, default=None):
514 """Returns the table if it exists or (optionally) a default if it doesn't."""
515 try:
516 return self[tag]
517 except KeyError:
518 return default
519
520 def setGlyphOrder(self, glyphOrder):
521 """Set the glyph order
522
523 Args:
524 glyphOrder ([str]): List of glyph names in order.
525 """
526 self.glyphOrder = glyphOrder
527 if hasattr(self, "_reverseGlyphOrderDict"):
528 del self._reverseGlyphOrderDict
529 if self.isLoaded("glyf"):
530 self["glyf"].setGlyphOrder(glyphOrder)
531
532 def getGlyphOrder(self):
533 """Returns a list of glyph names ordered by their position in the font."""
534 try:
535 return self.glyphOrder
536 except AttributeError:
537 pass
538 if "CFF " in self:
539 cff = self["CFF "]
540 self.glyphOrder = cff.getGlyphOrder()
541 elif "post" in self:
542 # TrueType font
543 glyphOrder = self["post"].getGlyphOrder()
544 if glyphOrder is None:
545 #
546 # No names found in the 'post' table.
547 # Try to create glyph names from the unicode cmap (if available)
548 # in combination with the Adobe Glyph List (AGL).
549 #
550 self._getGlyphNamesFromCmap()
551 elif len(glyphOrder) < self["maxp"].numGlyphs:
552 #
553 # Not enough names found in the 'post' table.
554 # Can happen when 'post' format 1 is improperly used on a font that
555 # has more than 258 glyphs (the length of 'standardGlyphOrder').
556 #
557 log.warning(
558 "Not enough names found in the 'post' table, generating them from cmap instead"
559 )
560 self._getGlyphNamesFromCmap()
561 else:
562 self.glyphOrder = glyphOrder
563 else:
564 self._getGlyphNamesFromCmap()
565 return self.glyphOrder
566
567 def _getGlyphNamesFromCmap(self):
568 #
569 # This is rather convoluted, but then again, it's an interesting problem:
570 # - we need to use the unicode values found in the cmap table to
571 # build glyph names (eg. because there is only a minimal post table,
572 # or none at all).
573 # - but the cmap parser also needs glyph names to work with...
574 # So here's what we do:
575 # - make up glyph names based on glyphID
576 # - load a temporary cmap table based on those names
577 # - extract the unicode values, build the "real" glyph names
578 # - unload the temporary cmap table
579 #
580 if self.isLoaded("cmap"):
581 # Bootstrapping: we're getting called by the cmap parser
582 # itself. This means self.tables['cmap'] contains a partially
583 # loaded cmap, making it impossible to get at a unicode
584 # subtable here. We remove the partially loaded cmap and
585 # restore it later.
586 # This only happens if the cmap table is loaded before any
587 # other table that does f.getGlyphOrder() or f.getGlyphName().
588 cmapLoading = self.tables["cmap"]
589 del self.tables["cmap"]
590 else:
591 cmapLoading = None
592 # Make up glyph names based on glyphID, which will be used by the
593 # temporary cmap and by the real cmap in case we don't find a unicode
594 # cmap.
595 numGlyphs = int(self["maxp"].numGlyphs)
596 glyphOrder = ["glyph%.5d" % i for i in range(numGlyphs)]
597 glyphOrder[0] = ".notdef"
598 # Set the glyph order, so the cmap parser has something
599 # to work with (so we don't get called recursively).
600 self.glyphOrder = glyphOrder
601
602 # Make up glyph names based on the reversed cmap table. Because some
603 # glyphs (eg. ligatures or alternates) may not be reachable via cmap,
604 # this naming table will usually not cover all glyphs in the font.
605 # If the font has no Unicode cmap table, reversecmap will be empty.
606 if "cmap" in self:
607 reversecmap = self["cmap"].buildReversedMin()
608 else:
609 reversecmap = {}
610 useCount = {}
611 for i in range(numGlyphs):
612 tempName = glyphOrder[i]
613 if tempName in reversecmap:
614 # If a font maps both U+0041 LATIN CAPITAL LETTER A and
615 # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph,
616 # we prefer naming the glyph as "A".
617 glyphName = self._makeGlyphName(reversecmap[tempName])
618 numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1
619 if numUses > 1:
620 glyphName = "%s.alt%d" % (glyphName, numUses - 1)
621 glyphOrder[i] = glyphName
622
623 if "cmap" in self:
624 # Delete the temporary cmap table from the cache, so it can
625 # be parsed again with the right names.
626 del self.tables["cmap"]
627 self.glyphOrder = glyphOrder
628 if cmapLoading:
629 # restore partially loaded cmap, so it can continue loading
630 # using the proper names.
631 self.tables["cmap"] = cmapLoading
632
633 @staticmethod
634 def _makeGlyphName(codepoint):
635 from fontTools import agl # Adobe Glyph List
636
637 if codepoint in agl.UV2AGL:
638 return agl.UV2AGL[codepoint]
639 elif codepoint <= 0xFFFF:
640 return "uni%04X" % codepoint
641 else:
642 return "u%X" % codepoint
643
644 def getGlyphNames(self):
645 """Get a list of glyph names, sorted alphabetically."""
646 glyphNames = sorted(self.getGlyphOrder())
647 return glyphNames
648
649 def getGlyphNames2(self):
650 """Get a list of glyph names, sorted alphabetically,
651 but not case sensitive.
652 """
653 from fontTools.misc import textTools
654
655 return textTools.caselessSort(self.getGlyphOrder())
656
657 def getGlyphName(self, glyphID):
658 """Returns the name for the glyph with the given ID.
659
660 If no name is available, synthesises one with the form ``glyphXXXXX``` where
661 ```XXXXX`` is the zero-padded glyph ID.
662 """
663 try:
664 return self.getGlyphOrder()[glyphID]
665 except IndexError:
666 return "glyph%.5d" % glyphID
667
668 def getGlyphNameMany(self, lst):
669 """Converts a list of glyph IDs into a list of glyph names."""
670 glyphOrder = self.getGlyphOrder()
671 cnt = len(glyphOrder)
672 return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid for gid in lst]
673
674 def getGlyphID(self, glyphName):
675 """Returns the ID of the glyph with the given name."""
676 try:
677 return self.getReverseGlyphMap()[glyphName]
678 except KeyError:
679 if glyphName[:5] == "glyph":
680 try:
681 return int(glyphName[5:])
682 except (NameError, ValueError):
683 raise KeyError(glyphName)
684 raise
685
686 def getGlyphIDMany(self, lst):
687 """Converts a list of glyph names into a list of glyph IDs."""
688 d = self.getReverseGlyphMap()
689 try:
690 return [d[glyphName] for glyphName in lst]
691 except KeyError:
692 getGlyphID = self.getGlyphID
693 return [getGlyphID(glyphName) for glyphName in lst]
694
695 def getReverseGlyphMap(self, rebuild=False):
696 """Returns a mapping of glyph names to glyph IDs."""
697 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
698 self._buildReverseGlyphOrderDict()
699 return self._reverseGlyphOrderDict
700
701 def _buildReverseGlyphOrderDict(self):
702 self._reverseGlyphOrderDict = d = {}
703 for glyphID, glyphName in enumerate(self.getGlyphOrder()):
704 d[glyphName] = glyphID
705 return d
706
707 def _writeTable(self, tag, writer, done, tableCache=None):
708 """Internal helper function for self.save(). Keeps track of
709 inter-table dependencies.
710 """
711 if tag in done:
712 return
713 tableClass = getTableClass(tag)
714 for masterTable in tableClass.dependencies:
715 if masterTable not in done:
716 if masterTable in self:
717 self._writeTable(masterTable, writer, done, tableCache)
718 else:
719 done.append(masterTable)
720 done.append(tag)
721 tabledata = self.getTableData(tag)
722 if tableCache is not None:
723 entry = tableCache.get((Tag(tag), tabledata))
724 if entry is not None:
725 log.debug("reusing '%s' table", tag)
726 writer.setEntry(tag, entry)
727 return
728 log.debug("Writing '%s' table to disk", tag)
729 writer[tag] = tabledata
730 if tableCache is not None:
731 tableCache[(Tag(tag), tabledata)] = writer[tag]
732
733 def getTableData(self, tag):
734 """Returns the binary representation of a table.
735
736 If the table is currently loaded and in memory, the data is compiled to
737 binary and returned; if it is not currently loaded, the binary data is
738 read from the font file and returned.
739 """
740 tag = Tag(tag)
741 if self.isLoaded(tag):
742 log.debug("Compiling '%s' table", tag)
743 return self.tables[tag].compile(self)
744 elif self.reader and tag in self.reader:
745 log.debug("Reading '%s' table from disk", tag)
746 return self.reader[tag]
747 else:
748 raise KeyError(tag)
749
750 def getGlyphSet(
751 self, preferCFF=True, location=None, normalized=False, recalcBounds=True
752 ):
753 """Return a generic GlyphSet, which is a dict-like object
754 mapping glyph names to glyph objects. The returned glyph objects
755 have a ``.draw()`` method that supports the Pen protocol, and will
756 have an attribute named 'width'.
757
758 If the font is CFF-based, the outlines will be taken from the ``CFF ``
759 or ``CFF2`` tables. Otherwise the outlines will be taken from the
760 ``glyf`` table.
761
762 If the font contains both a ``CFF ``/``CFF2`` and a ``glyf`` table, you
763 can use the ``preferCFF`` argument to specify which one should be taken.
764 If the font contains both a ``CFF `` and a ``CFF2`` table, the latter is
765 taken.
766
767 If the ``location`` parameter is set, it should be a dictionary mapping
768 four-letter variation tags to their float values, and the returned
769 glyph-set will represent an instance of a variable font at that
770 location.
771
772 If the ``normalized`` variable is set to True, that location is
773 interpreted as in the normalized (-1..+1) space, otherwise it is in the
774 font's defined axes space.
775 """
776 if location and "fvar" not in self:
777 location = None
778 if location and not normalized:
779 location = self.normalizeLocation(location)
780 glyphSet = None
781 if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self):
782 glyphSet = _TTGlyphSetCFF(self, location)
783 elif "glyf" in self:
784 glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds)
785 else:
786 raise TTLibError("Font contains no outlines")
787 if "VARC" in self:
788 glyphSet = _TTGlyphSetVARC(self, location, glyphSet)
789 return glyphSet
790
791 def normalizeLocation(self, location):
792 """Normalize a ``location`` from the font's defined axes space (also
793 known as user space) into the normalized (-1..+1) space. It applies
794 ``avar`` mapping if the font contains an ``avar`` table.
795
796 The ``location`` parameter should be a dictionary mapping four-letter
797 variation tags to their float values.
798
799 Raises ``TTLibError`` if the font is not a variable font.
800 """
801 from fontTools.varLib.models import normalizeLocation
802
803 if "fvar" not in self:
804 raise TTLibError("Not a variable font")
805
806 axes = self["fvar"].getAxes()
807 location = normalizeLocation(location, axes)
808 if "avar" in self:
809 location = self["avar"].renormalizeLocation(location, self)
810 return location
811
812 def getBestCmap(
813 self,
814 cmapPreferences=(
815 (3, 10),
816 (0, 6),
817 (0, 4),
818 (3, 1),
819 (0, 3),
820 (0, 2),
821 (0, 1),
822 (0, 0),
823 ),
824 ):
825 """Returns the 'best' Unicode cmap dictionary available in the font
826 or ``None``, if no Unicode cmap subtable is available.
827
828 By default it will search for the following (platformID, platEncID)
829 pairs in order::
830
831 (3, 10), # Windows Unicode full repertoire
832 (0, 6), # Unicode full repertoire (format 13 subtable)
833 (0, 4), # Unicode 2.0 full repertoire
834 (3, 1), # Windows Unicode BMP
835 (0, 3), # Unicode 2.0 BMP
836 (0, 2), # Unicode ISO/IEC 10646
837 (0, 1), # Unicode 1.1
838 (0, 0) # Unicode 1.0
839
840 This particular order matches what HarfBuzz uses to choose what
841 subtable to use by default. This order prefers the largest-repertoire
842 subtable, and among those, prefers the Windows-platform over the
843 Unicode-platform as the former has wider support.
844
845 This order can be customized via the ``cmapPreferences`` argument.
846 """
847 return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
848
849 def reorderGlyphs(self, new_glyph_order):
850 from .reorderGlyphs import reorderGlyphs
851
852 reorderGlyphs(self, new_glyph_order)
853
854
855class GlyphOrder(object):
856 """A pseudo table. The glyph order isn't in the font as a separate
857 table, but it's nice to present it as such in the TTX format.
858 """
859
860 def __init__(self, tag=None):
861 pass
862
863 def toXML(self, writer, ttFont):
864 glyphOrder = ttFont.getGlyphOrder()
865 writer.comment(
866 "The 'id' attribute is only for humans; " "it is ignored when parsed."
867 )
868 writer.newline()
869 for i in range(len(glyphOrder)):
870 glyphName = glyphOrder[i]
871 writer.simpletag("GlyphID", id=i, name=glyphName)
872 writer.newline()
873
874 def fromXML(self, name, attrs, content, ttFont):
875 if not hasattr(self, "glyphOrder"):
876 self.glyphOrder = []
877 if name == "GlyphID":
878 self.glyphOrder.append(attrs["name"])
879 ttFont.setGlyphOrder(self.glyphOrder)
880
881
882def getTableModule(tag):
883 """Fetch the packer/unpacker module for a table.
884 Return None when no module is found.
885 """
886 from . import tables
887
888 pyTag = tagToIdentifier(tag)
889 try:
890 __import__("fontTools.ttLib.tables." + pyTag)
891 except ImportError as err:
892 # If pyTag is found in the ImportError message,
893 # means table is not implemented. If it's not
894 # there, then some other module is missing, don't
895 # suppress the error.
896 if str(err).find(pyTag) >= 0:
897 return None
898 else:
899 raise err
900 else:
901 return getattr(tables, pyTag)
902
903
904# Registry for custom table packer/unpacker classes. Keys are table
905# tags, values are (moduleName, className) tuples.
906# See registerCustomTableClass() and getCustomTableClass()
907_customTableRegistry = {}
908
909
910def registerCustomTableClass(tag, moduleName, className=None):
911 """Register a custom packer/unpacker class for a table.
912
913 The 'moduleName' must be an importable module. If no 'className'
914 is given, it is derived from the tag, for example it will be
915 ``table_C_U_S_T_`` for a 'CUST' tag.
916
917 The registered table class should be a subclass of
918 :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable`
919 """
920 if className is None:
921 className = "table_" + tagToIdentifier(tag)
922 _customTableRegistry[tag] = (moduleName, className)
923
924
925def unregisterCustomTableClass(tag):
926 """Unregister the custom packer/unpacker class for a table."""
927 del _customTableRegistry[tag]
928
929
930def getCustomTableClass(tag):
931 """Return the custom table class for tag, if one has been registered
932 with 'registerCustomTableClass()'. Else return None.
933 """
934 if tag not in _customTableRegistry:
935 return None
936 import importlib
937
938 moduleName, className = _customTableRegistry[tag]
939 module = importlib.import_module(moduleName)
940 return getattr(module, className)
941
942
943def getTableClass(tag):
944 """Fetch the packer/unpacker class for a table."""
945 tableClass = getCustomTableClass(tag)
946 if tableClass is not None:
947 return tableClass
948 module = getTableModule(tag)
949 if module is None:
950 from .tables.DefaultTable import DefaultTable
951
952 return DefaultTable
953 pyTag = tagToIdentifier(tag)
954 tableClass = getattr(module, "table_" + pyTag)
955 return tableClass
956
957
958def getClassTag(klass):
959 """Fetch the table tag for a class object."""
960 name = klass.__name__
961 assert name[:6] == "table_"
962 name = name[6:] # Chop 'table_'
963 return identifierToTag(name)
964
965
966def newTable(tag):
967 """Return a new instance of a table."""
968 tableClass = getTableClass(tag)
969 return tableClass(tag)
970
971
972def _escapechar(c):
973 """Helper function for tagToIdentifier()"""
974 import re
975
976 if re.match("[a-z0-9]", c):
977 return "_" + c
978 elif re.match("[A-Z]", c):
979 return c + "_"
980 else:
981 return hex(byteord(c))[2:]
982
983
984def tagToIdentifier(tag):
985 """Convert a table tag to a valid (but UGLY) python identifier,
986 as well as a filename that's guaranteed to be unique even on a
987 caseless file system. Each character is mapped to two characters.
988 Lowercase letters get an underscore before the letter, uppercase
989 letters get an underscore after the letter. Trailing spaces are
990 trimmed. Illegal characters are escaped as two hex bytes. If the
991 result starts with a number (as the result of a hex escape), an
992 extra underscore is prepended. Examples:
993 .. code-block:: pycon
994
995 >>>
996 >> tagToIdentifier('glyf')
997 '_g_l_y_f'
998 >> tagToIdentifier('cvt ')
999 '_c_v_t'
1000 >> tagToIdentifier('OS/2')
1001 'O_S_2f_2'
1002 """
1003 import re
1004
1005 tag = Tag(tag)
1006 if tag == "GlyphOrder":
1007 return tag
1008 assert len(tag) == 4, "tag should be 4 characters long"
1009 while len(tag) > 1 and tag[-1] == " ":
1010 tag = tag[:-1]
1011 ident = ""
1012 for c in tag:
1013 ident = ident + _escapechar(c)
1014 if re.match("[0-9]", ident):
1015 ident = "_" + ident
1016 return ident
1017
1018
1019def identifierToTag(ident):
1020 """the opposite of tagToIdentifier()"""
1021 if ident == "GlyphOrder":
1022 return ident
1023 if len(ident) % 2 and ident[0] == "_":
1024 ident = ident[1:]
1025 assert not (len(ident) % 2)
1026 tag = ""
1027 for i in range(0, len(ident), 2):
1028 if ident[i] == "_":
1029 tag = tag + ident[i + 1]
1030 elif ident[i + 1] == "_":
1031 tag = tag + ident[i]
1032 else:
1033 # assume hex
1034 tag = tag + chr(int(ident[i : i + 2], 16))
1035 # append trailing spaces
1036 tag = tag + (4 - len(tag)) * " "
1037 return Tag(tag)
1038
1039
1040def tagToXML(tag):
1041 """Similarly to tagToIdentifier(), this converts a TT tag
1042 to a valid XML element name. Since XML element names are
1043 case sensitive, this is a fairly simple/readable translation.
1044 """
1045 import re
1046
1047 tag = Tag(tag)
1048 if tag == "OS/2":
1049 return "OS_2"
1050 elif tag == "GlyphOrder":
1051 return tag
1052 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
1053 return tag.strip()
1054 else:
1055 return tagToIdentifier(tag)
1056
1057
1058def xmlToTag(tag):
1059 """The opposite of tagToXML()"""
1060 if tag == "OS_2":
1061 return Tag("OS/2")
1062 if len(tag) == 8:
1063 return identifierToTag(tag)
1064 else:
1065 return Tag(tag + " " * (4 - len(tag)))
1066
1067
1068# Table order as recommended in the OpenType specification 1.4
1069TTFTableOrder = [
1070 "head",
1071 "hhea",
1072 "maxp",
1073 "OS/2",
1074 "hmtx",
1075 "LTSH",
1076 "VDMX",
1077 "hdmx",
1078 "cmap",
1079 "fpgm",
1080 "prep",
1081 "cvt ",
1082 "loca",
1083 "glyf",
1084 "kern",
1085 "name",
1086 "post",
1087 "gasp",
1088 "PCLT",
1089]
1090
1091OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "]
1092
1093
1094def sortedTagList(tagList, tableOrder=None):
1095 """Return a sorted copy of tagList, sorted according to the OpenType
1096 specification, or according to a custom tableOrder. If given and not
1097 None, tableOrder needs to be a list of tag names.
1098 """
1099 tagList = sorted(tagList)
1100 if tableOrder is None:
1101 if "DSIG" in tagList:
1102 # DSIG should be last (XXX spec reference?)
1103 tagList.remove("DSIG")
1104 tagList.append("DSIG")
1105 if "CFF " in tagList:
1106 tableOrder = OTFTableOrder
1107 else:
1108 tableOrder = TTFTableOrder
1109 orderedTables = []
1110 for tag in tableOrder:
1111 if tag in tagList:
1112 orderedTables.append(tag)
1113 tagList.remove(tag)
1114 orderedTables.extend(tagList)
1115 return orderedTables
1116
1117
1118def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False):
1119 """Rewrite a font file, ordering the tables as recommended by the
1120 OpenType specification 1.4.
1121 """
1122 inFile.seek(0)
1123 outFile.seek(0)
1124 reader = SFNTReader(inFile, checkChecksums=checkChecksums)
1125 writer = SFNTWriter(
1126 outFile,
1127 len(reader.tables),
1128 reader.sfntVersion,
1129 reader.flavor,
1130 reader.flavorData,
1131 )
1132 tables = list(reader.keys())
1133 for tag in sortedTagList(tables, tableOrder):
1134 writer[tag] = reader[tag]
1135 writer.close()
1136
1137
1138def maxPowerOfTwo(x):
1139 """Return the highest exponent of two, so that
1140 (2 ** exponent) <= x. Return 0 if x is 0.
1141 """
1142 exponent = 0
1143 while x:
1144 x = x >> 1
1145 exponent = exponent + 1
1146 return max(exponent - 1, 0)
1147
1148
1149def getSearchRange(n, itemSize=16):
1150 """Calculate searchRange, entrySelector, rangeShift."""
1151 # itemSize defaults to 16, for backward compatibility
1152 # with upstream fonttools.
1153 exponent = maxPowerOfTwo(n)
1154 searchRange = (2**exponent) * itemSize
1155 entrySelector = exponent
1156 rangeShift = max(0, n * itemSize - searchRange)
1157 return searchRange, entrySelector, rangeShift