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