woff2_test: add tests for transformed hmtx and untransformed glyf+loca
This commit is contained in:
parent
ba41877d10
commit
8bd83f36bd
@ -6,14 +6,18 @@ from fontTools.ttLib.woff2 import (
|
||||
woff2FlagsSize, woff2UnknownTagSize, woff2Base128MaxSize, WOFF2DirectoryEntry,
|
||||
getKnownTagIndex, packBase128, base128Size, woff2UnknownTagIndex,
|
||||
WOFF2FlavorData, woff2TransformedTableTags, WOFF2GlyfTable, WOFF2LocaTable,
|
||||
WOFF2Writer, unpackBase128, unpack255UShort, pack255UShort)
|
||||
WOFF2HmtxTable, WOFF2Writer, unpackBase128, unpack255UShort, pack255UShort)
|
||||
import unittest
|
||||
from fontTools.misc import sstruct
|
||||
from fontTools import fontBuilder
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
import struct
|
||||
import os
|
||||
import random
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
import pytest
|
||||
|
||||
haveBrotli = False
|
||||
try:
|
||||
@ -288,6 +292,35 @@ class WOFF2DirectoryEntryTest(unittest.TestCase):
|
||||
data = self.entry.toString()
|
||||
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):
|
||||
|
||||
@ -351,6 +384,24 @@ class WOFF2FlavorDataTest(unittest.TestCase):
|
||||
self.assertEqual(flavorData.majorVersion, 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):
|
||||
|
||||
@ -509,6 +560,30 @@ class WOFF2WriterTest(unittest.TestCase):
|
||||
flavorData.majorVersion, flavorData.minorVersion = (10, 11)
|
||||
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):
|
||||
|
||||
@ -537,6 +612,35 @@ class WOFF2WriterTTFTest(WOFF2WriterTest):
|
||||
for tag in normTables:
|
||||
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):
|
||||
|
||||
@ -723,6 +827,376 @@ class WOFF2GlyfTableTest(unittest.TestCase):
|
||||
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 Base128Test(unittest.TestCase):
|
||||
|
||||
def test_unpackBase128(self):
|
||||
|
Loading…
x
Reference in New Issue
Block a user