Merge pull request #2551 from fonttools/unlazy

add ensureDecompiled method to decompile all the tables irrespective of lazy attribute
This commit is contained in:
Cosimo Lupo 2022-03-18 10:55:48 +00:00 committed by GitHub
commit d7169774ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 7 deletions

View File

@ -156,6 +156,12 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
else:
seenOffsets[offset] = i
tables.append(table)
if ttFont.lazy is False: # Be lazy for None and True
self.ensureDecompiled()
def ensureDecompiled(self):
for st in self.tables:
st.ensureDecompiled()
def compile(self, ttFont):
self.tables.sort() # sort according to the spec; see CmapSubtable.__lt__()
@ -232,16 +238,21 @@ class CmapSubtable(object):
self.platEncID = None #: The encoding ID of this subtable (interpretation depends on ``platformID``)
self.language = None #: The language ID of this subtable (Macintosh platform only)
def ensureDecompiled(self):
if self.data is None:
return
self.decompile(None, None) # use saved data.
self.data = None # Once this table has been decompiled, make sure we don't
# just return the original data. Also avoids recursion when
# called with an attribute that the cmap subtable doesn't have.
def __getattr__(self, attr):
# allow lazy decompilation of subtables.
if attr[:2] == '__': # don't handle requests for member functions like '__lt__'
raise AttributeError(attr)
if self.data is None:
raise AttributeError(attr)
self.decompile(None, None) # use saved data.
self.data = None # Once this table has been decompiled, make sure we don't
# just return the original data. Also avoids recursion when
# called with an attribute that the cmap subtable doesn't have.
self.ensureDecompiled()
return getattr(self, attr)
def decompileHeader(self, data, ttFont):

View File

@ -110,8 +110,11 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
if noname:
log.warning('%s glyphs have no name', noname)
if ttFont.lazy is False: # Be lazy for None and True
for glyph in self.glyphs.values():
glyph.expand(self)
self.ensureDecompiled()
def ensureDecompiled(self):
for glyph in self.glyphs.values():
glyph.expand(self)
def compile(self, ttFont):
if not hasattr(self, "glyphOrder"):

View File

@ -4,6 +4,7 @@ import sys
import array
import struct
import logging
from typing import Iterator, NamedTuple, Optional
log = logging.getLogger(__name__)
@ -108,6 +109,9 @@ class BaseTTXConverter(DefaultTable):
self.table.fromXML(name, attrs, content, font)
self.table.populateDefaults()
def ensureDecompiled(self):
self.table.ensureDecompiled(recurse=True)
# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928
assert len(struct.pack('i', 0)) == 4
@ -596,13 +600,16 @@ class BaseTable(object):
raise AttributeError(attr)
def ensureDecompiled(self):
def ensureDecompiled(self, recurse=False):
reader = self.__dict__.get("reader")
if reader:
del self.reader
font = self.font
del self.font
self.decompile(reader, font)
if recurse:
for subtable in self.iterSubTables():
subtable.value.ensureDecompiled(recurse)
@classmethod
def getRecordSize(cls, reader):
@ -851,6 +858,37 @@ class BaseTable(object):
return self.__dict__ == other.__dict__
class SubTableEntry(NamedTuple):
"""See BaseTable.iterSubTables()"""
name: str
value: "BaseTable"
index: Optional[int] = None # index into given array, None for single values
def iterSubTables(self) -> Iterator[SubTableEntry]:
"""Yield (name, value, index) namedtuples for all subtables of current table.
A sub-table is an instance of BaseTable (or subclass thereof) that is a child
of self, the current parent table.
The tuples also contain the attribute name (str) of the of parent table to get
a subtable, and optionally, for lists of subtables (i.e. attributes associated
with a converter that has a 'repeat'), an index into the list containing the
given subtable value.
This method can be useful to traverse trees of otTables.
"""
for conv in self.getConverters():
name = conv.name
value = getattr(self, name, None)
if value is None:
continue
if isinstance(value, BaseTable):
yield self.SubTableEntry(name, value)
elif isinstance(value, list):
yield from (
self.SubTableEntry(name, v, index=i)
for i, v in enumerate(value)
if isinstance(v, BaseTable)
)
class FormatSwitchingBaseTable(BaseTable):
@ -862,6 +900,15 @@ class FormatSwitchingBaseTable(BaseTable):
return NotImplemented
def getConverters(self):
try:
fmt = self.Format
except AttributeError:
# some FormatSwitchingBaseTables (e.g. Coverage) no longer have 'Format'
# attribute after fully decompiled, only gain one in preWrite before being
# recompiled. In the decompiled state, these hand-coded classes defined in
# otTables.py lose their format-specific nature and gain more high-level
# attributes that are not tied to converters.
return []
return self.converters.get(self.Format, [])
def getConverterByName(self, name):

View File

@ -378,6 +378,14 @@ class TTFont(object):
keys = sortedTagList(keys)
return ["GlyphOrder"] + keys
def ensureDecompiled(self):
"""Decompile all the tables, even if a TTFont was opened in 'lazy' mode."""
for tag in self.keys():
table = self[tag]
if self.lazy is not False and hasattr(table, "ensureDecompiled"):
table.ensureDecompiled()
self.lazy = False
def __len__(self):
return len(list(self.keys()))

View File

@ -2,8 +2,12 @@ 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")
@ -137,3 +141,74 @@ def test_setGlyphOrder_also_updates_glyf_glyphOrder():
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__