836 lines
32 KiB
Python
836 lines
32 KiB
Python
from fontTools.misc import sstruct
|
|
from fontTools.misc.textTools import (
|
|
bytechr,
|
|
byteord,
|
|
bytesjoin,
|
|
strjoin,
|
|
safeEval,
|
|
readHex,
|
|
hexStr,
|
|
deHexStr,
|
|
)
|
|
from .BitmapGlyphMetrics import (
|
|
BigGlyphMetrics,
|
|
bigGlyphMetricsFormat,
|
|
SmallGlyphMetrics,
|
|
smallGlyphMetricsFormat,
|
|
)
|
|
from . import DefaultTable
|
|
import itertools
|
|
import os
|
|
import struct
|
|
import logging
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
ebdtTableVersionFormat = """
|
|
> # big endian
|
|
version: 16.16F
|
|
"""
|
|
|
|
ebdtComponentFormat = """
|
|
> # big endian
|
|
glyphCode: H
|
|
xOffset: b
|
|
yOffset: b
|
|
"""
|
|
|
|
|
|
class table_E_B_D_T_(DefaultTable.DefaultTable):
|
|
"""Embedded Bitmap Data table
|
|
|
|
The ``EBDT`` table contains monochrome or grayscale bitmap data for
|
|
glyphs. It must be used in concert with the ``EBLC`` table.
|
|
|
|
See also https://learn.microsoft.com/en-us/typography/opentype/spec/ebdt
|
|
"""
|
|
|
|
# Keep a reference to the name of the data locator table.
|
|
locatorName = "EBLC"
|
|
|
|
# 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 getImageFormatClass(self, imageFormat):
|
|
return ebdt_bitmap_classes[imageFormat]
|
|
|
|
def decompile(self, data, ttFont):
|
|
# Get the version but don't advance the slice.
|
|
# Most of the lookup for this table is done relative
|
|
# to the begining so slice by the offsets provided
|
|
# in the EBLC table.
|
|
sstruct.unpack2(ebdtTableVersionFormat, data, self)
|
|
|
|
# Keep a dict of glyphs that have been seen so they aren't remade.
|
|
# This dict maps intervals of data to the BitmapGlyph.
|
|
glyphDict = {}
|
|
|
|
# Pull out the EBLC table and loop through glyphs.
|
|
# A strike is a concept that spans both tables.
|
|
# The actual bitmap data is stored in the EBDT.
|
|
locator = ttFont[self.__class__.locatorName]
|
|
self.strikeData = []
|
|
for curStrike in locator.strikes:
|
|
bitmapGlyphDict = {}
|
|
self.strikeData.append(bitmapGlyphDict)
|
|
for indexSubTable in curStrike.indexSubTables:
|
|
dataIter = zip(indexSubTable.names, indexSubTable.locations)
|
|
for curName, curLoc in dataIter:
|
|
# Don't create duplicate data entries for the same glyphs.
|
|
# Instead just use the structures that already exist if they exist.
|
|
if curLoc in glyphDict:
|
|
curGlyph = glyphDict[curLoc]
|
|
else:
|
|
curGlyphData = data[slice(*curLoc)]
|
|
imageFormatClass = self.getImageFormatClass(
|
|
indexSubTable.imageFormat
|
|
)
|
|
curGlyph = imageFormatClass(curGlyphData, ttFont)
|
|
glyphDict[curLoc] = curGlyph
|
|
bitmapGlyphDict[curName] = curGlyph
|
|
|
|
def compile(self, ttFont):
|
|
dataList = []
|
|
dataList.append(sstruct.pack(ebdtTableVersionFormat, self))
|
|
dataSize = len(dataList[0])
|
|
|
|
# Keep a dict of glyphs that have been seen so they aren't remade.
|
|
# This dict maps the id of the BitmapGlyph to the interval
|
|
# in the data.
|
|
glyphDict = {}
|
|
|
|
# Go through the bitmap glyph data. Just in case the data for a glyph
|
|
# changed the size metrics should be recalculated. There are a variety
|
|
# of formats and they get stored in the EBLC table. That is why
|
|
# recalculation is defered to the EblcIndexSubTable class and just
|
|
# pass what is known about bitmap glyphs from this particular table.
|
|
locator = ttFont[self.__class__.locatorName]
|
|
for curStrike, curGlyphDict in zip(locator.strikes, self.strikeData):
|
|
for curIndexSubTable in curStrike.indexSubTables:
|
|
dataLocations = []
|
|
for curName in curIndexSubTable.names:
|
|
# Handle the data placement based on seeing the glyph or not.
|
|
# Just save a reference to the location if the glyph has already
|
|
# been saved in compile. This code assumes that glyphs will only
|
|
# be referenced multiple times from indexFormat5. By luck the
|
|
# code may still work when referencing poorly ordered fonts with
|
|
# duplicate references. If there is a font that is unlucky the
|
|
# respective compile methods for the indexSubTables will fail
|
|
# their assertions. All fonts seem to follow this assumption.
|
|
# More complicated packing may be needed if a counter-font exists.
|
|
glyph = curGlyphDict[curName]
|
|
objectId = id(glyph)
|
|
if objectId not in glyphDict:
|
|
data = glyph.compile(ttFont)
|
|
data = curIndexSubTable.padBitmapData(data)
|
|
startByte = dataSize
|
|
dataSize += len(data)
|
|
endByte = dataSize
|
|
dataList.append(data)
|
|
dataLoc = (startByte, endByte)
|
|
glyphDict[objectId] = dataLoc
|
|
else:
|
|
dataLoc = glyphDict[objectId]
|
|
dataLocations.append(dataLoc)
|
|
# Just use the new data locations in the indexSubTable.
|
|
# The respective compile implementations will take care
|
|
# of any of the problems in the convertion that may arise.
|
|
curIndexSubTable.locations = dataLocations
|
|
|
|
return bytesjoin(dataList)
|
|
|
|
def toXML(self, writer, ttFont):
|
|
# When exporting to XML if one of the data export formats
|
|
# requires metrics then those metrics may be in the locator.
|
|
# In this case populate the bitmaps with "export metrics".
|
|
if ttFont.bitmapGlyphDataFormat in ("row", "bitwise"):
|
|
locator = ttFont[self.__class__.locatorName]
|
|
for curStrike, curGlyphDict in zip(locator.strikes, self.strikeData):
|
|
for curIndexSubTable in curStrike.indexSubTables:
|
|
for curName in curIndexSubTable.names:
|
|
glyph = curGlyphDict[curName]
|
|
# I'm not sure which metrics have priority here.
|
|
# For now if both metrics exist go with glyph metrics.
|
|
if hasattr(glyph, "metrics"):
|
|
glyph.exportMetrics = glyph.metrics
|
|
else:
|
|
glyph.exportMetrics = curIndexSubTable.metrics
|
|
glyph.exportBitDepth = curStrike.bitmapSizeTable.bitDepth
|
|
|
|
writer.simpletag("header", [("version", self.version)])
|
|
writer.newline()
|
|
locator = ttFont[self.__class__.locatorName]
|
|
for strikeIndex, bitmapGlyphDict in enumerate(self.strikeData):
|
|
writer.begintag("strikedata", [("index", strikeIndex)])
|
|
writer.newline()
|
|
for curName, curBitmap in bitmapGlyphDict.items():
|
|
curBitmap.toXML(strikeIndex, curName, writer, ttFont)
|
|
writer.endtag("strikedata")
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, content, ttFont):
|
|
if name == "header":
|
|
self.version = safeEval(attrs["version"])
|
|
elif name == "strikedata":
|
|
if not hasattr(self, "strikeData"):
|
|
self.strikeData = []
|
|
strikeIndex = safeEval(attrs["index"])
|
|
|
|
bitmapGlyphDict = {}
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attrs, content = element
|
|
if name[4:].startswith(_bitmapGlyphSubclassPrefix[4:]):
|
|
imageFormat = safeEval(name[len(_bitmapGlyphSubclassPrefix) :])
|
|
glyphName = attrs["name"]
|
|
imageFormatClass = self.getImageFormatClass(imageFormat)
|
|
curGlyph = imageFormatClass(None, None)
|
|
curGlyph.fromXML(name, attrs, content, ttFont)
|
|
assert glyphName not in bitmapGlyphDict, (
|
|
"Duplicate glyphs with the same name '%s' in the same strike."
|
|
% glyphName
|
|
)
|
|
bitmapGlyphDict[glyphName] = curGlyph
|
|
else:
|
|
log.warning("%s being ignored by %s", name, self.__class__.__name__)
|
|
|
|
# Grow the strike data array to the appropriate size. The XML
|
|
# format allows the strike index value to be out of order.
|
|
if strikeIndex >= len(self.strikeData):
|
|
self.strikeData += [None] * (strikeIndex + 1 - len(self.strikeData))
|
|
assert (
|
|
self.strikeData[strikeIndex] is None
|
|
), "Duplicate strike EBDT indices."
|
|
self.strikeData[strikeIndex] = bitmapGlyphDict
|
|
|
|
|
|
class EbdtComponent(object):
|
|
def toXML(self, writer, ttFont):
|
|
writer.begintag("ebdtComponent", [("name", self.name)])
|
|
writer.newline()
|
|
for componentName in sstruct.getformat(ebdtComponentFormat)[1][1:]:
|
|
writer.simpletag(componentName, value=getattr(self, componentName))
|
|
writer.newline()
|
|
writer.endtag("ebdtComponent")
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, content, ttFont):
|
|
self.name = attrs["name"]
|
|
componentNames = set(sstruct.getformat(ebdtComponentFormat)[1][1:])
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attrs, content = element
|
|
if name in componentNames:
|
|
vars(self)[name] = safeEval(attrs["value"])
|
|
else:
|
|
log.warning("unknown name '%s' being ignored by EbdtComponent.", name)
|
|
|
|
|
|
# Helper functions for dealing with binary.
|
|
|
|
|
|
def _data2binary(data, numBits):
|
|
binaryList = []
|
|
for curByte in data:
|
|
value = byteord(curByte)
|
|
numBitsCut = min(8, numBits)
|
|
for i in range(numBitsCut):
|
|
if value & 0x1:
|
|
binaryList.append("1")
|
|
else:
|
|
binaryList.append("0")
|
|
value = value >> 1
|
|
numBits -= numBitsCut
|
|
return strjoin(binaryList)
|
|
|
|
|
|
def _binary2data(binary):
|
|
byteList = []
|
|
for bitLoc in range(0, len(binary), 8):
|
|
byteString = binary[bitLoc : bitLoc + 8]
|
|
curByte = 0
|
|
for curBit in reversed(byteString):
|
|
curByte = curByte << 1
|
|
if curBit == "1":
|
|
curByte |= 1
|
|
byteList.append(bytechr(curByte))
|
|
return bytesjoin(byteList)
|
|
|
|
|
|
def _memoize(f):
|
|
class memodict(dict):
|
|
def __missing__(self, key):
|
|
ret = f(key)
|
|
if isinstance(key, int) or len(key) == 1:
|
|
self[key] = ret
|
|
return ret
|
|
|
|
return memodict().__getitem__
|
|
|
|
|
|
# 00100111 -> 11100100 per byte, not to be confused with little/big endian.
|
|
# Bitmap data per byte is in the order that binary is written on the page
|
|
# with the least significant bit as far right as possible. This is the
|
|
# opposite of what makes sense algorithmically and hence this function.
|
|
@_memoize
|
|
def _reverseBytes(data):
|
|
r"""
|
|
>>> bin(ord(_reverseBytes(0b00100111)))
|
|
'0b11100100'
|
|
>>> _reverseBytes(b'\x00\xf0')
|
|
b'\x00\x0f'
|
|
"""
|
|
if isinstance(data, bytes) and len(data) != 1:
|
|
return bytesjoin(map(_reverseBytes, data))
|
|
byte = byteord(data)
|
|
result = 0
|
|
for i in range(8):
|
|
result = result << 1
|
|
result |= byte & 1
|
|
byte = byte >> 1
|
|
return bytechr(result)
|
|
|
|
|
|
# This section of code is for reading and writing image data to/from XML.
|
|
|
|
|
|
def _writeRawImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont):
|
|
writer.begintag("rawimagedata")
|
|
writer.newline()
|
|
writer.dumphex(bitmapObject.imageData)
|
|
writer.endtag("rawimagedata")
|
|
writer.newline()
|
|
|
|
|
|
def _readRawImageData(bitmapObject, name, attrs, content, ttFont):
|
|
bitmapObject.imageData = readHex(content)
|
|
|
|
|
|
def _writeRowImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont):
|
|
metrics = bitmapObject.exportMetrics
|
|
del bitmapObject.exportMetrics
|
|
bitDepth = bitmapObject.exportBitDepth
|
|
del bitmapObject.exportBitDepth
|
|
|
|
writer.begintag(
|
|
"rowimagedata", bitDepth=bitDepth, width=metrics.width, height=metrics.height
|
|
)
|
|
writer.newline()
|
|
for curRow in range(metrics.height):
|
|
rowData = bitmapObject.getRow(curRow, bitDepth=bitDepth, metrics=metrics)
|
|
writer.simpletag("row", value=hexStr(rowData))
|
|
writer.newline()
|
|
writer.endtag("rowimagedata")
|
|
writer.newline()
|
|
|
|
|
|
def _readRowImageData(bitmapObject, name, attrs, content, ttFont):
|
|
bitDepth = safeEval(attrs["bitDepth"])
|
|
metrics = SmallGlyphMetrics()
|
|
metrics.width = safeEval(attrs["width"])
|
|
metrics.height = safeEval(attrs["height"])
|
|
|
|
dataRows = []
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attr, content = element
|
|
# Chop off 'imagedata' from the tag to get just the option.
|
|
if name == "row":
|
|
dataRows.append(deHexStr(attr["value"]))
|
|
bitmapObject.setRows(dataRows, bitDepth=bitDepth, metrics=metrics)
|
|
|
|
|
|
def _writeBitwiseImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont):
|
|
metrics = bitmapObject.exportMetrics
|
|
del bitmapObject.exportMetrics
|
|
bitDepth = bitmapObject.exportBitDepth
|
|
del bitmapObject.exportBitDepth
|
|
|
|
# A dict for mapping binary to more readable/artistic ASCII characters.
|
|
binaryConv = {"0": ".", "1": "@"}
|
|
|
|
writer.begintag(
|
|
"bitwiseimagedata",
|
|
bitDepth=bitDepth,
|
|
width=metrics.width,
|
|
height=metrics.height,
|
|
)
|
|
writer.newline()
|
|
for curRow in range(metrics.height):
|
|
rowData = bitmapObject.getRow(
|
|
curRow, bitDepth=1, metrics=metrics, reverseBytes=True
|
|
)
|
|
rowData = _data2binary(rowData, metrics.width)
|
|
# Make the output a readable ASCII art form.
|
|
rowData = strjoin(map(binaryConv.get, rowData))
|
|
writer.simpletag("row", value=rowData)
|
|
writer.newline()
|
|
writer.endtag("bitwiseimagedata")
|
|
writer.newline()
|
|
|
|
|
|
def _readBitwiseImageData(bitmapObject, name, attrs, content, ttFont):
|
|
bitDepth = safeEval(attrs["bitDepth"])
|
|
metrics = SmallGlyphMetrics()
|
|
metrics.width = safeEval(attrs["width"])
|
|
metrics.height = safeEval(attrs["height"])
|
|
|
|
# A dict for mapping from ASCII to binary. All characters are considered
|
|
# a '1' except space, period and '0' which maps to '0'.
|
|
binaryConv = {" ": "0", ".": "0", "0": "0"}
|
|
|
|
dataRows = []
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attr, content = element
|
|
if name == "row":
|
|
mapParams = zip(attr["value"], itertools.repeat("1"))
|
|
rowData = strjoin(itertools.starmap(binaryConv.get, mapParams))
|
|
dataRows.append(_binary2data(rowData))
|
|
|
|
bitmapObject.setRows(
|
|
dataRows, bitDepth=bitDepth, metrics=metrics, reverseBytes=True
|
|
)
|
|
|
|
|
|
def _writeExtFileImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont):
|
|
try:
|
|
folder = os.path.dirname(writer.file.name)
|
|
except AttributeError:
|
|
# fall back to current directory if output file's directory isn't found
|
|
folder = "."
|
|
folder = os.path.join(folder, "bitmaps")
|
|
filename = glyphName + bitmapObject.fileExtension
|
|
if not os.path.isdir(folder):
|
|
os.makedirs(folder)
|
|
folder = os.path.join(folder, "strike%d" % strikeIndex)
|
|
if not os.path.isdir(folder):
|
|
os.makedirs(folder)
|
|
|
|
fullPath = os.path.join(folder, filename)
|
|
writer.simpletag("extfileimagedata", value=fullPath)
|
|
writer.newline()
|
|
|
|
with open(fullPath, "wb") as file:
|
|
file.write(bitmapObject.imageData)
|
|
|
|
|
|
def _readExtFileImageData(bitmapObject, name, attrs, content, ttFont):
|
|
fullPath = attrs["value"]
|
|
with open(fullPath, "rb") as file:
|
|
bitmapObject.imageData = file.read()
|
|
|
|
|
|
# End of XML writing code.
|
|
|
|
# Important information about the naming scheme. Used for identifying formats
|
|
# in XML.
|
|
_bitmapGlyphSubclassPrefix = "ebdt_bitmap_format_"
|
|
|
|
|
|
class BitmapGlyph(object):
|
|
# For the external file format. This can be changed in subclasses. This way
|
|
# when the extfile option is turned on files have the form: glyphName.ext
|
|
# The default is just a flat binary file with no meaning.
|
|
fileExtension = ".bin"
|
|
|
|
# Keep track of reading and writing of various forms.
|
|
xmlDataFunctions = {
|
|
"raw": (_writeRawImageData, _readRawImageData),
|
|
"row": (_writeRowImageData, _readRowImageData),
|
|
"bitwise": (_writeBitwiseImageData, _readBitwiseImageData),
|
|
"extfile": (_writeExtFileImageData, _readExtFileImageData),
|
|
}
|
|
|
|
def __init__(self, data, ttFont):
|
|
self.data = data
|
|
self.ttFont = ttFont
|
|
# TODO Currently non-lazy decompilation is untested here...
|
|
# if not ttFont.lazy:
|
|
# self.decompile()
|
|
# del self.data
|
|
|
|
def __getattr__(self, attr):
|
|
# Allow lazy decompile.
|
|
if attr[:2] == "__":
|
|
raise AttributeError(attr)
|
|
if attr == "data":
|
|
raise AttributeError(attr)
|
|
self.decompile()
|
|
del self.data
|
|
return getattr(self, attr)
|
|
|
|
def ensureDecompiled(self, recurse=False):
|
|
if hasattr(self, "data"):
|
|
self.decompile()
|
|
del self.data
|
|
|
|
# Not a fan of this but it is needed for safer safety checking.
|
|
def getFormat(self):
|
|
return safeEval(self.__class__.__name__[len(_bitmapGlyphSubclassPrefix) :])
|
|
|
|
def toXML(self, strikeIndex, glyphName, writer, ttFont):
|
|
writer.begintag(self.__class__.__name__, [("name", glyphName)])
|
|
writer.newline()
|
|
|
|
self.writeMetrics(writer, ttFont)
|
|
# Use the internal write method to write using the correct output format.
|
|
self.writeData(strikeIndex, glyphName, writer, ttFont)
|
|
|
|
writer.endtag(self.__class__.__name__)
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, content, ttFont):
|
|
self.readMetrics(name, attrs, content, ttFont)
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attr, content = element
|
|
if not name.endswith("imagedata"):
|
|
continue
|
|
# Chop off 'imagedata' from the tag to get just the option.
|
|
option = name[: -len("imagedata")]
|
|
assert option in self.__class__.xmlDataFunctions
|
|
self.readData(name, attr, content, ttFont)
|
|
|
|
# Some of the glyphs have the metrics. This allows for metrics to be
|
|
# added if the glyph format has them. Default behavior is to do nothing.
|
|
def writeMetrics(self, writer, ttFont):
|
|
pass
|
|
|
|
# The opposite of write metrics.
|
|
def readMetrics(self, name, attrs, content, ttFont):
|
|
pass
|
|
|
|
def writeData(self, strikeIndex, glyphName, writer, ttFont):
|
|
try:
|
|
writeFunc, readFunc = self.__class__.xmlDataFunctions[
|
|
ttFont.bitmapGlyphDataFormat
|
|
]
|
|
except KeyError:
|
|
writeFunc = _writeRawImageData
|
|
writeFunc(strikeIndex, glyphName, self, writer, ttFont)
|
|
|
|
def readData(self, name, attrs, content, ttFont):
|
|
# Chop off 'imagedata' from the tag to get just the option.
|
|
option = name[: -len("imagedata")]
|
|
writeFunc, readFunc = self.__class__.xmlDataFunctions[option]
|
|
readFunc(self, name, attrs, content, ttFont)
|
|
|
|
|
|
# A closure for creating a mixin for the two types of metrics handling.
|
|
# Most of the code is very similar so its easier to deal with here.
|
|
# Everything works just by passing the class that the mixin is for.
|
|
def _createBitmapPlusMetricsMixin(metricsClass):
|
|
# Both metrics names are listed here to make meaningful error messages.
|
|
metricStrings = [BigGlyphMetrics.__name__, SmallGlyphMetrics.__name__]
|
|
curMetricsName = metricsClass.__name__
|
|
# Find which metrics this is for and determine the opposite name.
|
|
metricsId = metricStrings.index(curMetricsName)
|
|
oppositeMetricsName = metricStrings[1 - metricsId]
|
|
|
|
class BitmapPlusMetricsMixin(object):
|
|
def writeMetrics(self, writer, ttFont):
|
|
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 == curMetricsName:
|
|
self.metrics = metricsClass()
|
|
self.metrics.fromXML(name, attrs, content, ttFont)
|
|
elif name == oppositeMetricsName:
|
|
log.warning(
|
|
"Warning: %s being ignored in format %d.",
|
|
oppositeMetricsName,
|
|
self.getFormat(),
|
|
)
|
|
|
|
return BitmapPlusMetricsMixin
|
|
|
|
|
|
# Since there are only two types of mixin's just create them here.
|
|
BitmapPlusBigMetricsMixin = _createBitmapPlusMetricsMixin(BigGlyphMetrics)
|
|
BitmapPlusSmallMetricsMixin = _createBitmapPlusMetricsMixin(SmallGlyphMetrics)
|
|
|
|
|
|
# Data that is bit aligned can be tricky to deal with. These classes implement
|
|
# helper functionality for dealing with the data and getting a particular row
|
|
# of bitwise data. Also helps implement fancy data export/import in XML.
|
|
class BitAlignedBitmapMixin(object):
|
|
def _getBitRange(self, row, bitDepth, metrics):
|
|
rowBits = bitDepth * metrics.width
|
|
bitOffset = row * rowBits
|
|
return (bitOffset, bitOffset + rowBits)
|
|
|
|
def getRow(self, row, bitDepth=1, metrics=None, reverseBytes=False):
|
|
if metrics is None:
|
|
metrics = self.metrics
|
|
assert 0 <= row and row < metrics.height, "Illegal row access in bitmap"
|
|
|
|
# Loop through each byte. This can cover two bytes in the original data or
|
|
# a single byte if things happen to be aligned. The very last entry might
|
|
# not be aligned so take care to trim the binary data to size and pad with
|
|
# zeros in the row data. Bit aligned data is somewhat tricky.
|
|
#
|
|
# Example of data cut. Data cut represented in x's.
|
|
# '|' represents byte boundary.
|
|
# data = ...0XX|XXXXXX00|000... => XXXXXXXX
|
|
# or
|
|
# data = ...0XX|XXXX0000|000... => XXXXXX00
|
|
# or
|
|
# data = ...000|XXXXXXXX|000... => XXXXXXXX
|
|
# or
|
|
# data = ...000|00XXXX00|000... => XXXX0000
|
|
#
|
|
dataList = []
|
|
bitRange = self._getBitRange(row, bitDepth, metrics)
|
|
stepRange = bitRange + (8,)
|
|
for curBit in range(*stepRange):
|
|
endBit = min(curBit + 8, bitRange[1])
|
|
numBits = endBit - curBit
|
|
cutPoint = curBit % 8
|
|
firstByteLoc = curBit // 8
|
|
secondByteLoc = endBit // 8
|
|
if firstByteLoc < secondByteLoc:
|
|
numBitsCut = 8 - cutPoint
|
|
else:
|
|
numBitsCut = endBit - curBit
|
|
curByte = _reverseBytes(self.imageData[firstByteLoc])
|
|
firstHalf = byteord(curByte) >> cutPoint
|
|
firstHalf = ((1 << numBitsCut) - 1) & firstHalf
|
|
newByte = firstHalf
|
|
if firstByteLoc < secondByteLoc and secondByteLoc < len(self.imageData):
|
|
curByte = _reverseBytes(self.imageData[secondByteLoc])
|
|
secondHalf = byteord(curByte) << numBitsCut
|
|
newByte = (firstHalf | secondHalf) & ((1 << numBits) - 1)
|
|
dataList.append(bytechr(newByte))
|
|
|
|
# The way the data is kept is opposite the algorithm used.
|
|
data = bytesjoin(dataList)
|
|
if not reverseBytes:
|
|
data = _reverseBytes(data)
|
|
return data
|
|
|
|
def setRows(self, dataRows, bitDepth=1, metrics=None, reverseBytes=False):
|
|
if metrics is None:
|
|
metrics = self.metrics
|
|
if not reverseBytes:
|
|
dataRows = list(map(_reverseBytes, dataRows))
|
|
|
|
# Keep track of a list of ordinal values as they are easier to modify
|
|
# than a list of strings. Map to actual strings later.
|
|
numBytes = (self._getBitRange(len(dataRows), bitDepth, metrics)[0] + 7) // 8
|
|
ordDataList = [0] * numBytes
|
|
for row, data in enumerate(dataRows):
|
|
bitRange = self._getBitRange(row, bitDepth, metrics)
|
|
stepRange = bitRange + (8,)
|
|
for curBit, curByte in zip(range(*stepRange), data):
|
|
endBit = min(curBit + 8, bitRange[1])
|
|
cutPoint = curBit % 8
|
|
firstByteLoc = curBit // 8
|
|
secondByteLoc = endBit // 8
|
|
if firstByteLoc < secondByteLoc:
|
|
numBitsCut = 8 - cutPoint
|
|
else:
|
|
numBitsCut = endBit - curBit
|
|
curByte = byteord(curByte)
|
|
firstByte = curByte & ((1 << numBitsCut) - 1)
|
|
ordDataList[firstByteLoc] |= firstByte << cutPoint
|
|
if firstByteLoc < secondByteLoc and secondByteLoc < numBytes:
|
|
secondByte = (curByte >> numBitsCut) & ((1 << 8 - numBitsCut) - 1)
|
|
ordDataList[secondByteLoc] |= secondByte
|
|
|
|
# Save the image data with the bits going the correct way.
|
|
self.imageData = _reverseBytes(bytesjoin(map(bytechr, ordDataList)))
|
|
|
|
|
|
class ByteAlignedBitmapMixin(object):
|
|
def _getByteRange(self, row, bitDepth, metrics):
|
|
rowBytes = (bitDepth * metrics.width + 7) // 8
|
|
byteOffset = row * rowBytes
|
|
return (byteOffset, byteOffset + rowBytes)
|
|
|
|
def getRow(self, row, bitDepth=1, metrics=None, reverseBytes=False):
|
|
if metrics is None:
|
|
metrics = self.metrics
|
|
assert 0 <= row and row < metrics.height, "Illegal row access in bitmap"
|
|
byteRange = self._getByteRange(row, bitDepth, metrics)
|
|
data = self.imageData[slice(*byteRange)]
|
|
if reverseBytes:
|
|
data = _reverseBytes(data)
|
|
return data
|
|
|
|
def setRows(self, dataRows, bitDepth=1, metrics=None, reverseBytes=False):
|
|
if metrics is None:
|
|
metrics = self.metrics
|
|
if reverseBytes:
|
|
dataRows = map(_reverseBytes, dataRows)
|
|
self.imageData = bytesjoin(dataRows)
|
|
|
|
|
|
class ebdt_bitmap_format_1(
|
|
ByteAlignedBitmapMixin, BitmapPlusSmallMetricsMixin, BitmapGlyph
|
|
):
|
|
def decompile(self):
|
|
self.metrics = SmallGlyphMetrics()
|
|
dummy, data = sstruct.unpack2(smallGlyphMetricsFormat, self.data, self.metrics)
|
|
self.imageData = data
|
|
|
|
def compile(self, ttFont):
|
|
data = sstruct.pack(smallGlyphMetricsFormat, self.metrics)
|
|
return data + self.imageData
|
|
|
|
|
|
class ebdt_bitmap_format_2(
|
|
BitAlignedBitmapMixin, BitmapPlusSmallMetricsMixin, BitmapGlyph
|
|
):
|
|
def decompile(self):
|
|
self.metrics = SmallGlyphMetrics()
|
|
dummy, data = sstruct.unpack2(smallGlyphMetricsFormat, self.data, self.metrics)
|
|
self.imageData = data
|
|
|
|
def compile(self, ttFont):
|
|
data = sstruct.pack(smallGlyphMetricsFormat, self.metrics)
|
|
return data + self.imageData
|
|
|
|
|
|
class ebdt_bitmap_format_5(BitAlignedBitmapMixin, BitmapGlyph):
|
|
def decompile(self):
|
|
self.imageData = self.data
|
|
|
|
def compile(self, ttFont):
|
|
return self.imageData
|
|
|
|
|
|
class ebdt_bitmap_format_6(
|
|
ByteAlignedBitmapMixin, BitmapPlusBigMetricsMixin, BitmapGlyph
|
|
):
|
|
def decompile(self):
|
|
self.metrics = BigGlyphMetrics()
|
|
dummy, data = sstruct.unpack2(bigGlyphMetricsFormat, self.data, self.metrics)
|
|
self.imageData = data
|
|
|
|
def compile(self, ttFont):
|
|
data = sstruct.pack(bigGlyphMetricsFormat, self.metrics)
|
|
return data + self.imageData
|
|
|
|
|
|
class ebdt_bitmap_format_7(
|
|
BitAlignedBitmapMixin, BitmapPlusBigMetricsMixin, BitmapGlyph
|
|
):
|
|
def decompile(self):
|
|
self.metrics = BigGlyphMetrics()
|
|
dummy, data = sstruct.unpack2(bigGlyphMetricsFormat, self.data, self.metrics)
|
|
self.imageData = data
|
|
|
|
def compile(self, ttFont):
|
|
data = sstruct.pack(bigGlyphMetricsFormat, self.metrics)
|
|
return data + self.imageData
|
|
|
|
|
|
class ComponentBitmapGlyph(BitmapGlyph):
|
|
def toXML(self, strikeIndex, glyphName, writer, ttFont):
|
|
writer.begintag(self.__class__.__name__, [("name", glyphName)])
|
|
writer.newline()
|
|
|
|
self.writeMetrics(writer, ttFont)
|
|
|
|
writer.begintag("components")
|
|
writer.newline()
|
|
for curComponent in self.componentArray:
|
|
curComponent.toXML(writer, ttFont)
|
|
writer.endtag("components")
|
|
writer.newline()
|
|
|
|
writer.endtag(self.__class__.__name__)
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, content, ttFont):
|
|
self.readMetrics(name, attrs, content, ttFont)
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attr, content = element
|
|
if name == "components":
|
|
self.componentArray = []
|
|
for compElement in content:
|
|
if not isinstance(compElement, tuple):
|
|
continue
|
|
name, attrs, content = compElement
|
|
if name == "ebdtComponent":
|
|
curComponent = EbdtComponent()
|
|
curComponent.fromXML(name, attrs, content, ttFont)
|
|
self.componentArray.append(curComponent)
|
|
else:
|
|
log.warning("'%s' being ignored in component array.", name)
|
|
|
|
|
|
class ebdt_bitmap_format_8(BitmapPlusSmallMetricsMixin, ComponentBitmapGlyph):
|
|
def decompile(self):
|
|
self.metrics = SmallGlyphMetrics()
|
|
dummy, data = sstruct.unpack2(smallGlyphMetricsFormat, self.data, self.metrics)
|
|
data = data[1:]
|
|
|
|
(numComponents,) = struct.unpack(">H", data[:2])
|
|
data = data[2:]
|
|
self.componentArray = []
|
|
for i in range(numComponents):
|
|
curComponent = EbdtComponent()
|
|
dummy, data = sstruct.unpack2(ebdtComponentFormat, data, curComponent)
|
|
curComponent.name = self.ttFont.getGlyphName(curComponent.glyphCode)
|
|
self.componentArray.append(curComponent)
|
|
|
|
def compile(self, ttFont):
|
|
dataList = []
|
|
dataList.append(sstruct.pack(smallGlyphMetricsFormat, self.metrics))
|
|
dataList.append(b"\0")
|
|
dataList.append(struct.pack(">H", len(self.componentArray)))
|
|
for curComponent in self.componentArray:
|
|
curComponent.glyphCode = ttFont.getGlyphID(curComponent.name)
|
|
dataList.append(sstruct.pack(ebdtComponentFormat, curComponent))
|
|
return bytesjoin(dataList)
|
|
|
|
|
|
class ebdt_bitmap_format_9(BitmapPlusBigMetricsMixin, ComponentBitmapGlyph):
|
|
def decompile(self):
|
|
self.metrics = BigGlyphMetrics()
|
|
dummy, data = sstruct.unpack2(bigGlyphMetricsFormat, self.data, self.metrics)
|
|
(numComponents,) = struct.unpack(">H", data[:2])
|
|
data = data[2:]
|
|
self.componentArray = []
|
|
for i in range(numComponents):
|
|
curComponent = EbdtComponent()
|
|
dummy, data = sstruct.unpack2(ebdtComponentFormat, data, curComponent)
|
|
curComponent.name = self.ttFont.getGlyphName(curComponent.glyphCode)
|
|
self.componentArray.append(curComponent)
|
|
|
|
def compile(self, ttFont):
|
|
dataList = []
|
|
dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics))
|
|
dataList.append(struct.pack(">H", len(self.componentArray)))
|
|
for curComponent in self.componentArray:
|
|
curComponent.glyphCode = ttFont.getGlyphID(curComponent.name)
|
|
dataList.append(sstruct.pack(ebdtComponentFormat, curComponent))
|
|
return bytesjoin(dataList)
|
|
|
|
|
|
# Dictionary of bitmap formats to the class representing that format
|
|
# currently only the ones listed in this map are the ones supported.
|
|
ebdt_bitmap_classes = {
|
|
1: ebdt_bitmap_format_1,
|
|
2: ebdt_bitmap_format_2,
|
|
5: ebdt_bitmap_format_5,
|
|
6: ebdt_bitmap_format_6,
|
|
7: ebdt_bitmap_format_7,
|
|
8: ebdt_bitmap_format_8,
|
|
9: ebdt_bitmap_format_9,
|
|
}
|