Merge pull request #1639 from anthrotype/woff2-untransformed
[woff2] support hmtx transform + glyf/loca without transformation
This commit is contained in:
commit
00e054336f
@ -16,7 +16,7 @@ from fontTools.ttLib.tables import ttProgram
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger("fontTools.ttLib.woff2")
|
||||||
|
|
||||||
haveBrotli = False
|
haveBrotli = False
|
||||||
try:
|
try:
|
||||||
@ -82,7 +82,7 @@ class WOFF2Reader(SFNTReader):
|
|||||||
"""Fetch the raw table data. Reconstruct transformed tables."""
|
"""Fetch the raw table data. Reconstruct transformed tables."""
|
||||||
entry = self.tables[Tag(tag)]
|
entry = self.tables[Tag(tag)]
|
||||||
if not hasattr(entry, 'data'):
|
if not hasattr(entry, 'data'):
|
||||||
if tag in woff2TransformedTableTags:
|
if entry.transformed:
|
||||||
entry.data = self.reconstructTable(tag)
|
entry.data = self.reconstructTable(tag)
|
||||||
else:
|
else:
|
||||||
entry.data = entry.loadData(self.transformBuffer)
|
entry.data = entry.loadData(self.transformBuffer)
|
||||||
@ -90,8 +90,6 @@ class WOFF2Reader(SFNTReader):
|
|||||||
|
|
||||||
def reconstructTable(self, tag):
|
def reconstructTable(self, tag):
|
||||||
"""Reconstruct table named 'tag' from transformed data."""
|
"""Reconstruct table named 'tag' from transformed data."""
|
||||||
if tag not in woff2TransformedTableTags:
|
|
||||||
raise TTLibError("transform for table '%s' is unknown" % tag)
|
|
||||||
entry = self.tables[Tag(tag)]
|
entry = self.tables[Tag(tag)]
|
||||||
rawData = entry.loadData(self.transformBuffer)
|
rawData = entry.loadData(self.transformBuffer)
|
||||||
if tag == 'glyf':
|
if tag == 'glyf':
|
||||||
@ -100,8 +98,10 @@ 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 NotImplementedError
|
raise TTLibError("transform for table '%s' is unknown" % tag)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _reconstructGlyf(self, data, padding=None):
|
def _reconstructGlyf(self, data, padding=None):
|
||||||
@ -130,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):
|
||||||
|
|
||||||
@ -199,7 +227,7 @@ class WOFF2Writer(SFNTWriter):
|
|||||||
# See:
|
# See:
|
||||||
# https://github.com/khaledhosny/ots/issues/60
|
# https://github.com/khaledhosny/ots/issues/60
|
||||||
# https://github.com/google/woff2/issues/15
|
# https://github.com/google/woff2/issues/15
|
||||||
if isTrueType:
|
if isTrueType and "glyf" in self.flavorData.transformedTables:
|
||||||
self._normaliseGlyfAndLoca(padding=4)
|
self._normaliseGlyfAndLoca(padding=4)
|
||||||
self._setHeadTransformFlag()
|
self._setHeadTransformFlag()
|
||||||
|
|
||||||
@ -234,13 +262,7 @@ class WOFF2Writer(SFNTWriter):
|
|||||||
if self.sfntVersion == "OTTO":
|
if self.sfntVersion == "OTTO":
|
||||||
return
|
return
|
||||||
|
|
||||||
# make up glyph names required to decompile glyf table
|
for tag in ('maxp', 'head', 'loca', 'glyf'):
|
||||||
self._decompileTable('maxp')
|
|
||||||
numGlyphs = self.ttFont['maxp'].numGlyphs
|
|
||||||
glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, numGlyphs)]
|
|
||||||
self.ttFont.setGlyphOrder(glyphOrder)
|
|
||||||
|
|
||||||
for tag in ('head', 'loca', 'glyf'):
|
|
||||||
self._decompileTable(tag)
|
self._decompileTable(tag)
|
||||||
self.ttFont['glyf'].padding = padding
|
self.ttFont['glyf'].padding = padding
|
||||||
for tag in ('glyf', 'loca'):
|
for tag in ('glyf', 'loca'):
|
||||||
@ -265,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)
|
||||||
@ -293,11 +317,17 @@ class WOFF2Writer(SFNTWriter):
|
|||||||
|
|
||||||
def _transformTables(self):
|
def _transformTables(self):
|
||||||
"""Return transformed font data."""
|
"""Return transformed font data."""
|
||||||
|
transformedTables = self.flavorData.transformedTables
|
||||||
for tag, entry in self.tables.items():
|
for tag, entry in self.tables.items():
|
||||||
if tag in woff2TransformedTableTags:
|
data = None
|
||||||
|
if tag in transformedTables:
|
||||||
data = self.transformTable(tag)
|
data = self.transformTable(tag)
|
||||||
else:
|
if data is not None:
|
||||||
|
entry.transformed = True
|
||||||
|
if data is None:
|
||||||
|
# pass-through the table data without transformation
|
||||||
data = entry.data
|
data = entry.data
|
||||||
|
entry.transformed = False
|
||||||
entry.offset = self.nextTableOffset
|
entry.offset = self.nextTableOffset
|
||||||
entry.saveData(self.transformBuffer, data)
|
entry.saveData(self.transformBuffer, data)
|
||||||
self.nextTableOffset += entry.length
|
self.nextTableOffset += entry.length
|
||||||
@ -306,9 +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
|
||||||
if tag not in woff2TransformedTableTags:
|
met -- in which case, the non-transformed table data will be used.
|
||||||
raise TTLibError("Transform for table '%s' is unknown" % tag)
|
"""
|
||||||
if tag == "loca":
|
if tag == "loca":
|
||||||
data = b""
|
data = b""
|
||||||
elif tag == "glyf":
|
elif tag == "glyf":
|
||||||
@ -316,8 +346,15 @@ 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 NotImplementedError
|
raise TTLibError("Transform for table '%s' is unknown" % tag)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _calcMasterChecksum(self):
|
def _calcMasterChecksum(self):
|
||||||
@ -533,11 +570,9 @@ class WOFF2DirectoryEntry(DirectoryEntry):
|
|||||||
# otherwise, tag is derived from a fixed 'Known Tags' table
|
# otherwise, tag is derived from a fixed 'Known Tags' table
|
||||||
self.tag = woff2KnownTags[self.flags & 0x3F]
|
self.tag = woff2KnownTags[self.flags & 0x3F]
|
||||||
self.tag = Tag(self.tag)
|
self.tag = Tag(self.tag)
|
||||||
if self.flags & 0xC0 != 0:
|
|
||||||
raise TTLibError('bits 6-7 are reserved and must be 0')
|
|
||||||
self.origLength, data = unpackBase128(data)
|
self.origLength, data = unpackBase128(data)
|
||||||
self.length = self.origLength
|
self.length = self.origLength
|
||||||
if self.tag in woff2TransformedTableTags:
|
if self.transformed:
|
||||||
self.length, data = unpackBase128(data)
|
self.length, data = unpackBase128(data)
|
||||||
if self.tag == 'loca' and self.length != 0:
|
if self.tag == 'loca' and self.length != 0:
|
||||||
raise TTLibError(
|
raise TTLibError(
|
||||||
@ -550,10 +585,44 @@ class WOFF2DirectoryEntry(DirectoryEntry):
|
|||||||
if (self.flags & 0x3F) == 0x3F:
|
if (self.flags & 0x3F) == 0x3F:
|
||||||
data += struct.pack('>4s', self.tag.tobytes())
|
data += struct.pack('>4s', self.tag.tobytes())
|
||||||
data += packBase128(self.origLength)
|
data += packBase128(self.origLength)
|
||||||
if self.tag in woff2TransformedTableTags:
|
if self.transformed:
|
||||||
data += packBase128(self.length)
|
data += packBase128(self.length)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def transformVersion(self):
|
||||||
|
"""Return bits 6-7 of table entry's flags, which indicate the preprocessing
|
||||||
|
transformation version number (between 0 and 3).
|
||||||
|
"""
|
||||||
|
return self.flags >> 6
|
||||||
|
|
||||||
|
@transformVersion.setter
|
||||||
|
def transformVersion(self, value):
|
||||||
|
assert 0 <= value <= 3
|
||||||
|
self.flags |= value << 6
|
||||||
|
|
||||||
|
@property
|
||||||
|
def transformed(self):
|
||||||
|
"""Return True if the table has any transformation, else return False."""
|
||||||
|
# For all tables in a font, except for 'glyf' and 'loca', the transformation
|
||||||
|
# version 0 indicates the null transform (where the original table data is
|
||||||
|
# passed directly to the Brotli compressor). For 'glyf' and 'loca' tables,
|
||||||
|
# transformation version 3 indicates the null transform
|
||||||
|
if self.tag in {"glyf", "loca"}:
|
||||||
|
return self.transformVersion != 3
|
||||||
|
else:
|
||||||
|
return self.transformVersion != 0
|
||||||
|
|
||||||
|
@transformed.setter
|
||||||
|
def transformed(self, booleanValue):
|
||||||
|
# here we assume that a non-null transform means version 0 for 'glyf' and
|
||||||
|
# 'loca' and 1 for every other table (e.g. hmtx); but that may change as
|
||||||
|
# new transformation formats are introduced in the future (if ever).
|
||||||
|
if self.tag in {"glyf", "loca"}:
|
||||||
|
self.transformVersion = 3 if not booleanValue else 0
|
||||||
|
else:
|
||||||
|
self.transformVersion = int(booleanValue)
|
||||||
|
|
||||||
|
|
||||||
class WOFF2LocaTable(getTableClass('loca')):
|
class WOFF2LocaTable(getTableClass('loca')):
|
||||||
"""Same as parent class. The only difference is that it attempts to preserve
|
"""Same as parent class. The only difference is that it attempts to preserve
|
||||||
@ -652,19 +721,7 @@ class WOFF2GlyfTable(getTableClass('glyf')):
|
|||||||
def transform(self, ttFont):
|
def transform(self, ttFont):
|
||||||
""" Return transformed 'glyf' data """
|
""" Return transformed 'glyf' data """
|
||||||
self.numGlyphs = len(self.glyphs)
|
self.numGlyphs = len(self.glyphs)
|
||||||
if not hasattr(self, "glyphOrder"):
|
assert len(self.glyphOrder) == self.numGlyphs
|
||||||
try:
|
|
||||||
self.glyphOrder = ttFont.getGlyphOrder()
|
|
||||||
except:
|
|
||||||
self.glyphOrder = None
|
|
||||||
if self.glyphOrder is None:
|
|
||||||
self.glyphOrder = [".notdef"]
|
|
||||||
self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)])
|
|
||||||
if len(self.glyphOrder) != self.numGlyphs:
|
|
||||||
raise TTLibError(
|
|
||||||
"incorrect glyphOrder: expected %d glyphs, found %d" %
|
|
||||||
(len(self.glyphOrder), self.numGlyphs))
|
|
||||||
|
|
||||||
if 'maxp' in ttFont:
|
if 'maxp' in ttFont:
|
||||||
ttFont['maxp'].numGlyphs = self.numGlyphs
|
ttFont['maxp'].numGlyphs = self.numGlyphs
|
||||||
self.indexFormat = ttFont['head'].indexToLocFormat
|
self.indexFormat = ttFont['head'].indexToLocFormat
|
||||||
@ -909,13 +966,193 @@ 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'
|
||||||
|
|
||||||
def __init__(self, reader=None):
|
def __init__(self, reader=None, transformedTables=None):
|
||||||
|
"""Data class that holds the WOFF2 header major/minor version, any
|
||||||
|
metadata or private data (as bytes strings), and the set of
|
||||||
|
table tags that have transformations applied (if reader is not None),
|
||||||
|
or will have once the WOFF2 font is compiled.
|
||||||
|
"""
|
||||||
if not haveBrotli:
|
if not haveBrotli:
|
||||||
raise ImportError("No module named brotli")
|
raise ImportError("No module named brotli")
|
||||||
|
|
||||||
|
if reader is not None and transformedTables is not None:
|
||||||
|
raise TypeError(
|
||||||
|
"'reader' and 'transformedTables' arguments are mutually exclusive"
|
||||||
|
)
|
||||||
|
|
||||||
|
if transformedTables is None:
|
||||||
|
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
|
||||||
self.metaData = None
|
self.metaData = None
|
||||||
@ -935,6 +1172,13 @@ class WOFF2FlavorData(WOFFFlavorData):
|
|||||||
data = reader.file.read(reader.privLength)
|
data = reader.file.read(reader.privLength)
|
||||||
assert len(data) == reader.privLength
|
assert len(data) == reader.privLength
|
||||||
self.privData = data
|
self.privData = data
|
||||||
|
transformedTables = [
|
||||||
|
tag
|
||||||
|
for tag, entry in reader.tables.items()
|
||||||
|
if entry.transformed
|
||||||
|
]
|
||||||
|
|
||||||
|
self.transformedTables = set(transformedTables)
|
||||||
|
|
||||||
|
|
||||||
def unpackBase128(data):
|
def unpackBase128(data):
|
||||||
@ -1091,6 +1335,164 @@ def pack255UShort(value):
|
|||||||
return struct.pack(">BH", 253, value)
|
return struct.pack(">BH", 253, value)
|
||||||
|
|
||||||
|
|
||||||
|
def compress(input_file, output_file, transform_tables=None):
|
||||||
|
"""Compress OpenType font to WOFF2.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_file: a file path, file or file-like object (open in binary mode)
|
||||||
|
containing an OpenType font (either CFF- or TrueType-flavored).
|
||||||
|
output_file: a file path, file or file-like object where to save the
|
||||||
|
compressed WOFF2 font.
|
||||||
|
transform_tables: Optional[Iterable[str]]: a set of table tags for which
|
||||||
|
to enable preprocessing transformations. By default, only 'glyf'
|
||||||
|
and 'loca' tables are transformed. An empty set means disable all
|
||||||
|
transformations.
|
||||||
|
"""
|
||||||
|
log.info("Processing %s => %s" % (input_file, output_file))
|
||||||
|
|
||||||
|
font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
|
||||||
|
font.flavor = "woff2"
|
||||||
|
|
||||||
|
if transform_tables is not None:
|
||||||
|
font.flavorData = WOFF2FlavorData(transformedTables=transform_tables)
|
||||||
|
|
||||||
|
font.save(output_file, reorderTables=False)
|
||||||
|
|
||||||
|
|
||||||
|
def decompress(input_file, output_file):
|
||||||
|
"""Decompress WOFF2 font to OpenType font.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_file: a file path, file or file-like object (open in binary mode)
|
||||||
|
containing a compressed WOFF2 font.
|
||||||
|
output_file: a file path, file or file-like object where to save the
|
||||||
|
decompressed OpenType font.
|
||||||
|
"""
|
||||||
|
log.info("Processing %s => %s" % (input_file, output_file))
|
||||||
|
|
||||||
|
font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
|
||||||
|
font.flavor = None
|
||||||
|
font.flavorData = None
|
||||||
|
font.save(output_file, reorderTables=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
import argparse
|
||||||
|
from fontTools import configLogger
|
||||||
|
from fontTools.ttx import makeOutputFileName
|
||||||
|
|
||||||
|
class _NoGlyfTransformAction(argparse.Action):
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
namespace.transform_tables.difference_update({"glyf", "loca"})
|
||||||
|
|
||||||
|
class _HmtxTransformAction(argparse.Action):
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
namespace.transform_tables.add("hmtx")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="fonttools ttLib.woff2",
|
||||||
|
description="Compress and decompress WOFF2 fonts",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser_group = parser.add_subparsers(title="sub-commands")
|
||||||
|
parser_compress = parser_group.add_parser("compress")
|
||||||
|
parser_decompress = parser_group.add_parser("decompress")
|
||||||
|
|
||||||
|
for subparser in (parser_compress, parser_decompress):
|
||||||
|
group = subparser.add_mutually_exclusive_group(required=False)
|
||||||
|
group.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="print more messages to console",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"-q",
|
||||||
|
"--quiet",
|
||||||
|
action="store_true",
|
||||||
|
help="do not print messages to console",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser_compress.add_argument(
|
||||||
|
"input_file",
|
||||||
|
metavar="INPUT",
|
||||||
|
help="the input OpenType font (.ttf or .otf)",
|
||||||
|
)
|
||||||
|
parser_decompress.add_argument(
|
||||||
|
"input_file",
|
||||||
|
metavar="INPUT",
|
||||||
|
help="the input WOFF2 font",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser_compress.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output-file",
|
||||||
|
metavar="OUTPUT",
|
||||||
|
help="the output WOFF2 font",
|
||||||
|
)
|
||||||
|
parser_decompress.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output-file",
|
||||||
|
metavar="OUTPUT",
|
||||||
|
help="the output OpenType font",
|
||||||
|
)
|
||||||
|
|
||||||
|
transform_group = parser_compress.add_argument_group()
|
||||||
|
transform_group.add_argument(
|
||||||
|
"--no-glyf-transform",
|
||||||
|
dest="transform_tables",
|
||||||
|
nargs=0,
|
||||||
|
action=_NoGlyfTransformAction,
|
||||||
|
help="Do not transform glyf (and loca) tables",
|
||||||
|
)
|
||||||
|
transform_group.add_argument(
|
||||||
|
"--hmtx-transform",
|
||||||
|
dest="transform_tables",
|
||||||
|
nargs=0,
|
||||||
|
action=_HmtxTransformAction,
|
||||||
|
help="Enable optional transformation for 'hmtx' table",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser_compress.set_defaults(
|
||||||
|
subcommand=compress,
|
||||||
|
transform_tables={"glyf", "loca"},
|
||||||
|
)
|
||||||
|
parser_decompress.set_defaults(subcommand=decompress)
|
||||||
|
|
||||||
|
options = vars(parser.parse_args(args))
|
||||||
|
|
||||||
|
subcommand = options.pop("subcommand", None)
|
||||||
|
if not subcommand:
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
quiet = options.pop("quiet")
|
||||||
|
verbose = options.pop("verbose")
|
||||||
|
configLogger(
|
||||||
|
level=("ERROR" if quiet else "DEBUG" if verbose else "INFO"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not options["output_file"]:
|
||||||
|
if subcommand is compress:
|
||||||
|
extension = ".woff2"
|
||||||
|
elif subcommand is decompress:
|
||||||
|
# choose .ttf/.otf file extension depending on sfntVersion
|
||||||
|
with open(options["input_file"], "rb") as f:
|
||||||
|
f.seek(4) # skip 'wOF2' signature
|
||||||
|
sfntVersion = f.read(4)
|
||||||
|
assert len(sfntVersion) == 4, "not enough data"
|
||||||
|
extension = ".otf" if sfntVersion == b"OTTO" else ".ttf"
|
||||||
|
else:
|
||||||
|
raise AssertionError(subcommand)
|
||||||
|
options["output_file"] = makeOutputFileName(
|
||||||
|
options["input_file"], outputDir=None, extension=extension
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subcommand(**options)
|
||||||
|
except TTLibError as e:
|
||||||
|
parser.error(e)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import doctest
|
sys.exit(main())
|
||||||
sys.exit(doctest.testmod().failed)
|
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from __future__ import print_function, division, absolute_import
|
|
||||||
from fontTools.misc.py23 import *
|
|
||||||
from fontTools.ttLib import TTFont
|
|
||||||
from fontTools.ttx import makeOutputFileName
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
if args is None:
|
|
||||||
args = sys.argv[1:]
|
|
||||||
if len(args) < 1:
|
|
||||||
print("One argument, the input filename, must be provided.", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
filename = args[0]
|
|
||||||
outfilename = makeOutputFileName(filename, outputDir=None, extension='.woff2')
|
|
||||||
|
|
||||||
print("Processing %s => %s" % (filename, outfilename))
|
|
||||||
|
|
||||||
font = TTFont(filename, recalcBBoxes=False, recalcTimestamp=False)
|
|
||||||
font.flavor = "woff2"
|
|
||||||
font.save(outfilename, reorderTables=False)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main())
|
|
@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from __future__ import print_function, division, absolute_import
|
|
||||||
from fontTools.misc.py23 import *
|
|
||||||
from fontTools.ttLib import TTFont
|
|
||||||
from fontTools.ttx import makeOutputFileName
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def make_output_name(filename):
|
|
||||||
with open(filename, "rb") as f:
|
|
||||||
f.seek(4)
|
|
||||||
sfntVersion = f.read(4)
|
|
||||||
assert len(sfntVersion) == 4, "not enough data"
|
|
||||||
ext = '.ttf' if sfntVersion == b"\x00\x01\x00\x00" else ".otf"
|
|
||||||
outfilename = makeOutputFileName(filename, outputDir=None, extension=ext)
|
|
||||||
return outfilename
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
if args is None:
|
|
||||||
args = sys.argv[1:]
|
|
||||||
if len(args) < 1:
|
|
||||||
print("One argument, the input filename, must be provided.", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
filename = args[0]
|
|
||||||
outfilename = make_output_name(filename)
|
|
||||||
|
|
||||||
print("Processing %s => %s" % (filename, outfilename))
|
|
||||||
|
|
||||||
font = TTFont(filename, recalcBBoxes=False, recalcTimestamp=False)
|
|
||||||
font.flavor = None
|
|
||||||
font.save(outfilename, reorderTables=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main())
|
|
@ -1,19 +1,24 @@
|
|||||||
from __future__ import print_function, division, absolute_import, unicode_literals
|
from __future__ import print_function, division, absolute_import, unicode_literals
|
||||||
from fontTools.misc.py23 import *
|
from fontTools.misc.py23 import *
|
||||||
from fontTools import ttLib
|
from fontTools import ttLib
|
||||||
|
from fontTools.ttLib import woff2
|
||||||
from fontTools.ttLib.woff2 import (
|
from fontTools.ttLib.woff2 import (
|
||||||
WOFF2Reader, woff2DirectorySize, woff2DirectoryFormat,
|
WOFF2Reader, woff2DirectorySize, woff2DirectoryFormat,
|
||||||
woff2FlagsSize, woff2UnknownTagSize, woff2Base128MaxSize, WOFF2DirectoryEntry,
|
woff2FlagsSize, woff2UnknownTagSize, woff2Base128MaxSize, WOFF2DirectoryEntry,
|
||||||
getKnownTagIndex, packBase128, base128Size, woff2UnknownTagIndex,
|
getKnownTagIndex, packBase128, base128Size, woff2UnknownTagIndex,
|
||||||
WOFF2FlavorData, woff2TransformedTableTags, WOFF2GlyfTable, WOFF2LocaTable,
|
WOFF2FlavorData, woff2TransformedTableTags, WOFF2GlyfTable, WOFF2LocaTable,
|
||||||
WOFF2Writer, unpackBase128, unpack255UShort, pack255UShort)
|
WOFF2HmtxTable, WOFF2Writer, unpackBase128, unpack255UShort, pack255UShort)
|
||||||
import unittest
|
import unittest
|
||||||
from fontTools.misc import sstruct
|
from fontTools.misc import sstruct
|
||||||
|
from fontTools import fontBuilder
|
||||||
|
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||||
import struct
|
import struct
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import copy
|
import copy
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from functools import partial
|
||||||
|
import pytest
|
||||||
|
|
||||||
haveBrotli = False
|
haveBrotli = False
|
||||||
try:
|
try:
|
||||||
@ -122,7 +127,7 @@ class WOFF2ReaderTest(unittest.TestCase):
|
|||||||
def test_reconstruct_unknown(self):
|
def test_reconstruct_unknown(self):
|
||||||
reader = WOFF2Reader(self.file)
|
reader = WOFF2Reader(self.file)
|
||||||
with self.assertRaisesRegex(ttLib.TTLibError, 'transform for table .* unknown'):
|
with self.assertRaisesRegex(ttLib.TTLibError, 'transform for table .* unknown'):
|
||||||
reader.reconstructTable('ZZZZ')
|
reader.reconstructTable('head')
|
||||||
|
|
||||||
|
|
||||||
class WOFF2ReaderTTFTest(WOFF2ReaderTest):
|
class WOFF2ReaderTTFTest(WOFF2ReaderTest):
|
||||||
@ -243,10 +248,6 @@ class WOFF2DirectoryEntryTest(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'tag'"):
|
with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'tag'"):
|
||||||
self.entry.fromString(bytes(incompleteData))
|
self.entry.fromString(bytes(incompleteData))
|
||||||
|
|
||||||
def test_table_reserved_flags(self):
|
|
||||||
with self.assertRaisesRegex(ttLib.TTLibError, "bits 6-7 are reserved"):
|
|
||||||
self.entry.fromString(bytechr(0xC0))
|
|
||||||
|
|
||||||
def test_loca_zero_transformLength(self):
|
def test_loca_zero_transformLength(self):
|
||||||
data = bytechr(getKnownTagIndex('loca')) # flags
|
data = bytechr(getKnownTagIndex('loca')) # flags
|
||||||
data += packBase128(random.randint(1, 100)) # origLength
|
data += packBase128(random.randint(1, 100)) # origLength
|
||||||
@ -292,6 +293,35 @@ class WOFF2DirectoryEntryTest(unittest.TestCase):
|
|||||||
data = self.entry.toString()
|
data = self.entry.toString()
|
||||||
self.assertEqual(len(data), expectedSize)
|
self.assertEqual(len(data), expectedSize)
|
||||||
|
|
||||||
|
def test_glyf_loca_transform_flags(self):
|
||||||
|
for tag in ("glyf", "loca"):
|
||||||
|
entry = WOFF2DirectoryEntry()
|
||||||
|
entry.tag = Tag(tag)
|
||||||
|
entry.flags = getKnownTagIndex(entry.tag)
|
||||||
|
|
||||||
|
self.assertEqual(entry.transformVersion, 0)
|
||||||
|
self.assertTrue(entry.transformed)
|
||||||
|
|
||||||
|
entry.transformed = False
|
||||||
|
|
||||||
|
self.assertEqual(entry.transformVersion, 3)
|
||||||
|
self.assertEqual(entry.flags & 0b11000000, (3 << 6))
|
||||||
|
self.assertFalse(entry.transformed)
|
||||||
|
|
||||||
|
def test_other_transform_flags(self):
|
||||||
|
entry = WOFF2DirectoryEntry()
|
||||||
|
entry.tag = Tag('ZZZZ')
|
||||||
|
entry.flags = woff2UnknownTagIndex
|
||||||
|
|
||||||
|
self.assertEqual(entry.transformVersion, 0)
|
||||||
|
self.assertFalse(entry.transformed)
|
||||||
|
|
||||||
|
entry.transformed = True
|
||||||
|
|
||||||
|
self.assertEqual(entry.transformVersion, 1)
|
||||||
|
self.assertEqual(entry.flags & 0b11000000, (1 << 6))
|
||||||
|
self.assertTrue(entry.transformed)
|
||||||
|
|
||||||
|
|
||||||
class DummyReader(WOFF2Reader):
|
class DummyReader(WOFF2Reader):
|
||||||
|
|
||||||
@ -300,6 +330,7 @@ class DummyReader(WOFF2Reader):
|
|||||||
for attr in ('majorVersion', 'minorVersion', 'metaOffset', 'metaLength',
|
for attr in ('majorVersion', 'minorVersion', 'metaOffset', 'metaLength',
|
||||||
'metaOrigLength', 'privLength', 'privOffset'):
|
'metaOrigLength', 'privLength', 'privOffset'):
|
||||||
setattr(self, attr, 0)
|
setattr(self, attr, 0)
|
||||||
|
self.tables = {}
|
||||||
|
|
||||||
|
|
||||||
class WOFF2FlavorDataTest(unittest.TestCase):
|
class WOFF2FlavorDataTest(unittest.TestCase):
|
||||||
@ -354,6 +385,24 @@ class WOFF2FlavorDataTest(unittest.TestCase):
|
|||||||
self.assertEqual(flavorData.majorVersion, 1)
|
self.assertEqual(flavorData.majorVersion, 1)
|
||||||
self.assertEqual(flavorData.minorVersion, 1)
|
self.assertEqual(flavorData.minorVersion, 1)
|
||||||
|
|
||||||
|
def test_mutually_exclusive_args(self):
|
||||||
|
reader = DummyReader(self.file)
|
||||||
|
with self.assertRaisesRegex(TypeError, "arguments are mutually exclusive"):
|
||||||
|
WOFF2FlavorData(reader, transformedTables={"hmtx"})
|
||||||
|
|
||||||
|
def test_transformTables_default(self):
|
||||||
|
flavorData = WOFF2FlavorData()
|
||||||
|
self.assertEqual(flavorData.transformedTables, set(woff2TransformedTableTags))
|
||||||
|
|
||||||
|
def test_transformTables_invalid(self):
|
||||||
|
msg = r"'glyf' and 'loca' must be transformed \(or not\) together"
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, msg):
|
||||||
|
WOFF2FlavorData(transformedTables={"glyf"})
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, msg):
|
||||||
|
WOFF2FlavorData(transformedTables={"loca"})
|
||||||
|
|
||||||
|
|
||||||
class WOFF2WriterTest(unittest.TestCase):
|
class WOFF2WriterTest(unittest.TestCase):
|
||||||
|
|
||||||
@ -512,6 +561,30 @@ class WOFF2WriterTest(unittest.TestCase):
|
|||||||
flavorData.majorVersion, flavorData.minorVersion = (10, 11)
|
flavorData.majorVersion, flavorData.minorVersion = (10, 11)
|
||||||
self.assertEqual((10, 11), self.writer._getVersion())
|
self.assertEqual((10, 11), self.writer._getVersion())
|
||||||
|
|
||||||
|
def test_hmtx_trasform(self):
|
||||||
|
tableTransforms = {"glyf", "loca", "hmtx"}
|
||||||
|
|
||||||
|
writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
|
||||||
|
writer.flavorData = WOFF2FlavorData(transformedTables=tableTransforms)
|
||||||
|
|
||||||
|
for tag in self.tags:
|
||||||
|
writer[tag] = self.font.getTableData(tag)
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
# enabling hmtx transform has no effect when font has no glyf table
|
||||||
|
self.assertEqual(writer.file.getvalue(), CFF_WOFF2.getvalue())
|
||||||
|
|
||||||
|
def test_no_transforms(self):
|
||||||
|
writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
|
||||||
|
writer.flavorData = WOFF2FlavorData(transformedTables=())
|
||||||
|
|
||||||
|
for tag in self.tags:
|
||||||
|
writer[tag] = self.font.getTableData(tag)
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
# transforms settings have no effect when font is CFF-flavored, since
|
||||||
|
# all the current transforms only apply to TrueType-flavored fonts.
|
||||||
|
self.assertEqual(writer.file.getvalue(), CFF_WOFF2.getvalue())
|
||||||
|
|
||||||
class WOFF2WriterTTFTest(WOFF2WriterTest):
|
class WOFF2WriterTTFTest(WOFF2WriterTest):
|
||||||
|
|
||||||
@ -540,6 +613,35 @@ class WOFF2WriterTTFTest(WOFF2WriterTest):
|
|||||||
for tag in normTables:
|
for tag in normTables:
|
||||||
self.assertEqual(self.writer.tables[tag].data, normTables[tag])
|
self.assertEqual(self.writer.tables[tag].data, normTables[tag])
|
||||||
|
|
||||||
|
def test_hmtx_trasform(self):
|
||||||
|
tableTransforms = {"glyf", "loca", "hmtx"}
|
||||||
|
|
||||||
|
writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
|
||||||
|
writer.flavorData = WOFF2FlavorData(transformedTables=tableTransforms)
|
||||||
|
|
||||||
|
for tag in self.tags:
|
||||||
|
writer[tag] = self.font.getTableData(tag)
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
length = len(writer.file.getvalue())
|
||||||
|
|
||||||
|
# enabling optional hmtx transform shaves off a few bytes
|
||||||
|
self.assertLess(length, len(TT_WOFF2.getvalue()))
|
||||||
|
|
||||||
|
def test_no_transforms(self):
|
||||||
|
writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
|
||||||
|
writer.flavorData = WOFF2FlavorData(transformedTables=())
|
||||||
|
|
||||||
|
for tag in self.tags:
|
||||||
|
writer[tag] = self.font.getTableData(tag)
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
self.assertNotEqual(writer.file.getvalue(), TT_WOFF2.getvalue())
|
||||||
|
|
||||||
|
writer.file.seek(0)
|
||||||
|
reader = WOFF2Reader(writer.file)
|
||||||
|
self.assertEqual(len(reader.flavorData.transformedTables), 0)
|
||||||
|
|
||||||
|
|
||||||
class WOFF2LocaTableTest(unittest.TestCase):
|
class WOFF2LocaTableTest(unittest.TestCase):
|
||||||
|
|
||||||
@ -709,28 +811,6 @@ class WOFF2GlyfTableTest(unittest.TestCase):
|
|||||||
data = glyfTable.transform(self.font)
|
data = glyfTable.transform(self.font)
|
||||||
self.assertEqual(self.transformedGlyfData, data)
|
self.assertEqual(self.transformedGlyfData, data)
|
||||||
|
|
||||||
def test_transform_glyf_incorrect_glyphOrder(self):
|
|
||||||
glyfTable = self.font['glyf']
|
|
||||||
badGlyphOrder = self.font.getGlyphOrder()[:-1]
|
|
||||||
del glyfTable.glyphOrder
|
|
||||||
self.font.setGlyphOrder(badGlyphOrder)
|
|
||||||
with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"):
|
|
||||||
glyfTable.transform(self.font)
|
|
||||||
glyfTable.glyphOrder = badGlyphOrder
|
|
||||||
with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"):
|
|
||||||
glyfTable.transform(self.font)
|
|
||||||
|
|
||||||
def test_transform_glyf_missing_glyphOrder(self):
|
|
||||||
glyfTable = self.font['glyf']
|
|
||||||
del glyfTable.glyphOrder
|
|
||||||
del self.font.glyphOrder
|
|
||||||
numGlyphs = self.font['maxp'].numGlyphs
|
|
||||||
del self.font['maxp']
|
|
||||||
glyfTable.transform(self.font)
|
|
||||||
expected = [".notdef"]
|
|
||||||
expected.extend(["glyph%.5d" % i for i in range(1, numGlyphs)])
|
|
||||||
self.assertEqual(expected, glyfTable.glyphOrder)
|
|
||||||
|
|
||||||
def test_roundtrip_glyf_reconstruct_and_transform(self):
|
def test_roundtrip_glyf_reconstruct_and_transform(self):
|
||||||
glyfTable = WOFF2GlyfTable()
|
glyfTable = WOFF2GlyfTable()
|
||||||
glyfTable.reconstruct(self.transformedGlyfData, self.font)
|
glyfTable.reconstruct(self.transformedGlyfData, self.font)
|
||||||
@ -748,6 +828,471 @@ class WOFF2GlyfTableTest(unittest.TestCase):
|
|||||||
self.assertEqual(normGlyfData, reconstructedData)
|
self.assertEqual(normGlyfData, reconstructedData)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def fontfile():
|
||||||
|
|
||||||
|
class Glyph(object):
|
||||||
|
def __init__(self, empty=False, **kwargs):
|
||||||
|
if not empty:
|
||||||
|
self.draw = partial(self.drawRect, **kwargs)
|
||||||
|
else:
|
||||||
|
self.draw = lambda pen: None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def drawRect(pen, xMin, xMax):
|
||||||
|
pen.moveTo((xMin, 0))
|
||||||
|
pen.lineTo((xMin, 1000))
|
||||||
|
pen.lineTo((xMax, 1000))
|
||||||
|
pen.lineTo((xMax, 0))
|
||||||
|
pen.closePath()
|
||||||
|
|
||||||
|
class CompositeGlyph(object):
|
||||||
|
def __init__(self, components):
|
||||||
|
self.components = components
|
||||||
|
|
||||||
|
def draw(self, pen):
|
||||||
|
for baseGlyph, (offsetX, offsetY) in self.components:
|
||||||
|
pen.addComponent(baseGlyph, (1, 0, 0, 1, offsetX, offsetY))
|
||||||
|
|
||||||
|
fb = fontBuilder.FontBuilder(unitsPerEm=1000, isTTF=True)
|
||||||
|
fb.setupGlyphOrder(
|
||||||
|
[".notdef", "space", "A", "acutecomb", "Aacute", "zero", "one", "two"]
|
||||||
|
)
|
||||||
|
fb.setupCharacterMap(
|
||||||
|
{
|
||||||
|
0x20: "space",
|
||||||
|
0x41: "A",
|
||||||
|
0x0301: "acutecomb",
|
||||||
|
0xC1: "Aacute",
|
||||||
|
0x30: "zero",
|
||||||
|
0x31: "one",
|
||||||
|
0x32: "two",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fb.setupHorizontalMetrics(
|
||||||
|
{
|
||||||
|
".notdef": (500, 50),
|
||||||
|
"space": (600, 0),
|
||||||
|
"A": (550, 40),
|
||||||
|
"acutecomb": (0, -40),
|
||||||
|
"Aacute": (550, 40),
|
||||||
|
"zero": (500, 30),
|
||||||
|
"one": (500, 50),
|
||||||
|
"two": (500, 40),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fb.setupHorizontalHeader(ascent=1000, descent=-200)
|
||||||
|
|
||||||
|
srcGlyphs = {
|
||||||
|
".notdef": Glyph(xMin=50, xMax=450),
|
||||||
|
"space": Glyph(empty=True),
|
||||||
|
"A": Glyph(xMin=40, xMax=510),
|
||||||
|
"acutecomb": Glyph(xMin=-40, xMax=60),
|
||||||
|
"Aacute": CompositeGlyph([("A", (0, 0)), ("acutecomb", (200, 0))]),
|
||||||
|
"zero": Glyph(xMin=30, xMax=470),
|
||||||
|
"one": Glyph(xMin=50, xMax=450),
|
||||||
|
"two": Glyph(xMin=40, xMax=460),
|
||||||
|
}
|
||||||
|
pen = TTGlyphPen(srcGlyphs)
|
||||||
|
glyphSet = {}
|
||||||
|
for glyphName, glyph in srcGlyphs.items():
|
||||||
|
glyph.draw(pen)
|
||||||
|
glyphSet[glyphName] = pen.glyph()
|
||||||
|
fb.setupGlyf(glyphSet)
|
||||||
|
|
||||||
|
fb.setupNameTable(
|
||||||
|
{
|
||||||
|
"familyName": "TestWOFF2",
|
||||||
|
"styleName": "Regular",
|
||||||
|
"uniqueFontIdentifier": "TestWOFF2 Regular; Version 1.000; ABCD",
|
||||||
|
"fullName": "TestWOFF2 Regular",
|
||||||
|
"version": "Version 1.000",
|
||||||
|
"psName": "TestWOFF2-Regular",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fb.setupOS2()
|
||||||
|
fb.setupPost()
|
||||||
|
|
||||||
|
buf = BytesIO()
|
||||||
|
fb.save(buf)
|
||||||
|
buf.seek(0)
|
||||||
|
|
||||||
|
assert fb.font["maxp"].numGlyphs == 8
|
||||||
|
assert fb.font["hhea"].numberOfHMetrics == 6
|
||||||
|
for glyphName in fb.font.getGlyphOrder():
|
||||||
|
xMin = getattr(fb.font["glyf"][glyphName], "xMin", 0)
|
||||||
|
assert xMin == fb.font["hmtx"][glyphName][1]
|
||||||
|
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ttFont(fontfile):
|
||||||
|
return ttLib.TTFont(fontfile, recalcBBoxes=False, recalcTimestamp=False)
|
||||||
|
|
||||||
|
|
||||||
|
class WOFF2HmtxTableTest(object):
|
||||||
|
def test_transform_no_sidebearings(self, ttFont):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
hmtxTable.metrics = ttFont["hmtx"].metrics
|
||||||
|
|
||||||
|
data = hmtxTable.transform(ttFont)
|
||||||
|
|
||||||
|
assert data == (
|
||||||
|
b"\x03" # 00000011 | bits 0 and 1 are set (no sidebearings arrays)
|
||||||
|
|
||||||
|
# advanceWidthArray
|
||||||
|
b'\x01\xf4' # .notdef: 500
|
||||||
|
b'\x02X' # space: 600
|
||||||
|
b'\x02&' # A: 550
|
||||||
|
b'\x00\x00' # acutecomb: 0
|
||||||
|
b'\x02&' # Aacute: 550
|
||||||
|
b'\x01\xf4' # zero: 500
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_transform_proportional_sidebearings(self, ttFont):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
metrics = ttFont["hmtx"].metrics
|
||||||
|
# force one of the proportional glyphs to have its left sidebearing be
|
||||||
|
# different from its xMin (40)
|
||||||
|
metrics["A"] = (550, 39)
|
||||||
|
hmtxTable.metrics = metrics
|
||||||
|
|
||||||
|
assert ttFont["glyf"]["A"].xMin != metrics["A"][1]
|
||||||
|
|
||||||
|
data = hmtxTable.transform(ttFont)
|
||||||
|
|
||||||
|
assert data == (
|
||||||
|
b"\x02" # 00000010 | bits 0 unset: explicit proportional sidebearings
|
||||||
|
|
||||||
|
# advanceWidthArray
|
||||||
|
b'\x01\xf4' # .notdef: 500
|
||||||
|
b'\x02X' # space: 600
|
||||||
|
b'\x02&' # A: 550
|
||||||
|
b'\x00\x00' # acutecomb: 0
|
||||||
|
b'\x02&' # Aacute: 550
|
||||||
|
b'\x01\xf4' # zero: 500
|
||||||
|
|
||||||
|
# lsbArray
|
||||||
|
b'\x002' # .notdef: 50
|
||||||
|
b'\x00\x00' # space: 0
|
||||||
|
b"\x00'" # A: 39 (xMin: 40)
|
||||||
|
b'\xff\xd8' # acutecomb: -40
|
||||||
|
b'\x00(' # Aacute: 40
|
||||||
|
b'\x00\x1e' # zero: 30
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_transform_monospaced_sidebearings(self, ttFont):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
metrics = ttFont["hmtx"].metrics
|
||||||
|
hmtxTable.metrics = metrics
|
||||||
|
|
||||||
|
# force one of the monospaced glyphs at the end of hmtx table to have
|
||||||
|
# its xMin different from its left sidebearing (50)
|
||||||
|
ttFont["glyf"]["one"].xMin = metrics["one"][1] + 1
|
||||||
|
|
||||||
|
data = hmtxTable.transform(ttFont)
|
||||||
|
|
||||||
|
assert data == (
|
||||||
|
b"\x01" # 00000001 | bits 1 unset: explicit monospaced sidebearings
|
||||||
|
|
||||||
|
# advanceWidthArray
|
||||||
|
b'\x01\xf4' # .notdef: 500
|
||||||
|
b'\x02X' # space: 600
|
||||||
|
b'\x02&' # A: 550
|
||||||
|
b'\x00\x00' # acutecomb: 0
|
||||||
|
b'\x02&' # Aacute: 550
|
||||||
|
b'\x01\xf4' # zero: 500
|
||||||
|
|
||||||
|
# leftSideBearingArray
|
||||||
|
b'\x002' # one: 50 (xMin: 51)
|
||||||
|
b'\x00(' # two: 40
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_transform_not_applicable(self, ttFont):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
metrics = ttFont["hmtx"].metrics
|
||||||
|
# force both a proportional and monospaced glyph to have sidebearings
|
||||||
|
# different from the respective xMin coordinates
|
||||||
|
metrics["A"] = (550, 39)
|
||||||
|
metrics["one"] = (500, 51)
|
||||||
|
hmtxTable.metrics = metrics
|
||||||
|
|
||||||
|
# 'None' signals to fall back using untransformed hmtx table data
|
||||||
|
assert hmtxTable.transform(ttFont) is None
|
||||||
|
|
||||||
|
def test_reconstruct_no_sidebearings(self, ttFont):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
|
||||||
|
data = (
|
||||||
|
b"\x03" # 00000011 | bits 0 and 1 are set (no sidebearings arrays)
|
||||||
|
|
||||||
|
# advanceWidthArray
|
||||||
|
b'\x01\xf4' # .notdef: 500
|
||||||
|
b'\x02X' # space: 600
|
||||||
|
b'\x02&' # A: 550
|
||||||
|
b'\x00\x00' # acutecomb: 0
|
||||||
|
b'\x02&' # Aacute: 550
|
||||||
|
b'\x01\xf4' # zero: 500
|
||||||
|
)
|
||||||
|
|
||||||
|
hmtxTable.reconstruct(data, ttFont)
|
||||||
|
|
||||||
|
assert hmtxTable.metrics == {
|
||||||
|
".notdef": (500, 50),
|
||||||
|
"space": (600, 0),
|
||||||
|
"A": (550, 40),
|
||||||
|
"acutecomb": (0, -40),
|
||||||
|
"Aacute": (550, 40),
|
||||||
|
"zero": (500, 30),
|
||||||
|
"one": (500, 50),
|
||||||
|
"two": (500, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_reconstruct_proportional_sidebearings(self, ttFont):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
|
||||||
|
data = (
|
||||||
|
b"\x02" # 00000010 | bits 0 unset: explicit proportional sidebearings
|
||||||
|
|
||||||
|
# advanceWidthArray
|
||||||
|
b'\x01\xf4' # .notdef: 500
|
||||||
|
b'\x02X' # space: 600
|
||||||
|
b'\x02&' # A: 550
|
||||||
|
b'\x00\x00' # acutecomb: 0
|
||||||
|
b'\x02&' # Aacute: 550
|
||||||
|
b'\x01\xf4' # zero: 500
|
||||||
|
|
||||||
|
# lsbArray
|
||||||
|
b'\x002' # .notdef: 50
|
||||||
|
b'\x00\x00' # space: 0
|
||||||
|
b"\x00'" # A: 39 (xMin: 40)
|
||||||
|
b'\xff\xd8' # acutecomb: -40
|
||||||
|
b'\x00(' # Aacute: 40
|
||||||
|
b'\x00\x1e' # zero: 30
|
||||||
|
)
|
||||||
|
|
||||||
|
hmtxTable.reconstruct(data, ttFont)
|
||||||
|
|
||||||
|
assert hmtxTable.metrics == {
|
||||||
|
".notdef": (500, 50),
|
||||||
|
"space": (600, 0),
|
||||||
|
"A": (550, 39),
|
||||||
|
"acutecomb": (0, -40),
|
||||||
|
"Aacute": (550, 40),
|
||||||
|
"zero": (500, 30),
|
||||||
|
"one": (500, 50),
|
||||||
|
"two": (500, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert ttFont["glyf"]["A"].xMin == 40
|
||||||
|
|
||||||
|
def test_reconstruct_monospaced_sidebearings(self, ttFont):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
|
||||||
|
data = (
|
||||||
|
b"\x01" # 00000001 | bits 1 unset: explicit monospaced sidebearings
|
||||||
|
|
||||||
|
# advanceWidthArray
|
||||||
|
b'\x01\xf4' # .notdef: 500
|
||||||
|
b'\x02X' # space: 600
|
||||||
|
b'\x02&' # A: 550
|
||||||
|
b'\x00\x00' # acutecomb: 0
|
||||||
|
b'\x02&' # Aacute: 550
|
||||||
|
b'\x01\xf4' # zero: 500
|
||||||
|
|
||||||
|
# leftSideBearingArray
|
||||||
|
b'\x003' # one: 51 (xMin: 50)
|
||||||
|
b'\x00(' # two: 40
|
||||||
|
)
|
||||||
|
|
||||||
|
hmtxTable.reconstruct(data, ttFont)
|
||||||
|
|
||||||
|
assert hmtxTable.metrics == {
|
||||||
|
".notdef": (500, 50),
|
||||||
|
"space": (600, 0),
|
||||||
|
"A": (550, 40),
|
||||||
|
"acutecomb": (0, -40),
|
||||||
|
"Aacute": (550, 40),
|
||||||
|
"zero": (500, 30),
|
||||||
|
"one": (500, 51),
|
||||||
|
"two": (500, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert ttFont["glyf"]["one"].xMin == 50
|
||||||
|
|
||||||
|
def test_reconstruct_flags_reserved_bits(self):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
ttLib.TTLibError, match="Bits 2-7 of 'hmtx' flags are reserved"
|
||||||
|
):
|
||||||
|
hmtxTable.reconstruct(b"\xFF", ttFont=None)
|
||||||
|
|
||||||
|
def test_reconstruct_flags_required_bits(self):
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
|
||||||
|
with pytest.raises(ttLib.TTLibError, match="either bits 0 or 1 .* must set"):
|
||||||
|
hmtxTable.reconstruct(b"\x00", ttFont=None)
|
||||||
|
|
||||||
|
def test_reconstruct_too_much_data(self, ttFont):
|
||||||
|
ttFont["hhea"].numberOfHMetrics = 2
|
||||||
|
data = b'\x03\x01\xf4\x02X\x02&'
|
||||||
|
hmtxTable = WOFF2HmtxTable()
|
||||||
|
|
||||||
|
with pytest.raises(ttLib.TTLibError, match="too much 'hmtx' table data"):
|
||||||
|
hmtxTable.reconstruct(data, ttFont)
|
||||||
|
|
||||||
|
|
||||||
|
class WOFF2RoundtripTest(object):
|
||||||
|
@staticmethod
|
||||||
|
def roundtrip(infile):
|
||||||
|
infile.seek(0)
|
||||||
|
ttFont = ttLib.TTFont(infile, recalcBBoxes=False, recalcTimestamp=False)
|
||||||
|
outfile = BytesIO()
|
||||||
|
ttFont.save(outfile)
|
||||||
|
return outfile, ttFont
|
||||||
|
|
||||||
|
def test_roundtrip_default_transforms(self, ttFont):
|
||||||
|
ttFont.flavor = "woff2"
|
||||||
|
# ttFont.flavorData = None
|
||||||
|
tmp = BytesIO()
|
||||||
|
ttFont.save(tmp)
|
||||||
|
|
||||||
|
tmp2, ttFont2 = self.roundtrip(tmp)
|
||||||
|
|
||||||
|
assert tmp.getvalue() == tmp2.getvalue()
|
||||||
|
assert ttFont2.reader.flavorData.transformedTables == {"glyf", "loca"}
|
||||||
|
|
||||||
|
def test_roundtrip_no_transforms(self, ttFont):
|
||||||
|
ttFont.flavor = "woff2"
|
||||||
|
ttFont.flavorData = WOFF2FlavorData(transformedTables=[])
|
||||||
|
tmp = BytesIO()
|
||||||
|
ttFont.save(tmp)
|
||||||
|
|
||||||
|
tmp2, ttFont2 = self.roundtrip(tmp)
|
||||||
|
|
||||||
|
assert tmp.getvalue() == tmp2.getvalue()
|
||||||
|
assert not ttFont2.reader.flavorData.transformedTables
|
||||||
|
|
||||||
|
def test_roundtrip_all_transforms(self, ttFont):
|
||||||
|
ttFont.flavor = "woff2"
|
||||||
|
ttFont.flavorData = WOFF2FlavorData(transformedTables=["glyf", "loca", "hmtx"])
|
||||||
|
tmp = BytesIO()
|
||||||
|
ttFont.save(tmp)
|
||||||
|
|
||||||
|
tmp2, ttFont2 = self.roundtrip(tmp)
|
||||||
|
|
||||||
|
assert tmp.getvalue() == tmp2.getvalue()
|
||||||
|
assert ttFont2.reader.flavorData.transformedTables == {"glyf", "loca", "hmtx"}
|
||||||
|
|
||||||
|
def test_roundtrip_only_hmtx_no_glyf_transform(self, ttFont):
|
||||||
|
ttFont.flavor = "woff2"
|
||||||
|
ttFont.flavorData = WOFF2FlavorData(transformedTables=["hmtx"])
|
||||||
|
tmp = BytesIO()
|
||||||
|
ttFont.save(tmp)
|
||||||
|
|
||||||
|
tmp2, ttFont2 = self.roundtrip(tmp)
|
||||||
|
|
||||||
|
assert tmp.getvalue() == tmp2.getvalue()
|
||||||
|
assert ttFont2.reader.flavorData.transformedTables == {"hmtx"}
|
||||||
|
|
||||||
|
|
||||||
|
class MainTest(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_ttf(tmpdir):
|
||||||
|
ttFont = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
|
||||||
|
ttFont.importXML(TTX)
|
||||||
|
filename = str(tmpdir / "TestTTF-Regular.ttf")
|
||||||
|
ttFont.save(filename)
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def test_compress_ttf(self, tmpdir):
|
||||||
|
input_file = self.make_ttf(tmpdir)
|
||||||
|
|
||||||
|
assert woff2.main(["compress", input_file]) is None
|
||||||
|
|
||||||
|
assert (tmpdir / "TestTTF-Regular.woff2").check(file=True)
|
||||||
|
|
||||||
|
def test_compress_ttf_no_glyf_transform(self, tmpdir):
|
||||||
|
input_file = self.make_ttf(tmpdir)
|
||||||
|
|
||||||
|
assert woff2.main(["compress", "--no-glyf-transform", input_file]) is None
|
||||||
|
|
||||||
|
assert (tmpdir / "TestTTF-Regular.woff2").check(file=True)
|
||||||
|
|
||||||
|
def test_compress_ttf_hmtx_transform(self, tmpdir):
|
||||||
|
input_file = self.make_ttf(tmpdir)
|
||||||
|
|
||||||
|
assert woff2.main(["compress", "--hmtx-transform", input_file]) is None
|
||||||
|
|
||||||
|
assert (tmpdir / "TestTTF-Regular.woff2").check(file=True)
|
||||||
|
|
||||||
|
def test_compress_ttf_no_glyf_transform_hmtx_transform(self, tmpdir):
|
||||||
|
input_file = self.make_ttf(tmpdir)
|
||||||
|
|
||||||
|
assert woff2.main(
|
||||||
|
["compress", "--no-glyf-transform", "--hmtx-transform", input_file]
|
||||||
|
) is None
|
||||||
|
|
||||||
|
assert (tmpdir / "TestTTF-Regular.woff2").check(file=True)
|
||||||
|
|
||||||
|
def test_compress_output_file(self, tmpdir):
|
||||||
|
input_file = self.make_ttf(tmpdir)
|
||||||
|
output_file = tmpdir / "TestTTF.woff2"
|
||||||
|
|
||||||
|
assert woff2.main(
|
||||||
|
["compress", "-o", str(output_file), str(input_file)]
|
||||||
|
) is None
|
||||||
|
|
||||||
|
assert output_file.check(file=True)
|
||||||
|
|
||||||
|
def test_compress_otf(self, tmpdir):
|
||||||
|
ttFont = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
|
||||||
|
ttFont.importXML(OTX)
|
||||||
|
input_file = str(tmpdir / "TestOTF-Regular.otf")
|
||||||
|
ttFont.save(input_file)
|
||||||
|
|
||||||
|
assert woff2.main(["compress", input_file]) is None
|
||||||
|
|
||||||
|
assert (tmpdir / "TestOTF-Regular.woff2").check(file=True)
|
||||||
|
|
||||||
|
def test_decompress_ttf(self, tmpdir):
|
||||||
|
input_file = tmpdir / "TestTTF-Regular.woff2"
|
||||||
|
input_file.write_binary(TT_WOFF2.getvalue())
|
||||||
|
|
||||||
|
assert woff2.main(["decompress", str(input_file)]) is None
|
||||||
|
|
||||||
|
assert (tmpdir / "TestTTF-Regular.ttf").check(file=True)
|
||||||
|
|
||||||
|
def test_decompress_otf(self, tmpdir):
|
||||||
|
input_file = tmpdir / "TestTTF-Regular.woff2"
|
||||||
|
input_file.write_binary(CFF_WOFF2.getvalue())
|
||||||
|
|
||||||
|
assert woff2.main(["decompress", str(input_file)]) is None
|
||||||
|
|
||||||
|
assert (tmpdir / "TestTTF-Regular.otf").check(file=True)
|
||||||
|
|
||||||
|
def test_decompress_output_file(self, tmpdir):
|
||||||
|
input_file = tmpdir / "TestTTF-Regular.woff2"
|
||||||
|
input_file.write_binary(TT_WOFF2.getvalue())
|
||||||
|
output_file = tmpdir / "TestTTF.ttf"
|
||||||
|
|
||||||
|
assert woff2.main(
|
||||||
|
["decompress", "-o", str(output_file), str(input_file)]
|
||||||
|
) is None
|
||||||
|
|
||||||
|
assert output_file.check(file=True)
|
||||||
|
|
||||||
|
def test_no_subcommand_show_help(self, capsys):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
woff2.main(["--help"])
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "usage: fonttools ttLib.woff2" in captured.out
|
||||||
|
|
||||||
|
|
||||||
class Base128Test(unittest.TestCase):
|
class Base128Test(unittest.TestCase):
|
||||||
|
|
||||||
def test_unpackBase128(self):
|
def test_unpackBase128(self):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user