diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index dd57a0507..16b7ee167 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -656,11 +656,7 @@ class FontBuilder(object): if validateGlyphFormat and self.font["head"].glyphDataFormat == 0: for name, g in glyphs.items(): - if g.isVarComposite(): - raise ValueError( - f"Glyph {name!r} is a variable composite, but glyphDataFormat=0" - ) - elif g.numberOfContours > 0 and any(f & flagCubic for f in g.flags): + if g.numberOfContours > 0 and any(f & flagCubic for f in g.flags): raise ValueError( f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; " "either convert to quadratics with cu2qu or set glyphDataFormat=1." diff --git a/Lib/fontTools/merge/tables.py b/Lib/fontTools/merge/tables.py index d132cb2a0..208a5099f 100644 --- a/Lib/fontTools/merge/tables.py +++ b/Lib/fontTools/merge/tables.py @@ -225,7 +225,7 @@ def merge(self, m, tables): g.removeHinting() # Expand composite glyphs to load their # composite glyph names. - if g.isComposite() or g.isVarComposite(): + if g.isComposite(): g.expand(table) return DefaultTable.merge(self, m, tables) diff --git a/Lib/fontTools/misc/iterTools.py b/Lib/fontTools/misc/iterTools.py new file mode 100644 index 000000000..d7b830532 --- /dev/null +++ b/Lib/fontTools/misc/iterTools.py @@ -0,0 +1,12 @@ +from itertools import * + +# Python 3.12: +if "batched" not in globals(): + # https://docs.python.org/3/library/itertools.html#itertools.batched + def batched(iterable, n): + # batched('ABCDEFG', 3) --> ABC DEF G + if n < 1: + raise ValueError("n must be at least one") + it = iter(iterable) + while batch := tuple(islice(it, n)): + yield batch diff --git a/Lib/fontTools/misc/lazyTools.py b/Lib/fontTools/misc/lazyTools.py new file mode 100644 index 000000000..91cb80c99 --- /dev/null +++ b/Lib/fontTools/misc/lazyTools.py @@ -0,0 +1,42 @@ +from collections import UserDict, UserList + +__all__ = ["LazyDict", "LazyList"] + + +class LazyDict(UserDict): + def __init__(self, data): + super().__init__() + self.data = data + + def __getitem__(self, k): + v = self.data[k] + if callable(v): + v = v(k) + self.data[k] = v + return v + + +class LazyList(UserList): + def __getitem__(self, k): + if isinstance(k, slice): + indices = range(*k.indices(len(self))) + return [self[i] for i in indices] + v = self.data[k] + if callable(v): + v = v(k) + self.data[k] = v + return v + + def __add__(self, other): + if isinstance(other, LazyList): + other = list(other) + elif isinstance(other, list): + pass + else: + return NotImplemented + return list(self) + other + + def __radd__(self, other): + if not isinstance(other, list): + return NotImplemented + return other + list(self) diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index 0f9f3a5d8..9025b79ec 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -422,6 +422,19 @@ class DecomposedTransform: tCenterX: float = 0 tCenterY: float = 0 + def __bool__(self): + return ( + self.translateX != 0 + or self.translateY != 0 + or self.rotation != 0 + or self.scaleX != 1 + or self.scaleY != 1 + or self.skewX != 0 + or self.skewY != 0 + or self.tCenterX != 0 + or self.tCenterY != 0 + ) + @classmethod def fromTransform(self, transform): # Adapted from an answer on diff --git a/Lib/fontTools/pens/svgPathPen.py b/Lib/fontTools/pens/svgPathPen.py index 29d41a802..8231603f8 100644 --- a/Lib/fontTools/pens/svgPathPen.py +++ b/Lib/fontTools/pens/svgPathPen.py @@ -2,7 +2,7 @@ from typing import Callable from fontTools.pens.basePen import BasePen -def pointToString(pt, ntos=str): +def pointToString(pt, ntos): return " ".join(ntos(i) for i in pt) @@ -37,7 +37,13 @@ class SVGPathPen(BasePen): print(tpen.getCommands()) """ - def __init__(self, glyphSet, ntos: Callable[[float], str] = str): + def __init__( + self, + glyphSet, + ntos: Callable[[float], str] = ( + lambda x: ("%.2f" % x) if x != int(x) else str(int(x)) + ), + ): BasePen.__init__(self, glyphSet) self._commands = [] self._lastCommand = None diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 7e6985db2..4aa60ad84 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -14,7 +14,7 @@ from fontTools.misc.cliTools import makeOutputFileName from fontTools.subset.util import _add_method, _uniq_sort from fontTools.subset.cff import * from fontTools.subset.svg import * -from fontTools.varLib import varStore # for subset_varidxes +from fontTools.varLib import varStore, multiVarStore # For monkey-patching from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor import sys import struct @@ -2630,6 +2630,88 @@ def closure_glyphs(self, s): s.glyphs.update(variants) +@_add_method(ttLib.getTableClass("VARC")) +def subset_glyphs(self, s): + indices = self.table.Coverage.subset(s.glyphs) + self.table.VarCompositeGlyphs.VarCompositeGlyph = _list_subset( + self.table.VarCompositeGlyphs.VarCompositeGlyph, indices + ) + return bool(self.table.VarCompositeGlyphs.VarCompositeGlyph) + + +@_add_method(ttLib.getTableClass("VARC")) +def closure_glyphs(self, s): + if self.table.VarCompositeGlyphs is None: + return + + glyphMap = {glyphName: i for i, glyphName in enumerate(self.table.Coverage.glyphs)} + glyphRecords = self.table.VarCompositeGlyphs.VarCompositeGlyph + + glyphs = s.glyphs + covered = set() + new = set(glyphs) + while new: + oldNew = new + new = set() + for glyphName in oldNew: + if glyphName in covered: + continue + idx = glyphMap.get(glyphName) + if idx is None: + continue + glyph = glyphRecords[idx] + for comp in glyph.components: + name = comp.glyphName + glyphs.add(name) + if name not in covered: + new.add(name) + + +@_add_method(ttLib.getTableClass("VARC")) +def prune_post_subset(self, font, options): + table = self.table + + store = table.MultiVarStore + if store is not None: + usedVarIdxes = set() + table.collect_varidxes(usedVarIdxes) + varidx_map = store.subset_varidxes(usedVarIdxes) + table.remap_varidxes(varidx_map) + + axisIndicesList = table.AxisIndicesList.Item + if axisIndicesList is not None: + usedIndices = set() + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.axisIndicesIndex is not None: + usedIndices.add(comp.axisIndicesIndex) + usedIndices = sorted(usedIndices) + table.AxisIndicesList.Item = _list_subset(axisIndicesList, usedIndices) + mapping = {old: new for new, old in enumerate(usedIndices)} + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.axisIndicesIndex is not None: + comp.axisIndicesIndex = mapping[comp.axisIndicesIndex] + + conditionList = table.ConditionList + if conditionList is not None: + conditionTables = conditionList.ConditionTable + usedIndices = set() + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.conditionIndex is not None: + usedIndices.add(comp.conditionIndex) + usedIndices = sorted(usedIndices) + conditionList.ConditionTable = _list_subset(conditionTables, usedIndices) + mapping = {old: new for new, old in enumerate(usedIndices)} + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.conditionIndex is not None: + comp.conditionIndex = mapping[comp.conditionIndex] + + return True + + @_add_method(ttLib.getTableClass("MATH")) def closure_glyphs(self, s): if self.table.MathVariants: @@ -3298,20 +3380,6 @@ class Subsetter(object): self.glyphs.add(font.getGlyphName(i)) log.info("Added first four glyphs to subset") - if self.options.layout_closure and "GSUB" in font: - with timer("close glyph list over 'GSUB'"): - log.info( - "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs) - ) - log.glyphs(self.glyphs, font=font) - font["GSUB"].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info( - "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs) - ) - log.glyphs(self.glyphs, font=font) - self.glyphs_gsubed = frozenset(self.glyphs) - if "MATH" in font: with timer("close glyph list over 'MATH'"): log.info( @@ -3326,6 +3394,20 @@ class Subsetter(object): log.glyphs(self.glyphs, font=font) self.glyphs_mathed = frozenset(self.glyphs) + if self.options.layout_closure and "GSUB" in font: + with timer("close glyph list over 'GSUB'"): + log.info( + "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs) + ) + log.glyphs(self.glyphs, font=font) + font["GSUB"].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info( + "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs) + ) + log.glyphs(self.glyphs, font=font) + self.glyphs_gsubed = frozenset(self.glyphs) + for table in ("COLR", "bsln"): if table in font: with timer("close glyph list over '%s'" % table): @@ -3345,6 +3427,20 @@ class Subsetter(object): log.glyphs(self.glyphs, font=font) setattr(self, f"glyphs_{table.lower()}ed", frozenset(self.glyphs)) + if "VARC" in font: + with timer("close glyph list over 'VARC'"): + log.info( + "Closing glyph list over 'VARC': %d glyphs before", len(self.glyphs) + ) + log.glyphs(self.glyphs, font=font) + font["VARC"].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info( + "Closed glyph list over 'VARC': %d glyphs after", len(self.glyphs) + ) + log.glyphs(self.glyphs, font=font) + self.glyphs_glyfed = frozenset(self.glyphs) + if "glyf" in font: with timer("close glyph list over 'glyf'"): log.info( diff --git a/Lib/fontTools/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index 2909bfcb2..68709825b 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -10,8 +10,10 @@ import fontTools.ttLib.tables.otTables as otTables from fontTools.cffLib import VarStoreData import fontTools.cffLib.specializer as cffSpecializer from fontTools.varLib import builder # for VarData.calculateNumShorts +from fontTools.varLib.multiVarStore import OnlineMultiVarStoreBuilder +from fontTools.misc.vector import Vector from fontTools.misc.fixedTools import otRound -from fontTools.ttLib.tables._g_l_y_f import VarComponentFlags +from fontTools.misc.iterTools import batched __all__ = ["scale_upem", "ScalerVisitor"] @@ -123,13 +125,6 @@ def visit(visitor, obj, attr, glyphs): component.y = visitor.scale(component.y) continue - if g.isVarComposite(): - for component in g.components: - for attr in ("translateX", "translateY", "tCenterX", "tCenterY"): - v = getattr(component.transform, attr) - setattr(component.transform, attr, visitor.scale(v)) - continue - if hasattr(g, "coordinates"): coordinates = g.coordinates for i, (x, y) in enumerate(coordinates): @@ -138,59 +133,107 @@ def visit(visitor, obj, attr, glyphs): @ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations") def visit(visitor, obj, attr, variations): - # VarComposites are a pain to handle :-( glyfTable = visitor.font["glyf"] for glyphName, varlist in variations.items(): glyph = glyfTable[glyphName] - isVarComposite = glyph.isVarComposite() for var in varlist: coordinates = var.coordinates - - if not isVarComposite: - for i, xy in enumerate(coordinates): - if xy is None: - continue - coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) - continue - - # VarComposite glyph - - i = 0 - for component in glyph.components: - if component.flags & VarComponentFlags.AXES_HAVE_VARIATION: - i += len(component.location) - if component.flags & ( - VarComponentFlags.HAVE_TRANSLATE_X - | VarComponentFlags.HAVE_TRANSLATE_Y - ): - xy = coordinates[i] - coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) - i += 1 - if component.flags & VarComponentFlags.HAVE_ROTATION: - i += 1 - if component.flags & ( - VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y - ): - i += 1 - if component.flags & ( - VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y - ): - i += 1 - if component.flags & ( - VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y - ): - xy = coordinates[i] - coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) - i += 1 - - # Phantom points - assert i + 4 == len(coordinates) - for i in range(i, len(coordinates)): - xy = coordinates[i] + for i, xy in enumerate(coordinates): + if xy is None: + continue coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) +@ScalerVisitor.register_attr(ttLib.getTableClass("VARC"), "table") +def visit(visitor, obj, attr, varc): + # VarComposite variations are a pain + + fvar = visitor.font["fvar"] + fvarAxes = [a.axisTag for a in fvar.axes] + + store = varc.MultiVarStore + storeBuilder = OnlineMultiVarStoreBuilder(fvarAxes) + + for g in varc.VarCompositeGlyphs.VarCompositeGlyph: + for component in g.components: + t = component.transform + t.translateX = visitor.scale(t.translateX) + t.translateY = visitor.scale(t.translateY) + t.tCenterX = visitor.scale(t.tCenterX) + t.tCenterY = visitor.scale(t.tCenterY) + + if component.axisValuesVarIndex != otTables.NO_VARIATION_INDEX: + varIdx = component.axisValuesVarIndex + # TODO Move this code duplicated below to MultiVarStore.__getitem__, + # or a getDeltasAndSupports(). + if varIdx != otTables.NO_VARIATION_INDEX: + major = varIdx >> 16 + minor = varIdx & 0xFFFF + varData = store.MultiVarData[major] + vec = varData.Item[minor] + storeBuilder.setSupports(store.get_supports(major, fvar.axes)) + if vec: + m = len(vec) // varData.VarRegionCount + vec = list(batched(vec, m)) + vec = [Vector(v) for v in vec] + component.axisValuesVarIndex = storeBuilder.storeDeltas(vec) + else: + component.axisValuesVarIndex = otTables.NO_VARIATION_INDEX + + if component.transformVarIndex != otTables.NO_VARIATION_INDEX: + varIdx = component.transformVarIndex + if varIdx != otTables.NO_VARIATION_INDEX: + major = varIdx >> 16 + minor = varIdx & 0xFFFF + vec = varData.Item[varIdx & 0xFFFF] + major = varIdx >> 16 + minor = varIdx & 0xFFFF + varData = store.MultiVarData[major] + vec = varData.Item[minor] + storeBuilder.setSupports(store.get_supports(major, fvar.axes)) + if vec: + m = len(vec) // varData.VarRegionCount + flags = component.flags + vec = list(batched(vec, m)) + newVec = [] + for v in vec: + v = list(v) + i = 0 + ## Scale translate & tCenter + if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_X: + v[i] = visitor.scale(v[i]) + i += 1 + if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_Y: + v[i] = visitor.scale(v[i]) + i += 1 + if flags & otTables.VarComponentFlags.HAVE_ROTATION: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_SCALE_X: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_SCALE_Y: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_SKEW_X: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_SKEW_Y: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_TCENTER_X: + v[i] = visitor.scale(v[i]) + i += 1 + if flags & otTables.VarComponentFlags.HAVE_TCENTER_Y: + v[i] = visitor.scale(v[i]) + i += 1 + + newVec.append(Vector(v)) + vec = newVec + + component.transformVarIndex = storeBuilder.storeDeltas(vec) + else: + component.transformVarIndex = otTables.NO_VARIATION_INDEX + + varc.MultiVarStore = storeBuilder.finish() + + @ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables") def visit(visitor, obj, attr, kernTables): for table in kernTables: diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py index 027ac1534..a98bca2e0 100644 --- a/Lib/fontTools/ttLib/tables/TupleVariation.py +++ b/Lib/fontTools/ttLib/tables/TupleVariation.py @@ -22,6 +22,8 @@ PRIVATE_POINT_NUMBERS = 0x2000 DELTAS_ARE_ZERO = 0x80 DELTAS_ARE_WORDS = 0x40 +DELTAS_ARE_LONGS = 0xC0 +DELTAS_SIZE_MASK = 0xC0 DELTA_RUN_COUNT_MASK = 0x3F POINTS_ARE_WORDS = 0x80 @@ -366,8 +368,10 @@ class TupleVariation(object): pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr) elif -128 <= value <= 127: pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) - else: + elif -32768 <= value <= 32767: pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr) + else: + pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr) return bytearr @staticmethod @@ -420,6 +424,7 @@ class TupleVariation(object): numDeltas = len(deltas) while pos < numDeltas: value = deltas[pos] + # Within a word-encoded run of deltas, it is easiest # to start a new run (with a different encoding) # whenever we encounter a zero value. For example, @@ -442,6 +447,10 @@ class TupleVariation(object): and (-128 <= deltas[pos + 1] <= 127) ): break + + if not (-32768 <= value <= 32767): + break + pos += 1 runLength = pos - offset while runLength >= 64: @@ -461,18 +470,47 @@ class TupleVariation(object): return pos @staticmethod - def decompileDeltas_(numDeltas, data, offset): + def encodeDeltaRunAsLongs_(deltas, offset, bytearr): + pos = offset + numDeltas = len(deltas) + while pos < numDeltas: + value = deltas[pos] + if -32768 <= value <= 32767: + break + pos += 1 + runLength = pos - offset + while runLength >= 64: + bytearr.append(DELTAS_ARE_LONGS | 63) + a = array.array("i", deltas[offset : offset + 64]) + if sys.byteorder != "big": + a.byteswap() + bytearr.extend(a) + offset += 64 + runLength -= 64 + if runLength: + bytearr.append(DELTAS_ARE_LONGS | (runLength - 1)) + a = array.array("i", deltas[offset:pos]) + if sys.byteorder != "big": + a.byteswap() + bytearr.extend(a) + return pos + + @staticmethod + def decompileDeltas_(numDeltas, data, offset=0): """(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)""" result = [] pos = offset - while len(result) < numDeltas: + while len(result) < numDeltas if numDeltas is not None else pos < len(data): runHeader = data[pos] pos += 1 numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1 - if (runHeader & DELTAS_ARE_ZERO) != 0: + if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_ZERO: result.extend([0] * numDeltasInRun) else: - if (runHeader & DELTAS_ARE_WORDS) != 0: + if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_LONGS: + deltas = array.array("i") + deltasSize = numDeltasInRun * 4 + elif (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_WORDS: deltas = array.array("h") deltasSize = numDeltasInRun * 2 else: @@ -481,10 +519,10 @@ class TupleVariation(object): deltas.frombytes(data[pos : pos + deltasSize]) if sys.byteorder != "big": deltas.byteswap() - assert len(deltas) == numDeltasInRun + assert len(deltas) == numDeltasInRun, (len(deltas), numDeltasInRun) pos += deltasSize result.extend(deltas) - assert len(result) == numDeltas + assert numDeltas is None or len(result) == numDeltas return (result, pos) @staticmethod diff --git a/Lib/fontTools/ttLib/tables/V_A_R_C_.py b/Lib/fontTools/ttLib/tables/V_A_R_C_.py new file mode 100644 index 000000000..5a0088716 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/V_A_R_C_.py @@ -0,0 +1,5 @@ +from .otBase import BaseTTXConverter + + +class table_V_A_R_C_(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 683912be9..fa11cf8f4 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -424,29 +424,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable): for c in glyph.components ], ) - elif glyph.isVarComposite(): - coords = [] - controls = [] - - for component in glyph.components: - ( - componentCoords, - componentControls, - ) = component.getCoordinatesAndControls() - coords.extend(componentCoords) - controls.extend(componentControls) - - coords = GlyphCoordinates(coords) - - controls = _GlyphControls( - numberOfContours=glyph.numberOfContours, - endPts=list(range(len(coords))), - flags=None, - components=[ - (c.glyphName, getattr(c, "flags", None)) for c in glyph.components - ], - ) - else: coords, endPts, flags = glyph.getCoordinates(self) coords = coords.copy() @@ -492,10 +469,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable): for p, comp in zip(coord, glyph.components): if hasattr(comp, "x"): comp.x, comp.y = p - elif glyph.isVarComposite(): - for comp in glyph.components: - coord = comp.setCoordinates(coord) - assert not coord elif glyph.numberOfContours == 0: assert len(coord) == 0 else: @@ -737,8 +710,6 @@ class Glyph(object): return if self.isComposite(): self.decompileComponents(data, glyfTable) - elif self.isVarComposite(): - self.decompileVarComponents(data, glyfTable) else: self.decompileCoordinates(data) @@ -758,8 +729,6 @@ class Glyph(object): data = sstruct.pack(glyphHeaderFormat, self) if self.isComposite(): data = data + self.compileComponents(glyfTable) - elif self.isVarComposite(): - data = data + self.compileVarComponents(glyfTable) else: data = data + self.compileCoordinates() return data @@ -769,10 +738,6 @@ class Glyph(object): for compo in self.components: compo.toXML(writer, ttFont) haveInstructions = hasattr(self, "program") - elif self.isVarComposite(): - for compo in self.components: - compo.toXML(writer, ttFont) - haveInstructions = False else: last = 0 for i in range(self.numberOfContours): @@ -842,15 +807,6 @@ class Glyph(object): component = GlyphComponent() self.components.append(component) component.fromXML(name, attrs, content, ttFont) - elif name == "varComponent": - if self.numberOfContours > 0: - raise ttLib.TTLibError("can't mix composites and contours in glyph") - self.numberOfContours = -2 - if not hasattr(self, "components"): - self.components = [] - component = GlyphVarComponent() - self.components.append(component) - component.fromXML(name, attrs, content, ttFont) elif name == "instructions": self.program = ttProgram.Program() for element in content: @@ -860,7 +816,7 @@ class Glyph(object): self.program.fromXML(name, attrs, content, ttFont) def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): - assert self.isComposite() or self.isVarComposite() + assert self.isComposite() nContours = 0 nPoints = 0 initialMaxComponentDepth = maxComponentDepth @@ -904,13 +860,6 @@ class Glyph(object): len(data), ) - def decompileVarComponents(self, data, glyfTable): - self.components = [] - while len(data) >= GlyphVarComponent.MIN_SIZE: - component = GlyphVarComponent() - data = component.decompile(data, glyfTable) - self.components.append(component) - def decompileCoordinates(self, data): endPtsOfContours = array.array("H") endPtsOfContours.frombytes(data[: 2 * self.numberOfContours]) @@ -1027,9 +976,6 @@ class Glyph(object): data = data + struct.pack(">h", len(instructions)) + instructions return data - def compileVarComponents(self, glyfTable): - return b"".join(c.compile(glyfTable) for c in self.components) - def compileCoordinates(self): assert len(self.coordinates) == len(self.flags) data = [] @@ -1231,13 +1177,6 @@ class Glyph(object): else: return self.numberOfContours == -1 - def isVarComposite(self): - """Test whether a glyph has variable components""" - if hasattr(self, "data"): - return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False - else: - return self.numberOfContours == -2 - def getCoordinates(self, glyfTable): """Return the coordinates, end points and flags @@ -1308,8 +1247,6 @@ class Glyph(object): allCoords.extend(coordinates) allFlags.extend(flags) return allCoords, allEndPts, allFlags - elif self.isVarComposite(): - raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs") else: return GlyphCoordinates(), [], bytearray() @@ -1319,12 +1256,8 @@ class Glyph(object): This method can be used on simple glyphs (in which case it returns an empty list) or composite glyphs. """ - if hasattr(self, "data") and self.isVarComposite(): - # TODO(VarComposite) Add implementation without expanding glyph - self.expand(glyfTable) - if not hasattr(self, "data"): - if self.isComposite() or self.isVarComposite(): + if self.isComposite(): return [c.glyphName for c in self.components] else: return [] @@ -1367,8 +1300,6 @@ class Glyph(object): if self.isComposite(): if hasattr(self, "program"): del self.program - elif self.isVarComposite(): - pass # Doesn't have hinting else: self.program = ttProgram.Program() self.program.fromBytecode([]) @@ -1450,13 +1381,6 @@ class Glyph(object): i += 2 + instructionLen # Remove padding data = data[:i] - elif self.isVarComposite(): - i = 0 - MIN_SIZE = GlyphVarComponent.MIN_SIZE - while len(data[i : i + MIN_SIZE]) >= MIN_SIZE: - size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE]) - i += size - data = data[:i] self.data = data @@ -1942,391 +1866,6 @@ class GlyphComponent(object): return result if result is NotImplemented else not result -# -# Variable Composite glyphs -# https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md -# - - -class VarComponentFlags(IntFlag): - USE_MY_METRICS = 0x0001 - AXIS_INDICES_ARE_SHORT = 0x0002 - UNIFORM_SCALE = 0x0004 - HAVE_TRANSLATE_X = 0x0008 - HAVE_TRANSLATE_Y = 0x0010 - HAVE_ROTATION = 0x0020 - HAVE_SCALE_X = 0x0040 - HAVE_SCALE_Y = 0x0080 - HAVE_SKEW_X = 0x0100 - HAVE_SKEW_Y = 0x0200 - HAVE_TCENTER_X = 0x0400 - HAVE_TCENTER_Y = 0x0800 - GID_IS_24BIT = 0x1000 - AXES_HAVE_VARIATION = 0x2000 - RESET_UNSPECIFIED_AXES = 0x4000 - - -VarComponentTransformMappingValues = namedtuple( - "VarComponentTransformMappingValues", - ["flag", "fractionalBits", "scale", "defaultValue"], -) - -VAR_COMPONENT_TRANSFORM_MAPPING = { - "translateX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 - ), - "translateY": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 - ), - "rotation": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_ROTATION, 12, 180, 0 - ), - "scaleX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_SCALE_X, 10, 1, 1 - ), - "scaleY": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1 - ), - "skewX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_SKEW_X, 12, -180, 0 - ), - "skewY": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0 - ), - "tCenterX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0 - ), - "tCenterY": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0 - ), -} - - -class GlyphVarComponent(object): - MIN_SIZE = 5 - - def __init__(self): - self.location = {} - self.transform = DecomposedTransform() - - @staticmethod - def getSize(data): - size = 5 - flags = struct.unpack(">H", data[:2])[0] - numAxes = int(data[2]) - - if flags & VarComponentFlags.GID_IS_24BIT: - size += 1 - - size += numAxes - if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: - size += 2 * numAxes - else: - axisIndices = array.array("B", data[:numAxes]) - size += numAxes - - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - if flags & mapping_values.flag: - size += 2 - - return size - - def decompile(self, data, glyfTable): - flags = struct.unpack(">H", data[:2])[0] - self.flags = int(flags) - data = data[2:] - - numAxes = int(data[0]) - data = data[1:] - - if flags & VarComponentFlags.GID_IS_24BIT: - glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0]) - data = data[3:] - flags ^= VarComponentFlags.GID_IS_24BIT - else: - glyphID = int(struct.unpack(">H", data[:2])[0]) - data = data[2:] - self.glyphName = glyfTable.getGlyphName(int(glyphID)) - - if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: - axisIndices = array.array("H", data[: 2 * numAxes]) - if sys.byteorder != "big": - axisIndices.byteswap() - data = data[2 * numAxes :] - flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT - else: - axisIndices = array.array("B", data[:numAxes]) - data = data[numAxes:] - assert len(axisIndices) == numAxes - axisIndices = list(axisIndices) - - axisValues = array.array("h", data[: 2 * numAxes]) - if sys.byteorder != "big": - axisValues.byteswap() - data = data[2 * numAxes :] - assert len(axisValues) == numAxes - axisValues = [fi2fl(v, 14) for v in axisValues] - - self.location = { - glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues) - } - - def read_transform_component(data, values): - if flags & values.flag: - return ( - data[2:], - fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits) - * values.scale, - ) - else: - return data, values.defaultValue - - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - data, value = read_transform_component(data, mapping_values) - setattr(self.transform, attr_name, value) - - if flags & VarComponentFlags.UNIFORM_SCALE: - if flags & VarComponentFlags.HAVE_SCALE_X and not ( - flags & VarComponentFlags.HAVE_SCALE_Y - ): - self.transform.scaleY = self.transform.scaleX - flags |= VarComponentFlags.HAVE_SCALE_Y - flags ^= VarComponentFlags.UNIFORM_SCALE - - return data - - def compile(self, glyfTable): - data = b"" - - if not hasattr(self, "flags"): - flags = 0 - # Calculate optimal transform component flags - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - value = getattr(self.transform, attr_name) - if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi( - mapping.defaultValue / mapping.scale, mapping.fractionalBits - ): - flags |= mapping.flag - else: - flags = self.flags - - if ( - flags & VarComponentFlags.HAVE_SCALE_X - and flags & VarComponentFlags.HAVE_SCALE_Y - and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10) - ): - flags |= VarComponentFlags.UNIFORM_SCALE - flags ^= VarComponentFlags.HAVE_SCALE_Y - - numAxes = len(self.location) - - data = data + struct.pack(">B", numAxes) - - glyphID = glyfTable.getGlyphID(self.glyphName) - if glyphID > 65535: - flags |= VarComponentFlags.GID_IS_24BIT - data = data + struct.pack(">L", glyphID)[1:] - else: - data = data + struct.pack(">H", glyphID) - - axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()] - if all(a <= 255 for a in axisIndices): - axisIndices = array.array("B", axisIndices) - else: - axisIndices = array.array("H", axisIndices) - if sys.byteorder != "big": - axisIndices.byteswap() - flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT - data = data + bytes(axisIndices) - - axisValues = self.location.values() - axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues)) - if sys.byteorder != "big": - axisValues.byteswap() - data = data + bytes(axisValues) - - def write_transform_component(data, value, values): - if flags & values.flag: - return data + struct.pack( - ">h", fl2fi(value / values.scale, values.fractionalBits) - ) - else: - return data - - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - value = getattr(self.transform, attr_name) - data = write_transform_component(data, value, mapping_values) - - return struct.pack(">H", flags) + data - - def toXML(self, writer, ttFont): - attrs = [("glyphName", self.glyphName)] - - if hasattr(self, "flags"): - attrs = attrs + [("flags", hex(self.flags))] - - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - v = getattr(self.transform, attr_name) - if v != mapping.defaultValue: - attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) - - writer.begintag("varComponent", attrs) - writer.newline() - - writer.begintag("location") - writer.newline() - for tag, v in self.location.items(): - writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))]) - writer.newline() - writer.endtag("location") - writer.newline() - - writer.endtag("varComponent") - writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - self.glyphName = attrs["glyphName"] - - if "flags" in attrs: - self.flags = safeEval(attrs["flags"]) - - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - if attr_name not in attrs: - continue - v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits) - setattr(self.transform, attr_name, v) - - for c in content: - if not isinstance(c, tuple): - continue - name, attrs, content = c - if name != "location": - continue - for c in content: - if not isinstance(c, tuple): - continue - name, attrs, content = c - assert name == "axis" - assert not content - self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14) - - def getPointCount(self): - assert hasattr(self, "flags"), "VarComponent with variations must have flags" - - count = 0 - - if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: - count += len(self.location) - - if self.flags & ( - VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y - ): - count += 1 - if self.flags & VarComponentFlags.HAVE_ROTATION: - count += 1 - if self.flags & ( - VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y - ): - count += 1 - if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): - count += 1 - if self.flags & ( - VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y - ): - count += 1 - - return count - - def getCoordinatesAndControls(self): - coords = [] - controls = [] - - if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: - for tag, v in self.location.items(): - controls.append(tag) - coords.append((fl2fi(v, 14), 0)) - - if self.flags & ( - VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y - ): - controls.append("translate") - coords.append((self.transform.translateX, self.transform.translateY)) - if self.flags & VarComponentFlags.HAVE_ROTATION: - controls.append("rotation") - coords.append((fl2fi(self.transform.rotation / 180, 12), 0)) - if self.flags & ( - VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y - ): - controls.append("scale") - coords.append( - (fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10)) - ) - if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): - controls.append("skew") - coords.append( - ( - fl2fi(self.transform.skewX / -180, 12), - fl2fi(self.transform.skewY / 180, 12), - ) - ) - if self.flags & ( - VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y - ): - controls.append("tCenter") - coords.append((self.transform.tCenterX, self.transform.tCenterY)) - - return coords, controls - - def setCoordinates(self, coords): - i = 0 - - if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: - newLocation = {} - for tag in self.location: - newLocation[tag] = fi2fl(coords[i][0], 14) - i += 1 - self.location = newLocation - - self.transform = DecomposedTransform() - if self.flags & ( - VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y - ): - self.transform.translateX, self.transform.translateY = coords[i] - i += 1 - if self.flags & VarComponentFlags.HAVE_ROTATION: - self.transform.rotation = fi2fl(coords[i][0], 12) * 180 - i += 1 - if self.flags & ( - VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y - ): - self.transform.scaleX, self.transform.scaleY = fi2fl( - coords[i][0], 10 - ), fi2fl(coords[i][1], 10) - i += 1 - if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): - self.transform.skewX, self.transform.skewY = ( - fi2fl(coords[i][0], 12) * -180, - fi2fl(coords[i][1], 12) * 180, - ) - i += 1 - if self.flags & ( - VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y - ): - self.transform.tCenterX, self.transform.tCenterY = coords[i] - i += 1 - - return coords[i:] - - def __eq__(self, other): - if type(self) != type(other): - return NotImplemented - return self.__dict__ == other.__dict__ - - def __ne__(self, other): - result = self.__eq__(other) - return result if result is NotImplemented else not result - - class GlyphCoordinates(object): """A list of glyph coordinates. diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index 11485bf09..044f65f71 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -1,7 +1,8 @@ -from collections import UserDict, deque +from collections import deque from functools import partial from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval +from fontTools.misc.lazyTools import LazyDict from . import DefaultTable import array import itertools @@ -39,19 +40,6 @@ GVAR_HEADER_FORMAT = """ GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT) -class _LazyDict(UserDict): - def __init__(self, data): - super().__init__() - self.data = data - - def __getitem__(self, k): - v = self.data[k] - if callable(v): - v = v() - self.data[k] = v - return v - - class table__g_v_a_r(DefaultTable.DefaultTable): dependencies = ["fvar", "glyf"] @@ -116,11 +104,6 @@ class table__g_v_a_r(DefaultTable.DefaultTable): sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self) assert len(glyphs) == self.glyphCount assert len(axisTags) == self.axisCount - offsets = self.decompileOffsets_( - data[GVAR_HEADER_SIZE:], - tableFormat=(self.flags & 1), - glyphCount=self.glyphCount, - ) sharedCoords = tv.decompileSharedTuples( axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples ) @@ -128,20 +111,35 @@ class table__g_v_a_r(DefaultTable.DefaultTable): offsetToData = self.offsetToGlyphVariationData glyf = ttFont["glyf"] - def decompileVarGlyph(glyphName, gid): - gvarData = data[ - offsetToData + offsets[gid] : offsetToData + offsets[gid + 1] - ] - if not gvarData: - return [] - glyph = glyf[glyphName] - numPointsInGlyph = self.getNumPoints_(glyph) - return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData) + def get_read_item(): + reverseGlyphMap = ttFont.getReverseGlyphMap() + tableFormat = self.flags & 1 - for gid in range(self.glyphCount): - glyphName = glyphs[gid] - variations[glyphName] = partial(decompileVarGlyph, glyphName, gid) - self.variations = _LazyDict(variations) + def read_item(glyphName): + gid = reverseGlyphMap[glyphName] + offsetSize = 2 if tableFormat == 0 else 4 + startOffset = GVAR_HEADER_SIZE + offsetSize * gid + endOffset = startOffset + offsetSize * 2 + offsets = table__g_v_a_r.decompileOffsets_( + data[startOffset:endOffset], + tableFormat=tableFormat, + glyphCount=1, + ) + gvarData = data[offsetToData + offsets[0] : offsetToData + offsets[1]] + if not gvarData: + return [] + glyph = glyf[glyphName] + numPointsInGlyph = self.getNumPoints_(glyph) + return decompileGlyph_( + numPointsInGlyph, sharedCoords, axisTags, gvarData + ) + + return read_item + + read_item = get_read_item() + l = LazyDict({glyphs[gid]: read_item for gid in range(self.glyphCount)}) + + self.variations = l if ttFont.lazy is False: # Be lazy for None and True self.ensureDecompiled() @@ -245,11 +243,6 @@ class table__g_v_a_r(DefaultTable.DefaultTable): if glyph.isComposite(): return len(glyph.components) + NUM_PHANTOM_POINTS - elif glyph.isVarComposite(): - count = 0 - for component in glyph.components: - count += component.getPointCount() - return count + NUM_PHANTOM_POINTS else: # Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute. return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS diff --git a/Lib/fontTools/ttLib/tables/_l_o_c_a.py b/Lib/fontTools/ttLib/tables/_l_o_c_a.py index 5884cef45..39c0c9e39 100644 --- a/Lib/fontTools/ttLib/tables/_l_o_c_a.py +++ b/Lib/fontTools/ttLib/tables/_l_o_c_a.py @@ -21,10 +21,7 @@ class table__l_o_c_a(DefaultTable.DefaultTable): if sys.byteorder != "big": locations.byteswap() if not longFormat: - l = array.array("I") - for i in range(len(locations)): - l.append(locations[i] * 2) - locations = l + locations = array.array("I", (2 * l for l in locations)) if len(locations) < (ttFont["maxp"].numGlyphs + 1): log.warning( "corrupt 'loca' table, or wrong numGlyphs in 'maxp': %d %d", diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py index 53abd13b4..8df7c236b 100644 --- a/Lib/fontTools/ttLib/tables/otBase.py +++ b/Lib/fontTools/ttLib/tables/otBase.py @@ -1146,7 +1146,10 @@ class BaseTable(object): except KeyError: raise # XXX on KeyError, raise nice error value = conv.xmlRead(attrs, content, font) - if conv.repeat: + # Some manually-written tables have a conv.repeat of "" + # to represent lists. Hence comparing to None here to + # allow those lists to be read correctly from XML. + if conv.repeat is not None: seq = getattr(self, conv.name, None) if seq is None: seq = [] diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index a2f672567..656836bd3 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -6,8 +6,10 @@ from fontTools.misc.fixedTools import ( ensureVersionIsLong as fi2ve, versionToFixed as ve2fi, ) +from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval +from fontTools.misc.lazyTools import LazyList from fontTools.ttLib import getSearchRange from .otBase import ( CountReference, @@ -18,6 +20,7 @@ from .otBase import ( ) from .otTables import ( lookupTypes, + VarCompositeGlyph, AATStateTable, AATState, AATAction, @@ -29,8 +32,9 @@ from .otTables import ( CompositeMode as _CompositeMode, NO_VARIATION_INDEX, ) -from itertools import zip_longest +from itertools import zip_longest, accumulate from functools import partial +from types import SimpleNamespace import re import struct from typing import Optional @@ -78,7 +82,7 @@ def buildConverters(tableSpec, tableNamespace): conv = converterClass(name, repeat, aux, description=descr) if conv.tableClass: - # A "template" such as OffsetTo(AType) knowss the table class already + # A "template" such as OffsetTo(AType) knows the table class already tableClass = conv.tableClass elif tp in ("MortChain", "MortSubtable", "MorxChain"): tableClass = tableNamespace.get(tp) @@ -105,46 +109,6 @@ def buildConverters(tableSpec, tableNamespace): return converters, convertersByName -class _MissingItem(tuple): - __slots__ = () - - -try: - from collections import UserList -except ImportError: - from UserList import UserList - - -class _LazyList(UserList): - def __getslice__(self, i, j): - return self.__getitem__(slice(i, j)) - - def __getitem__(self, k): - if isinstance(k, slice): - indices = range(*k.indices(len(self))) - return [self[i] for i in indices] - item = self.data[k] - if isinstance(item, _MissingItem): - self.reader.seek(self.pos + item[0] * self.recordSize) - item = self.conv.read(self.reader, self.font, {}) - self.data[k] = item - return item - - def __add__(self, other): - if isinstance(other, _LazyList): - other = list(other) - elif isinstance(other, list): - pass - else: - return NotImplemented - return list(self) + other - - def __radd__(self, other): - if not isinstance(other, list): - return NotImplemented - return other + list(self) - - class BaseConverter(object): """Base class for converter objects. Apart from the constructor, this is an abstract class.""" @@ -176,6 +140,7 @@ class BaseConverter(object): "AxisCount", "BaseGlyphRecordCount", "LayerRecordCount", + "AxisIndicesList", ] self.description = description @@ -192,14 +157,21 @@ class BaseConverter(object): l.append(self.read(reader, font, tableDict)) return l else: - l = _LazyList() - l.reader = reader.copy() - l.pos = l.reader.pos - l.font = font - l.conv = self - l.recordSize = recordSize - l.extend(_MissingItem([i]) for i in range(count)) + + def get_read_item(): + reader_copy = reader.copy() + pos = reader.pos + + def read_item(i): + reader_copy.seek(pos + i * recordSize) + return self.read(reader_copy, font, {}) + + return read_item + + read_item = get_read_item() + l = LazyList(read_item for i in range(count)) reader.advance(count * recordSize) + return l def getRecordSize(self, reader): @@ -1833,6 +1805,169 @@ class VarDataValue(BaseConverter): return safeEval(attrs["value"]) +class TupleValues: + def read(self, data, font): + return TupleVariation.decompileDeltas_(None, data)[0] + + def write(self, writer, font, tableDict, values, repeatIndex=None): + return bytes(TupleVariation.compileDeltaValues_(values)) + + def xmlRead(self, attrs, content, font): + return safeEval(attrs["value"]) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + xmlWriter.newline() + + +class CFF2Index(BaseConverter): + def __init__( + self, + name, + repeat, + aux, + tableClass=None, + *, + itemClass=None, + itemConverterClass=None, + description="", + ): + BaseConverter.__init__( + self, name, repeat, aux, tableClass, description=description + ) + self._itemClass = itemClass + self._converter = ( + itemConverterClass() if itemConverterClass is not None else None + ) + + def read(self, reader, font, tableDict): + count = reader.readULong() + if count == 0: + return [] + offSize = reader.readUInt8() + + def getReadArray(reader, offSize): + return { + 1: reader.readUInt8Array, + 2: reader.readUShortArray, + 3: reader.readUInt24Array, + 4: reader.readULongArray, + }[offSize] + + readArray = getReadArray(reader, offSize) + + lazy = font.lazy is not False and count > 8 + if not lazy: + offsets = readArray(count + 1) + items = [] + lastOffset = offsets.pop(0) + reader.readData(lastOffset - 1) # In case first offset is not 1 + + for offset in offsets: + assert lastOffset <= offset + item = reader.readData(offset - lastOffset) + + if self._itemClass is not None: + obj = self._itemClass() + obj.decompile(item, font, reader.localState) + item = obj + elif self._converter is not None: + item = self._converter.read(item, font) + + items.append(item) + lastOffset = offset + return items + else: + + def get_read_item(): + reader_copy = reader.copy() + offset_pos = reader.pos + data_pos = offset_pos + (count + 1) * offSize - 1 + readArray = getReadArray(reader_copy, offSize) + + def read_item(i): + reader_copy.seek(offset_pos + i * offSize) + offsets = readArray(2) + reader_copy.seek(data_pos + offsets[0]) + item = reader_copy.readData(offsets[1] - offsets[0]) + + if self._itemClass is not None: + obj = self._itemClass() + obj.decompile(item, font, reader_copy.localState) + item = obj + elif self._converter is not None: + item = self._converter.read(item, font) + return item + + return read_item + + read_item = get_read_item() + l = LazyList([read_item] * count) + + # TODO: Advance reader + + return l + + def write(self, writer, font, tableDict, values, repeatIndex=None): + items = values + + writer.writeULong(len(items)) + if not len(items): + return + + if self._itemClass is not None: + items = [item.compile(font) for item in items] + elif self._converter is not None: + items = [ + self._converter.write(writer, font, tableDict, item, i) + for i, item in enumerate(items) + ] + + offsets = [len(item) for item in items] + offsets = list(accumulate(offsets, initial=1)) + + lastOffset = offsets[-1] + offSize = ( + 1 + if lastOffset < 0x100 + else 2 if lastOffset < 0x10000 else 3 if lastOffset < 0x1000000 else 4 + ) + writer.writeUInt8(offSize) + + writeArray = { + 1: writer.writeUInt8Array, + 2: writer.writeUShortArray, + 3: writer.writeUInt24Array, + 4: writer.writeULongArray, + }[offSize] + + writeArray(offsets) + for item in items: + writer.writeData(item) + + def xmlRead(self, attrs, content, font): + if self._itemClass is not None: + obj = self._itemClass() + obj.fromXML(None, attrs, content, font) + return obj + elif self._converter is not None: + return self._converter.xmlRead(attrs, content, font) + else: + raise NotImplementedError() + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + if self._itemClass is not None: + for i, item in enumerate(value): + item.toXML(xmlWriter, font, [("index", i)], name) + elif self._converter is not None: + for i, item in enumerate(value): + self._converter.xmlWrite( + xmlWriter, font, item, name, attrs + [("index", i)] + ) + else: + raise NotImplementedError() + + class LookupFlag(UShort): def xmlWrite(self, xmlWriter, font, value, name, attrs): xmlWriter.simpletag(name, attrs + [("value", value)]) @@ -1910,6 +2045,8 @@ converterMapping = { "ExtendMode": ExtendMode, "CompositeMode": CompositeMode, "STATFlags": STATFlags, + "TupleList": partial(CFF2Index, itemConverterClass=TupleValues), + "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph), # AAT "CIDGlyphMap": CIDGlyphMap, "GlyphCIDMap": GlyphCIDMap, diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 56716824e..3a01f5934 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3168,6 +3168,25 @@ otData = [ ), ], ), + ( + "ConditionList", + [ + ( + "uint32", + "ConditionCount", + None, + None, + "Number of condition tables in the ConditionTable array", + ), + ( + "LOffset", + "ConditionTable", + "ConditionCount", + 0, + "Array of offset to condition tables, from the beginning of the ConditionList table.", + ), + ], + ), ( "ConditionSet", [ @@ -3183,7 +3202,7 @@ otData = [ "ConditionTable", "ConditionCount", 0, - "Array of condition tables.", + "Array of offset to condition tables, from the beginning of the ConditionSet table.", ), ], ), @@ -3214,6 +3233,79 @@ otData = [ ), ], ), + ( + "ConditionTableFormat2", + [ + ("uint16", "Format", None, None, "Format, = 2"), + ( + "int16", + "DefaultValue", + None, + None, + "Value at default instance.", + ), + ( + "uint32", + "VarIdx", + None, + None, + "Variation index to vary the value based on current designspace location.", + ), + ], + ), + ( + "ConditionTableFormat3", + [ + ("uint16", "Format", None, None, "Format, = 3"), + ( + "uint8", + "ConditionCount", + None, + None, + "Index for the variation axis within the fvar table, base 0.", + ), + ( + "Offset24", + "ConditionTable", + "ConditionCount", + 0, + "Array of condition tables for this conjunction (AND) expression.", + ), + ], + ), + ( + "ConditionTableFormat4", + [ + ("uint16", "Format", None, None, "Format, = 4"), + ( + "uint8", + "ConditionCount", + None, + None, + "Index for the variation axis within the fvar table, base 0.", + ), + ( + "Offset24", + "ConditionTable", + "ConditionCount", + 0, + "Array of condition tables for this disjunction (OR) expression.", + ), + ], + ), + ( + "ConditionTableFormat5", + [ + ("uint16", "Format", None, None, "Format, = 5"), + ( + "Offset24", + "ConditionTable", + None, + None, + "Condition to negate.", + ), + ], + ), ( "FeatureTableSubstitution", [ @@ -3322,6 +3414,78 @@ otData = [ ("VarIdxMapValue", "mapping", "", 0, "Array of compressed data"), ], ), + # MultiVariationStore + ( + "SparseVarRegionAxis", + [ + ("uint16", "AxisIndex", None, None, ""), + ("F2Dot14", "StartCoord", None, None, ""), + ("F2Dot14", "PeakCoord", None, None, ""), + ("F2Dot14", "EndCoord", None, None, ""), + ], + ), + ( + "SparseVarRegion", + [ + ("uint16", "SparseRegionCount", None, None, ""), + ("struct", "SparseVarRegionAxis", "SparseRegionCount", 0, ""), + ], + ), + ( + "SparseVarRegionList", + [ + ("uint16", "RegionCount", None, None, ""), + ("LOffsetTo(SparseVarRegion)", "Region", "RegionCount", 0, ""), + ], + ), + ( + "MultiVarData", + [ + ("uint8", "Format", None, None, "Set to 1."), + ("uint16", "VarRegionCount", None, None, ""), + ("uint16", "VarRegionIndex", "VarRegionCount", 0, ""), + ("TupleList", "Item", "", 0, ""), + ], + ), + ( + "MultiVarStore", + [ + ("uint16", "Format", None, None, "Set to 1."), + ("LOffset", "SparseVarRegionList", None, None, ""), + ("uint16", "MultiVarDataCount", None, None, ""), + ("LOffset", "MultiVarData", "MultiVarDataCount", 0, ""), + ], + ), + # VariableComposites + ( + "VARC", + [ + ( + "Version", + "Version", + None, + None, + "Version of the HVAR table-initially = 0x00010000", + ), + ("LOffset", "Coverage", None, None, ""), + ("LOffset", "MultiVarStore", None, None, "(may be NULL)"), + ("LOffset", "ConditionList", None, None, "(may be NULL)"), + ("LOffset", "AxisIndicesList", None, None, "(may be NULL)"), + ("LOffset", "VarCompositeGlyphs", None, None, ""), + ], + ), + ( + "AxisIndicesList", + [ + ("TupleList", "Item", "", 0, ""), + ], + ), + ( + "VarCompositeGlyphs", + [ + ("VarCompositeGlyphList", "VarCompositeGlyph", "", None, ""), + ], + ), # Glyph advance variations ( "HVAR", diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 879610a1e..bc7fbad91 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -11,11 +11,13 @@ from functools import reduce from math import radians import itertools from collections import defaultdict, namedtuple +from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables.otTraverse import dfs_base_table from fontTools.misc.arrayTools import quantizeRect from fontTools.misc.roundTools import otRound -from fontTools.misc.transform import Transform, Identity +from fontTools.misc.transform import Transform, Identity, DecomposedTransform from fontTools.misc.textTools import bytesjoin, pad, safeEval +from fontTools.misc.vector import Vector from fontTools.pens.boundsPen import ControlBoundsPen from fontTools.pens.transformPen import TransformPen from .otBase import ( @@ -25,9 +27,18 @@ from .otBase import ( CountReference, getFormatSwitchingBaseTableClass, ) +from fontTools.misc.fixedTools import ( + fixedToFloat as fi2fl, + floatToFixed as fl2fi, + floatToFixedToStr as fl2str, + strToFixedToFloat as str2fl, +) from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY import logging import struct +import array +import sys +from enum import IntFlag from typing import TYPE_CHECKING, Iterator, List, Optional, Set if TYPE_CHECKING: @@ -37,6 +48,389 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) +class VarComponentFlags(IntFlag): + RESET_UNSPECIFIED_AXES = 1 << 0 + + HAVE_AXES = 1 << 1 + + AXIS_VALUES_HAVE_VARIATION = 1 << 2 + TRANSFORM_HAS_VARIATION = 1 << 3 + + HAVE_TRANSLATE_X = 1 << 4 + HAVE_TRANSLATE_Y = 1 << 5 + HAVE_ROTATION = 1 << 6 + + HAVE_CONDITION = 1 << 7 + + HAVE_SCALE_X = 1 << 8 + HAVE_SCALE_Y = 1 << 9 + HAVE_TCENTER_X = 1 << 10 + HAVE_TCENTER_Y = 1 << 11 + + GID_IS_24BIT = 1 << 12 + + HAVE_SKEW_X = 1 << 13 + HAVE_SKEW_Y = 1 << 14 + + RESERVED_MASK = (1 << 32) - (1 << 15) + + +VarTransformMappingValues = namedtuple( + "VarTransformMappingValues", + ["flag", "fractionalBits", "scale", "defaultValue"], +) + +VAR_TRANSFORM_MAPPING = { + "translateX": VarTransformMappingValues( + VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 + ), + "translateY": VarTransformMappingValues( + VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 + ), + "rotation": VarTransformMappingValues(VarComponentFlags.HAVE_ROTATION, 12, 180, 0), + "scaleX": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_X, 10, 1, 1), + "scaleY": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1), + "skewX": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_X, 12, -180, 0), + "skewY": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0), + "tCenterX": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0), + "tCenterY": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0), +} + +# Probably should be somewhere in fontTools.misc +_packer = { + 1: lambda v: struct.pack(">B", v), + 2: lambda v: struct.pack(">H", v), + 3: lambda v: struct.pack(">L", v)[1:], + 4: lambda v: struct.pack(">L", v), +} +_unpacker = { + 1: lambda v: struct.unpack(">B", v)[0], + 2: lambda v: struct.unpack(">H", v)[0], + 3: lambda v: struct.unpack(">L", b"\0" + v)[0], + 4: lambda v: struct.unpack(">L", v)[0], +} + + +def _read_uint32var(data, i): + """Read a variable-length number from data starting at index i. + + Return the number and the next index. + """ + + b0 = data[i] + if b0 < 0x80: + return b0, i + 1 + elif b0 < 0xC0: + return (b0 - 0x80) << 8 | data[i + 1], i + 2 + elif b0 < 0xE0: + return (b0 - 0xC0) << 16 | data[i + 1] << 8 | data[i + 2], i + 3 + elif b0 < 0xF0: + return (b0 - 0xE0) << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[ + i + 3 + ], i + 4 + else: + return (b0 - 0xF0) << 32 | data[i + 1] << 24 | data[i + 2] << 16 | data[ + i + 3 + ] << 8 | data[i + 4], i + 5 + + +def _write_uint32var(v): + """Write a variable-length number. + + Return the data. + """ + if v < 0x80: + return struct.pack(">B", v) + elif v < 0x4000: + return struct.pack(">H", (v | 0x8000)) + elif v < 0x200000: + return struct.pack(">L", (v | 0xC00000))[1:] + elif v < 0x10000000: + return struct.pack(">L", (v | 0xE0000000)) + else: + return struct.pack(">B", 0xF0) + struct.pack(">L", v) + + +class VarComponent: + def __init__(self): + self.populateDefaults() + + def populateDefaults(self, propagator=None): + self.flags = 0 + self.glyphName = None + self.conditionIndex = None + self.axisIndicesIndex = None + self.axisValues = () + self.axisValuesVarIndex = NO_VARIATION_INDEX + self.transformVarIndex = NO_VARIATION_INDEX + self.transform = DecomposedTransform() + + def decompile(self, data, font, localState): + i = 0 + self.flags, i = _read_uint32var(data, i) + flags = self.flags + + gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2 + glyphID = _unpacker[gidSize](data[i : i + gidSize]) + i += gidSize + self.glyphName = font.glyphOrder[glyphID] + + if flags & VarComponentFlags.HAVE_CONDITION: + self.conditionIndex, i = _read_uint32var(data, i) + + if flags & VarComponentFlags.HAVE_AXES: + self.axisIndicesIndex, i = _read_uint32var(data, i) + else: + self.axisIndicesIndex = None + + if self.axisIndicesIndex is None: + numAxes = 0 + else: + axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex] + numAxes = len(axisIndices) + + if flags & VarComponentFlags.HAVE_AXES: + axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i) + self.axisValues = tuple(fi2fl(v, 14) for v in axisValues) + else: + self.axisValues = () + assert len(self.axisValues) == numAxes + + if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + self.axisValuesVarIndex, i = _read_uint32var(data, i) + else: + self.axisValuesVarIndex = NO_VARIATION_INDEX + if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: + self.transformVarIndex, i = _read_uint32var(data, i) + else: + self.transformVarIndex = NO_VARIATION_INDEX + + self.transform = DecomposedTransform() + + def read_transform_component(values): + nonlocal i + if flags & values.flag: + v = ( + fi2fl( + struct.unpack(">h", data[i : i + 2])[0], values.fractionalBits + ) + * values.scale + ) + i += 2 + return v + else: + return values.defaultValue + + for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): + value = read_transform_component(mapping_values) + setattr(self.transform, attr_name, value) + + if not (flags & VarComponentFlags.HAVE_SCALE_Y): + self.transform.scaleY = self.transform.scaleX + + n = flags & VarComponentFlags.RESERVED_MASK + while n: + _, i = _read_uint32var(data, i) + n &= n - 1 + + return data[i:] + + def compile(self, font): + data = [] + + flags = self.flags + + glyphID = font.getGlyphID(self.glyphName) + if glyphID > 65535: + flags |= VarComponentFlags.GID_IS_24BIT + data.append(_packer[3](glyphID)) + else: + flags &= ~VarComponentFlags.GID_IS_24BIT + data.append(_packer[2](glyphID)) + + if self.conditionIndex is not None: + flags |= VarComponentFlags.HAVE_CONDITION + data.append(_write_uint32var(self.conditionIndex)) + + numAxes = len(self.axisValues) + + if numAxes: + flags |= VarComponentFlags.HAVE_AXES + data.append(_write_uint32var(self.axisIndicesIndex)) + data.append( + TupleVariation.compileDeltaValues_( + [fl2fi(v, 14) for v in self.axisValues] + ) + ) + else: + flags &= ~VarComponentFlags.HAVE_AXES + + if self.axisValuesVarIndex != NO_VARIATION_INDEX: + flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + data.append(_write_uint32var(self.axisValuesVarIndex)) + else: + flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + if self.transformVarIndex != NO_VARIATION_INDEX: + flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION + data.append(_write_uint32var(self.transformVarIndex)) + else: + flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION + + def write_transform_component(value, values): + if flags & values.flag: + return struct.pack( + ">h", fl2fi(value / values.scale, values.fractionalBits) + ) + else: + return b"" + + for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): + value = getattr(self.transform, attr_name) + data.append(write_transform_component(value, mapping_values)) + + return _write_uint32var(flags) + bytesjoin(data) + + def toXML(self, writer, ttFont, attrs): + writer.begintag("VarComponent", attrs) + writer.newline() + + def write(name, value, attrs=()): + if value is not None: + writer.simpletag(name, (("value", value),) + attrs) + writer.newline() + + write("glyphName", self.glyphName) + + if self.conditionIndex is not None: + write("conditionIndex", self.conditionIndex) + if self.axisIndicesIndex is not None: + write("axisIndicesIndex", self.axisIndicesIndex) + if ( + self.axisIndicesIndex is not None + or self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES + ): + if self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES: + attrs = (("resetUnspecifiedAxes", 1),) + else: + attrs = () + write("axisValues", [float(fl2str(v, 14)) for v in self.axisValues], attrs) + + if self.axisValuesVarIndex != NO_VARIATION_INDEX: + write("axisValuesVarIndex", self.axisValuesVarIndex) + if self.transformVarIndex != NO_VARIATION_INDEX: + write("transformVarIndex", self.transformVarIndex) + + # Only write transform components that are specified in the + # flags, even if they are the default value. + for attr_name, mapping in VAR_TRANSFORM_MAPPING.items(): + if not (self.flags & mapping.flag): + continue + v = getattr(self.transform, attr_name) + write(attr_name, fl2str(v, mapping.fractionalBits)) + + writer.endtag("VarComponent") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + content = [c for c in content if isinstance(c, tuple)] + + self.populateDefaults() + + for name, attrs, content in content: + assert not content + v = attrs["value"] + + if name == "glyphName": + self.glyphName = v + elif name == "conditionIndex": + self.conditionIndex = safeEval(v) + elif name == "axisIndicesIndex": + self.axisIndicesIndex = safeEval(v) + elif name == "axisValues": + self.axisValues = tuple(str2fl(v, 14) for v in safeEval(v)) + if safeEval(attrs.get("resetUnspecifiedAxes", "0")): + self.flags |= VarComponentFlags.RESET_UNSPECIFIED_AXES + elif name == "axisValuesVarIndex": + self.axisValuesVarIndex = safeEval(v) + elif name == "transformVarIndex": + self.transformVarIndex = safeEval(v) + elif name in VAR_TRANSFORM_MAPPING: + setattr( + self.transform, + name, + safeEval(v), + ) + self.flags |= VAR_TRANSFORM_MAPPING[name].flag + else: + assert False, name + + def applyTransformDeltas(self, deltas): + i = 0 + + def read_transform_component_delta(values): + nonlocal i + if self.flags & values.flag: + v = fi2fl(deltas[i], values.fractionalBits) * values.scale + i += 1 + return v + else: + return 0 + + for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): + value = read_transform_component_delta(mapping_values) + setattr( + self.transform, attr_name, getattr(self.transform, attr_name) + value + ) + + if not (self.flags & VarComponentFlags.HAVE_SCALE_Y): + self.transform.scaleY = self.transform.scaleX + + assert i == len(deltas), (i, len(deltas)) + + def __eq__(self, other): + if type(self) != type(other): + return NotImplemented + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + result = self.__eq__(other) + return result if result is NotImplemented else not result + + +class VarCompositeGlyph: + def __init__(self, components=None): + self.components = components if components is not None else [] + + def decompile(self, data, font, localState): + self.components = [] + while data: + component = VarComponent() + data = component.decompile(data, font, localState) + self.components.append(component) + + def compile(self, font): + data = [] + for component in self.components: + data.append(component.compile(font)) + return bytesjoin(data) + + def toXML(self, xmlWriter, font, attrs, name): + xmlWriter.begintag("VarCompositeGlyph", attrs) + xmlWriter.newline() + for i, component in enumerate(self.components): + component.toXML(xmlWriter, font, [("index", i)]) + xmlWriter.endtag("VarCompositeGlyph") + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + content = [c for c in content if isinstance(c, tuple)] + for name, attrs, content in content: + assert name == "VarComponent" + component = VarComponent() + component.fromXML(name, attrs, content, font) + self.components.append(component) + + class AATStateTable(object): def __init__(self): self.GlyphClasses = {} # GlyphID --> GlyphClass diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py index 8a9f146b2..f4a539678 100644 --- a/Lib/fontTools/ttLib/ttFont.py +++ b/Lib/fontTools/ttLib/ttFont.py @@ -4,7 +4,12 @@ from fontTools.misc.configTools import AbstractConfig from fontTools.misc.textTools import Tag, byteord, tostr from fontTools.misc.loggingTools import deprecateArgument from fontTools.ttLib import TTLibError -from fontTools.ttLib.ttGlyphSet import _TTGlyph, _TTGlyphSetCFF, _TTGlyphSetGlyf +from fontTools.ttLib.ttGlyphSet import ( + _TTGlyph, + _TTGlyphSetCFF, + _TTGlyphSetGlyf, + _TTGlyphSetVARC, +) from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter from io import BytesIO, StringIO, UnsupportedOperation import os @@ -764,12 +769,16 @@ class TTFont(object): location = None if location and not normalized: location = self.normalizeLocation(location) + glyphSet = None if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self): - return _TTGlyphSetCFF(self, location) + glyphSet = _TTGlyphSetCFF(self, location) elif "glyf" in self: - return _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) + glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) else: raise TTLibError("Font contains no outlines") + if "VARC" in self: + glyphSet = _TTGlyphSetVARC(self, location, glyphSet) + return glyphSet def normalizeLocation(self, location): """Normalize a ``location`` from the font's defined axes space (also diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index b4beb3e76..446c81e7d 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -3,11 +3,12 @@ from abc import ABC, abstractmethod from collections.abc import Mapping from contextlib import contextmanager -from copy import copy +from copy import copy, deepcopy from types import SimpleNamespace -from fontTools.misc.fixedTools import otRound +from fontTools.misc.vector import Vector +from fontTools.misc.fixedTools import otRound, fixedToFloat as fi2fl from fontTools.misc.loggingTools import deprecateFunction -from fontTools.misc.transform import Transform +from fontTools.misc.transform import Transform, DecomposedTransform from fontTools.pens.transformPen import TransformPen, TransformPointPen from fontTools.pens.recordingPen import ( DecomposingRecordingPen, @@ -103,6 +104,16 @@ class _TTGlyphSetGlyf(_TTGlyphSet): return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) +class _TTGlyphSetGlyf(_TTGlyphSet): + def __init__(self, font, location, recalcBounds=True): + self.glyfTable = font["glyf"] + super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds) + self.gvarTable = font.get("gvar") + + def __getitem__(self, glyphName): + return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) + + class _TTGlyphSetCFF(_TTGlyphSet): def __init__(self, font, location): tableTag = "CFF2" if "CFF2" in font else "CFF " @@ -123,6 +134,19 @@ class _TTGlyphSetCFF(_TTGlyphSet): return _TTGlyphCFF(self, glyphName) +class _TTGlyphSetVARC(_TTGlyphSet): + def __init__(self, font, location, glyphSet): + self.glyphSet = glyphSet + super().__init__(font, location, glyphSet) + self.varcTable = font["VARC"].table + + def __getitem__(self, glyphName): + varc = self.varcTable + if glyphName not in varc.Coverage.glyphs: + return self.glyphSet[glyphName] + return _TTGlyphVARC(self, glyphName) + + class _TTGlyph(ABC): """Glyph object that supports the Pen protocol, meaning that it has .draw() and .drawPoints() methods that take a pen object as their only @@ -178,10 +202,6 @@ class _TTGlyphGlyf(_TTGlyph): if depth: offset = 0 # Offset should only apply at top-level - if glyph.isVarComposite(): - self._drawVarComposite(glyph, pen, False) - return - glyph.draw(pen, self.glyphSet.glyfTable, offset) def drawPoints(self, pen): @@ -194,35 +214,8 @@ class _TTGlyphGlyf(_TTGlyph): if depth: offset = 0 # Offset should only apply at top-level - if glyph.isVarComposite(): - self._drawVarComposite(glyph, pen, True) - return - glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) - def _drawVarComposite(self, glyph, pen, isPointPen): - from fontTools.ttLib.tables._g_l_y_f import ( - VarComponentFlags, - VAR_COMPONENT_TRANSFORM_MAPPING, - ) - - for comp in glyph.components: - with self.glyphSet.pushLocation( - comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES - ): - try: - pen.addVarComponent( - comp.glyphName, comp.transform, self.glyphSet.rawLocation - ) - except AttributeError: - t = comp.transform.toTransform() - if isPointPen: - tPen = TransformPointPen(pen, t) - self.glyphSet[comp.glyphName].drawPoints(tPen) - else: - tPen = TransformPen(pen, t) - self.glyphSet[comp.glyphName].draw(tPen) - def _getGlyphAndOffset(self): if self.glyphSet.location and self.glyphSet.gvarTable is not None: glyph = self._getGlyphInstance() @@ -283,6 +276,128 @@ class _TTGlyphCFF(_TTGlyph): self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) +def _evaluateCondition(condition, fvarAxes, location, instancer): + if condition.Format == 1: + # ConditionAxisRange + axisIndex = condition.AxisIndex + axisTag = fvarAxes[axisIndex].axisTag + axisValue = location.get(axisTag, 0) + minValue = condition.FilterRangeMinValue + maxValue = condition.FilterRangeMaxValue + return minValue <= axisValue <= maxValue + elif condition.Format == 2: + # ConditionValue + value = condition.DefaultValue + value += instancer[condition.VarIdx][0] + return value > 0 + elif condition.Format == 3: + # ConditionAnd + for subcondition in condition.ConditionTable: + if not _evaluateCondition(subcondition, fvarAxes, location, instancer): + return False + return True + elif condition.Format == 4: + # ConditionOr + for subcondition in condition.ConditionTable: + if _evaluateCondition(subcondition, fvarAxes, location, instancer): + return True + return False + elif condition.Format == 5: + # ConditionNegate + return not _evaluateCondition( + condition.conditionTable, fvarAxes, location, instancer + ) + else: + return False # Unkonwn condition format + + +class _TTGlyphVARC(_TTGlyph): + def _draw(self, pen, isPointPen): + """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details + how that works. + """ + from fontTools.ttLib.tables.otTables import ( + VarComponentFlags, + NO_VARIATION_INDEX, + ) + + glyphSet = self.glyphSet + varc = glyphSet.varcTable + idx = varc.Coverage.glyphs.index(self.name) + glyph = varc.VarCompositeGlyphs.VarCompositeGlyph[idx] + + from fontTools.varLib.multiVarStore import MultiVarStoreInstancer + from fontTools.varLib.varStore import VarStoreInstancer + + fvarAxes = glyphSet.font["fvar"].axes + instancer = MultiVarStoreInstancer( + varc.MultiVarStore, fvarAxes, self.glyphSet.location + ) + + for comp in glyph.components: + + if comp.flags & VarComponentFlags.HAVE_CONDITION: + condition = varc.ConditionList.ConditionTable[comp.conditionIndex] + if not _evaluateCondition( + condition, fvarAxes, self.glyphSet.location, instancer + ): + continue + + location = {} + if comp.axisIndicesIndex is not None: + axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] + axisValues = Vector(comp.axisValues) + if comp.axisValuesVarIndex != NO_VARIATION_INDEX: + axisValues += fi2fl(instancer[comp.axisValuesVarIndex], 14) + assert len(axisIndices) == len(axisValues), ( + len(axisIndices), + len(axisValues), + ) + location = { + fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues) + } + + if comp.transformVarIndex != NO_VARIATION_INDEX: + deltas = instancer[comp.transformVarIndex] + comp = deepcopy(comp) + comp.applyTransformDeltas(deltas) + transform = comp.transform + + reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES + with self.glyphSet.glyphSet.pushLocation(location, reset): + with self.glyphSet.pushLocation(location, reset): + shouldDecompose = self.name == comp.glyphName + + if not shouldDecompose: + try: + pen.addVarComponent( + comp.glyphName, transform, self.glyphSet.rawLocation + ) + except AttributeError: + shouldDecompose = True + + if shouldDecompose: + t = transform.toTransform() + compGlyphSet = ( + self.glyphSet + if comp.glyphName != self.name + else glyphSet.glyphSet + ) + g = compGlyphSet[comp.glyphName] + if isPointPen: + tPen = TransformPointPen(pen, t) + g.drawPoints(tPen) + else: + tPen = TransformPen(pen, t) + g.draw(tPen) + + def draw(self, pen): + self._draw(pen, False) + + def drawPoints(self, pen): + self._draw(pen, True) + + def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True): # Handle phantom points for (left, right, top, bottom) positions. assert len(coord) >= 4 @@ -300,11 +415,6 @@ def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True): for p, comp in zip(coord, glyph.components): if hasattr(comp, "x"): comp.x, comp.y = p - elif glyph.isVarComposite(): - glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy - for comp in glyph.components: - coord = comp.setCoordinates(coord) - assert not coord elif glyph.numberOfContours == 0: assert len(coord) == 0 else: diff --git a/Lib/fontTools/ttLib/woff2.py b/Lib/fontTools/ttLib/woff2.py index 9da2f7e6d..03667e834 100644 --- a/Lib/fontTools/ttLib/woff2.py +++ b/Lib/fontTools/ttLib/woff2.py @@ -1017,8 +1017,6 @@ class WOFF2GlyfTable(getTableClass("glyf")): return elif glyph.isComposite(): self._encodeComponents(glyph) - elif glyph.isVarComposite(): - raise NotImplementedError else: self._encodeCoordinates(glyph) self._encodeOverlapSimpleFlag(glyph, glyphID) diff --git a/Lib/fontTools/varLib/builder.py b/Lib/fontTools/varLib/builder.py index 94cc5bf06..456c34c4d 100644 --- a/Lib/fontTools/varLib/builder.py +++ b/Lib/fontTools/varLib/builder.py @@ -10,6 +10,13 @@ def buildVarRegionAxis(axisSupport): return self +def buildSparseVarRegionAxis(axisIndex, axisSupport): + self = ot.SparseVarRegionAxis() + self.AxisIndex = axisIndex + self.StartCoord, self.PeakCoord, self.EndCoord = [float(v) for v in axisSupport] + return self + + def buildVarRegion(support, axisTags): assert all(tag in axisTags for tag in support.keys()), ( "Unknown axis tag found.", @@ -23,6 +30,24 @@ def buildVarRegion(support, axisTags): return self +def buildSparseVarRegion(support, axisTags): + assert all(tag in axisTags for tag in support.keys()), ( + "Unknown axis tag found.", + support, + axisTags, + ) + self = ot.SparseVarRegion() + self.SparseVarRegionAxis = [] + for i, tag in enumerate(axisTags): + if tag not in support: + continue + self.SparseVarRegionAxis.append( + buildSparseVarRegionAxis(i, support.get(tag, (0, 0, 0))) + ) + self.SparseRegionCount = len(self.SparseVarRegionAxis) + return self + + def buildVarRegionList(supports, axisTags): self = ot.VarRegionList() self.RegionAxisCount = len(axisTags) @@ -33,6 +58,16 @@ def buildVarRegionList(supports, axisTags): return self +def buildSparseVarRegionList(supports, axisTags): + self = ot.SparseVarRegionList() + self.RegionAxisCount = len(axisTags) + self.Region = [] + for support in supports: + self.Region.append(buildSparseVarRegion(support, axisTags)) + self.RegionCount = len(self.Region) + return self + + def _reorderItem(lst, mapping): return [lst[i] for i in mapping] @@ -130,6 +165,29 @@ def buildVarStore(varRegionList, varDataList): return self +def buildMultiVarData(varRegionIndices, items): + self = ot.MultiVarData() + self.Format = 1 + self.VarRegionIndex = list(varRegionIndices) + regionCount = self.VarRegionCount = len(self.VarRegionIndex) + records = self.Item = [] + if items: + for item in items: + assert len(item) == regionCount + records.append(list(item)) + self.ItemCount = len(self.Item) + return self + + +def buildMultiVarStore(varRegionList, multiVarDataList): + self = ot.MultiVarStore() + self.Format = 1 + self.SparseVarRegionList = varRegionList + self.MultiVarData = list(multiVarDataList) + self.MultiVarDataCount = len(self.MultiVarData) + return self + + # Variation helpers diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index 7120b0831..f8c43187c 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -111,6 +111,7 @@ from fontTools.varLib.instancer import names from .featureVars import instantiateFeatureVariations from fontTools.misc.cliTools import makeOutputFileName from fontTools.varLib.instancer import solver +from fontTools.ttLib.tables.otTables import VarComponentFlags import collections import dataclasses from contextlib import contextmanager @@ -465,6 +466,42 @@ class OverlapMode(IntEnum): REMOVE_AND_IGNORE_ERRORS = 3 +def instantiateVARC(varfont, axisLimits): + log.info("Instantiating VARC tables") + + # TODO(behdad) My confidence in this function is rather low; + # It needs more testing. Specially with partial-instancing, + # I don't think it currently works. + + varc = varfont["VARC"].table + fvarAxes = varfont["fvar"].axes if "fvar" in varfont else [] + + location = axisLimits.pinnedLocation() + axisMap = [i for i, axis in enumerate(fvarAxes) if axis.axisTag not in location] + reverseAxisMap = {i: j for j, i in enumerate(axisMap)} + + if varc.AxisIndicesList: + axisIndicesList = varc.AxisIndicesList.Item + for i, axisIndices in enumerate(axisIndicesList): + if any(fvarAxes[j].axisTag in axisLimits for j in axisIndices): + raise NotImplementedError( + "Instancing across VarComponent axes is not supported." + ) + axisIndicesList[i] = [reverseAxisMap[j] for j in axisIndices] + + store = varc.MultiVarStore + if store: + for region in store.SparseVarRegionList.Region: + newRegionAxis = [] + for regionRecord in region.SparseVarRegionAxis: + tag = fvarAxes[regionRecord.AxisIndex].axisTag + if tag in axisLimits: + raise NotImplementedError( + "Instancing across VarComponent axes is not supported." + ) + regionRecord.AxisIndex = reverseAxisMap[regionRecord.AxisIndex] + + def instantiateTupleVariationStore( variations, axisLimits, origCoords=None, endPts=None ): @@ -843,23 +880,6 @@ def _instantiateGvarGlyph( if defaultDeltas: coordinates += _g_l_y_f.GlyphCoordinates(defaultDeltas) - glyph = glyf[glyphname] - if glyph.isVarComposite(): - for component in glyph.components: - newLocation = {} - for tag, loc in component.location.items(): - if tag not in axisLimits: - newLocation[tag] = loc - continue - if component.flags & _g_l_y_f.VarComponentFlags.AXES_HAVE_VARIATION: - raise NotImplementedError( - "Instancing accross VarComposite axes with variation is not supported." - ) - limits = axisLimits[tag] - loc = limits.renormalizeValue(loc, extrapolate=False) - newLocation[tag] = loc - component.location = newLocation - # _setCoordinates also sets the hmtx/vmtx advance widths and sidebearings from # the four phantom points and glyph bounding boxes. # We call it unconditionally even if a glyph has no variations or no deltas are @@ -910,7 +930,7 @@ def instantiateGvar(varfont, axisLimits, optimize=True): key=lambda name: ( ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() or glyf[name].isVarComposite() + if glyf[name].isComposite() else 0 ), name, @@ -1598,6 +1618,9 @@ def instantiateVariableFont( log.info("Updating name table") names.updateNameTable(varfont, axisLimits) + if "VARC" in varfont: + instantiateVARC(varfont, normalizedLimits) + if "CFF2" in varfont: instantiateCFF2(varfont, normalizedLimits, downgrade=downgradeCFF2) diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py index 59815316f..819596991 100644 --- a/Lib/fontTools/varLib/models.py +++ b/Lib/fontTools/varLib/models.py @@ -75,7 +75,7 @@ def normalizeValue(v, triple, extrapolate=False): return (v - default) / (upper - default) -def normalizeLocation(location, axes, extrapolate=False): +def normalizeLocation(location, axes, extrapolate=False, *, validate=False): """Normalizes location based on axis min/default/max values from axes. >>> axes = {"wght": (100, 400, 900)} @@ -114,6 +114,10 @@ def normalizeLocation(location, axes, extrapolate=False): >>> normalizeLocation({"wght": 1001}, axes) {'wght': 0.0} """ + if validate: + assert set(location.keys()) <= set(axes.keys()), set(location.keys()) - set( + axes.keys() + ) out = {} for tag, triple in axes.items(): v = location.get(tag, triple[1]) @@ -453,7 +457,10 @@ class VariationModel(object): self.deltaWeights.append(deltaWeight) def getDeltas(self, masterValues, *, round=noRound): - assert len(masterValues) == len(self.deltaWeights) + assert len(masterValues) == len(self.deltaWeights), ( + len(masterValues), + len(self.deltaWeights), + ) mapping = self.reverseMapping out = [] for i, weights in enumerate(self.deltaWeights): diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py new file mode 100644 index 000000000..f24a6e6f7 --- /dev/null +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -0,0 +1,253 @@ +from fontTools.misc.roundTools import noRound, otRound +from fontTools.misc.intTools import bit_count +from fontTools.misc.vector import Vector +from fontTools.ttLib.tables import otTables as ot +from fontTools.varLib.models import supportScalar +import fontTools.varLib.varStore # For monkey-patching +from fontTools.varLib.builder import ( + buildVarRegionList, + buildSparseVarRegionList, + buildSparseVarRegion, + buildMultiVarStore, + buildMultiVarData, +) +from fontTools.misc.iterTools import batched +from functools import partial +from collections import defaultdict +from heapq import heappush, heappop + + +NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX +ot.MultiVarStore.NO_VARIATION_INDEX = NO_VARIATION_INDEX + + +def _getLocationKey(loc): + return tuple(sorted(loc.items(), key=lambda kv: kv[0])) + + +class OnlineMultiVarStoreBuilder(object): + def __init__(self, axisTags): + self._axisTags = axisTags + self._regionMap = {} + self._regionList = buildSparseVarRegionList([], axisTags) + self._store = buildMultiVarStore(self._regionList, []) + self._data = None + self._model = None + self._supports = None + self._varDataIndices = {} + self._varDataCaches = {} + self._cache = None + + def setModel(self, model): + self.setSupports(model.supports) + self._model = model + + def setSupports(self, supports): + self._model = None + self._supports = list(supports) + if not self._supports[0]: + del self._supports[0] # Drop base master support + self._cache = None + self._data = None + + def finish(self, optimize=True): + self._regionList.RegionCount = len(self._regionList.Region) + self._store.MultiVarDataCount = len(self._store.MultiVarData) + return self._store + + def _add_MultiVarData(self): + regionMap = self._regionMap + regionList = self._regionList + + regions = self._supports + regionIndices = [] + for region in regions: + key = _getLocationKey(region) + idx = regionMap.get(key) + if idx is None: + varRegion = buildSparseVarRegion(region, self._axisTags) + idx = regionMap[key] = len(regionList.Region) + regionList.Region.append(varRegion) + regionIndices.append(idx) + + # Check if we have one already... + key = tuple(regionIndices) + varDataIdx = self._varDataIndices.get(key) + if varDataIdx is not None: + self._outer = varDataIdx + self._data = self._store.MultiVarData[varDataIdx] + self._cache = self._varDataCaches[key] + if len(self._data.Item) == 0xFFFF: + # This is full. Need new one. + varDataIdx = None + + if varDataIdx is None: + self._data = buildMultiVarData(regionIndices, []) + self._outer = len(self._store.MultiVarData) + self._store.MultiVarData.append(self._data) + self._varDataIndices[key] = self._outer + if key not in self._varDataCaches: + self._varDataCaches[key] = {} + self._cache = self._varDataCaches[key] + + def storeMasters(self, master_values, *, round=round): + deltas = self._model.getDeltas(master_values, round=round) + base = deltas.pop(0) + return base, self.storeDeltas(deltas, round=noRound) + + def storeDeltas(self, deltas, *, round=round): + deltas = tuple(round(d) for d in deltas) + + if not any(deltas): + return NO_VARIATION_INDEX + + deltas_tuple = tuple(tuple(d) for d in deltas) + + if not self._data: + self._add_MultiVarData() + + varIdx = self._cache.get(deltas_tuple) + if varIdx is not None: + return varIdx + + inner = len(self._data.Item) + if inner == 0xFFFF: + # Full array. Start new one. + self._add_MultiVarData() + return self.storeDeltas(deltas, round=noRound) + self._data.addItem(deltas, round=noRound) + + varIdx = (self._outer << 16) + inner + self._cache[deltas_tuple] = varIdx + return varIdx + + +def MultiVarData_addItem(self, deltas, *, round=round): + deltas = tuple(round(d) for d in deltas) + + assert len(deltas) == self.VarRegionCount + + values = [] + for d in deltas: + values.extend(d) + + self.Item.append(values) + self.ItemCount = len(self.Item) + + +ot.MultiVarData.addItem = MultiVarData_addItem + + +def SparseVarRegion_get_support(self, fvar_axes): + return { + fvar_axes[reg.AxisIndex].axisTag: (reg.StartCoord, reg.PeakCoord, reg.EndCoord) + for reg in self.SparseVarRegionAxis + } + + +ot.SparseVarRegion.get_support = SparseVarRegion_get_support + + +def MultiVarStore___bool__(self): + return bool(self.MultiVarData) + + +ot.MultiVarStore.__bool__ = MultiVarStore___bool__ + + +class MultiVarStoreInstancer(object): + def __init__(self, multivarstore, fvar_axes, location={}): + self.fvar_axes = fvar_axes + assert multivarstore is None or multivarstore.Format == 1 + self._varData = multivarstore.MultiVarData if multivarstore else [] + self._regions = ( + multivarstore.SparseVarRegionList.Region if multivarstore else [] + ) + self.setLocation(location) + + def setLocation(self, location): + self.location = dict(location) + self._clearCaches() + + def _clearCaches(self): + self._scalars = {} + + def _getScalar(self, regionIdx): + scalar = self._scalars.get(regionIdx) + if scalar is None: + support = self._regions[regionIdx].get_support(self.fvar_axes) + scalar = supportScalar(self.location, support) + self._scalars[regionIdx] = scalar + return scalar + + @staticmethod + def interpolateFromDeltasAndScalars(deltas, scalars): + if not deltas: + return Vector([]) + assert len(deltas) % len(scalars) == 0, (len(deltas), len(scalars)) + m = len(deltas) // len(scalars) + delta = Vector([0] * m) + for d, s in zip(batched(deltas, m), scalars): + if not s: + continue + delta += Vector(d) * s + return delta + + def __getitem__(self, varidx): + major, minor = varidx >> 16, varidx & 0xFFFF + if varidx == NO_VARIATION_INDEX: + return Vector([]) + varData = self._varData + scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex] + deltas = varData[major].Item[minor] + return self.interpolateFromDeltasAndScalars(deltas, scalars) + + def interpolateFromDeltas(self, varDataIndex, deltas): + varData = self._varData + scalars = [self._getScalar(ri) for ri in varData[varDataIndex].VarRegionIndex] + return self.interpolateFromDeltasAndScalars(deltas, scalars) + + +def MultiVarStore_subset_varidxes(self, varIdxes): + return ot.VarStore.subset_varidxes(self, varIdxes, VarData="MultiVarData") + + +def MultiVarStore_prune_regions(self): + return ot.VarStore.prune_regions( + self, VarData="MultiVarData", VarRegionList="SparseVarRegionList" + ) + + +ot.MultiVarStore.prune_regions = MultiVarStore_prune_regions +ot.MultiVarStore.subset_varidxes = MultiVarStore_subset_varidxes + + +def MultiVarStore_get_supports(self, major, fvarAxes): + supports = [] + varData = self.MultiVarData[major] + for regionIdx in varData.VarRegionIndex: + region = self.SparseVarRegionList.Region[regionIdx] + support = region.get_support(fvarAxes) + supports.append(support) + return supports + + +ot.MultiVarStore.get_supports = MultiVarStore_get_supports + + +def VARC_collect_varidxes(self, varidxes): + for glyph in self.VarCompositeGlyphs.VarCompositeGlyph: + for component in glyph.components: + varidxes.add(component.axisValuesVarIndex) + varidxes.add(component.transformVarIndex) + + +def VARC_remap_varidxes(self, varidxes_map): + for glyph in self.VarCompositeGlyphs.VarCompositeGlyph: + for component in glyph.components: + component.axisValuesVarIndex = varidxes_map[component.axisValuesVarIndex] + component.transformVarIndex = varidxes_map[component.transformVarIndex] + + +ot.VARC.collect_varidxes = VARC_collect_varidxes +ot.VARC.remap_varidxes = VARC_remap_varidxes diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py index c7c37dabc..6c327f945 100644 --- a/Lib/fontTools/varLib/mutator.py +++ b/Lib/fontTools/varLib/mutator.py @@ -201,7 +201,7 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True): key=lambda name: ( ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() or glyf[name].isVarComposite() + if glyf[name].isComposite() else 0 ), name, diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py index 780576907..f54fad2db 100644 --- a/Lib/fontTools/varLib/varStore.py +++ b/Lib/fontTools/varLib/varStore.py @@ -32,7 +32,7 @@ class OnlineVarStoreBuilder(object): self._supports = None self._varDataIndices = {} self._varDataCaches = {} - self._cache = {} + self._cache = None def setModel(self, model): self.setSupports(model.supports) @@ -43,7 +43,7 @@ class OnlineVarStoreBuilder(object): self._supports = list(supports) if not self._supports[0]: del self._supports[0] # Drop base master support - self._cache = {} + self._cache = None self._data = None def finish(self, optimize=True): @@ -54,7 +54,7 @@ class OnlineVarStoreBuilder(object): data.calculateNumShorts(optimize=optimize) return self._store - def _add_VarData(self): + def _add_VarData(self, num_items=1): regionMap = self._regionMap regionList = self._regionList @@ -76,7 +76,7 @@ class OnlineVarStoreBuilder(object): self._outer = varDataIdx self._data = self._store.VarData[varDataIdx] self._cache = self._varDataCaches[key] - if len(self._data.Item) == 0xFFFF: + if len(self._data.Item) + num_items > 0xFFFF: # This is full. Need new one. varDataIdx = None @@ -94,6 +94,14 @@ class OnlineVarStoreBuilder(object): base = deltas.pop(0) return base, self.storeDeltas(deltas, round=noRound) + def storeMastersMany(self, master_values_list, *, round=round): + deltas_list = [ + self._model.getDeltas(master_values, round=round) + for master_values in master_values_list + ] + base_list = [deltas.pop(0) for deltas in deltas_list] + return base_list, self.storeDeltasMany(deltas_list, round=noRound) + def storeDeltas(self, deltas, *, round=round): deltas = [round(d) for d in deltas] if len(deltas) == len(self._supports) + 1: @@ -102,23 +110,51 @@ class OnlineVarStoreBuilder(object): assert len(deltas) == len(self._supports) deltas = tuple(deltas) + if not self._data: + self._add_VarData() + varIdx = self._cache.get(deltas) if varIdx is not None: return varIdx - if not self._data: - self._add_VarData() inner = len(self._data.Item) if inner == 0xFFFF: # Full array. Start new one. self._add_VarData() - return self.storeDeltas(deltas) + return self.storeDeltas(deltas, round=noRound) self._data.addItem(deltas, round=noRound) varIdx = (self._outer << 16) + inner self._cache[deltas] = varIdx return varIdx + def storeDeltasMany(self, deltas_list, *, round=round): + deltas_list = [[round(d) for d in deltas] for deltas in deltas_list] + deltas_list = tuple(tuple(deltas) for deltas in deltas_list) + + if not self._data: + self._add_VarData(len(deltas_list)) + + varIdx = self._cache.get(deltas_list) + if varIdx is not None: + return varIdx + + inner = len(self._data.Item) + if inner + len(deltas_list) > 0xFFFF: + # Full array. Start new one. + self._add_VarData(len(deltas_list)) + return self.storeDeltasMany(deltas_list, round=noRound) + for i, deltas in enumerate(deltas_list): + self._data.addItem(deltas, round=noRound) + + varIdx = (self._outer << 16) + inner + i + self._cache[deltas] = varIdx + + varIdx = (self._outer << 16) + inner + self._cache[deltas_list] = varIdx + + return varIdx + def VarData_addItem(self, deltas, *, round=round): deltas = [round(d) for d in deltas] @@ -210,26 +246,29 @@ class VarStoreInstancer(object): def VarStore_subset_varidxes( - self, varIdxes, optimize=True, retainFirstMap=False, advIdxes=set() + self, + varIdxes, + optimize=True, + retainFirstMap=False, + advIdxes=set(), + *, + VarData="VarData", ): # Sort out used varIdxes by major/minor. - used = {} + used = defaultdict(set) for varIdx in varIdxes: if varIdx == NO_VARIATION_INDEX: continue major = varIdx >> 16 minor = varIdx & 0xFFFF - d = used.get(major) - if d is None: - d = used[major] = set() - d.add(minor) + used[major].add(minor) del varIdxes # # Subset VarData # - varData = self.VarData + varData = getattr(self, VarData) newVarData = [] varDataMap = {NO_VARIATION_INDEX: NO_VARIATION_INDEX} for major, data in enumerate(varData): @@ -260,10 +299,11 @@ def VarStore_subset_varidxes( data.Item = newItems data.ItemCount = len(data.Item) - data.calculateNumShorts(optimize=optimize) + if VarData == "VarData": + data.calculateNumShorts(optimize=optimize) - self.VarData = newVarData - self.VarDataCount = len(self.VarData) + setattr(self, VarData, newVarData) + setattr(self, VarData + "Count", len(newVarData)) self.prune_regions() @@ -273,7 +313,7 @@ def VarStore_subset_varidxes( ot.VarStore.subset_varidxes = VarStore_subset_varidxes -def VarStore_prune_regions(self): +def VarStore_prune_regions(self, *, VarData="VarData", VarRegionList="VarRegionList"): """Remove unused VarRegions.""" # # Subset VarRegionList @@ -281,10 +321,10 @@ def VarStore_prune_regions(self): # Collect. usedRegions = set() - for data in self.VarData: + for data in getattr(self, VarData): usedRegions.update(data.VarRegionIndex) # Subset. - regionList = self.VarRegionList + regionList = getattr(self, VarRegionList) regions = regionList.Region newRegions = [] regionMap = {} @@ -294,7 +334,7 @@ def VarStore_prune_regions(self): regionList.Region = newRegions regionList.RegionCount = len(regionList.Region) # Map. - for data in self.VarData: + for data in getattr(self, VarData): data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex] diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py index 5c6dfbb2f..abb82687e 100644 --- a/Tests/subset/subset_test.py +++ b/Tests/subset/subset_test.py @@ -428,14 +428,14 @@ class SubsetTest: def test_varComposite(self): fontpath = self.getpath("..", "..", "ttLib", "data", "varc-ac00-ac01.ttf") origfont = TTFont(fontpath) - assert len(origfont.getGlyphOrder()) == 6 + assert len(origfont.getGlyphOrder()) == 11 subsetpath = self.temp_path(".ttf") subset.main([fontpath, "--unicodes=ac00", "--output-file=%s" % subsetpath]) subsetfont = TTFont(subsetpath) - assert len(subsetfont.getGlyphOrder()) == 4 + assert len(subsetfont.getGlyphOrder()) == 6 subset.main([fontpath, "--unicodes=ac01", "--output-file=%s" % subsetpath]) subsetfont = TTFont(subsetpath) - assert len(subsetfont.getGlyphOrder()) == 5 + assert len(subsetfont.getGlyphOrder()) == 8 def test_timing_publishes_parts(self): fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index aa55df211..6041d6cc1 100644 Binary files a/Tests/ttLib/data/varc-6868.ttf and b/Tests/ttLib/data/varc-6868.ttf differ diff --git a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx index db32c06eb..9976b5bf7 100644 --- a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx +++ b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx @@ -1,5 +1,5 @@ - + @@ -9,17 +9,22 @@ + + + + + - - + + - - - - + + + + @@ -34,9 +39,9 @@ - - - + + + @@ -50,66 +55,18 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + @@ -135,272 +92,134 @@ - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + + + - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + - - butchered-hangul-serif + + rcjk - - smarties-variable + + varc - + + Weight + + + OpticalSize + + 0000 - + 0001 - + 0002 - + 0003 - + 0004 - + 0005 + + rcjk + + + varc + + + Weight + + + OpticalSize + + + 0000 + + + 0001 + + + 0002 + + + 0003 + - 0006 + 0004 - 0007 - - - Weight + 0005 @@ -416,1638 +235,917 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - 0000 - 0x1 - 0.0 - 0.0 - 1.0 + wght + 0x0 + 356.5 + 356.5 + 840.3 256 - + - 0001 - 0x1 + opsz + 0x0 0.0 0.0 1.0 257 - + - 0002 - 0x1 - 0.0 + 0000 + 0x0 + -1.0 0.0 1.0 258 - + - 0003 - 0x1 - 0.0 + 0001 + 0x0 + -1.0 0.0 1.0 259 - + - 0004 - 0x1 - 0.0 + 0002 + 0x0 + -1.0 0.0 1.0 260 - + - 0005 - 0x1 - 0.0 + 0003 + 0x0 + -1.0 0.0 1.0 261 - + - 0006 - 0x1 - 0.0 + 0004 + 0x0 + -1.0 0.0 1.0 262 - + - 0007 - 0x1 - 0.0 + 0005 + 0x0 + -1.0 0.0 1.0 263 - - - - wght - 0x0 - 200.0 - 200.0 - 900.0 - 264 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index 1e385bad3..fb3a08780 100644 Binary files a/Tests/ttLib/data/varc-ac00-ac01.ttf and b/Tests/ttLib/data/varc-ac00-ac01.ttf differ diff --git a/Tests/ttLib/data/varc-ac01-conditional.ttf b/Tests/ttLib/data/varc-ac01-conditional.ttf new file mode 100644 index 000000000..912a146e2 Binary files /dev/null and b/Tests/ttLib/data/varc-ac01-conditional.ttf differ diff --git a/Tests/ttLib/scaleUpem_test.py b/Tests/ttLib/scaleUpem_test.py index 6024758f3..524b7f1b0 100644 --- a/Tests/ttLib/scaleUpem_test.py +++ b/Tests/ttLib/scaleUpem_test.py @@ -1,5 +1,6 @@ from fontTools.ttLib import TTFont from fontTools.ttLib.scaleUpem import scale_upem +from io import BytesIO import difflib import os import shutil @@ -70,6 +71,12 @@ class ScaleUpemTest(unittest.TestCase): scale_upem(font, 500) + # Save / load to ensure calculated values are correct + # XXX This wans't needed before. So needs investigation. + iobytes = BytesIO() + font.save(iobytes) + # Just saving is enough to fix the numbers. Sigh... + expected_ttx_path = self.get_path("varc-ac00-ac01-500upem.ttx") self.expect_ttx(font, expected_ttx_path, tables) diff --git a/Tests/ttLib/tables/V_A_R_C_test.py b/Tests/ttLib/tables/V_A_R_C_test.py new file mode 100644 index 000000000..dcd35da8e --- /dev/null +++ b/Tests/ttLib/tables/V_A_R_C_test.py @@ -0,0 +1,87 @@ +from fontTools.ttLib import TTFont +from io import StringIO, BytesIO +import pytest +import os +import unittest + +CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +DATA_DIR = os.path.join(CURR_DIR, "data") + + +class VarCompositeTest(unittest.TestCase): + def test_basic(self): + font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf") + font = TTFont(font_path) + varc = font["VARC"] + + assert varc.table.Coverage.glyphs == [ + "uniAC00", + "uniAC01", + "glyph00003", + "glyph00005", + "glyph00007", + "glyph00008", + "glyph00009", + ] + + font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf") + font = TTFont(font_path) + varc = font["VARC"] + + assert varc.table.Coverage.glyphs == [ + "uni6868", + "glyph00002", + "glyph00005", + "glyph00007", + ] + + def test_roundtrip(self): + font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf") + font = TTFont(font_path) + tables = [ + table_tag + for table_tag in font.keys() + if table_tag not in {"head", "maxp", "hhea"} + ] + xml = StringIO() + font.saveXML(xml) + xml1 = StringIO() + font.saveXML(xml1, tables=tables) + xml.seek(0) + font = TTFont() + font.importXML(xml) + ttf = BytesIO() + font.save(ttf) + ttf.seek(0) + font = TTFont(ttf) + xml2 = StringIO() + font.saveXML(xml2, tables=tables) + assert xml1.getvalue() == xml2.getvalue() + + font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf") + font = TTFont(font_path) + tables = [ + table_tag + for table_tag in font.keys() + if table_tag not in {"head", "maxp", "hhea", "name", "fvar"} + ] + xml = StringIO() + font.saveXML(xml) + xml1 = StringIO() + font.saveXML(xml1, tables=tables) + xml.seek(0) + font = TTFont() + font.importXML(xml) + ttf = BytesIO() + font.save(ttf) + ttf.seek(0) + font = TTFont(ttf) + xml2 = StringIO() + font.saveXML(xml2, tables=tables) + assert xml1.getvalue() == xml2.getvalue() + + +if __name__ == "__main__": + import sys + + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 39f48b266..9a3fd2eaf 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -719,65 +719,6 @@ class GlyphComponentTest: assert (comp.firstPt, comp.secondPt) == (1, 2) assert not hasattr(comp, "transform") - def test_trim_varComposite_glyph(self): - font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf") - font = TTFont(font_path) - glyf = font["glyf"] - - glyf.glyphs["uniAC00"].trim() - glyf.glyphs["uniAC01"].trim() - - font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf") - font = TTFont(font_path) - glyf = font["glyf"] - - glyf.glyphs["uni6868"].trim() - - def test_varComposite_basic(self): - font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf") - font = TTFont(font_path) - tables = [ - table_tag - for table_tag in font.keys() - if table_tag not in {"head", "maxp", "hhea"} - ] - xml = StringIO() - font.saveXML(xml) - xml1 = StringIO() - font.saveXML(xml1, tables=tables) - xml.seek(0) - font = TTFont() - font.importXML(xml) - ttf = BytesIO() - font.save(ttf) - ttf.seek(0) - font = TTFont(ttf) - xml2 = StringIO() - font.saveXML(xml2, tables=tables) - assert xml1.getvalue() == xml2.getvalue() - - font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf") - font = TTFont(font_path) - tables = [ - table_tag - for table_tag in font.keys() - if table_tag not in {"head", "maxp", "hhea", "name", "fvar"} - ] - xml = StringIO() - font.saveXML(xml) - xml1 = StringIO() - font.saveXML(xml1, tables=tables) - xml.seek(0) - font = TTFont() - font.importXML(xml) - ttf = BytesIO() - font.save(ttf) - ttf.seek(0) - font = TTFont(ttf) - xml2 = StringIO() - font.saveXML(xml2, tables=tables) - assert xml1.getvalue() == xml2.getvalue() - class GlyphCubicTest: def test_roundtrip(self): diff --git a/Tests/ttLib/tables/otConverters_test.py b/Tests/ttLib/tables/otConverters_test.py index 94b62a1f7..f1be8ea23 100644 --- a/Tests/ttLib/tables/otConverters_test.py +++ b/Tests/ttLib/tables/otConverters_test.py @@ -427,9 +427,12 @@ class AATLookupTest(unittest.TestCase): ) +from fontTools.misc.lazyTools import LazyList + + class LazyListTest(unittest.TestCase): def test_slice(self): - ll = otConverters._LazyList([10, 11, 12, 13]) + ll = LazyList([10, 11, 12, 13]) sl = ll[:] self.assertIsNot(sl, ll) @@ -438,26 +441,9 @@ class LazyListTest(unittest.TestCase): self.assertEqual([11, 12], ll[1:3]) - def test_getitem(self): - count = 2 - reader = OTTableReader(b"\x00\xFE\xFF\x00\x00\x00", offset=1) - converter = otConverters.UInt8("UInt8", 0, None, None) - recordSize = converter.staticSize - l = otConverters._LazyList() - l.reader = reader - l.pos = l.reader.pos - l.font = None - l.conv = converter - l.recordSize = recordSize - l.extend(otConverters._MissingItem([i]) for i in range(count)) - reader.advance(count * recordSize) - - self.assertEqual(l[0], 254) - self.assertEqual(l[1], 255) - def test_add_both_LazyList(self): - ll1 = otConverters._LazyList([1]) - ll2 = otConverters._LazyList([2]) + ll1 = LazyList([1]) + ll2 = LazyList([2]) l3 = ll1 + ll2 @@ -465,7 +451,7 @@ class LazyListTest(unittest.TestCase): self.assertEqual([1, 2], l3) def test_add_LazyList_and_list(self): - ll1 = otConverters._LazyList([1]) + ll1 = LazyList([1]) l2 = [2] l3 = ll1 + l2 @@ -475,13 +461,13 @@ class LazyListTest(unittest.TestCase): def test_add_not_implemented(self): with self.assertRaises(TypeError): - otConverters._LazyList() + 0 + LazyList() + 0 with self.assertRaises(TypeError): - otConverters._LazyList() + tuple() + LazyList() + tuple() def test_radd_list_and_LazyList(self): l1 = [1] - ll2 = otConverters._LazyList([2]) + ll2 = LazyList([2]) l3 = l1 + ll2 @@ -490,9 +476,9 @@ class LazyListTest(unittest.TestCase): def test_radd_not_implemented(self): with self.assertRaises(TypeError): - 0 + otConverters._LazyList() + 0 + LazyList() with self.assertRaises(TypeError): - tuple() + otConverters._LazyList() + tuple() + LazyList() if __name__ == "__main__": diff --git a/Tests/ttLib/ttGlyphSet_test.py b/Tests/ttLib/ttGlyphSet_test.py index 177b8a4e7..7c95a847b 100644 --- a/Tests/ttLib/ttGlyphSet_test.py +++ b/Tests/ttLib/ttGlyphSet_test.py @@ -227,33 +227,57 @@ class TTGlyphSetTest(object): "addVarComponent", ( "glyph00003", - DecomposedTransform(460.0, 676.0, 0, 1, 1, 0, 0, 0, 0), - { - "0000": 0.84661865234375, - "0001": 0.98944091796875, - "0002": 0.47283935546875, - "0003": 0.446533203125, - }, + DecomposedTransform( + translateX=0, + translateY=0, + rotation=0, + scaleX=1, + scaleY=1, + skewX=0, + skewY=0, + tCenterX=0, + tCenterY=0, + ), + {}, ), ), ( "addVarComponent", ( - "glyph00004", - DecomposedTransform(932.0, 382.0, 0, 1, 1, 0, 0, 0, 0), - { - "0000": 0.93359375, - "0001": 0.916015625, - "0002": 0.523193359375, - "0003": 0.32806396484375, - "0004": 0.85089111328125, - }, + "glyph00005", + DecomposedTransform( + translateX=0, + translateY=0, + rotation=0, + scaleX=1, + scaleY=1, + skewX=0, + skewY=0, + tCenterX=0, + tCenterY=0, + ), + {}, ), ), ] assert actual == expected, (actual, expected) + def test_glyphset_varComposite_conditional(self): + font = TTFont(self.getpath("varc-ac01-conditional.ttf")) + + glyphset = font.getGlyphSet() + pen = RecordingPen() + glyph = glyphset["uniAC01"] + glyph.draw(pen) + assert len(pen.value) == 2 + + glyphset = font.getGlyphSet(location={"wght": 800}) + pen = RecordingPen() + glyph = glyphset["uniAC01"] + glyph.draw(pen) + assert len(pen.value) == 3 + def test_glyphset_varComposite1(self): font = TTFont(self.getpath("varc-ac00-ac01.ttf")) glyphset = font.getGlyphSet(location={"wght": 600}) @@ -265,77 +289,24 @@ class TTGlyphSetTest(object): actual = pen.value expected = [ - ("moveTo", ((432, 678),)), - ("lineTo", ((432, 620),)), - ( - "qCurveTo", - ( - (419, 620), - (374, 621), - (324, 619), - (275, 618), - (237, 617), - (228, 616), - ), - ), - ("qCurveTo", ((218, 616), (188, 612), (160, 605), (149, 601))), - ("qCurveTo", ((127, 611), (83, 639), (67, 654))), - ("qCurveTo", ((64, 657), (63, 662), (64, 666))), - ("lineTo", ((72, 678),)), - ("qCurveTo", ((93, 674), (144, 672), (164, 672))), - ( - "qCurveTo", - ( - (173, 672), - (213, 672), - (266, 673), - (323, 674), - (377, 675), - (421, 678), - (432, 678), - ), - ), + ("moveTo", ((82, 108),)), + ("qCurveTo", ((188, 138), (350, 240), (461, 384), (518, 567), (518, 678))), + ("lineTo", ((518, 732),)), + ("lineTo", ((74, 732),)), + ("lineTo", ((74, 630),)), + ("lineTo", ((456, 630),)), + ("lineTo", ((403, 660),)), + ("qCurveTo", ((403, 575), (358, 431), (267, 314), (128, 225), (34, 194))), ("closePath", ()), - ("moveTo", ((525, 619),)), - ("lineTo", ((412, 620),)), - ("lineTo", ((429, 678),)), - ("lineTo", ((466, 697),)), - ("qCurveTo", ((470, 698), (482, 698), (486, 697))), - ("qCurveTo", ((494, 693), (515, 682), (536, 670), (541, 667))), - ("qCurveTo", ((545, 663), (545, 656), (543, 652))), - ("lineTo", ((525, 619),)), + ("moveTo", ((702, 385),)), + ("lineTo", ((897, 385),)), + ("lineTo", ((897, 485),)), + ("lineTo", ((702, 485),)), ("closePath", ()), - ("moveTo", ((63, 118),)), - ("lineTo", ((47, 135),)), - ("qCurveTo", ((42, 141), (48, 146))), - ("qCurveTo", ((135, 213), (278, 373), (383, 541), (412, 620))), - ("lineTo", ((471, 642),)), - ("lineTo", ((525, 619),)), - ("qCurveTo", ((496, 529), (365, 342), (183, 179), (75, 121))), - ("qCurveTo", ((72, 119), (65, 118), (63, 118))), - ("closePath", ()), - ("moveTo", ((925, 372),)), - ("lineTo", ((739, 368),)), - ("lineTo", ((739, 427),)), - ("lineTo", ((822, 430),)), - ("lineTo", ((854, 451),)), - ("qCurveTo", ((878, 453), (930, 449), (944, 445))), - ("qCurveTo", ((961, 441), (962, 426))), - ("qCurveTo", ((964, 411), (956, 386), (951, 381))), - ("qCurveTo", ((947, 376), (931, 372), (925, 372))), - ("closePath", ()), - ("moveTo", ((729, -113),)), - ("lineTo", ((674, -113),)), - ("qCurveTo", ((671, -98), (669, -42), (666, 22), (665, 83), (665, 102))), - ("lineTo", ((665, 763),)), - ("qCurveTo", ((654, 780), (608, 810), (582, 820))), - ("lineTo", ((593, 850),)), - ("qCurveTo", ((594, 852), (599, 856), (607, 856))), - ("qCurveTo", ((628, 855), (684, 846), (736, 834), (752, 827))), - ("qCurveTo", ((766, 818), (766, 802))), - ("lineTo", ((762, 745),)), - ("lineTo", ((762, 134),)), - ("qCurveTo", ((762, 107), (757, 43), (749, -25), (737, -87), (729, -113))), + ("moveTo", ((641, -92),)), + ("lineTo", ((752, -92),)), + ("lineTo", ((752, 813),)), + ("lineTo", ((641, 813),)), ("closePath", ()), ] @@ -530,7 +501,7 @@ class TTGlyphSetTest(object): "qCurveTo", ( (919, 41), - (854, 67), + (854, 68), (790, 98), (729, 134), (671, 173), @@ -542,7 +513,7 @@ class TTGlyphSetTest(object): ("lineTo", ((522, 286),)), ("qCurveTo", ((511, 267), (498, 235), (493, 213), (492, 206))), ("lineTo", ((515, 209),)), - ("qCurveTo", ((569, 146), (695, 44), (835, -32), (913, -57))), + ("qCurveTo", ((569, 146), (695, 45), (835, -32), (913, -57))), ("closePath", ()), ("moveTo", ((474, 274),)), ("lineTo", ((452, 284),)), diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py index ca7ea93f0..b9a45058b 100644 --- a/Tests/varLib/instancer/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -1699,23 +1699,33 @@ class InstantiateVariableFontTest(object): def test_varComposite(self): input_path = os.path.join( - TESTDATA, "..", "..", "..", "ttLib", "data", "varc-ac00-ac01.ttf" + TESTDATA, "..", "..", "..", "ttLib", "data", "varc-6868.ttf" ) varfont = ttLib.TTFont(input_path) location = {"wght": 600} - instance = instancer.instantiateVariableFont( - varfont, - location, - ) + # We currently do not allow this either; although in theory + # it should be possible. + with pytest.raises( + NotImplementedError, + match="is not supported.", + ): + instance = instancer.instantiateVariableFont( + varfont, + location, + ) location = {"0000": 0.5} - instance = instancer.instantiateVariableFont( - varfont, - location, - ) + with pytest.raises( + NotImplementedError, + match="is not supported.", + ): + instance = instancer.instantiateVariableFont( + varfont, + location, + ) def _conditionSetAsDict(conditionSet, axisOrder):