def readTTCHeader(file): file.seek(0) data = file.read(ttcHeaderSize) if len(data) != ttcHeaderSize: raise TTLibError("Not a Font Collection (not enough data)") self = SimpleNamespace() sstruct.unpack(ttcHeaderFormat, data, self) if self.TTCTag != "ttcf": raise TTLibError("Not a Font Collection") assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version self.offsetTable = struct.unpack(">%dL" % self.numFonts, file.read(self.numFonts * 4)) if self.Version == 0x00020000: pass # ignoring version 2.0 signatures return self
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 getGlyphSet(self, preferCFF=True): """Return a generic GlyphSet, which is a dict-like object mapping glyph names to glyph objects. The returned glyph objects have a .draw() method that supports the Pen protocol, and will have an attribute named 'width'. If the font is CFF-based, the outlines will be taken from the 'CFF ' or 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use the 'preferCFF' argument to specify which one should be taken. If the font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. """ glyphs = None if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): table_tag = "CFF2" if "CFF2" in self else "CFF " glyphs = _TTGlyphSet( self, list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) if glyphs is None and "glyf" in self: glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) if glyphs is None: raise TTLibError("Font contains no outlines") return glyphs
def __setitem__(self, tag, data): """Write raw table data to disk.""" if tag in self.tables: raise TTLibError("cannot rewrite '%s' table" % tag) entry = self.DirectoryEntry() entry.tag = tag entry.offset = self.nextTableOffset if tag == 'head': entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) self.headTable = data entry.uncompressed = True else: entry.checkSum = calcChecksum(data) entry.saveData(self.file, data) if self.flavor == "woff": entry.origOffset = self.origNextTableOffset self.origNextTableOffset += (entry.origLength + 3) & ~3 self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3) # Add NUL bytes to pad the table data to a 4-byte boundary. # Don't depend on f.seek() as we need to add the padding even if no # subsequent write follows (seek is lazy), ie. after the final table # in the font. self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) assert self.nextTableOffset == self.file.tell() self.setEntry(tag, entry)
def fromXML(self, name, attrs, content, ttFont): if name == "hexdata": self.data[attrs["tag"]] = readHex(content) elif name == "text" and attrs["tag"] in ["dlng", "slng"]: self.data[attrs["tag"]] = strjoin(content).strip() else: raise TTLibError("can't handle '%s' element" % name)
def decompile(self, data, offset): # initial offset is from the start of trak table to the current TrackData trackDataHeader = data[offset:offset + TRACK_DATA_FORMAT_SIZE] if len(trackDataHeader) != TRACK_DATA_FORMAT_SIZE: raise TTLibError('not enough data to decompile TrackData header') sstruct.unpack(TRACK_DATA_FORMAT, trackDataHeader, self) offset += TRACK_DATA_FORMAT_SIZE nSizes = self.nSizes sizeTableOffset = self.sizeTableOffset sizeTable = [] for i in range(nSizes): sizeValueData = data[sizeTableOffset:sizeTableOffset + SIZE_VALUE_FORMAT_SIZE] if len(sizeValueData) < SIZE_VALUE_FORMAT_SIZE: raise TTLibError( 'not enough data to decompile TrackData size subtable') sizeValue, = struct.unpack(SIZE_VALUE_FORMAT, sizeValueData) sizeTable.append(fi2fl(sizeValue, 16)) sizeTableOffset += SIZE_VALUE_FORMAT_SIZE for i in range(self.nTracks): entry = TrackTableEntry() entryData = data[offset:offset + TRACK_TABLE_ENTRY_FORMAT_SIZE] if len(entryData) < TRACK_TABLE_ENTRY_FORMAT_SIZE: raise TTLibError( 'not enough data to decompile TrackTableEntry record') sstruct.unpack(TRACK_TABLE_ENTRY_FORMAT, entryData, entry) perSizeOffset = entry.offset for j in range(nSizes): size = sizeTable[j] perSizeValueData = data[perSizeOffset:perSizeOffset + PER_SIZE_VALUE_FORMAT_SIZE] if len(perSizeValueData) < PER_SIZE_VALUE_FORMAT_SIZE: raise TTLibError( 'not enough data to decompile per-size track values') perSizeValue, = struct.unpack(PER_SIZE_VALUE_FORMAT, perSizeValueData) entry[size] = perSizeValue perSizeOffset += PER_SIZE_VALUE_FORMAT_SIZE self[entry.track] = entry offset += TRACK_TABLE_ENTRY_FORMAT_SIZE
def sizes(self): if not self: return frozenset() tracks = list(self.tracks()) sizes = self[tracks.pop(0)].sizes() for track in tracks: entrySizes = self[track].sizes() if sizes != entrySizes: raise TTLibError( "'trak' table entries must specify the same sizes: " "%s != %s" % (sorted(sizes), sorted(entrySizes))) return frozenset(sizes)
def decompile(self, data, ttFont): axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] header = {} headerSize = sstruct.calcsize(AVAR_HEADER_FORMAT) header = sstruct.unpack(AVAR_HEADER_FORMAT, data[0:headerSize]) majorVersion = header["majorVersion"] if majorVersion != 1: raise TTLibError("unsupported 'avar' version %d" % majorVersion) pos = headerSize for axis in axisTags: segments = self.segments[axis] = {} numPairs = struct.unpack(">H", data[pos:pos+2])[0] pos = pos + 2 for _ in range(numPairs): fromValue, toValue = struct.unpack(">hh", data[pos:pos+4]) segments[fi2fl(fromValue, 14)] = fi2fl(toValue, 14) pos = pos + 4
def decompile(self, data, ttFont): headerSize = sstruct.calcsize(META_HEADER_FORMAT) header = sstruct.unpack(META_HEADER_FORMAT, data[0 : headerSize]) if header["version"] != 1: raise TTLibError("unsupported 'meta' version %d" % header["version"]) dataMapSize = sstruct.calcsize(DATA_MAP_FORMAT) for i in range(header["numDataMaps"]): dataMapOffset = headerSize + i * dataMapSize dataMap = sstruct.unpack( DATA_MAP_FORMAT, data[dataMapOffset : dataMapOffset + dataMapSize]) tag = dataMap["tag"] offset = dataMap["dataOffset"] self.data[tag] = data[offset : offset + dataMap["dataLength"]] if tag in ["dlng", "slng"]: self.data[tag] = self.data[tag].decode("utf-8")
def decompile(self, data, ttFont): if not self.apple: version, length, subtableFormat, coverage = struct.unpack( ">HHBB", data[:6]) if version != 0: from fontemon_blender_addon.fontTools.ttLib import TTLibError raise TTLibError("unsupported kern subtable version: %d" % version) tupleIndex = None # Should we also assert length == len(data)? data = data[6:] else: length, coverage, subtableFormat, tupleIndex = struct.unpack( ">LBBH", data[:8]) data = data[8:] assert self.format == subtableFormat, "unsupported format" self.coverage = coverage self.tupleIndex = tupleIndex self.kernTable = kernTable = {} nPairs, searchRange, entrySelector, rangeShift = struct.unpack( ">HHHH", data[:8]) data = data[8:] datas = array.array("H", data[:6 * nPairs]) if sys.byteorder != "big": datas.byteswap() it = iter(datas) glyphOrder = ttFont.getGlyphOrder() for k in range(nPairs): left, right, value = next(it), next(it), next(it) if value >= 32768: value -= 65536 try: kernTable[(glyphOrder[left], glyphOrder[right])] = value except IndexError: # Slower, but will not throw an IndexError on an invalid # glyph id. kernTable[(ttFont.getGlyphName(left), ttFont.getGlyphName(right))] = value if len(data) > 6 * nPairs + 4: # Ignore up to 4 bytes excess log.warning("excess data in 'kern' subtable: %d bytes", len(data) - 6 * nPairs)
def decompile(self, data, ttFont): header = {} headerSize = sstruct.calcsize(FVAR_HEADER_FORMAT) header = sstruct.unpack(FVAR_HEADER_FORMAT, data[0:headerSize]) if header["version"] != 0x00010000: raise TTLibError("unsupported 'fvar' version %04x" % header["version"]) pos = header["offsetToData"] axisSize = header["axisSize"] for _ in range(header["axisCount"]): axis = Axis() axis.decompile(data[pos:pos + axisSize]) self.axes.append(axis) pos += axisSize instanceSize = header["instanceSize"] axisTags = [axis.axisTag for axis in self.axes] for _ in range(header["instanceCount"]): instance = NamedInstance() instance.decompile(data[pos:pos + instanceSize], axisTags) self.instances.append(instance) pos += instanceSize
def save(self, file, reorderTables=True): """Save the font to disk. Similarly to the constructor, the 'file' argument can be either a pathname or a writable file object. """ if not hasattr(file, "write"): if self.lazy and self.reader.file.name == file: raise TTLibError( "Can't overwrite TTFont when 'lazy' attribute is True") closeStream = True file = open(file, "wb") else: # assume "file" is a writable file object closeStream = False tmp = BytesIO() writer_reordersTables = self._save(tmp) if (reorderTables is None or writer_reordersTables or (reorderTables is False and self.reader is None)): # don't reorder tables and save as is file.write(tmp.getvalue()) tmp.close() else: if reorderTables is False: # sort tables using the original font's order tableOrder = list(self.reader.keys()) else: # use the recommended order from the OpenType specification tableOrder = None tmp.flush() tmp2 = BytesIO() reorderFontTables(tmp, tmp2, tableOrder) file.write(tmp2.getvalue()) tmp.close() tmp2.close() if closeStream: file.close()
def close(self): """All tables must have been written to disk. Now write the directory. """ tables = sorted(self.tables.items()) if len(tables) != self.numTables: raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))) if self.flavor == "woff": self.signature = b"wOFF" self.reserved = 0 self.totalSfntSize = 12 self.totalSfntSize += 16 * len(tables) for tag, entry in tables: self.totalSfntSize += (entry.origLength + 3) & ~3 data = self.flavorData if self.flavorData else WOFFFlavorData() if data.majorVersion is not None and data.minorVersion is not None: self.majorVersion = data.majorVersion self.minorVersion = data.minorVersion else: if hasattr(self, 'headTable'): self.majorVersion, self.minorVersion = struct.unpack(">HH", self.headTable[4:8]) else: self.majorVersion = self.minorVersion = 0 if data.metaData: self.metaOrigLength = len(data.metaData) self.file.seek(0,2) self.metaOffset = self.file.tell() compressedMetaData = compress(data.metaData) self.metaLength = len(compressedMetaData) self.file.write(compressedMetaData) else: self.metaOffset = self.metaLength = self.metaOrigLength = 0 if data.privData: self.file.seek(0,2) off = self.file.tell() paddedOff = (off + 3) & ~3 self.file.write('\0' * (paddedOff - off)) self.privOffset = self.file.tell() self.privLength = len(data.privData) self.file.write(data.privData) else: self.privOffset = self.privLength = 0 self.file.seek(0,2) self.length = self.file.tell() else: assert not self.flavor, "Unknown flavor '%s'" % self.flavor pass directory = sstruct.pack(self.directoryFormat, self) self.file.seek(self.directoryOffset + self.directorySize) seenHead = 0 for tag, entry in tables: if tag == "head": seenHead = 1 directory = directory + entry.toString() if seenHead: self.writeMasterChecksum(directory) self.file.seek(self.directoryOffset) self.file.write(directory)
def setEntry(self, tag, entry): if tag in self.tables: raise TTLibError("cannot rewrite '%s' table" % tag) self.tables[tag] = entry