woff2: add support for hmtx transformation
no tests yet fixup
This commit is contained in:
parent
0162446f4c
commit
deaeb909a7
@ -98,6 +98,8 @@ class WOFF2Reader(SFNTReader):
|
|||||||
data = self._reconstructGlyf(rawData, padding)
|
data = self._reconstructGlyf(rawData, padding)
|
||||||
elif tag == 'loca':
|
elif tag == 'loca':
|
||||||
data = self._reconstructLoca()
|
data = self._reconstructLoca()
|
||||||
|
elif tag == 'hmtx':
|
||||||
|
data = self._reconstructHmtx(rawData)
|
||||||
else:
|
else:
|
||||||
raise TTLibError("transform for table '%s' is unknown" % tag)
|
raise TTLibError("transform for table '%s' is unknown" % tag)
|
||||||
return data
|
return data
|
||||||
@ -128,6 +130,34 @@ class WOFF2Reader(SFNTReader):
|
|||||||
% (self.tables['loca'].origLength, len(data)))
|
% (self.tables['loca'].origLength, len(data)))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def _reconstructHmtx(self, data):
|
||||||
|
""" Return reconstructed hmtx table data. """
|
||||||
|
# Before reconstructing 'hmtx' table we need to parse other tables:
|
||||||
|
# 'glyf' is required for reconstructing the sidebearings from the glyphs'
|
||||||
|
# bounding box; 'hhea' is needed for the numberOfHMetrics field.
|
||||||
|
if "glyf" in self.flavorData.transformedTables:
|
||||||
|
# transformed 'glyf' table is self-contained, thus 'loca' not needed
|
||||||
|
tableDependencies = ("maxp", "hhea", "glyf")
|
||||||
|
else:
|
||||||
|
# decompiling untransformed 'glyf' requires 'loca', which requires 'head'
|
||||||
|
tableDependencies = ("maxp", "head", "hhea", "loca", "glyf")
|
||||||
|
for tag in tableDependencies:
|
||||||
|
self._decompileTable(tag)
|
||||||
|
hmtxTable = self.ttFont["hmtx"] = WOFF2HmtxTable()
|
||||||
|
hmtxTable.reconstruct(data, self.ttFont)
|
||||||
|
data = hmtxTable.compile(self.ttFont)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _decompileTable(self, tag):
|
||||||
|
"""Decompile table data and store it inside self.ttFont."""
|
||||||
|
data = self[tag]
|
||||||
|
if self.ttFont.isLoaded(tag):
|
||||||
|
return self.ttFont[tag]
|
||||||
|
tableClass = getTableClass(tag)
|
||||||
|
table = tableClass(tag)
|
||||||
|
self.ttFont.tables[tag] = table
|
||||||
|
table.decompile(data, self.ttFont)
|
||||||
|
|
||||||
|
|
||||||
class WOFF2Writer(SFNTWriter):
|
class WOFF2Writer(SFNTWriter):
|
||||||
|
|
||||||
@ -257,6 +287,8 @@ class WOFF2Writer(SFNTWriter):
|
|||||||
tableClass = WOFF2LocaTable
|
tableClass = WOFF2LocaTable
|
||||||
elif tag == 'glyf':
|
elif tag == 'glyf':
|
||||||
tableClass = WOFF2GlyfTable
|
tableClass = WOFF2GlyfTable
|
||||||
|
elif tag == 'hmtx':
|
||||||
|
tableClass = WOFF2HmtxTable
|
||||||
else:
|
else:
|
||||||
tableClass = getTableClass(tag)
|
tableClass = getTableClass(tag)
|
||||||
table = tableClass(tag)
|
table = tableClass(tag)
|
||||||
@ -286,19 +318,13 @@ class WOFF2Writer(SFNTWriter):
|
|||||||
def _transformTables(self):
|
def _transformTables(self):
|
||||||
"""Return transformed font data."""
|
"""Return transformed font data."""
|
||||||
transformedTables = self.flavorData.transformedTables
|
transformedTables = self.flavorData.transformedTables
|
||||||
if (
|
|
||||||
"glyf" in transformedTables and "loca" not in transformedTables
|
|
||||||
or "loca" in transformedTables and "glyf" not in transformedTables
|
|
||||||
):
|
|
||||||
raise ValueError(
|
|
||||||
"'glyf' and 'loca' must be transformed (or not) together"
|
|
||||||
)
|
|
||||||
|
|
||||||
for tag, entry in self.tables.items():
|
for tag, entry in self.tables.items():
|
||||||
|
data = None
|
||||||
if tag in transformedTables:
|
if tag in transformedTables:
|
||||||
data = self.transformTable(tag)
|
data = self.transformTable(tag)
|
||||||
entry.transformed = True
|
if data is not None:
|
||||||
else:
|
entry.transformed = True
|
||||||
|
if data is None:
|
||||||
# pass-through the table data without transformation
|
# pass-through the table data without transformation
|
||||||
data = entry.data
|
data = entry.data
|
||||||
entry.transformed = False
|
entry.transformed = False
|
||||||
@ -310,7 +336,9 @@ class WOFF2Writer(SFNTWriter):
|
|||||||
return fontData
|
return fontData
|
||||||
|
|
||||||
def transformTable(self, tag):
|
def transformTable(self, tag):
|
||||||
"""Return transformed table data."""
|
"""Return transformed table data, or None if some pre-conditions aren't
|
||||||
|
met -- in which case, the non-transformed table data will be used.
|
||||||
|
"""
|
||||||
if tag == "loca":
|
if tag == "loca":
|
||||||
data = b""
|
data = b""
|
||||||
elif tag == "glyf":
|
elif tag == "glyf":
|
||||||
@ -318,6 +346,13 @@ class WOFF2Writer(SFNTWriter):
|
|||||||
self._decompileTable(tag)
|
self._decompileTable(tag)
|
||||||
glyfTable = self.ttFont['glyf']
|
glyfTable = self.ttFont['glyf']
|
||||||
data = glyfTable.transform(self.ttFont)
|
data = glyfTable.transform(self.ttFont)
|
||||||
|
elif tag == "hmtx":
|
||||||
|
if "glyf" not in self.tables:
|
||||||
|
return
|
||||||
|
for tag in ("maxp", "head", "hhea", "loca", "glyf", "hmtx"):
|
||||||
|
self._decompileTable(tag)
|
||||||
|
hmtxTable = self.ttFont["hmtx"]
|
||||||
|
data = hmtxTable.transform(self.ttFont) # can be None
|
||||||
else:
|
else:
|
||||||
raise TTLibError("Transform for table '%s' is unknown" % tag)
|
raise TTLibError("Transform for table '%s' is unknown" % tag)
|
||||||
return data
|
return data
|
||||||
@ -931,6 +966,164 @@ class WOFF2GlyfTable(getTableClass('glyf')):
|
|||||||
self.glyphStream += triplets.tostring()
|
self.glyphStream += triplets.tostring()
|
||||||
|
|
||||||
|
|
||||||
|
class WOFF2HmtxTable(getTableClass("hmtx")):
|
||||||
|
|
||||||
|
def __init__(self, tag=None):
|
||||||
|
self.tableTag = Tag(tag or 'hmtx')
|
||||||
|
|
||||||
|
def reconstruct(self, data, ttFont):
|
||||||
|
flags, = struct.unpack(">B", data[:1])
|
||||||
|
data = data[1:]
|
||||||
|
if flags & 0b11111100 != 0:
|
||||||
|
raise TTLibError("Bits 2-7 of '%s' flags are reserved" % self.tableTag)
|
||||||
|
|
||||||
|
# When bit 0 is _not_ set, the lsb[] array is present
|
||||||
|
hasLsbArray = flags & 1 == 0
|
||||||
|
# When bit 1 is _not_ set, the leftSideBearing[] array is present
|
||||||
|
hasLeftSideBearingArray = flags & 2 == 0
|
||||||
|
if hasLsbArray and hasLeftSideBearingArray:
|
||||||
|
raise TTLibError(
|
||||||
|
"either bits 0 or 1 (or both) must set in transformed '%s' flags"
|
||||||
|
% self.tableTag
|
||||||
|
)
|
||||||
|
|
||||||
|
glyfTable = ttFont["glyf"]
|
||||||
|
headerTable = ttFont["hhea"]
|
||||||
|
glyphOrder = glyfTable.glyphOrder
|
||||||
|
numGlyphs = len(glyphOrder)
|
||||||
|
numberOfHMetrics = min(int(headerTable.numberOfHMetrics), numGlyphs)
|
||||||
|
|
||||||
|
assert len(data) >= 2 * numberOfHMetrics
|
||||||
|
advanceWidthArray = array.array("H", data[:2 * numberOfHMetrics])
|
||||||
|
if sys.byteorder != "big":
|
||||||
|
advanceWidthArray.byteswap()
|
||||||
|
data = data[2 * numberOfHMetrics:]
|
||||||
|
|
||||||
|
if hasLsbArray:
|
||||||
|
assert len(data) >= 2 * numberOfHMetrics
|
||||||
|
lsbArray = array.array("h", data[:2 * numberOfHMetrics])
|
||||||
|
if sys.byteorder != "big":
|
||||||
|
lsbArray.byteswap()
|
||||||
|
data = data[2 * numberOfHMetrics:]
|
||||||
|
else:
|
||||||
|
# compute (proportional) glyphs' lsb from their xMin
|
||||||
|
lsbArray = array.array("h")
|
||||||
|
for i, glyphName in enumerate(glyphOrder):
|
||||||
|
if i >= numberOfHMetrics:
|
||||||
|
break
|
||||||
|
glyph = glyfTable[glyphName]
|
||||||
|
xMin = getattr(glyph, "xMin", 0)
|
||||||
|
lsbArray.append(xMin)
|
||||||
|
|
||||||
|
numberOfSideBearings = numGlyphs - numberOfHMetrics
|
||||||
|
if hasLeftSideBearingArray:
|
||||||
|
assert len(data) >= 2 * numberOfSideBearings
|
||||||
|
leftSideBearingArray = array.array("h", data[:2 * numberOfSideBearings])
|
||||||
|
if sys.byteorder != "big":
|
||||||
|
leftSideBearingArray.byteswap()
|
||||||
|
data = data[2 * numberOfSideBearings:]
|
||||||
|
else:
|
||||||
|
# compute (monospaced) glyphs' leftSideBearing from their xMin
|
||||||
|
leftSideBearingArray = array.array("h")
|
||||||
|
for i, glyphName in enumerate(glyphOrder):
|
||||||
|
if i < numberOfHMetrics:
|
||||||
|
continue
|
||||||
|
glyph = glyfTable[glyphName]
|
||||||
|
xMin = getattr(glyph, "xMin", 0)
|
||||||
|
leftSideBearingArray.append(xMin)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
raise TTLibError("too much '%s' table data" % self.tableTag)
|
||||||
|
|
||||||
|
self.metrics = {}
|
||||||
|
for i in range(numberOfHMetrics):
|
||||||
|
glyphName = glyphOrder[i]
|
||||||
|
advanceWidth, lsb = advanceWidthArray[i], lsbArray[i]
|
||||||
|
self.metrics[glyphName] = (advanceWidth, lsb)
|
||||||
|
lastAdvance = advanceWidthArray[-1]
|
||||||
|
for i in range(numberOfSideBearings):
|
||||||
|
glyphName = glyphOrder[i + numberOfHMetrics]
|
||||||
|
self.metrics[glyphName] = (lastAdvance, leftSideBearingArray[i])
|
||||||
|
|
||||||
|
def transform(self, ttFont):
|
||||||
|
glyphOrder = ttFont.getGlyphOrder()
|
||||||
|
glyf = ttFont["glyf"]
|
||||||
|
hhea = ttFont["hhea"]
|
||||||
|
numberOfHMetrics = hhea.numberOfHMetrics
|
||||||
|
|
||||||
|
# check if any of the proportional glyphs has left sidebearings that
|
||||||
|
# differ from their xMin bounding box values.
|
||||||
|
hasLsbArray = False
|
||||||
|
for i in range(numberOfHMetrics):
|
||||||
|
glyphName = glyphOrder[i]
|
||||||
|
lsb = self.metrics[glyphName][1]
|
||||||
|
if lsb != getattr(glyf[glyphName], "xMin", 0):
|
||||||
|
hasLsbArray = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# do the same for the monospaced glyphs (if any) at the end of hmtx table
|
||||||
|
hasLeftSideBearingArray = False
|
||||||
|
for i in range(numberOfHMetrics, len(glyphOrder)):
|
||||||
|
glyphName = glyphOrder[i]
|
||||||
|
lsb = self.metrics[glyphName][1]
|
||||||
|
if lsb != getattr(glyf[glyphName], "xMin", 0):
|
||||||
|
hasLeftSideBearingArray = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# if we need to encode both sidebearings arrays, then no transformation is
|
||||||
|
# applicable, and we must use the untransformed hmtx data
|
||||||
|
if hasLsbArray and hasLeftSideBearingArray:
|
||||||
|
return
|
||||||
|
|
||||||
|
# set bit 0 and 1 when the respective arrays are _not_ present
|
||||||
|
flags = 0
|
||||||
|
if not hasLsbArray:
|
||||||
|
flags |= 1 << 0
|
||||||
|
if not hasLeftSideBearingArray:
|
||||||
|
flags |= 1 << 1
|
||||||
|
|
||||||
|
data = struct.pack(">B", flags)
|
||||||
|
|
||||||
|
advanceWidthArray = array.array(
|
||||||
|
"H",
|
||||||
|
[
|
||||||
|
self.metrics[glyphName][0]
|
||||||
|
for i, glyphName in enumerate(glyphOrder)
|
||||||
|
if i < numberOfHMetrics
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if sys.byteorder != "big":
|
||||||
|
advanceWidthArray.byteswap()
|
||||||
|
data += advanceWidthArray.tostring()
|
||||||
|
|
||||||
|
if hasLsbArray:
|
||||||
|
lsbArray = array.array(
|
||||||
|
"h",
|
||||||
|
[
|
||||||
|
self.metrics[glyphName][1]
|
||||||
|
for i, glyphName in enumerate(glyphOrder)
|
||||||
|
if i < numberOfHMetrics
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if sys.byteorder != "big":
|
||||||
|
lsbArray.byteswap()
|
||||||
|
data += lsbArray.tostring()
|
||||||
|
|
||||||
|
if hasLeftSideBearingArray:
|
||||||
|
leftSideBearingArray = array.array(
|
||||||
|
"h",
|
||||||
|
[
|
||||||
|
self.metrics[glyphOrder[i]][1]
|
||||||
|
for i in range(numberOfHMetrics, len(glyphOrder))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if sys.byteorder != "big":
|
||||||
|
leftSideBearingArray.byteswap()
|
||||||
|
data += leftSideBearingArray.tostring()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class WOFF2FlavorData(WOFFFlavorData):
|
class WOFF2FlavorData(WOFFFlavorData):
|
||||||
|
|
||||||
Flavor = 'woff2'
|
Flavor = 'woff2'
|
||||||
@ -948,8 +1141,17 @@ class WOFF2FlavorData(WOFFFlavorData):
|
|||||||
raise TypeError(
|
raise TypeError(
|
||||||
"'reader' and 'transformedTables' arguments are mutually exclusive"
|
"'reader' and 'transformedTables' arguments are mutually exclusive"
|
||||||
)
|
)
|
||||||
|
|
||||||
if transformedTables is None:
|
if transformedTables is None:
|
||||||
transformedTables = woff2TransformedTableTags
|
transformedTables = woff2TransformedTableTags
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
"glyf" in transformedTables and "loca" not in transformedTables
|
||||||
|
or "loca" in transformedTables and "glyf" not in transformedTables
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"'glyf' and 'loca' must be transformed (or not) together"
|
||||||
|
)
|
||||||
|
|
||||||
self.majorVersion = None
|
self.majorVersion = None
|
||||||
self.minorVersion = None
|
self.minorVersion = None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user