215 lines
6.6 KiB
Python
215 lines
6.6 KiB
Python
import io
|
|
import os
|
|
import re
|
|
import random
|
|
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
|
|
from fontTools.ttLib import TTFont, newTable, registerCustomTableClass, unregisterCustomTableClass
|
|
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
|
from fontTools.ttLib.tables._c_m_a_p import CmapSubtable
|
|
import pytest
|
|
|
|
|
|
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
|
|
|
|
|
|
class CustomTableClass(DefaultTable):
|
|
|
|
def decompile(self, data, ttFont):
|
|
self.numbers = list(data)
|
|
|
|
def compile(self, ttFont):
|
|
return bytes(self.numbers)
|
|
|
|
# not testing XML read/write
|
|
|
|
|
|
table_C_U_S_T_ = CustomTableClass # alias for testing
|
|
|
|
|
|
TABLETAG = "CUST"
|
|
|
|
|
|
def normalize_TTX(string):
|
|
string = re.sub(' ttLibVersion=".*"', "", string)
|
|
string = re.sub('checkSumAdjustment value=".*"', "", string)
|
|
string = re.sub('modified value=".*"', "", string)
|
|
return string
|
|
|
|
|
|
def test_registerCustomTableClass():
|
|
font = TTFont()
|
|
font[TABLETAG] = newTable(TABLETAG)
|
|
font[TABLETAG].data = b"\x00\x01\xff"
|
|
f = io.BytesIO()
|
|
font.save(f)
|
|
f.seek(0)
|
|
assert font[TABLETAG].data == b"\x00\x01\xff"
|
|
registerCustomTableClass(TABLETAG, "ttFont_test", "CustomTableClass")
|
|
try:
|
|
font = TTFont(f)
|
|
assert font[TABLETAG].numbers == [0, 1, 255]
|
|
assert font[TABLETAG].compile(font) == b"\x00\x01\xff"
|
|
finally:
|
|
unregisterCustomTableClass(TABLETAG)
|
|
|
|
|
|
def test_registerCustomTableClassStandardName():
|
|
registerCustomTableClass(TABLETAG, "ttFont_test")
|
|
try:
|
|
font = TTFont()
|
|
font[TABLETAG] = newTable(TABLETAG)
|
|
font[TABLETAG].numbers = [4, 5, 6]
|
|
assert font[TABLETAG].compile(font) == b"\x04\x05\x06"
|
|
finally:
|
|
unregisterCustomTableClass(TABLETAG)
|
|
|
|
|
|
ttxTTF = r"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.9.0">
|
|
<hmtx>
|
|
<mtx name=".notdef" width="300" lsb="0"/>
|
|
</hmtx>
|
|
</ttFont>
|
|
"""
|
|
|
|
|
|
ttxOTF = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<ttFont sfntVersion="OTTO" ttLibVersion="4.9.0">
|
|
<hmtx>
|
|
<mtx name=".notdef" width="300" lsb="0"/>
|
|
</hmtx>
|
|
</ttFont>
|
|
"""
|
|
|
|
|
|
def test_sfntVersionFromTTX():
|
|
# https://github.com/fonttools/fonttools/issues/2370
|
|
font = TTFont()
|
|
assert font.sfntVersion == "\x00\x01\x00\x00"
|
|
ttx = io.StringIO(ttxOTF)
|
|
# Font is "empty", TTX file will determine sfntVersion
|
|
font.importXML(ttx)
|
|
assert font.sfntVersion == "OTTO"
|
|
ttx = io.StringIO(ttxTTF)
|
|
# Font is not "empty", sfntVersion in TTX file will be ignored
|
|
font.importXML(ttx)
|
|
assert font.sfntVersion == "OTTO"
|
|
|
|
|
|
def test_virtualGlyphId():
|
|
otfpath = os.path.join(DATA_DIR, "TestVGID-Regular.otf")
|
|
ttxpath = os.path.join(DATA_DIR, "TestVGID-Regular.ttx")
|
|
|
|
otf = TTFont(otfpath)
|
|
|
|
ttx = TTFont()
|
|
ttx.importXML(ttxpath)
|
|
|
|
with open(ttxpath, encoding="utf-8") as fp:
|
|
xml = normalize_TTX(fp.read()).splitlines()
|
|
|
|
for font in (otf, ttx):
|
|
GSUB = font["GSUB"].table
|
|
assert GSUB.LookupList.LookupCount == 37
|
|
lookup = GSUB.LookupList.Lookup[32]
|
|
assert lookup.LookupType == 8
|
|
subtable = lookup.SubTable[0]
|
|
assert subtable.LookAheadGlyphCount == 1
|
|
lookahead = subtable.LookAheadCoverage[0]
|
|
assert len(lookahead.glyphs) == 46
|
|
assert "glyph00453" in lookahead.glyphs
|
|
|
|
out = io.StringIO()
|
|
font.saveXML(out)
|
|
outxml = normalize_TTX(out.getvalue()).splitlines()
|
|
assert xml == outxml
|
|
|
|
|
|
def test_setGlyphOrder_also_updates_glyf_glyphOrder():
|
|
# https://github.com/fonttools/fonttools/issues/2060#issuecomment-1063932428
|
|
font = TTFont()
|
|
font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx"))
|
|
current_order = font.getGlyphOrder()
|
|
|
|
assert current_order == font["glyf"].glyphOrder
|
|
|
|
new_order = list(current_order)
|
|
while new_order == current_order:
|
|
random.shuffle(new_order)
|
|
|
|
font.setGlyphOrder(new_order)
|
|
|
|
assert font.getGlyphOrder() == new_order
|
|
assert font["glyf"].glyphOrder == new_order
|
|
|
|
|
|
@pytest.mark.parametrize("lazy", [None, True, False])
|
|
def test_ensureDecompiled(lazy):
|
|
# test that no matter the lazy value, ensureDecompiled decompiles all tables
|
|
font = TTFont()
|
|
font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx"))
|
|
# test font has no OTL so we add some, as an example of otData-driven tables
|
|
addOpenTypeFeaturesFromString(
|
|
font,
|
|
"""
|
|
feature calt {
|
|
sub period' period' period' space by ellipsis;
|
|
} calt;
|
|
|
|
feature dist {
|
|
pos period period -30;
|
|
} dist;
|
|
"""
|
|
)
|
|
# also add an additional cmap subtable that will be lazily-loaded
|
|
cm = CmapSubtable.newSubtable(14)
|
|
cm.platformID = 0
|
|
cm.platEncID = 5
|
|
cm.language = 0
|
|
cm.cmap = {}
|
|
cm.uvsDict = {0xFE00: [(0x002e, None)]}
|
|
font["cmap"].tables.append(cm)
|
|
|
|
# save and reload, potentially lazily
|
|
buf = io.BytesIO()
|
|
font.save(buf)
|
|
buf.seek(0)
|
|
font = TTFont(buf, lazy=lazy)
|
|
|
|
# check no table is loaded until/unless requested, no matter the laziness
|
|
for tag in font.keys():
|
|
assert not font.isLoaded(tag)
|
|
|
|
if lazy is not False:
|
|
# additional cmap doesn't get decompiled automatically unless lazy=False;
|
|
# can't use hasattr or else cmap's maginc __getattr__ kicks in...
|
|
cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14)
|
|
assert cm.data is not None
|
|
assert "uvsDict" not in cm.__dict__
|
|
# glyf glyphs are not expanded unless lazy=False
|
|
assert font["glyf"].glyphs["period"].data is not None
|
|
assert not hasattr(font["glyf"].glyphs["period"], "coordinates")
|
|
|
|
if lazy is True:
|
|
# OTL tables hold a 'reader' to lazily load when lazy=True
|
|
assert "reader" in font["GSUB"].table.LookupList.__dict__
|
|
assert "reader" in font["GPOS"].table.LookupList.__dict__
|
|
|
|
font.ensureDecompiled()
|
|
|
|
# all tables are decompiled now
|
|
for tag in font.keys():
|
|
assert font.isLoaded(tag)
|
|
# including the additional cmap
|
|
cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14)
|
|
assert cm.data is None
|
|
assert "uvsDict" in cm.__dict__
|
|
# expanded glyf glyphs lost the 'data' attribute
|
|
assert not hasattr(font["glyf"].glyphs["period"], "data")
|
|
assert hasattr(font["glyf"].glyphs["period"], "coordinates")
|
|
# and OTL tables have read their 'reader'
|
|
assert "reader" not in font["GSUB"].table.LookupList.__dict__
|
|
assert "Lookup" in font["GSUB"].table.LookupList.__dict__
|
|
assert "reader" not in font["GPOS"].table.LookupList.__dict__
|
|
assert "Lookup" in font["GPOS"].table.LookupList.__dict__
|