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