woff2_test: add tests for transformed hmtx and untransformed glyf+loca

This commit is contained in:
Cosimo Lupo 2019-06-13 18:32:57 +01:00
parent ba41877d10
commit 8bd83f36bd
No known key found for this signature in database
GPG Key ID: 20D4A261E4A0E642

View File

@ -6,14 +6,18 @@ from fontTools.ttLib.woff2 import (
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:
@ -288,6 +292,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):
@ -351,6 +384,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):
@ -509,6 +560,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):
@ -537,6 +612,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):
@ -723,6 +827,376 @@ 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 Base128Test(unittest.TestCase): class Base128Test(unittest.TestCase):
def test_unpackBase128(self): def test_unpackBase128(self):