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 = self.keys()
263 tags.pop(0) # skip GlyphOrder tag
264 numTables = len(tags)
265 # write to a temporary stream to allow saving to unseekable streams
266 writer = SFNTWriter(
267 file, numTables, self.sfntVersion, self.flavor, self.flavorData
268 )
269
270 done = []
271 for tag in tags:
272 self._writeTable(tag, writer, done, tableCache)
273
274 writer.close()
275
276 return writer.reordersTables()
277
278 def saveXML(self, fileOrPath, newlinestr="\n", **kwargs):
279 """Export the font as TTX (an XML-based text file), or as a series of text
280 files when splitTables is true. In the latter case, the 'fileOrPath'
281 argument should be a path to a directory.
282 The 'tables' argument must either be false (dump all tables) or a
283 list of tables to dump. The 'skipTables' argument may be a list of tables
284 to skip, but only when the 'tables' argument is false.
285 """
286
287 writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
288 self._saveXML(writer, **kwargs)
289 writer.close()
290
291 def _saveXML(
292 self,
293 writer,
294 writeVersion=True,
295 quiet=None,
296 tables=None,
297 skipTables=None,
298 splitTables=False,
299 splitGlyphs=False,
300 disassembleInstructions=True,
301 bitmapGlyphDataFormat="raw",
302 ):
303 if quiet is not None:
304 deprecateArgument("quiet", "configure logging instead")
305
306 self.disassembleInstructions = disassembleInstructions
307 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat
308 if not tables:
309 tables = self.keys()
310 if skipTables:
311 tables = [tag for tag in tables if tag not in skipTables]
312
313 if writeVersion:
314 from fontTools import version
315
316 version = ".".join(version.split(".")[:2])
317 writer.begintag(
318 "ttFont",
319 sfntVersion=repr(tostr(self.sfntVersion))[1:-1],
320 ttLibVersion=version,
321 )
322 else:
323 writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1])
324 writer.newline()
325
326 # always splitTables if splitGlyphs is enabled
327 splitTables = splitTables or splitGlyphs
328
329 if not splitTables:
330 writer.newline()
331 else:
332 path, ext = os.path.splitext(writer.filename)
333
334 for tag in tables:
335 if splitTables:
336 tablePath = path + "." + tagToIdentifier(tag) + ext
337 tableWriter = xmlWriter.XMLWriter(
338 tablePath, newlinestr=writer.newlinestr
339 )
340 tableWriter.begintag("ttFont", ttLibVersion=version)
341 tableWriter.newline()
342 tableWriter.newline()
343 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
344 writer.newline()
345 else:
346 tableWriter = writer
347 self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs)
348 if splitTables:
349 tableWriter.endtag("ttFont")
350 tableWriter.newline()
351 tableWriter.close()
352 writer.endtag("ttFont")
353 writer.newline()
354
355 def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False):
356 if quiet is not None:
357 deprecateArgument("quiet", "configure logging instead")
358 if tag in self:
359 table = self[tag]
360 report = "Dumping '%s' table..." % tag
361 else:
362 report = "No '%s' table found." % tag
363 log.info(report)
364 if tag not in self:
365 return
366 xmlTag = tagToXML(tag)
367 attrs = dict()
368 if hasattr(table, "ERROR"):
369 attrs["ERROR"] = "decompilation error"
370 from .tables.DefaultTable import DefaultTable
371
372 if table.__class__ == DefaultTable:
373 attrs["raw"] = True
374 writer.begintag(xmlTag, **attrs)
375 writer.newline()
376 if tag == "glyf":
377 table.toXML(writer, self, splitGlyphs=splitGlyphs)
378 else:
379 table.toXML(writer, self)
380 writer.endtag(xmlTag)
381 writer.newline()
382 writer.newline()
383
384 def importXML(self, fileOrPath, quiet=None):
385 """Import a TTX file (an XML-based text format), so as to recreate
386 a font object.
387 """
388 if quiet is not None:
389 deprecateArgument("quiet", "configure logging instead")
390
391 if "maxp" in self and "post" in self:
392 # Make sure the glyph order is loaded, as it otherwise gets
393 # lost if the XML doesn't contain the glyph order, yet does
394 # contain the table which was originally used to extract the
395 # glyph names from (ie. 'post', 'cmap' or 'CFF ').
396 self.getGlyphOrder()
397
398 from fontTools.misc import xmlReader
399
400 reader = xmlReader.XMLReader(fileOrPath, self)
401 reader.read()
402
403 def isLoaded(self, tag):
404 """Return true if the table identified by ``tag`` has been
405 decompiled and loaded into memory."""
406 return tag in self.tables
407
408 def has_key(self, tag):
409 """Test if the table identified by ``tag`` is present in the font.
410
411 As well as this method, ``tag in font`` can also be used to determine the
412 presence of the table."""
413 if self.isLoaded(tag):
414 return True
415 elif self.reader and tag in self.reader:
416 return True
417 elif tag == "GlyphOrder":
418 return True
419 else:
420 return False
421
422 __contains__ = has_key
423
424 def keys(self):
425 """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table."""
426 keys = list(self.tables.keys())
427 if self.reader:
428 for key in list(self.reader.keys()):
429 if key not in keys:
430 keys.append(key)
431
432 if "GlyphOrder" in keys:
433 keys.remove("GlyphOrder")
434 keys = sortedTagList(keys)
435 return ["GlyphOrder"] + keys
436
437 def ensureDecompiled(self, recurse=None):
438 """Decompile all the tables, even if a TTFont was opened in 'lazy' mode."""
439 for tag in self.keys():
440 table = self[tag]
441 if recurse is None:
442 recurse = self.lazy is not False
443 if recurse and hasattr(table, "ensureDecompiled"):
444 table.ensureDecompiled(recurse=recurse)
445 self.lazy = False
446
447 def __len__(self):
448 return len(list(self.keys()))
449
450 def __getitem__(self, tag):
451 tag = Tag(tag)
452 table = self.tables.get(tag)
453 if table is None:
454 if tag == "GlyphOrder":
455 table = GlyphOrder(tag)
456 self.tables[tag] = table
457 elif self.reader is not None:
458 table = self._readTable(tag)
459 else:
460 raise KeyError("'%s' table not found" % tag)
461 return table
462
463 def _readTable(self, tag):
464 log.debug("Reading '%s' table from disk", tag)
465 data = self.reader[tag]
466 if self._tableCache is not None:
467 table = self._tableCache.get((tag, data))
468 if table is not None:
469 return table
470 tableClass = getTableClass(tag)
471 table = tableClass(tag)
472 self.tables[tag] = table
473 log.debug("Decompiling '%s' table", tag)
474 try:
475 table.decompile(data, self)
476 except Exception:
477 if not self.ignoreDecompileErrors:
478 raise
479 # fall back to DefaultTable, retaining the binary table data
480 log.exception(
481 "An exception occurred during the decompilation of the '%s' table", tag
482 )
483 from .tables.DefaultTable import DefaultTable
484
485 file = StringIO()
486 traceback.print_exc(file=file)
487 table = DefaultTable(tag)
488 table.ERROR = file.getvalue()
489 self.tables[tag] = table
490 table.decompile(data, self)
491 if self._tableCache is not None:
492 self._tableCache[(tag, data)] = table
493 return table
494
495 def __setitem__(self, tag, table):
496 self.tables[Tag(tag)] = table
497
498 def __delitem__(self, tag):
499 if tag not in self:
500 raise KeyError("'%s' table not found" % tag)
501 if tag in self.tables:
502 del self.tables[tag]
503 if self.reader and tag in self.reader:
504 del self.reader[tag]
505
506 def get(self, tag, default=None):
507 """Returns the table if it exists or (optionally) a default if it doesn't."""
508 try:
509 return self[tag]
510 except KeyError:
511 return default
512
513 def setGlyphOrder(self, glyphOrder):
514 """Set the glyph order
515
516 Args:
517 glyphOrder ([str]): List of glyph names in order.
518 """
519 self.glyphOrder = glyphOrder
520 if hasattr(self, "_reverseGlyphOrderDict"):
521 del self._reverseGlyphOrderDict
522 if self.isLoaded("glyf"):
523 self["glyf"].setGlyphOrder(glyphOrder)
524
525 def getGlyphOrder(self):
526 """Returns a list of glyph names ordered by their position in the font."""
527 try:
528 return self.glyphOrder
529 except AttributeError:
530 pass
531 if "CFF " in self:
532 cff = self["CFF "]
533 self.glyphOrder = cff.getGlyphOrder()
534 elif "post" in self:
535 # TrueType font
536 glyphOrder = self["post"].getGlyphOrder()
537 if glyphOrder is None:
538 #
539 # No names found in the 'post' table.
540 # Try to create glyph names from the unicode cmap (if available)
541 # in combination with the Adobe Glyph List (AGL).
542 #
543 self._getGlyphNamesFromCmap()
544 elif len(glyphOrder) < self["maxp"].numGlyphs:
545 #
546 # Not enough names found in the 'post' table.
547 # Can happen when 'post' format 1 is improperly used on a font that
548 # has more than 258 glyphs (the length of 'standardGlyphOrder').
549 #
550 log.warning(
551 "Not enough names found in the 'post' table, generating them from cmap instead"
552 )
553 self._getGlyphNamesFromCmap()
554 else:
555 self.glyphOrder = glyphOrder
556 else:
557 self._getGlyphNamesFromCmap()
558 return self.glyphOrder
559
560 def _getGlyphNamesFromCmap(self):
561 #
562 # This is rather convoluted, but then again, it's an interesting problem:
563 # - we need to use the unicode values found in the cmap table to
564 # build glyph names (eg. because there is only a minimal post table,
565 # or none at all).
566 # - but the cmap parser also needs glyph names to work with...
567 # So here's what we do:
568 # - make up glyph names based on glyphID
569 # - load a temporary cmap table based on those names
570 # - extract the unicode values, build the "real" glyph names
571 # - unload the temporary cmap table
572 #
573 if self.isLoaded("cmap"):
574 # Bootstrapping: we're getting called by the cmap parser
575 # itself. This means self.tables['cmap'] contains a partially
576 # loaded cmap, making it impossible to get at a unicode
577 # subtable here. We remove the partially loaded cmap and
578 # restore it later.
579 # This only happens if the cmap table is loaded before any
580 # other table that does f.getGlyphOrder() or f.getGlyphName().
581 cmapLoading = self.tables["cmap"]
582 del self.tables["cmap"]
583 else:
584 cmapLoading = None
585 # Make up glyph names based on glyphID, which will be used by the
586 # temporary cmap and by the real cmap in case we don't find a unicode
587 # cmap.
588 numGlyphs = int(self["maxp"].numGlyphs)
589 glyphOrder = ["glyph%.5d" % i for i in range(numGlyphs)]
590 glyphOrder[0] = ".notdef"
591 # Set the glyph order, so the cmap parser has something
592 # to work with (so we don't get called recursively).
593 self.glyphOrder = glyphOrder
594
595 # Make up glyph names based on the reversed cmap table. Because some
596 # glyphs (eg. ligatures or alternates) may not be reachable via cmap,
597 # this naming table will usually not cover all glyphs in the font.
598 # If the font has no Unicode cmap table, reversecmap will be empty.
599 if "cmap" in self:
600 reversecmap = self["cmap"].buildReversedMin()
601 else:
602 reversecmap = {}
603 useCount = {}
604 for i, tempName in enumerate(glyphOrder):
605 if tempName in reversecmap:
606 # If a font maps both U+0041 LATIN CAPITAL LETTER A and
607 # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph,
608 # we prefer naming the glyph as "A".
609 glyphName = self._makeGlyphName(reversecmap[tempName])
610 numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1
611 if numUses > 1:
612 glyphName = "%s.alt%d" % (glyphName, numUses - 1)
613 glyphOrder[i] = glyphName
614
615 if "cmap" in self:
616 # Delete the temporary cmap table from the cache, so it can
617 # be parsed again with the right names.
618 del self.tables["cmap"]
619 self.glyphOrder = glyphOrder
620 if cmapLoading:
621 # restore partially loaded cmap, so it can continue loading
622 # using the proper names.
623 self.tables["cmap"] = cmapLoading
624
625 @staticmethod
626 def _makeGlyphName(codepoint):
627 from fontTools import agl # Adobe Glyph List
628
629 if codepoint in agl.UV2AGL:
630 return agl.UV2AGL[codepoint]
631 elif codepoint <= 0xFFFF:
632 return "uni%04X" % codepoint
633 else:
634 return "u%X" % codepoint
635
636 def getGlyphNames(self):
637 """Get a list of glyph names, sorted alphabetically."""
638 glyphNames = sorted(self.getGlyphOrder())
639 return glyphNames
640
641 def getGlyphNames2(self):
642 """Get a list of glyph names, sorted alphabetically,
643 but not case sensitive.
644 """
645 from fontTools.misc import textTools
646
647 return textTools.caselessSort(self.getGlyphOrder())
648
649 def getGlyphName(self, glyphID):
650 """Returns the name for the glyph with the given ID.
651
652 If no name is available, synthesises one with the form ``glyphXXXXX``` where
653 ```XXXXX`` is the zero-padded glyph ID.
654 """
655 try:
656 return self.getGlyphOrder()[glyphID]
657 except IndexError:
658 return "glyph%.5d" % glyphID
659
660 def getGlyphNameMany(self, lst):
661 """Converts a list of glyph IDs into a list of glyph names."""
662 glyphOrder = self.getGlyphOrder()
663 cnt = len(glyphOrder)
664 return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid for gid in lst]
665
666 def getGlyphID(self, glyphName):
667 """Returns the ID of the glyph with the given name."""
668 try:
669 return self.getReverseGlyphMap()[glyphName]
670 except KeyError:
671 if glyphName[:5] == "glyph":
672 try:
673 return int(glyphName[5:])
674 except (NameError, ValueError):
675 raise KeyError(glyphName)
676 raise
677
678 def getGlyphIDMany(self, lst):
679 """Converts a list of glyph names into a list of glyph IDs."""
680 d = self.getReverseGlyphMap()
681 try:
682 return [d[glyphName] for glyphName in lst]
683 except KeyError:
684 getGlyphID = self.getGlyphID
685 return [getGlyphID(glyphName) for glyphName in lst]
686
687 def getReverseGlyphMap(self, rebuild=False):
688 """Returns a mapping of glyph names to glyph IDs."""
689 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
690 self._buildReverseGlyphOrderDict()
691 return self._reverseGlyphOrderDict
692
693 def _buildReverseGlyphOrderDict(self):
694 self._reverseGlyphOrderDict = d = {}
695 for glyphID, glyphName in enumerate(self.getGlyphOrder()):
696 d[glyphName] = glyphID
697 return d
698
699 def _writeTable(self, tag, writer, done, tableCache=None):
700 """Internal helper function for self.save(). Keeps track of
701 inter-table dependencies.
702 """
703 if tag in done:
704 return
705 tableClass = getTableClass(tag)
706 for masterTable in tableClass.dependencies:
707 if masterTable not in done:
708 if masterTable in self:
709 self._writeTable(masterTable, writer, done, tableCache)
710 else:
711 done.append(masterTable)
712 done.append(tag)
713 tabledata = self.getTableData(tag)
714 if tableCache is not None:
715 entry = tableCache.get((Tag(tag), tabledata))
716 if entry is not None:
717 log.debug("reusing '%s' table", tag)
718 writer.setEntry(tag, entry)
719 return
720 log.debug("Writing '%s' table to disk", tag)
721 writer[tag] = tabledata
722 if tableCache is not None:
723 tableCache[(Tag(tag), tabledata)] = writer[tag]
724
725 def getTableData(self, tag):
726 """Returns the binary representation of a table.
727
728 If the table is currently loaded and in memory, the data is compiled to
729 binary and returned; if it is not currently loaded, the binary data is
730 read from the font file and returned.
731 """
732 tag = Tag(tag)
733 if self.isLoaded(tag):
734 log.debug("Compiling '%s' table", tag)
735 return self.tables[tag].compile(self)
736 elif self.reader and tag in self.reader:
737 log.debug("Reading '%s' table from disk", tag)
738 return self.reader[tag]
739 else:
740 raise KeyError(tag)
741
742 def getGlyphSet(
743 self, preferCFF=True, location=None, normalized=False, recalcBounds=True
744 ):
745 """Return a generic GlyphSet, which is a dict-like object
746 mapping glyph names to glyph objects. The returned glyph objects
747 have a ``.draw()`` method that supports the Pen protocol, and will
748 have an attribute named 'width'.
749
750 If the font is CFF-based, the outlines will be taken from the ``CFF ``
751 or ``CFF2`` tables. Otherwise the outlines will be taken from the
752 ``glyf`` table.
753
754 If the font contains both a ``CFF ``/``CFF2`` and a ``glyf`` table, you
755 can use the ``preferCFF`` argument to specify which one should be taken.
756 If the font contains both a ``CFF `` and a ``CFF2`` table, the latter is
757 taken.
758
759 If the ``location`` parameter is set, it should be a dictionary mapping
760 four-letter variation tags to their float values, and the returned
761 glyph-set will represent an instance of a variable font at that
762 location.
763
764 If the ``normalized`` variable is set to True, that location is
765 interpreted as in the normalized (-1..+1) space, otherwise it is in the
766 font's defined axes space.
767 """
768 if location and "fvar" not in self:
769 location = None
770 if location and not normalized:
771 location = self.normalizeLocation(location)
772 glyphSet = None
773 if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self):
774 glyphSet = _TTGlyphSetCFF(self, location)
775 elif "glyf" in self:
776 glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds)
777 else:
778 raise TTLibError("Font contains no outlines")
779 if "VARC" in self:
780 glyphSet = _TTGlyphSetVARC(self, location, glyphSet)
781 return glyphSet
782
783 def normalizeLocation(self, location):
784 """Normalize a ``location`` from the font's defined axes space (also
785 known as user space) into the normalized (-1..+1) space. It applies
786 ``avar`` mapping if the font contains an ``avar`` table.
787
788 The ``location`` parameter should be a dictionary mapping four-letter
789 variation tags to their float values.
790
791 Raises ``TTLibError`` if the font is not a variable font.
792 """
793 from fontTools.varLib.models import normalizeLocation
794
795 if "fvar" not in self:
796 raise TTLibError("Not a variable font")
797
798 axes = self["fvar"].getAxes()
799 location = normalizeLocation(location, axes)
800 if "avar" in self:
801 location = self["avar"].renormalizeLocation(location, self)
802 return location
803
804 def getBestCmap(
805 self,
806 cmapPreferences=(
807 (3, 10),
808 (0, 6),
809 (0, 4),
810 (3, 1),
811 (0, 3),
812 (0, 2),
813 (0, 1),
814 (0, 0),
815 ),
816 ):
817 """Returns the 'best' Unicode cmap dictionary available in the font
818 or ``None``, if no Unicode cmap subtable is available.
819
820 By default it will search for the following (platformID, platEncID)
821 pairs in order::
822
823 (3, 10), # Windows Unicode full repertoire
824 (0, 6), # Unicode full repertoire (format 13 subtable)
825 (0, 4), # Unicode 2.0 full repertoire
826 (3, 1), # Windows Unicode BMP
827 (0, 3), # Unicode 2.0 BMP
828 (0, 2), # Unicode ISO/IEC 10646
829 (0, 1), # Unicode 1.1
830 (0, 0) # Unicode 1.0
831
832 This particular order matches what HarfBuzz uses to choose what
833 subtable to use by default. This order prefers the largest-repertoire
834 subtable, and among those, prefers the Windows-platform over the
835 Unicode-platform as the former has wider support.
836
837 This order can be customized via the ``cmapPreferences`` argument.
838 """
839 return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
840
841 def reorderGlyphs(self, new_glyph_order):
842 from .reorderGlyphs import reorderGlyphs
843
844 reorderGlyphs(self, new_glyph_order)
845
846
847class GlyphOrder(object):
848 """A pseudo table. The glyph order isn't in the font as a separate
849 table, but it's nice to present it as such in the TTX format.
850 """
851
852 def __init__(self, tag=None):
853 pass
854
855 def toXML(self, writer, ttFont):
856 glyphOrder = ttFont.getGlyphOrder()
857 writer.comment(
858 "The 'id' attribute is only for humans; " "it is ignored when parsed."
859 )
860 writer.newline()
861 for i, glyphName in enumerate(glyphOrder):
862 writer.simpletag("GlyphID", id=i, name=glyphName)
863 writer.newline()
864
865 def fromXML(self, name, attrs, content, ttFont):
866 if not hasattr(self, "glyphOrder"):
867 self.glyphOrder = []
868 if name == "GlyphID":
869 self.glyphOrder.append(attrs["name"])
870 ttFont.setGlyphOrder(self.glyphOrder)
871
872
873def getTableModule(tag):
874 """Fetch the packer/unpacker module for a table.
875 Return None when no module is found.
876 """
877 from . import tables
878
879 pyTag = tagToIdentifier(tag)
880 try:
881 __import__("fontTools.ttLib.tables." + pyTag)
882 except ImportError as err:
883 # If pyTag is found in the ImportError message,
884 # means table is not implemented. If it's not
885 # there, then some other module is missing, don't
886 # suppress the error.
887 if str(err).find(pyTag) >= 0:
888 return None
889 else:
890 raise err
891 else:
892 return getattr(tables, pyTag)
893
894
895# Registry for custom table packer/unpacker classes. Keys are table
896# tags, values are (moduleName, className) tuples.
897# See registerCustomTableClass() and getCustomTableClass()
898_customTableRegistry = {}
899
900
901def registerCustomTableClass(tag, moduleName, className=None):
902 """Register a custom packer/unpacker class for a table.
903
904 The 'moduleName' must be an importable module. If no 'className'
905 is given, it is derived from the tag, for example it will be
906 ``table_C_U_S_T_`` for a 'CUST' tag.
907
908 The registered table class should be a subclass of
909 :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable`
910 """
911 if className is None:
912 className = "table_" + tagToIdentifier(tag)
913 _customTableRegistry[tag] = (moduleName, className)
914
915
916def unregisterCustomTableClass(tag):
917 """Unregister the custom packer/unpacker class for a table."""
918 del _customTableRegistry[tag]
919
920
921def getCustomTableClass(tag):
922 """Return the custom table class for tag, if one has been registered
923 with 'registerCustomTableClass()'. Else return None.
924 """
925 if tag not in _customTableRegistry:
926 return None
927 import importlib
928
929 moduleName, className = _customTableRegistry[tag]
930 module = importlib.import_module(moduleName)
931 return getattr(module, className)
932
933
934def getTableClass(tag):
935 """Fetch the packer/unpacker class for a table."""
936 tableClass = getCustomTableClass(tag)
937 if tableClass is not None:
938 return tableClass
939 module = getTableModule(tag)
940 if module is None:
941 from .tables.DefaultTable import DefaultTable
942
943 return DefaultTable
944 pyTag = tagToIdentifier(tag)
945 tableClass = getattr(module, "table_" + pyTag)
946 return tableClass
947
948
949def getClassTag(klass):
950 """Fetch the table tag for a class object."""
951 name = klass.__name__
952 assert name[:6] == "table_"
953 name = name[6:] # Chop 'table_'
954 return identifierToTag(name)
955
956
957def newTable(tag):
958 """Return a new instance of a table."""
959 tableClass = getTableClass(tag)
960 return tableClass(tag)
961
962
963def _escapechar(c):
964 """Helper function for tagToIdentifier()"""
965 import re
966
967 if re.match("[a-z0-9]", c):
968 return "_" + c
969 elif re.match("[A-Z]", c):
970 return c + "_"
971 else:
972 return hex(byteord(c))[2:]
973
974
975def tagToIdentifier(tag):
976 """Convert a table tag to a valid (but UGLY) python identifier,
977 as well as a filename that's guaranteed to be unique even on a
978 caseless file system. Each character is mapped to two characters.
979 Lowercase letters get an underscore before the letter, uppercase
980 letters get an underscore after the letter. Trailing spaces are
981 trimmed. Illegal characters are escaped as two hex bytes. If the
982 result starts with a number (as the result of a hex escape), an
983 extra underscore is prepended. Examples:
984 .. code-block:: pycon
985
986 >>>
987 >> tagToIdentifier('glyf')
988 '_g_l_y_f'
989 >> tagToIdentifier('cvt ')
990 '_c_v_t'
991 >> tagToIdentifier('OS/2')
992 'O_S_2f_2'
993 """
994 import re
995
996 tag = Tag(tag)
997 if tag == "GlyphOrder":
998 return tag
999 assert len(tag) == 4, "tag should be 4 characters long"
1000 while len(tag) > 1 and tag[-1] == " ":
1001 tag = tag[:-1]
1002 ident = ""
1003 for c in tag:
1004 ident = ident + _escapechar(c)
1005 if re.match("[0-9]", ident):
1006 ident = "_" + ident
1007 return ident
1008
1009
1010def identifierToTag(ident):
1011 """the opposite of tagToIdentifier()"""
1012 if ident == "GlyphOrder":
1013 return ident
1014 if len(ident) % 2 and ident[0] == "_":
1015 ident = ident[1:]
1016 assert not (len(ident) % 2)
1017 tag = ""
1018 for i in range(0, len(ident), 2):
1019 if ident[i] == "_":
1020 tag = tag + ident[i + 1]
1021 elif ident[i + 1] == "_":
1022 tag = tag + ident[i]
1023 else:
1024 # assume hex
1025 tag = tag + chr(int(ident[i : i + 2], 16))
1026 # append trailing spaces
1027 tag = tag + (4 - len(tag)) * " "
1028 return Tag(tag)
1029
1030
1031def tagToXML(tag):
1032 """Similarly to tagToIdentifier(), this converts a TT tag
1033 to a valid XML element name. Since XML element names are
1034 case sensitive, this is a fairly simple/readable translation.
1035 """
1036 import re
1037
1038 tag = Tag(tag)
1039 if tag == "OS/2":
1040 return "OS_2"
1041 elif tag == "GlyphOrder":
1042 return tag
1043 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
1044 return tag.strip()
1045 else:
1046 return tagToIdentifier(tag)
1047
1048
1049def xmlToTag(tag):
1050 """The opposite of tagToXML()"""
1051 if tag == "OS_2":
1052 return Tag("OS/2")
1053 if len(tag) == 8:
1054 return identifierToTag(tag)
1055 else:
1056 return Tag(tag + " " * (4 - len(tag)))
1057
1058
1059# Table order as recommended in the OpenType specification 1.4
1060TTFTableOrder = [
1061 "head",
1062 "hhea",
1063 "maxp",
1064 "OS/2",
1065 "hmtx",
1066 "LTSH",
1067 "VDMX",
1068 "hdmx",
1069 "cmap",
1070 "fpgm",
1071 "prep",
1072 "cvt ",
1073 "loca",
1074 "glyf",
1075 "kern",
1076 "name",
1077 "post",
1078 "gasp",
1079 "PCLT",
1080]
1081
1082OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "]
1083
1084
1085def sortedTagList(tagList, tableOrder=None):
1086 """Return a sorted copy of tagList, sorted according to the OpenType
1087 specification, or according to a custom tableOrder. If given and not
1088 None, tableOrder needs to be a list of tag names.
1089 """
1090 tagList = sorted(tagList)
1091 if tableOrder is None:
1092 if "DSIG" in tagList:
1093 # DSIG should be last (XXX spec reference?)
1094 tagList.remove("DSIG")
1095 tagList.append("DSIG")
1096 if "CFF " in tagList:
1097 tableOrder = OTFTableOrder
1098 else:
1099 tableOrder = TTFTableOrder
1100 orderedTables = []
1101 for tag in tableOrder:
1102 if tag in tagList:
1103 orderedTables.append(tag)
1104 tagList.remove(tag)
1105 orderedTables.extend(tagList)
1106 return orderedTables
1107
1108
1109def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False):
1110 """Rewrite a font file, ordering the tables as recommended by the
1111 OpenType specification 1.4.
1112 """
1113 inFile.seek(0)
1114 outFile.seek(0)
1115 reader = SFNTReader(inFile, checkChecksums=checkChecksums)
1116 writer = SFNTWriter(
1117 outFile,
1118 len(reader.tables),
1119 reader.sfntVersion,
1120 reader.flavor,
1121 reader.flavorData,
1122 )
1123 tables = list(reader.keys())
1124 for tag in sortedTagList(tables, tableOrder):
1125 writer[tag] = reader[tag]
1126 writer.close()
1127
1128
1129def maxPowerOfTwo(x):
1130 """Return the highest exponent of two, so that
1131 (2 ** exponent) <= x. Return 0 if x is 0.
1132 """
1133 exponent = 0
1134 while x:
1135 x = x >> 1
1136 exponent = exponent + 1
1137 return max(exponent - 1, 0)
1138
1139
1140def getSearchRange(n, itemSize=16):
1141 """Calculate searchRange, entrySelector, rangeShift."""
1142 # itemSize defaults to 16, for backward compatibility
1143 # with upstream fonttools.
1144 exponent = maxPowerOfTwo(n)
1145 searchRange = (2**exponent) * itemSize
1146 entrySelector = exponent
1147 rangeShift = max(0, n * itemSize - searchRange)
1148 return searchRange, entrySelector, rangeShift