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 >> from fontTools import ttLib
32 >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file
33 >> tt['maxp'].numGlyphs
34 242
35 >> tt['OS/2'].achVendID
36 'B&H\000'
37 >> tt['head'].unitsPerEm
38 2048
39
40 For details of the objects returned when accessing each table, see :ref:`tables`.
41 To add a table to the font, use the :py:func:`newTable` function::
42
43 >> os2 = newTable("OS/2")
44 >> os2.version = 4
45 >> # set other attributes
46 >> font["OS/2"] = os2
47
48 TrueType fonts can also be serialized to and from XML format (see also the
49 :ref:`ttx` binary)::
50
51 >> tt.saveXML("afont.ttx")
52 Dumping 'LTSH' table...
53 Dumping 'OS/2' table...
54 [...]
55
56 >> tt2 = ttLib.TTFont() # Create a new font object
57 >> tt2.importXML("afont.ttx")
58 >> tt2['maxp'].numGlyphs
59 242
60
61 The TTFont object may be used as a context manager; this will cause the file
62 reader to be closed after the context ``with`` block is exited::
63
64 with TTFont(filename) as f:
65 # Do stuff
66
67 Args:
68 file: When reading a font from disk, either a pathname pointing to a file,
69 or a readable file object.
70 res_name_or_index: If running on a Macintosh, either a sfnt resource name or
71 an sfnt resource index number. If the index number is zero, TTLib will
72 autodetect whether the file is a flat file or a suitcase. (If it is a suitcase,
73 only the first 'sfnt' resource will be read.)
74 sfntVersion (str): When constructing a font object from scratch, sets the four-byte
75 sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create
76 an OpenType file, use ``OTTO``.
77 flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2
78 file.
79 checkChecksums (int): How checksum data should be treated. Default is 0
80 (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to
81 raise an exception if any wrong checksums are found.
82 recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``,
83 ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save.
84 Also compiles the glyphs on importing, which saves memory consumption and
85 time.
86 ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation
87 will be ignored, and the binary data will be returned for those tables instead.
88 recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in
89 the ``head`` table on save.
90 fontNumber (int): The index of the font in a TrueType Collection file.
91 lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon
92 access only. If it is set to False, many data structures are loaded immediately.
93 The default is ``lazy=None`` which is somewhere in between.
94 """
95
96 def __init__(
97 self,
98 file=None,
99 res_name_or_index=None,
100 sfntVersion="\000\001\000\000",
101 flavor=None,
102 checkChecksums=0,
103 verbose=None,
104 recalcBBoxes=True,
105 allowVID=NotImplemented,
106 ignoreDecompileErrors=False,
107 recalcTimestamp=True,
108 fontNumber=-1,
109 lazy=None,
110 quiet=None,
111 _tableCache=None,
112 cfg={},
113 ):
114 for name in ("verbose", "quiet"):
115 val = locals().get(name)
116 if val is not None:
117 deprecateArgument(name, "configure logging instead")
118 setattr(self, name, val)
119
120 self.lazy = lazy
121 self.recalcBBoxes = recalcBBoxes
122 self.recalcTimestamp = recalcTimestamp
123 self.tables = {}
124 self.reader = None
125 self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg)
126 self.ignoreDecompileErrors = ignoreDecompileErrors
127
128 if not file:
129 self.sfntVersion = sfntVersion
130 self.flavor = flavor
131 self.flavorData = None
132 return
133 seekable = True
134 if not hasattr(file, "read"):
135 closeStream = True
136 # assume file is a string
137 if res_name_or_index is not None:
138 # see if it contains 'sfnt' resources in the resource or data fork
139 from . import macUtils
140
141 if res_name_or_index == 0:
142 if macUtils.getSFNTResIndices(file):
143 # get the first available sfnt font.
144 file = macUtils.SFNTResourceReader(file, 1)
145 else:
146 file = open(file, "rb")
147 else:
148 file = macUtils.SFNTResourceReader(file, res_name_or_index)
149 else:
150 file = open(file, "rb")
151 else:
152 # assume "file" is a readable file object
153 closeStream = False
154 # SFNTReader wants the input file to be seekable.
155 # SpooledTemporaryFile has no seekable() on < 3.11, but still can seek:
156 # https://github.com/fonttools/fonttools/issues/3052
157 if hasattr(file, "seekable"):
158 seekable = file.seekable()
159 elif hasattr(file, "seek"):
160 try:
161 file.seek(0)
162 except UnsupportedOperation:
163 seekable = False
164
165 if not self.lazy:
166 # read input file in memory and wrap a stream around it to allow overwriting
167 if seekable:
168 file.seek(0)
169 tmp = BytesIO(file.read())
170 if hasattr(file, "name"):
171 # save reference to input file name
172 tmp.name = file.name
173 if closeStream:
174 file.close()
175 file = tmp
176 elif not seekable:
177 raise TTLibError("Input file must be seekable when lazy=True")
178 self._tableCache = _tableCache
179 self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber)
180 self.sfntVersion = self.reader.sfntVersion
181 self.flavor = self.reader.flavor
182 self.flavorData = self.reader.flavorData
183
184 def __enter__(self):
185 return self
186
187 def __exit__(self, type, value, traceback):
188 self.close()
189
190 def close(self):
191 """If we still have a reader object, close it."""
192 if self.reader is not None:
193 self.reader.close()
194
195 def save(self, file, reorderTables=True):
196 """Save the font to disk.
197
198 Args:
199 file: Similarly to the constructor, can be either a pathname or a writable
200 file object.
201 reorderTables (Option[bool]): If true (the default), reorder the tables,
202 sorting them by tag (recommended by the OpenType specification). If
203 false, retain the original font order. If None, reorder by table
204 dependency (fastest).
205 """
206 if not hasattr(file, "write"):
207 if self.lazy and self.reader.file.name == file:
208 raise TTLibError("Can't overwrite TTFont when 'lazy' attribute is True")
209 createStream = True
210 else:
211 # assume "file" is a writable file object
212 createStream = False
213
214 tmp = BytesIO()
215
216 writer_reordersTables = self._save(tmp)
217
218 if not (
219 reorderTables is None
220 or writer_reordersTables
221 or (reorderTables is False and self.reader is None)
222 ):
223 if reorderTables is False:
224 # sort tables using the original font's order
225 tableOrder = list(self.reader.keys())
226 else:
227 # use the recommended order from the OpenType specification
228 tableOrder = None
229 tmp.flush()
230 tmp2 = BytesIO()
231 reorderFontTables(tmp, tmp2, tableOrder)
232 tmp.close()
233 tmp = tmp2
234
235 if createStream:
236 # "file" is a path
237 with open(file, "wb") as file:
238 file.write(tmp.getvalue())
239 else:
240 file.write(tmp.getvalue())
241
242 tmp.close()
243
244 def _save(self, file, tableCache=None):
245 """Internal function, to be shared by save() and TTCollection.save()"""
246
247 if self.recalcTimestamp and "head" in self:
248 self[
249 "head"
250 ] # make sure 'head' is loaded so the recalculation is actually done
251
252 tags = list(self.keys())
253 if "GlyphOrder" in tags:
254 tags.remove("GlyphOrder")
255 numTables = len(tags)
256 # write to a temporary stream to allow saving to unseekable streams
257 writer = SFNTWriter(
258 file, numTables, self.sfntVersion, self.flavor, self.flavorData
259 )
260
261 done = []
262 for tag in tags:
263 self._writeTable(tag, writer, done, tableCache)
264
265 writer.close()
266
267 return writer.reordersTables()
268
269 def saveXML(self, fileOrPath, newlinestr="\n", **kwargs):
270 """Export the font as TTX (an XML-based text file), or as a series of text
271 files when splitTables is true. In the latter case, the 'fileOrPath'
272 argument should be a path to a directory.
273 The 'tables' argument must either be false (dump all tables) or a
274 list of tables to dump. The 'skipTables' argument may be a list of tables
275 to skip, but only when the 'tables' argument is false.
276 """
277
278 writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
279 self._saveXML(writer, **kwargs)
280 writer.close()
281
282 def _saveXML(
283 self,
284 writer,
285 writeVersion=True,
286 quiet=None,
287 tables=None,
288 skipTables=None,
289 splitTables=False,
290 splitGlyphs=False,
291 disassembleInstructions=True,
292 bitmapGlyphDataFormat="raw",
293 ):
294 if quiet is not None:
295 deprecateArgument("quiet", "configure logging instead")
296
297 self.disassembleInstructions = disassembleInstructions
298 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat
299 if not tables:
300 tables = list(self.keys())
301 if "GlyphOrder" not in tables:
302 tables = ["GlyphOrder"] + tables
303 if skipTables:
304 for tag in skipTables:
305 if tag in tables:
306 tables.remove(tag)
307 numTables = len(tables)
308
309 if writeVersion:
310 from fontTools import version
311
312 version = ".".join(version.split(".")[:2])
313 writer.begintag(
314 "ttFont",
315 sfntVersion=repr(tostr(self.sfntVersion))[1:-1],
316 ttLibVersion=version,
317 )
318 else:
319 writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1])
320 writer.newline()
321
322 # always splitTables if splitGlyphs is enabled
323 splitTables = splitTables or splitGlyphs
324
325 if not splitTables:
326 writer.newline()
327 else:
328 path, ext = os.path.splitext(writer.filename)
329
330 for i in range(numTables):
331 tag = tables[i]
332 if splitTables:
333 tablePath = path + "." + tagToIdentifier(tag) + ext
334 tableWriter = xmlWriter.XMLWriter(
335 tablePath, newlinestr=writer.newlinestr
336 )
337 tableWriter.begintag("ttFont", ttLibVersion=version)
338 tableWriter.newline()
339 tableWriter.newline()
340 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
341 writer.newline()
342 else:
343 tableWriter = writer
344 self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs)
345 if splitTables:
346 tableWriter.endtag("ttFont")
347 tableWriter.newline()
348 tableWriter.close()
349 writer.endtag("ttFont")
350 writer.newline()
351
352 def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False):
353 if quiet is not None:
354 deprecateArgument("quiet", "configure logging instead")
355 if tag in self:
356 table = self[tag]
357 report = "Dumping '%s' table..." % tag
358 else:
359 report = "No '%s' table found." % tag
360 log.info(report)
361 if tag not in self:
362 return
363 xmlTag = tagToXML(tag)
364 attrs = dict()
365 if hasattr(table, "ERROR"):
366 attrs["ERROR"] = "decompilation error"
367 from .tables.DefaultTable import DefaultTable
368
369 if table.__class__ == DefaultTable:
370 attrs["raw"] = True
371 writer.begintag(xmlTag, **attrs)
372 writer.newline()
373 if tag == "glyf":
374 table.toXML(writer, self, splitGlyphs=splitGlyphs)
375 else:
376 table.toXML(writer, self)
377 writer.endtag(xmlTag)
378 writer.newline()
379 writer.newline()
380
381 def importXML(self, fileOrPath, quiet=None):
382 """Import a TTX file (an XML-based text format), so as to recreate
383 a font object.
384 """
385 if quiet is not None:
386 deprecateArgument("quiet", "configure logging instead")
387
388 if "maxp" in self and "post" in self:
389 # Make sure the glyph order is loaded, as it otherwise gets
390 # lost if the XML doesn't contain the glyph order, yet does
391 # contain the table which was originally used to extract the
392 # glyph names from (ie. 'post', 'cmap' or 'CFF ').
393 self.getGlyphOrder()
394
395 from fontTools.misc import xmlReader
396
397 reader = xmlReader.XMLReader(fileOrPath, self)
398 reader.read()
399
400 def isLoaded(self, tag):
401 """Return true if the table identified by ``tag`` has been
402 decompiled and loaded into memory."""
403 return tag in self.tables
404
405 def has_key(self, tag):
406 """Test if the table identified by ``tag`` is present in the font.
407
408 As well as this method, ``tag in font`` can also be used to determine the
409 presence of the table."""
410 if self.isLoaded(tag):
411 return True
412 elif self.reader and tag in self.reader:
413 return True
414 elif tag == "GlyphOrder":
415 return True
416 else:
417 return False
418
419 __contains__ = has_key
420
421 def keys(self):
422 """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table."""
423 keys = list(self.tables.keys())
424 if self.reader:
425 for key in list(self.reader.keys()):
426 if key not in keys:
427 keys.append(key)
428
429 if "GlyphOrder" in keys:
430 keys.remove("GlyphOrder")
431 keys = sortedTagList(keys)
432 return ["GlyphOrder"] + keys
433
434 def ensureDecompiled(self, recurse=None):
435 """Decompile all the tables, even if a TTFont was opened in 'lazy' mode."""
436 for tag in self.keys():
437 table = self[tag]
438 if recurse is None:
439 recurse = self.lazy is not False
440 if recurse and hasattr(table, "ensureDecompiled"):
441 table.ensureDecompiled(recurse=recurse)
442 self.lazy = False
443
444 def __len__(self):
445 return len(list(self.keys()))
446
447 def __getitem__(self, tag):
448 tag = Tag(tag)
449 table = self.tables.get(tag)
450 if table is None:
451 if tag == "GlyphOrder":
452 table = GlyphOrder(tag)
453 self.tables[tag] = table
454 elif self.reader is not None:
455 table = self._readTable(tag)
456 else:
457 raise KeyError("'%s' table not found" % tag)
458 return table
459
460 def _readTable(self, tag):
461 log.debug("Reading '%s' table from disk", tag)
462 data = self.reader[tag]
463 if self._tableCache is not None:
464 table = self._tableCache.get((tag, data))
465 if table is not None:
466 return table
467 tableClass = getTableClass(tag)
468 table = tableClass(tag)
469 self.tables[tag] = table
470 log.debug("Decompiling '%s' table", tag)
471 try:
472 table.decompile(data, self)
473 except Exception:
474 if not self.ignoreDecompileErrors:
475 raise
476 # fall back to DefaultTable, retaining the binary table data
477 log.exception(
478 "An exception occurred during the decompilation of the '%s' table", tag
479 )
480 from .tables.DefaultTable import DefaultTable
481
482 file = StringIO()
483 traceback.print_exc(file=file)
484 table = DefaultTable(tag)
485 table.ERROR = file.getvalue()
486 self.tables[tag] = table
487 table.decompile(data, self)
488 if self._tableCache is not None:
489 self._tableCache[(tag, data)] = table
490 return table
491
492 def __setitem__(self, tag, table):
493 self.tables[Tag(tag)] = table
494
495 def __delitem__(self, tag):
496 if tag not in self:
497 raise KeyError("'%s' table not found" % tag)
498 if tag in self.tables:
499 del self.tables[tag]
500 if self.reader and tag in self.reader:
501 del self.reader[tag]
502
503 def get(self, tag, default=None):
504 """Returns the table if it exists or (optionally) a default if it doesn't."""
505 try:
506 return self[tag]
507 except KeyError:
508 return default
509
510 def setGlyphOrder(self, glyphOrder):
511 """Set the glyph order
512
513 Args:
514 glyphOrder ([str]): List of glyph names in order.
515 """
516 self.glyphOrder = glyphOrder
517 if hasattr(self, "_reverseGlyphOrderDict"):
518 del self._reverseGlyphOrderDict
519 if self.isLoaded("glyf"):
520 self["glyf"].setGlyphOrder(glyphOrder)
521
522 def getGlyphOrder(self):
523 """Returns a list of glyph names ordered by their position in the font."""
524 try:
525 return self.glyphOrder
526 except AttributeError:
527 pass
528 if "CFF " in self:
529 cff = self["CFF "]
530 self.glyphOrder = cff.getGlyphOrder()
531 elif "post" in self:
532 # TrueType font
533 glyphOrder = self["post"].getGlyphOrder()
534 if glyphOrder is None:
535 #
536 # No names found in the 'post' table.
537 # Try to create glyph names from the unicode cmap (if available)
538 # in combination with the Adobe Glyph List (AGL).
539 #
540 self._getGlyphNamesFromCmap()
541 elif len(glyphOrder) < self["maxp"].numGlyphs:
542 #
543 # Not enough names found in the 'post' table.
544 # Can happen when 'post' format 1 is improperly used on a font that
545 # has more than 258 glyphs (the length of 'standardGlyphOrder').
546 #
547 log.warning(
548 "Not enough names found in the 'post' table, generating them from cmap instead"
549 )
550 self._getGlyphNamesFromCmap()
551 else:
552 self.glyphOrder = glyphOrder
553 else:
554 self._getGlyphNamesFromCmap()
555 return self.glyphOrder
556
557 def _getGlyphNamesFromCmap(self):
558 #
559 # This is rather convoluted, but then again, it's an interesting problem:
560 # - we need to use the unicode values found in the cmap table to
561 # build glyph names (eg. because there is only a minimal post table,
562 # or none at all).
563 # - but the cmap parser also needs glyph names to work with...
564 # So here's what we do:
565 # - make up glyph names based on glyphID
566 # - load a temporary cmap table based on those names
567 # - extract the unicode values, build the "real" glyph names
568 # - unload the temporary cmap table
569 #
570 if self.isLoaded("cmap"):
571 # Bootstrapping: we're getting called by the cmap parser
572 # itself. This means self.tables['cmap'] contains a partially
573 # loaded cmap, making it impossible to get at a unicode
574 # subtable here. We remove the partially loaded cmap and
575 # restore it later.
576 # This only happens if the cmap table is loaded before any
577 # other table that does f.getGlyphOrder() or f.getGlyphName().
578 cmapLoading = self.tables["cmap"]
579 del self.tables["cmap"]
580 else:
581 cmapLoading = None
582 # Make up glyph names based on glyphID, which will be used by the
583 # temporary cmap and by the real cmap in case we don't find a unicode
584 # cmap.
585 numGlyphs = int(self["maxp"].numGlyphs)
586 glyphOrder = [None] * numGlyphs
587 glyphOrder[0] = ".notdef"
588 for i in range(1, numGlyphs):
589 glyphOrder[i] = "glyph%.5d" % i
590 # Set the glyph order, so the cmap parser has something
591 # to work with (so we don't get called recursively).
592 self.glyphOrder = glyphOrder
593
594 # Make up glyph names based on the reversed cmap table. Because some
595 # glyphs (eg. ligatures or alternates) may not be reachable via cmap,
596 # this naming table will usually not cover all glyphs in the font.
597 # If the font has no Unicode cmap table, reversecmap will be empty.
598 if "cmap" in self:
599 reversecmap = self["cmap"].buildReversed()
600 else:
601 reversecmap = {}
602 useCount = {}
603 for i in range(numGlyphs):
604 tempName = glyphOrder[i]
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(min(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 in range(len(glyphOrder)):
862 glyphName = glyphOrder[i]
863 writer.simpletag("GlyphID", id=i, name=glyphName)
864 writer.newline()
865
866 def fromXML(self, name, attrs, content, ttFont):
867 if not hasattr(self, "glyphOrder"):
868 self.glyphOrder = []
869 if name == "GlyphID":
870 self.glyphOrder.append(attrs["name"])
871 ttFont.setGlyphOrder(self.glyphOrder)
872
873
874def getTableModule(tag):
875 """Fetch the packer/unpacker module for a table.
876 Return None when no module is found.
877 """
878 from . import tables
879
880 pyTag = tagToIdentifier(tag)
881 try:
882 __import__("fontTools.ttLib.tables." + pyTag)
883 except ImportError as err:
884 # If pyTag is found in the ImportError message,
885 # means table is not implemented. If it's not
886 # there, then some other module is missing, don't
887 # suppress the error.
888 if str(err).find(pyTag) >= 0:
889 return None
890 else:
891 raise err
892 else:
893 return getattr(tables, pyTag)
894
895
896# Registry for custom table packer/unpacker classes. Keys are table
897# tags, values are (moduleName, className) tuples.
898# See registerCustomTableClass() and getCustomTableClass()
899_customTableRegistry = {}
900
901
902def registerCustomTableClass(tag, moduleName, className=None):
903 """Register a custom packer/unpacker class for a table.
904
905 The 'moduleName' must be an importable module. If no 'className'
906 is given, it is derived from the tag, for example it will be
907 ``table_C_U_S_T_`` for a 'CUST' tag.
908
909 The registered table class should be a subclass of
910 :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable`
911 """
912 if className is None:
913 className = "table_" + tagToIdentifier(tag)
914 _customTableRegistry[tag] = (moduleName, className)
915
916
917def unregisterCustomTableClass(tag):
918 """Unregister the custom packer/unpacker class for a table."""
919 del _customTableRegistry[tag]
920
921
922def getCustomTableClass(tag):
923 """Return the custom table class for tag, if one has been registered
924 with 'registerCustomTableClass()'. Else return None.
925 """
926 if tag not in _customTableRegistry:
927 return None
928 import importlib
929
930 moduleName, className = _customTableRegistry[tag]
931 module = importlib.import_module(moduleName)
932 return getattr(module, className)
933
934
935def getTableClass(tag):
936 """Fetch the packer/unpacker class for a table."""
937 tableClass = getCustomTableClass(tag)
938 if tableClass is not None:
939 return tableClass
940 module = getTableModule(tag)
941 if module is None:
942 from .tables.DefaultTable import DefaultTable
943
944 return DefaultTable
945 pyTag = tagToIdentifier(tag)
946 tableClass = getattr(module, "table_" + pyTag)
947 return tableClass
948
949
950def getClassTag(klass):
951 """Fetch the table tag for a class object."""
952 name = klass.__name__
953 assert name[:6] == "table_"
954 name = name[6:] # Chop 'table_'
955 return identifierToTag(name)
956
957
958def newTable(tag):
959 """Return a new instance of a table."""
960 tableClass = getTableClass(tag)
961 return tableClass(tag)
962
963
964def _escapechar(c):
965 """Helper function for tagToIdentifier()"""
966 import re
967
968 if re.match("[a-z0-9]", c):
969 return "_" + c
970 elif re.match("[A-Z]", c):
971 return c + "_"
972 else:
973 return hex(byteord(c))[2:]
974
975
976def tagToIdentifier(tag):
977 """Convert a table tag to a valid (but UGLY) python identifier,
978 as well as a filename that's guaranteed to be unique even on a
979 caseless file system. Each character is mapped to two characters.
980 Lowercase letters get an underscore before the letter, uppercase
981 letters get an underscore after the letter. Trailing spaces are
982 trimmed. Illegal characters are escaped as two hex bytes. If the
983 result starts with a number (as the result of a hex escape), an
984 extra underscore is prepended. Examples::
985
986 >>> tagToIdentifier('glyf')
987 '_g_l_y_f'
988 >>> tagToIdentifier('cvt ')
989 '_c_v_t'
990 >>> tagToIdentifier('OS/2')
991 'O_S_2f_2'
992 """
993 import re
994
995 tag = Tag(tag)
996 if tag == "GlyphOrder":
997 return tag
998 assert len(tag) == 4, "tag should be 4 characters long"
999 while len(tag) > 1 and tag[-1] == " ":
1000 tag = tag[:-1]
1001 ident = ""
1002 for c in tag:
1003 ident = ident + _escapechar(c)
1004 if re.match("[0-9]", ident):
1005 ident = "_" + ident
1006 return ident
1007
1008
1009def identifierToTag(ident):
1010 """the opposite of tagToIdentifier()"""
1011 if ident == "GlyphOrder":
1012 return ident
1013 if len(ident) % 2 and ident[0] == "_":
1014 ident = ident[1:]
1015 assert not (len(ident) % 2)
1016 tag = ""
1017 for i in range(0, len(ident), 2):
1018 if ident[i] == "_":
1019 tag = tag + ident[i + 1]
1020 elif ident[i + 1] == "_":
1021 tag = tag + ident[i]
1022 else:
1023 # assume hex
1024 tag = tag + chr(int(ident[i : i + 2], 16))
1025 # append trailing spaces
1026 tag = tag + (4 - len(tag)) * " "
1027 return Tag(tag)
1028
1029
1030def tagToXML(tag):
1031 """Similarly to tagToIdentifier(), this converts a TT tag
1032 to a valid XML element name. Since XML element names are
1033 case sensitive, this is a fairly simple/readable translation.
1034 """
1035 import re
1036
1037 tag = Tag(tag)
1038 if tag == "OS/2":
1039 return "OS_2"
1040 elif tag == "GlyphOrder":
1041 return tag
1042 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
1043 return tag.strip()
1044 else:
1045 return tagToIdentifier(tag)
1046
1047
1048def xmlToTag(tag):
1049 """The opposite of tagToXML()"""
1050 if tag == "OS_2":
1051 return Tag("OS/2")
1052 if len(tag) == 8:
1053 return identifierToTag(tag)
1054 else:
1055 return Tag(tag + " " * (4 - len(tag)))
1056
1057
1058# Table order as recommended in the OpenType specification 1.4
1059TTFTableOrder = [
1060 "head",
1061 "hhea",
1062 "maxp",
1063 "OS/2",
1064 "hmtx",
1065 "LTSH",
1066 "VDMX",
1067 "hdmx",
1068 "cmap",
1069 "fpgm",
1070 "prep",
1071 "cvt ",
1072 "loca",
1073 "glyf",
1074 "kern",
1075 "name",
1076 "post",
1077 "gasp",
1078 "PCLT",
1079]
1080
1081OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "]
1082
1083
1084def sortedTagList(tagList, tableOrder=None):
1085 """Return a sorted copy of tagList, sorted according to the OpenType
1086 specification, or according to a custom tableOrder. If given and not
1087 None, tableOrder needs to be a list of tag names.
1088 """
1089 tagList = sorted(tagList)
1090 if tableOrder is None:
1091 if "DSIG" in tagList:
1092 # DSIG should be last (XXX spec reference?)
1093 tagList.remove("DSIG")
1094 tagList.append("DSIG")
1095 if "CFF " in tagList:
1096 tableOrder = OTFTableOrder
1097 else:
1098 tableOrder = TTFTableOrder
1099 orderedTables = []
1100 for tag in tableOrder:
1101 if tag in tagList:
1102 orderedTables.append(tag)
1103 tagList.remove(tag)
1104 orderedTables.extend(tagList)
1105 return orderedTables
1106
1107
1108def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False):
1109 """Rewrite a font file, ordering the tables as recommended by the
1110 OpenType specification 1.4.
1111 """
1112 inFile.seek(0)
1113 outFile.seek(0)
1114 reader = SFNTReader(inFile, checkChecksums=checkChecksums)
1115 writer = SFNTWriter(
1116 outFile,
1117 len(reader.tables),
1118 reader.sfntVersion,
1119 reader.flavor,
1120 reader.flavorData,
1121 )
1122 tables = list(reader.keys())
1123 for tag in sortedTagList(tables, tableOrder):
1124 writer[tag] = reader[tag]
1125 writer.close()
1126
1127
1128def maxPowerOfTwo(x):
1129 """Return the highest exponent of two, so that
1130 (2 ** exponent) <= x. Return 0 if x is 0.
1131 """
1132 exponent = 0
1133 while x:
1134 x = x >> 1
1135 exponent = exponent + 1
1136 return max(exponent - 1, 0)
1137
1138
1139def getSearchRange(n, itemSize=16):
1140 """Calculate searchRange, entrySelector, rangeShift."""
1141 # itemSize defaults to 16, for backward compatibility
1142 # with upstream fonttools.
1143 exponent = maxPowerOfTwo(n)
1144 searchRange = (2**exponent) * itemSize
1145 entrySelector = exponent
1146 rangeShift = max(0, n * itemSize - searchRange)
1147 return searchRange, entrySelector, rangeShift