def _writeTable(self, tag, writer, done, tableCache=None): """Internal helper function for self.save(). Keeps track of inter-table dependencies. """ if tag in done: return tableClass = getTableClass(tag) for masterTable in tableClass.dependencies: if masterTable not in done: if masterTable in self: self._writeTable(masterTable, writer, done, tableCache) else: done.append(masterTable) done.append(tag) tabledata = self.getTableData(tag) if tableCache is not None: entry = tableCache.get((Tag(tag), tabledata)) if entry is not None: log.debug("reusing '%s' table", tag) writer.setEntry(tag, entry) return log.debug("Writing '%s' table to disk", tag) writer[tag] = tabledata if tableCache is not None: tableCache[(Tag(tag), tabledata)] = writer[tag]
def __init__(self, file, numTables, sfntVersion="\000\001\000\000", flavor=None, flavorData=None): if not haveBrotli: log.error( 'The WOFF2 encoder requires the Brotli Python extension, available at: ' 'https://github.com/google/brotli') raise ImportError("No module named brotli") self.file = file self.numTables = numTables self.sfntVersion = Tag(sfntVersion) self.flavorData = WOFF2FlavorData(data=flavorData) self.directoryFormat = woff2DirectoryFormat self.directorySize = woff2DirectorySize self.DirectoryEntry = WOFF2DirectoryEntry self.signature = Tag("wOF2") self.nextTableOffset = 0 self.transformBuffer = BytesIO() self.tables = OrderedDict() # make empty TTFont to store data while normalising and transforming tables self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
def xmlToTag(tag): """The opposite of tagToXML()""" if tag == "OS_2": return Tag("OS/2") if len(tag) == 8: return identifierToTag(tag) else: return Tag(tag + " " * (4 - len(tag)))
def __init__(self, file, checkChecksums=0, fontNumber=-1): self.file = file self.checkChecksums = checkChecksums self.flavor = None self.flavorData = None self.DirectoryEntry = SFNTDirectoryEntry self.file.seek(0) self.sfntVersion = self.file.read(4) self.file.seek(0) if self.sfntVersion == b"ttcf": header = readTTCHeader(self.file) numFonts = header.numFonts if not 0 <= fontNumber < numFonts: raise TTLibError( "specify a font number between 0 and %d (inclusive)" % (numFonts - 1)) self.numFonts = numFonts self.file.seek(header.offsetTable[fontNumber]) data = self.file.read(sfntDirectorySize) if len(data) != sfntDirectorySize: raise TTLibError("Not a Font Collection (not enough data)") sstruct.unpack(sfntDirectoryFormat, data, self) elif self.sfntVersion == b"wOFF": self.flavor = "woff" self.DirectoryEntry = WOFFDirectoryEntry data = self.file.read(woffDirectorySize) if len(data) != woffDirectorySize: raise TTLibError("Not a WOFF font (not enough data)") sstruct.unpack(woffDirectoryFormat, data, self) else: data = self.file.read(sfntDirectorySize) if len(data) != sfntDirectorySize: raise TTLibError( "Not a TrueType or OpenType font (not enough data)") sstruct.unpack(sfntDirectoryFormat, data, self) self.sfntVersion = Tag(self.sfntVersion) if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): raise TTLibError( "Not a TrueType or OpenType font (bad sfntVersion)") tables = {} for i in range(self.numTables): entry = self.DirectoryEntry() entry.fromFile(self.file) tag = Tag(entry.tag) tables[tag] = entry self.tables = OrderedDict( sorted(tables.items(), key=lambda i: i[1].offset)) # Load flavor data if any if self.flavor == "woff": self.flavorData = WOFFFlavorData(self)
def tagToXML(tag): """Similarly to tagToIdentifier(), this converts a TT tag to a valid XML element name. Since XML element names are case sensitive, this is a fairly simple/readable translation. """ import re tag = Tag(tag) if tag == "OS/2": return "OS_2" elif tag == "GlyphOrder": return tag if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): return tag.strip() else: return tagToIdentifier(tag)
def guessFileType(fileName): base, ext = os.path.splitext(fileName) try: with open(fileName, "rb") as f: header = f.read(256) except IOError: return None if header.startswith(b'\xef\xbb\xbf<?xml'): header = header.lstrip(b'\xef\xbb\xbf') cr, tp = getMacCreatorAndType(fileName) if tp in ("sfnt", "FFIL"): return "TTF" if ext == ".dfont": return "TTF" head = Tag(header[:4]) if head == "OTTO": return "OTF" elif head == "ttcf": return "TTC" elif head in ("\0\1\0\0", "true"): return "TTF" elif head == "wOFF": return "WOFF" elif head == "wOF2": return "WOFF2" elif head == "<?xm": # Use 'latin1' because that can't fail. header = tostr(header, 'latin1') if opentypeheaderRE.search(header): return "OTX" else: return "TTX" return None
def tagToIdentifier(tag): """Convert a table tag to a valid (but UGLY) python identifier, as well as a filename that's guaranteed to be unique even on a caseless file system. Each character is mapped to two characters. Lowercase letters get an underscore before the letter, uppercase letters get an underscore after the letter. Trailing spaces are trimmed. Illegal characters are escaped as two hex bytes. If the result starts with a number (as the result of a hex escape), an extra underscore is prepended. Examples:: >>> tagToIdentifier('glyf') '_g_l_y_f' >>> tagToIdentifier('cvt ') '_c_v_t' >>> tagToIdentifier('OS/2') 'O_S_2f_2' """ import re tag = Tag(tag) if tag == "GlyphOrder": return tag assert len(tag) == 4, "tag should be 4 characters long" while len(tag) > 1 and tag[-1] == ' ': tag = tag[:-1] ident = "" for c in tag: ident = ident + _escapechar(c) if re.match("[0-9]", ident): ident = "_" + ident return ident
def __init__(self, file, numTables, sfntVersion="\000\001\000\000", flavor=None, flavorData=None): self.file = file self.numTables = numTables self.sfntVersion = Tag(sfntVersion) self.flavor = flavor self.flavorData = flavorData if self.flavor == "woff": self.directoryFormat = woffDirectoryFormat self.directorySize = woffDirectorySize self.DirectoryEntry = WOFFDirectoryEntry self.signature = "wOFF" # to calculate WOFF checksum adjustment, we also need the original SFNT offsets self.origNextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize else: assert not self.flavor, "Unknown flavor '%s'" % self.flavor self.directoryFormat = sfntDirectoryFormat self.directorySize = sfntDirectorySize self.DirectoryEntry = SFNTDirectoryEntry from fontTools.ttLib import getSearchRange self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables, 16) self.directoryOffset = self.file.tell() self.nextTableOffset = self.directoryOffset + self.directorySize + numTables * self.DirectoryEntry.formatSize # clear out directory area self.file.seek(self.nextTableOffset) # make sure we're actually where we want to be. (old cStringIO bug) self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) self.tables = OrderedDict()
def readTag(self): pos = self.pos newpos = pos + 4 value = Tag(self.data[pos:newpos]) assert len(value) == 4, value self.pos = newpos return value
def __init__(self, file, checkChecksums=0, fontNumber=-1): if not haveBrotli: log.error( 'The WOFF2 decoder requires the Brotli Python extension, available at: ' 'https://github.com/google/brotli') raise ImportError("No module named brotli") self.file = file signature = Tag(self.file.read(4)) if signature != b"wOF2": raise TTLibError("Not a WOFF2 font (bad signature)") self.file.seek(0) self.DirectoryEntry = WOFF2DirectoryEntry data = self.file.read(woff2DirectorySize) if len(data) != woff2DirectorySize: raise TTLibError('Not a WOFF2 font (not enough data)') sstruct.unpack(woff2DirectoryFormat, data, self) self.tables = OrderedDict() offset = 0 for i in range(self.numTables): entry = self.DirectoryEntry() entry.fromFile(self.file) tag = Tag(entry.tag) self.tables[tag] = entry entry.offset = offset offset += entry.length totalUncompressedSize = offset compressedData = self.file.read(self.totalCompressedSize) decompressedData = brotli.decompress(compressedData) if len(decompressedData) != totalUncompressedSize: raise TTLibError( 'unexpected size for decompressed font data: expected %d, found %d' % (totalUncompressedSize, len(decompressedData))) self.transformBuffer = BytesIO(decompressedData) self.file.seek(0, 2) if self.length != self.file.tell(): raise TTLibError("reported 'length' doesn't match the actual file size") self.flavorData = WOFF2FlavorData(self) # make empty TTFont to store data while reconstructing tables self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
def __getitem__(self, tag): """Fetch the raw table data. Reconstruct transformed tables.""" entry = self.tables[Tag(tag)] if not hasattr(entry, 'data'): if entry.transformed: entry.data = self.reconstructTable(tag) else: entry.data = entry.loadData(self.transformBuffer) return entry.data
def getTableData(self, tag): """Returns raw table data, whether compiled or directly read from disk. """ tag = Tag(tag) if self.isLoaded(tag): log.debug("Compiling '%s' table", tag) return self.tables[tag].compile(self) elif self.reader and tag in self.reader: log.debug("Reading '%s' table from disk", tag) return self.reader[tag] else: raise KeyError(tag)
def getMacCreatorAndType(path): """Returns file creator and file type codes for a path. Args: path (str): A file path. Returns: A tuple of two :py:class:`fontTools.textTools.Tag` objects, the first representing the file creator and the second representing the file type. """ if xattr is not None: try: finderInfo = xattr.getxattr(path, 'com.apple.FinderInfo') except (KeyError, IOError): pass else: fileType = Tag(finderInfo[:4]) fileCreator = Tag(finderInfo[4:8]) return fileCreator, fileType return None, None
def __getitem__(self, tag): tag = Tag(tag) table = self.tables.get(tag) if table is None: if tag == "GlyphOrder": table = GlyphOrder(tag) self.tables[tag] = table elif self.reader is not None: table = self._readTable(tag) else: raise KeyError("'%s' table not found" % tag) return table
def fromString(self, data): if len(data) < 1: raise TTLibError("can't read table 'flags': not enough data") dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self) if self.flags & 0x3F == 0x3F: # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value if len(data) < woff2UnknownTagSize: raise TTLibError("can't read table 'tag': not enough data") dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self) else: # otherwise, tag is derived from a fixed 'Known Tags' table self.tag = woff2KnownTags[self.flags & 0x3F] self.tag = Tag(self.tag) self.origLength, data = unpackBase128(data) self.length = self.origLength if self.transformed: self.length, data = unpackBase128(data) if self.tag == 'loca' and self.length != 0: raise TTLibError( "the transformLength of the 'loca' table must be 0") # return left over data return data
def fromXML(self, name, _attrs, content, ttFont): assert (name == "Axis") for tag, _, value in filter(lambda t: type(t) is tuple, content): value = ''.join(value) if tag == "AxisTag": self.axisTag = Tag(value) elif tag in { "Flags", "MinValue", "DefaultValue", "MaxValue", "AxisNameID" }: setattr( self, tag[0].lower() + tag[1:], str2fl(value, 16) if tag.endswith("Value") else safeEval(value))
def reconstructTable(self, tag): """Reconstruct table named 'tag' from transformed data.""" entry = self.tables[Tag(tag)] rawData = entry.loadData(self.transformBuffer) if tag == 'glyf': # no need to pad glyph data when reconstructing padding = self.padding if hasattr(self, 'padding') else None data = self._reconstructGlyf(rawData, padding) elif tag == 'loca': data = self._reconstructLoca() elif tag == 'hmtx': data = self._reconstructHmtx(rawData) else: raise TTLibError("transform for table '%s' is unknown" % tag) return data
def __new__(cls, *args, **kwargs): """ Return an instance of the SFNTReader sub-class which is compatible with the input file type. """ if args and cls is SFNTReader: infile = args[0] infile.seek(0) sfntVersion = Tag(infile.read(4)) infile.seek(0) if sfntVersion == "wOF2": # return new WOFF2Reader object from fontTools.ttLib.woff2 import WOFF2Reader return object.__new__(WOFF2Reader) # return default object return object.__new__(cls)
def getTableData(self, tag): """Returns the binary representation of a table. If the table is currently loaded and in memory, the data is compiled to binary and returned; if it is not currently loaded, the binary data is read from the font file and returned. """ tag = Tag(tag) if self.isLoaded(tag): log.debug("Compiling '%s' table", tag) return self.tables[tag].compile(self) elif self.reader and tag in self.reader: log.debug("Reading '%s' table from disk", tag) return self.reader[tag] else: raise KeyError(tag)
def __getitem__(self, tag): """Fetch the raw table data.""" entry = self.tables[Tag(tag)] data = entry.loadData(self.file) if self.checkChecksums: if tag == 'head': # Beh: we have to special-case the 'head' table. checksum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) else: checksum = calcChecksum(data) if self.checkChecksums > 1: # Be obnoxious, and barf when it's wrong assert checksum == entry.checkSum, "bad checksum for '%s' table" % tag elif checksum != entry.checkSum: # Be friendly, and just log a warning. log.warning("bad checksum for '%s' table", tag) return data
def __setitem__(self, tag, data): """Associate new entry named 'tag' with raw table data.""" if tag in self.tables: raise TTLibError("cannot rewrite '%s' table" % tag) if tag == 'DSIG': # always drop DSIG table, since the encoding process can invalidate it self.numTables -= 1 return entry = self.DirectoryEntry() entry.tag = Tag(tag) entry.flags = getKnownTagIndex(entry.tag) # WOFF2 table data are written to disk only on close(), after all tags # have been specified entry.data = data self.tables[tag] = entry
def identifierToTag(ident): """the opposite of tagToIdentifier()""" if ident == "GlyphOrder": return ident if len(ident) % 2 and ident[0] == "_": ident = ident[1:] assert not (len(ident) % 2) tag = "" for i in range(0, len(ident), 2): if ident[i] == "_": tag = tag + ident[i + 1] elif ident[i + 1] == "_": tag = tag + ident[i] else: # assume hex tag = tag + chr(int(ident[i:i + 2], 16)) # append trailing spaces tag = tag + (4 - len(tag)) * ' ' return Tag(tag)
def _decompileTable(self, tag): """ Fetch table data, decompile it, and store it inside self.ttFont. """ tag = Tag(tag) if tag not in self.tables: raise TTLibError("missing required table: %s" % tag) if self.ttFont.isLoaded(tag): return data = self.tables[tag].data if tag == 'loca': tableClass = WOFF2LocaTable elif tag == 'glyf': tableClass = WOFF2GlyfTable elif tag == 'hmtx': tableClass = WOFF2HmtxTable else: tableClass = getTableClass(tag) table = tableClass(tag) self.ttFont.tables[tag] = table table.decompile(data, self.ttFont)
def __delitem__(self, tag): del self.tables[Tag(tag)]
def __init__(self, tag=None): if tag is None: tag = getClassTag(self.__class__) self.tableTag = Tag(tag)
def writeTag(self, tag): tag = Tag(tag).tobytes() assert len(tag) == 4, tag self.items.append(tag)
def __setitem__(self, tag, table): self.tables[Tag(tag)] = table
def __init__(self, tag=None): self.tableTag = Tag(tag or 'glyf')
def _add_fvar(font, axes, instances): """ Add 'fvar' table to font. axes is an ordered dictionary of DesignspaceAxis objects. instances is list of dictionary objects with 'location', 'stylename', and possibly 'postscriptfontname' entries. """ assert axes assert isinstance(axes, OrderedDict) log.info("Generating fvar") fvar = newTable('fvar') nameTable = font['name'] for a in axes.values(): axis = Axis() axis.axisTag = Tag(a.tag) # TODO Skip axes that have no variation. axis.minValue, axis.defaultValue, axis.maxValue = a.minimum, a.default, a.maximum axis.axisNameID = nameTable.addMultilingualName(a.labelNames, font, minNameID=256) axis.flags = int(a.hidden) fvar.axes.append(axis) for instance in instances: coordinates = instance.location if "en" not in instance.localisedStyleName: if not instance.styleName: raise VarLibValidationError( f"Instance at location '{coordinates}' must have a default English " "style name ('stylename' attribute on the instance element or a " "stylename element with an 'xml:lang=\"en\"' attribute).") localisedStyleName = dict(instance.localisedStyleName) localisedStyleName["en"] = tostr(instance.styleName) else: localisedStyleName = instance.localisedStyleName psname = instance.postScriptFontName inst = NamedInstance() inst.subfamilyNameID = nameTable.addMultilingualName( localisedStyleName) if psname is not None: psname = tostr(psname) inst.postscriptNameID = nameTable.addName(psname) inst.coordinates = { axes[k].tag: axes[k].map_backward(v) for k, v in coordinates.items() } #inst.coordinates = {axes[k].tag:v for k,v in coordinates.items()} fvar.instances.append(inst) assert "fvar" not in font font['fvar'] = fvar return fvar
def __init__(self, tag=None): self.tableTag = Tag(tag or 'hmtx')