• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc import sstruct
2from . import DefaultTable
3from fontTools.misc.textTools import bytesjoin, safeEval
4from .BitmapGlyphMetrics import (
5    BigGlyphMetrics,
6    bigGlyphMetricsFormat,
7    SmallGlyphMetrics,
8    smallGlyphMetricsFormat,
9)
10import struct
11import itertools
12from collections import deque
13import logging
14
15
16log = logging.getLogger(__name__)
17
18eblcHeaderFormat = """
19	> # big endian
20	version:  16.16F
21	numSizes: I
22"""
23# The table format string is split to handle sbitLineMetrics simply.
24bitmapSizeTableFormatPart1 = """
25	> # big endian
26	indexSubTableArrayOffset: I
27	indexTablesSize:          I
28	numberOfIndexSubTables:   I
29	colorRef:                 I
30"""
31# The compound type for hori and vert.
32sbitLineMetricsFormat = """
33	> # big endian
34	ascender:              b
35	descender:             b
36	widthMax:              B
37	caretSlopeNumerator:   b
38	caretSlopeDenominator: b
39	caretOffset:           b
40	minOriginSB:           b
41	minAdvanceSB:          b
42	maxBeforeBL:           b
43	minAfterBL:            b
44	pad1:                  b
45	pad2:                  b
46"""
47# hori and vert go between the two parts.
48bitmapSizeTableFormatPart2 = """
49	> # big endian
50	startGlyphIndex: H
51	endGlyphIndex:   H
52	ppemX:           B
53	ppemY:           B
54	bitDepth:        B
55	flags:           b
56"""
57
58indexSubTableArrayFormat = ">HHL"
59indexSubTableArraySize = struct.calcsize(indexSubTableArrayFormat)
60
61indexSubHeaderFormat = ">HHL"
62indexSubHeaderSize = struct.calcsize(indexSubHeaderFormat)
63
64codeOffsetPairFormat = ">HH"
65codeOffsetPairSize = struct.calcsize(codeOffsetPairFormat)
66
67
68class table_E_B_L_C_(DefaultTable.DefaultTable):
69    dependencies = ["EBDT"]
70
71    # This method can be overridden in subclasses to support new formats
72    # without changing the other implementation. Also can be used as a
73    # convenience method for coverting a font file to an alternative format.
74    def getIndexFormatClass(self, indexFormat):
75        return eblc_sub_table_classes[indexFormat]
76
77    def decompile(self, data, ttFont):
78        # Save the original data because offsets are from the start of the table.
79        origData = data
80        i = 0
81
82        dummy = sstruct.unpack(eblcHeaderFormat, data[:8], self)
83        i += 8
84
85        self.strikes = []
86        for curStrikeIndex in range(self.numSizes):
87            curStrike = Strike()
88            self.strikes.append(curStrike)
89            curTable = curStrike.bitmapSizeTable
90            dummy = sstruct.unpack2(
91                bitmapSizeTableFormatPart1, data[i : i + 16], curTable
92            )
93            i += 16
94            for metric in ("hori", "vert"):
95                metricObj = SbitLineMetrics()
96                vars(curTable)[metric] = metricObj
97                dummy = sstruct.unpack2(
98                    sbitLineMetricsFormat, data[i : i + 12], metricObj
99                )
100                i += 12
101            dummy = sstruct.unpack(
102                bitmapSizeTableFormatPart2, data[i : i + 8], curTable
103            )
104            i += 8
105
106        for curStrike in self.strikes:
107            curTable = curStrike.bitmapSizeTable
108            for subtableIndex in range(curTable.numberOfIndexSubTables):
109                i = (
110                    curTable.indexSubTableArrayOffset
111                    + subtableIndex * indexSubTableArraySize
112                )
113
114                tup = struct.unpack(
115                    indexSubTableArrayFormat, data[i : i + indexSubTableArraySize]
116                )
117                (firstGlyphIndex, lastGlyphIndex, additionalOffsetToIndexSubtable) = tup
118                i = curTable.indexSubTableArrayOffset + additionalOffsetToIndexSubtable
119
120                tup = struct.unpack(
121                    indexSubHeaderFormat, data[i : i + indexSubHeaderSize]
122                )
123                (indexFormat, imageFormat, imageDataOffset) = tup
124
125                indexFormatClass = self.getIndexFormatClass(indexFormat)
126                indexSubTable = indexFormatClass(data[i + indexSubHeaderSize :], ttFont)
127                indexSubTable.firstGlyphIndex = firstGlyphIndex
128                indexSubTable.lastGlyphIndex = lastGlyphIndex
129                indexSubTable.additionalOffsetToIndexSubtable = (
130                    additionalOffsetToIndexSubtable
131                )
132                indexSubTable.indexFormat = indexFormat
133                indexSubTable.imageFormat = imageFormat
134                indexSubTable.imageDataOffset = imageDataOffset
135                indexSubTable.decompile()  # https://github.com/fonttools/fonttools/issues/317
136                curStrike.indexSubTables.append(indexSubTable)
137
138    def compile(self, ttFont):
139        dataList = []
140        self.numSizes = len(self.strikes)
141        dataList.append(sstruct.pack(eblcHeaderFormat, self))
142
143        # Data size of the header + bitmapSizeTable needs to be calculated
144        # in order to form offsets. This value will hold the size of the data
145        # in dataList after all the data is consolidated in dataList.
146        dataSize = len(dataList[0])
147
148        # The table will be structured in the following order:
149        # (0) header
150        # (1) Each bitmapSizeTable [1 ... self.numSizes]
151        # (2) Alternate between indexSubTableArray and indexSubTable
152        #     for each bitmapSizeTable present.
153        #
154        # The issue is maintaining the proper offsets when table information
155        # gets moved around. All offsets and size information must be recalculated
156        # when building the table to allow editing within ttLib and also allow easy
157        # import/export to and from XML. All of this offset information is lost
158        # when exporting to XML so everything must be calculated fresh so importing
159        # from XML will work cleanly. Only byte offset and size information is
160        # calculated fresh. Count information like numberOfIndexSubTables is
161        # checked through assertions. If the information in this table was not
162        # touched or was changed properly then these types of values should match.
163        #
164        # The table will be rebuilt the following way:
165        # (0) Precompute the size of all the bitmapSizeTables. This is needed to
166        #     compute the offsets properly.
167        # (1) For each bitmapSizeTable compute the indexSubTable and
168        #    	indexSubTableArray pair. The indexSubTable must be computed first
169        #     so that the offset information in indexSubTableArray can be
170        #     calculated. Update the data size after each pairing.
171        # (2) Build each bitmapSizeTable.
172        # (3) Consolidate all the data into the main dataList in the correct order.
173
174        for _ in self.strikes:
175            dataSize += sstruct.calcsize(bitmapSizeTableFormatPart1)
176            dataSize += len(("hori", "vert")) * sstruct.calcsize(sbitLineMetricsFormat)
177            dataSize += sstruct.calcsize(bitmapSizeTableFormatPart2)
178
179        indexSubTablePairDataList = []
180        for curStrike in self.strikes:
181            curTable = curStrike.bitmapSizeTable
182            curTable.numberOfIndexSubTables = len(curStrike.indexSubTables)
183            curTable.indexSubTableArrayOffset = dataSize
184
185            # Precompute the size of the indexSubTableArray. This information
186            # is important for correctly calculating the new value for
187            # additionalOffsetToIndexSubtable.
188            sizeOfSubTableArray = (
189                curTable.numberOfIndexSubTables * indexSubTableArraySize
190            )
191            lowerBound = dataSize
192            dataSize += sizeOfSubTableArray
193            upperBound = dataSize
194
195            indexSubTableDataList = []
196            for indexSubTable in curStrike.indexSubTables:
197                indexSubTable.additionalOffsetToIndexSubtable = (
198                    dataSize - curTable.indexSubTableArrayOffset
199                )
200                glyphIds = list(map(ttFont.getGlyphID, indexSubTable.names))
201                indexSubTable.firstGlyphIndex = min(glyphIds)
202                indexSubTable.lastGlyphIndex = max(glyphIds)
203                data = indexSubTable.compile(ttFont)
204                indexSubTableDataList.append(data)
205                dataSize += len(data)
206            curTable.startGlyphIndex = min(
207                ist.firstGlyphIndex for ist in curStrike.indexSubTables
208            )
209            curTable.endGlyphIndex = max(
210                ist.lastGlyphIndex for ist in curStrike.indexSubTables
211            )
212
213            for i in curStrike.indexSubTables:
214                data = struct.pack(
215                    indexSubHeaderFormat,
216                    i.firstGlyphIndex,
217                    i.lastGlyphIndex,
218                    i.additionalOffsetToIndexSubtable,
219                )
220                indexSubTablePairDataList.append(data)
221            indexSubTablePairDataList.extend(indexSubTableDataList)
222            curTable.indexTablesSize = dataSize - curTable.indexSubTableArrayOffset
223
224        for curStrike in self.strikes:
225            curTable = curStrike.bitmapSizeTable
226            data = sstruct.pack(bitmapSizeTableFormatPart1, curTable)
227            dataList.append(data)
228            for metric in ("hori", "vert"):
229                metricObj = vars(curTable)[metric]
230                data = sstruct.pack(sbitLineMetricsFormat, metricObj)
231                dataList.append(data)
232            data = sstruct.pack(bitmapSizeTableFormatPart2, curTable)
233            dataList.append(data)
234        dataList.extend(indexSubTablePairDataList)
235
236        return bytesjoin(dataList)
237
238    def toXML(self, writer, ttFont):
239        writer.simpletag("header", [("version", self.version)])
240        writer.newline()
241        for curIndex, curStrike in enumerate(self.strikes):
242            curStrike.toXML(curIndex, writer, ttFont)
243
244    def fromXML(self, name, attrs, content, ttFont):
245        if name == "header":
246            self.version = safeEval(attrs["version"])
247        elif name == "strike":
248            if not hasattr(self, "strikes"):
249                self.strikes = []
250            strikeIndex = safeEval(attrs["index"])
251            curStrike = Strike()
252            curStrike.fromXML(name, attrs, content, ttFont, self)
253
254            # Grow the strike array to the appropriate size. The XML format
255            # allows for the strike index value to be out of order.
256            if strikeIndex >= len(self.strikes):
257                self.strikes += [None] * (strikeIndex + 1 - len(self.strikes))
258            assert self.strikes[strikeIndex] is None, "Duplicate strike EBLC indices."
259            self.strikes[strikeIndex] = curStrike
260
261
262class Strike(object):
263    def __init__(self):
264        self.bitmapSizeTable = BitmapSizeTable()
265        self.indexSubTables = []
266
267    def toXML(self, strikeIndex, writer, ttFont):
268        writer.begintag("strike", [("index", strikeIndex)])
269        writer.newline()
270        self.bitmapSizeTable.toXML(writer, ttFont)
271        writer.comment(
272            "GlyphIds are written but not read. The firstGlyphIndex and\nlastGlyphIndex values will be recalculated by the compiler."
273        )
274        writer.newline()
275        for indexSubTable in self.indexSubTables:
276            indexSubTable.toXML(writer, ttFont)
277        writer.endtag("strike")
278        writer.newline()
279
280    def fromXML(self, name, attrs, content, ttFont, locator):
281        for element in content:
282            if not isinstance(element, tuple):
283                continue
284            name, attrs, content = element
285            if name == "bitmapSizeTable":
286                self.bitmapSizeTable.fromXML(name, attrs, content, ttFont)
287            elif name.startswith(_indexSubTableSubclassPrefix):
288                indexFormat = safeEval(name[len(_indexSubTableSubclassPrefix) :])
289                indexFormatClass = locator.getIndexFormatClass(indexFormat)
290                indexSubTable = indexFormatClass(None, None)
291                indexSubTable.indexFormat = indexFormat
292                indexSubTable.fromXML(name, attrs, content, ttFont)
293                self.indexSubTables.append(indexSubTable)
294
295
296class BitmapSizeTable(object):
297    # Returns all the simple metric names that bitmap size table
298    # cares about in terms of XML creation.
299    def _getXMLMetricNames(self):
300        dataNames = sstruct.getformat(bitmapSizeTableFormatPart1)[1]
301        dataNames = dataNames + sstruct.getformat(bitmapSizeTableFormatPart2)[1]
302        # Skip the first 3 data names because they are byte offsets and counts.
303        return dataNames[3:]
304
305    def toXML(self, writer, ttFont):
306        writer.begintag("bitmapSizeTable")
307        writer.newline()
308        for metric in ("hori", "vert"):
309            getattr(self, metric).toXML(metric, writer, ttFont)
310        for metricName in self._getXMLMetricNames():
311            writer.simpletag(metricName, value=getattr(self, metricName))
312            writer.newline()
313        writer.endtag("bitmapSizeTable")
314        writer.newline()
315
316    def fromXML(self, name, attrs, content, ttFont):
317        # Create a lookup for all the simple names that make sense to
318        # bitmap size table. Only read the information from these names.
319        dataNames = set(self._getXMLMetricNames())
320        for element in content:
321            if not isinstance(element, tuple):
322                continue
323            name, attrs, content = element
324            if name == "sbitLineMetrics":
325                direction = attrs["direction"]
326                assert direction in (
327                    "hori",
328                    "vert",
329                ), "SbitLineMetrics direction specified invalid."
330                metricObj = SbitLineMetrics()
331                metricObj.fromXML(name, attrs, content, ttFont)
332                vars(self)[direction] = metricObj
333            elif name in dataNames:
334                vars(self)[name] = safeEval(attrs["value"])
335            else:
336                log.warning("unknown name '%s' being ignored in BitmapSizeTable.", name)
337
338
339class SbitLineMetrics(object):
340    def toXML(self, name, writer, ttFont):
341        writer.begintag("sbitLineMetrics", [("direction", name)])
342        writer.newline()
343        for metricName in sstruct.getformat(sbitLineMetricsFormat)[1]:
344            writer.simpletag(metricName, value=getattr(self, metricName))
345            writer.newline()
346        writer.endtag("sbitLineMetrics")
347        writer.newline()
348
349    def fromXML(self, name, attrs, content, ttFont):
350        metricNames = set(sstruct.getformat(sbitLineMetricsFormat)[1])
351        for element in content:
352            if not isinstance(element, tuple):
353                continue
354            name, attrs, content = element
355            if name in metricNames:
356                vars(self)[name] = safeEval(attrs["value"])
357
358
359# Important information about the naming scheme. Used for identifying subtables.
360_indexSubTableSubclassPrefix = "eblc_index_sub_table_"
361
362
363class EblcIndexSubTable(object):
364    def __init__(self, data, ttFont):
365        self.data = data
366        self.ttFont = ttFont
367        # TODO Currently non-lazy decompiling doesn't work for this class...
368        # if not ttFont.lazy:
369        # 	self.decompile()
370        # 	del self.data, self.ttFont
371
372    def __getattr__(self, attr):
373        # Allow lazy decompile.
374        if attr[:2] == "__":
375            raise AttributeError(attr)
376        if attr == "data":
377            raise AttributeError(attr)
378        self.decompile()
379        return getattr(self, attr)
380
381    def ensureDecompiled(self, recurse=False):
382        if hasattr(self, "data"):
383            self.decompile()
384
385    # This method just takes care of the indexSubHeader. Implementing subclasses
386    # should call it to compile the indexSubHeader and then continue compiling
387    # the remainder of their unique format.
388    def compile(self, ttFont):
389        return struct.pack(
390            indexSubHeaderFormat,
391            self.indexFormat,
392            self.imageFormat,
393            self.imageDataOffset,
394        )
395
396    # Creates the XML for bitmap glyphs. Each index sub table basically makes
397    # the same XML except for specific metric information that is written
398    # out via a method call that a subclass implements optionally.
399    def toXML(self, writer, ttFont):
400        writer.begintag(
401            self.__class__.__name__,
402            [
403                ("imageFormat", self.imageFormat),
404                ("firstGlyphIndex", self.firstGlyphIndex),
405                ("lastGlyphIndex", self.lastGlyphIndex),
406            ],
407        )
408        writer.newline()
409        self.writeMetrics(writer, ttFont)
410        # Write out the names as thats all thats needed to rebuild etc.
411        # For font debugging of consecutive formats the ids are also written.
412        # The ids are not read when moving from the XML format.
413        glyphIds = map(ttFont.getGlyphID, self.names)
414        for glyphName, glyphId in zip(self.names, glyphIds):
415            writer.simpletag("glyphLoc", name=glyphName, id=glyphId)
416            writer.newline()
417        writer.endtag(self.__class__.__name__)
418        writer.newline()
419
420    def fromXML(self, name, attrs, content, ttFont):
421        # Read all the attributes. Even though the glyph indices are
422        # recalculated, they are still read in case there needs to
423        # be an immediate export of the data.
424        self.imageFormat = safeEval(attrs["imageFormat"])
425        self.firstGlyphIndex = safeEval(attrs["firstGlyphIndex"])
426        self.lastGlyphIndex = safeEval(attrs["lastGlyphIndex"])
427
428        self.readMetrics(name, attrs, content, ttFont)
429
430        self.names = []
431        for element in content:
432            if not isinstance(element, tuple):
433                continue
434            name, attrs, content = element
435            if name == "glyphLoc":
436                self.names.append(attrs["name"])
437
438    # A helper method that writes the metrics for the index sub table. It also
439    # is responsible for writing the image size for fixed size data since fixed
440    # size is not recalculated on compile. Default behavior is to do nothing.
441    def writeMetrics(self, writer, ttFont):
442        pass
443
444    # A helper method that is the inverse of writeMetrics.
445    def readMetrics(self, name, attrs, content, ttFont):
446        pass
447
448    # This method is for fixed glyph data sizes. There are formats where
449    # the glyph data is fixed but are actually composite glyphs. To handle
450    # this the font spec in indexSubTable makes the data the size of the
451    # fixed size by padding the component arrays. This function abstracts
452    # out this padding process. Input is data unpadded. Output is data
453    # padded only in fixed formats. Default behavior is to return the data.
454    def padBitmapData(self, data):
455        return data
456
457    # Remove any of the glyph locations and names that are flagged as skipped.
458    # This only occurs in formats {1,3}.
459    def removeSkipGlyphs(self):
460        # Determines if a name, location pair is a valid data location.
461        # Skip glyphs are marked when the size is equal to zero.
462        def isValidLocation(args):
463            (name, (startByte, endByte)) = args
464            return startByte < endByte
465
466        # Remove all skip glyphs.
467        dataPairs = list(filter(isValidLocation, zip(self.names, self.locations)))
468        self.names, self.locations = list(map(list, zip(*dataPairs)))
469
470
471# A closure for creating a custom mixin. This is done because formats 1 and 3
472# are very similar. The only difference between them is the size per offset
473# value. Code put in here should handle both cases generally.
474def _createOffsetArrayIndexSubTableMixin(formatStringForDataType):
475    # Prep the data size for the offset array data format.
476    dataFormat = ">" + formatStringForDataType
477    offsetDataSize = struct.calcsize(dataFormat)
478
479    class OffsetArrayIndexSubTableMixin(object):
480        def decompile(self):
481            numGlyphs = self.lastGlyphIndex - self.firstGlyphIndex + 1
482            indexingOffsets = [
483                glyphIndex * offsetDataSize for glyphIndex in range(numGlyphs + 2)
484            ]
485            indexingLocations = zip(indexingOffsets, indexingOffsets[1:])
486            offsetArray = [
487                struct.unpack(dataFormat, self.data[slice(*loc)])[0]
488                for loc in indexingLocations
489            ]
490
491            glyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex + 1))
492            modifiedOffsets = [offset + self.imageDataOffset for offset in offsetArray]
493            self.locations = list(zip(modifiedOffsets, modifiedOffsets[1:]))
494
495            self.names = list(map(self.ttFont.getGlyphName, glyphIds))
496            self.removeSkipGlyphs()
497            del self.data, self.ttFont
498
499        def compile(self, ttFont):
500            # First make sure that all the data lines up properly. Formats 1 and 3
501            # must have all its data lined up consecutively. If not this will fail.
502            for curLoc, nxtLoc in zip(self.locations, self.locations[1:]):
503                assert (
504                    curLoc[1] == nxtLoc[0]
505                ), "Data must be consecutive in indexSubTable offset formats"
506
507            glyphIds = list(map(ttFont.getGlyphID, self.names))
508            # Make sure that all ids are sorted strictly increasing.
509            assert all(glyphIds[i] < glyphIds[i + 1] for i in range(len(glyphIds) - 1))
510
511            # Run a simple algorithm to add skip glyphs to the data locations at
512            # the places where an id is not present.
513            idQueue = deque(glyphIds)
514            locQueue = deque(self.locations)
515            allGlyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex + 1))
516            allLocations = []
517            for curId in allGlyphIds:
518                if curId != idQueue[0]:
519                    allLocations.append((locQueue[0][0], locQueue[0][0]))
520                else:
521                    idQueue.popleft()
522                    allLocations.append(locQueue.popleft())
523
524            # Now that all the locations are collected, pack them appropriately into
525            # offsets. This is the form where offset[i] is the location and
526            # offset[i+1]-offset[i] is the size of the data location.
527            offsets = list(allLocations[0]) + [loc[1] for loc in allLocations[1:]]
528            # Image data offset must be less than or equal to the minimum of locations.
529            # This offset may change the value for round tripping but is safer and
530            # allows imageDataOffset to not be required to be in the XML version.
531            self.imageDataOffset = min(offsets)
532            offsetArray = [offset - self.imageDataOffset for offset in offsets]
533
534            dataList = [EblcIndexSubTable.compile(self, ttFont)]
535            dataList += [
536                struct.pack(dataFormat, offsetValue) for offsetValue in offsetArray
537            ]
538            # Take care of any padding issues. Only occurs in format 3.
539            if offsetDataSize * len(offsetArray) % 4 != 0:
540                dataList.append(struct.pack(dataFormat, 0))
541            return bytesjoin(dataList)
542
543    return OffsetArrayIndexSubTableMixin
544
545
546# A Mixin for functionality shared between the different kinds
547# of fixed sized data handling. Both kinds have big metrics so
548# that kind of special processing is also handled in this mixin.
549class FixedSizeIndexSubTableMixin(object):
550    def writeMetrics(self, writer, ttFont):
551        writer.simpletag("imageSize", value=self.imageSize)
552        writer.newline()
553        self.metrics.toXML(writer, ttFont)
554
555    def readMetrics(self, name, attrs, content, ttFont):
556        for element in content:
557            if not isinstance(element, tuple):
558                continue
559            name, attrs, content = element
560            if name == "imageSize":
561                self.imageSize = safeEval(attrs["value"])
562            elif name == BigGlyphMetrics.__name__:
563                self.metrics = BigGlyphMetrics()
564                self.metrics.fromXML(name, attrs, content, ttFont)
565            elif name == SmallGlyphMetrics.__name__:
566                log.warning(
567                    "SmallGlyphMetrics being ignored in format %d.", self.indexFormat
568                )
569
570    def padBitmapData(self, data):
571        # Make sure that the data isn't bigger than the fixed size.
572        assert len(data) <= self.imageSize, (
573            "Data in indexSubTable format %d must be less than the fixed size."
574            % self.indexFormat
575        )
576        # Pad the data so that it matches the fixed size.
577        pad = (self.imageSize - len(data)) * b"\0"
578        return data + pad
579
580
581class eblc_index_sub_table_1(
582    _createOffsetArrayIndexSubTableMixin("L"), EblcIndexSubTable
583):
584    pass
585
586
587class eblc_index_sub_table_2(FixedSizeIndexSubTableMixin, EblcIndexSubTable):
588    def decompile(self):
589        (self.imageSize,) = struct.unpack(">L", self.data[:4])
590        self.metrics = BigGlyphMetrics()
591        sstruct.unpack2(bigGlyphMetricsFormat, self.data[4:], self.metrics)
592        glyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex + 1))
593        offsets = [
594            self.imageSize * i + self.imageDataOffset for i in range(len(glyphIds) + 1)
595        ]
596        self.locations = list(zip(offsets, offsets[1:]))
597        self.names = list(map(self.ttFont.getGlyphName, glyphIds))
598        del self.data, self.ttFont
599
600    def compile(self, ttFont):
601        glyphIds = list(map(ttFont.getGlyphID, self.names))
602        # Make sure all the ids are consecutive. This is required by Format 2.
603        assert glyphIds == list(
604            range(self.firstGlyphIndex, self.lastGlyphIndex + 1)
605        ), "Format 2 ids must be consecutive."
606        self.imageDataOffset = min(next(iter(zip(*self.locations))))
607
608        dataList = [EblcIndexSubTable.compile(self, ttFont)]
609        dataList.append(struct.pack(">L", self.imageSize))
610        dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics))
611        return bytesjoin(dataList)
612
613
614class eblc_index_sub_table_3(
615    _createOffsetArrayIndexSubTableMixin("H"), EblcIndexSubTable
616):
617    pass
618
619
620class eblc_index_sub_table_4(EblcIndexSubTable):
621    def decompile(self):
622        (numGlyphs,) = struct.unpack(">L", self.data[:4])
623        data = self.data[4:]
624        indexingOffsets = [
625            glyphIndex * codeOffsetPairSize for glyphIndex in range(numGlyphs + 2)
626        ]
627        indexingLocations = zip(indexingOffsets, indexingOffsets[1:])
628        glyphArray = [
629            struct.unpack(codeOffsetPairFormat, data[slice(*loc)])
630            for loc in indexingLocations
631        ]
632        glyphIds, offsets = list(map(list, zip(*glyphArray)))
633        # There are one too many glyph ids. Get rid of the last one.
634        glyphIds.pop()
635
636        offsets = [offset + self.imageDataOffset for offset in offsets]
637        self.locations = list(zip(offsets, offsets[1:]))
638        self.names = list(map(self.ttFont.getGlyphName, glyphIds))
639        del self.data, self.ttFont
640
641    def compile(self, ttFont):
642        # First make sure that all the data lines up properly. Format 4
643        # must have all its data lined up consecutively. If not this will fail.
644        for curLoc, nxtLoc in zip(self.locations, self.locations[1:]):
645            assert (
646                curLoc[1] == nxtLoc[0]
647            ), "Data must be consecutive in indexSubTable format 4"
648
649        offsets = list(self.locations[0]) + [loc[1] for loc in self.locations[1:]]
650        # Image data offset must be less than or equal to the minimum of locations.
651        # Resetting this offset may change the value for round tripping but is safer
652        # and allows imageDataOffset to not be required to be in the XML version.
653        self.imageDataOffset = min(offsets)
654        offsets = [offset - self.imageDataOffset for offset in offsets]
655        glyphIds = list(map(ttFont.getGlyphID, self.names))
656        # Create an iterator over the ids plus a padding value.
657        idsPlusPad = list(itertools.chain(glyphIds, [0]))
658
659        dataList = [EblcIndexSubTable.compile(self, ttFont)]
660        dataList.append(struct.pack(">L", len(glyphIds)))
661        tmp = [
662            struct.pack(codeOffsetPairFormat, *cop) for cop in zip(idsPlusPad, offsets)
663        ]
664        dataList += tmp
665        data = bytesjoin(dataList)
666        return data
667
668
669class eblc_index_sub_table_5(FixedSizeIndexSubTableMixin, EblcIndexSubTable):
670    def decompile(self):
671        self.origDataLen = 0
672        (self.imageSize,) = struct.unpack(">L", self.data[:4])
673        data = self.data[4:]
674        self.metrics, data = sstruct.unpack2(
675            bigGlyphMetricsFormat, data, BigGlyphMetrics()
676        )
677        (numGlyphs,) = struct.unpack(">L", data[:4])
678        data = data[4:]
679        glyphIds = [
680            struct.unpack(">H", data[2 * i : 2 * (i + 1)])[0] for i in range(numGlyphs)
681        ]
682
683        offsets = [
684            self.imageSize * i + self.imageDataOffset for i in range(len(glyphIds) + 1)
685        ]
686        self.locations = list(zip(offsets, offsets[1:]))
687        self.names = list(map(self.ttFont.getGlyphName, glyphIds))
688        del self.data, self.ttFont
689
690    def compile(self, ttFont):
691        self.imageDataOffset = min(next(iter(zip(*self.locations))))
692        dataList = [EblcIndexSubTable.compile(self, ttFont)]
693        dataList.append(struct.pack(">L", self.imageSize))
694        dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics))
695        glyphIds = list(map(ttFont.getGlyphID, self.names))
696        dataList.append(struct.pack(">L", len(glyphIds)))
697        dataList += [struct.pack(">H", curId) for curId in glyphIds]
698        if len(glyphIds) % 2 == 1:
699            dataList.append(struct.pack(">H", 0))
700        return bytesjoin(dataList)
701
702
703# Dictionary of indexFormat to the class representing that format.
704eblc_sub_table_classes = {
705    1: eblc_index_sub_table_1,
706    2: eblc_index_sub_table_2,
707    3: eblc_index_sub_table_3,
708    4: eblc_index_sub_table_4,
709    5: eblc_index_sub_table_5,
710}
711