from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from . import DefaultTable from fontTools.misc.textTools import safeEval from .BitmapGlyphMetrics import BigGlyphMetrics, bigGlyphMetricsFormat, SmallGlyphMetrics, smallGlyphMetricsFormat import struct import itertools from collections import deque import logging log = logging.getLogger(__name__) eblcHeaderFormat = """ > # big endian version: 16.16F numSizes: I """ # The table format string is split to handle sbitLineMetrics simply. bitmapSizeTableFormatPart1 = """ > # big endian indexSubTableArrayOffset: I indexTablesSize: I numberOfIndexSubTables: I colorRef: I """ # The compound type for hori and vert. sbitLineMetricsFormat = """ > # big endian ascender: b descender: b widthMax: B caretSlopeNumerator: b caretSlopeDenominator: b caretOffset: b minOriginSB: b minAdvanceSB: b maxBeforeBL: b minAfterBL: b pad1: b pad2: b """ # hori and vert go between the two parts. bitmapSizeTableFormatPart2 = """ > # big endian startGlyphIndex: H endGlyphIndex: H ppemX: B ppemY: B bitDepth: B flags: b """ indexSubTableArrayFormat = ">HHL" indexSubTableArraySize = struct.calcsize(indexSubTableArrayFormat) indexSubHeaderFormat = ">HHL" indexSubHeaderSize = struct.calcsize(indexSubHeaderFormat) codeOffsetPairFormat = ">HH" codeOffsetPairSize = struct.calcsize(codeOffsetPairFormat) class table_E_B_L_C_(DefaultTable.DefaultTable): dependencies = ['EBDT'] # This method can be overridden in subclasses to support new formats # without changing the other implementation. Also can be used as a # convenience method for coverting a font file to an alternative format. def getIndexFormatClass(self, indexFormat): return eblc_sub_table_classes[indexFormat] def decompile(self, data, ttFont): # Save the original data because offsets are from the start of the table. origData = data i = 0; dummy = sstruct.unpack(eblcHeaderFormat, data[:8], self) i += 8; self.strikes = [] for curStrikeIndex in range(self.numSizes): curStrike = Strike() self.strikes.append(curStrike) curTable = curStrike.bitmapSizeTable dummy = sstruct.unpack2(bitmapSizeTableFormatPart1, data[i:i+16], curTable) i += 16 for metric in ('hori', 'vert'): metricObj = SbitLineMetrics() vars(curTable)[metric] = metricObj dummy = sstruct.unpack2(sbitLineMetricsFormat, data[i:i+12], metricObj) i += 12 dummy = sstruct.unpack(bitmapSizeTableFormatPart2, data[i:i+8], curTable) i += 8 for curStrike in self.strikes: curTable = curStrike.bitmapSizeTable for subtableIndex in range(curTable.numberOfIndexSubTables): i = curTable.indexSubTableArrayOffset + subtableIndex * indexSubTableArraySize tup = struct.unpack(indexSubTableArrayFormat, data[i:i+indexSubTableArraySize]) (firstGlyphIndex, lastGlyphIndex, additionalOffsetToIndexSubtable) = tup i = curTable.indexSubTableArrayOffset + additionalOffsetToIndexSubtable tup = struct.unpack(indexSubHeaderFormat, data[i:i+indexSubHeaderSize]) (indexFormat, imageFormat, imageDataOffset) = tup indexFormatClass = self.getIndexFormatClass(indexFormat) indexSubTable = indexFormatClass(data[i+indexSubHeaderSize:], ttFont) indexSubTable.firstGlyphIndex = firstGlyphIndex indexSubTable.lastGlyphIndex = lastGlyphIndex indexSubTable.additionalOffsetToIndexSubtable = additionalOffsetToIndexSubtable indexSubTable.indexFormat = indexFormat indexSubTable.imageFormat = imageFormat indexSubTable.imageDataOffset = imageDataOffset indexSubTable.decompile() # https://github.com/fonttools/fonttools/issues/317 curStrike.indexSubTables.append(indexSubTable) def compile(self, ttFont): dataList = [] self.numSizes = len(self.strikes) dataList.append(sstruct.pack(eblcHeaderFormat, self)) # Data size of the header + bitmapSizeTable needs to be calculated # in order to form offsets. This value will hold the size of the data # in dataList after all the data is consolidated in dataList. dataSize = len(dataList[0]) # The table will be structured in the following order: # (0) header # (1) Each bitmapSizeTable [1 ... self.numSizes] # (2) Alternate between indexSubTableArray and indexSubTable # for each bitmapSizeTable present. # # The issue is maintaining the proper offsets when table information # gets moved around. All offsets and size information must be recalculated # when building the table to allow editing within ttLib and also allow easy # import/export to and from XML. All of this offset information is lost # when exporting to XML so everything must be calculated fresh so importing # from XML will work cleanly. Only byte offset and size information is # calculated fresh. Count information like numberOfIndexSubTables is # checked through assertions. If the information in this table was not # touched or was changed properly then these types of values should match. # # The table will be rebuilt the following way: # (0) Precompute the size of all the bitmapSizeTables. This is needed to # compute the offsets properly. # (1) For each bitmapSizeTable compute the indexSubTable and # indexSubTableArray pair. The indexSubTable must be computed first # so that the offset information in indexSubTableArray can be # calculated. Update the data size after each pairing. # (2) Build each bitmapSizeTable. # (3) Consolidate all the data into the main dataList in the correct order. for curStrike in self.strikes: dataSize += sstruct.calcsize(bitmapSizeTableFormatPart1) dataSize += len(('hori', 'vert')) * sstruct.calcsize(sbitLineMetricsFormat) dataSize += sstruct.calcsize(bitmapSizeTableFormatPart2) indexSubTablePairDataList = [] for curStrike in self.strikes: curTable = curStrike.bitmapSizeTable curTable.numberOfIndexSubTables = len(curStrike.indexSubTables) curTable.indexSubTableArrayOffset = dataSize # Precompute the size of the indexSubTableArray. This information # is important for correctly calculating the new value for # additionalOffsetToIndexSubtable. sizeOfSubTableArray = curTable.numberOfIndexSubTables * indexSubTableArraySize lowerBound = dataSize dataSize += sizeOfSubTableArray upperBound = dataSize indexSubTableDataList = [] for indexSubTable in curStrike.indexSubTables: indexSubTable.additionalOffsetToIndexSubtable = dataSize - curTable.indexSubTableArrayOffset glyphIds = list(map(ttFont.getGlyphID, indexSubTable.names)) indexSubTable.firstGlyphIndex = min(glyphIds) indexSubTable.lastGlyphIndex = max(glyphIds) data = indexSubTable.compile(ttFont) indexSubTableDataList.append(data) dataSize += len(data) curTable.startGlyphIndex = min(ist.firstGlyphIndex for ist in curStrike.indexSubTables) curTable.endGlyphIndex = max(ist.lastGlyphIndex for ist in curStrike.indexSubTables) for i in curStrike.indexSubTables: data = struct.pack(indexSubHeaderFormat, i.firstGlyphIndex, i.lastGlyphIndex, i.additionalOffsetToIndexSubtable) indexSubTablePairDataList.append(data) indexSubTablePairDataList.extend(indexSubTableDataList) curTable.indexTablesSize = dataSize - curTable.indexSubTableArrayOffset for curStrike in self.strikes: curTable = curStrike.bitmapSizeTable data = sstruct.pack(bitmapSizeTableFormatPart1, curTable) dataList.append(data) for metric in ('hori', 'vert'): metricObj = vars(curTable)[metric] data = sstruct.pack(sbitLineMetricsFormat, metricObj) dataList.append(data) data = sstruct.pack(bitmapSizeTableFormatPart2, curTable) dataList.append(data) dataList.extend(indexSubTablePairDataList) return bytesjoin(dataList) def toXML(self, writer, ttFont): writer.simpletag('header', [('version', self.version)]) writer.newline() for curIndex, curStrike in enumerate(self.strikes): curStrike.toXML(curIndex, writer, ttFont) def fromXML(self, name, attrs, content, ttFont): if name == 'header': self.version = safeEval(attrs['version']) elif name == 'strike': if not hasattr(self, 'strikes'): self.strikes = [] strikeIndex = safeEval(attrs['index']) curStrike = Strike() curStrike.fromXML(name, attrs, content, ttFont, self) # Grow the strike array to the appropriate size. The XML format # allows for the strike index value to be out of order. if strikeIndex >= len(self.strikes): self.strikes += [None] * (strikeIndex + 1 - len(self.strikes)) assert self.strikes[strikeIndex] is None, "Duplicate strike EBLC indices." self.strikes[strikeIndex] = curStrike class Strike(object): def __init__(self): self.bitmapSizeTable = BitmapSizeTable() self.indexSubTables = [] def toXML(self, strikeIndex, writer, ttFont): writer.begintag('strike', [('index', strikeIndex)]) writer.newline() self.bitmapSizeTable.toXML(writer, ttFont) writer.comment('GlyphIds are written but not read. The firstGlyphIndex and\nlastGlyphIndex values will be recalculated by the compiler.') writer.newline() for indexSubTable in self.indexSubTables: indexSubTable.toXML(writer, ttFont) writer.endtag('strike') writer.newline() def fromXML(self, name, attrs, content, ttFont, locator): for element in content: if not isinstance(element, tuple): continue name, attrs, content = element if name == 'bitmapSizeTable': self.bitmapSizeTable.fromXML(name, attrs, content, ttFont) elif name.startswith(_indexSubTableSubclassPrefix): indexFormat = safeEval(name[len(_indexSubTableSubclassPrefix):]) indexFormatClass = locator.getIndexFormatClass(indexFormat) indexSubTable = indexFormatClass(None, None) indexSubTable.indexFormat = indexFormat indexSubTable.fromXML(name, attrs, content, ttFont) self.indexSubTables.append(indexSubTable) class BitmapSizeTable(object): # Returns all the simple metric names that bitmap size table # cares about in terms of XML creation. def _getXMLMetricNames(self): dataNames = sstruct.getformat(bitmapSizeTableFormatPart1)[1] dataNames = dataNames + sstruct.getformat(bitmapSizeTableFormatPart2)[1] # Skip the first 3 data names because they are byte offsets and counts. return dataNames[3:] def toXML(self, writer, ttFont): writer.begintag('bitmapSizeTable') writer.newline() for metric in ('hori', 'vert'): getattr(self, metric).toXML(metric, writer, ttFont) for metricName in self._getXMLMetricNames(): writer.simpletag(metricName, value=getattr(self, metricName)) writer.newline() writer.endtag('bitmapSizeTable') writer.newline() def fromXML(self, name, attrs, content, ttFont): # Create a lookup for all the simple names that make sense to # bitmap size table. Only read the information from these names. dataNames = set(self._getXMLMetricNames()) for element in content: if not isinstance(element, tuple): continue name, attrs, content = element if name == 'sbitLineMetrics': direction = attrs['direction'] assert direction in ('hori', 'vert'), "SbitLineMetrics direction specified invalid." metricObj = SbitLineMetrics() metricObj.fromXML(name, attrs, content, ttFont) vars(self)[direction] = metricObj elif name in dataNames: vars(self)[name] = safeEval(attrs['value']) else: log.warning("unknown name '%s' being ignored in BitmapSizeTable.", name) class SbitLineMetrics(object): def toXML(self, name, writer, ttFont): writer.begintag('sbitLineMetrics', [('direction', name)]) writer.newline() for metricName in sstruct.getformat(sbitLineMetricsFormat)[1]: writer.simpletag(metricName, value=getattr(self, metricName)) writer.newline() writer.endtag('sbitLineMetrics') writer.newline() def fromXML(self, name, attrs, content, ttFont): metricNames = set(sstruct.getformat(sbitLineMetricsFormat)[1]) for element in content: if not isinstance(element, tuple): continue name, attrs, content = element if name in metricNames: vars(self)[name] = safeEval(attrs['value']) # Important information about the naming scheme. Used for identifying subtables. _indexSubTableSubclassPrefix = 'eblc_index_sub_table_' class EblcIndexSubTable(object): def __init__(self, data, ttFont): self.data = data self.ttFont = ttFont # TODO Currently non-lazy decompiling doesn't work for this class... #if not ttFont.lazy: # self.decompile() # del self.data, self.ttFont def __getattr__(self, attr): # Allow lazy decompile. if attr[:2] == '__': raise AttributeError(attr) if not hasattr(self, "data"): raise AttributeError(attr) self.decompile() return getattr(self, attr) # This method just takes care of the indexSubHeader. Implementing subclasses # should call it to compile the indexSubHeader and then continue compiling # the remainder of their unique format. def compile(self, ttFont): return struct.pack(indexSubHeaderFormat, self.indexFormat, self.imageFormat, self.imageDataOffset) # Creates the XML for bitmap glyphs. Each index sub table basically makes # the same XML except for specific metric information that is written # out via a method call that a subclass implements optionally. def toXML(self, writer, ttFont): writer.begintag(self.__class__.__name__, [ ('imageFormat', self.imageFormat), ('firstGlyphIndex', self.firstGlyphIndex), ('lastGlyphIndex', self.lastGlyphIndex), ]) writer.newline() self.writeMetrics(writer, ttFont) # Write out the names as thats all thats needed to rebuild etc. # For font debugging of consecutive formats the ids are also written. # The ids are not read when moving from the XML format. glyphIds = map(ttFont.getGlyphID, self.names) for glyphName, glyphId in zip(self.names, glyphIds): writer.simpletag('glyphLoc', name=glyphName, id=glyphId) writer.newline() writer.endtag(self.__class__.__name__) writer.newline() def fromXML(self, name, attrs, content, ttFont): # Read all the attributes. Even though the glyph indices are # recalculated, they are still read in case there needs to # be an immediate export of the data. self.imageFormat = safeEval(attrs['imageFormat']) self.firstGlyphIndex = safeEval(attrs['firstGlyphIndex']) self.lastGlyphIndex = safeEval(attrs['lastGlyphIndex']) self.readMetrics(name, attrs, content, ttFont) self.names = [] for element in content: if not isinstance(element, tuple): continue name, attrs, content = element if name == 'glyphLoc': self.names.append(attrs['name']) # A helper method that writes the metrics for the index sub table. It also # is responsible for writing the image size for fixed size data since fixed # size is not recalculated on compile. Default behavior is to do nothing. def writeMetrics(self, writer, ttFont): pass # A helper method that is the inverse of writeMetrics. def readMetrics(self, name, attrs, content, ttFont): pass # This method is for fixed glyph data sizes. There are formats where # the glyph data is fixed but are actually composite glyphs. To handle # this the font spec in indexSubTable makes the data the size of the # fixed size by padding the component arrays. This function abstracts # out this padding process. Input is data unpadded. Output is data # padded only in fixed formats. Default behavior is to return the data. def padBitmapData(self, data): return data # Remove any of the glyph locations and names that are flagged as skipped. # This only occurs in formats {1,3}. def removeSkipGlyphs(self): # Determines if a name, location pair is a valid data location. # Skip glyphs are marked when the size is equal to zero. def isValidLocation(args): (name, (startByte, endByte)) = args return startByte < endByte # Remove all skip glyphs. dataPairs = list(filter(isValidLocation, zip(self.names, self.locations))) self.names, self.locations = list(map(list, zip(*dataPairs))) # A closure for creating a custom mixin. This is done because formats 1 and 3 # are very similar. The only difference between them is the size per offset # value. Code put in here should handle both cases generally. def _createOffsetArrayIndexSubTableMixin(formatStringForDataType): # Prep the data size for the offset array data format. dataFormat = '>'+formatStringForDataType offsetDataSize = struct.calcsize(dataFormat) class OffsetArrayIndexSubTableMixin(object): def decompile(self): numGlyphs = self.lastGlyphIndex - self.firstGlyphIndex + 1 indexingOffsets = [glyphIndex * offsetDataSize for glyphIndex in range(numGlyphs+2)] indexingLocations = zip(indexingOffsets, indexingOffsets[1:]) offsetArray = [struct.unpack(dataFormat, self.data[slice(*loc)])[0] for loc in indexingLocations] glyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex+1)) modifiedOffsets = [offset + self.imageDataOffset for offset in offsetArray] self.locations = list(zip(modifiedOffsets, modifiedOffsets[1:])) self.names = list(map(self.ttFont.getGlyphName, glyphIds)) self.removeSkipGlyphs() del self.data, self.ttFont def compile(self, ttFont): # First make sure that all the data lines up properly. Formats 1 and 3 # must have all its data lined up consecutively. If not this will fail. for curLoc, nxtLoc in zip(self.locations, self.locations[1:]): assert curLoc[1] == nxtLoc[0], "Data must be consecutive in indexSubTable offset formats" glyphIds = list(map(ttFont.getGlyphID, self.names)) # Make sure that all ids are sorted strictly increasing. assert all(glyphIds[i] < glyphIds[i+1] for i in range(len(glyphIds)-1)) # Run a simple algorithm to add skip glyphs to the data locations at # the places where an id is not present. idQueue = deque(glyphIds) locQueue = deque(self.locations) allGlyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex+1)) allLocations = [] for curId in allGlyphIds: if curId != idQueue[0]: allLocations.append((locQueue[0][0], locQueue[0][0])) else: idQueue.popleft() allLocations.append(locQueue.popleft()) # Now that all the locations are collected, pack them appropriately into # offsets. This is the form where offset[i] is the location and # offset[i+1]-offset[i] is the size of the data location. offsets = list(allLocations[0]) + [loc[1] for loc in allLocations[1:]] # Image data offset must be less than or equal to the minimum of locations. # This offset may change the value for round tripping but is safer and # allows imageDataOffset to not be required to be in the XML version. self.imageDataOffset = min(offsets) offsetArray = [offset - self.imageDataOffset for offset in offsets] dataList = [EblcIndexSubTable.compile(self, ttFont)] dataList += [struct.pack(dataFormat, offsetValue) for offsetValue in offsetArray] # Take care of any padding issues. Only occurs in format 3. if offsetDataSize * len(dataList) % 4 != 0: dataList.append(struct.pack(dataFormat, 0)) return bytesjoin(dataList) return OffsetArrayIndexSubTableMixin # A Mixin for functionality shared between the different kinds # of fixed sized data handling. Both kinds have big metrics so # that kind of special processing is also handled in this mixin. class FixedSizeIndexSubTableMixin(object): def writeMetrics(self, writer, ttFont): writer.simpletag('imageSize', value=self.imageSize) writer.newline() self.metrics.toXML(writer, ttFont) def readMetrics(self, name, attrs, content, ttFont): for element in content: if not isinstance(element, tuple): continue name, attrs, content = element if name == 'imageSize': self.imageSize = safeEval(attrs['value']) elif name == BigGlyphMetrics.__name__: self.metrics = BigGlyphMetrics() self.metrics.fromXML(name, attrs, content, ttFont) elif name == SmallGlyphMetrics.__name__: log.warning("SmallGlyphMetrics being ignored in format %d.", self.indexFormat) def padBitmapData(self, data): # Make sure that the data isn't bigger than the fixed size. assert len(data) <= self.imageSize, "Data in indexSubTable format %d must be less than the fixed size." % self.indexFormat # Pad the data so that it matches the fixed size. pad = (self.imageSize - len(data)) * b'\0' return data + pad class eblc_index_sub_table_1(_createOffsetArrayIndexSubTableMixin('L'), EblcIndexSubTable): pass class eblc_index_sub_table_2(FixedSizeIndexSubTableMixin, EblcIndexSubTable): def decompile(self): (self.imageSize,) = struct.unpack(">L", self.data[:4]) self.metrics = BigGlyphMetrics() sstruct.unpack2(bigGlyphMetricsFormat, self.data[4:], self.metrics) glyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex+1)) offsets = [self.imageSize * i + self.imageDataOffset for i in range(len(glyphIds)+1)] self.locations = list(zip(offsets, offsets[1:])) self.names = list(map(self.ttFont.getGlyphName, glyphIds)) del self.data, self.ttFont def compile(self, ttFont): glyphIds = list(map(ttFont.getGlyphID, self.names)) # Make sure all the ids are consecutive. This is required by Format 2. assert glyphIds == list(range(self.firstGlyphIndex, self.lastGlyphIndex+1)), "Format 2 ids must be consecutive." self.imageDataOffset = min(next(iter(zip(*self.locations)))) dataList = [EblcIndexSubTable.compile(self, ttFont)] dataList.append(struct.pack(">L", self.imageSize)) dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics)) return bytesjoin(dataList) class eblc_index_sub_table_3(_createOffsetArrayIndexSubTableMixin('H'), EblcIndexSubTable): pass class eblc_index_sub_table_4(EblcIndexSubTable): def decompile(self): (numGlyphs,) = struct.unpack(">L", self.data[:4]) data = self.data[4:] indexingOffsets = [glyphIndex * codeOffsetPairSize for glyphIndex in range(numGlyphs+2)] indexingLocations = zip(indexingOffsets, indexingOffsets[1:]) glyphArray = [struct.unpack(codeOffsetPairFormat, data[slice(*loc)]) for loc in indexingLocations] glyphIds, offsets = list(map(list, zip(*glyphArray))) # There are one too many glyph ids. Get rid of the last one. glyphIds.pop() offsets = [offset + self.imageDataOffset for offset in offsets] self.locations = list(zip(offsets, offsets[1:])) self.names = list(map(self.ttFont.getGlyphName, glyphIds)) del self.data, self.ttFont def compile(self, ttFont): # First make sure that all the data lines up properly. Format 4 # must have all its data lined up consecutively. If not this will fail. for curLoc, nxtLoc in zip(self.locations, self.locations[1:]): assert curLoc[1] == nxtLoc[0], "Data must be consecutive in indexSubTable format 4" offsets = list(self.locations[0]) + [loc[1] for loc in self.locations[1:]] # Image data offset must be less than or equal to the minimum of locations. # Resetting this offset may change the value for round tripping but is safer # and allows imageDataOffset to not be required to be in the XML version. self.imageDataOffset = min(offsets) offsets = [offset - self.imageDataOffset for offset in offsets] glyphIds = list(map(ttFont.getGlyphID, self.names)) # Create an iterator over the ids plus a padding value. idsPlusPad = list(itertools.chain(glyphIds, [0])) dataList = [EblcIndexSubTable.compile(self, ttFont)] dataList.append(struct.pack(">L", len(glyphIds))) tmp = [struct.pack(codeOffsetPairFormat, *cop) for cop in zip(idsPlusPad, offsets)] dataList += tmp data = bytesjoin(dataList) return data class eblc_index_sub_table_5(FixedSizeIndexSubTableMixin, EblcIndexSubTable): def decompile(self): self.origDataLen = 0 (self.imageSize,) = struct.unpack(">L", self.data[:4]) data = self.data[4:] self.metrics, data = sstruct.unpack2(bigGlyphMetricsFormat, data, BigGlyphMetrics()) (numGlyphs,) = struct.unpack(">L", data[:4]) data = data[4:] glyphIds = [struct.unpack(">H", data[2*i:2*(i+1)])[0] for i in range(numGlyphs)] offsets = [self.imageSize * i + self.imageDataOffset for i in range(len(glyphIds)+1)] self.locations = list(zip(offsets, offsets[1:])) self.names = list(map(self.ttFont.getGlyphName, glyphIds)) del self.data, self.ttFont def compile(self, ttFont): self.imageDataOffset = min(next(iter(zip(*self.locations)))) dataList = [EblcIndexSubTable.compile(self, ttFont)] dataList.append(struct.pack(">L", self.imageSize)) dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics)) glyphIds = list(map(ttFont.getGlyphID, self.names)) dataList.append(struct.pack(">L", len(glyphIds))) dataList += [struct.pack(">H", curId) for curId in glyphIds] if len(glyphIds) % 2 == 1: dataList.append(struct.pack(">H", 0)) return bytesjoin(dataList) # Dictionary of indexFormat to the class representing that format. eblc_sub_table_classes = { 1: eblc_index_sub_table_1, 2: eblc_index_sub_table_2, 3: eblc_index_sub_table_3, 4: eblc_index_sub_table_4, 5: eblc_index_sub_table_5, }