• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc import sstruct
2from fontTools.misc.textTools import (
3    bytechr,
4    byteord,
5    bytesjoin,
6    strjoin,
7    safeEval,
8    readHex,
9    hexStr,
10    deHexStr,
11)
12from .BitmapGlyphMetrics import (
13    BigGlyphMetrics,
14    bigGlyphMetricsFormat,
15    SmallGlyphMetrics,
16    smallGlyphMetricsFormat,
17)
18from . import DefaultTable
19import itertools
20import os
21import struct
22import logging
23
24
25log = logging.getLogger(__name__)
26
27ebdtTableVersionFormat = """
28	> # big endian
29	version: 16.16F
30"""
31
32ebdtComponentFormat = """
33	> # big endian
34	glyphCode: H
35	xOffset:   b
36	yOffset:   b
37"""
38
39
40class table_E_B_D_T_(DefaultTable.DefaultTable):
41    # Keep a reference to the name of the data locator table.
42    locatorName = "EBLC"
43
44    # This method can be overridden in subclasses to support new formats
45    # without changing the other implementation. Also can be used as a
46    # convenience method for coverting a font file to an alternative format.
47    def getImageFormatClass(self, imageFormat):
48        return ebdt_bitmap_classes[imageFormat]
49
50    def decompile(self, data, ttFont):
51        # Get the version but don't advance the slice.
52        # Most of the lookup for this table is done relative
53        # to the begining so slice by the offsets provided
54        # in the EBLC table.
55        sstruct.unpack2(ebdtTableVersionFormat, data, self)
56
57        # Keep a dict of glyphs that have been seen so they aren't remade.
58        # This dict maps intervals of data to the BitmapGlyph.
59        glyphDict = {}
60
61        # Pull out the EBLC table and loop through glyphs.
62        # A strike is a concept that spans both tables.
63        # The actual bitmap data is stored in the EBDT.
64        locator = ttFont[self.__class__.locatorName]
65        self.strikeData = []
66        for curStrike in locator.strikes:
67            bitmapGlyphDict = {}
68            self.strikeData.append(bitmapGlyphDict)
69            for indexSubTable in curStrike.indexSubTables:
70                dataIter = zip(indexSubTable.names, indexSubTable.locations)
71                for curName, curLoc in dataIter:
72                    # Don't create duplicate data entries for the same glyphs.
73                    # Instead just use the structures that already exist if they exist.
74                    if curLoc in glyphDict:
75                        curGlyph = glyphDict[curLoc]
76                    else:
77                        curGlyphData = data[slice(*curLoc)]
78                        imageFormatClass = self.getImageFormatClass(
79                            indexSubTable.imageFormat
80                        )
81                        curGlyph = imageFormatClass(curGlyphData, ttFont)
82                        glyphDict[curLoc] = curGlyph
83                    bitmapGlyphDict[curName] = curGlyph
84
85    def compile(self, ttFont):
86        dataList = []
87        dataList.append(sstruct.pack(ebdtTableVersionFormat, self))
88        dataSize = len(dataList[0])
89
90        # Keep a dict of glyphs that have been seen so they aren't remade.
91        # This dict maps the id of the BitmapGlyph to the interval
92        # in the data.
93        glyphDict = {}
94
95        # Go through the bitmap glyph data. Just in case the data for a glyph
96        # changed the size metrics should be recalculated. There are a variety
97        # of formats and they get stored in the EBLC table. That is why
98        # recalculation is defered to the EblcIndexSubTable class and just
99        # pass what is known about bitmap glyphs from this particular table.
100        locator = ttFont[self.__class__.locatorName]
101        for curStrike, curGlyphDict in zip(locator.strikes, self.strikeData):
102            for curIndexSubTable in curStrike.indexSubTables:
103                dataLocations = []
104                for curName in curIndexSubTable.names:
105                    # Handle the data placement based on seeing the glyph or not.
106                    # Just save a reference to the location if the glyph has already
107                    # been saved in compile. This code assumes that glyphs will only
108                    # be referenced multiple times from indexFormat5. By luck the
109                    # code may still work when referencing poorly ordered fonts with
110                    # duplicate references. If there is a font that is unlucky the
111                    # respective compile methods for the indexSubTables will fail
112                    # their assertions. All fonts seem to follow this assumption.
113                    # More complicated packing may be needed if a counter-font exists.
114                    glyph = curGlyphDict[curName]
115                    objectId = id(glyph)
116                    if objectId not in glyphDict:
117                        data = glyph.compile(ttFont)
118                        data = curIndexSubTable.padBitmapData(data)
119                        startByte = dataSize
120                        dataSize += len(data)
121                        endByte = dataSize
122                        dataList.append(data)
123                        dataLoc = (startByte, endByte)
124                        glyphDict[objectId] = dataLoc
125                    else:
126                        dataLoc = glyphDict[objectId]
127                    dataLocations.append(dataLoc)
128                # Just use the new data locations in the indexSubTable.
129                # The respective compile implementations will take care
130                # of any of the problems in the convertion that may arise.
131                curIndexSubTable.locations = dataLocations
132
133        return bytesjoin(dataList)
134
135    def toXML(self, writer, ttFont):
136        # When exporting to XML if one of the data export formats
137        # requires metrics then those metrics may be in the locator.
138        # In this case populate the bitmaps with "export metrics".
139        if ttFont.bitmapGlyphDataFormat in ("row", "bitwise"):
140            locator = ttFont[self.__class__.locatorName]
141            for curStrike, curGlyphDict in zip(locator.strikes, self.strikeData):
142                for curIndexSubTable in curStrike.indexSubTables:
143                    for curName in curIndexSubTable.names:
144                        glyph = curGlyphDict[curName]
145                        # I'm not sure which metrics have priority here.
146                        # For now if both metrics exist go with glyph metrics.
147                        if hasattr(glyph, "metrics"):
148                            glyph.exportMetrics = glyph.metrics
149                        else:
150                            glyph.exportMetrics = curIndexSubTable.metrics
151                        glyph.exportBitDepth = curStrike.bitmapSizeTable.bitDepth
152
153        writer.simpletag("header", [("version", self.version)])
154        writer.newline()
155        locator = ttFont[self.__class__.locatorName]
156        for strikeIndex, bitmapGlyphDict in enumerate(self.strikeData):
157            writer.begintag("strikedata", [("index", strikeIndex)])
158            writer.newline()
159            for curName, curBitmap in bitmapGlyphDict.items():
160                curBitmap.toXML(strikeIndex, curName, writer, ttFont)
161            writer.endtag("strikedata")
162            writer.newline()
163
164    def fromXML(self, name, attrs, content, ttFont):
165        if name == "header":
166            self.version = safeEval(attrs["version"])
167        elif name == "strikedata":
168            if not hasattr(self, "strikeData"):
169                self.strikeData = []
170            strikeIndex = safeEval(attrs["index"])
171
172            bitmapGlyphDict = {}
173            for element in content:
174                if not isinstance(element, tuple):
175                    continue
176                name, attrs, content = element
177                if name[4:].startswith(_bitmapGlyphSubclassPrefix[4:]):
178                    imageFormat = safeEval(name[len(_bitmapGlyphSubclassPrefix) :])
179                    glyphName = attrs["name"]
180                    imageFormatClass = self.getImageFormatClass(imageFormat)
181                    curGlyph = imageFormatClass(None, None)
182                    curGlyph.fromXML(name, attrs, content, ttFont)
183                    assert glyphName not in bitmapGlyphDict, (
184                        "Duplicate glyphs with the same name '%s' in the same strike."
185                        % glyphName
186                    )
187                    bitmapGlyphDict[glyphName] = curGlyph
188                else:
189                    log.warning("%s being ignored by %s", name, self.__class__.__name__)
190
191            # Grow the strike data array to the appropriate size. The XML
192            # format allows the strike index value to be out of order.
193            if strikeIndex >= len(self.strikeData):
194                self.strikeData += [None] * (strikeIndex + 1 - len(self.strikeData))
195            assert (
196                self.strikeData[strikeIndex] is None
197            ), "Duplicate strike EBDT indices."
198            self.strikeData[strikeIndex] = bitmapGlyphDict
199
200
201class EbdtComponent(object):
202    def toXML(self, writer, ttFont):
203        writer.begintag("ebdtComponent", [("name", self.name)])
204        writer.newline()
205        for componentName in sstruct.getformat(ebdtComponentFormat)[1][1:]:
206            writer.simpletag(componentName, value=getattr(self, componentName))
207            writer.newline()
208        writer.endtag("ebdtComponent")
209        writer.newline()
210
211    def fromXML(self, name, attrs, content, ttFont):
212        self.name = attrs["name"]
213        componentNames = set(sstruct.getformat(ebdtComponentFormat)[1][1:])
214        for element in content:
215            if not isinstance(element, tuple):
216                continue
217            name, attrs, content = element
218            if name in componentNames:
219                vars(self)[name] = safeEval(attrs["value"])
220            else:
221                log.warning("unknown name '%s' being ignored by EbdtComponent.", name)
222
223
224# Helper functions for dealing with binary.
225
226
227def _data2binary(data, numBits):
228    binaryList = []
229    for curByte in data:
230        value = byteord(curByte)
231        numBitsCut = min(8, numBits)
232        for i in range(numBitsCut):
233            if value & 0x1:
234                binaryList.append("1")
235            else:
236                binaryList.append("0")
237            value = value >> 1
238        numBits -= numBitsCut
239    return strjoin(binaryList)
240
241
242def _binary2data(binary):
243    byteList = []
244    for bitLoc in range(0, len(binary), 8):
245        byteString = binary[bitLoc : bitLoc + 8]
246        curByte = 0
247        for curBit in reversed(byteString):
248            curByte = curByte << 1
249            if curBit == "1":
250                curByte |= 1
251        byteList.append(bytechr(curByte))
252    return bytesjoin(byteList)
253
254
255def _memoize(f):
256    class memodict(dict):
257        def __missing__(self, key):
258            ret = f(key)
259            if isinstance(key, int) or len(key) == 1:
260                self[key] = ret
261            return ret
262
263    return memodict().__getitem__
264
265
266# 00100111 -> 11100100 per byte, not to be confused with little/big endian.
267# Bitmap data per byte is in the order that binary is written on the page
268# with the least significant bit as far right as possible. This is the
269# opposite of what makes sense algorithmically and hence this function.
270@_memoize
271def _reverseBytes(data):
272    r"""
273    >>> bin(ord(_reverseBytes(0b00100111)))
274    '0b11100100'
275    >>> _reverseBytes(b'\x00\xf0')
276    b'\x00\x0f'
277    """
278    if isinstance(data, bytes) and len(data) != 1:
279        return bytesjoin(map(_reverseBytes, data))
280    byte = byteord(data)
281    result = 0
282    for i in range(8):
283        result = result << 1
284        result |= byte & 1
285        byte = byte >> 1
286    return bytechr(result)
287
288
289# This section of code is for reading and writing image data to/from XML.
290
291
292def _writeRawImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont):
293    writer.begintag("rawimagedata")
294    writer.newline()
295    writer.dumphex(bitmapObject.imageData)
296    writer.endtag("rawimagedata")
297    writer.newline()
298
299
300def _readRawImageData(bitmapObject, name, attrs, content, ttFont):
301    bitmapObject.imageData = readHex(content)
302
303
304def _writeRowImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont):
305    metrics = bitmapObject.exportMetrics
306    del bitmapObject.exportMetrics
307    bitDepth = bitmapObject.exportBitDepth
308    del bitmapObject.exportBitDepth
309
310    writer.begintag(
311        "rowimagedata", bitDepth=bitDepth, width=metrics.width, height=metrics.height
312    )
313    writer.newline()
314    for curRow in range(metrics.height):
315        rowData = bitmapObject.getRow(curRow, bitDepth=bitDepth, metrics=metrics)
316        writer.simpletag("row", value=hexStr(rowData))
317        writer.newline()
318    writer.endtag("rowimagedata")
319    writer.newline()
320
321
322def _readRowImageData(bitmapObject, name, attrs, content, ttFont):
323    bitDepth = safeEval(attrs["bitDepth"])
324    metrics = SmallGlyphMetrics()
325    metrics.width = safeEval(attrs["width"])
326    metrics.height = safeEval(attrs["height"])
327
328    dataRows = []
329    for element in content:
330        if not isinstance(element, tuple):
331            continue
332        name, attr, content = element
333        # Chop off 'imagedata' from the tag to get just the option.
334        if name == "row":
335            dataRows.append(deHexStr(attr["value"]))
336    bitmapObject.setRows(dataRows, bitDepth=bitDepth, metrics=metrics)
337
338
339def _writeBitwiseImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont):
340    metrics = bitmapObject.exportMetrics
341    del bitmapObject.exportMetrics
342    bitDepth = bitmapObject.exportBitDepth
343    del bitmapObject.exportBitDepth
344
345    # A dict for mapping binary to more readable/artistic ASCII characters.
346    binaryConv = {"0": ".", "1": "@"}
347
348    writer.begintag(
349        "bitwiseimagedata",
350        bitDepth=bitDepth,
351        width=metrics.width,
352        height=metrics.height,
353    )
354    writer.newline()
355    for curRow in range(metrics.height):
356        rowData = bitmapObject.getRow(
357            curRow, bitDepth=1, metrics=metrics, reverseBytes=True
358        )
359        rowData = _data2binary(rowData, metrics.width)
360        # Make the output a readable ASCII art form.
361        rowData = strjoin(map(binaryConv.get, rowData))
362        writer.simpletag("row", value=rowData)
363        writer.newline()
364    writer.endtag("bitwiseimagedata")
365    writer.newline()
366
367
368def _readBitwiseImageData(bitmapObject, name, attrs, content, ttFont):
369    bitDepth = safeEval(attrs["bitDepth"])
370    metrics = SmallGlyphMetrics()
371    metrics.width = safeEval(attrs["width"])
372    metrics.height = safeEval(attrs["height"])
373
374    # A dict for mapping from ASCII to binary. All characters are considered
375    # a '1' except space, period and '0' which maps to '0'.
376    binaryConv = {" ": "0", ".": "0", "0": "0"}
377
378    dataRows = []
379    for element in content:
380        if not isinstance(element, tuple):
381            continue
382        name, attr, content = element
383        if name == "row":
384            mapParams = zip(attr["value"], itertools.repeat("1"))
385            rowData = strjoin(itertools.starmap(binaryConv.get, mapParams))
386            dataRows.append(_binary2data(rowData))
387
388    bitmapObject.setRows(
389        dataRows, bitDepth=bitDepth, metrics=metrics, reverseBytes=True
390    )
391
392
393def _writeExtFileImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont):
394    try:
395        folder = os.path.dirname(writer.file.name)
396    except AttributeError:
397        # fall back to current directory if output file's directory isn't found
398        folder = "."
399    folder = os.path.join(folder, "bitmaps")
400    filename = glyphName + bitmapObject.fileExtension
401    if not os.path.isdir(folder):
402        os.makedirs(folder)
403    folder = os.path.join(folder, "strike%d" % strikeIndex)
404    if not os.path.isdir(folder):
405        os.makedirs(folder)
406
407    fullPath = os.path.join(folder, filename)
408    writer.simpletag("extfileimagedata", value=fullPath)
409    writer.newline()
410
411    with open(fullPath, "wb") as file:
412        file.write(bitmapObject.imageData)
413
414
415def _readExtFileImageData(bitmapObject, name, attrs, content, ttFont):
416    fullPath = attrs["value"]
417    with open(fullPath, "rb") as file:
418        bitmapObject.imageData = file.read()
419
420
421# End of XML writing code.
422
423# Important information about the naming scheme. Used for identifying formats
424# in XML.
425_bitmapGlyphSubclassPrefix = "ebdt_bitmap_format_"
426
427
428class BitmapGlyph(object):
429    # For the external file format. This can be changed in subclasses. This way
430    # when the extfile option is turned on files have the form: glyphName.ext
431    # The default is just a flat binary file with no meaning.
432    fileExtension = ".bin"
433
434    # Keep track of reading and writing of various forms.
435    xmlDataFunctions = {
436        "raw": (_writeRawImageData, _readRawImageData),
437        "row": (_writeRowImageData, _readRowImageData),
438        "bitwise": (_writeBitwiseImageData, _readBitwiseImageData),
439        "extfile": (_writeExtFileImageData, _readExtFileImageData),
440    }
441
442    def __init__(self, data, ttFont):
443        self.data = data
444        self.ttFont = ttFont
445        # TODO Currently non-lazy decompilation is untested here...
446        # if not ttFont.lazy:
447        # 	self.decompile()
448        # 	del self.data
449
450    def __getattr__(self, attr):
451        # Allow lazy decompile.
452        if attr[:2] == "__":
453            raise AttributeError(attr)
454        if attr == "data":
455            raise AttributeError(attr)
456        self.decompile()
457        del self.data
458        return getattr(self, attr)
459
460    def ensureDecompiled(self, recurse=False):
461        if hasattr(self, "data"):
462            self.decompile()
463            del self.data
464
465    # Not a fan of this but it is needed for safer safety checking.
466    def getFormat(self):
467        return safeEval(self.__class__.__name__[len(_bitmapGlyphSubclassPrefix) :])
468
469    def toXML(self, strikeIndex, glyphName, writer, ttFont):
470        writer.begintag(self.__class__.__name__, [("name", glyphName)])
471        writer.newline()
472
473        self.writeMetrics(writer, ttFont)
474        # Use the internal write method to write using the correct output format.
475        self.writeData(strikeIndex, glyphName, writer, ttFont)
476
477        writer.endtag(self.__class__.__name__)
478        writer.newline()
479
480    def fromXML(self, name, attrs, content, ttFont):
481        self.readMetrics(name, attrs, content, ttFont)
482        for element in content:
483            if not isinstance(element, tuple):
484                continue
485            name, attr, content = element
486            if not name.endswith("imagedata"):
487                continue
488            # Chop off 'imagedata' from the tag to get just the option.
489            option = name[: -len("imagedata")]
490            assert option in self.__class__.xmlDataFunctions
491            self.readData(name, attr, content, ttFont)
492
493    # Some of the glyphs have the metrics. This allows for metrics to be
494    # added if the glyph format has them. Default behavior is to do nothing.
495    def writeMetrics(self, writer, ttFont):
496        pass
497
498    # The opposite of write metrics.
499    def readMetrics(self, name, attrs, content, ttFont):
500        pass
501
502    def writeData(self, strikeIndex, glyphName, writer, ttFont):
503        try:
504            writeFunc, readFunc = self.__class__.xmlDataFunctions[
505                ttFont.bitmapGlyphDataFormat
506            ]
507        except KeyError:
508            writeFunc = _writeRawImageData
509        writeFunc(strikeIndex, glyphName, self, writer, ttFont)
510
511    def readData(self, name, attrs, content, ttFont):
512        # Chop off 'imagedata' from the tag to get just the option.
513        option = name[: -len("imagedata")]
514        writeFunc, readFunc = self.__class__.xmlDataFunctions[option]
515        readFunc(self, name, attrs, content, ttFont)
516
517
518# A closure for creating a mixin for the two types of metrics handling.
519# Most of the code is very similar so its easier to deal with here.
520# Everything works just by passing the class that the mixin is for.
521def _createBitmapPlusMetricsMixin(metricsClass):
522    # Both metrics names are listed here to make meaningful error messages.
523    metricStrings = [BigGlyphMetrics.__name__, SmallGlyphMetrics.__name__]
524    curMetricsName = metricsClass.__name__
525    # Find which metrics this is for and determine the opposite name.
526    metricsId = metricStrings.index(curMetricsName)
527    oppositeMetricsName = metricStrings[1 - metricsId]
528
529    class BitmapPlusMetricsMixin(object):
530        def writeMetrics(self, writer, ttFont):
531            self.metrics.toXML(writer, ttFont)
532
533        def readMetrics(self, name, attrs, content, ttFont):
534            for element in content:
535                if not isinstance(element, tuple):
536                    continue
537                name, attrs, content = element
538                if name == curMetricsName:
539                    self.metrics = metricsClass()
540                    self.metrics.fromXML(name, attrs, content, ttFont)
541                elif name == oppositeMetricsName:
542                    log.warning(
543                        "Warning: %s being ignored in format %d.",
544                        oppositeMetricsName,
545                        self.getFormat(),
546                    )
547
548    return BitmapPlusMetricsMixin
549
550
551# Since there are only two types of mixin's just create them here.
552BitmapPlusBigMetricsMixin = _createBitmapPlusMetricsMixin(BigGlyphMetrics)
553BitmapPlusSmallMetricsMixin = _createBitmapPlusMetricsMixin(SmallGlyphMetrics)
554
555
556# Data that is bit aligned can be tricky to deal with. These classes implement
557# helper functionality for dealing with the data and getting a particular row
558# of bitwise data. Also helps implement fancy data export/import in XML.
559class BitAlignedBitmapMixin(object):
560    def _getBitRange(self, row, bitDepth, metrics):
561        rowBits = bitDepth * metrics.width
562        bitOffset = row * rowBits
563        return (bitOffset, bitOffset + rowBits)
564
565    def getRow(self, row, bitDepth=1, metrics=None, reverseBytes=False):
566        if metrics is None:
567            metrics = self.metrics
568        assert 0 <= row and row < metrics.height, "Illegal row access in bitmap"
569
570        # Loop through each byte. This can cover two bytes in the original data or
571        # a single byte if things happen to be aligned. The very last entry might
572        # not be aligned so take care to trim the binary data to size and pad with
573        # zeros in the row data. Bit aligned data is somewhat tricky.
574        #
575        # Example of data cut. Data cut represented in x's.
576        # '|' represents byte boundary.
577        # data = ...0XX|XXXXXX00|000... => XXXXXXXX
578        # 		or
579        # data = ...0XX|XXXX0000|000... => XXXXXX00
580        #   or
581        # data = ...000|XXXXXXXX|000... => XXXXXXXX
582        #   or
583        # data = ...000|00XXXX00|000... => XXXX0000
584        #
585        dataList = []
586        bitRange = self._getBitRange(row, bitDepth, metrics)
587        stepRange = bitRange + (8,)
588        for curBit in range(*stepRange):
589            endBit = min(curBit + 8, bitRange[1])
590            numBits = endBit - curBit
591            cutPoint = curBit % 8
592            firstByteLoc = curBit // 8
593            secondByteLoc = endBit // 8
594            if firstByteLoc < secondByteLoc:
595                numBitsCut = 8 - cutPoint
596            else:
597                numBitsCut = endBit - curBit
598            curByte = _reverseBytes(self.imageData[firstByteLoc])
599            firstHalf = byteord(curByte) >> cutPoint
600            firstHalf = ((1 << numBitsCut) - 1) & firstHalf
601            newByte = firstHalf
602            if firstByteLoc < secondByteLoc and secondByteLoc < len(self.imageData):
603                curByte = _reverseBytes(self.imageData[secondByteLoc])
604                secondHalf = byteord(curByte) << numBitsCut
605                newByte = (firstHalf | secondHalf) & ((1 << numBits) - 1)
606            dataList.append(bytechr(newByte))
607
608        # The way the data is kept is opposite the algorithm used.
609        data = bytesjoin(dataList)
610        if not reverseBytes:
611            data = _reverseBytes(data)
612        return data
613
614    def setRows(self, dataRows, bitDepth=1, metrics=None, reverseBytes=False):
615        if metrics is None:
616            metrics = self.metrics
617        if not reverseBytes:
618            dataRows = list(map(_reverseBytes, dataRows))
619
620        # Keep track of a list of ordinal values as they are easier to modify
621        # than a list of strings. Map to actual strings later.
622        numBytes = (self._getBitRange(len(dataRows), bitDepth, metrics)[0] + 7) // 8
623        ordDataList = [0] * numBytes
624        for row, data in enumerate(dataRows):
625            bitRange = self._getBitRange(row, bitDepth, metrics)
626            stepRange = bitRange + (8,)
627            for curBit, curByte in zip(range(*stepRange), data):
628                endBit = min(curBit + 8, bitRange[1])
629                cutPoint = curBit % 8
630                firstByteLoc = curBit // 8
631                secondByteLoc = endBit // 8
632                if firstByteLoc < secondByteLoc:
633                    numBitsCut = 8 - cutPoint
634                else:
635                    numBitsCut = endBit - curBit
636                curByte = byteord(curByte)
637                firstByte = curByte & ((1 << numBitsCut) - 1)
638                ordDataList[firstByteLoc] |= firstByte << cutPoint
639                if firstByteLoc < secondByteLoc and secondByteLoc < numBytes:
640                    secondByte = (curByte >> numBitsCut) & ((1 << 8 - numBitsCut) - 1)
641                    ordDataList[secondByteLoc] |= secondByte
642
643        # Save the image data with the bits going the correct way.
644        self.imageData = _reverseBytes(bytesjoin(map(bytechr, ordDataList)))
645
646
647class ByteAlignedBitmapMixin(object):
648    def _getByteRange(self, row, bitDepth, metrics):
649        rowBytes = (bitDepth * metrics.width + 7) // 8
650        byteOffset = row * rowBytes
651        return (byteOffset, byteOffset + rowBytes)
652
653    def getRow(self, row, bitDepth=1, metrics=None, reverseBytes=False):
654        if metrics is None:
655            metrics = self.metrics
656        assert 0 <= row and row < metrics.height, "Illegal row access in bitmap"
657        byteRange = self._getByteRange(row, bitDepth, metrics)
658        data = self.imageData[slice(*byteRange)]
659        if reverseBytes:
660            data = _reverseBytes(data)
661        return data
662
663    def setRows(self, dataRows, bitDepth=1, metrics=None, reverseBytes=False):
664        if metrics is None:
665            metrics = self.metrics
666        if reverseBytes:
667            dataRows = map(_reverseBytes, dataRows)
668        self.imageData = bytesjoin(dataRows)
669
670
671class ebdt_bitmap_format_1(
672    ByteAlignedBitmapMixin, BitmapPlusSmallMetricsMixin, BitmapGlyph
673):
674    def decompile(self):
675        self.metrics = SmallGlyphMetrics()
676        dummy, data = sstruct.unpack2(smallGlyphMetricsFormat, self.data, self.metrics)
677        self.imageData = data
678
679    def compile(self, ttFont):
680        data = sstruct.pack(smallGlyphMetricsFormat, self.metrics)
681        return data + self.imageData
682
683
684class ebdt_bitmap_format_2(
685    BitAlignedBitmapMixin, BitmapPlusSmallMetricsMixin, BitmapGlyph
686):
687    def decompile(self):
688        self.metrics = SmallGlyphMetrics()
689        dummy, data = sstruct.unpack2(smallGlyphMetricsFormat, self.data, self.metrics)
690        self.imageData = data
691
692    def compile(self, ttFont):
693        data = sstruct.pack(smallGlyphMetricsFormat, self.metrics)
694        return data + self.imageData
695
696
697class ebdt_bitmap_format_5(BitAlignedBitmapMixin, BitmapGlyph):
698    def decompile(self):
699        self.imageData = self.data
700
701    def compile(self, ttFont):
702        return self.imageData
703
704
705class ebdt_bitmap_format_6(
706    ByteAlignedBitmapMixin, BitmapPlusBigMetricsMixin, BitmapGlyph
707):
708    def decompile(self):
709        self.metrics = BigGlyphMetrics()
710        dummy, data = sstruct.unpack2(bigGlyphMetricsFormat, self.data, self.metrics)
711        self.imageData = data
712
713    def compile(self, ttFont):
714        data = sstruct.pack(bigGlyphMetricsFormat, self.metrics)
715        return data + self.imageData
716
717
718class ebdt_bitmap_format_7(
719    BitAlignedBitmapMixin, BitmapPlusBigMetricsMixin, BitmapGlyph
720):
721    def decompile(self):
722        self.metrics = BigGlyphMetrics()
723        dummy, data = sstruct.unpack2(bigGlyphMetricsFormat, self.data, self.metrics)
724        self.imageData = data
725
726    def compile(self, ttFont):
727        data = sstruct.pack(bigGlyphMetricsFormat, self.metrics)
728        return data + self.imageData
729
730
731class ComponentBitmapGlyph(BitmapGlyph):
732    def toXML(self, strikeIndex, glyphName, writer, ttFont):
733        writer.begintag(self.__class__.__name__, [("name", glyphName)])
734        writer.newline()
735
736        self.writeMetrics(writer, ttFont)
737
738        writer.begintag("components")
739        writer.newline()
740        for curComponent in self.componentArray:
741            curComponent.toXML(writer, ttFont)
742        writer.endtag("components")
743        writer.newline()
744
745        writer.endtag(self.__class__.__name__)
746        writer.newline()
747
748    def fromXML(self, name, attrs, content, ttFont):
749        self.readMetrics(name, attrs, content, ttFont)
750        for element in content:
751            if not isinstance(element, tuple):
752                continue
753            name, attr, content = element
754            if name == "components":
755                self.componentArray = []
756                for compElement in content:
757                    if not isinstance(compElement, tuple):
758                        continue
759                    name, attrs, content = compElement
760                    if name == "ebdtComponent":
761                        curComponent = EbdtComponent()
762                        curComponent.fromXML(name, attrs, content, ttFont)
763                        self.componentArray.append(curComponent)
764                    else:
765                        log.warning("'%s' being ignored in component array.", name)
766
767
768class ebdt_bitmap_format_8(BitmapPlusSmallMetricsMixin, ComponentBitmapGlyph):
769    def decompile(self):
770        self.metrics = SmallGlyphMetrics()
771        dummy, data = sstruct.unpack2(smallGlyphMetricsFormat, self.data, self.metrics)
772        data = data[1:]
773
774        (numComponents,) = struct.unpack(">H", data[:2])
775        data = data[2:]
776        self.componentArray = []
777        for i in range(numComponents):
778            curComponent = EbdtComponent()
779            dummy, data = sstruct.unpack2(ebdtComponentFormat, data, curComponent)
780            curComponent.name = self.ttFont.getGlyphName(curComponent.glyphCode)
781            self.componentArray.append(curComponent)
782
783    def compile(self, ttFont):
784        dataList = []
785        dataList.append(sstruct.pack(smallGlyphMetricsFormat, self.metrics))
786        dataList.append(b"\0")
787        dataList.append(struct.pack(">H", len(self.componentArray)))
788        for curComponent in self.componentArray:
789            curComponent.glyphCode = ttFont.getGlyphID(curComponent.name)
790            dataList.append(sstruct.pack(ebdtComponentFormat, curComponent))
791        return bytesjoin(dataList)
792
793
794class ebdt_bitmap_format_9(BitmapPlusBigMetricsMixin, ComponentBitmapGlyph):
795    def decompile(self):
796        self.metrics = BigGlyphMetrics()
797        dummy, data = sstruct.unpack2(bigGlyphMetricsFormat, self.data, self.metrics)
798        (numComponents,) = struct.unpack(">H", data[:2])
799        data = data[2:]
800        self.componentArray = []
801        for i in range(numComponents):
802            curComponent = EbdtComponent()
803            dummy, data = sstruct.unpack2(ebdtComponentFormat, data, curComponent)
804            curComponent.name = self.ttFont.getGlyphName(curComponent.glyphCode)
805            self.componentArray.append(curComponent)
806
807    def compile(self, ttFont):
808        dataList = []
809        dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics))
810        dataList.append(struct.pack(">H", len(self.componentArray)))
811        for curComponent in self.componentArray:
812            curComponent.glyphCode = ttFont.getGlyphID(curComponent.name)
813            dataList.append(sstruct.pack(ebdtComponentFormat, curComponent))
814        return bytesjoin(dataList)
815
816
817# Dictionary of bitmap formats to the class representing that format
818# currently only the ones listed in this map are the ones supported.
819ebdt_bitmap_classes = {
820    1: ebdt_bitmap_format_1,
821    2: ebdt_bitmap_format_2,
822    5: ebdt_bitmap_format_5,
823    6: ebdt_bitmap_format_6,
824    7: ebdt_bitmap_format_7,
825    8: ebdt_bitmap_format_8,
826    9: ebdt_bitmap_format_9,
827}
828