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