From ef6903e097af642f5b91f1c9fe91aaec5b557be5 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Thu, 14 Dec 2023 15:59:53 -0700 Subject: [PATCH 001/114] [VARC] Start Based on https://github.com/harfbuzz/boring-expansion-spec/issues/103#issuecomment-1856325577 --- Lib/fontTools/ttLib/tables/V_A_R_C_.py | 5 + Lib/fontTools/ttLib/tables/otConverters.py | 83 +++++- Lib/fontTools/ttLib/tables/otData.py | 29 ++ Lib/fontTools/ttLib/tables/otTables.py | 299 ++++++++++++++++++++- 4 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 Lib/fontTools/ttLib/tables/V_A_R_C_.py 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/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index a2f672567..205b01934 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -18,6 +18,7 @@ from .otBase import ( ) from .otTables import ( lookupTypes, + VarCompositeGlyphRecord, AATStateTable, AATState, AATAction, @@ -29,7 +30,7 @@ from .otTables import ( CompositeMode as _CompositeMode, NO_VARIATION_INDEX, ) -from itertools import zip_longest +from itertools import zip_longest, accumulate from functools import partial import re import struct @@ -78,7 +79,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) @@ -1833,6 +1834,83 @@ class VarDataValue(BaseConverter): return safeEval(attrs["value"]) +class CFF2Index(BaseConverter): + def __init__( + self, name, repeat, aux, tableClass=None, *, itemClass=None, description="" + ): + BaseConverter.__init__( + self, name, repeat, aux, tableClass, description=description + ) + self.itemClass = itemClass + + def read(self, reader, font, tableDict): + count = reader.readULong() + if count == 0: + return [] + offSize = reader.readUInt8() + readArray = { + 1: reader.readUInt8Array, + 2: reader.readUShortArray, + 3: reader.readUInt24Array, + 4: reader.readULongArray, + }[offSize] + offsets = readArray(count + 1) + + items = [] + lastOffset = offsets[0] + reader.readData(lastOffset) # In case first offset is not 0 + for offset in offsets[1:]: + assert lastOffset <= offset + items.append(reader.readData(offset - lastOffset)) + lastOffset = offset + + if self.itemClass is not None: + newItems = [] + for item in items: + obj = self.itemClass() + obj.decompile(item, font) + newItems.append(obj) + items = newItems + + return items + + 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] + + offsets = [len(item) for item in items] + offsets = [0] + list(accumulate(offsets)) + + 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) + + class LookupFlag(UShort): def xmlWrite(self, xmlWriter, font, value, name, attrs): xmlWriter.simpletag(name, attrs + [("value", value)]) @@ -1910,6 +1988,7 @@ converterMapping = { "ExtendMode": ExtendMode, "CompositeMode": CompositeMode, "STATFlags": STATFlags, + "VarCompositeGlyphRecords": partial(CFF2Index, itemClass=VarCompositeGlyphRecord), # AAT "CIDGlyphMap": CIDGlyphMap, "GlyphCIDMap": GlyphCIDMap, diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 56716824e..be3716897 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3322,6 +3322,35 @@ otData = [ ("VarIdxMapValue", "mapping", "", 0, "Array of compressed data"), ], ), + # VariableComposites + ( + "VARC", + [ + ( + "Version", + "Version", + None, + None, + "Version of the HVAR table-initially = 0x00010000", + ), + ("LOffset", "Coverage", None, None, ""), + ( + "LOffsetTo(DeltaSetIndexMap)", + "VarIndexMap", + None, + None, + "Offset to DeltaSetIndexMap table (may be NULL)", + ), + ("LOffset", "VarStore", None, None, "(may be NULL)"), + ("LOffset", "VarCompositeGlyphs", None, None, ""), + ], + ), + ( + "VarCompositeGlyphs", + [ + ("VarCompositeGlyphRecords", "items", None, None, ""), + ], + ), # Glyph advance variations ( "HVAR", diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 879610a1e..64b7d4c90 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -14,7 +14,7 @@ from collections import defaultdict, namedtuple 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.pens.boundsPen import ControlBoundsPen from fontTools.pens.transformPen import TransformPen @@ -25,9 +25,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 +46,294 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) +class VarComponentFlags(IntFlag): + GID_IS_24BIT = 0x0001 + AXIS_INDICES_ARE_SHORT = 0x0002 + AXIS_VALUES_HAVE_VARIATION = 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 + TRANSFORM_HAS_VARIATION = 0x1000 + RESET_UNSPECIFIED_AXES = 0x2000 + USE_MY_METRICS = 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 VarComponentRecord: + def __init__(self): + self.glyphName = None + self.location = {} + self.locationVarIdxBase = NO_VARIATION_INDEX + self.transform = DecomposedTransform() + self.transformVarIdxBase = NO_VARIATION_INDEX + + def decompile(self, data, font): + i = 0 + flags = struct.unpack(">H", data[i : i + 2])[0] + i += 2 + self.flags = flags + + numAxes = data[i] + i += 1 + + if flags & VarComponentFlags.GID_IS_24BIT: + glyphID = struct.unpack(">L", b"\0" + data[i : i + 3])[0] + i += 3 + flags ^= VarComponentFlags.GID_IS_24BIT + else: + glyphID = struct.unpack(">H", data[i : i + 2])[0] + i += 2 + self.glyphName = font.glyphOrder[glyphID] + + if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: + axisIndices = array.array("H", data[i : i + 2 * numAxes]) + i += 2 * numAxes + if sys.byteorder != "big": + axisIndices.byteswap() + flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT + else: + axisIndices = array.array("B", data[i : i + numAxes]) + i += numAxes + assert len(axisIndices) == numAxes + axisIndices = list(axisIndices) + + axisValues = array.array("h", data[i : i + 2 * numAxes]) + i += 2 * numAxes + if sys.byteorder != "big": + axisValues.byteswap() + assert len(axisValues) == numAxes + axisValues = [fi2fl(v, 14) for v in axisValues] + + axes = font["fvar"].axes + self.location = {axes[i].axisTag: v for i, v in zip(axisIndices, axisValues)} + + if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + self.locationVarIdxBase = struct.unpack(">L", data[i : i + 4])[0] + i += 4 + + def read_transform_component(data, 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_COMPONENT_TRANSFORM_MAPPING.items(): + value = read_transform_component(data, mapping_values) + setattr(self.transform, attr_name, value) + + if not (flags & VarComponentFlags.HAVE_SCALE_Y): + self.transform.scaleY = self.transform.scaleX + + if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: + self.transformVarIdxBase = struct.unpack(">L", data[i : i + 4])[0] + i += 4 + + return data[i:] + + def compile(self, font): + data = [] + + 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 + if (flags & VarComponentFlags.HAVE_SCALE_Y) and fl2fi( + self.transform.scaleX, 10 + ) == fl2fi(self.transform.scaleY, 10): + flags ^= VarComponentFlags.HAVE_SCALE_Y + else: + flags = self.flags + + numAxes = len(self.location) + + data.append(struct.pack(">B", numAxes)) + + glyphID = font.getGlyphID(self.glyphName) + if glyphID > 65535: + flags |= VarComponentFlags.GID_IS_24BIT + data.append(struct.pack(">L", glyphID)[1:]) + else: + data.append(struct.pack(">H", glyphID)) + + fvarAxisIndices = {axis.axisTag: i for i, axis in enumerate(font["fvar"].axes)} + axisIndices = [fvarAxisIndices[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.append(bytes(axisIndices)) + + axisValues = self.location.values() + axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues)) + if sys.byteorder != "big": + axisValues.byteswap() + data.append(bytes(axisValues)) + + if self.locationVarIdxBase != NO_VARIATION_INDEX: + flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + data.append(struct.pack(">L", self.locationVarIdxBase)) + + 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_COMPONENT_TRANSFORM_MAPPING.items(): + value = getattr(self.transform, attr_name) + data.append(write_transform_component(value, mapping_values)) + + if self.transformVarIdxBase != NO_VARIATION_INDEX: + flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION + data.append(struct.pack(">L", self.transformVarIdxBase)) + + return struct.pack(">H", flags) + bytesjoin(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 __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 VarCompositeGlyphRecord: + def populateDefaults(self, propagator=None): + if not hasattr(self, "components"): + self.components = [] + + def decompile(self, data, font): + self.components = [] + while data: + component = VarComponentRecord() + data = component.decompile(data, font) + 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): + raise NotImplementedError + + def fromXML(self, name, attrs, content, font): + raise NotImplementedError + + class AATStateTable(object): def __init__(self): self.GlyphClasses = {} # GlyphID --> GlyphClass From c3175b271a2267eb4dc1fa0bbea4d593cff20f8d Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Thu, 14 Dec 2023 16:13:01 -0700 Subject: [PATCH 002/114] [transform] Add __eq__ / __ne__ to DecomposedTransform --- Lib/fontTools/misc/transform.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index 0f9f3a5d8..a8c1dd317 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -422,6 +422,26 @@ class DecomposedTransform: tCenterX: float = 0 tCenterY: float = 0 + def __eq__(self, other): + if not isinstance(other, DecomposedTransform): + return NotImplemented + return ( + self.translateX == other.translateX + and self.translateY == other.translateY + and self.rotation == other.rotation + and self.scaleX == other.scaleX + and self.scaleY == other.scaleY + and self.skewX == other.skewX + and self.skewY == other.skewY + and self.tCenterX == other.tCenterX + and self.tCenterY == other.tCenterY + ) + + def __ne__(self, other): + if not isinstance(other, DecomposedTransform): + return NotImplemented + return not self == other + @classmethod def fromTransform(self, transform): # Adapted from an answer on From aad01a9d85ed6e7bff605b3ede3c2ea063e71efd Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Thu, 14 Dec 2023 20:40:37 -0700 Subject: [PATCH 003/114] [VARC] Towards XML --- Lib/fontTools/ttLib/tables/otConverters.py | 16 ++++++- Lib/fontTools/ttLib/tables/otData.py | 2 +- Lib/fontTools/ttLib/tables/otTables.py | 52 +++++++++++++++++----- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 205b01934..6ce6ad5da 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -18,7 +18,7 @@ from .otBase import ( ) from .otTables import ( lookupTypes, - VarCompositeGlyphRecord, + VarCompositeGlyph, AATStateTable, AATState, AATAction, @@ -1910,6 +1910,18 @@ class CFF2Index(BaseConverter): for item in items: writer.writeData(item) + def xmlRead(self, attrs, content, font): + abort + items = [] + for eName, eAttrs, _eContent in filter(istuple, content): + print(eName) + return items + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + for i, item in enumerate(value): + item.toXML(xmlWriter, font, [("index", i)], name) + + class LookupFlag(UShort): def xmlWrite(self, xmlWriter, font, value, name, attrs): @@ -1988,7 +2000,7 @@ converterMapping = { "ExtendMode": ExtendMode, "CompositeMode": CompositeMode, "STATFlags": STATFlags, - "VarCompositeGlyphRecords": partial(CFF2Index, itemClass=VarCompositeGlyphRecord), + "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 be3716897..39e7f55e5 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3348,7 +3348,7 @@ otData = [ ( "VarCompositeGlyphs", [ - ("VarCompositeGlyphRecords", "items", None, None, ""), + ("VarCompositeGlyphList", "glyphs", None, None, ""), ], ), # Glyph advance variations diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 64b7d4c90..b1a71d968 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -100,7 +100,7 @@ VAR_COMPONENT_TRANSFORM_MAPPING = { } -class VarComponentRecord: +class VarComponent: def __init__(self): self.glyphName = None self.location = {} @@ -159,7 +159,7 @@ class VarComponentRecord: fi2fl( struct.unpack(">h", data[i : i + 2])[0], values.fractionalBits ) - * values.scale, + * values.scale ) i += 2 return v @@ -207,12 +207,16 @@ class VarComponentRecord: flags |= VarComponentFlags.GID_IS_24BIT data.append(struct.pack(">L", glyphID)[1:]) else: + if flags & VarComponentFlags.GID_IS_24BIT: + flags ^= VarComponentFlags.GID_IS_24BIT data.append(struct.pack(">H", glyphID)) fvarAxisIndices = {axis.axisTag: i for i, axis in enumerate(font["fvar"].axes)} axisIndices = [fvarAxisIndices[tag] for tag in self.location.keys()] if all(a <= 255 for a in axisIndices): axisIndices = array.array("B", axisIndices) + if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: + flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT else: axisIndices = array.array("H", axisIndices) if sys.byteorder != "big": @@ -229,6 +233,8 @@ class VarComponentRecord: if self.locationVarIdxBase != NO_VARIATION_INDEX: flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION data.append(struct.pack(">L", self.locationVarIdxBase)) + elif flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + flags ^= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION def write_transform_component(value, values): if flags & values.flag: @@ -245,11 +251,13 @@ class VarComponentRecord: if self.transformVarIdxBase != NO_VARIATION_INDEX: flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION data.append(struct.pack(">L", self.transformVarIdxBase)) + elif flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: + flags ^= VarComponentFlags.TRANSFORM_HAS_VARIATION return struct.pack(">H", flags) + bytesjoin(data) - def toXML(self, writer, ttFont): - attrs = [("glyphName", self.glyphName)] + def toXML(self, writer, ttFont, attrs): + attrs.append(("glyphName", self.glyphName)) if hasattr(self, "flags"): attrs = attrs + [("flags", hex(self.flags))] @@ -259,7 +267,7 @@ class VarComponentRecord: if v != mapping.defaultValue: attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) - writer.begintag("varComponent", attrs) + writer.begintag("VarComponent", attrs) writer.newline() writer.begintag("location") @@ -270,7 +278,7 @@ class VarComponentRecord: writer.endtag("location") writer.newline() - writer.endtag("varComponent") + writer.endtag("VarComponent") writer.newline() def fromXML(self, name, attrs, content, ttFont): @@ -309,7 +317,8 @@ class VarComponentRecord: return result if result is NotImplemented else not result -class VarCompositeGlyphRecord: +class VarCompositeGlyph(BaseTable): + def populateDefaults(self, propagator=None): if not hasattr(self, "components"): self.components = [] @@ -317,7 +326,7 @@ class VarCompositeGlyphRecord: def decompile(self, data, font): self.components = [] while data: - component = VarComponentRecord() + component = VarComponent() data = component.decompile(data, font) self.components.append(component) @@ -328,11 +337,34 @@ class VarCompositeGlyphRecord: return bytesjoin(data) def toXML(self, xmlWriter, font, attrs, name): - raise NotImplementedError + 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): - raise NotImplementedError + self.populateDefaults() + if name == "VarComponent": + component = VarComponent() + component.fromXML(name, attrs, content, font) + self.components.append(component) +class VarCompositeGlyphs(BaseTable): + + def populateDefaults(self, propagator=None): + if not hasattr(self, "glyphs"): + self.glyphs = [] + + def fromXML(self, name, attrs, content, font): + self.populateDefaults() + if name == "VarCompositeGlyph": + glyph = VarCompositeGlyph() + content = [t for t in content if isinstance(t, tuple)] + for eltName, eltAttrs, eltContent in content: + glyph.fromXML(eltName, eltAttrs, eltContent, font) + self.glyphs.append(glyph) class AATStateTable(object): def __init__(self): From 1a1e9e198b55273a0b095aaf3af7243922c653f5 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Thu, 14 Dec 2023 20:59:49 -0700 Subject: [PATCH 004/114] [VARC] Use one varIndexBase only --- Lib/fontTools/ttLib/tables/otConverters.py | 1 - Lib/fontTools/ttLib/tables/otTables.py | 42 +++++++++++----------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 6ce6ad5da..57261429d 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1922,7 +1922,6 @@ class CFF2Index(BaseConverter): item.toXML(xmlWriter, font, [("index", i)], name) - class LookupFlag(UShort): def xmlWrite(self, xmlWriter, font, value, name, attrs): xmlWriter.simpletag(name, attrs + [("value", value)]) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index b1a71d968..41e482719 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -104,9 +104,8 @@ class VarComponent: def __init__(self): self.glyphName = None self.location = {} - self.locationVarIdxBase = NO_VARIATION_INDEX self.transform = DecomposedTransform() - self.transformVarIdxBase = NO_VARIATION_INDEX + self.varIndexBase = NO_VARIATION_INDEX def decompile(self, data, font): i = 0 @@ -148,10 +147,6 @@ class VarComponent: axes = font["fvar"].axes self.location = {axes[i].axisTag: v for i, v in zip(axisIndices, axisValues)} - if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - self.locationVarIdxBase = struct.unpack(">L", data[i : i + 4])[0] - i += 4 - def read_transform_component(data, values): nonlocal i if flags & values.flag: @@ -173,8 +168,11 @@ class VarComponent: if not (flags & VarComponentFlags.HAVE_SCALE_Y): self.transform.scaleY = self.transform.scaleX - if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: - self.transformVarIdxBase = struct.unpack(">L", data[i : i + 4])[0] + if flags & ( + VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + | VarComponentFlags.TRANSFORM_HAS_VARIATION + ): + self.varIndexBase = struct.unpack(">L", data[i : i + 4])[0] i += 4 return data[i:] @@ -230,12 +228,6 @@ class VarComponent: axisValues.byteswap() data.append(bytes(axisValues)) - if self.locationVarIdxBase != NO_VARIATION_INDEX: - flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION - data.append(struct.pack(">L", self.locationVarIdxBase)) - elif flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - flags ^= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION - def write_transform_component(value, values): if flags & values.flag: return struct.pack( @@ -248,11 +240,11 @@ class VarComponent: value = getattr(self.transform, attr_name) data.append(write_transform_component(value, mapping_values)) - if self.transformVarIdxBase != NO_VARIATION_INDEX: - flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION - data.append(struct.pack(">L", self.transformVarIdxBase)) - elif flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: - flags ^= VarComponentFlags.TRANSFORM_HAS_VARIATION + if flags & ( + VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + | VarComponentFlags.TRANSFORM_HAS_VARIATION + ): + data.append(struct.pack(">L", self.varIndexBase)) return struct.pack(">H", flags) + bytesjoin(data) @@ -262,6 +254,11 @@ class VarComponent: if hasattr(self, "flags"): attrs = attrs + [("flags", hex(self.flags))] + # TODO Move to an element? + if self.varIndexBase != NO_VARIATION_INDEX: + attrs = attrs + [("varIndexBase", self.varIndexBase)] + + # TODO Move transform into it's own element? for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): v = getattr(self.transform, attr_name) if v != mapping.defaultValue: @@ -287,6 +284,9 @@ class VarComponent: if "flags" in attrs: self.flags = safeEval(attrs["flags"]) + if "varIndexBase" in attrs: + self.varIndexBase = safeEval(attrs["varIndexBase"]) + for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): if attr_name not in attrs: continue @@ -318,7 +318,6 @@ class VarComponent: class VarCompositeGlyph(BaseTable): - def populateDefaults(self, propagator=None): if not hasattr(self, "components"): self.components = [] @@ -351,8 +350,8 @@ class VarCompositeGlyph(BaseTable): component.fromXML(name, attrs, content, font) self.components.append(component) -class VarCompositeGlyphs(BaseTable): +class VarCompositeGlyphs(BaseTable): def populateDefaults(self, propagator=None): if not hasattr(self, "glyphs"): self.glyphs = [] @@ -366,6 +365,7 @@ class VarCompositeGlyphs(BaseTable): glyph.fromXML(eltName, eltAttrs, eltContent, font) self.glyphs.append(glyph) + class AATStateTable(object): def __init__(self): self.GlyphClasses = {} # GlyphID --> GlyphClass From 6f4feffa1176eae24ac67b7ce75d31a4d945d98e Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 10:45:07 -0700 Subject: [PATCH 005/114] [varStore] Add storeMastersMany / storeDeltasMany --- Lib/fontTools/varLib/varStore.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py index 780576907..f93bf8562 100644 --- a/Lib/fontTools/varLib/varStore.py +++ b/Lib/fontTools/varLib/varStore.py @@ -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: @@ -112,13 +120,30 @@ class OnlineVarStoreBuilder(object): 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] + + if not self._data: + self._add_VarData() + inner = len(self._data.Item) + if inner + len(deltas_list) > 0xFFFF: + # Full array. Start new one. + self._add_VarData() + return self.storeDeltasMany(deltas_list, round=noRound) + for deltas in deltas_list: + self._data.addItem(deltas, round=noRound) + # TODO Insert into the cache + + varIdx = (self._outer << 16) + inner + return varIdx + def VarData_addItem(self, deltas, *, round=round): deltas = [round(d) for d in deltas] From a8005130ccad80ee5fa21774776b41b9555667d0 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 10:52:28 -0700 Subject: [PATCH 006/114] [varStore] Cache individual items in store*Many() --- Lib/fontTools/varLib/varStore.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py index f93bf8562..f7948bfea 100644 --- a/Lib/fontTools/varLib/varStore.py +++ b/Lib/fontTools/varLib/varStore.py @@ -137,9 +137,11 @@ class OnlineVarStoreBuilder(object): # Full array. Start new one. self._add_VarData() return self.storeDeltasMany(deltas_list, round=noRound) - for deltas in deltas_list: + for i, deltas in enumerate(deltas_list): self._data.addItem(deltas, round=noRound) - # TODO Insert into the cache + + varIdx = (self._outer << 16) + inner + i + self._cache[deltas] = varIdx varIdx = (self._outer << 16) + inner return varIdx From b7ab1d8c129e8c197221042f855b310a1e865368 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 11:09:21 -0700 Subject: [PATCH 007/114] [varStore] Fix overflow logic --- Lib/fontTools/varLib/varStore.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py index f7948bfea..e74bd5d52 100644 --- a/Lib/fontTools/varLib/varStore.py +++ b/Lib/fontTools/varLib/varStore.py @@ -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 @@ -129,13 +129,18 @@ class OnlineVarStoreBuilder(object): 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) + + varIdx = self._cache.get(deltas_list) + if varIdx is not None: + return varIdx if not self._data: - self._add_VarData() + self._add_VarData(len(deltas_list)) inner = len(self._data.Item) if inner + len(deltas_list) > 0xFFFF: # Full array. Start new one. - self._add_VarData() + 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) @@ -144,6 +149,8 @@ class OnlineVarStoreBuilder(object): self._cache[deltas] = varIdx varIdx = (self._outer << 16) + inner + self._cache[deltas_list] = varIdx + return varIdx From 0e9eff89901abd6cadd7c5a2486f48cf9b0095b9 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 13:29:13 -0700 Subject: [PATCH 008/114] Add MultiVarStore --- Lib/fontTools/ttLib/tables/otConverters.py | 2 + Lib/fontTools/ttLib/tables/otData.py | 28 +++-- Lib/fontTools/ttLib/tables/otTables.py | 29 +++++ Lib/fontTools/varLib/builder.py | 23 ++++ Lib/fontTools/varLib/multiVarStore.py | 138 +++++++++++++++++++++ 5 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 Lib/fontTools/varLib/multiVarStore.py diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 57261429d..789ec7cfb 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -19,6 +19,7 @@ from .otBase import ( from .otTables import ( lookupTypes, VarCompositeGlyph, + CvarEncodedValues, AATStateTable, AATState, AATAction, @@ -2000,6 +2001,7 @@ converterMapping = { "CompositeMode": CompositeMode, "STATFlags": STATFlags, "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph), + "MultiVarDataValue": partial(CFF2Index, itemClass=CvarEncodedValues), # AAT "CIDGlyphMap": CIDGlyphMap, "GlyphCIDMap": GlyphCIDMap, diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 39e7f55e5..a71c25476 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3322,6 +3322,25 @@ otData = [ ("VarIdxMapValue", "mapping", "", 0, "Array of compressed data"), ], ), + # MultiVariationStore + ( + "MultiVarData", + [ + ("uint8", "Format", None, None, "Set to 1."), + ("uint16", "VarRegionCount", None, None, ""), + ("uint16", "VarRegionIndex", "VarRegionCount", 0, ""), + ("MultiVarDataValue", "Item", "", 0, ""), + ], + ), + ( + "MultiVarStore", + [ + ("uint16", "Format", None, None, "Set to 1."), + ("LOffset", "VarRegionList", None, None, ""), + ("uint16", "VarDataCount", None, None, ""), + ("LOffsetTo(MultiVarData)", "VarData", "VarDataCount", 0, ""), + ], + ), # VariableComposites ( "VARC", @@ -3334,14 +3353,7 @@ otData = [ "Version of the HVAR table-initially = 0x00010000", ), ("LOffset", "Coverage", None, None, ""), - ( - "LOffsetTo(DeltaSetIndexMap)", - "VarIndexMap", - None, - None, - "Offset to DeltaSetIndexMap table (may be NULL)", - ), - ("LOffset", "VarStore", None, None, "(may be NULL)"), + ("LOffset", "MultiVarStore", None, None, "(may be NULL)"), ("LOffset", "VarCompositeGlyphs", None, None, ""), ], ), diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 41e482719..7a953dca1 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -317,6 +317,35 @@ class VarComponent: return result if result is NotImplemented else not result +class CvarEncodedValues(BaseTable): + def __init__(self, values=None): + self.values = values or [] + + def populateDefaults(self, propagator=None): + if not hasattr(self, "values"): + self.values = () + + def decompile(self, data, font): + values = array.array("h", data) + if sys.byteorder != "big": + values.byteswap() + self.values = list(values) + + def compile(self, font): + values = array.array("h", self.values) + if sys.byteorder != "big": + values.byteswap() + return bytes(values) + + def toXML(self, xmlWriter, font, attrs, name): + xmlWriter.simpletag(name, attrs + [("value", self.values)]) + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + self.populateDefaults() + values = safeEval(attrs["value"]) + + class VarCompositeGlyph(BaseTable): def populateDefaults(self, propagator=None): if not hasattr(self, "components"): diff --git a/Lib/fontTools/varLib/builder.py b/Lib/fontTools/varLib/builder.py index 94cc5bf06..d5c146762 100644 --- a/Lib/fontTools/varLib/builder.py +++ b/Lib/fontTools/varLib/builder.py @@ -130,6 +130,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, varDataList): + self = ot.MultiVarStore() + self.Format = 1 + self.VarRegionList = varRegionList + self.VarData = list(varDataList) + self.VarDataCount = len(self.VarData) + return self + + # Variation helpers diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py new file mode 100644 index 000000000..591e1239c --- /dev/null +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -0,0 +1,138 @@ +from fontTools.misc.roundTools import noRound, otRound +from fontTools.misc.intTools import bit_count +from fontTools.ttLib.tables import otTables as ot +from fontTools.varLib.models import supportScalar +from fontTools.varLib.builder import ( + buildVarRegionList, + buildVarRegion, + buildMultiVarStore, + buildMultiVarData, +) +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 = buildVarRegionList([], axisTags) + self._store = buildMultiVarStore(self._regionList, []) + self._data = None + self._model = None + self._supports = None + self._varDataIndices = {} + self._varDataCaches = {} + self._cache = {} + + 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 = {} + self._data = None + + def finish(self, optimize=True): + self._regionList.RegionCount = len(self._regionList.Region) + self._store.VarDataCount = len(self._store.VarData) + return self._store + + def _add_MultiVarData(self, num_items=1): + 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 = buildVarRegion(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.VarData[varDataIdx] + self._cache = self._varDataCaches[key] + if len(self._data.Item) + num_items > 0xFFFF: + # This is full. Need new one. + varDataIdx = None + + if varDataIdx is None: + self._data = buildMultiVarData(regionIndices, []) + self._outer = len(self._store.VarData) + self._store.VarData.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) + deltas_tuple = tuple(tuple(d) for d in deltas) + + varIdx = self._cache.get(deltas_tuple) + if varIdx is not None: + return varIdx + + if not self._data: + self._add_MultiVarData() + 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) + + values = ot.CvarEncodedValues(values) + + self.Item.append(values) + self.ItemCount = len(self.Item) + + +ot.MultiVarData.addItem = MultiVarData_addItem + + +def MultiVarStore___bool__(self): + return bool(self.VarData) + + +ot.MultiVarStore.__bool__ = MultiVarStore___bool__ From 02c6a94529d63930b43a6cc1caea4af06cdd635f Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 15:19:29 -0700 Subject: [PATCH 009/114] [VARC] Use TupleVariation value encoding --- Lib/fontTools/ttLib/tables/TupleVariation.py | 12 +++++++++--- Lib/fontTools/ttLib/tables/otTables.py | 13 ++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py index 027ac1534..db38eff06 100644 --- a/Lib/fontTools/ttLib/tables/TupleVariation.py +++ b/Lib/fontTools/ttLib/tables/TupleVariation.py @@ -420,6 +420,12 @@ class TupleVariation(object): numDeltas = len(deltas) while pos < numDeltas: value = deltas[pos] + + if value == 32768: + # Special case for the value -32768, which cannot + # be represented as a 16-bit signed integer. + value -= 1 + # 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, @@ -461,11 +467,11 @@ class TupleVariation(object): return pos @staticmethod - def decompileDeltas_(numDeltas, data, offset): + 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 @@ -484,7 +490,7 @@ class TupleVariation(object): assert 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/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 7a953dca1..3ce72dcb6 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -12,6 +12,7 @@ from math import radians import itertools from collections import defaultdict, namedtuple from fontTools.ttLib.tables.otTraverse import dfs_base_table +from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.misc.arrayTools import quantizeRect from fontTools.misc.roundTools import otRound from fontTools.misc.transform import Transform, Identity, DecomposedTransform @@ -323,19 +324,13 @@ class CvarEncodedValues(BaseTable): def populateDefaults(self, propagator=None): if not hasattr(self, "values"): - self.values = () + self.values = [] def decompile(self, data, font): - values = array.array("h", data) - if sys.byteorder != "big": - values.byteswap() - self.values = list(values) + self.values = TupleVariation.decompileDeltas_(None, data)[0] def compile(self, font): - values = array.array("h", self.values) - if sys.byteorder != "big": - values.byteswap() - return bytes(values) + return bytes(TupleVariation.compileDeltaValues_(self.values, bytearr=None)) def toXML(self, xmlWriter, font, attrs, name): xmlWriter.simpletag(name, attrs + [("value", self.values)]) From 6bcab786a1803aa67606245018eb1902d1a0b497 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 15:45:05 -0700 Subject: [PATCH 010/114] [varStore] Fix caching when setModel() is called repeatedly --- Lib/fontTools/varLib/varStore.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py index e74bd5d52..d4e199a4d 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): @@ -110,12 +110,13 @@ 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. @@ -131,12 +132,13 @@ class OnlineVarStoreBuilder(object): 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 - if not self._data: - self._add_VarData(len(deltas_list)) inner = len(self._data.Item) if inner + len(deltas_list) > 0xFFFF: # Full array. Start new one. From 5fe9da49f32acb016b1ede0f32c946696482b774 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 15:46:55 -0700 Subject: [PATCH 011/114] [MultiVarStore] Fix caching --- Lib/fontTools/varLib/multiVarStore.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index 591e1239c..0d06ffd5e 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -32,7 +32,7 @@ class OnlineMultiVarStoreBuilder(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 OnlineMultiVarStoreBuilder(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): @@ -51,7 +51,7 @@ class OnlineMultiVarStoreBuilder(object): self._store.VarDataCount = len(self._store.VarData) return self._store - def _add_MultiVarData(self, num_items=1): + def _add_MultiVarData(self): regionMap = self._regionMap regionList = self._regionList @@ -73,7 +73,7 @@ class OnlineMultiVarStoreBuilder(object): self._outer = varDataIdx self._data = self._store.VarData[varDataIdx] self._cache = self._varDataCaches[key] - if len(self._data.Item) + num_items > 0xFFFF: + if len(self._data.Item) == 0xFFFF: # This is full. Need new one. varDataIdx = None @@ -95,12 +95,13 @@ class OnlineMultiVarStoreBuilder(object): deltas = tuple(round(d) for d in deltas) 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 - if not self._data: - self._add_MultiVarData() inner = len(self._data.Item) if inner == 0xFFFF: # Full array. Start new one. From b4d3fc54b2eaf169cab9182ba74d6746349c915e Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 15:59:28 -0700 Subject: [PATCH 012/114] [TupleVariation] Support 32bit encoding in delta-encoding Uses an used combination of top two bits. --- Lib/fontTools/ttLib/tables/TupleVariation.py | 42 ++++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py index db38eff06..4fab05ac8 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 @@ -421,11 +425,6 @@ class TupleVariation(object): while pos < numDeltas: value = deltas[pos] - if value == 32768: - # Special case for the value -32768, which cannot - # be represented as a 16-bit signed integer. - value -= 1 - # 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, @@ -466,6 +465,30 @@ class TupleVariation(object): bytearr.extend(a) return pos + @staticmethod + def encodeDeltaRunAsLongs_(deltas, offset, bytearr): + pos = offset + numDeltas = len(deltas) + while pos < numDeltas: + value = deltas[pos] + pos += 1 + runLength = pos - offset + while runLength >= 64: + bytearr.append(DELTAS_ARE_LONGS | 63) + a = array.array("l", 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("l", 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)""" @@ -475,10 +498,13 @@ class TupleVariation(object): 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("l") + deltasSize = numDeltasInRun * 4 + if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_WORDS: deltas = array.array("h") deltasSize = numDeltasInRun * 2 else: From c78ba01c8067b6c065a7ce91686804f10e78237a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 16:16:45 -0700 Subject: [PATCH 013/114] [VarCompositeGlyph] Use two varIdxes per component Seems to save space now with MultiVarStore. --- Lib/fontTools/ttLib/tables/otTables.py | 36 +++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 3ce72dcb6..87d006983 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -106,7 +106,8 @@ class VarComponent: self.glyphName = None self.location = {} self.transform = DecomposedTransform() - self.varIndexBase = NO_VARIATION_INDEX + self.locationVarIndex = NO_VARIATION_INDEX + self.transformVarIndex = NO_VARIATION_INDEX def decompile(self, data, font): i = 0 @@ -148,6 +149,10 @@ class VarComponent: axes = font["fvar"].axes self.location = {axes[i].axisTag: v for i, v in zip(axisIndices, axisValues)} + if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + self.locationVarIndex = struct.unpack(">L", data[i : i + 4])[0] + i += 4 + def read_transform_component(data, values): nonlocal i if flags & values.flag: @@ -169,11 +174,8 @@ class VarComponent: if not (flags & VarComponentFlags.HAVE_SCALE_Y): self.transform.scaleY = self.transform.scaleX - if flags & ( - VarComponentFlags.AXIS_VALUES_HAVE_VARIATION - | VarComponentFlags.TRANSFORM_HAS_VARIATION - ): - self.varIndexBase = struct.unpack(">L", data[i : i + 4])[0] + if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: + self.transformVarIndex = struct.unpack(">L", data[i : i + 4])[0] i += 4 return data[i:] @@ -229,6 +231,9 @@ class VarComponent: axisValues.byteswap() data.append(bytes(axisValues)) + if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + data.append(struct.pack(">L", self.locationVarIndex)) + def write_transform_component(value, values): if flags & values.flag: return struct.pack( @@ -241,11 +246,8 @@ class VarComponent: value = getattr(self.transform, attr_name) data.append(write_transform_component(value, mapping_values)) - if flags & ( - VarComponentFlags.AXIS_VALUES_HAVE_VARIATION - | VarComponentFlags.TRANSFORM_HAS_VARIATION - ): - data.append(struct.pack(">L", self.varIndexBase)) + if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: + data.append(struct.pack(">L", self.transformVarIndex)) return struct.pack(">H", flags) + bytesjoin(data) @@ -256,8 +258,10 @@ class VarComponent: attrs = attrs + [("flags", hex(self.flags))] # TODO Move to an element? - if self.varIndexBase != NO_VARIATION_INDEX: - attrs = attrs + [("varIndexBase", self.varIndexBase)] + if self.locationVarIndex != NO_VARIATION_INDEX: + attrs = attrs + [("locationVarIndex", self.locationVarIndex)] + if self.transformVarIndex != NO_VARIATION_INDEX: + attrs = attrs + [("transformVarIndex", self.transformVarIndex)] # TODO Move transform into it's own element? for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): @@ -285,8 +289,10 @@ class VarComponent: if "flags" in attrs: self.flags = safeEval(attrs["flags"]) - if "varIndexBase" in attrs: - self.varIndexBase = safeEval(attrs["varIndexBase"]) + if "locationVarIndex" in attrs: + self.locationVarIndex = safeEval(attrs["locationVarIndex"]) + if "transformVarIndex" in attrs: + self.transformVarIndex = safeEval(attrs["transformVarIndex"]) for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): if attr_name not in attrs: From ec78b572c9ea1516f54a281524e08a1a9cc5137d Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 17:27:31 -0700 Subject: [PATCH 014/114] [MultiVarStore] Fix up XML read/write Looks complete now. --- Lib/fontTools/ttLib/tables/otBase.py | 2 +- Lib/fontTools/ttLib/tables/otConverters.py | 8 +++----- Lib/fontTools/ttLib/tables/otData.py | 4 ++-- Lib/fontTools/ttLib/tables/otTables.py | 2 +- Lib/fontTools/varLib/builder.py | 6 +++--- Lib/fontTools/varLib/multiVarStore.py | 10 +++++----- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py index 53abd13b4..18c17d72f 100644 --- a/Lib/fontTools/ttLib/tables/otBase.py +++ b/Lib/fontTools/ttLib/tables/otBase.py @@ -1146,7 +1146,7 @@ class BaseTable(object): except KeyError: raise # XXX on KeyError, raise nice error value = conv.xmlRead(attrs, content, font) - if conv.repeat: + 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 789ec7cfb..8bd685d0b 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1912,11 +1912,9 @@ class CFF2Index(BaseConverter): writer.writeData(item) def xmlRead(self, attrs, content, font): - abort - items = [] - for eName, eAttrs, _eContent in filter(istuple, content): - print(eName) - return items + obj = self.itemClass() + obj.fromXML(None, attrs, content, font) + return obj def xmlWrite(self, xmlWriter, font, value, name, attrs): for i, item in enumerate(value): diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index a71c25476..9c9efc595 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3337,8 +3337,8 @@ otData = [ [ ("uint16", "Format", None, None, "Set to 1."), ("LOffset", "VarRegionList", None, None, ""), - ("uint16", "VarDataCount", None, None, ""), - ("LOffsetTo(MultiVarData)", "VarData", "VarDataCount", 0, ""), + ("uint16", "MultiVarDataCount", None, None, ""), + ("LOffset", "MultiVarData", "MultiVarDataCount", 0, ""), ], ), # VariableComposites diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 87d006983..3a239aca5 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -344,7 +344,7 @@ class CvarEncodedValues(BaseTable): def fromXML(self, name, attrs, content, font): self.populateDefaults() - values = safeEval(attrs["value"]) + self.values = safeEval(attrs["value"]) class VarCompositeGlyph(BaseTable): diff --git a/Lib/fontTools/varLib/builder.py b/Lib/fontTools/varLib/builder.py index d5c146762..f05802770 100644 --- a/Lib/fontTools/varLib/builder.py +++ b/Lib/fontTools/varLib/builder.py @@ -144,12 +144,12 @@ def buildMultiVarData(varRegionIndices, items): return self -def buildMultiVarStore(varRegionList, varDataList): +def buildMultiVarStore(varRegionList, multiVarDataList): self = ot.MultiVarStore() self.Format = 1 self.VarRegionList = varRegionList - self.VarData = list(varDataList) - self.VarDataCount = len(self.VarData) + self.MultiVarData = list(multiVarDataList) + self.MultiVarDataCount = len(self.MultiVarData) return self diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index 0d06ffd5e..a0b246a11 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -48,7 +48,7 @@ class OnlineMultiVarStoreBuilder(object): def finish(self, optimize=True): self._regionList.RegionCount = len(self._regionList.Region) - self._store.VarDataCount = len(self._store.VarData) + self._store.MultiVarDataCount = len(self._store.MultiVarData) return self._store def _add_MultiVarData(self): @@ -71,7 +71,7 @@ class OnlineMultiVarStoreBuilder(object): varDataIdx = self._varDataIndices.get(key) if varDataIdx is not None: self._outer = varDataIdx - self._data = self._store.VarData[varDataIdx] + self._data = self._store.MultiVarData[varDataIdx] self._cache = self._varDataCaches[key] if len(self._data.Item) == 0xFFFF: # This is full. Need new one. @@ -79,8 +79,8 @@ class OnlineMultiVarStoreBuilder(object): if varDataIdx is None: self._data = buildMultiVarData(regionIndices, []) - self._outer = len(self._store.VarData) - self._store.VarData.append(self._data) + 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] = {} @@ -133,7 +133,7 @@ ot.MultiVarData.addItem = MultiVarData_addItem def MultiVarStore___bool__(self): - return bool(self.VarData) + return bool(self.MultiVarData) ot.MultiVarStore.__bool__ = MultiVarStore___bool__ From bcd5e4c2163b8a6b792aa8b78983a1ce219296fe Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 6 Feb 2024 15:30:27 -0700 Subject: [PATCH 015/114] Rip out glyf1 VarComposites In favor of separate VARC table. ttGlyphSet does NOT yet know how to draw VARC table though. The 9 failing tests are all VarComposite-related and need to be updated with VARC equivalents eventually when we add VARC support to subsetter and instancer. --- Lib/fontTools/fontBuilder.py | 6 +- Lib/fontTools/merge/tables.py | 2 +- Lib/fontTools/ttLib/scaleUpem.py | 55 +-- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 465 +-------------------- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 5 - Lib/fontTools/ttLib/ttGlyphSet.py | 36 -- Lib/fontTools/ttLib/woff2.py | 2 - Lib/fontTools/varLib/instancer/__init__.py | 25 +- Lib/fontTools/varLib/mutator.py | 14 +- 9 files changed, 16 insertions(+), 594 deletions(-) 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/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index 2909bfcb2..0df563f27 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -11,7 +11,6 @@ from fontTools.cffLib import VarStoreData import fontTools.cffLib.specializer as cffSpecializer from fontTools.varLib import builder # for VarData.calculateNumShorts from fontTools.misc.fixedTools import otRound -from fontTools.ttLib.tables._g_l_y_f import VarComponentFlags __all__ = ["scale_upem", "ScalerVisitor"] @@ -123,13 +122,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,56 +130,15 @@ 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]) 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..e6b7ee8d6 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -245,11 +245,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/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index b4beb3e76..7cfdfd75f 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -178,10 +178,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 +190,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() @@ -300,11 +269,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/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index 7120b0831..78b608ef2 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -843,23 +843,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 @@ -908,11 +891,9 @@ def instantiateGvar(varfont, axisLimits, optimize=True): glyphnames = sorted( glyf.glyphOrder, key=lambda name: ( - ( - glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() or glyf[name].isVarComposite() - else 0 - ), + glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth + if glyf[name].isComposite() + else 0, name, ), ) diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py index c7c37dabc..ae848cc4e 100644 --- a/Lib/fontTools/varLib/mutator.py +++ b/Lib/fontTools/varLib/mutator.py @@ -199,11 +199,9 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True): glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( - ( - glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() or glyf[name].isVarComposite() - else 0 - ), + glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth + if glyf[name].isComposite() + else 0, name, ), ) @@ -307,9 +305,9 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True): if applies: assert record.FeatureTableSubstitution.Version == 0x00010000 for rec in record.FeatureTableSubstitution.SubstitutionRecord: - table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = ( - rec.Feature - ) + table.FeatureList.FeatureRecord[ + rec.FeatureIndex + ].Feature = rec.Feature break del table.FeatureVariations From e9551c483ac42bea8bd59f0ed09c52fa541bba48 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 19:20:39 -0700 Subject: [PATCH 016/114] Remove some more remnants of VarComposites in ttGlyphSet Going to re-add for VARC table. --- Lib/fontTools/ttLib/ttGlyphSet.py | 47 ++----------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 7cfdfd75f..8f329885e 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -30,11 +30,6 @@ class _TTGlyphSet(Mapping): else {} ) self.location = location if location is not None else {} - self.rawLocation = {} # VarComponent-only location - self.originalLocation = location if location is not None else {} - self.depth = 0 - self.locationStack = [] - self.rawLocationStack = [] self.glyphsMapping = glyphsMapping self.hMetrics = font["hmtx"].metrics self.vMetrics = getattr(font.get("vmtx"), "metrics", None) @@ -49,34 +44,6 @@ class _TTGlyphSet(Mapping): ) # TODO VVAR, VORG - @contextmanager - def pushLocation(self, location, reset: bool): - self.locationStack.append(self.location) - self.rawLocationStack.append(self.rawLocation) - if reset: - self.location = self.originalLocation.copy() - self.rawLocation = self.defaultLocationNormalized.copy() - else: - self.location = self.location.copy() - self.rawLocation = {} - self.location.update(location) - self.rawLocation.update(location) - - try: - yield None - finally: - self.location = self.locationStack.pop() - self.rawLocation = self.rawLocationStack.pop() - - @contextmanager - def pushDepth(self): - try: - depth = self.depth - self.depth += 1 - yield depth - finally: - self.depth -= 1 - def __contains__(self, glyphName): return glyphName in self.glyphsMapping @@ -173,24 +140,14 @@ class _TTGlyphGlyf(_TTGlyph): how that works. """ glyph, offset = self._getGlyphAndOffset() - - with self.glyphSet.pushDepth() as depth: - if depth: - offset = 0 # Offset should only apply at top-level - - glyph.draw(pen, self.glyphSet.glyfTable, offset) + glyph.draw(pen, self.glyphSet.glyfTable, offset) def drawPoints(self, pen): """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details how that works. """ glyph, offset = self._getGlyphAndOffset() - - with self.glyphSet.pushDepth() as depth: - if depth: - offset = 0 # Offset should only apply at top-level - - glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) + glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) def _getGlyphAndOffset(self): if self.glyphSet.location and self.glyphSet.gvarTable is not None: From a30ebf0a2ff0c7b8ac172fea10e9de47cc4d8731 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 19:22:24 -0700 Subject: [PATCH 017/114] Revert "Remove some more remnants of VarComposites in ttGlyphSet" This reverts commit 98d30dcf45e71b4352d9bfe2fee6ce4bfa198248. --- Lib/fontTools/ttLib/ttGlyphSet.py | 47 +++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 8f329885e..7cfdfd75f 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -30,6 +30,11 @@ class _TTGlyphSet(Mapping): else {} ) self.location = location if location is not None else {} + self.rawLocation = {} # VarComponent-only location + self.originalLocation = location if location is not None else {} + self.depth = 0 + self.locationStack = [] + self.rawLocationStack = [] self.glyphsMapping = glyphsMapping self.hMetrics = font["hmtx"].metrics self.vMetrics = getattr(font.get("vmtx"), "metrics", None) @@ -44,6 +49,34 @@ class _TTGlyphSet(Mapping): ) # TODO VVAR, VORG + @contextmanager + def pushLocation(self, location, reset: bool): + self.locationStack.append(self.location) + self.rawLocationStack.append(self.rawLocation) + if reset: + self.location = self.originalLocation.copy() + self.rawLocation = self.defaultLocationNormalized.copy() + else: + self.location = self.location.copy() + self.rawLocation = {} + self.location.update(location) + self.rawLocation.update(location) + + try: + yield None + finally: + self.location = self.locationStack.pop() + self.rawLocation = self.rawLocationStack.pop() + + @contextmanager + def pushDepth(self): + try: + depth = self.depth + self.depth += 1 + yield depth + finally: + self.depth -= 1 + def __contains__(self, glyphName): return glyphName in self.glyphsMapping @@ -140,14 +173,24 @@ class _TTGlyphGlyf(_TTGlyph): how that works. """ glyph, offset = self._getGlyphAndOffset() - glyph.draw(pen, self.glyphSet.glyfTable, offset) + + with self.glyphSet.pushDepth() as depth: + if depth: + offset = 0 # Offset should only apply at top-level + + glyph.draw(pen, self.glyphSet.glyfTable, offset) def drawPoints(self, pen): """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details how that works. """ glyph, offset = self._getGlyphAndOffset() - glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) + + with self.glyphSet.pushDepth() as depth: + if depth: + offset = 0 # Offset should only apply at top-level + + glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) def _getGlyphAndOffset(self): if self.glyphSet.location and self.glyphSet.gvarTable is not None: From 0e52857ffe3ba5aa6d96c49310d9d8937c46f5cb Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 21:29:02 -0700 Subject: [PATCH 018/114] [VARC] Start drawing VARC glyphs --- Lib/fontTools/ttLib/ttFont.py | 10 +++-- Lib/fontTools/ttLib/ttGlyphSet.py | 65 +++++++++++++++++++++++++++ Lib/fontTools/varLib/multiVarStore.py | 51 +++++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py index 8a9f146b2..c2008e84a 100644 --- a/Lib/fontTools/ttLib/ttFont.py +++ b/Lib/fontTools/ttLib/ttFont.py @@ -4,7 +4,7 @@ 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 +764,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 7cfdfd75f..295bc5371 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -93,6 +93,15 @@ class _TTGlyphSet(Mapping): return glyphName in self.glyphsMapping +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 _TTGlyphSetGlyf(_TTGlyphSet): def __init__(self, font, location, recalcBounds=True): self.glyfTable = font["glyf"] @@ -123,6 +132,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 @@ -252,6 +274,49 @@ class _TTGlyphCFF(_TTGlyph): self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) +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 + glyphSet = self.glyphSet + varc = glyphSet.varcTable + idx = varc.Coverage.glyphs.index(self.name) + glyph = varc.VarCompositeGlyphs.glyphs[idx] + + from fontTools.varLib.multiVarStore import MultiVarStoreInstancer + + instancer = MultiVarStoreInstancer(varc.MultiVarStore, self.glyphSet.font["fvar"].axes, self.glyphSet.location) + + for comp in glyph.components: + + comp = copy(comp) # Shallow copy + + with self.glyphSet.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 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 diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index a0b246a11..c74b57075 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -1,5 +1,6 @@ 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 from fontTools.varLib.builder import ( @@ -8,6 +9,7 @@ from fontTools.varLib.builder import ( buildMultiVarStore, buildMultiVarData, ) +from itertools import batched from functools import partial from collections import defaultdict from heapq import heappush, heappop @@ -137,3 +139,52 @@ def MultiVarStore___bool__(self): 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.VarRegionList.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): + assert len(deltas) % len(scalars) == 0 + m = len(deltas) // len(scalars) + delta = Vector([0] * m) + for d, s in zip((Vector(d) for d in batched(deltas, m)), scalars): + if not s: + continue + delta += d * s + return tuple(delta) + + def __getitem__(self, varidx): + major, minor = varidx >> 16, varidx & 0xFFFF + if varidx == NO_VARIATION_INDEX: + return () + 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) From 8cce745d90a5c8439a200755d44096196be9d177 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 21:40:12 -0700 Subject: [PATCH 019/114] [TupleVariation] Assert message --- Lib/fontTools/ttLib/tables/TupleVariation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py index 4fab05ac8..30c06f3fd 100644 --- a/Lib/fontTools/ttLib/tables/TupleVariation.py +++ b/Lib/fontTools/ttLib/tables/TupleVariation.py @@ -513,7 +513,7 @@ 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 numDeltas is None or len(result) == numDeltas From 68277fc0b5552d08920208da6c051b44e7ba9e0a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 15 Dec 2023 21:46:39 -0700 Subject: [PATCH 020/114] [TupleVariation] Fix 32bit reading / writing --- Lib/fontTools/ttLib/tables/TupleVariation.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py index 30c06f3fd..a98bca2e0 100644 --- a/Lib/fontTools/ttLib/tables/TupleVariation.py +++ b/Lib/fontTools/ttLib/tables/TupleVariation.py @@ -447,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: @@ -471,11 +475,13 @@ class TupleVariation(object): 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("l", deltas[offset : offset + 64]) + a = array.array("i", deltas[offset : offset + 64]) if sys.byteorder != "big": a.byteswap() bytearr.extend(a) @@ -483,7 +489,7 @@ class TupleVariation(object): runLength -= 64 if runLength: bytearr.append(DELTAS_ARE_LONGS | (runLength - 1)) - a = array.array("l", deltas[offset:pos]) + a = array.array("i", deltas[offset:pos]) if sys.byteorder != "big": a.byteswap() bytearr.extend(a) @@ -502,9 +508,9 @@ class TupleVariation(object): result.extend([0] * numDeltasInRun) else: if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_LONGS: - deltas = array.array("l") + deltas = array.array("i") deltasSize = numDeltasInRun * 4 - if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_WORDS: + elif (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_WORDS: deltas = array.array("h") deltasSize = numDeltasInRun * 2 else: From 4b6c574d54ea046c4bf4b0617d3a7d08564eca03 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 00:37:55 -0700 Subject: [PATCH 021/114] [VARC] More towards drawing --- Lib/fontTools/ttLib/tables/otTables.py | 26 +++++++++++++ Lib/fontTools/ttLib/ttGlyphSet.py | 51 ++++++++++++++++++-------- Lib/fontTools/varLib/multiVarStore.py | 13 ++++--- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 3a239aca5..c30e11b01 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -17,6 +17,7 @@ from fontTools.misc.arrayTools import quantizeRect from fontTools.misc.roundTools import otRound 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 ( @@ -244,6 +245,7 @@ class VarComponent: for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): value = getattr(self.transform, attr_name) + # How to drop scaleX == scaleY here? data.append(write_transform_component(value, mapping_values)) if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: @@ -323,6 +325,30 @@ class VarComponent: result = self.__eq__(other) return result if result is NotImplemented else not result + def getComponentValues(self): + flags = self.flags + + if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + locationValues = [fl2fi(v, 14) for v in self.location.values()] + else: + locationValues = [] + + transformValues = [] + + def append_transform_component(value, values): + nonlocal transformValues + if flags & values.flag: + transformValues.append( + fl2fi(value / values.scale, values.fractionalBits) + ) + + if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: + for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): + value = getattr(self.transform, attr_name) + append_transform_component(value, mapping_values) + + return Vector(locationValues), Vector(transformValues) + class CvarEncodedValues(BaseTable): def __init__(self, values=None): diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 295bc5371..1754de7b3 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -102,6 +102,7 @@ class _TTGlyphSetGlyf(_TTGlyphSet): def __getitem__(self, glyphName): return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) + class _TTGlyphSetGlyf(_TTGlyphSet): def __init__(self, font, location, recalcBounds=True): self.glyfTable = font["glyf"] @@ -275,12 +276,15 @@ class _TTGlyphCFF(_TTGlyph): 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 + from fontTools.ttLib.tables.otTables import ( + VarComponentFlags, + NO_VARIATION_INDEX, + ) + glyphSet = self.glyphSet varc = glyphSet.varcTable idx = varc.Coverage.glyphs.index(self.name) @@ -288,27 +292,42 @@ class _TTGlyphVARC(_TTGlyph): from fontTools.varLib.multiVarStore import MultiVarStoreInstancer - instancer = MultiVarStoreInstancer(varc.MultiVarStore, self.glyphSet.font["fvar"].axes, self.glyphSet.location) + instancer = MultiVarStoreInstancer( + varc.MultiVarStore, self.glyphSet.font["fvar"].axes, self.glyphSet.location + ) + instancer.setLocation(self.glyphSet.location) for comp in glyph.components: + comp = copy(comp) # Shallow copy + locationValues, transformValues = comp.getComponentValues() - comp = copy(comp) # Shallow copy + if comp.locationVarIndex != NO_VARIATION_INDEX: + assert locationValues + locationDeltas = instancer[comp.locationVarIndex] + locationValues = list(locationValues + locationDeltas) + if comp.transformVarIndex != NO_VARIATION_INDEX: + assert transformValues + transformDeltas = instancer[comp.transformVarIndex] + transformValues = list(transformValues + transformDeltas) with self.glyphSet.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) + 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 draw(self, pen): self._draw(pen, False) diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index c74b57075..edb389e82 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -166,19 +166,22 @@ class MultiVarStoreInstancer(object): @staticmethod def interpolateFromDeltasAndScalars(deltas, scalars): - assert len(deltas) % len(scalars) == 0 + deltas = deltas.values + 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((Vector(d) for d in batched(deltas, m)), scalars): + for d, s in zip(batched(deltas, m), scalars): if not s: continue - delta += d * s - return tuple(delta) + delta += Vector(d) * s + return delta def __getitem__(self, varidx): major, minor = varidx >> 16, varidx & 0xFFFF if varidx == NO_VARIATION_INDEX: - return () + return Vector([]) varData = self._varData scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex] deltas = varData[major].Item[minor] From c69fd12251115fb2a9b2d8be5c1d42660a18ba78 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 00:48:22 -0700 Subject: [PATCH 022/114] [VARC] Move code around --- Lib/fontTools/ttLib/tables/otTables.py | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index c30e11b01..ddce172be 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -350,29 +350,6 @@ class VarComponent: return Vector(locationValues), Vector(transformValues) -class CvarEncodedValues(BaseTable): - def __init__(self, values=None): - self.values = values or [] - - def populateDefaults(self, propagator=None): - if not hasattr(self, "values"): - self.values = [] - - def decompile(self, data, font): - self.values = TupleVariation.decompileDeltas_(None, data)[0] - - def compile(self, font): - return bytes(TupleVariation.compileDeltaValues_(self.values, bytearr=None)) - - def toXML(self, xmlWriter, font, attrs, name): - xmlWriter.simpletag(name, attrs + [("value", self.values)]) - xmlWriter.newline() - - def fromXML(self, name, attrs, content, font): - self.populateDefaults() - self.values = safeEval(attrs["value"]) - - class VarCompositeGlyph(BaseTable): def populateDefaults(self, propagator=None): if not hasattr(self, "components"): @@ -422,6 +399,29 @@ class VarCompositeGlyphs(BaseTable): self.glyphs.append(glyph) +class CvarEncodedValues(BaseTable): + def __init__(self, values=None): + self.values = values or [] + + def populateDefaults(self, propagator=None): + if not hasattr(self, "values"): + self.values = [] + + def decompile(self, data, font): + self.values = TupleVariation.decompileDeltas_(None, data)[0] + + def compile(self, font): + return bytes(TupleVariation.compileDeltaValues_(self.values, bytearr=None)) + + def toXML(self, xmlWriter, font, attrs, name): + xmlWriter.simpletag(name, attrs + [("value", self.values)]) + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + self.populateDefaults() + self.values = safeEval(attrs["value"]) + + class AATStateTable(object): def __init__(self): self.GlyphClasses = {} # GlyphID --> GlyphClass From bbad70ef8a58a5e98ee303ee7198938f2fc14d68 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 01:32:46 -0700 Subject: [PATCH 023/114] [VARC] bool(Vector) is useless... :( --- Lib/fontTools/ttLib/ttGlyphSet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 1754de7b3..a026dda41 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -302,11 +302,11 @@ class _TTGlyphVARC(_TTGlyph): locationValues, transformValues = comp.getComponentValues() if comp.locationVarIndex != NO_VARIATION_INDEX: - assert locationValues + assert len(locationValues) locationDeltas = instancer[comp.locationVarIndex] locationValues = list(locationValues + locationDeltas) if comp.transformVarIndex != NO_VARIATION_INDEX: - assert transformValues + assert len(transformValues) transformDeltas = instancer[comp.transformVarIndex] transformValues = list(transformValues + transformDeltas) From 950d39b9d3a99c9d26864a3cf171aa96b22f2042 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 01:45:34 -0700 Subject: [PATCH 024/114] [VARC] Finish drawing! --- Lib/fontTools/ttLib/tables/otTables.py | 33 ++++++++++++++++++++++---- Lib/fontTools/ttLib/ttGlyphSet.py | 2 ++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index ddce172be..5c449b78e 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -328,10 +328,7 @@ class VarComponent: def getComponentValues(self): flags = self.flags - if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - locationValues = [fl2fi(v, 14) for v in self.location.values()] - else: - locationValues = [] + locationValues = [fl2fi(v, 14) for v in self.location.values()] transformValues = [] @@ -349,6 +346,34 @@ class VarComponent: return Vector(locationValues), Vector(transformValues) + def setComponentValues(self, locationValues, transformValues): + flags = self.flags + + assert len(locationValues) == len(self.location), ( + len(locationValues), + len(self.location), + ) + self.location = { + tag: fi2fl(v, 14) for tag, v in zip(self.location.keys(), locationValues) + } + + self.transform = DecomposedTransform(self.transform) + i = 0 + + def read_transform_component(values): + nonlocal transformValues, i + if flags & values.flag: + v = fi2fl(transformValues[i], values.fractionalBits) * values.scale + i += 1 + return v + else: + return values.defaultValue + + for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): + value = read_transform_component(mapping_values) + setattr(self.transform, attr_name, value) + assert i == len(transformValues), (i, len(transformValues)) + class VarCompositeGlyph(BaseTable): def populateDefaults(self, propagator=None): diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index a026dda41..2e3deb2d5 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -310,6 +310,8 @@ class _TTGlyphVARC(_TTGlyph): transformDeltas = instancer[comp.transformVarIndex] transformValues = list(transformValues + transformDeltas) + comp.setComponentValues(locationValues, transformValues) + with self.glyphSet.glyphSet.pushLocation( comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES ): From e22953e807195491f4951cf8123294f0a9d6bb04 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 01:48:56 -0700 Subject: [PATCH 025/114] Black --- Lib/fontTools/ttLib/ttFont.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py index c2008e84a..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, _TTGlyphSetVARC +from fontTools.ttLib.ttGlyphSet import ( + _TTGlyph, + _TTGlyphSetCFF, + _TTGlyphSetGlyf, + _TTGlyphSetVARC, +) from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter from io import BytesIO, StringIO, UnsupportedOperation import os From c50a0f61959d8036c1fdb300f5eed56f8a5c9309 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 11:11:59 -0700 Subject: [PATCH 026/114] [VARC] Rename a type --- Lib/fontTools/ttLib/tables/otConverters.py | 4 ++-- Lib/fontTools/ttLib/tables/otTables.py | 2 +- Lib/fontTools/varLib/multiVarStore.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 8bd685d0b..4bb61e947 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -19,7 +19,7 @@ from .otBase import ( from .otTables import ( lookupTypes, VarCompositeGlyph, - CvarEncodedValues, + TupleValues, AATStateTable, AATState, AATAction, @@ -1999,7 +1999,7 @@ converterMapping = { "CompositeMode": CompositeMode, "STATFlags": STATFlags, "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph), - "MultiVarDataValue": partial(CFF2Index, itemClass=CvarEncodedValues), + "MultiVarDataValue": partial(CFF2Index, itemClass=TupleValues), # AAT "CIDGlyphMap": CIDGlyphMap, "GlyphCIDMap": GlyphCIDMap, diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 5c449b78e..861ea4f68 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -424,7 +424,7 @@ class VarCompositeGlyphs(BaseTable): self.glyphs.append(glyph) -class CvarEncodedValues(BaseTable): +class TupleValues(BaseTable): def __init__(self, values=None): self.values = values or [] diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index edb389e82..fbee351a4 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -125,7 +125,7 @@ def MultiVarData_addItem(self, deltas, *, round=round): for d in deltas: values.extend(d) - values = ot.CvarEncodedValues(values) + values = ot.TupleValues(values) self.Item.append(values) self.ItemCount = len(self.Item) From c952237ed3333613ca0aa096a97503e339d45e87 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 11:51:15 -0700 Subject: [PATCH 027/114] [otConverters] Make _LazyList generic --- Lib/fontTools/ttLib/tables/otConverters.py | 43 +++++++++++----------- Tests/ttLib/tables/otConverters_test.py | 17 --------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 4bb61e947..83b8bc0f8 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -33,6 +33,7 @@ from .otTables import ( ) from itertools import zip_longest, accumulate from functools import partial +from types import SimpleNamespace import re import struct from typing import Optional @@ -107,10 +108,6 @@ def buildConverters(tableSpec, tableNamespace): return converters, convertersByName -class _MissingItem(tuple): - __slots__ = () - - try: from collections import UserList except ImportError: @@ -118,19 +115,15 @@ except ImportError: 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 + v = self.data[k] + if callable(v): + v = v() + self.data[k] = v + return v def __add__(self, other): if isinstance(other, _LazyList): @@ -194,15 +187,23 @@ 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)) + closure = SimpleNamespace() + closure.reader = reader.copy() + closure.pos = closure.reader.pos + closure.font = font + closure.conv = self + closure.recordSize = recordSize + + def get_callable(i): + def read_item(i): + closure.reader.seek(closure.pos + i * closure.recordSize) + return closure.conv.read(closure.reader, closure.font, {}) + + return lambda: read_item(i) + reader.advance(count * recordSize) - return l + + return _LazyList(get_callable(i) for i in range(count)) def getRecordSize(self, reader): if hasattr(self, "staticSize"): diff --git a/Tests/ttLib/tables/otConverters_test.py b/Tests/ttLib/tables/otConverters_test.py index 94b62a1f7..37ae467d0 100644 --- a/Tests/ttLib/tables/otConverters_test.py +++ b/Tests/ttLib/tables/otConverters_test.py @@ -438,23 +438,6 @@ 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]) From 5faf139417b023b4df32669789156daa8db4fe48 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 11:53:56 -0700 Subject: [PATCH 028/114] [CFF2Index] Minor massage --- Lib/fontTools/ttLib/tables/otConverters.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 83b8bc0f8..b0704f80f 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1846,6 +1846,8 @@ class CFF2Index(BaseConverter): self.itemClass = itemClass def read(self, reader, font, tableDict): + lazy = font.lazy and count > 8 + count = reader.readULong() if count == 0: return [] @@ -1861,18 +1863,18 @@ class CFF2Index(BaseConverter): items = [] lastOffset = offsets[0] reader.readData(lastOffset) # In case first offset is not 0 + for offset in offsets[1:]: assert lastOffset <= offset - items.append(reader.readData(offset - lastOffset)) - lastOffset = offset + item = reader.readData(offset - lastOffset) - if self.itemClass is not None: - newItems = [] - for item in items: + if self.itemClass is not None: obj = self.itemClass() obj.decompile(item, font) - newItems.append(obj) - items = newItems + item = obj + + items.append(item) + lastOffset = offset return items From 450c8f158125b6e788932bf97324dca57a97755e Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 12:25:17 -0700 Subject: [PATCH 029/114] [CFF2Index] Make loading lazy Still too slow, but much faster. --- Lib/fontTools/ttLib/tables/otConverters.py | 52 +++++++++++++++------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index b0704f80f..ab7f13d1e 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1846,8 +1846,6 @@ class CFF2Index(BaseConverter): self.itemClass = itemClass def read(self, reader, font, tableDict): - lazy = font.lazy and count > 8 - count = reader.readULong() if count == 0: return [] @@ -1860,23 +1858,47 @@ class CFF2Index(BaseConverter): }[offSize] offsets = readArray(count + 1) - items = [] - lastOffset = offsets[0] - reader.readData(lastOffset) # In case first offset is not 0 + lazy = font.lazy is not False and count > 8 + if not lazy: + items = [] + lastOffset = offsets.pop(0) + reader.readData(lastOffset) # In case first offset is not 0 - for offset in offsets[1:]: - assert lastOffset <= offset - item = reader.readData(offset - lastOffset) + 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) - item = obj + if self.itemClass is not None: + obj = self.itemClass() + obj.decompile(item, font) + item = obj - items.append(item) - lastOffset = offset + items.append(item) + lastOffset = offset + return items + else: + closure = SimpleNamespace() + closure.reader = reader.copy() + closure.pos = closure.reader.pos + closure.font = font + closure.itemClass = self.itemClass + closure.offsets = offsets - return items + def get_callable(i): + def read_item(i): + closure.reader.seek(closure.pos + closure.offsets[i]) + item = closure.reader.readData(closure.offsets[i + 1] - closure.offsets[i]) + + if closure.itemClass is not None: + obj = closure.itemClass() + obj.decompile(item, closure.font) + item = obj + + return item + + return lambda: read_item(i) + + return _LazyList(get_callable(i) for i in range(count)) def write(self, writer, font, tableDict, values, repeatIndex=None): items = values From feb6820d03d2ab5e1bce8482e2a79341a47181a3 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 12:30:26 -0700 Subject: [PATCH 030/114] [_LazyList] Make much faster --- Lib/fontTools/ttLib/tables/otConverters.py | 58 ++++++++++------------ 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index ab7f13d1e..1c57063e5 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -121,7 +121,7 @@ class _LazyList(UserList): return [self[i] for i in indices] v = self.data[k] if callable(v): - v = v() + v = v(self, k) self.data[k] = v return v @@ -187,23 +187,20 @@ class BaseConverter(object): l.append(self.read(reader, font, tableDict)) return l else: - closure = SimpleNamespace() - closure.reader = reader.copy() - closure.pos = closure.reader.pos - closure.font = font - closure.conv = self - closure.recordSize = recordSize + def read_item(self, i): + self.reader.seek(self.pos + i * self.recordSize) + return self.conv.read(self.reader, self.font, {}) - def get_callable(i): - def read_item(i): - closure.reader.seek(closure.pos + i * closure.recordSize) - return closure.conv.read(closure.reader, closure.font, {}) - - return lambda: read_item(i) + l = _LazyList(read_item for i in range(count)) + l.reader = reader.copy() + l.pos = l.reader.pos + l.font = font + l.conv = self + l.recordSize = recordSize reader.advance(count * recordSize) - return _LazyList(get_callable(i) for i in range(count)) + return l def getRecordSize(self, reader): if hasattr(self, "staticSize"): @@ -1877,28 +1874,25 @@ class CFF2Index(BaseConverter): lastOffset = offset return items else: - closure = SimpleNamespace() - closure.reader = reader.copy() - closure.pos = closure.reader.pos - closure.font = font - closure.itemClass = self.itemClass - closure.offsets = offsets + def read_item(self, i): + self.reader.seek(self.pos + self.offsets[i]) + item = self.reader.readData(self.offsets[i + 1] - self.offsets[i]) - def get_callable(i): - def read_item(i): - closure.reader.seek(closure.pos + closure.offsets[i]) - item = closure.reader.readData(closure.offsets[i + 1] - closure.offsets[i]) + if self.itemClass is not None: + obj = self.itemClass() + obj.decompile(item, self.font) + item = obj - if closure.itemClass is not None: - obj = closure.itemClass() - obj.decompile(item, closure.font) - item = obj + return item - return item + l = _LazyList(read_item for i in range(count)) + l.reader = reader.copy() + l.pos = l.reader.pos + l.font = font + l.itemClass = self.itemClass + l.offsets = offsets - return lambda: read_item(i) - - return _LazyList(get_callable(i) for i in range(count)) + return l def write(self, writer, font, tableDict, values, repeatIndex=None): items = values From 7de1306d9267e076f4c616665d28d2564fb21d92 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 12:50:57 -0700 Subject: [PATCH 031/114] [CFF2Index] Make even faster Load offsets on-demand as well. --- Lib/fontTools/ttLib/tables/otConverters.py | 33 ++++++++++++++-------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 1c57063e5..fe5fa364e 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1847,16 +1847,19 @@ class CFF2Index(BaseConverter): if count == 0: return [] offSize = reader.readUInt8() - readArray = { - 1: reader.readUInt8Array, - 2: reader.readUShortArray, - 3: reader.readUInt24Array, - 4: reader.readULongArray, - }[offSize] - offsets = readArray(count + 1) + + 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) # In case first offset is not 0 @@ -1875,8 +1878,10 @@ class CFF2Index(BaseConverter): return items else: def read_item(self, i): - self.reader.seek(self.pos + self.offsets[i]) - item = self.reader.readData(self.offsets[i + 1] - self.offsets[i]) + self.reader.seek(self.offset_pos + i * self.offSize) + offsets = self.readArray(2) + self.reader.seek(self.data_pos + offsets[0]) + item = self.reader.readData(offsets[1] - offsets[0]) if self.itemClass is not None: obj = self.itemClass() @@ -1885,12 +1890,16 @@ class CFF2Index(BaseConverter): return item - l = _LazyList(read_item for i in range(count)) + l = _LazyList([read_item] * count) l.reader = reader.copy() - l.pos = l.reader.pos + l.offset_pos = l.reader.pos + l.data_pos = l.offset_pos + (count + 1) * offSize l.font = font l.itemClass = self.itemClass - l.offsets = offsets + l.offSize = offSize + l.readArray = getReadArray(l.reader, offSize) + + # TODO: Advance reader return l From 61916c1034519a0c8eec2bae44180ead624648db Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 20:19:51 -0700 Subject: [PATCH 032/114] [gvar] Speed up loading a bit More to come. --- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index e6b7ee8d6..5b3b10005 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -47,7 +47,7 @@ class _LazyDict(UserDict): def __getitem__(self, k): v = self.data[k] if callable(v): - v = v() + v = v(self, k) self.data[k] = v return v @@ -128,20 +128,23 @@ class table__g_v_a_r(DefaultTable.DefaultTable): offsetToData = self.offsetToGlyphVariationData glyf = ttFont["glyf"] - def decompileVarGlyph(glyphName, gid): + def decompileVarGlyph(self, glyphName): + gid = self.reverseGlyphMap[glyphName] gvarData = data[ offsetToData + offsets[gid] : offsetToData + offsets[gid + 1] ] if not gvarData: return [] - glyph = glyf[glyphName] - numPointsInGlyph = self.getNumPoints_(glyph) + glyph = self._glyf[glyphName] + numPointsInGlyph = self._gvar.getNumPoints_(glyph) return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData) - for gid in range(self.glyphCount): - glyphName = glyphs[gid] - variations[glyphName] = partial(decompileVarGlyph, glyphName, gid) - self.variations = _LazyDict(variations) + l = _LazyDict({glyphs[gid]: decompileVarGlyph for gid in range(self.glyphCount)}) + l.reverseGlyphMap = ttFont.getReverseGlyphMap() + l._glyf = glyf + l._gvar = self + + self.variations = l if ttFont.lazy is False: # Be lazy for None and True self.ensureDecompiled() From 7471ac6d4d533a5434d253b3f9697463ba978a26 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 20:53:20 -0700 Subject: [PATCH 033/114] [gvar] Speed up loading by not reading all offsets --- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 45 ++++++++++++++++---------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index 5b3b10005..70b655f60 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -52,6 +52,26 @@ class _LazyDict(UserDict): return v +def _decompileVarGlyph(self, glyphName): + gid = self.reverseGlyphMap[glyphName] + offsetSize = 2 if self.tableFormat == 0 else 4 + startOffset = GVAR_HEADER_SIZE + offsetSize * gid + endOffset = startOffset + offsetSize * 2 + offsets = table__g_v_a_r.decompileOffsets_( + self.gvarData[startOffset:endOffset], + tableFormat=self.tableFormat, + glyphCount=1, + ) + gvarData = self.gvarData[ + self.offsetToData + offsets[0] : self.offsetToData + offsets[1] + ] + if not gvarData: + return [] + glyph = self._glyf[glyphName] + numPointsInGlyph = self._gvar.getNumPoints_(glyph) + return decompileGlyph_(numPointsInGlyph, self.sharedCoords, self.axisTags, gvarData) + + class table__g_v_a_r(DefaultTable.DefaultTable): dependencies = ["fvar", "glyf"] @@ -116,11 +136,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,21 +143,17 @@ class table__g_v_a_r(DefaultTable.DefaultTable): offsetToData = self.offsetToGlyphVariationData glyf = ttFont["glyf"] - def decompileVarGlyph(self, glyphName): - gid = self.reverseGlyphMap[glyphName] - gvarData = data[ - offsetToData + offsets[gid] : offsetToData + offsets[gid + 1] - ] - if not gvarData: - return [] - glyph = self._glyf[glyphName] - numPointsInGlyph = self._gvar.getNumPoints_(glyph) - return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData) - - l = _LazyDict({glyphs[gid]: decompileVarGlyph for gid in range(self.glyphCount)}) + l = _LazyDict( + {glyphs[gid]: _decompileVarGlyph for gid in range(self.glyphCount)} + ) l.reverseGlyphMap = ttFont.getReverseGlyphMap() l._glyf = glyf l._gvar = self + l.gvarData = data + l.offsetToData = offsetToData + l.sharedCoords = sharedCoords + l.axisTags = axisTags + l.tableFormat = self.flags & 1 self.variations = l From 3806fd25588ec561f27617f0cd3f61bfb213c06a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 20:55:26 -0700 Subject: [PATCH 034/114] Move a couple of functions outline --- Lib/fontTools/ttLib/tables/otConverters.py | 41 ++++++++++++---------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index fe5fa364e..ce55ed55b 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -140,6 +140,11 @@ class _LazyList(UserList): return other + list(self) +def _base_converter_read_item(self, i): + self.reader.seek(self.pos + i * self.recordSize) + return self.conv.read(self.reader, self.font, {}) + + class BaseConverter(object): """Base class for converter objects. Apart from the constructor, this is an abstract class.""" @@ -187,11 +192,7 @@ class BaseConverter(object): l.append(self.read(reader, font, tableDict)) return l else: - def read_item(self, i): - self.reader.seek(self.pos + i * self.recordSize) - return self.conv.read(self.reader, self.font, {}) - - l = _LazyList(read_item for i in range(count)) + l = _LazyList(_base_converter_read_item for i in range(count)) l.reader = reader.copy() l.pos = l.reader.pos l.font = font @@ -1833,6 +1834,20 @@ class VarDataValue(BaseConverter): return safeEval(attrs["value"]) +def cff2_index_read_item(self, i): + self.reader.seek(self.offset_pos + i * self.offSize) + offsets = self.readArray(2) + self.reader.seek(self.data_pos + offsets[0]) + item = self.reader.readData(offsets[1] - offsets[0]) + + if self.itemClass is not None: + obj = self.itemClass() + obj.decompile(item, self.font) + item = obj + + return item + + class CFF2Index(BaseConverter): def __init__( self, name, repeat, aux, tableClass=None, *, itemClass=None, description="" @@ -1855,6 +1870,7 @@ class CFF2Index(BaseConverter): 3: reader.readUInt24Array, 4: reader.readULongArray, }[offSize] + readArray = getReadArray(reader, offSize) lazy = font.lazy is not False and count > 8 @@ -1877,20 +1893,7 @@ class CFF2Index(BaseConverter): lastOffset = offset return items else: - def read_item(self, i): - self.reader.seek(self.offset_pos + i * self.offSize) - offsets = self.readArray(2) - self.reader.seek(self.data_pos + offsets[0]) - item = self.reader.readData(offsets[1] - offsets[0]) - - if self.itemClass is not None: - obj = self.itemClass() - obj.decompile(item, self.font) - item = obj - - return item - - l = _LazyList([read_item] * count) + l = _LazyList([cff2_index_read_item] * count) l.reader = reader.copy() l.offset_pos = l.reader.pos l.data_pos = l.offset_pos + (count + 1) * offSize From 3ff2ee61e18add5ecf3ca30fb4913412a782e7b2 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 20:58:51 -0700 Subject: [PATCH 035/114] Move lazy datastructures to misc.lazyTools --- Lib/fontTools/misc/lazyTools.py | 42 ++++++++++++++++++++++ Lib/fontTools/ttLib/tables/_g_v_a_r.py | 18 ++-------- Lib/fontTools/ttLib/tables/otConverters.py | 37 ++----------------- Tests/ttLib/tables/otConverters_test.py | 21 ++++++----- 4 files changed, 60 insertions(+), 58 deletions(-) create mode 100644 Lib/fontTools/misc/lazyTools.py diff --git a/Lib/fontTools/misc/lazyTools.py b/Lib/fontTools/misc/lazyTools.py new file mode 100644 index 000000000..a3cc52e53 --- /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(self, 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(self, 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/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index 70b655f60..a44f72b66 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, k) - self.data[k] = v - return v - - def _decompileVarGlyph(self, glyphName): gid = self.reverseGlyphMap[glyphName] offsetSize = 2 if self.tableFormat == 0 else 4 @@ -143,7 +131,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable): offsetToData = self.offsetToGlyphVariationData glyf = ttFont["glyf"] - l = _LazyDict( + l = LazyDict( {glyphs[gid]: _decompileVarGlyph for gid in range(self.glyphCount)} ) l.reverseGlyphMap = ttFont.getReverseGlyphMap() diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index ce55ed55b..b46eeb522 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -8,6 +8,7 @@ from fontTools.misc.fixedTools import ( ) 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, @@ -108,38 +109,6 @@ def buildConverters(tableSpec, tableNamespace): return converters, convertersByName -try: - from collections import UserList -except ImportError: - from UserList import UserList - - -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(self, 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) - - def _base_converter_read_item(self, i): self.reader.seek(self.pos + i * self.recordSize) return self.conv.read(self.reader, self.font, {}) @@ -192,7 +161,7 @@ class BaseConverter(object): l.append(self.read(reader, font, tableDict)) return l else: - l = _LazyList(_base_converter_read_item for i in range(count)) + l = LazyList(_base_converter_read_item for i in range(count)) l.reader = reader.copy() l.pos = l.reader.pos l.font = font @@ -1893,7 +1862,7 @@ class CFF2Index(BaseConverter): lastOffset = offset return items else: - l = _LazyList([cff2_index_read_item] * count) + l = LazyList([cff2_index_read_item] * count) l.reader = reader.copy() l.offset_pos = l.reader.pos l.data_pos = l.offset_pos + (count + 1) * offSize diff --git a/Tests/ttLib/tables/otConverters_test.py b/Tests/ttLib/tables/otConverters_test.py index 37ae467d0..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) @@ -439,8 +442,8 @@ class LazyListTest(unittest.TestCase): self.assertEqual([11, 12], ll[1:3]) def test_add_both_LazyList(self): - ll1 = otConverters._LazyList([1]) - ll2 = otConverters._LazyList([2]) + ll1 = LazyList([1]) + ll2 = LazyList([2]) l3 = ll1 + ll2 @@ -448,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 @@ -458,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 @@ -473,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__": From cae76d540a4c2fde0671eb7c953bb18267ad66f2 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 21:15:20 -0700 Subject: [PATCH 036/114] [glyf] Load using LazyDict Going to revert. No speedup since glyphs are loaded lazy anyway. And one test fails I have no idea why: Tests/ttLib/woff2_test.py::WOFF2GlyfTableTest::test_reconstruct_glyf_padded_4 --- Lib/fontTools/misc/lazyTools.py | 4 --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 48 +++++++++++--------------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/Lib/fontTools/misc/lazyTools.py b/Lib/fontTools/misc/lazyTools.py index a3cc52e53..846757d7e 100644 --- a/Lib/fontTools/misc/lazyTools.py +++ b/Lib/fontTools/misc/lazyTools.py @@ -4,10 +4,6 @@ __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): diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index fa11cf8f4..83dd0bc41 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -8,6 +8,7 @@ from fontTools.misc.transform import DecomposedTransform from fontTools.misc.textTools import tostr, safeEval, pad from fontTools.misc.arrayTools import updateBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds +from fontTools.misc.lazyTools import LazyDict from fontTools.misc.fixedTools import ( fixedToFloat as fi2fl, floatToFixed as fl2fi, @@ -53,6 +54,19 @@ version = ".".join(version.split(".")[:2]) SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple +def _load_glyph(self, glyphName): + gid = self.reverseGlyphMap[glyphName] + offsets = self.loca[gid : gid + 2] + glyphdata = self.glyphData[offsets[0] : offsets[1]] + if len(glyphdata) != offsets[1] - offsets[0]: + raise ttLib.TTLibError( + "not enough glyph data for glyph '%s' at " + "offset %d: expected %d bytes, found %d" + % (glyphName, offsets[0], offsets[1] - offsets[0], len(glyphdata)) + ) + return Glyph(glyphdata) + + class table__g_l_y_f(DefaultTable.DefaultTable): """Glyph Data Table @@ -97,33 +111,13 @@ class table__g_l_y_f(DefaultTable.DefaultTable): [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else [] ) loca = ttFont["loca"] - pos = int(loca[0]) - nextPos = 0 - noname = 0 - self.glyphs = {} - self.glyphOrder = glyphOrder = ttFont.getGlyphOrder() - self._reverseGlyphOrder = {} - for i in range(0, len(loca) - 1): - try: - glyphName = glyphOrder[i] - except IndexError: - noname = noname + 1 - glyphName = "ttxautoglyph%s" % i - nextPos = int(loca[i + 1]) - glyphdata = data[pos:nextPos] - if len(glyphdata) != (nextPos - pos): - raise ttLib.TTLibError("not enough 'glyf' table data") - glyph = Glyph(glyphdata) - self.glyphs[glyphName] = glyph - pos = nextPos - if len(data) - nextPos >= 4: - log.warning( - "too much 'glyf' table data: expected %d, received %d bytes", - nextPos, - len(data), - ) - if noname: - log.warning("%s glyphs have no name", noname) + self.glyphOrder = ttFont.getGlyphOrder() + + l = self.glyphs = LazyDict({k: _load_glyph for k in self.glyphOrder}) + l.reverseGlyphMap = self._reverseGlyphOrder = ttFont.getReverseGlyphMap() + l.loca = loca + l.glyphData = data + if ttFont.lazy is False: # Be lazy for None and True self.ensureDecompiled() From 0a7993998d9ca6841a579796dfaa30f31d163815 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 21:37:05 -0700 Subject: [PATCH 037/114] Revert "[glyf] Load using LazyDict" This reverts commit b493729eff954909672694b67a453964f33ac893. --- Lib/fontTools/misc/lazyTools.py | 4 +++ Lib/fontTools/ttLib/tables/_g_l_y_f.py | 48 +++++++++++++++----------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Lib/fontTools/misc/lazyTools.py b/Lib/fontTools/misc/lazyTools.py index 846757d7e..a3cc52e53 100644 --- a/Lib/fontTools/misc/lazyTools.py +++ b/Lib/fontTools/misc/lazyTools.py @@ -4,6 +4,10 @@ __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): diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 83dd0bc41..fa11cf8f4 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -8,7 +8,6 @@ from fontTools.misc.transform import DecomposedTransform from fontTools.misc.textTools import tostr, safeEval, pad from fontTools.misc.arrayTools import updateBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds -from fontTools.misc.lazyTools import LazyDict from fontTools.misc.fixedTools import ( fixedToFloat as fi2fl, floatToFixed as fl2fi, @@ -54,19 +53,6 @@ version = ".".join(version.split(".")[:2]) SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple -def _load_glyph(self, glyphName): - gid = self.reverseGlyphMap[glyphName] - offsets = self.loca[gid : gid + 2] - glyphdata = self.glyphData[offsets[0] : offsets[1]] - if len(glyphdata) != offsets[1] - offsets[0]: - raise ttLib.TTLibError( - "not enough glyph data for glyph '%s' at " - "offset %d: expected %d bytes, found %d" - % (glyphName, offsets[0], offsets[1] - offsets[0], len(glyphdata)) - ) - return Glyph(glyphdata) - - class table__g_l_y_f(DefaultTable.DefaultTable): """Glyph Data Table @@ -111,13 +97,33 @@ class table__g_l_y_f(DefaultTable.DefaultTable): [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else [] ) loca = ttFont["loca"] - self.glyphOrder = ttFont.getGlyphOrder() - - l = self.glyphs = LazyDict({k: _load_glyph for k in self.glyphOrder}) - l.reverseGlyphMap = self._reverseGlyphOrder = ttFont.getReverseGlyphMap() - l.loca = loca - l.glyphData = data - + pos = int(loca[0]) + nextPos = 0 + noname = 0 + self.glyphs = {} + self.glyphOrder = glyphOrder = ttFont.getGlyphOrder() + self._reverseGlyphOrder = {} + for i in range(0, len(loca) - 1): + try: + glyphName = glyphOrder[i] + except IndexError: + noname = noname + 1 + glyphName = "ttxautoglyph%s" % i + nextPos = int(loca[i + 1]) + glyphdata = data[pos:nextPos] + if len(glyphdata) != (nextPos - pos): + raise ttLib.TTLibError("not enough 'glyf' table data") + glyph = Glyph(glyphdata) + self.glyphs[glyphName] = glyph + pos = nextPos + if len(data) - nextPos >= 4: + log.warning( + "too much 'glyf' table data: expected %d, received %d bytes", + nextPos, + len(data), + ) + if noname: + log.warning("%s glyphs have no name", noname) if ttFont.lazy is False: # Be lazy for None and True self.ensureDecompiled() From 4db90f588ea319747c18f78986d47d7be57da1b3 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 21:54:46 -0700 Subject: [PATCH 038/114] [loca] Minor speedup --- Lib/fontTools/ttLib/tables/_l_o_c_a.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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", From 44a32f8a2af6a7eb50aea68cbb3c9707a1a7f11c Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 23:17:31 -0700 Subject: [PATCH 039/114] Minor refactor --- Lib/fontTools/ttLib/ttGlyphSet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 2e3deb2d5..4602b09a8 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -324,12 +324,13 @@ class _TTGlyphVARC(_TTGlyph): ) except AttributeError: t = comp.transform.toTransform() + glyph = self.glyphSet[comp.glyphName] if isPointPen: tPen = TransformPointPen(pen, t) - self.glyphSet[comp.glyphName].drawPoints(tPen) + glyph.drawPoints(tPen) else: tPen = TransformPen(pen, t) - self.glyphSet[comp.glyphName].draw(tPen) + glyph.draw(tPen) def draw(self, pen): self._draw(pen, False) From 42d6b6b4fedf51dd741d3134da74df04339335b4 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Dec 2023 23:33:18 -0700 Subject: [PATCH 040/114] [svgPen] Write two digits after decimal by default --- Lib/fontTools/pens/svgPathPen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/pens/svgPathPen.py b/Lib/fontTools/pens/svgPathPen.py index 29d41a802..8e2a6f3a6 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,7 @@ 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 From 15141589ff41361a4cfc276a671d59241edcafa8 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 00:03:38 -0700 Subject: [PATCH 041/114] [subset] Support VARC We don't prune the MultiVarStore currently. :( --- Lib/fontTools/subset/__init__.py | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 7e6985db2..c48edeeca 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -2629,6 +2629,26 @@ 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) + # TODO Subset MultiVarStore + self.table.VarCompositeGlyphs.glyphs = _list_subset(self.table.VarCompositeGlyphs.glyphs, indices) + return bool(self.table.VarCompositeGlyphs.glyphs) + +@_add_method(ttLib.getTableClass("VARC")) +def closure_glyphs(self, s): + if self.table.VarCompositeGlyphs: + glyphs = s.glyphs + new = True + while new: + new = set() + for glyph in self.table.VarCompositeGlyphs.glyphs: + for comp in glyph.components: + name = comp.glyphName + if name not in glyphs: + glyphs.add(name) + new.add(name) @_add_method(ttLib.getTableClass("MATH")) def closure_glyphs(self, s): @@ -3345,6 +3365,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( From f37f2e4a03779a96dcca8d14a45722bc55f5d945 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 00:09:10 -0700 Subject: [PATCH 042/114] [subset] Close over MATH before GSUB I *think* that's the correct way. --- Lib/fontTools/subset/__init__.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index c48edeeca..909ba7d80 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -3318,20 +3318,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( @@ -3346,6 +3332,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): From 5c278464e0b39443846b10a8a68c2dc4ccbe4673 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 10:52:43 -0700 Subject: [PATCH 043/114] [subset/VARC] Subset MultiVarStore --- Lib/fontTools/subset/__init__.py | 32 +++++++++++++++++++++++++-- Lib/fontTools/varLib/multiVarStore.py | 27 ++++++++++++++++++++++ Lib/fontTools/varLib/varStore.py | 32 +++++++++++++++------------ 3 files changed, 75 insertions(+), 16 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 909ba7d80..4016c3534 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 @@ -2629,13 +2629,17 @@ 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) # TODO Subset MultiVarStore - self.table.VarCompositeGlyphs.glyphs = _list_subset(self.table.VarCompositeGlyphs.glyphs, indices) + self.table.VarCompositeGlyphs.glyphs = _list_subset( + self.table.VarCompositeGlyphs.glyphs, indices + ) return bool(self.table.VarCompositeGlyphs.glyphs) + @_add_method(ttLib.getTableClass("VARC")) def closure_glyphs(self, s): if self.table.VarCompositeGlyphs: @@ -2650,6 +2654,30 @@ def closure_glyphs(self, s): glyphs.add(name) new.add(name) + +@_add_method(ttLib.getTableClass("VARC")) +def prune_post_subset(self, font, options): + table = self.table + + if not hasattr(table, "MultiVarStore"): + return True + + store = table.MultiVarStore + + usedVarIdxes = set() + + # Collect. + table.collect_varidxes(usedVarIdxes) + + # Subset. + varidx_map = store.subset_varidxes(usedVarIdxes) + + # Map. + table.remap_varidxes(varidx_map) + + return True + + @_add_method(ttLib.getTableClass("MATH")) def closure_glyphs(self, s): if self.table.MathVariants: diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index fbee351a4..365e7aad1 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -3,6 +3,7 @@ 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, buildVarRegion, @@ -191,3 +192,29 @@ class MultiVarStoreInstancer(object): 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") + + +ot.MultiVarStore.prune_regions = ot.VarStore.prune_regions +ot.MultiVarStore.subset_varidxes = MultiVarStore_subset_varidxes + + +def VARC_collect_varidxes(self, varidxes): + for glyph in self.VarCompositeGlyphs.glyphs: + for component in glyph.components: + varidxes.add(component.locationVarIndex) + varidxes.add(component.transformVarIndex) + + +def VARC_remap_varidxes(self, varidxes_map): + for glyph in self.VarCompositeGlyphs.glyphs: + for component in glyph.components: + component.locationVarIndex = varidxes_map[component.locationVarIndex] + 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/varStore.py b/Lib/fontTools/varLib/varStore.py index d4e199a4d..ecfc02fc7 100644 --- a/Lib/fontTools/varLib/varStore.py +++ b/Lib/fontTools/varLib/varStore.py @@ -246,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): @@ -296,12 +299,13 @@ 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() + self.prune_regions(VarData=VarData) return varDataMap @@ -309,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"): """Remove unused VarRegions.""" # # Subset VarRegionList @@ -317,7 +321,7 @@ 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 @@ -330,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] From 27e5182677fe84d40b8e02fd1db1b0f4ae07c551 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 22 May 2024 18:46:01 -0600 Subject: [PATCH 044/114] [VARC/instancer] Implement --- Lib/fontTools/varLib/instancer/__init__.py | 57 ++++++++++++++++++++++ Lib/fontTools/varLib/multiVarStore.py | 6 ++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index 78b608ef2..4f8833cea 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -465,6 +465,60 @@ class OverlapMode(IntEnum): REMOVE_AND_IGNORE_ERRORS = 3 +def instantiateVARC(varfont, axisLimits): + log.info("Instantiating VARC tables") + + varc = varfont["VARC"].table + if varc.VarCompositeGlyphs: + for glyph in varc.VarCompositeGlyphs.glyphs: + 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 & VarComponentFlags.AXES_HAVE_VARIATION: + raise NotImplementedError( + "Instancing accross VarComponent axes with variation is not supported." + ) + limits = axisLimits[tag] + loc = limits.renormalizeValue(loc, extrapolate=False) + newLocation[tag] = loc + component.location = newLocation + + if varc.MultiVarStore: + store = varc.MultiVarStore + store.prune_regions() + + fvar = varfont["fvar"] + location = axisLimits.pinnedLocation() + for region in store.VarRegionList.Region: + assert len(region.VarRegionAxis) == len(fvar.axes) + newRegionAxis = [] + for regionTriple, fvarAxis in zip(region.VarRegionAxis, fvar.axes): + tag = fvarAxis.axisTag + if tag in location: + continue + if tag in axisLimits: + limits = axisLimits[tag] + triple = ( + regionTriple.StartCoord, + regionTriple.PeakCoord, + regionTriple.EndCoord, + ) + triple = tuple( + limits.renormalizeValue(v, extrapolate=False) for v in triple + ) + ( + regionTriple.StartCoord, + regionTriple.PeakCoord, + regionTriple.EndCoord, + ) = triple + newRegionAxis.append(regionTriple) + region.VarRegionAxis = newRegionAxis + region.VarRegionAxisCount = len(newRegionAxis) + + def instantiateTupleVariationStore( variations, axisLimits, origCoords=None, endPts=None ): @@ -1579,6 +1633,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/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index 365e7aad1..b3440cc63 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -198,7 +198,11 @@ def MultiVarStore_subset_varidxes(self, varIdxes): return ot.VarStore.subset_varidxes(self, varIdxes, VarData="MultiVarData") -ot.MultiVarStore.prune_regions = ot.VarStore.prune_regions +def MultiVarStore_prune_regions(self, *, VarData="VarData"): + return ot.VarStore.prune_regions(self, VarData="MultiVarData") + + +ot.MultiVarStore.prune_regions = MultiVarStore_prune_regions ot.MultiVarStore.subset_varidxes = MultiVarStore_subset_varidxes From 272f73632a73c7c9182482aa3206b375cf3a7b46 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 12:01:43 -0700 Subject: [PATCH 045/114] [VARC] Fix instanciating component --- Lib/fontTools/ttLib/tables/otTables.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 861ea4f68..4e855bf6f 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -339,10 +339,9 @@ class VarComponent: fl2fi(value / values.scale, values.fractionalBits) ) - if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - value = getattr(self.transform, attr_name) - append_transform_component(value, mapping_values) + for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): + value = getattr(self.transform, attr_name) + append_transform_component(value, mapping_values) return Vector(locationValues), Vector(transformValues) From 38d190a3cc1e98046011138ea0cbc26c40457392 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 12:07:12 -0700 Subject: [PATCH 046/114] Black --- Lib/fontTools/pens/svgPathPen.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/pens/svgPathPen.py b/Lib/fontTools/pens/svgPathPen.py index 8e2a6f3a6..8231603f8 100644 --- a/Lib/fontTools/pens/svgPathPen.py +++ b/Lib/fontTools/pens/svgPathPen.py @@ -37,7 +37,13 @@ class SVGPathPen(BasePen): print(tpen.getCommands()) """ - def __init__(self, glyphSet, ntos: Callable[[float], str] = (lambda x: ("%.2f" % x) if x != int(x) else str(int(x)))): + 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 From 57dd288d1b60a37fafa4188a1b6a0d32451e7721 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 12:14:30 -0700 Subject: [PATCH 047/114] [VARC/instancer] Comment --- Lib/fontTools/varLib/instancer/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index 4f8833cea..7548186c3 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -468,6 +468,8 @@ class OverlapMode(IntEnum): def instantiateVARC(varfont, axisLimits): log.info("Instantiating VARC tables") + # TODO(behdad) My confidence in this function is rather low + varc = varfont["VARC"].table if varc.VarCompositeGlyphs: for glyph in varc.VarCompositeGlyphs.glyphs: @@ -488,7 +490,7 @@ def instantiateVARC(varfont, axisLimits): if varc.MultiVarStore: store = varc.MultiVarStore - store.prune_regions() + store.prune_regions() # Needed? fvar = varfont["fvar"] location = axisLimits.pinnedLocation() From 3c60c0ea35eef4d2891ad822e1d4af86276b67d0 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 12:42:01 -0700 Subject: [PATCH 048/114] [VARC/subset] Fix closure Was closing over everything. --- Lib/fontTools/subset/__init__.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 4016c3534..1e9e13148 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -2643,11 +2643,22 @@ def subset_glyphs(self, s): @_add_method(ttLib.getTableClass("VARC")) def closure_glyphs(self, s): if self.table.VarCompositeGlyphs: + allGlyphs = { + glyphName: glyph + for glyphName, glyph in zip( + self.table.Coverage.glyphs, self.table.VarCompositeGlyphs.glyphs + ) + } + glyphs = s.glyphs - new = True + new = set(s.glyphs) while new: + oldNew = new new = set() - for glyph in self.table.VarCompositeGlyphs.glyphs: + for glyphName in oldNew: + glyph = allGlyphs.get(glyphName) + if glyph is None: + continue for comp in glyph.components: name = comp.glyphName if name not in glyphs: From ebd877bbcbb9cbb98fceabf681b77dac366f0208 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 12:49:21 -0700 Subject: [PATCH 049/114] [VARC/test] Start adding --- Tests/ttLib/data/varc-6868.ttf | Bin 10848 -> 20296 bytes Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 4808 -> 3552 bytes Tests/ttLib/tables/V_A_R_C_test.py | 87 ++++++++++++++++++++++++++++ Tests/ttLib/tables/_g_l_y_f_test.py | 61 +------------------ 4 files changed, 88 insertions(+), 60 deletions(-) create mode 100644 Tests/ttLib/tables/V_A_R_C_test.py diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index aa55df2119e5a230d25e34713728f0524974c11b..66cf83e30d3a31822ebc03dd304b869ca1c61e0c 100644 GIT binary patch literal 20296 zcmeHP3v?7k*1lEU(=(Y&CJ*xDnUEkafh3a%VdGMhgrMN63mn(wATG)y`peHF_&`_f zo}NiEApub{vXNB;R92ByR}onjqT*v+gCe?sfH@$)yduaWAtW>1|LvKMkPZZJkLT>J zQ+>MYtFNkV)xCAQx_VC60096Sh`<56#W}t(e)cyW0Ba^d&iXrNO`QX_lD`5(?7>nw zGwz!@Z~ClVg8}&U0Nloz_biw(HRsw}04(z{f8$Kd|ETYhSpdI01Q0W&Wct);69-7~ zSic1GuPH%7J!JVMrm;R`l+2p{fInH?gz4p&PP}LKom1gYumICHYw1~2AD9DCkb`OL z52XI@)LGL@*EMK8{sh}^oHM(0KK2`fHAc(FGmDN#hVT(Uz=G)zrid*ejFoK=qhkVz z&{M}+nQR?%h)3#}M_iy|6XLNt77*)g5i{aRIqXS;(0obMEszRZHQeu zjzV0iV>{xv`jSLiqv@k=_6L;SjqyCL47<9NjH>No-M$2v|#{JD;k5PzlP zWW@V*oPzj>j=Lj1spB4qTXozMEQIQ~7h*rnqkh%0qG z6!GIYVt3*Q{tw&-Q-k<7FdgoKnNR}r;U<`a*SoO9RJaEwAbJpUGdmnvdMaCw9eSLj z$GLiZjUErz;}LqC7mVAbWI+gJp{Hu?WI;$?7TzPFbVyzngydyGNM06%Jf#UQ4c$EMm?gDGwKn8oKcThXC??QI90# zjCv#^XVfDFIinukku&Pi139A}J&`l&(F-}F9=(w>>d^;X!>C6paz;JUkTdGh7dfLI z>Bt%N$Ux4hM?d6@dR&E^QIGz}8TA-|oKcT~=*~tx1|etEV=!_?J+4O1sK*fGjCu?W z-FMs}7zeTV$Q~&;at8_NV-^R)nD%&_b_-(&mWu<(O$ZjG=~;tldzlF@zBmETapT4T zX~8uDcL+=4iXbbEq;ZE@GX2SzCh8`&E_lbIl=K9Lqkm66!I97B<`%^9W``r57tDg0 zBOFIL;s~~?R-mLgU;;=;%Zg2lHYu=k8rrS1;X~}Y@3ix!j|c5ne&;j zea^Q=XB@UqXPca3>Y(miB8)m%AnZ9E%+V0pjdiLiY-nWO^|woeCh zbV`Kn)5#=RxIcNqN3NQ5nR8AdV``FY(_2IVg!8MaT? zM*8o!Pk2`8+5;|@8@AiUEOROTzyF@Plm*V^hrQ^WEiR41p3}9B!uAQqxj$e3`FHpo zgJ_>4_GtwwfiU|7m5H$eJXP`}39=Gi742N~1|5WW{=8le+l= zOUet{di(R*CW;&%(_CLZu62rkW7~&thmycAOwCFuA!Fz^b9M8sa%VtN2TQE^DbX1< zEH=qqVA|fC+xC{k`~yX&YCU$CfECr?@!MVjqQQ(`6I&nxIRNZ_^2A5q-2N2VQs`n( z=7j(wyvPDaNt@z=Hg*PPjODqXEUIi(Jy(HR3zrQ7ndr=+fGJ19Dv z&z?#bS?fNL`9BevrAcC@B_bU=VIEWaK%);#5;z{?6hBy9#Qtli@{T$lY9*4B%t$Qp z)Jx^8?SpbC7=7l`w)D1p)oLZa4$4^Ct_lz7Q0?4pl~Otp$- zG;!BqrY5W*Jw+#x)!SgLL~dwfs)@W^FA+!!DlMF+a12D^_OS%qlxD|I*OMR)y5SbG zWJtk}p?l!R&%K~G^np~Y*%#6w1Nxzf1a0WcU@uujR*~cM#y_y69-G%WPi*j~xUfGW zJ5wzd)IrmB^DK$=94j*W{URG&BNg;I&0u$ni$zX!F{@4MvBY*4Jqf8)Ozd1SIxf}0EA10n}%U4=?OIi}~}STRd2FQ>#JGcSKUmQDHR`X z49q=ot7+8!#pXz5z0AxF5{zg6w_Pka`0&BWjgvg=)DBm1!{nyl{d4_}<8<-K*;vZi zXlW}$FMJOjsvf(CntwOf%LX(rVMFF_-t2RZ`bE@sk!38DH_LRuJuq5zD4W&TzzjTI z2h*FY(YR;(YlG7|L}YCriq1Mv-WJ)w1s=v=^D9BAdHuzrD0}OW=*h}0gI>j!QYu*6 zTP~-t`f7{B=#8rx`N!s^BILUnRMm1Z%JTzdrhEZI@M^1o`VH_*At{Er>P)3cq91o( z7xZSp03jLvZ9B{~95| z60@VhU2ynT(_Bl;;~i@IipW3w15JNPc->@w{%LBpKF^d@u+PU$OoB!)X{G434hc>a zD!-t|)$wYU1WkqN{g4!B4b)3TBnk!vn$%hqD#*#R3=TkjdnK9i-b&6Hab$;^1#WRO zy3xY|t38a~>aHnn&ZRbiKihv#%E?ESw_Pl7^V>|Av1D{hv$a>^Qb=pu+q&P)&icJL zi9{^uKAZMB{xCi1X3A2r=tR=7)|1QmSIBU&;L`?Mp1`}=;Ut$cVz}jvnhp|{6-FL3 zR|i&fOLsXI_R`}pgemKxaVfW^HyrhlRw9kz$Ae9A_`gB*91x+-5{ngUpphg% zFWOgKrS@@?c*-zqU$8ji$9uWD_{4oKM)7W;8>^PqGTQ87>UHJ(`*1IiF-n%IsFNXH zy<6R@4kXF6ciaAQ@-8s&ESDKsg2OYj3{TVtXA&N&=>`~&i98XG7n57ny8@%t-E<@* z2G+p$FK=F0sI3~b;|aWb4&a^i$x0#E(#OhZ#R1(&rn(pB_+*i^4SX3riKlp3MS791 zXiesxXuz#Byh*S`MB1Y4(J|e$EqbMNc7)eb3Nfbb)>q9v%vQ@CQQH$n*~>X!aO=ni zaD=Pn9EeHg9sy`jf*D0Cv==Gi=%^mrV^rA(%lUo$TEtvtPdhe@yLTe2T01eZ35nKi zToIo~mq%~nC(%dAf!Ilz%n@=!k+f=B!OoN#%1k5xQXdiV0?hLMlYXLRL_nW~) zO@cKt${tZ`lA`9?_CjgY3ao(JT+}DwIovg~mUEC(xSfRp&G%cdJSSMNeY@#B+y|Y- z@8uUlDL#g^Dx6wM`&nNmGk&PT=li3yDrg+DCDiQ!T7SS^%pD8@(CVU@w17(obvT{N zKg&5d2YnQ-LI#b4+krbsh>mf1lC0S`=C0_!Hf}MwE+hvaxmlPYw9;#_7UgEs7qm@3 zh`D^fHK?Xp>vxphY~>CLyR>Fa+tJ*4dec0NB@WvG?EGA^9iLEhO9V7nraw}v#apaV(J^uHiS}J& zK@XaHvx6oc<}Gvx)&ujW+Srf-wD7c-KE$oX6qmUtG)9Pd@)60TkV_he}+7ci~SuI+)O^H#pQ zDbMuV5X0IteuDU?lzpzdKV>Wbsj|;m=X~^^s!h1x=Z|6thi%f?CKqDgCBSP z6MVVChZxS@k@)bj;q$aRUago)#;XnAK=HpPFlEM5fCTYEYW~30{eXTn=#THesba7? zyNHmwi||)K$J$BLsWX1H_2fI-4mN`|W!Na$32%UG8fgz$b05|43Sqv(rrL7uWw27N zaH=EJiG1~kkT=f>Yv3cfrbZ;}5e8mbI<~h=dyg$eOdetGno2>4OLG)YnkIYA=7fxF zcGGRsYw2IoeDY)kx>$Nav(`D$L7P zzE-z&&(X8)8j)9y{d0XyfjV0qtB$CW_}1@sZuB|T67`0vnhIgsq+&-}oZzXE1Zs{> z?&rv_s)cm*~X>v*Rwmg&)~QrOonP+rEvE5l@iIn@4UKXo zyfRu9l-m^>9=EFkyn-cR!f0qE&ymx3Jcs|AG(qPpjAMm${v*mMn3qkEjpAo3c+7mc6i{zTsHwBGEkUwwqXXMuNH8 z$KazyByAdl7i9)-EG8MH@(;3LOQqm%K5?S4VJngZ)Cw2fzpfg^6IY!CZ#mV6)Q7pn zi&yM$OXjmj_iU|uZRN5mpI3Xj{FW?hsPLjlQkvoQxrEkZ4fPvdSXT9eaIbmG)`rFt zCz=CpU)l1x=XCJNqUH0Q-<~G+9s`C}iI(r1{6N8f`goN{ zaZc9W;v#XtC1>>V=;d;GAvvzZDzW6aTrS2E9@hs1=Wc)jINJ}xHA7FV9EbHTFoRs7 z-(1SeBCua3e4Yrw-^DHX{}^UG1msNED?41q-T5!}C?20!9xi@xPMXA}4@nv9i7FqiC5arwgo&sH#~8@^1cFrUt+Oj zJmv@Tl}Kd|tn^ee^>>`~#_rQ)3wc!usHz9u*WqyVNwo5Ao|s}Y9Bv=}UiuiGvd;zC zk4r32^n}m1&}52E$r$QzyM(CtZZS3sSLLDDvudZ-!jnEmuCDR9xwPED@iJ%`gcw2$u-OC%kHg$01dK0Asiid1pqRuK3Q??b)U&g% zz>)`xE7%htfDjMI<)C$GMMa9Jh>uh~v;{<}D3w=)0%i%x?!Eo*?)6CTIlX)4&dfJ6 z-^}+v?j*;0$kz*b|sL`yg)EziQ>< ztMQ=tBscu&n4dbWApa)C-&U|Gb){0WtV)IH5vx{V2E-;K=9G8* zabqKbQ&ba8h)q>tX2fb#SQ4IDEhHH+wU89VZW9vUpQIwFR+5I8T1h%$Y9$$n$x0GW z!h$0ZQwzyNOf4kK>a^O_O0vljJb%Zh2$Zo7BUjCxR8YX6Q>@9 zpjydj#MDai5mPG}gP2-K0fvWK2zt(byAXMQQiz~hNfBadCB=xTm5fD9?UEA2)I#n+ zOf96;YIWMxO2*+twUROnYPFK_h~2J47E+FwTF9MF&~(Gh<%7YrnYroG^RQD3WS_=PZa^u5vu2JOB|T0{%pua00O2Q^<jvJnnGK(+ckFTggY7=g8OGqGHO=Rm$ZQEBQuMf^~9ch4wtu*<4|(@@iKqRxu`YL3VDg)Q?&FsI{ODdeOj}x@g$iNmr6dP(_kJkvy%zwP){o~O)#(V zq!vuF^>AtYA)kQ}=Fx5lt~S1%fn121l`#Ms6inH7-3=Z%MJk9ueo2;*XGlAa%k!|@ zbZY@Mn%OB-kh%2u_-NZ;+b{!i~Zw-lKJXn~~NJsEs>#RmPlLv*8#^Ac@g8RrTO>;;;*fRVX zIhmVd8;Le@_t28)!Ql_!Au(IqZnfpLkh%18Lucfa3nWh$d9T?=()&80(9`un6sg?98v}G9cck)u_N4}&5Q08Kjic)7%dQw^q`j!Er;PY`ZMTWM>*{Cf{14L zgfruSi|ICUsA438_<)I7EMJM zcob5h4pQJDY1=~&XR&_7hIcaY4bIBe_!tA4-dpAkf!MYeDtsX({=E%f z$!+0{>_WsPXkPlhbuD}Q-A*)IIN)#&yf`ireVSYaR`{};S?WBj^qydN3%hi7bJ#6h zdo!BnMkjD~vs2!wi^1;AVPW97VTFs0uIDWENzTeQxrF>5aN{p7HSFrZkBoU2J3Z)T zNekV~oGg3J1@qDGvF=?R4374))3xmmF(%d^P~E}AE4`c9p#xjOn_1DTUZ!u}6lRw; zaHf5lqgL%=lb^Hkhg~db8HXc6bXbh=;Et(TPP$+muxKA=HKn{ZC3UDGtwDTX{HvVw zv@)sL+d-OvNw5KyGs*5}q>eLjz$;t;W6;M^n>@q{EY`}|zmIdr`Df2J^e^_Y8zFTNExvvKN*ROj(E{JE~hZKE5qPo+8XYl*M~ZA==a=dbTN7#uILr-$JFeQ z9U$2^fNnjRicvq{UB@z7IJ1H;E|FmS`_`@K!*P)v;Z3Zfb*nQRVsHq33tzVFWaQ&@ z1}#2(8{YDTaWV@uOW7}T{^E^0;`9QD$Se4u=#BWZ$oFsB$=`oW=VkYS^DQYl%1f04U zQ)d~bP8Oz4E+&o1WX+>rjA)o9XToNn3xq#Ig3KQ0y!7^#Is!>8^vaS zsIx9oO_TN7Tcuy-U3ZFaxLIsICs!+&c%W+w^mpxG@+>?*uQiN1lXgA-W;sq^{RcTQ z_cC#JkDr?1Y|nN<#Q)O#p->N_k#>Ab^V&TaY+kw$`mxcpmhN@J`= zVBOCh(r=|Q=}|cC5y+(2We*eg@Vf6xF2JRCqI>n(#`F_kae<`728ev+Tc_tZv71ec z$ZnWTa;0H9EwHDH9HaNn-*B<)+wsk7T4XBK|o@xEtLpx_P{KH88`9iK* z@^Q3G7tVf7m+Gd%r4#PiN#_lnu@^Io-GXkT;Si^XE%MIBF31Y3XR!ym7=3v|DCB4K zAT|#Muoc_0L+&ry0Xz+QFifine&7M)R*O-s!RV$K-*R;85KG2i5{$-P62^K`JhF44 z6b#`BbM_HR#s>dYms2ZW6FMx(ma{f>ERFWeU|(e{(qn+ diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index 1e385bad335ceb9def30d56c22da8fdcc1c1d78f..a8cbf6f7415c8d267e03749a9fcbcd4468054277 100644 GIT binary patch literal 3552 zcmcguYitzP6+ZXgnc0We-d!^(4_gLj3858L@%jNoZndl}WFaJ~G=-E%P!|XeHN_Z8 zQ`E=o&KN_tQPhM~6-}fhQq(k|$-f9!NxMqimMG3jjZo_(#7RiWquGLaE!cQ>`rX-K zBTt?Em)_Bw`q|Csp z?cRgEgMWUv{%P30OT>2U>i_Y*@sk^$A`+vpAK3-F-SK3a$ajoLTi>^{cgL2(&z?tn z3-*pa5ZVi(8TvT%rG0x2+#gKYQ_x?8zNmli_FihC&CvUyFWA$2{{Ssuwa_;}U-E^5|Oqe^7rW=^*VSn?WFr?7xmEr+DHSC_rar=`srIhhhSYf>uGVdxLdcn zb(>qayLE?KuX5|vZoS5-Z$@cB)@1w^tYEn=$7`WBoT8)y_55g8-vPG6!0y48 z>)G%+@<@9J_8)?+T%4Agh#vk&3KPxmSlZFi-rjsm^X()P&-W`q2AAJ6!U&hyk_W$L#{f$V{7Pl?pt5K(2RP@0A)Up_VHoh)W{Ekc|Q4OoX z36q68vbvauJ=5*9L)#~wyFRup+=t68@t`LJOL1 zkcF{ToA+g88f1=~9!Ql)kpj0oJ|v>{@AhR1;j_SnI%YIomnI^6I?||8fD1+Qzq4=R z41ci9lWG@m4`PQ!8U+Q>Cb#L6uR1o3c9-HnLRw_Y3qPGWBU-^IXtMyd+&}V-QUG!V z)j$&4Dy64vLvd3X5V9m3`8h&IHZ*Jo&6wqoouyS2j?KvpH)5hf&jB*#TJ|b?btu!t9xgw_ zhNkP3)AgL`JA1aNDWBJMGh?z)XvkoXK6)f0i&4)Wr{##Z0=sm%Q3;%=p#K3e%s}2Z zVZ&oecyveg%u{j+$1qDYVNIG0hsr`G95knBz=~AHnXT^WqlHGrqe{$X!|6)qxr)~k zWVxXnFAM)Fo{MhwT0P#)SXI5mw>N*I!P5Ap$*IU4_n2Mm3OmalU=g-Yw`f}zV=Tqq zVwX*}|5SJBWbs<@A^D`b%XRU?(y!RDZqpD8zOpPbq$r`_)Q4tLB>B(Tda=*8?G#O> zXls%+@~;a9zqU<1jYCSX;g(NdGV6l1(RsSA=kxho?(}?`jGH3p6Kq&apMN`N8k`A!Sh+g!diF(A8{ECFQLc|>Uz)rYYTsn4 zHzJE0w1TCGfO7GDQ-p&WPqTbJrPylympjoXBb!;OzObQT@p4lvtPhC8ip{k;UE1*3 zwv_y*7YZ|0ikQi995Lqbt#J&h#oeR%z3VNJN1;Qk@JX0kXgpOjmh8K0BZLJoIB d-pOY!kGp{*ca|lknX#xe1TkI_%ZKn;`!6IAIXM6T literal 4808 zcmZ`+32>Cxk?#MV`<~H^j?sNsNT6E;a3q^0#@OB@HcqH4 z?)Uz8^y~im{lpQGnF1ux*S`A5qLxT+>_@1Xtmx~JDY^<4ai3yI{-ke6)Ox^6p} z4z7mmg535v+SQuqQ$##OI=N=!@y;uzS2+HbBXdiYSj-&qpB2&fYjqAQsv`iVt z_$$%BV>1+DKplqs3goKITX#MyKbw9GasaaJ$?tAh$6e)$ATNe&*}Cr8?WAavNnXfh z+tzK}=sWnORp2iYaaG&DyJIKz0g|=x%TSE;BWO5LPaHKs&hb1|K+5vizK;Gr&vW3v zk>`2vkL7s*{Ns6E1ivlMOW>c*^D_AF<#`4CKj(QJndoI&PrGO*ZJ^Dxk)A^N4mE;b zhmkg+g8%jCyOEl1dz$l7OTKQ+*KPT_Jzsa^ z>rVP6#@K?HH)9SMfNZd-p0qQqYXYIpk=Dj>1Xc4bRklCJanQBgq2r-zy+bEJ*LH_a zgs%M#odjLS9Xc7h&O3CrFQ5qh;&PRCSQ$X*o=36GFh)XdHc9gDgi@h>ml z#~tSnkXIs6Q;JIuFWFzd+r>I|2?x0E5#Fm{FW&avqG;EDIhHzk?kIop^Y@(pr+>?_ z!SiHG3*LjCCy6-M@4Jcr9Ziv-u4kKQIa#Qf7SJ-HW_LX=%Sr`GO-*f0O=qXa)7tW7 zv9qAIwzH$9z|R+WJc?3KThY0Yuk9>wc{~NOTxnl+-`}+wbv~cYQDNbEPO{hA3@yu7 zt!Oj0cL-iV;YEW1dQOq75@)cQ1izP8Bu?f_p%i67vKx4jGl~+<2bwCW)P(Me!ter z7xL}xt%?iA^*r>gZ{BOMHCtVEHj$H^O%7AnHy&HF+|tq}dKFo6ilSh!mBAKmyeNxR z9IT<26_-&Ivqs_}*UObAK@gx)ynnVYXt1qWm_`s@#=c@SSGEFBkonN z=lf}uHu4vV$JifoufPYzFRK3*|Haq4Ktu=WASZ0s`ipxx{~hm@Wo2!38j`xSaLU#q zm7x~3NWuslW7;^3t>*IU%i=D2{4L5YL;qLJy2ErhtJGJ^F30`me=6EmV5s_W*@4El z7rxU?-CCRWt{x8ow{vHL{F2o<`5y5X(r>cT7yi1zYvp$8zD>-JeTVKg@v|{*e^cx8 zxhBVbE~7C^-T7Lpx+$RL79Cjb&(RPkj)!s@12p`Z=%*o`n~|Z@Qyu*hHs6mu??JBH zgHzsvK6_yIJ;c*)jIjqc2=eih2OeS1u{G>r_8?oz>RA=@s!W|#W7%hpiL8t@uvWD1 zas2As9M!Rm-RUhXscu}<+}u>Rob6@LupMld@h8XQ%u`n~@yp&&pquRt1$^PL@aX8s zjT?hkE?t|9#S`Nut*b~gp z2#d+6nNUzLG(5Wd>xh&6^dIzVdYj&*KhbeIO=svV%4zzT-lN~sZ|Nt_?O!PqDF$fD zq*BTGxw*MSBDO-^u0F5sR`;rXY^!*g=40nqunp=_^$9%N)a{|bQoUZUL(v<|7PBG> z9H$~e1VMvJGU=JB5%5To6w^!lUi+7yy*H{in!^A6^7nuE;!C0662axTvY6wWZ!~lNedlv<`iO+2U(i_2{FkABL@Bu~;-Z1Bygu z5_9wO$y5rqf~nMCX)MMh5MT&vq1e8C`(AtFMN{(Uqz>g)X$5P<2Qz`Umh43=Ty?pZYCucD^XRBUv z-^vwTUus`eQCi~nm*3T~?C$njkHTnKOepr3SJrhbU%7JSeal>t@$sohEaqL-xv;Xd z*z3aNeOMHhHgMrW|J7(Rt#YD9ush6tv7gg{BudXGXX6*snT(PSU+TYc>Du@lsp)hm z=;bZN3pzU57A?4|y13Bi^%a%XH#XK5J9NR)+DYDIwmUp!cP(gWY^e2HOlCPXJ!z<| zt*)uAEcIjZp~0cyA^h++8pGsFJ8doN5u@-B;jl$PolnfhqTw_r=vXW~G!Y)1NWil& z`4V28oeYnSPetPim?p2j zZf#mnQ(fUNDlG67msD0)`MnN?=EEGk_V6{2ycAHx#OT3y)$u^hA>+`zZp@1hc zJv}{zGCegJnN22{su`T)ddeBarU=I`OX}QQJT?=H$K%OpWO^zdjYVU#kg)N@;Hg7z z{pPp-^X~gcj-Nbz`qYVI|NH*?N6%cHO3^9n+*o*c=-Q{pj(%|D=&5N0k!-eF$4~WL z8@X|FdKw!qE-EZU9(KDu1$Hi-nguo_u~`insJu9n0Hq|e&EYO6H;Iy}8a&0W@)DO> ziy@$!kyhglEYK4`w>3bw)j+p30l-_1x^FQ=Yr(Ds=KTXOZw)YSEii9w;IsDbJcl|^ z|A#+ZxG_l*KuM8Aj^6q8!7Tma)fZoW`IR64^o_UQJAC-ip+oQg@!fYq0V6Y-oKA<` zYSQsskKxTX-#YY{fvcOhJ@fnu3Q`Y+@bpl?u>1MvcCH_}eDd&HZ-xT(ix-!=t&|2L zCDR#3_pDm=fJR?iyK%#Y_1|9m)qB6ZG)s4P-4hC!xWsG(Xb?>#;TFNKs2}6`i+V~u ztA3)MwX`(WRQdfSB_*YmRaJL&u24^?=hOl9JUV=gmtn!Qid%Pb@ax^@69W*#} zV`Ma(qtVe&P=_lGum#jx)YsS7f8pZA{tE*G7juNr?7MXFLVsUhD1gXBuON>V^%iCF zq^dc;ML^~$1He{pIr9re$BupU;n5>UfE}RIr%!9unX^Ye_~7VA$6yw}-)6+^I|p}+ zWMd90EpDIB>n(6wa7D}#Pzh^IrC`7L`BbL22rxnEoC9JAqO2%-dwfpA5R6-0 z?e*9VdQSnyap|aZY9c&@-G}2TSuxo>l?$6Y+uPdO7S)xP!eZ%`w#A^11=UWo6fByB zrzaBe@aWBP-1@Tdlb9M$@eF_#hyCcvoK zY}83uiHRY|T`rf?YLZ1hSY$LBtS+xlRe9cEz-|p#Kd<^reNMA6J2ucSlhNWonPS9T zZjTq_c028MtJNT~Og1+#j%V;hZU(VAJspY0(`rvqEEdUF$|@~ZqX8KVUn_zn>n#nX z7GXXX3MkXlKynNjEYnVh;!~438XMIzb$AL}Mz&1kqFg8NBJSS+hTbB?=lpCeHaIjI z9vi!P^X6C>uUw18XA!xfKq)*rnPxyhq)CjcIRtKXkhF}SPOIPNIi`k!lBR<*YyQ(C zu3h`smYw%dfI^lBAAIPchqRKV2kxOCP_OX(w)Jaaj={n4$!I16e83zT8X3z``1<7w z1AXWFuMCfjjEpoiC4Jw?UEg-Bc8 z0eP~iPI{8pQ(CpVstVhSPo$wdJE&2y$_WJ9u|Lo`c8QZ`A z-~a9PP*0P&to`2Q-HY2A{KyR@)on|cwN`5>dFh1c@Dx^d-rd!;v?>vsg4Y6c+80!X z0?tp*ox?`^&YwFsFcyi;F%3Xo@D7~gw9m0bY-Va85*ZJV+&F(eIz2HN!5$bs*%W*v z$*=+lmm>u(&IP@)s)352XrF<& Date: Sun, 17 Dec 2023 12:55:28 -0700 Subject: [PATCH 050/114] [VARC/instancer] Fix --- Lib/fontTools/varLib/instancer/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index 7548186c3..69977d257 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 @@ -479,7 +480,7 @@ def instantiateVARC(varfont, axisLimits): if tag not in axisLimits: newLocation[tag] = loc continue - if component.flags & VarComponentFlags.AXES_HAVE_VARIATION: + if component.flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: raise NotImplementedError( "Instancing accross VarComponent axes with variation is not supported." ) From 22e02ccb473df6b0c32d9d1b59f152a235d895cc Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 12:56:29 -0700 Subject: [PATCH 051/114] [VARC/test] Adjust a test This worked with the previous font it seems, but not with the new font. Or maybe I've screwed up something... --- Tests/varLib/instancer/instancer_test.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py index ca7ea93f0..809d6bd35 100644 --- a/Tests/varLib/instancer/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -1712,10 +1712,14 @@ class InstantiateVariableFontTest(object): location = {"0000": 0.5} - instance = instancer.instantiateVariableFont( - varfont, - location, - ) + with pytest.raises( + NotImplementedError, + match="Instancing accross VarComponent axes with variation is not supported.", + ): + instance = instancer.instantiateVariableFont( + varfont, + location, + ) def _conditionSetAsDict(conditionSet, axisOrder): From 28385ec07e2e2d4861286bf851879c85aa97efb0 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 13:01:04 -0700 Subject: [PATCH 052/114] [VARC/test] Adjust test This subset font is from a different source and has different glyphs. --- Tests/subset/subset_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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") From ce3e2609556f0e51a14e4e41afea01a7cdcfa32a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 13:08:12 -0700 Subject: [PATCH 053/114] [VARC/test] Update the rest of the test expectations We're using a different font for varc-ac00-ac01... --- Tests/ttLib/ttGlyphSet_test.py | 132 +++++++++++---------------------- 1 file changed, 44 insertions(+), 88 deletions(-) diff --git a/Tests/ttLib/ttGlyphSet_test.py b/Tests/ttLib/ttGlyphSet_test.py index 177b8a4e7..c9c5150d2 100644 --- a/Tests/ttLib/ttGlyphSet_test.py +++ b/Tests/ttLib/ttGlyphSet_test.py @@ -226,28 +226,37 @@ 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, - }, + "glyph00007", + 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, - }, + "glyph00003", + DecomposedTransform( + translateX=0, + translateY=0, + rotation=0, + scaleX=1, + scaleY=1, + skewX=0, + skewY=0, + tCenterX=0, + tCenterY=0, + ), + {}, ), ), ] @@ -265,77 +274,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 +486,7 @@ class TTGlyphSetTest(object): "qCurveTo", ( (919, 41), - (854, 67), + (854, 68), (790, 98), (729, 134), (671, 173), @@ -542,7 +498,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),)), From 28520553f3f5325306566be3e7710cfde7c4ab1a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 13:14:01 -0700 Subject: [PATCH 054/114] [VARC] Allow drawing same-name glyph --- Lib/fontTools/ttLib/ttGlyphSet.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 4602b09a8..d848c8670 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -324,7 +324,12 @@ class _TTGlyphVARC(_TTGlyph): ) except AttributeError: t = comp.transform.toTransform() - glyph = self.glyphSet[comp.glyphName] + compGlyphSet = ( + self.glyphSet + if comp.glyphName != self.name + else glyphSet.glyphSet + ) + glyph = compGlyphSet[comp.glyphName] if isPointPen: tPen = TransformPointPen(pen, t) glyph.drawPoints(tPen) From 205657761478eeb31ec12698d45118f9371688bd Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 13:22:11 -0700 Subject: [PATCH 055/114] [VARC/scaleUpem] Start --- Lib/fontTools/ttLib/scaleUpem.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Lib/fontTools/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index 0df563f27..ff1cfa78b 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -142,6 +142,20 @@ def visit(visitor, obj, attr, variations): 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 + for g in varc.VarCompositeGlyphs.glyphs: + 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) + + # TODO: MultiVarStore + + @ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables") def visit(visitor, obj, attr, kernTables): for table in kernTables: From f24808c58cd49092a525b45d00d29d9f45e6e598 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 13:23:01 -0700 Subject: [PATCH 056/114] Black --- Tests/ttLib/tables/_g_l_y_f_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 7dbdbfd3a..9a3fd2eaf 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -719,8 +719,8 @@ class GlyphComponentTest: assert (comp.firstPt, comp.secondPt) == (1, 2) assert not hasattr(comp, "transform") -class GlyphCubicTest: +class GlyphCubicTest: def test_roundtrip(self): font_path = os.path.join(DATA_DIR, "NotoSans-VF-cubic.subset.ttf") font = TTFont(font_path) From febbb34dbaad33978119ceaf540e3f930a8f479a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 13:40:41 -0700 Subject: [PATCH 057/114] [CFF2Index] Avoid infinite loop visitor by hiding symbol --- Lib/fontTools/ttLib/tables/otConverters.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index b46eeb522..33b1787a0 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1809,8 +1809,8 @@ def cff2_index_read_item(self, i): self.reader.seek(self.data_pos + offsets[0]) item = self.reader.readData(offsets[1] - offsets[0]) - if self.itemClass is not None: - obj = self.itemClass() + if self._itemClass is not None: + obj = self._itemClass() obj.decompile(item, self.font) item = obj @@ -1824,7 +1824,7 @@ class CFF2Index(BaseConverter): BaseConverter.__init__( self, name, repeat, aux, tableClass, description=description ) - self.itemClass = itemClass + self._itemClass = itemClass def read(self, reader, font, tableDict): count = reader.readULong() @@ -1853,8 +1853,8 @@ class CFF2Index(BaseConverter): assert lastOffset <= offset item = reader.readData(offset - lastOffset) - if self.itemClass is not None: - obj = self.itemClass() + if self._itemClass is not None: + obj = self._itemClass() obj.decompile(item, font) item = obj @@ -1867,7 +1867,7 @@ class CFF2Index(BaseConverter): l.offset_pos = l.reader.pos l.data_pos = l.offset_pos + (count + 1) * offSize l.font = font - l.itemClass = self.itemClass + l._itemClass = self._itemClass l.offSize = offSize l.readArray = getReadArray(l.reader, offSize) @@ -1882,7 +1882,7 @@ class CFF2Index(BaseConverter): if not len(items): return - if self.itemClass is not None: + if self._itemClass is not None: items = [item.compile(font) for item in items] offsets = [len(item) for item in items] @@ -1912,7 +1912,7 @@ class CFF2Index(BaseConverter): writer.writeData(item) def xmlRead(self, attrs, content, font): - obj = self.itemClass() + obj = self._itemClass() obj.fromXML(None, attrs, content, font) return obj From 0f0148e54ab68e6e3d0691398ddf66e470465bf8 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 17:39:13 -0700 Subject: [PATCH 058/114] [VARC/scaleUpem] Implement --- Lib/fontTools/ttLib/scaleUpem.py | 80 +- Lib/fontTools/varLib/multiVarStore.py | 11 + Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 3053 ++++++++----------- Tests/ttLib/scaleUpem_test.py | 7 + 4 files changed, 1328 insertions(+), 1823 deletions(-) diff --git a/Lib/fontTools/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index ff1cfa78b..84002d115 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -10,7 +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 itertools import batched __all__ = ["scale_upem", "ScalerVisitor"] @@ -145,6 +148,13 @@ def visit(visitor, obj, attr, variations): @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.glyphs: for component in g.components: t = component.transform @@ -153,7 +163,75 @@ def visit(visitor, obj, attr, varc): t.tCenterX = visitor.scale(t.tCenterX) t.tCenterY = visitor.scale(t.tCenterY) - # TODO: MultiVarStore + if component.flags & otTables.VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + varIdx = component.locationVarIndex + if varIdx == otTables.NO_VARIATION_INDEX: + continue + major = varIdx >> 16 + minor = varIdx & 0xFFFF + varData = store.MultiVarData[major] + vec = varData.Item[minor] + storeBuilder.setSupports(store.get_supports(major, fvar.axes)) + if vec.values: + m = len(vec.values) // varData.VarRegionCount + vec = list(batched(vec.values, m)) + vec = [Vector(v) for v in vec] + component.locationVarIndex = storeBuilder.storeDeltas(vec) + else: + component.transformVarIndex = otTables.NO_VARIATION_INDEX + + if component.flags & otTables.VarComponentFlags.TRANSFORM_HAS_VARIATION: + varIdx = component.transformVarIndex + if varIdx == otTables.NO_VARIATION_INDEX: + continue + 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.values: + m = len(vec.values) // varData.VarRegionCount + flags = component.flags + vec = list(batched(vec.values, 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") diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index b3440cc63..3cc78ae8e 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -205,6 +205,17 @@ def MultiVarStore_prune_regions(self, *, VarData="VarData"): 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.VarRegionList.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.glyphs: diff --git a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx index db32c06eb..dd9cc2f4b 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,131 @@ - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + - butchered-hangul-serif + rcjk - smarties-variable + varc - 0000 + Weight - 0001 + OpticalSize - 0002 + 0000 - 0003 + 0001 - 0004 + 0002 - 0005 + 0003 - 0006 + 0004 - 0007 + 0005 - Weight + 0006 + + + 0007 + + + 0008 + + + 0009 + + + 0010 + + + 0011 + + + 0012 + + + 0013 + + + 0014 @@ -416,1638 +232,1231 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - 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 + 0006 0x0 - 200.0 - 200.0 - 900.0 + -1.0 + 0.0 + 1.0 264 + + + + 0007 + 0x0 + -1.0 + 0.0 + 1.0 + 265 + + + + + 0008 + 0x0 + -1.0 + 0.0 + 1.0 + 266 + + + + + 0009 + 0x0 + -1.0 + 0.0 + 1.0 + 267 + + + + + 0010 + 0x0 + -1.0 + 0.0 + 1.0 + 268 + + + + + 0011 + 0x0 + -1.0 + 0.0 + 1.0 + 269 + + + + + 0012 + 0x0 + -1.0 + 0.0 + 1.0 + 270 + + + + + 0013 + 0x0 + -1.0 + 0.0 + 1.0 + 271 + + + + + 0014 + 0x0 + -1.0 + 0.0 + 1.0 + 272 + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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) From 2b09b0da8a21435735f0e129d5c8a656ae78867f Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 18:25:13 -0700 Subject: [PATCH 059/114] Black --- Lib/fontTools/varLib/multiVarStore.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index 3cc78ae8e..f273650ee 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -205,6 +205,7 @@ def MultiVarStore_prune_regions(self, *, VarData="VarData"): 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] @@ -214,6 +215,7 @@ def MultiVarStore_get_supports(self, major, fvarAxes): supports.append(support) return supports + ot.MultiVarStore.get_supports = MultiVarStore_get_supports From 45f7f4f70fb46982eb1a0e6712c90ca410112643 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 18:37:25 -0700 Subject: [PATCH 060/114] Try fixing Python < 3.12 --- Lib/fontTools/misc/iterTools.py | 12 ++++++++++++ Lib/fontTools/ttLib/scaleUpem.py | 2 +- Lib/fontTools/varLib/multiVarStore.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 Lib/fontTools/misc/iterTools.py 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/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index 84002d115..b138f7da4 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -13,7 +13,7 @@ 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 itertools import batched +from fontTools.misc.iterTools import batched __all__ = ["scale_upem", "ScalerVisitor"] diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index f273650ee..625e05e00 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -10,7 +10,7 @@ from fontTools.varLib.builder import ( buildMultiVarStore, buildMultiVarData, ) -from itertools import batched +from fontTools.misc.iterTools import batched from functools import partial from collections import defaultdict from heapq import heappush, heappop From 93fe24050b031dbe9976248f2de14972d42e6eb6 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 18:54:57 -0700 Subject: [PATCH 061/114] [scaleUpem] Comment --- Lib/fontTools/ttLib/scaleUpem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/fontTools/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index b138f7da4..aeea692e3 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -165,6 +165,8 @@ def visit(visitor, obj, attr, varc): if component.flags & otTables.VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: varIdx = component.locationVarIndex + # TODO Move this code duplicated below to MultiVarStore.__getitem__, + # or a getDeltasAndSupports(). if varIdx == otTables.NO_VARIATION_INDEX: continue major = varIdx >> 16 From 232d9cf0475f2f093ea72ecfeab70edd98674fa7 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 17 Dec 2023 21:35:26 -0700 Subject: [PATCH 062/114] [VARC/scaleUpem] Remove early return --- Lib/fontTools/ttLib/scaleUpem.py | 118 +++++++++++++++---------------- 1 file changed, 58 insertions(+), 60 deletions(-) diff --git a/Lib/fontTools/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index aeea692e3..ccf3aa781 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -167,71 +167,69 @@ def visit(visitor, obj, attr, varc): varIdx = component.locationVarIndex # TODO Move this code duplicated below to MultiVarStore.__getitem__, # or a getDeltasAndSupports(). - if varIdx == otTables.NO_VARIATION_INDEX: - continue - major = varIdx >> 16 - minor = varIdx & 0xFFFF - varData = store.MultiVarData[major] - vec = varData.Item[minor] - storeBuilder.setSupports(store.get_supports(major, fvar.axes)) - if vec.values: - m = len(vec.values) // varData.VarRegionCount - vec = list(batched(vec.values, m)) - vec = [Vector(v) for v in vec] - component.locationVarIndex = storeBuilder.storeDeltas(vec) - else: - component.transformVarIndex = otTables.NO_VARIATION_INDEX + 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.values: + m = len(vec.values) // varData.VarRegionCount + vec = list(batched(vec.values, m)) + vec = [Vector(v) for v in vec] + component.locationVarIndex = storeBuilder.storeDeltas(vec) + else: + component.transformVarIndex = otTables.NO_VARIATION_INDEX if component.flags & otTables.VarComponentFlags.TRANSFORM_HAS_VARIATION: varIdx = component.transformVarIndex - if varIdx == otTables.NO_VARIATION_INDEX: - continue - 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.values: - m = len(vec.values) // varData.VarRegionCount - flags = component.flags - vec = list(batched(vec.values, 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 + 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.values: + m = len(vec.values) // varData.VarRegionCount + flags = component.flags + vec = list(batched(vec.values, 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 + newVec.append(Vector(v)) + vec = newVec - component.transformVarIndex = storeBuilder.storeDeltas(vec) - else: - component.transformVarIndex = otTables.NO_VARIATION_INDEX + component.transformVarIndex = storeBuilder.storeDeltas(vec) + else: + component.transformVarIndex = otTables.NO_VARIATION_INDEX varc.MultiVarStore = storeBuilder.finish() From a958c68d7921b1dadc5ec4bab8484e2ea29e2486 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 18 Dec 2023 12:17:31 -0700 Subject: [PATCH 063/114] [VARC] Simplify TupleValues Move it to a converter instead of a class. --- Lib/fontTools/ttLib/scaleUpem.py | 12 ++--- Lib/fontTools/ttLib/tables/otConverters.py | 63 ++++++++++++++++++---- Lib/fontTools/ttLib/tables/otTables.py | 24 --------- Lib/fontTools/varLib/multiVarStore.py | 3 -- 4 files changed, 60 insertions(+), 42 deletions(-) diff --git a/Lib/fontTools/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index ccf3aa781..a3754d8e8 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -173,9 +173,9 @@ def visit(visitor, obj, attr, varc): varData = store.MultiVarData[major] vec = varData.Item[minor] storeBuilder.setSupports(store.get_supports(major, fvar.axes)) - if vec.values: - m = len(vec.values) // varData.VarRegionCount - vec = list(batched(vec.values, m)) + if vec: + m = len(vec) // varData.VarRegionCount + vec = list(batched(vec, m)) vec = [Vector(v) for v in vec] component.locationVarIndex = storeBuilder.storeDeltas(vec) else: @@ -192,10 +192,10 @@ def visit(visitor, obj, attr, varc): varData = store.MultiVarData[major] vec = varData.Item[minor] storeBuilder.setSupports(store.get_supports(major, fvar.axes)) - if vec.values: - m = len(vec.values) // varData.VarRegionCount + if vec: + m = len(vec) // varData.VarRegionCount flags = component.flags - vec = list(batched(vec.values, m)) + vec = list(batched(vec, m)) newVec = [] for v in vec: v = list(v) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 33b1787a0..e0dfe3e93 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -6,6 +6,7 @@ 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 @@ -20,7 +21,6 @@ from .otBase import ( from .otTables import ( lookupTypes, VarCompositeGlyph, - TupleValues, AATStateTable, AATState, AATAction, @@ -1803,6 +1803,21 @@ 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, values): + 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() + + def cff2_index_read_item(self, i): self.reader.seek(self.offset_pos + i * self.offSize) offsets = self.readArray(2) @@ -1813,18 +1828,30 @@ def cff2_index_read_item(self, i): obj = self._itemClass() obj.decompile(item, self.font) item = obj - + elif self._converter is not None: + item = self._converter.read(item, self.font) return item class CFF2Index(BaseConverter): def __init__( - self, name, repeat, aux, tableClass=None, *, itemClass=None, description="" + 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() @@ -1857,6 +1884,8 @@ class CFF2Index(BaseConverter): obj = self._itemClass() obj.decompile(item, font) item = obj + elif self._converter is not None: + item = self._converter.read(item, font) items.append(item) lastOffset = offset @@ -1870,6 +1899,7 @@ class CFF2Index(BaseConverter): l._itemClass = self._itemClass l.offSize = offSize l.readArray = getReadArray(l.reader, offSize) + l._converter = self._converter # TODO: Advance reader @@ -1884,6 +1914,8 @@ class CFF2Index(BaseConverter): 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, item) for item in items] offsets = [len(item) for item in items] offsets = [0] + list(accumulate(offsets)) @@ -1912,13 +1944,26 @@ class CFF2Index(BaseConverter): writer.writeData(item) def xmlRead(self, attrs, content, font): - obj = self._itemClass() - obj.fromXML(None, attrs, content, font) - return obj + 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): - for i, item in enumerate(value): - item.toXML(xmlWriter, font, [("index", i)], name) + 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): @@ -1999,7 +2044,7 @@ converterMapping = { "CompositeMode": CompositeMode, "STATFlags": STATFlags, "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph), - "MultiVarDataValue": partial(CFF2Index, itemClass=TupleValues), + "MultiVarDataValue": partial(CFF2Index, itemConverterClass=TupleValues), # AAT "CIDGlyphMap": CIDGlyphMap, "GlyphCIDMap": GlyphCIDMap, diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 4e855bf6f..57c22e609 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -12,7 +12,6 @@ from math import radians import itertools from collections import defaultdict, namedtuple from fontTools.ttLib.tables.otTraverse import dfs_base_table -from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.misc.arrayTools import quantizeRect from fontTools.misc.roundTools import otRound from fontTools.misc.transform import Transform, Identity, DecomposedTransform @@ -423,29 +422,6 @@ class VarCompositeGlyphs(BaseTable): self.glyphs.append(glyph) -class TupleValues(BaseTable): - def __init__(self, values=None): - self.values = values or [] - - def populateDefaults(self, propagator=None): - if not hasattr(self, "values"): - self.values = [] - - def decompile(self, data, font): - self.values = TupleVariation.decompileDeltas_(None, data)[0] - - def compile(self, font): - return bytes(TupleVariation.compileDeltaValues_(self.values, bytearr=None)) - - def toXML(self, xmlWriter, font, attrs, name): - xmlWriter.simpletag(name, attrs + [("value", self.values)]) - xmlWriter.newline() - - def fromXML(self, name, attrs, content, font): - self.populateDefaults() - self.values = safeEval(attrs["value"]) - - class AATStateTable(object): def __init__(self): self.GlyphClasses = {} # GlyphID --> GlyphClass diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index 625e05e00..3d7ba71ae 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -126,8 +126,6 @@ def MultiVarData_addItem(self, deltas, *, round=round): for d in deltas: values.extend(d) - values = ot.TupleValues(values) - self.Item.append(values) self.ItemCount = len(self.Item) @@ -167,7 +165,6 @@ class MultiVarStoreInstancer(object): @staticmethod def interpolateFromDeltasAndScalars(deltas, scalars): - deltas = deltas.values if not deltas: return Vector([]) assert len(deltas) % len(scalars) == 0, (len(deltas), len(scalars)) From c91984ef7727906061708345817ce8c2967169a7 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 18 Dec 2023 13:15:38 -0700 Subject: [PATCH 064/114] [VARC] Use sparse-regions in MultiVarStore Might revert as the savings are small. https://github.com/harfbuzz/boring-expansion-spec/issues/103#issuecomment-1861531669 --- Lib/fontTools/ttLib/tables/otData.py | 25 +- Lib/fontTools/varLib/builder.py | 37 ++- Lib/fontTools/varLib/instancer/__init__.py | 25 +- Lib/fontTools/varLib/multiVarStore.py | 29 ++- Lib/fontTools/varLib/varStore.py | 6 +- Tests/ttLib/data/varc-6868.ttf | Bin 20296 -> 9852 bytes Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 263 ++------------------ Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 3552 -> 3292 bytes 8 files changed, 115 insertions(+), 270 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 9c9efc595..6e34945bf 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3323,6 +3323,29 @@ otData = [ ], ), # 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", [ @@ -3336,7 +3359,7 @@ otData = [ "MultiVarStore", [ ("uint16", "Format", None, None, "Set to 1."), - ("LOffset", "VarRegionList", None, None, ""), + ("LOffset", "SparseVarRegionList", None, None, ""), ("uint16", "MultiVarDataCount", None, None, ""), ("LOffset", "MultiVarData", "MultiVarDataCount", 0, ""), ], diff --git a/Lib/fontTools/varLib/builder.py b/Lib/fontTools/varLib/builder.py index f05802770..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] @@ -147,7 +182,7 @@ def buildMultiVarData(varRegionIndices, items): def buildMultiVarStore(varRegionList, multiVarDataList): self = ot.MultiVarStore() self.Format = 1 - self.VarRegionList = varRegionList + self.SparseVarRegionList = varRegionList self.MultiVarData = list(multiVarDataList) self.MultiVarDataCount = len(self.MultiVarData) return self diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index 69977d257..b3e27bd2c 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -469,7 +469,9 @@ class OverlapMode(IntEnum): def instantiateVARC(varfont, axisLimits): log.info("Instantiating VARC tables") - # TODO(behdad) My confidence in this function is rather low + # 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 if varc.VarCompositeGlyphs: @@ -495,29 +497,28 @@ def instantiateVARC(varfont, axisLimits): fvar = varfont["fvar"] location = axisLimits.pinnedLocation() - for region in store.VarRegionList.Region: - assert len(region.VarRegionAxis) == len(fvar.axes) + for region in store.SparseVarRegionList.Region: newRegionAxis = [] - for regionTriple, fvarAxis in zip(region.VarRegionAxis, fvar.axes): - tag = fvarAxis.axisTag + for regionRecord in region.SparseVarRegionAxis: + tag = fvar.axes[regionRecord.AxisIndex].axisTag if tag in location: continue if tag in axisLimits: limits = axisLimits[tag] triple = ( - regionTriple.StartCoord, - regionTriple.PeakCoord, - regionTriple.EndCoord, + regionRecord.StartCoord, + regionRecord.PeakCoord, + regionRecord.EndCoord, ) triple = tuple( limits.renormalizeValue(v, extrapolate=False) for v in triple ) ( - regionTriple.StartCoord, - regionTriple.PeakCoord, - regionTriple.EndCoord, + regionRecord.StartCoord, + regionRecord.PeakCoord, + regionRecord.EndCoord, ) = triple - newRegionAxis.append(regionTriple) + newRegionAxis.append(regionRecord) region.VarRegionAxis = newRegionAxis region.VarRegionAxisCount = len(newRegionAxis) diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index 3d7ba71ae..6c755118b 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -6,7 +6,8 @@ from fontTools.varLib.models import supportScalar import fontTools.varLib.varStore # For monkey-patching from fontTools.varLib.builder import ( buildVarRegionList, - buildVarRegion, + buildSparseVarRegionList, + buildSparseVarRegion, buildMultiVarStore, buildMultiVarData, ) @@ -28,7 +29,7 @@ class OnlineMultiVarStoreBuilder(object): def __init__(self, axisTags): self._axisTags = axisTags self._regionMap = {} - self._regionList = buildVarRegionList([], axisTags) + self._regionList = buildSparseVarRegionList([], axisTags) self._store = buildMultiVarStore(self._regionList, []) self._data = None self._model = None @@ -64,7 +65,7 @@ class OnlineMultiVarStoreBuilder(object): key = _getLocationKey(region) idx = regionMap.get(key) if idx is None: - varRegion = buildVarRegion(region, self._axisTags) + varRegion = buildSparseVarRegion(region, self._axisTags) idx = regionMap[key] = len(regionList.Region) regionList.Region.append(varRegion) regionIndices.append(idx) @@ -133,6 +134,16 @@ def MultiVarData_addItem(self, deltas, *, round=round): 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) @@ -145,7 +156,9 @@ class MultiVarStoreInstancer(object): self.fvar_axes = fvar_axes assert multivarstore is None or multivarstore.Format == 1 self._varData = multivarstore.MultiVarData if multivarstore else [] - self._regions = multivarstore.VarRegionList.Region if multivarstore else [] + self._regions = ( + multivarstore.SparseVarRegionList.Region if multivarstore else [] + ) self.setLocation(location) def setLocation(self, location): @@ -195,8 +208,10 @@ def MultiVarStore_subset_varidxes(self, varIdxes): return ot.VarStore.subset_varidxes(self, varIdxes, VarData="MultiVarData") -def MultiVarStore_prune_regions(self, *, VarData="VarData"): - return ot.VarStore.prune_regions(self, VarData="MultiVarData") +def MultiVarStore_prune_regions(self): + return ot.VarStore.prune_regions( + self, VarData="MultiVarData", VarRegionList="SparseVarRegionList" + ) ot.MultiVarStore.prune_regions = MultiVarStore_prune_regions @@ -207,7 +222,7 @@ def MultiVarStore_get_supports(self, major, fvarAxes): supports = [] varData = self.MultiVarData[major] for regionIdx in varData.VarRegionIndex: - region = self.VarRegionList.Region[regionIdx] + region = self.SparseVarRegionList.Region[regionIdx] support = region.get_support(fvarAxes) supports.append(support) return supports diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py index ecfc02fc7..f54fad2db 100644 --- a/Lib/fontTools/varLib/varStore.py +++ b/Lib/fontTools/varLib/varStore.py @@ -305,7 +305,7 @@ def VarStore_subset_varidxes( setattr(self, VarData, newVarData) setattr(self, VarData + "Count", len(newVarData)) - self.prune_regions(VarData=VarData) + self.prune_regions() return varDataMap @@ -313,7 +313,7 @@ def VarStore_subset_varidxes( ot.VarStore.subset_varidxes = VarStore_subset_varidxes -def VarStore_prune_regions(self, *, VarData="VarData"): +def VarStore_prune_regions(self, *, VarData="VarData", VarRegionList="VarRegionList"): """Remove unused VarRegions.""" # # Subset VarRegionList @@ -324,7 +324,7 @@ def VarStore_prune_regions(self, *, VarData="VarData"): for data in getattr(self, VarData): usedRegions.update(data.VarRegionIndex) # Subset. - regionList = self.VarRegionList + regionList = getattr(self, VarRegionList) regions = regionList.Region newRegions = [] regionMap = {} diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index 66cf83e30d3a31822ebc03dd304b869ca1c61e0c..71d94e04244b1661728d69e8139c10978cea12df 100644 GIT binary patch delta 648 zcmX|;JuE{}6oAireJ!uAs;DX+gN0EfmPWWDDTBn%AR;6Nvx&s^#N0)i7|Yn$s98FY zGzNpoU^0k^!DNsQzP{I<+16HlG(ss5wUkgVm%^`Et06pB!wzagoaoxmK<8|9B zxJcW2u^=jw&_quyqlsya^=YhMZ9t8WFj2ihwVc|J8t-VLyx|4a5w%e@Uu{f{pEm8q R9ILiBTXHR_ea`P^{sEKMNX`HN delta 790 zcmez4b7CH&3 - - + - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index a8cbf6f7415c8d267e03749a9fcbcd4468054277..cfe10a044f2552e06cac8c125266d6f11c43e369 100644 GIT binary patch delta 122 zcmaDLeMeG;fsuiMfrp`iftkS}%rVG$d)?Hz3=FIR3=E786J@NJ*d}a@FkxYdGj_Z; zc>>EhCWaN06Iew|861Eb7#R3~SOSQZz{UUt9f0~kY%q%nC<7A%(UVuRz1TdJ-J1;n D%}o`z delta 157 zcmca3`9NBRfsuiMfrp`iftkS}%rVG$>Vcig3=FIR3=B+86J@NJ*r#laFkxX?xbDZ9 z$rD)4F)^;1oWLr=$>_u&2-L#hFxin)X7UF%0Tf)o!q13Rh6`w!0}$fUxRUh+Bg5ti HY(8uNAN4Hy From 42a5fbdfddadac23b8a9e9f5a7009c4a36f2d3c3 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 19 Dec 2023 14:35:44 -0700 Subject: [PATCH 065/114] [VARC] Redesign table No spec yet. For results see: https://github.com/harfbuzz/boring-expansion-spec/issues/103#issuecomment-1863533305 --- Lib/fontTools/misc/transform.py | 13 + Lib/fontTools/ttLib/tables/otConverters.py | 14 +- Lib/fontTools/ttLib/tables/otData.py | 30 +- Lib/fontTools/ttLib/tables/otTables.py | 409 +++++++++------------ Lib/fontTools/varLib/models.py | 5 +- Lib/fontTools/varLib/multiVarStore.py | 4 + 6 files changed, 229 insertions(+), 246 deletions(-) diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index a8c1dd317..755900bf2 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -442,6 +442,19 @@ class DecomposedTransform: return NotImplemented return not self == other + 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/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index e0dfe3e93..d9c2b36be 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -21,6 +21,7 @@ from .otBase import ( from .otTables import ( lookupTypes, VarCompositeGlyph, + VarTransform, AATStateTable, AATState, AATAction, @@ -1945,9 +1946,13 @@ class CFF2Index(BaseConverter): def xmlRead(self, attrs, content, font): if self._itemClass is not None: - obj = self._itemClass() - obj.fromXML(None, attrs, content, font) - return obj + lst = [] + content = [t for t in content if isinstance(t, tuple)] + for eltName, eltAttrs, eltContent in content: + obj = self._itemClass() + obj.fromXML(eltName, eltAttrs, eltContent, font) + lst.append(obj) + return lst elif self._converter is not None: return self._converter.xmlRead(attrs, content, font) else: @@ -2043,8 +2048,9 @@ converterMapping = { "ExtendMode": ExtendMode, "CompositeMode": CompositeMode, "STATFlags": STATFlags, + "TupleList": partial(CFF2Index, itemConverterClass=TupleValues), "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph), - "MultiVarDataValue": partial(CFF2Index, itemConverterClass=TupleValues), + "VarTransformList": partial(CFF2Index, itemClass=VarTransform), # AAT "CIDGlyphMap": CIDGlyphMap, "GlyphCIDMap": GlyphCIDMap, diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 6e34945bf..d31e64769 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3352,7 +3352,7 @@ otData = [ ("uint8", "Format", None, None, "Set to 1."), ("uint16", "VarRegionCount", None, None, ""), ("uint16", "VarRegionIndex", "VarRegionCount", 0, ""), - ("MultiVarDataValue", "Item", "", 0, ""), + ("TupleList", "Item", "", 0, ""), ], ), ( @@ -3377,13 +3377,39 @@ otData = [ ), ("LOffset", "Coverage", None, None, ""), ("LOffset", "MultiVarStore", None, None, "(may be NULL)"), + ("LOffset", "AxisIndicesList", None, None, "(may be NULL)"), + ("LOffset", "AxisValuesList", None, None, "(may be NULL)"), + ("LOffset", "TransformList", None, None, "(may be NULL)"), ("LOffset", "VarCompositeGlyphs", None, None, ""), ], ), + ( + "AxisIndicesList", + [ + ("TupleList", "Item", "", 0, ""), + ], + ), + ( + "AxisValuesList", + [ + # A hack here would be to encode the VarIndex as the first member + # of the tuple to stored. But I find that ugly. The current + # design is not ideal but cleaner. + ("uint32", "VarIndicesCount", None, None, ""), + ("VarIndex", "VarIndices", "VarIndicesCount", 0, ""), + ("TupleList", "Item", "", 0, ""), + ], + ), + ( + "TransformList", + [ + ("VarTransformList", "VarTransform", "", None, ""), + ], + ), ( "VarCompositeGlyphs", [ - ("VarCompositeGlyphList", "glyphs", None, None, ""), + ("VarCompositeGlyphList", "VarCompositeGlyph", "", None, ""), ], ), # Glyph advance variations diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 57c22e609..5dd52cd2c 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -47,110 +47,55 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -class VarComponentFlags(IntFlag): - GID_IS_24BIT = 0x0001 - AXIS_INDICES_ARE_SHORT = 0x0002 - AXIS_VALUES_HAVE_VARIATION = 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 - TRANSFORM_HAS_VARIATION = 0x1000 - RESET_UNSPECIFIED_AXES = 0x2000 - USE_MY_METRICS = 0x4000 +class VarTransformFlags(IntFlag): + HAVE_TRANSLATE_X = 0x0001 + HAVE_TRANSLATE_Y = 0x0002 + HAVE_ROTATION = 0x0004 + HAVE_SCALE_X = 0x0008 + HAVE_SCALE_Y = 0x0010 + HAVE_SKEW_X = 0x0020 + HAVE_SKEW_Y = 0x0040 + HAVE_TCENTER_X = 0x0080 + HAVE_TCENTER_Y = 0x0100 + HAVE_VARIATIONS = 0x0200 + RESERVED = 0x0400 | 0x0800 | 0x1000 | 0x2000 | 0x4000 -VarComponentTransformMappingValues = namedtuple( - "VarComponentTransformMappingValues", +VarTransformMappingValues = namedtuple( + "VarTransformMappingValues", ["flag", "fractionalBits", "scale", "defaultValue"], ) -VAR_COMPONENT_TRANSFORM_MAPPING = { - "translateX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 +VAR_TRANSFORM_MAPPING = { + "translateX": VarTransformMappingValues( + VarTransformFlags.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 + "translateY": VarTransformMappingValues( + VarTransformFlags.HAVE_TRANSLATE_Y, 0, 1, 0 ), + "rotation": VarTransformMappingValues(VarTransformFlags.HAVE_ROTATION, 12, 180, 0), + "scaleX": VarTransformMappingValues(VarTransformFlags.HAVE_SCALE_X, 10, 1, 1), + "scaleY": VarTransformMappingValues(VarTransformFlags.HAVE_SCALE_Y, 10, 1, 1), + "skewX": VarTransformMappingValues(VarTransformFlags.HAVE_SKEW_X, 12, -180, 0), + "skewY": VarTransformMappingValues(VarTransformFlags.HAVE_SKEW_Y, 12, 180, 0), + "tCenterX": VarTransformMappingValues(VarTransformFlags.HAVE_TCENTER_X, 0, 1, 0), + "tCenterY": VarTransformMappingValues(VarTransformFlags.HAVE_TCENTER_Y, 0, 1, 0), } -class VarComponent: +class VarTransform: def __init__(self): - self.glyphName = None - self.location = {} + self.flags = 0 self.transform = DecomposedTransform() - self.locationVarIndex = NO_VARIATION_INDEX - self.transformVarIndex = NO_VARIATION_INDEX + self.varIndex = NO_VARIATION_INDEX def decompile(self, data, font): i = 0 - flags = struct.unpack(">H", data[i : i + 2])[0] + self.flags = flags = struct.unpack(">H", data[i : i + 2])[0] i += 2 - self.flags = flags - numAxes = data[i] - i += 1 - - if flags & VarComponentFlags.GID_IS_24BIT: - glyphID = struct.unpack(">L", b"\0" + data[i : i + 3])[0] - i += 3 - flags ^= VarComponentFlags.GID_IS_24BIT - else: - glyphID = struct.unpack(">H", data[i : i + 2])[0] - i += 2 - self.glyphName = font.glyphOrder[glyphID] - - if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: - axisIndices = array.array("H", data[i : i + 2 * numAxes]) - i += 2 * numAxes - if sys.byteorder != "big": - axisIndices.byteswap() - flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT - else: - axisIndices = array.array("B", data[i : i + numAxes]) - i += numAxes - assert len(axisIndices) == numAxes - axisIndices = list(axisIndices) - - axisValues = array.array("h", data[i : i + 2 * numAxes]) - i += 2 * numAxes - if sys.byteorder != "big": - axisValues.byteswap() - assert len(axisValues) == numAxes - axisValues = [fi2fl(v, 14) for v in axisValues] - - axes = font["fvar"].axes - self.location = {axes[i].axisTag: v for i, v in zip(axisIndices, axisValues)} - - if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - self.locationVarIndex = struct.unpack(">L", data[i : i + 4])[0] + if flags & VarTransformFlags.HAVE_VARIATIONS: + self.varIndex = struct.unpack(">L", data[i : i + 4])[0] i += 4 def read_transform_component(data, values): @@ -167,72 +112,39 @@ class VarComponent: else: return values.defaultValue - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): + for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): value = read_transform_component(data, mapping_values) setattr(self.transform, attr_name, value) - if not (flags & VarComponentFlags.HAVE_SCALE_Y): + if not (flags & VarTransformFlags.HAVE_SCALE_Y): self.transform.scaleY = self.transform.scaleX - if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: - self.transformVarIndex = struct.unpack(">L", data[i : i + 4])[0] - i += 4 - return data[i:] def compile(self, font): data = [] + flags = 0 if not hasattr(self, "flags"): flags = 0 # Calculate optimal transform component flags - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): + for attr_name, mapping in VAR_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 - if (flags & VarComponentFlags.HAVE_SCALE_Y) and fl2fi( + if (flags & VarTransformFlags.HAVE_SCALE_Y) and fl2fi( self.transform.scaleX, 10 ) == fl2fi(self.transform.scaleY, 10): - flags ^= VarComponentFlags.HAVE_SCALE_Y + flags ^= VarTransformFlags.HAVE_SCALE_Y + if self.varIndex != NO_VARIATION_INDEX: + flags |= VarTransformFlags.HAVE_VARIATIONS else: flags = self.flags - numAxes = len(self.location) - - data.append(struct.pack(">B", numAxes)) - - glyphID = font.getGlyphID(self.glyphName) - if glyphID > 65535: - flags |= VarComponentFlags.GID_IS_24BIT - data.append(struct.pack(">L", glyphID)[1:]) - else: - if flags & VarComponentFlags.GID_IS_24BIT: - flags ^= VarComponentFlags.GID_IS_24BIT - data.append(struct.pack(">H", glyphID)) - - fvarAxisIndices = {axis.axisTag: i for i, axis in enumerate(font["fvar"].axes)} - axisIndices = [fvarAxisIndices[tag] for tag in self.location.keys()] - if all(a <= 255 for a in axisIndices): - axisIndices = array.array("B", axisIndices) - if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: - flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT - else: - axisIndices = array.array("H", axisIndices) - if sys.byteorder != "big": - axisIndices.byteswap() - flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT - data.append(bytes(axisIndices)) - - axisValues = self.location.values() - axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues)) - if sys.byteorder != "big": - axisValues.byteswap() - data.append(bytes(axisValues)) - - if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - data.append(struct.pack(">L", self.locationVarIndex)) + if flags & VarTransformFlags.HAVE_VARIATIONS: + data.append(struct.pack(">L", self.varIndex)) def write_transform_component(value, values): if flags & values.flag: @@ -242,79 +154,36 @@ class VarComponent: else: return b"" - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): + for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): value = getattr(self.transform, attr_name) - # How to drop scaleX == scaleY here? data.append(write_transform_component(value, mapping_values)) - if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: - data.append(struct.pack(">L", self.transformVarIndex)) - return struct.pack(">H", flags) + bytesjoin(data) - def toXML(self, writer, ttFont, attrs): - attrs.append(("glyphName", self.glyphName)) + def toXML(self, writer, ttFont, attrs, name): + if self.varIndex != NO_VARIATION_INDEX: + attrs.append(("varIndex", str(self.varIndex))) - if hasattr(self, "flags"): - attrs = attrs + [("flags", hex(self.flags))] - - # TODO Move to an element? - if self.locationVarIndex != NO_VARIATION_INDEX: - attrs = attrs + [("locationVarIndex", self.locationVarIndex)] - if self.transformVarIndex != NO_VARIATION_INDEX: - attrs = attrs + [("transformVarIndex", self.transformVarIndex)] - - # TODO Move transform into it's own element? - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): + for attr_name, mapping in VAR_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.simpletag("VarTransform", attrs) writer.newline() def fromXML(self, name, attrs, content, ttFont): - self.glyphName = attrs["glyphName"] + if "varIndex" in attrs: + self.varIndex = safeEval(attrs["varIndex"]) + else: + self.varIndex = NO_VARIATION_INDEX - if "flags" in attrs: - self.flags = safeEval(attrs["flags"]) - - if "locationVarIndex" in attrs: - self.locationVarIndex = safeEval(attrs["locationVarIndex"]) - if "transformVarIndex" in attrs: - self.transformVarIndex = safeEval(attrs["transformVarIndex"]) - - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): + for attr_name, mapping in VAR_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 __eq__(self, other): if type(self) != type(other): return NotImplemented @@ -324,53 +193,130 @@ class VarComponent: result = self.__eq__(other) return result if result is NotImplemented else not result - def getComponentValues(self): - flags = self.flags - locationValues = [fl2fi(v, 14) for v in self.location.values()] +class VarComponentFlags(IntFlag): + GID_IS_24BIT = 0x01 + INDICES_ARE_LONG = 0x02 + HAVE_LOCATION = 0x04 + HAVE_TRANSFORM = 0x08 + RESET_UNSPECIFIED_AXES = 0x10 + USE_MY_METRICS = 0x20 + RETAIN_FLAGS = RESET_UNSPECIFIED_AXES | USE_MY_METRICS + RESERVED = 0x40 | 0x80 - transformValues = [] - def append_transform_component(value, values): - nonlocal transformValues - if flags & values.flag: - transformValues.append( - fl2fi(value / values.scale, values.fractionalBits) - ) +class VarComponent: + def __init__(self): + self.flags = 0 + self.glyphName = None + self.AxisIndicesIndex = None + self.AxisValuesIndex = None + self.TransformIndex = None - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - value = getattr(self.transform, attr_name) - append_transform_component(value, mapping_values) - - return Vector(locationValues), Vector(transformValues) - - def setComponentValues(self, locationValues, transformValues): - flags = self.flags - - assert len(locationValues) == len(self.location), ( - len(locationValues), - len(self.location), - ) - self.location = { - tag: fi2fl(v, 14) for tag, v in zip(self.location.keys(), locationValues) - } - - self.transform = DecomposedTransform(self.transform) + def decompile(self, data, font): i = 0 + flags = data[i] + i += 1 + self.flags = flags & VarComponentFlags.RETAIN_FLAGS - def read_transform_component(values): - nonlocal transformValues, i - if flags & values.flag: - v = fi2fl(transformValues[i], values.fractionalBits) * values.scale - i += 1 - return v - else: - return values.defaultValue + if flags & VarComponentFlags.GID_IS_24BIT: + glyphID = struct.unpack(">L", b"\0" + data[i : i + 3])[0] + i += 3 + flags ^= VarComponentFlags.GID_IS_24BIT + else: + glyphID = struct.unpack(">H", data[i : i + 2])[0] + i += 2 + self.glyphName = font.glyphOrder[glyphID] - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - value = read_transform_component(mapping_values) - setattr(self.transform, attr_name, value) - assert i == len(transformValues), (i, len(transformValues)) + self.AxisIndicesIndex = self.AxisValuesIndex = self.TransformIndex = None + if flags & VarComponentFlags.INDICES_ARE_LONG == 0x0004: + if flags & VarComponentFlags.HAVE_LOCATION: + self.AxisIndicesIndex, self.AxisValuesIndex = struct.unpack( + ">LL", data[i : i + 8] + ) + i += 8 + if flags & VarComponentFlags.HAVE_TRANSFORM: + self.TransformIndex = struct.unpack(">L", data[i : i + 4])[0] + i += 4 + else: + if flags & VarComponentFlags.HAVE_LOCATION: + self.AxisIndicesIndex, self.AxisValuesIndex = struct.unpack( + ">HH", data[i : i + 4] + ) + i += 4 + if flags & VarComponentFlags.HAVE_TRANSFORM: + self.TransformIndex = struct.unpack(">H", data[i : i + 2])[0] + i += 2 + + return data[i:] + + def compile(self, font): + data = [] + + flags = self.flags & VarComponentFlags.RETAIN_FLAGS + + glyphID = font.getGlyphID(self.glyphName) + if glyphID > 65535: + flags |= VarComponentFlags.GID_IS_24BIT + data.append(struct.pack(">L", glyphID)[1:]) + else: + data.append(struct.pack(">H", glyphID)) + + for attr in ("AxisIndicesIndex", "AxisValuesIndex", "TransformIndex"): + value = getattr(self, attr) + if value is not None and value > 65535: + flags |= VarComponentFlags.INDICES_ARE_LONG + break + + fmt = "L" if flags & VarComponentFlags.INDICES_ARE_LONG else "H" + + assert (self.AxisIndicesIndex is None) == (self.AxisValuesIndex is None) + if self.AxisIndicesIndex is not None: + flags |= VarComponentFlags.HAVE_LOCATION + data.append( + struct.pack( + ">" + (fmt * 2), self.AxisIndicesIndex, self.AxisValuesIndex + ) + ) + if self.TransformIndex is not None: + flags |= VarComponentFlags.HAVE_TRANSFORM + data.append(struct.pack(">" + fmt, self.TransformIndex)) + + return struct.pack("B", flags) + bytesjoin(data) + + def toXML(self, writer, ttFont, attrs): + attrs.append(("glyphName", self.glyphName)) + attrs.append(("flags", self.flags & VarComponentFlags.RETAIN_FLAGS)) + + assert (self.AxisIndicesIndex is None) == (self.AxisValuesIndex is None) + if self.AxisIndicesIndex is not None: + attrs.append(("AxisIndicesIndex", self.AxisIndicesIndex)) + attrs.append(("AxisValuesIndex", self.AxisIndicesIndex)) + if self.TransformIndex is not None: + attrs.append(("TransformIndex", self.TransformIndex)) + + writer.simpletag("VarComponent", attrs) + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + self.flags = safeEval(attrs["flags"]) & VarComponentFlags.RETAIN_FLAGS + self.glyphName = attrs["glyphName"] + + assert ("AxisIndicesIndex" in attrs) == ("AxisValuesIndex" in attrs) + if "AxisIndicesIndex" in attrs: + self.AxisIndicesIndex = safeEval(attrs["AxisIndicesIndex"]) + self.AxisValuesIndex = safeEval(attrs["AxisValuesIndex"]) + if "TransformIndex" in attrs: + self.TransformIndex = safeEval(attrs["TransformIndex"]) + + 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(BaseTable): @@ -407,21 +353,6 @@ class VarCompositeGlyph(BaseTable): self.components.append(component) -class VarCompositeGlyphs(BaseTable): - def populateDefaults(self, propagator=None): - if not hasattr(self, "glyphs"): - self.glyphs = [] - - def fromXML(self, name, attrs, content, font): - self.populateDefaults() - if name == "VarCompositeGlyph": - glyph = VarCompositeGlyph() - content = [t for t in content if isinstance(t, tuple)] - for eltName, eltAttrs, eltContent in content: - glyph.fromXML(eltName, eltAttrs, eltContent, font) - self.glyphs.append(glyph) - - class AATStateTable(object): def __init__(self): self.GlyphClasses = {} # GlyphID --> GlyphClass diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py index 59815316f..e90b66885 100644 --- a/Lib/fontTools/varLib/models.py +++ b/Lib/fontTools/varLib/models.py @@ -453,7 +453,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 index 6c755118b..c29899746 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -97,6 +97,10 @@ class OnlineMultiVarStoreBuilder(object): 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: From 9cc3689fb16504921075666861dccee5a9dcefaa Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 19 Dec 2023 16:07:27 -0700 Subject: [PATCH 066/114] [VARC] Towards drawing new design --- Lib/fontTools/ttLib/tables/otTables.py | 24 +++++++++++- Lib/fontTools/ttLib/ttGlyphSet.py | 53 ++++++++++++++++---------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 5dd52cd2c..4e073bac7 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -143,6 +143,8 @@ class VarTransform: else: flags = self.flags + data.append(struct.pack(">H", flags)) + if flags & VarTransformFlags.HAVE_VARIATIONS: data.append(struct.pack(">L", self.varIndex)) @@ -158,7 +160,7 @@ class VarTransform: value = getattr(self.transform, attr_name) data.append(write_transform_component(value, mapping_values)) - return struct.pack(">H", flags) + bytesjoin(data) + return bytesjoin(data) def toXML(self, writer, ttFont, attrs, name): if self.varIndex != NO_VARIATION_INDEX: @@ -184,6 +186,26 @@ class VarTransform: v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits) setattr(self.transform, attr_name, v) + def applyDeltas(deltas): + i = 0 + + def read_transform_component_delta(data, values): + nonlocal i + if self.flags & values.flag: + v = fi2fl(data[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(data, mapping_values) + setattr( + self.transform, attr_name, getattr(self.transform, attr_name) + value + ) + + assert i == len(deltas) + def __eq__(self, other): if type(self) != type(other): return NotImplemented diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index d848c8670..d5107236b 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -5,9 +5,10 @@ from collections.abc import Mapping from contextlib import contextmanager from copy import copy from types import SimpleNamespace +from fontTools.misc.vector import Vector from fontTools.misc.fixedTools import otRound 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, @@ -288,42 +289,54 @@ class _TTGlyphVARC(_TTGlyph): glyphSet = self.glyphSet varc = glyphSet.varcTable idx = varc.Coverage.glyphs.index(self.name) - glyph = varc.VarCompositeGlyphs.glyphs[idx] + glyph = varc.VarCompositeGlyphs.VarCompositeGlyph[idx] from fontTools.varLib.multiVarStore import MultiVarStoreInstancer + fvarAxes = glyphSet.font["fvar"].axes instancer = MultiVarStoreInstancer( - varc.MultiVarStore, self.glyphSet.font["fvar"].axes, self.glyphSet.location + varc.MultiVarStore, fvarAxes, self.glyphSet.location ) - instancer.setLocation(self.glyphSet.location) for comp in glyph.components: - comp = copy(comp) # Shallow copy - locationValues, transformValues = comp.getComponentValues() + location = {} + assert (comp.AxisIndicesIndex is None) == (comp.AxisValuesIndex is None) + if comp.AxisIndicesIndex is not None: + axisIndices = varc.AxisIndicesList.Item[comp.AxisIndicesIndex] + axisValues = Vector(varc.AxisValuesList.Item[comp.AxisValuesIndex]) + # Apply variations + varIdx = NO_VARIATION_INDEX + if comp.AxisValuesIndex < varc.AxisValuesList.VarIndicesCount: + varIdx = varc.AxisValuesList.VarIndices[comp.AxisValuesIndex] + if varIdx != NO_VARIATION_INDEX: + axisValues = ( + axisValues + instancer[varIdx] + ) # TODO Implement __iadd__ for Vector + location = { + fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues) + } - if comp.locationVarIndex != NO_VARIATION_INDEX: - assert len(locationValues) - locationDeltas = instancer[comp.locationVarIndex] - locationValues = list(locationValues + locationDeltas) - if comp.transformVarIndex != NO_VARIATION_INDEX: - assert len(transformValues) - transformDeltas = instancer[comp.transformVarIndex] - transformValues = list(transformValues + transformDeltas) - - comp.setComponentValues(locationValues, transformValues) + transform = DecomposedTransform() + if comp.TransformIndex is not None: + varTransform = varc.TransformList.VarTransform[comp.TransformIndex] + varIdx = varTransform.varIndex + if varIdx != NO_VARIATION_INDEX: + deltas = instancer[varIdx] + varTransform.applyDeltas(deltas) + transform = varTransform.transform with self.glyphSet.glyphSet.pushLocation( - comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES + location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES ): with self.glyphSet.pushLocation( - comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES + location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES ): try: pen.addVarComponent( - comp.glyphName, comp.transform, self.glyphSet.rawLocation + comp.glyphName, transform, self.glyphSet.rawLocation ) except AttributeError: - t = comp.transform.toTransform() + t = transform.toTransform() compGlyphSet = ( self.glyphSet if comp.glyphName != self.name From f73d6f2a6f6cdbf5c47992af58e9ac00e974fff4 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 19 Dec 2023 17:02:05 -0700 Subject: [PATCH 067/114] [VARC] Fixups --- Lib/fontTools/ttLib/tables/otTables.py | 4 ++-- Lib/fontTools/ttLib/ttGlyphSet.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 4e073bac7..944ac696b 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -186,7 +186,7 @@ class VarTransform: v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits) setattr(self.transform, attr_name, v) - def applyDeltas(deltas): + def applyDeltas(self, deltas): i = 0 def read_transform_component_delta(data, values): @@ -199,7 +199,7 @@ class VarTransform: return 0 for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): - value = read_transform_component_delta(data, mapping_values) + value = read_transform_component_delta(deltas, mapping_values) setattr( self.transform, attr_name, getattr(self.transform, attr_name) + value ) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index d5107236b..f3772d118 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -6,7 +6,7 @@ from contextlib import contextmanager from copy import copy from types import SimpleNamespace from fontTools.misc.vector import Vector -from fontTools.misc.fixedTools import otRound +from fontTools.misc.fixedTools import otRound, fixedToFloat as fi2fl from fontTools.misc.loggingTools import deprecateFunction from fontTools.misc.transform import Transform, DecomposedTransform from fontTools.pens.transformPen import TransformPen, TransformPointPen @@ -312,8 +312,9 @@ class _TTGlyphVARC(_TTGlyph): axisValues = ( axisValues + instancer[varIdx] ) # TODO Implement __iadd__ for Vector + assert len(axisIndices) == len(axisValues), (len(axisIndices), len(axisValues)) location = { - fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues) + fvarAxes[i].axisTag: fi2fl(v, 14) for i, v in zip(axisIndices, axisValues) } transform = DecomposedTransform() From 8ea97657de625d1fa0bed1cc085928c60c374560 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 19 Dec 2023 18:13:29 -0700 Subject: [PATCH 068/114] [VARC] Fix copilot mistake in decompile Ouch! --- Lib/fontTools/ttLib/tables/otTables.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 944ac696b..807f39d42 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -244,14 +244,13 @@ class VarComponent: if flags & VarComponentFlags.GID_IS_24BIT: glyphID = struct.unpack(">L", b"\0" + data[i : i + 3])[0] i += 3 - flags ^= VarComponentFlags.GID_IS_24BIT else: glyphID = struct.unpack(">H", data[i : i + 2])[0] i += 2 self.glyphName = font.glyphOrder[glyphID] self.AxisIndicesIndex = self.AxisValuesIndex = self.TransformIndex = None - if flags & VarComponentFlags.INDICES_ARE_LONG == 0x0004: + if flags & VarComponentFlags.INDICES_ARE_LONG: if flags & VarComponentFlags.HAVE_LOCATION: self.AxisIndicesIndex, self.AxisValuesIndex = struct.unpack( ">LL", data[i : i + 8] From 2229607dd139fcf70027abae861da89b7049da32 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 19 Dec 2023 18:32:27 -0700 Subject: [PATCH 069/114] Black --- Lib/fontTools/ttLib/ttGlyphSet.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index f3772d118..e02caef69 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -312,9 +312,13 @@ class _TTGlyphVARC(_TTGlyph): axisValues = ( axisValues + instancer[varIdx] ) # TODO Implement __iadd__ for Vector - assert len(axisIndices) == len(axisValues), (len(axisIndices), len(axisValues)) + assert len(axisIndices) == len(axisValues), ( + len(axisIndices), + len(axisValues), + ) location = { - fvarAxes[i].axisTag: fi2fl(v, 14) for i, v in zip(axisIndices, axisValues) + fvarAxes[i].axisTag: fi2fl(v, 14) + for i, v in zip(axisIndices, axisValues) } transform = DecomposedTransform() From 43e054b377b73cf0b4bccbb6f94649926218b6f7 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 19 Dec 2023 19:06:00 -0700 Subject: [PATCH 070/114] [VARC] Minor --- Lib/fontTools/ttLib/tables/otTables.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 807f39d42..3e330bd41 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -134,10 +134,8 @@ class VarTransform: mapping.defaultValue / mapping.scale, mapping.fractionalBits ): flags |= mapping.flag - if (flags & VarTransformFlags.HAVE_SCALE_Y) and fl2fi( - self.transform.scaleX, 10 - ) == fl2fi(self.transform.scaleY, 10): - flags ^= VarTransformFlags.HAVE_SCALE_Y + if fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10): + flags &= ~VarTransformFlags.HAVE_SCALE_Y if self.varIndex != NO_VARIATION_INDEX: flags |= VarTransformFlags.HAVE_VARIATIONS else: From 822351f12d1a4adeba493db655d316650aef16ae Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 19 Dec 2023 19:30:19 -0700 Subject: [PATCH 071/114] [VARC] Minor rename --- Lib/fontTools/ttLib/ttGlyphSet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index e02caef69..4cba0744b 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -347,13 +347,13 @@ class _TTGlyphVARC(_TTGlyph): if comp.glyphName != self.name else glyphSet.glyphSet ) - glyph = compGlyphSet[comp.glyphName] + g = compGlyphSet[comp.glyphName] if isPointPen: tPen = TransformPointPen(pen, t) - glyph.drawPoints(tPen) + g.drawPoints(tPen) else: tPen = TransformPen(pen, t) - glyph.draw(tPen) + g.draw(tPen) def draw(self, pen): self._draw(pen, False) From 76d293ec05d1ad29d9cf6426194bb862496db53b Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 19 Dec 2023 22:51:09 -0700 Subject: [PATCH 072/114] [VARC] Simplify reading --- Lib/fontTools/ttLib/tables/otTables.py | 29 ++++++++++---------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 3e330bd41..154b4e9e0 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -247,25 +247,18 @@ class VarComponent: i += 2 self.glyphName = font.glyphOrder[glyphID] + fmt = "L" if flags & VarComponentFlags.INDICES_ARE_LONG else "H" self.AxisIndicesIndex = self.AxisValuesIndex = self.TransformIndex = None - if flags & VarComponentFlags.INDICES_ARE_LONG: - if flags & VarComponentFlags.HAVE_LOCATION: - self.AxisIndicesIndex, self.AxisValuesIndex = struct.unpack( - ">LL", data[i : i + 8] - ) - i += 8 - if flags & VarComponentFlags.HAVE_TRANSFORM: - self.TransformIndex = struct.unpack(">L", data[i : i + 4])[0] - i += 4 - else: - if flags & VarComponentFlags.HAVE_LOCATION: - self.AxisIndicesIndex, self.AxisValuesIndex = struct.unpack( - ">HH", data[i : i + 4] - ) - i += 4 - if flags & VarComponentFlags.HAVE_TRANSFORM: - self.TransformIndex = struct.unpack(">H", data[i : i + 2])[0] - i += 2 + if flags & VarComponentFlags.HAVE_LOCATION: + l = 8 if flags & VarComponentFlags.INDICES_ARE_LONG else 4 + self.AxisIndicesIndex, self.AxisValuesIndex = struct.unpack( + ">" + fmt * 2, data[i : i + l] + ) + i += l + if flags & VarComponentFlags.HAVE_TRANSFORM: + l = 4 if flags & VarComponentFlags.INDICES_ARE_LONG else 2 + self.TransformIndex = struct.unpack(">" + fmt, data[i : i + l])[0] + i += l return data[i:] From b772f1d68618fad5b4ac44421761c1583bb2b993 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 00:09:41 -0700 Subject: [PATCH 073/114] Reuse a variable --- Lib/fontTools/ttLib/ttGlyphSet.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 4cba0744b..1af88a7fa 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -330,12 +330,9 @@ class _TTGlyphVARC(_TTGlyph): varTransform.applyDeltas(deltas) transform = varTransform.transform - with self.glyphSet.glyphSet.pushLocation( - location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES - ): - with self.glyphSet.pushLocation( - location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES - ): + reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES + with self.glyphSet.glyphSet.pushLocation(location, reset): + with self.glyphSet.pushLocation(location, reset): try: pen.addVarComponent( comp.glyphName, transform, self.glyphSet.rawLocation From f7337b55af89dae0f02e505a839d3932a1d890f5 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 00:15:34 -0700 Subject: [PATCH 074/114] [VARC] Make HAVE_VARIATIONS flag automatic --- Lib/fontTools/ttLib/tables/otTables.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 154b4e9e0..aacf95fe4 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -97,6 +97,7 @@ class VarTransform: if flags & VarTransformFlags.HAVE_VARIATIONS: self.varIndex = struct.unpack(">L", data[i : i + 4])[0] i += 4 + flags = flags & ~VarTransformFlags.HAVE_VARIATIONS def read_transform_component(data, values): nonlocal i @@ -141,10 +142,11 @@ class VarTransform: else: flags = self.flags - data.append(struct.pack(">H", flags)) - - if flags & VarTransformFlags.HAVE_VARIATIONS: + if self.varIndex != NO_VARIATION_INDEX: data.append(struct.pack(">L", self.varIndex)) + flags |= VarTransformFlags.HAVE_VARIATIONS + else: + flags &= ~VarTransformFlags.HAVE_VARIATIONS def write_transform_component(value, values): if flags & values.flag: @@ -158,7 +160,7 @@ class VarTransform: value = getattr(self.transform, attr_name) data.append(write_transform_component(value, mapping_values)) - return bytesjoin(data) + return struct.pack(">H", flags) + bytesjoin(data) def toXML(self, writer, ttFont, attrs, name): if self.varIndex != NO_VARIATION_INDEX: From d07d960c75900aa90636363c874f07314c7f01c3 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 00:30:24 -0700 Subject: [PATCH 075/114] Fix RESERVED --- Lib/fontTools/ttLib/tables/otTables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index aacf95fe4..bdd6d0523 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -58,7 +58,7 @@ class VarTransformFlags(IntFlag): HAVE_TCENTER_X = 0x0080 HAVE_TCENTER_Y = 0x0100 HAVE_VARIATIONS = 0x0200 - RESERVED = 0x0400 | 0x0800 | 0x1000 | 0x2000 | 0x4000 + RESERVED = 0x0400 | 0x0800 | 0x1000 | 0x2000 | 0x4000 | 0x8000 VarTransformMappingValues = namedtuple( From cd1513923bb2f2cb85aca44f3b189c165f07218c Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 00:32:44 -0700 Subject: [PATCH 076/114] [VARC] Minor --- Lib/fontTools/ttLib/tables/otTables.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index bdd6d0523..891cf1fce 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -189,17 +189,17 @@ class VarTransform: def applyDeltas(self, deltas): i = 0 - def read_transform_component_delta(data, values): + def read_transform_component_delta(values): nonlocal i if self.flags & values.flag: - v = fi2fl(data[i], values.fractionalBits) * values.scale + 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(deltas, mapping_values) + value = read_transform_component_delta(mapping_values) setattr( self.transform, attr_name, getattr(self.transform, attr_name) + value ) From 263d4d9a682938250a9d19aa2bd2493c77d88a05 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 00:36:40 -0700 Subject: [PATCH 077/114] Handle scaleY --- Lib/fontTools/ttLib/tables/otTables.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 891cf1fce..cefd62269 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -204,6 +204,9 @@ class VarTransform: self.transform, attr_name, getattr(self.transform, attr_name) + value ) + if not (self.flags & VarTransformFlags.HAVE_SCALE_Y): + self.transform.scaleY = self.transform.scaleX + assert i == len(deltas) def __eq__(self, other): From d37b3942eecf7aebc05bf0f0ca2841538d61c38b Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 05:06:19 -0700 Subject: [PATCH 078/114] [VARC] Encode indices as 1,2,3,4 bytes long Previously it was 2 or 4. --- Lib/fontTools/ttLib/tables/otTables.py | 63 ++++++++++++++++---------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index cefd62269..f6009cbc5 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -220,12 +220,12 @@ class VarTransform: class VarComponentFlags(IntFlag): - GID_IS_24BIT = 0x01 - INDICES_ARE_LONG = 0x02 - HAVE_LOCATION = 0x04 - HAVE_TRANSFORM = 0x08 - RESET_UNSPECIFIED_AXES = 0x10 - USE_MY_METRICS = 0x20 + INDICES_SIZE = 0x03 # Currently has to be the first two bits; search for indicesSize + GID_IS_24BIT = 0x04 + USE_MY_METRICS = 0x8 + HAVE_LOCATION = 0x10 + HAVE_TRANSFORM = 0x20 + RESET_UNSPECIFIED_AXES = 0x40 RETAIN_FLAGS = RESET_UNSPECIFIED_AXES | USE_MY_METRICS RESERVED = 0x40 | 0x80 @@ -252,18 +252,23 @@ class VarComponent: i += 2 self.glyphName = font.glyphOrder[glyphID] - fmt = "L" if flags & VarComponentFlags.INDICES_ARE_LONG else "H" + indicesSize = (flags & VarComponentFlags.INDICES_SIZE) + 1 + 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], + }[indicesSize] + self.AxisIndicesIndex = self.AxisValuesIndex = self.TransformIndex = None if flags & VarComponentFlags.HAVE_LOCATION: - l = 8 if flags & VarComponentFlags.INDICES_ARE_LONG else 4 - self.AxisIndicesIndex, self.AxisValuesIndex = struct.unpack( - ">" + fmt * 2, data[i : i + l] - ) - i += l + self.AxisIndicesIndex = unpacker(data[i : i + indicesSize]) + i += indicesSize + self.AxisValuesIndex = unpacker(data[i : i + indicesSize]) + i += indicesSize if flags & VarComponentFlags.HAVE_TRANSFORM: - l = 4 if flags & VarComponentFlags.INDICES_ARE_LONG else 2 - self.TransformIndex = struct.unpack(">" + fmt, data[i : i + l])[0] - i += l + self.TransformIndex = unpacker(data[i : i + indicesSize]) + i += indicesSize return data[i:] @@ -279,25 +284,33 @@ class VarComponent: else: data.append(struct.pack(">H", glyphID)) + indicesSize = 1 for attr in ("AxisIndicesIndex", "AxisValuesIndex", "TransformIndex"): value = getattr(self, attr) - if value is not None and value > 65535: - flags |= VarComponentFlags.INDICES_ARE_LONG - break + if value is not None: + if value > 0xFFFFFF: + indicesSize = max(indicesSize, 4) + elif value > 0xFFFF: + indicesSize = max(indicesSize, 3) + elif value > 0xFF: + indicesSize = max(indicesSize, 2) + flags |= indicesSize - 1 - fmt = "L" if flags & VarComponentFlags.INDICES_ARE_LONG else "H" + 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), + }[indicesSize] assert (self.AxisIndicesIndex is None) == (self.AxisValuesIndex is None) if self.AxisIndicesIndex is not None: flags |= VarComponentFlags.HAVE_LOCATION - data.append( - struct.pack( - ">" + (fmt * 2), self.AxisIndicesIndex, self.AxisValuesIndex - ) - ) + data.append(packer(self.AxisIndicesIndex)) + data.append(packer(self.AxisValuesIndex)) if self.TransformIndex is not None: flags |= VarComponentFlags.HAVE_TRANSFORM - data.append(struct.pack(">" + fmt, self.TransformIndex)) + data.append(packer(self.TransformIndex)) return struct.pack("B", flags) + bytesjoin(data) From 58bb96be91bd0e83a08c38ee7e5eb5ea3ea549b8 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 05:20:32 -0700 Subject: [PATCH 079/114] [VARC] Use a DeltaSetIndexMap --- Lib/fontTools/ttLib/tables/otData.py | 6 +----- Lib/fontTools/ttLib/ttGlyphSet.py | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index d31e64769..3e1dc8f29 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3392,11 +3392,7 @@ otData = [ ( "AxisValuesList", [ - # A hack here would be to encode the VarIndex as the first member - # of the tuple to stored. But I find that ugly. The current - # design is not ideal but cleaner. - ("uint32", "VarIndicesCount", None, None, ""), - ("VarIndex", "VarIndices", "VarIndicesCount", 0, ""), + ("LOffsetTo(DeltaSetIndexMap)", "VarIndices", None, None, ""), ("TupleList", "Item", "", 0, ""), ], ), diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 1af88a7fa..0ea895b9b 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -306,8 +306,8 @@ class _TTGlyphVARC(_TTGlyph): axisValues = Vector(varc.AxisValuesList.Item[comp.AxisValuesIndex]) # Apply variations varIdx = NO_VARIATION_INDEX - if comp.AxisValuesIndex < varc.AxisValuesList.VarIndicesCount: - varIdx = varc.AxisValuesList.VarIndices[comp.AxisValuesIndex] + if comp.AxisValuesIndex < len(varc.AxisValuesList.VarIndices.mapping): + varIdx = varc.AxisValuesList.VarIndices.mapping[comp.AxisValuesIndex] if varIdx != NO_VARIATION_INDEX: axisValues = ( axisValues + instancer[varIdx] From 6af1d5c555142d38e1ec43a98f081b2f214ca476 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 05:23:11 -0700 Subject: [PATCH 080/114] Fix RESERVED --- Lib/fontTools/ttLib/tables/otTables.py | 6 ++++-- Lib/fontTools/ttLib/ttGlyphSet.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index f6009cbc5..3a929cabc 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -220,14 +220,16 @@ class VarTransform: class VarComponentFlags(IntFlag): - INDICES_SIZE = 0x03 # Currently has to be the first two bits; search for indicesSize + INDICES_SIZE = ( + 0x03 # Currently has to be the first two bits; search for indicesSize + ) GID_IS_24BIT = 0x04 USE_MY_METRICS = 0x8 HAVE_LOCATION = 0x10 HAVE_TRANSFORM = 0x20 RESET_UNSPECIFIED_AXES = 0x40 RETAIN_FLAGS = RESET_UNSPECIFIED_AXES | USE_MY_METRICS - RESERVED = 0x40 | 0x80 + RESERVED = 0x80 class VarComponent: diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 0ea895b9b..001944f9e 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -307,7 +307,9 @@ class _TTGlyphVARC(_TTGlyph): # Apply variations varIdx = NO_VARIATION_INDEX if comp.AxisValuesIndex < len(varc.AxisValuesList.VarIndices.mapping): - varIdx = varc.AxisValuesList.VarIndices.mapping[comp.AxisValuesIndex] + varIdx = varc.AxisValuesList.VarIndices.mapping[ + comp.AxisValuesIndex + ] if varIdx != NO_VARIATION_INDEX: axisValues = ( axisValues + instancer[varIdx] From bfb8490947c9582c33a7434938f60cc50693cdf7 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 06:41:08 -0700 Subject: [PATCH 081/114] [VARC] Apparently __iadd__ is optional --- Lib/fontTools/ttLib/ttGlyphSet.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 001944f9e..7fcf414ba 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -311,9 +311,7 @@ class _TTGlyphVARC(_TTGlyph): comp.AxisValuesIndex ] if varIdx != NO_VARIATION_INDEX: - axisValues = ( - axisValues + instancer[varIdx] - ) # TODO Implement __iadd__ for Vector + axisValues += instancer[varIdx] assert len(axisIndices) == len(axisValues), ( len(axisIndices), len(axisValues), From 668a40da380c30e95d15569ee3672bcc5ceb9b57 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 07:42:53 -0700 Subject: [PATCH 082/114] [VARC] Pivot on design again Just store the AxisIndices in a shared list. Don't explicitly store numAxes. --- Lib/fontTools/ttLib/tables/otConverters.py | 14 +- Lib/fontTools/ttLib/tables/otData.py | 15 - Lib/fontTools/ttLib/tables/otTables.py | 365 +++++++++------------ Lib/fontTools/ttLib/ttGlyphSet.py | 30 +- 4 files changed, 177 insertions(+), 247 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index d9c2b36be..1f8f49bea 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -21,7 +21,6 @@ from .otBase import ( from .otTables import ( lookupTypes, VarCompositeGlyph, - VarTransform, AATStateTable, AATState, AATAction, @@ -146,6 +145,7 @@ class BaseConverter(object): "AxisCount", "BaseGlyphRecordCount", "LayerRecordCount", + "AxisIndicesList", ] self.description = description @@ -1808,7 +1808,7 @@ class TupleValues: def read(self, data, font): return TupleVariation.decompileDeltas_(None, data)[0] - def write(self, writer, font, values): + def write(self, writer, font, tableDict, values, repeatIndex=None): return bytes(TupleVariation.compileDeltaValues_(values)) def xmlRead(self, attrs, content, font): @@ -1827,7 +1827,7 @@ def cff2_index_read_item(self, i): if self._itemClass is not None: obj = self._itemClass() - obj.decompile(item, self.font) + obj.decompile(item, self.font, self.reader.localState) item = obj elif self._converter is not None: item = self._converter.read(item, self.font) @@ -1883,7 +1883,7 @@ class CFF2Index(BaseConverter): if self._itemClass is not None: obj = self._itemClass() - obj.decompile(item, font) + obj.decompile(item, font, reader.localState) item = obj elif self._converter is not None: item = self._converter.read(item, font) @@ -1916,7 +1916,10 @@ class CFF2Index(BaseConverter): 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, item) for item in items] + items = [ + self._converter.write(writer, font, tableDict, item, i) + for i, item in enumerate(items) + ] offsets = [len(item) for item in items] offsets = [0] + list(accumulate(offsets)) @@ -2050,7 +2053,6 @@ converterMapping = { "STATFlags": STATFlags, "TupleList": partial(CFF2Index, itemConverterClass=TupleValues), "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph), - "VarTransformList": partial(CFF2Index, itemClass=VarTransform), # AAT "CIDGlyphMap": CIDGlyphMap, "GlyphCIDMap": GlyphCIDMap, diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 3e1dc8f29..cb7ad5de9 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3378,8 +3378,6 @@ otData = [ ("LOffset", "Coverage", None, None, ""), ("LOffset", "MultiVarStore", None, None, "(may be NULL)"), ("LOffset", "AxisIndicesList", None, None, "(may be NULL)"), - ("LOffset", "AxisValuesList", None, None, "(may be NULL)"), - ("LOffset", "TransformList", None, None, "(may be NULL)"), ("LOffset", "VarCompositeGlyphs", None, None, ""), ], ), @@ -3389,19 +3387,6 @@ otData = [ ("TupleList", "Item", "", 0, ""), ], ), - ( - "AxisValuesList", - [ - ("LOffsetTo(DeltaSetIndexMap)", "VarIndices", None, None, ""), - ("TupleList", "Item", "", 0, ""), - ], - ), - ( - "TransformList", - [ - ("VarTransformList", "VarTransform", "", None, ""), - ], - ), ( "VarCompositeGlyphs", [ diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 3a929cabc..9a9eed147 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -47,18 +47,26 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -class VarTransformFlags(IntFlag): - HAVE_TRANSLATE_X = 0x0001 - HAVE_TRANSLATE_Y = 0x0002 - HAVE_ROTATION = 0x0004 - HAVE_SCALE_X = 0x0008 - HAVE_SCALE_Y = 0x0010 - HAVE_SKEW_X = 0x0020 - HAVE_SKEW_Y = 0x0040 - HAVE_TCENTER_X = 0x0080 - HAVE_TCENTER_Y = 0x0100 - HAVE_VARIATIONS = 0x0200 - RESERVED = 0x0400 | 0x0800 | 0x1000 | 0x2000 | 0x4000 | 0x8000 +class VarComponentFlags(IntFlag): + AXIS_INDICES_INDEX_SIZE = 0x0003 # Keep it first; we rely on that. + + USE_MY_METRICS = 0x0004 + RESET_UNSPECIFIED_AXES = 0x0008 + + AXIS_VALUES_HAVE_VARIATION = 0x0010 + TRANSFORM_HAS_VARIATION = 0x0020 + + HAVE_TRANSLATE_X = 0x0040 + HAVE_TRANSLATE_Y = 0x0080 + HAVE_ROTATION = 0x0100 + HAVE_SCALE_X = 0x0200 + HAVE_SCALE_Y = 0x0400 + HAVE_SKEW_X = 0x0800 + HAVE_SKEW_Y = 0x1000 + HAVE_TCENTER_X = 0x2000 + HAVE_TCENTER_Y = 0x4000 + + GID_IS_24BIT = 0x8000 VarTransformMappingValues = namedtuple( @@ -68,38 +76,91 @@ VarTransformMappingValues = namedtuple( VAR_TRANSFORM_MAPPING = { "translateX": VarTransformMappingValues( - VarTransformFlags.HAVE_TRANSLATE_X, 0, 1, 0 + VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 ), "translateY": VarTransformMappingValues( - VarTransformFlags.HAVE_TRANSLATE_Y, 0, 1, 0 + VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 ), - "rotation": VarTransformMappingValues(VarTransformFlags.HAVE_ROTATION, 12, 180, 0), - "scaleX": VarTransformMappingValues(VarTransformFlags.HAVE_SCALE_X, 10, 1, 1), - "scaleY": VarTransformMappingValues(VarTransformFlags.HAVE_SCALE_Y, 10, 1, 1), - "skewX": VarTransformMappingValues(VarTransformFlags.HAVE_SKEW_X, 12, -180, 0), - "skewY": VarTransformMappingValues(VarTransformFlags.HAVE_SKEW_Y, 12, 180, 0), - "tCenterX": VarTransformMappingValues(VarTransformFlags.HAVE_TCENTER_X, 0, 1, 0), - "tCenterY": VarTransformMappingValues(VarTransformFlags.HAVE_TCENTER_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), +} +_unpackers = { + 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], } -class VarTransform: +class VarComponent: def __init__(self): self.flags = 0 + self.glyphName = None + self.axisIndicesIndex = None + self.axisValues = () + self.axisValuesVarIndex = NO_VARIATION_INDEX + self.transformVarIndex = NO_VARIATION_INDEX self.transform = DecomposedTransform() - self.varIndex = NO_VARIATION_INDEX - def decompile(self, data, font): + def decompile(self, data, font, localState): i = 0 - self.flags = flags = struct.unpack(">H", data[i : i + 2])[0] + self.flags = flags = _unpackers[2](data[i : i + 2]) i += 2 - if flags & VarTransformFlags.HAVE_VARIATIONS: - self.varIndex = struct.unpack(">L", data[i : i + 4])[0] - i += 4 - flags = flags & ~VarTransformFlags.HAVE_VARIATIONS + gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2 + glyphID = _unpackers[gidSize](data[i : i + gidSize]) + i += gidSize + self.glyphName = font.glyphOrder[glyphID] - def read_transform_component(data, values): + axisIndicesIndexSize = flags & VarComponentFlags.AXIS_INDICES_INDEX_SIZE + axisIndicesIndexSize = {0: 0, 1: 1, 2: 2, 3: 4}[axisIndicesIndexSize] + self.axisIndicesIndex = ( + None + if axisIndicesIndexSize == 0 + else _unpackers[axisIndicesIndexSize](data[i : i + axisIndicesIndexSize]) + ) + i += axisIndicesIndexSize + + if self.axisIndicesIndex is None: + numAxes = 0 + else: + axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex] + numAxes = len(axisIndices) + + axisValues = array.array("h", data[i : i + 2 * numAxes]) + i += 2 * numAxes + if sys.byteorder != "big": + axisValues.byteswap() + assert len(axisValues) == numAxes + self.axisValues = tuple(axisValues) + + if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + self.axisValuesVarIndex = _unpackers[4](data[i : i + 4]) + i += 4 + else: + self.axisValuesVarIndex = NO_VARIATION_INDEX + if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: + self.transformVarIndex = _unpackers[4](data[i : i + 4]) + i += 4 + else: + self.transformVarIndex = NO_VARIATION_INDEX + + self.transform = DecomposedTransform() + + def read_transform_component(values): nonlocal i if flags & values.flag: v = ( @@ -114,39 +175,63 @@ class VarTransform: return values.defaultValue for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): - value = read_transform_component(data, mapping_values) + value = read_transform_component(mapping_values) setattr(self.transform, attr_name, value) - if not (flags & VarTransformFlags.HAVE_SCALE_Y): + if not (flags & VarComponentFlags.HAVE_SCALE_Y): self.transform.scaleY = self.transform.scaleX return data[i:] def compile(self, font): data = [] - flags = 0 - if not hasattr(self, "flags"): - flags = 0 - # Calculate optimal transform component flags - for attr_name, mapping in VAR_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 - if fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10): - flags &= ~VarTransformFlags.HAVE_SCALE_Y - if self.varIndex != NO_VARIATION_INDEX: - flags |= VarTransformFlags.HAVE_VARIATIONS - else: - flags = self.flags + flags = self.flags - if self.varIndex != NO_VARIATION_INDEX: - data.append(struct.pack(">L", self.varIndex)) - flags |= VarTransformFlags.HAVE_VARIATIONS + glyphID = font.getGlyphID(self.glyphName) + if glyphID > 65535: + flags |= VarComponentFlags.GID_IS_24BIT + data.append(_packer[3](glyphID)) else: - flags &= ~VarTransformFlags.HAVE_VARIATIONS + flags &= ~VarComponentFlags.GID_IS_24BIT + data.append(_packer[2](glyphID)) + + numAxes = len(self.axisValues) + + axisIndicesIndexSize = ( + 0 + if self.axisIndicesIndex == None + else 1 + if self.axisIndicesIndex < 256 + else 2 + if self.axisIndicesIndex < 65536 + else 3 + ) + assert ( + axisIndicesIndexSize & VarComponentFlags.AXIS_INDICES_INDEX_SIZE + == axisIndicesIndexSize + ) + flags &= ~VarComponentFlags.AXIS_INDICES_INDEX_SIZE + flags |= axisIndicesIndexSize + axisIndicesIndexSize = {0: 0, 1: 1, 2: 2, 3: 4}[axisIndicesIndexSize] + if axisIndicesIndexSize: + data.append(_packer[axisIndicesIndexSize](self.axisIndicesIndex)) + + axisValues = array.array("h", self.axisValues) + if sys.byteorder != "big": + axisValues.byteswap() + data.append(axisValues.tobytes()) + + if self.axisValuesVarIndex != NO_VARIATION_INDEX: + flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + data.append(struct.pack(">L", self.axisValuesVarIndex)) + else: + flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + if self.transformVarIndex != NO_VARIATION_INDEX: + flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION + data.append(struct.pack(">L", self.transformVarIndex)) + else: + flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION def write_transform_component(value, values): if flags & values.flag: @@ -162,31 +247,31 @@ class VarTransform: return struct.pack(">H", flags) + bytesjoin(data) - def toXML(self, writer, ttFont, attrs, name): - if self.varIndex != NO_VARIATION_INDEX: - attrs.append(("varIndex", str(self.varIndex))) + def toXML(self, writer, ttFont, attrs): + attrs.append(("glyphName", self.glyphName)) + attrs.append(("flags", self.flags)) - for attr_name, mapping in VAR_TRANSFORM_MAPPING.items(): - v = getattr(self.transform, attr_name) - if v != mapping.defaultValue: - attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) + if self.axisIndicesIndex is not None: + attrs.append(("axisIndicesIndex", self.axisIndicesIndex)) - writer.simpletag("VarTransform", attrs) + # XXX TODO + + writer.simpletag("VarComponent", attrs) writer.newline() def fromXML(self, name, attrs, content, ttFont): - if "varIndex" in attrs: - self.varIndex = safeEval(attrs["varIndex"]) - else: - self.varIndex = NO_VARIATION_INDEX + self.flags = safeEval(attrs["flags"]) & VarComponentFlags.RETAIN_FLAGS + self.glyphName = attrs["glyphName"] - for attr_name, mapping in VAR_TRANSFORM_MAPPING.items(): - if attr_name not in attrs: - continue - v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits) - setattr(self.transform, attr_name, v) + # XXX TODO + assert ("AxisIndicesIndex" in attrs) == ("AxisValuesIndex" in attrs) + if "AxisIndicesIndex" in attrs: + self.AxisIndicesIndex = safeEval(attrs["AxisIndicesIndex"]) + self.AxisValuesIndex = safeEval(attrs["AxisValuesIndex"]) + if "TransformIndex" in attrs: + self.TransformIndex = safeEval(attrs["TransformIndex"]) - def applyDeltas(self, deltas): + def applyTransformDeltas(self, deltas): i = 0 def read_transform_component_delta(values): @@ -204,142 +289,10 @@ class VarTransform: self.transform, attr_name, getattr(self.transform, attr_name) + value ) - if not (self.flags & VarTransformFlags.HAVE_SCALE_Y): + if not (self.flags & VarComponentFlags.HAVE_SCALE_Y): self.transform.scaleY = self.transform.scaleX - assert 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 VarComponentFlags(IntFlag): - INDICES_SIZE = ( - 0x03 # Currently has to be the first two bits; search for indicesSize - ) - GID_IS_24BIT = 0x04 - USE_MY_METRICS = 0x8 - HAVE_LOCATION = 0x10 - HAVE_TRANSFORM = 0x20 - RESET_UNSPECIFIED_AXES = 0x40 - RETAIN_FLAGS = RESET_UNSPECIFIED_AXES | USE_MY_METRICS - RESERVED = 0x80 - - -class VarComponent: - def __init__(self): - self.flags = 0 - self.glyphName = None - self.AxisIndicesIndex = None - self.AxisValuesIndex = None - self.TransformIndex = None - - def decompile(self, data, font): - i = 0 - flags = data[i] - i += 1 - self.flags = flags & VarComponentFlags.RETAIN_FLAGS - - if flags & VarComponentFlags.GID_IS_24BIT: - glyphID = struct.unpack(">L", b"\0" + data[i : i + 3])[0] - i += 3 - else: - glyphID = struct.unpack(">H", data[i : i + 2])[0] - i += 2 - self.glyphName = font.glyphOrder[glyphID] - - indicesSize = (flags & VarComponentFlags.INDICES_SIZE) + 1 - 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], - }[indicesSize] - - self.AxisIndicesIndex = self.AxisValuesIndex = self.TransformIndex = None - if flags & VarComponentFlags.HAVE_LOCATION: - self.AxisIndicesIndex = unpacker(data[i : i + indicesSize]) - i += indicesSize - self.AxisValuesIndex = unpacker(data[i : i + indicesSize]) - i += indicesSize - if flags & VarComponentFlags.HAVE_TRANSFORM: - self.TransformIndex = unpacker(data[i : i + indicesSize]) - i += indicesSize - - return data[i:] - - def compile(self, font): - data = [] - - flags = self.flags & VarComponentFlags.RETAIN_FLAGS - - glyphID = font.getGlyphID(self.glyphName) - if glyphID > 65535: - flags |= VarComponentFlags.GID_IS_24BIT - data.append(struct.pack(">L", glyphID)[1:]) - else: - data.append(struct.pack(">H", glyphID)) - - indicesSize = 1 - for attr in ("AxisIndicesIndex", "AxisValuesIndex", "TransformIndex"): - value = getattr(self, attr) - if value is not None: - if value > 0xFFFFFF: - indicesSize = max(indicesSize, 4) - elif value > 0xFFFF: - indicesSize = max(indicesSize, 3) - elif value > 0xFF: - indicesSize = max(indicesSize, 2) - flags |= indicesSize - 1 - - 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), - }[indicesSize] - - assert (self.AxisIndicesIndex is None) == (self.AxisValuesIndex is None) - if self.AxisIndicesIndex is not None: - flags |= VarComponentFlags.HAVE_LOCATION - data.append(packer(self.AxisIndicesIndex)) - data.append(packer(self.AxisValuesIndex)) - if self.TransformIndex is not None: - flags |= VarComponentFlags.HAVE_TRANSFORM - data.append(packer(self.TransformIndex)) - - return struct.pack("B", flags) + bytesjoin(data) - - def toXML(self, writer, ttFont, attrs): - attrs.append(("glyphName", self.glyphName)) - attrs.append(("flags", self.flags & VarComponentFlags.RETAIN_FLAGS)) - - assert (self.AxisIndicesIndex is None) == (self.AxisValuesIndex is None) - if self.AxisIndicesIndex is not None: - attrs.append(("AxisIndicesIndex", self.AxisIndicesIndex)) - attrs.append(("AxisValuesIndex", self.AxisIndicesIndex)) - if self.TransformIndex is not None: - attrs.append(("TransformIndex", self.TransformIndex)) - - writer.simpletag("VarComponent", attrs) - writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - self.flags = safeEval(attrs["flags"]) & VarComponentFlags.RETAIN_FLAGS - self.glyphName = attrs["glyphName"] - - assert ("AxisIndicesIndex" in attrs) == ("AxisValuesIndex" in attrs) - if "AxisIndicesIndex" in attrs: - self.AxisIndicesIndex = safeEval(attrs["AxisIndicesIndex"]) - self.AxisValuesIndex = safeEval(attrs["AxisValuesIndex"]) - if "TransformIndex" in attrs: - self.TransformIndex = safeEval(attrs["TransformIndex"]) + assert i == len(deltas), (i, len(deltas)) def __eq__(self, other): if type(self) != type(other): @@ -356,11 +309,11 @@ class VarCompositeGlyph(BaseTable): if not hasattr(self, "components"): self.components = [] - def decompile(self, data, font): + def decompile(self, data, font, localState): self.components = [] while data: component = VarComponent() - data = component.decompile(data, font) + data = component.decompile(data, font, localState) self.components.append(component) def compile(self, font): diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 7fcf414ba..4a5af3b8e 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -300,18 +300,11 @@ class _TTGlyphVARC(_TTGlyph): for comp in glyph.components: location = {} - assert (comp.AxisIndicesIndex is None) == (comp.AxisValuesIndex is None) - if comp.AxisIndicesIndex is not None: - axisIndices = varc.AxisIndicesList.Item[comp.AxisIndicesIndex] - axisValues = Vector(varc.AxisValuesList.Item[comp.AxisValuesIndex]) - # Apply variations - varIdx = NO_VARIATION_INDEX - if comp.AxisValuesIndex < len(varc.AxisValuesList.VarIndices.mapping): - varIdx = varc.AxisValuesList.VarIndices.mapping[ - comp.AxisValuesIndex - ] - if varIdx != NO_VARIATION_INDEX: - axisValues += instancer[varIdx] + if comp.axisIndicesIndex is not None: + axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] + axisValues = Vector(comp.axisValues) + if comp.axisValuesVarIndex != NO_VARIATION_INDEX: + axisValues += instancer[comp.axisValuesVarIndex] assert len(axisIndices) == len(axisValues), ( len(axisIndices), len(axisValues), @@ -321,14 +314,11 @@ class _TTGlyphVARC(_TTGlyph): for i, v in zip(axisIndices, axisValues) } - transform = DecomposedTransform() - if comp.TransformIndex is not None: - varTransform = varc.TransformList.VarTransform[comp.TransformIndex] - varIdx = varTransform.varIndex - if varIdx != NO_VARIATION_INDEX: - deltas = instancer[varIdx] - varTransform.applyDeltas(deltas) - transform = varTransform.transform + if comp.transformVarIndex != NO_VARIATION_INDEX: + deltas = instancer[comp.transformVarIndex] + comp = copy(comp) # Shallow copy + comp.applyTransformDeltas(deltas) + transform = comp.transform reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES with self.glyphSet.glyphSet.pushLocation(location, reset): From cfc66a3a826fa0734d8e48141daab2aeb19b1a81 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 10:59:55 -0700 Subject: [PATCH 083/114] [VARC] Simplify VarCompositeRecord --- Lib/fontTools/ttLib/tables/otTables.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 9a9eed147..57eff9853 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -304,10 +304,10 @@ class VarComponent: return result if result is NotImplemented else not result -class VarCompositeGlyph(BaseTable): - def populateDefaults(self, propagator=None): - if not hasattr(self, "components"): - self.components = [] +class VarCompositeGlyph: + + def __init__(self, components=None): + self.components = components if components is not None else [] def decompile(self, data, font, localState): self.components = [] @@ -331,7 +331,6 @@ class VarCompositeGlyph(BaseTable): xmlWriter.newline() def fromXML(self, name, attrs, content, font): - self.populateDefaults() if name == "VarComponent": component = VarComponent() component.fromXML(name, attrs, content, font) From 735859f929ae20d460c6d16b49f6b67761dadc5e Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 11:32:26 -0700 Subject: [PATCH 084/114] [VARC] Implement XML read/write --- Lib/fontTools/ttLib/tables/otConverters.py | 10 ++--- Lib/fontTools/ttLib/tables/otTables.py | 50 +++++++++++++++++----- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 1f8f49bea..aee2de547 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1949,13 +1949,9 @@ class CFF2Index(BaseConverter): def xmlRead(self, attrs, content, font): if self._itemClass is not None: - lst = [] - content = [t for t in content if isinstance(t, tuple)] - for eltName, eltAttrs, eltContent in content: - obj = self._itemClass() - obj.fromXML(eltName, eltAttrs, eltContent, font) - lst.append(obj) - return lst + 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: diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 57eff9853..a4b18a281 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -253,23 +253,50 @@ class VarComponent: if self.axisIndicesIndex is not None: attrs.append(("axisIndicesIndex", self.axisIndicesIndex)) + attrs.append(("axisValues", self.axisValues)) - # XXX TODO + if self.axisValuesVarIndex != NO_VARIATION_INDEX: + attrs.append(("axisValuesVarIndex", self.axisValuesVarIndex)) + if self.transformVarIndex != NO_VARIATION_INDEX: + attrs.append(("transformVarIndex", self.transformVarIndex)) + + for attr_name, mapping in VAR_TRANSFORM_MAPPING.items(): + v = getattr(self.transform, attr_name) + if v != mapping.defaultValue: + attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) writer.simpletag("VarComponent", attrs) writer.newline() def fromXML(self, name, attrs, content, ttFont): - self.flags = safeEval(attrs["flags"]) & VarComponentFlags.RETAIN_FLAGS + self.flags = safeEval(attrs["flags"]) self.glyphName = attrs["glyphName"] - # XXX TODO - assert ("AxisIndicesIndex" in attrs) == ("AxisValuesIndex" in attrs) - if "AxisIndicesIndex" in attrs: - self.AxisIndicesIndex = safeEval(attrs["AxisIndicesIndex"]) - self.AxisValuesIndex = safeEval(attrs["AxisValuesIndex"]) - if "TransformIndex" in attrs: - self.TransformIndex = safeEval(attrs["TransformIndex"]) + assert ("axisIndicesIndex" in attrs) == ("axisValues" in attrs) + if "axisIndicesIndex" in attrs: + self.axisIndicesIndex = safeEval(attrs["axisIndicesIndex"]) + self.axisValues = safeEval(attrs["axisValues"]) + else: + self.axisIndicesIndex = None + self.axisValues = () + + if "axisValuesVarIndex" in attrs: + self.axisValuesVarIndex = safeEval(attrs["axisValuesVarIndex"]) + else: + self.axisValuesVarIndex = NO_VARIATION_INDEX + if "transformVarIndex" in attrs: + self.transformVarIndex = safeEval(attrs["transformVarIndex"]) + else: + self.transformVarIndex = NO_VARIATION_INDEX + + self.transform = DecomposedTransform() + for attr_name, mapping in VAR_TRANSFORM_MAPPING.items(): + if attr_name in attrs: + setattr( + self.transform, + attr_name, + safeEval(attrs[attr_name]), + ) def applyTransformDeltas(self, deltas): i = 0 @@ -305,7 +332,6 @@ class VarComponent: class VarCompositeGlyph: - def __init__(self, components=None): self.components = components if components is not None else [] @@ -331,7 +357,9 @@ class VarCompositeGlyph: xmlWriter.newline() def fromXML(self, name, attrs, content, font): - if name == "VarComponent": + 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) From a7ca67ada8e7c987c778824e6d8a79aeb07e5e7e Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 12:12:42 -0700 Subject: [PATCH 085/114] [VARC] Update subsetting --- Lib/fontTools/subset/__init__.py | 86 ++++++++++++++------------ Lib/fontTools/varLib/multiVarStore.py | 8 +-- Tests/ttLib/data/varc-6868.ttf | Bin 9852 -> 9880 bytes Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 3292 -> 3404 bytes 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 1e9e13148..2b7aafa66 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -2633,58 +2633,68 @@ def closure_glyphs(self, s): @_add_method(ttLib.getTableClass("VARC")) def subset_glyphs(self, s): indices = self.table.Coverage.subset(s.glyphs) - # TODO Subset MultiVarStore - self.table.VarCompositeGlyphs.glyphs = _list_subset( - self.table.VarCompositeGlyphs.glyphs, indices + self.table.VarCompositeGlyphs.VarCompositeGlyph = _list_subset( + self.table.VarCompositeGlyphs.VarCompositeGlyph, indices ) - return bool(self.table.VarCompositeGlyphs.glyphs) + return bool(self.table.VarCompositeGlyphs.VarCompositeGlyph) @_add_method(ttLib.getTableClass("VARC")) def closure_glyphs(self, s): - if self.table.VarCompositeGlyphs: - allGlyphs = { - glyphName: glyph - for glyphName, glyph in zip( - self.table.Coverage.glyphs, self.table.VarCompositeGlyphs.glyphs - ) - } + if self.table.VarCompositeGlyphs is None: + return - glyphs = s.glyphs - new = set(s.glyphs) - while new: - oldNew = new - new = set() - for glyphName in oldNew: - glyph = allGlyphs.get(glyphName) - if glyph is None: - continue - for comp in glyph.components: - name = comp.glyphName - if name not in glyphs: - glyphs.add(name) - new.add(name) + allGlyphs = { + glyphName: glyph + for glyphName, glyph in zip( + self.table.Coverage.glyphs, 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 + glyph = allGlyphs.get(glyphName) + if glyph is None: + continue + 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 - if not hasattr(table, "MultiVarStore"): - return True - 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) - usedVarIdxes = set() - - # Collect. - table.collect_varidxes(usedVarIdxes) - - # Subset. - varidx_map = store.subset_varidxes(usedVarIdxes) - - # Map. - 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] return True diff --git a/Lib/fontTools/varLib/multiVarStore.py b/Lib/fontTools/varLib/multiVarStore.py index c29899746..f24a6e6f7 100644 --- a/Lib/fontTools/varLib/multiVarStore.py +++ b/Lib/fontTools/varLib/multiVarStore.py @@ -236,16 +236,16 @@ ot.MultiVarStore.get_supports = MultiVarStore_get_supports def VARC_collect_varidxes(self, varidxes): - for glyph in self.VarCompositeGlyphs.glyphs: + for glyph in self.VarCompositeGlyphs.VarCompositeGlyph: for component in glyph.components: - varidxes.add(component.locationVarIndex) + varidxes.add(component.axisValuesVarIndex) varidxes.add(component.transformVarIndex) def VARC_remap_varidxes(self, varidxes_map): - for glyph in self.VarCompositeGlyphs.glyphs: + for glyph in self.VarCompositeGlyphs.VarCompositeGlyph: for component in glyph.components: - component.locationVarIndex = varidxes_map[component.locationVarIndex] + component.axisValuesVarIndex = varidxes_map[component.axisValuesVarIndex] component.transformVarIndex = varidxes_map[component.transformVarIndex] diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index 71d94e04244b1661728d69e8139c10978cea12df..62e071c17b3326812097f84b861f28b1018d30da 100644 GIT binary patch delta 1652 zcmah}Yitx%6h8OfdF;&Y&ThNgZMRQKDP_@C%cg)u?H&4{7&M3x5eYGx6k>?jRAY#m z?xRavD7JMBl9rc*3ZiL{{y>4SL<0W6MoclLN{s~qX(bvTH0`!K>zP6z{&Hr{ z^ZM>%X4RgjbAbSW146)nuhrL7PybT41wefimA3BOWn}3W>#`JKQQ-I-<4xtf(~IbQ z0A&@*&jwfX6b(RN0Mh41gL>6iWq&n8ItS+vhLMH)BfD|jULv^xIB;Xy{%#=qzA0~3 zX~8itnypdBeXGala0IlANi9$JHRt@`uN0HJbuzRrd5z$<5#VEVdxMiU9Fsa z6J@S~+Mf7f;it;+4Qd!xFNcdHn(S%UVK)qfI5D1dl}T+OzF?xm2)q2YUA(k09*_4L zdtCwh5xr`eb11~;?L1;!biI(*79XGswm@wvXvR}%$p(B+fEbH+a&oENSn1BE@d0DA zySQpcD?Hx9q2nmMP*lUAr=7XYzCaXu_W{qDleZ3Kz1X#79QEW#ep1z@u%YJ_3(9Ql-7p%P7}i`(?XL0frf`Rzeyn*8BF z4Y@QE5C<|&h}ua*PxFu?#`9@j!XPv-B7-u~!h%OXWf=TnpW=5-W&_q#~ISL1bQK28EK#$w3%!x*iR&fUnxPffI$IhYOg#7GM$4%{0dU1Y`}WpiA_@=+LRtK>}+(5K$G*3 zg`LeqQy;>|0+4rh9abz>S(e08EJTcWed;@pw+Qk zvjed3*t7NE^@u`JOK78TfCX8QHp6`E)&|C;cWyJ<7~4ue5wLbEtthMPmNaYy5QnTj z?*$PMD8f@D=(vHI8_5t9BV1CoVpdZWTijZ4-FH;N_Br~X3-)3zrDeapO*`2j8w6EG zuw5G_+j3~d>LB%v2{u~F;Y9%XJ49n&z{m|+{ZBi6hxJSM=)fm~myTVbfAc%cf$n)wU=1#4QGqW}N^ delta 1625 zcmZvceQZ-z6u{5D_kFalAFtikZe^Wg3^oQC>ugXQQ|{Xr3n=1DfEe@-5|J1KE`=DP z$zHb(tOH~|7_%UoFvw&|5JUxWT_ivvE`dxUkS%e#Kp2Y{)H1sE)%!YwF)=sy<=pem zIrp4j?m3;$C7$&H0st$hK!C1N8mccmza;@6`2Z~A?cT-6`}vw2GuO5Bg$ix6`Fxp! zSZu5?WcecHKuS_+O#NycD0v+?@Jdp6~h~wS4MTTNyrK z+2%RsoZ3a>d8ix|or~X6m1H0;29?Bq{f0{a3Th9m#SP2juO!Y*U4nJGi)))%?%9Br z;;Z7$*p2NiIhpPCY*utr=f;3U0Dk10Dt~mqE&d#~>eOLIKQzLYNJ67&?KGf^9H@ zcA*}053l%;X6Ht~)V{DUL_uxbUY#3_Hq+Rh&OH?E{+7F0F}_v`!pbVRh+2}`YI%X{ zC`FgL6zG6cDn*fIBInme!djc`!O_For*a88t^F*oULK}%W9_uGF&gbqmMpdnsdRQ* zOSA>geFJJ!em$CUCfBod68O`-RSD0-Vm(Wx@luseT~d_+(9fu}WNRy>5dBQs?3h!~ zOVQS)`+lEw{y;$wQfi%?jTx_V5Jp>3 z9I@LP!ek)JJ=#q1F}Ba`=!{StYo@9C?V6t9^`h@sf3$YDP}Htc{k3guhZty;axt7?sla6%bUxaZIA$~wj zfPBLO;B*L)#ny2i+CcDM<9y1dN%$0Ztt-MBgL=5zhpipE_#-5`m|Il~9=wfp5aRAX#j7bA%7t z8m%J`upMF^Y)qy0z{eb*l?fB@m_exD4IK{laSk-mh*Fyc&-O?G5txwS#pjF-_EuKqKO-zLYI3Goe2Gh{M{ zuHXu&M}{l!yZ(=U87?L3p`Pgk-!RA8j8&a3(fDv6qARRT>v4q1CV*3m+@%h)(Y=I? KMyzctn)f&R&#bcm diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index cfe10a044f2552e06cac8c125266d6f11c43e369..c011395a87da1e5e3b3a55a5ca31a654a4d847a8 100644 GIT binary patch delta 653 zcmZ8dO=uHA6#iy*c6N5NF-Z-?Xbg!z&_h~+dJ&3Dw3i}k6$})MRPZDo^dO$3)3k^7 z7`0A<;T{c?=?o zIroI1@FfH_0|r$*JiD-R-xZEU;ZrzJq6NjXtGnYLxD17!q0N30fblPUN67Z>@`r=I zYtZLZd^|*xpa`GZUuTlGq`!w-qZ3YnZx6haiNg6aVjplM_f`cO2Q-jz4J1>N46wI_ zk{9_B@58d7ZfoIwdq?D+1dU0W{qGD*CcsTv8002UB!5ZkNn>vW$Lx8PwL?`R3u*7E z(>X3NP0WT#$I*zYHx1to)EPUFB~?v<9S8g?T4BXr3v*1{-Jqfl8E7YW99FiA>UdP2 z$dEE>DI;~6(08nJ8HbtFCHl)rC9f|;Q#|fsANdv%Gjgxx-E9K8RY0%mQ`>$F>KVEn Tca=_7sBz!e55ZBp5x4&UApC{$ delta 617 zcmXw0JBU+36ut9)GcS2rUe-@c_U*&{2q_|LTrmm?6V}Q~ETV#}h*o0ZHX?o}PZ1Xj zS1#I#Vzs4Dxso1knd#E4*HWg!F=NjwvD9``fn+%t3Eoo=O9PDl_@M21|_t>%fP z*S8)&CGw9G3H>P9AB_&Y$cTb9*s+Z>3!b;U*zEI~_b!Yftiaa!f`9sUZep^O)($rR z92QS9ttrbWZf6v=jTm&Zc5}EHtl!nck7jAO=^YhCv3g$*-x#e{jSg~maE=s|E6+7f zt~|hoq;;-!zIC2;0VzSsEo_Z`P~?(wT)%75pO(5rH2NJUK823!1?tmEzvn!(1eQM< z&;p_pCobh6df)oCcgzHIakvkeYZ_Qad`4lNcUZXrbiDf?LZS&7M z4d)8KjAr<{+R2x_0!OmO*Hj64o^Plfh#{YfrfusTeyb9PG;hdMVXyyVn7pKWWU;*A zx}1-< Date: Wed, 20 Dec 2023 12:35:31 -0700 Subject: [PATCH 086/114] [VARC] Fix scaleUpem Instancing left. --- Lib/fontTools/ttLib/scaleUpem.py | 12 +- Lib/fontTools/varLib/instancer/__init__.py | 30 ++++- Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 124 +++++++------------- 3 files changed, 72 insertions(+), 94 deletions(-) diff --git a/Lib/fontTools/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py index a3754d8e8..68709825b 100644 --- a/Lib/fontTools/ttLib/scaleUpem.py +++ b/Lib/fontTools/ttLib/scaleUpem.py @@ -155,7 +155,7 @@ def visit(visitor, obj, attr, varc): store = varc.MultiVarStore storeBuilder = OnlineMultiVarStoreBuilder(fvarAxes) - for g in varc.VarCompositeGlyphs.glyphs: + for g in varc.VarCompositeGlyphs.VarCompositeGlyph: for component in g.components: t = component.transform t.translateX = visitor.scale(t.translateX) @@ -163,8 +163,8 @@ def visit(visitor, obj, attr, varc): t.tCenterX = visitor.scale(t.tCenterX) t.tCenterY = visitor.scale(t.tCenterY) - if component.flags & otTables.VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - varIdx = component.locationVarIndex + 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: @@ -177,11 +177,11 @@ def visit(visitor, obj, attr, varc): m = len(vec) // varData.VarRegionCount vec = list(batched(vec, m)) vec = [Vector(v) for v in vec] - component.locationVarIndex = storeBuilder.storeDeltas(vec) + component.axisValuesVarIndex = storeBuilder.storeDeltas(vec) else: - component.transformVarIndex = otTables.NO_VARIATION_INDEX + component.axisValuesVarIndex = otTables.NO_VARIATION_INDEX - if component.flags & otTables.VarComponentFlags.TRANSFORM_HAS_VARIATION: + if component.transformVarIndex != otTables.NO_VARIATION_INDEX: varIdx = component.transformVarIndex if varIdx != otTables.NO_VARIATION_INDEX: major = varIdx >> 16 diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index b3e27bd2c..c692433ed 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -474,15 +474,32 @@ def instantiateVARC(varfont, axisLimits): # I don't think it currently works. varc = varfont["VARC"].table - if varc.VarCompositeGlyphs: - for glyph in varc.VarCompositeGlyphs.glyphs: - for component in glyph.components: + fvarAxes = varfont["fvar"].axes if "fvar" in varfont else [] + if varc.VarCompositeGlyphs is not None: + for glyph in varc.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.axisIndicesIndex is None: + continue + + axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] + axisValues = comp.axisValues + + comp.axisValues = [] + for axisIndex, axisValue in zip(axisIndices, axisValues): + tag = fvarAxes[axisIndex].axisTag + if tag in axisLimits: + axisValue = axisLimits[tag].renormalizeValue( + axisValue, extrapolate=False + ) + comp.axisValues.append(axisValue) + + """ newLocation = {} for tag, loc in component.location.items(): if tag not in axisLimits: newLocation[tag] = loc continue - if component.flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + if comp.flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: raise NotImplementedError( "Instancing accross VarComponent axes with variation is not supported." ) @@ -490,9 +507,10 @@ def instantiateVARC(varfont, axisLimits): loc = limits.renormalizeValue(loc, extrapolate=False) newLocation[tag] = loc component.location = newLocation + """ - if varc.MultiVarStore: - store = varc.MultiVarStore + store = varc.MultiVarStore + if store: store.prune_regions() # Needed? fvar = varfont["fvar"] diff --git a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx index da7980037..a4931bcdf 100644 --- a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx +++ b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx @@ -288,84 +288,44 @@ - - + + - + + + + + + - - - - - - - - + + - - - - - - - - - - - - + + + - - - - - + - - - - - - - + - - - - - - - - - + - - - - - - - + - - - - - - - + @@ -563,7 +523,7 @@ - + @@ -593,7 +553,7 @@ - + @@ -608,7 +568,7 @@ - + @@ -640,7 +600,7 @@ - + @@ -680,7 +640,7 @@ - + @@ -703,7 +663,7 @@ - + @@ -749,7 +709,7 @@ - + @@ -795,7 +755,7 @@ - + @@ -819,7 +779,7 @@ - + @@ -843,7 +803,7 @@ - + @@ -890,8 +850,8 @@ - - + + @@ -915,7 +875,7 @@ - + @@ -939,7 +899,7 @@ - + @@ -963,8 +923,8 @@ - - + + @@ -989,7 +949,7 @@ - + @@ -1014,7 +974,7 @@ - + @@ -1038,8 +998,8 @@ - - + + @@ -1064,8 +1024,8 @@ - - + + @@ -1135,7 +1095,7 @@ - + @@ -1150,7 +1110,7 @@ - + From e88e47f8ffb783c2e9b17b8cbe8939f87dbfb8d7 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 13:00:15 -0700 Subject: [PATCH 087/114] [VARC] Take a stab at instancing --- Lib/fontTools/varLib/instancer/__init__.py | 69 +++++----------------- Tests/varLib/instancer/instancer_test.py | 18 ++++-- 2 files changed, 27 insertions(+), 60 deletions(-) diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index c692433ed..92e9c4737 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -475,70 +475,31 @@ def instantiateVARC(varfont, axisLimits): varc = varfont["VARC"].table fvarAxes = varfont["fvar"].axes if "fvar" in varfont else [] - if varc.VarCompositeGlyphs is not None: - for glyph in varc.VarCompositeGlyphs.VarCompositeGlyph: - for comp in glyph.components: - if comp.axisIndicesIndex is None: - continue - axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] - axisValues = comp.axisValues + 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)} - comp.axisValues = [] - for axisIndex, axisValue in zip(axisIndices, axisValues): - tag = fvarAxes[axisIndex].axisTag - if tag in axisLimits: - axisValue = axisLimits[tag].renormalizeValue( - axisValue, extrapolate=False - ) - comp.axisValues.append(axisValue) - - """ - newLocation = {} - for tag, loc in component.location.items(): - if tag not in axisLimits: - newLocation[tag] = loc - continue - if comp.flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - raise NotImplementedError( - "Instancing accross VarComponent axes with variation is not supported." - ) - limits = axisLimits[tag] - loc = limits.renormalizeValue(loc, extrapolate=False) - newLocation[tag] = loc - component.location = newLocation - """ + 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: - store.prune_regions() # Needed? - - fvar = varfont["fvar"] - location = axisLimits.pinnedLocation() for region in store.SparseVarRegionList.Region: newRegionAxis = [] for regionRecord in region.SparseVarRegionAxis: - tag = fvar.axes[regionRecord.AxisIndex].axisTag - if tag in location: - continue + tag = fvarAxes[regionRecord.AxisIndex].axisTag if tag in axisLimits: - limits = axisLimits[tag] - triple = ( - regionRecord.StartCoord, - regionRecord.PeakCoord, - regionRecord.EndCoord, + raise NotImplementedError( + "Instancing across VarComponent axes is not supported." ) - triple = tuple( - limits.renormalizeValue(v, extrapolate=False) for v in triple - ) - ( - regionRecord.StartCoord, - regionRecord.PeakCoord, - regionRecord.EndCoord, - ) = triple - newRegionAxis.append(regionRecord) - region.VarRegionAxis = newRegionAxis - region.VarRegionAxisCount = len(newRegionAxis) + regionRecord.AxisIndex = reverseAxisMap[regionRecord.AxisIndex] def instantiateTupleVariationStore( diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py index 809d6bd35..b9a45058b 100644 --- a/Tests/varLib/instancer/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -1699,22 +1699,28 @@ 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} with pytest.raises( NotImplementedError, - match="Instancing accross VarComponent axes with variation is not supported.", + match="is not supported.", ): instance = instancer.instantiateVariableFont( varfont, From bc829855810412e2d6306137b020a9e95f8baec6 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 16:36:21 -0700 Subject: [PATCH 088/114] [VARC] Speed up subsetting Don't decode the whole table. --- Lib/fontTools/subset/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 2b7aafa66..ed296ed65 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -2644,12 +2644,8 @@ def closure_glyphs(self, s): if self.table.VarCompositeGlyphs is None: return - allGlyphs = { - glyphName: glyph - for glyphName, glyph in zip( - self.table.Coverage.glyphs, self.table.VarCompositeGlyphs.VarCompositeGlyph - ) - } + glyphMap = {glyphName: i for i, glyphName in enumerate(self.table.Coverage.glyphs)} + glyphRecords = self.table.VarCompositeGlyphs.VarCompositeGlyph glyphs = s.glyphs covered = set() @@ -2660,9 +2656,10 @@ def closure_glyphs(self, s): for glyphName in oldNew: if glyphName in covered: continue - glyph = allGlyphs.get(glyphName) - if glyph is None: + idx = glyphMap.get(glyphName) + if idx is None: continue + glyph = glyphRecords[idx] for comp in glyph.components: name = comp.glyphName glyphs.add(name) From c3dfe10cce493b6f39fd5956d480681090830b9a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 17:51:31 -0700 Subject: [PATCH 089/114] [VARC] Use TupleVariations tuple encoding for axisValues Free improvement. --- Lib/fontTools/ttLib/tables/otTables.py | 11 +++-------- Tests/ttLib/data/varc-6868.ttf | Bin 9880 -> 9864 bytes Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 3404 -> 3404 bytes 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index a4b18a281..3406415d0 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -11,6 +11,7 @@ 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 @@ -140,10 +141,7 @@ class VarComponent: axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex] numAxes = len(axisIndices) - axisValues = array.array("h", data[i : i + 2 * numAxes]) - i += 2 * numAxes - if sys.byteorder != "big": - axisValues.byteswap() + axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i) assert len(axisValues) == numAxes self.axisValues = tuple(axisValues) @@ -217,10 +215,7 @@ class VarComponent: if axisIndicesIndexSize: data.append(_packer[axisIndicesIndexSize](self.axisIndicesIndex)) - axisValues = array.array("h", self.axisValues) - if sys.byteorder != "big": - axisValues.byteswap() - data.append(axisValues.tobytes()) + data.append(TupleVariation.compileDeltaValues_(self.axisValues)) if self.axisValuesVarIndex != NO_VARIATION_INDEX: flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index 62e071c17b3326812097f84b861f28b1018d30da..5e02bb1466860fbcdda27df91095d397e0d0a8e0 100644 GIT binary patch delta 436 zcmXAkPiqrl5Ql#=@9dl1H3=$7tfmD)*5(q>E~&O6z9BKLN<};rkDj783F}2XbkkB= z5d6b>kX~$i5HAL$AHgqB>8Xc;L7^1Ii=afX^^G2$%gk?{nRz+0JTnRckVOIoGh4sa zc=qYa6Tm+M9QyTq^a54h-yZSA4c`9Qc;K!1N4`?bIX!^VCcl&*zMck!Y9(pPK9A8o z#h1#~$QHHV$Ex(DhXEv0_dlv;_iFm65Rge%3&p9+-ip)Bb+S8%bL+{!5e6T;z?y)1 zDx@D6zdL~A8RtXNbkE9;JVs34lJt9_JZCEC*7Gg3J8x{sxN+A$01F+QL#?xw7?~QX z#p2)^h1oCER-F^Hg}|n@-^vH7d1`TTsrI?_MoVJScg3+{l;5U@Wv<-}xwI!PR}E~Z wUpzi-PQuEBHhCHm`Fal-{$I0tuBtX7d$T7CHl-~Y+O!`;yn>DMr8|fJ0(i-1Pyhe` delta 423 zcmYLFyDvjg82_E`ocmOb$Zd(%BawOx71~rh7E+NL2@we~nrg8~B_ak4QetT28l+o0 znHu6xFbiVpPz#B8BnB3d;JZ3F)0f}#`_A6<@pKddKmgm27+V;hOMl%>t^jsFgXUp$ zR1BRhHd`vud)jfnW)APvY3cE^5S)~ce*=8N z%=)tz4MoDq-6lIRlPYzW8}q`gNtkj&ty_jIAN7i_*KPqd{6|VCK2pP!NCqB6My`rm z#JF9zgE2YLxjUFk-G@(f1&pzWExhCR5cR#RQ(j)mOV|dB>w*=r5uwyjbw)~tfsuiMfrp`iftkS}%rVH>LO=>A!WzK9!05G6rhtVd=-$Dk%@bI98SCW? zJQFH_s@Z{<8Hibdm<5R07#tZmm>hU7{{PGn$iTtiaHgTbLGA}ohT;D&hK~%Kj81n8 znfmYDX#|RZG%@}E#$W)H;9_t*7xq%O0Vu=<#LNs?|BOKD8JynNG8^B!0~7<9&j?g5 M!N9P24%-Gc0PdD6JOBUy delta 180 zcmX>jbw)~tfsuiMfrp`iftkS}%rVG$>Xj8P3=FIR3=E8(8)XVuSf+@R5O&@ophg z|GhgP0U!WrV*3A$!2l@5#c(d{r7l>Q4M;LGWc@P&>0o$Y%WMqR0WzKus9FL@Z=S=p GfeiqAwkkaU From 6a09096800eeb53c8b99763be55cf5c9ce73ce38 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Dec 2023 21:21:51 -0700 Subject: [PATCH 090/114] [VARC] Use variable-length encoding for VarIdx'es --- Lib/fontTools/ttLib/tables/otTables.py | 58 ++++++++++++++++++++----- Tests/ttLib/data/varc-6868.ttf | Bin 9864 -> 9836 bytes Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 3404 -> 3384 bytes 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 3406415d0..d354f6ff6 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -98,7 +98,7 @@ _packer = { 3: lambda v: struct.pack(">L", v)[1:], 4: lambda v: struct.pack(">L", v), } -_unpackers = { +_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], @@ -106,6 +106,46 @@ _unpackers = { } +def _read_var_length_number(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_var_length_number(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.flags = 0 @@ -118,11 +158,11 @@ class VarComponent: def decompile(self, data, font, localState): i = 0 - self.flags = flags = _unpackers[2](data[i : i + 2]) + self.flags = flags = _unpacker[2](data[i : i + 2]) i += 2 gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2 - glyphID = _unpackers[gidSize](data[i : i + gidSize]) + glyphID = _unpacker[gidSize](data[i : i + gidSize]) i += gidSize self.glyphName = font.glyphOrder[glyphID] @@ -131,7 +171,7 @@ class VarComponent: self.axisIndicesIndex = ( None if axisIndicesIndexSize == 0 - else _unpackers[axisIndicesIndexSize](data[i : i + axisIndicesIndexSize]) + else _unpacker[axisIndicesIndexSize](data[i : i + axisIndicesIndexSize]) ) i += axisIndicesIndexSize @@ -146,13 +186,11 @@ class VarComponent: self.axisValues = tuple(axisValues) if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - self.axisValuesVarIndex = _unpackers[4](data[i : i + 4]) - i += 4 + self.axisValuesVarIndex, i = _read_var_length_number(data, i) else: self.axisValuesVarIndex = NO_VARIATION_INDEX if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: - self.transformVarIndex = _unpackers[4](data[i : i + 4]) - i += 4 + self.transformVarIndex, i = _read_var_length_number(data, i) else: self.transformVarIndex = NO_VARIATION_INDEX @@ -219,12 +257,12 @@ class VarComponent: if self.axisValuesVarIndex != NO_VARIATION_INDEX: flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION - data.append(struct.pack(">L", self.axisValuesVarIndex)) + data.append(_write_var_length_number(self.axisValuesVarIndex)) else: flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION if self.transformVarIndex != NO_VARIATION_INDEX: flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION - data.append(struct.pack(">L", self.transformVarIndex)) + data.append(_write_var_length_number(self.transformVarIndex)) else: flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index 5e02bb1466860fbcdda27df91095d397e0d0a8e0..b2c49f9789abbe8b1471ace718d17152d62543c0 100644 GIT binary patch delta 260 zcmeD1ed8m;z{tSBz{AkMz|7zf<{0F>OyX${0|S=<0|WP+i89uVJR2hgA2<)CRxS|W+r>-gA7au zC#Q;N2_9r%VPIhTKZpG#13Qz00>h!nOGV^a4lytuntVz`706&dH2JHDI?G`OhQpJU zMD>6Sro)rNL=}Mymcx^4MKxHCFfbgMyjoPp>j(qW5e8<)Crr*?I+?1O*Res(Ji@@j wc!9}^!GnT8z)Qi5eBv+4D5^-7&sU_m}YI(5K9mM0DYZCC;$Ke delta 288 zcmaFk)8Q+_z{tSBz{AkMz|7zf<{0EW;emS}0|S=<0|WQ>i89uV8XF@dI9WJU*Z$pH z!a1Ldy^-NHV=)u&WGN99b+BGWAbE`874s}+W450ROpFaKEdQ8f88euf?4^NHObm>Z z6GgPdfJ_#kO$-c7|L3s3WMF4Pl1E~Qr_lu~4n9M-t3lViTAcKK{d9tvm z9*7APpX@5C2x77TnOUM5Y+&K}$OAr76gS0Zp diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index 4597c2e612e4e8310daeae44f0bf718213a44d87..8c10394f71625250ba21b8e3c8416ec4cf7379c7 100644 GIT binary patch delta 124 zcmX>jwL?mVfsuiMfrp`iftkS}%rVH>@9^#n1_ssu1_nmci89uV9UCJ|SXflc>-KJ* zz|zOaqU#WtJXwiN+w%V}hK~%Kj81n8nfmYDX=G*k|Bb-_D8$9!crNUvZUY-LL)Jed akQxT3_qEK%_wKMT0+mQGY@Wroi46dKz$XC! delta 144 zcmdlXbw)~tfsuiMfrp`iftkS}%rVH>LO=>A!WzK9!00tm#+q@?#t0J@mY{nFlQvIa z>0@Lu@Jy(fti-163)J@i7sE#ePDZD@g-reT?lb~LSb>=7|2GB$pad6# Date: Thu, 21 Dec 2023 00:41:27 -0700 Subject: [PATCH 091/114] [VARC] Use var-int encoding for AxisIndicesIndex Slightly larger fonts, but more consistency, and freed a flag bit. --- Lib/fontTools/ttLib/tables/otTables.py | 60 +++++++------------- Tests/ttLib/data/varc-6868.ttf | Bin 9836 -> 9836 bytes Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 12 ++-- Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 3384 -> 3384 bytes 4 files changed, 28 insertions(+), 44 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index d354f6ff6..1a5fb39c7 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -49,10 +49,12 @@ log = logging.getLogger(__name__) class VarComponentFlags(IntFlag): - AXIS_INDICES_INDEX_SIZE = 0x0003 # Keep it first; we rely on that. + USE_MY_METRICS = 0x0001 + RESET_UNSPECIFIED_AXES = 0x0002 - USE_MY_METRICS = 0x0004 - RESET_UNSPECIFIED_AXES = 0x0008 + GID_IS_24BIT = 0x0004 + + HAVE_AXES = 0x0008 AXIS_VALUES_HAVE_VARIATION = 0x0010 TRANSFORM_HAS_VARIATION = 0x0020 @@ -67,7 +69,7 @@ class VarComponentFlags(IntFlag): HAVE_TCENTER_X = 0x2000 HAVE_TCENTER_Y = 0x4000 - GID_IS_24BIT = 0x8000 + RESERVED = 0x8000 VarTransformMappingValues = namedtuple( @@ -106,7 +108,7 @@ _unpacker = { } -def _read_var_length_number(data, i): +def _readVarInt32(data, i): """Read a variable-length number from data starting at index i. Return the number and the next index. @@ -129,7 +131,7 @@ def _read_var_length_number(data, i): ] << 8 | data[i + 4], i + 5 -def _write_var_length_number(v): +def _writeVarInt32(v): """Write a variable-length number. Return the data. @@ -166,14 +168,10 @@ class VarComponent: i += gidSize self.glyphName = font.glyphOrder[glyphID] - axisIndicesIndexSize = flags & VarComponentFlags.AXIS_INDICES_INDEX_SIZE - axisIndicesIndexSize = {0: 0, 1: 1, 2: 2, 3: 4}[axisIndicesIndexSize] - self.axisIndicesIndex = ( - None - if axisIndicesIndexSize == 0 - else _unpacker[axisIndicesIndexSize](data[i : i + axisIndicesIndexSize]) - ) - i += axisIndicesIndexSize + if flags & VarComponentFlags.HAVE_AXES: + self.axisIndicesIndex, i = _readVarInt32(data, i) + else: + self.axisIndicesIndex = None if self.axisIndicesIndex is None: numAxes = 0 @@ -186,11 +184,11 @@ class VarComponent: self.axisValues = tuple(axisValues) if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - self.axisValuesVarIndex, i = _read_var_length_number(data, i) + self.axisValuesVarIndex, i = _readVarInt32(data, i) else: self.axisValuesVarIndex = NO_VARIATION_INDEX if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: - self.transformVarIndex, i = _read_var_length_number(data, i) + self.transformVarIndex, i = _readVarInt32(data, i) else: self.transformVarIndex = NO_VARIATION_INDEX @@ -234,35 +232,21 @@ class VarComponent: numAxes = len(self.axisValues) - axisIndicesIndexSize = ( - 0 - if self.axisIndicesIndex == None - else 1 - if self.axisIndicesIndex < 256 - else 2 - if self.axisIndicesIndex < 65536 - else 3 - ) - assert ( - axisIndicesIndexSize & VarComponentFlags.AXIS_INDICES_INDEX_SIZE - == axisIndicesIndexSize - ) - flags &= ~VarComponentFlags.AXIS_INDICES_INDEX_SIZE - flags |= axisIndicesIndexSize - axisIndicesIndexSize = {0: 0, 1: 1, 2: 2, 3: 4}[axisIndicesIndexSize] - if axisIndicesIndexSize: - data.append(_packer[axisIndicesIndexSize](self.axisIndicesIndex)) - - data.append(TupleVariation.compileDeltaValues_(self.axisValues)) + if numAxes: + flags |= VarComponentFlags.HAVE_AXES + data.append(_writeVarInt32(self.axisIndicesIndex)) + data.append(TupleVariation.compileDeltaValues_(self.axisValues)) + else: + flags &= ~VarComponentFlags.HAVE_AXES if self.axisValuesVarIndex != NO_VARIATION_INDEX: flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION - data.append(_write_var_length_number(self.axisValuesVarIndex)) + data.append(_writeVarInt32(self.axisValuesVarIndex)) else: flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION if self.transformVarIndex != NO_VARIATION_INDEX: flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION - data.append(_write_var_length_number(self.transformVarIndex)) + data.append(_writeVarInt32(self.transformVarIndex)) else: flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index b2c49f9789abbe8b1471ace718d17152d62543c0..4006191880ee013a7c2d4d835c1b2c8e6065f467 100644 GIT binary patch delta 252 zcmaFk^TtPnfsuiMfrp`iftkS}%rVG$9^0YJjUq9eEN^e@sM}n^$;YM2#3`w+V{Y#e z6qi@az{JYL%)-jX&Ih45IJvlac=`ARCNCG3oBTvreezLZc@dzIEKES6lHnd>0^=vP zUkuD#ljTM97=KOn6Om*2#lXTnxj;lk@)ZL+gM$LYAqEB}`Tx5(7?@`<*rxwtU}c-U zRzw-7jFo-z6%kdSu3sWrKrRQMD%h42}eCNCG3oBT#te)3UabrGPEEKES6lHnd>0^=vP zpA5`wljTM97=KRo6Om*2$-u%sxj;lk@+AX1lY;`oAqEB}`Tx5(7?@`<*rxwvVC9&+ zRzw-7jFofp6%kdSu3sWrKrRQvWDQXbAU9AH#AV}}Tra8tVsKAhEvlgoHSGuk3*!YQ cD+UinE;hIjD}xu4E^``#2iwohzeI}!0JGRVp#T5? diff --git a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx index a4931bcdf..a4d572ed4 100644 --- a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx +++ b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx @@ -1,5 +1,5 @@ - + @@ -313,19 +313,19 @@ - + - + - + - + - + diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index 8c10394f71625250ba21b8e3c8416ec4cf7379c7..2cccc232f48e6a4b65814b0a33714598af25dbcc 100644 GIT binary patch delta 107 zcmV-x0F?i@8n_x10096101N`_y7q3Lfvr!kKWvY1p@!}05Aaf015y>&Q|FyfCd8q NYW^|+__K8e!v^VfBt-xK delta 107 zcmV-x0F?i@8n_x10096101N`@c;<{Lfvr!kKWvY1p@!}05AaY015y>&Q|FyfCd8q NYW^|+@w0UX!v^g;Bt-xK From b1142b60cb348067b369308b2a82906ebe769418 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 25 Dec 2023 13:04:12 -0700 Subject: [PATCH 092/114] [VARC] Adjust to latest flags change --- Lib/fontTools/ttLib/tables/otTables.py | 39 ++++++++++---------- Tests/ttLib/data/varc-6868.ttf | Bin 9836 -> 9836 bytes Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 10 ++--- Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 3384 -> 3372 bytes 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 1a5fb39c7..d385a7510 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -49,27 +49,28 @@ log = logging.getLogger(__name__) class VarComponentFlags(IntFlag): - USE_MY_METRICS = 0x0001 - RESET_UNSPECIFIED_AXES = 0x0002 + RESET_UNSPECIFIED_AXES = 0x0001 - GID_IS_24BIT = 0x0004 + HAVE_AXES = 0x0002 - HAVE_AXES = 0x0008 + AXIS_VALUES_HAVE_VARIATION = 0x0004 + TRANSFORM_HAS_VARIATION = 0x0008 - AXIS_VALUES_HAVE_VARIATION = 0x0010 - TRANSFORM_HAS_VARIATION = 0x0020 + HAVE_TRANSLATE_X = 0x0010 + HAVE_TRANSLATE_Y = 0x0020 + HAVE_ROTATION = 0x0040 - HAVE_TRANSLATE_X = 0x0040 - HAVE_TRANSLATE_Y = 0x0080 - HAVE_ROTATION = 0x0100 - HAVE_SCALE_X = 0x0200 - HAVE_SCALE_Y = 0x0400 - HAVE_SKEW_X = 0x0800 - HAVE_SKEW_Y = 0x1000 - HAVE_TCENTER_X = 0x2000 - HAVE_TCENTER_Y = 0x4000 + USE_MY_METRICS = 0x0080 - RESERVED = 0x8000 + HAVE_SCALE_X = 0x0100 + HAVE_SCALE_Y = 0x0200 + HAVE_TCENTER_X = 0x0400 + HAVE_TCENTER_Y = 0x0800 + + GID_IS_24BIT = 0x4000 + + HAVE_SKEW_X = 0x1000 + HAVE_SKEW_Y = 0x2000 VarTransformMappingValues = namedtuple( @@ -160,8 +161,8 @@ class VarComponent: def decompile(self, data, font, localState): i = 0 - self.flags = flags = _unpacker[2](data[i : i + 2]) - i += 2 + self.flags, i = _readVarInt32(data, i) + flags = self.flags gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2 glyphID = _unpacker[gidSize](data[i : i + gidSize]) @@ -262,7 +263,7 @@ class VarComponent: value = getattr(self.transform, attr_name) data.append(write_transform_component(value, mapping_values)) - return struct.pack(">H", flags) + bytesjoin(data) + return _writeVarInt32(flags) + bytesjoin(data) def toXML(self, writer, ttFont, attrs): attrs.append(("glyphName", self.glyphName)) diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index 4006191880ee013a7c2d4d835c1b2c8e6065f467..47f85e9b7ba7760f8ffd8046b312bf13e83aa18c 100644 GIT binary patch delta 209 zcmaFk^TtPpfsuiMfrp`iftkS}%rVG$uFUe?3=CWb3=G`2H_C)?vb7r_LFl()Wn;s8Q2*d6c`RMFfhsg-^IbeJd44$zkc#6 z5mhE2bx}keMEw%c1Zh(h)dKMXMKwWG4UpGdKY4|ymUXi=#FQfpEQ}YJtQb5Pxe!9E n3|>sS%xMfB&GrnOEX_w4*p4u;GhSfeVDMm?#ju%2EI|MOk8n6V delta 209 zcmaFk^TtPpfsuiMfrp`iftkS}%rVG$9^0Wz1_mwz1_tgs8)ZT`S>E2*QMb8-b3PYm zCBr?&1jbKnzb4Cw7%%}TKM`3bhF_C&MAXFDUoo&VI4Ce2VqjpB|G$fafq52#ZThdt zD@9b9fYb#MbrAJaL=&V>MN|vK3lP-=QPn^m`>)B%MYXKhpr#yQU}3z#WX0gY$b}GM mW$ - + - + - + - + - + diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index 2cccc232f48e6a4b65814b0a33714598af25dbcc..4b81eb6a8d4b4da76b5d169d3ad018775ca1ce36 100644 GIT binary patch delta 154 zcmdlXwMI&YfsuiMfrp`iftkS}%rVHBe@(Rq0|RRS0|TS>L>X(wnvD@AEG&zc{ukan zfu)-*v&XBrwD?;qkg5OPokmus|KAu4>=?Kh9M6Tl)NNp6X2|+y1XRJ`^uCta_}(2BMxX+T&FyR( F*#HN`Ds=z= delta 185 zcmZ1@wL?mVfsuiMfrp`iftkS}%rVG0=G5*o1_ssu1_nmci89uV9UCJ|SXekS>lSXF zz|zfF&mkhG>kya>)WiyjWG&JF From 9f66edb0d19d5c70aaa674cb0172b682cde6fcc1 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 2 Jan 2024 17:59:20 -0500 Subject: [PATCH 093/114] [varLib.models] Add validate=False to normalizeLocation --- Lib/fontTools/varLib/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py index e90b66885..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]) From d6482c9b32fda6480cf5dafff78ee861728e06f2 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 3 Jan 2024 15:21:02 -0700 Subject: [PATCH 094/114] [VARC] Rename VarInt32 to uint32var --- Lib/fontTools/ttLib/tables/otTables.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index d385a7510..bc7f64ced 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -109,7 +109,7 @@ _unpacker = { } -def _readVarInt32(data, i): +def _read_uint32var(data, i): """Read a variable-length number from data starting at index i. Return the number and the next index. @@ -132,7 +132,7 @@ def _readVarInt32(data, i): ] << 8 | data[i + 4], i + 5 -def _writeVarInt32(v): +def _write_uint32var(v): """Write a variable-length number. Return the data. @@ -161,7 +161,7 @@ class VarComponent: def decompile(self, data, font, localState): i = 0 - self.flags, i = _readVarInt32(data, i) + self.flags, i = _read_uint32var(data, i) flags = self.flags gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2 @@ -170,7 +170,7 @@ class VarComponent: self.glyphName = font.glyphOrder[glyphID] if flags & VarComponentFlags.HAVE_AXES: - self.axisIndicesIndex, i = _readVarInt32(data, i) + self.axisIndicesIndex, i = _read_uint32var(data, i) else: self.axisIndicesIndex = None @@ -185,11 +185,11 @@ class VarComponent: self.axisValues = tuple(axisValues) if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: - self.axisValuesVarIndex, i = _readVarInt32(data, i) + self.axisValuesVarIndex, i = _read_uint32var(data, i) else: self.axisValuesVarIndex = NO_VARIATION_INDEX if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: - self.transformVarIndex, i = _readVarInt32(data, i) + self.transformVarIndex, i = _read_uint32var(data, i) else: self.transformVarIndex = NO_VARIATION_INDEX @@ -235,19 +235,19 @@ class VarComponent: if numAxes: flags |= VarComponentFlags.HAVE_AXES - data.append(_writeVarInt32(self.axisIndicesIndex)) + data.append(_write_uint32var(self.axisIndicesIndex)) data.append(TupleVariation.compileDeltaValues_(self.axisValues)) else: flags &= ~VarComponentFlags.HAVE_AXES if self.axisValuesVarIndex != NO_VARIATION_INDEX: flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION - data.append(_writeVarInt32(self.axisValuesVarIndex)) + 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(_writeVarInt32(self.transformVarIndex)) + data.append(_write_uint32var(self.transformVarIndex)) else: flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION @@ -263,7 +263,7 @@ class VarComponent: value = getattr(self.transform, attr_name) data.append(write_transform_component(value, mapping_values)) - return _writeVarInt32(flags) + bytesjoin(data) + return _write_uint32var(flags) + bytesjoin(data) def toXML(self, writer, ttFont, attrs): attrs.append(("glyphName", self.glyphName)) From 87ddb244ea492c72fd19dbc7dfdf8a1c7c660b28 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 16 Jan 2024 09:14:29 -0700 Subject: [PATCH 095/114] [otBase] Add comment based on review feedback --- Lib/fontTools/ttLib/tables/otBase.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py index 18c17d72f..8df7c236b 100644 --- a/Lib/fontTools/ttLib/tables/otBase.py +++ b/Lib/fontTools/ttLib/tables/otBase.py @@ -1146,6 +1146,9 @@ class BaseTable(object): except KeyError: raise # XXX on KeyError, raise nice error value = conv.xmlRead(attrs, content, font) + # 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: From b3a0a21125a28cec6717399c4c20127766d26def Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 16 Jan 2024 09:15:58 -0700 Subject: [PATCH 096/114] [transform] Remove redundant methods --- Lib/fontTools/misc/transform.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index 755900bf2..9025b79ec 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -422,26 +422,6 @@ class DecomposedTransform: tCenterX: float = 0 tCenterY: float = 0 - def __eq__(self, other): - if not isinstance(other, DecomposedTransform): - return NotImplemented - return ( - self.translateX == other.translateX - and self.translateY == other.translateY - and self.rotation == other.rotation - and self.scaleX == other.scaleX - and self.scaleY == other.scaleY - and self.skewX == other.skewX - and self.skewY == other.skewY - and self.tCenterX == other.tCenterX - and self.tCenterY == other.tCenterY - ) - - def __ne__(self, other): - if not isinstance(other, DecomposedTransform): - return NotImplemented - return not self == other - def __bool__(self): return ( self.translateX != 0 From e3ba7a7e0b46719e031e5a9bbe6339035eeb2575 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 16 Jan 2024 13:35:09 -0700 Subject: [PATCH 097/114] [lazyTools] Simplify based on review feedback --- Lib/fontTools/misc/lazyTools.py | 4 +- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 58 ++++++++--------- Lib/fontTools/ttLib/tables/otConverters.py | 72 +++++++++++----------- 3 files changed, 66 insertions(+), 68 deletions(-) diff --git a/Lib/fontTools/misc/lazyTools.py b/Lib/fontTools/misc/lazyTools.py index a3cc52e53..91cb80c99 100644 --- a/Lib/fontTools/misc/lazyTools.py +++ b/Lib/fontTools/misc/lazyTools.py @@ -11,7 +11,7 @@ class LazyDict(UserDict): def __getitem__(self, k): v = self.data[k] if callable(v): - v = v(self, k) + v = v(k) self.data[k] = v return v @@ -23,7 +23,7 @@ class LazyList(UserList): return [self[i] for i in indices] v = self.data[k] if callable(v): - v = v(self, k) + v = v(k) self.data[k] = v return v diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index a44f72b66..044f65f71 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -40,26 +40,6 @@ GVAR_HEADER_FORMAT = """ GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT) -def _decompileVarGlyph(self, glyphName): - gid = self.reverseGlyphMap[glyphName] - offsetSize = 2 if self.tableFormat == 0 else 4 - startOffset = GVAR_HEADER_SIZE + offsetSize * gid - endOffset = startOffset + offsetSize * 2 - offsets = table__g_v_a_r.decompileOffsets_( - self.gvarData[startOffset:endOffset], - tableFormat=self.tableFormat, - glyphCount=1, - ) - gvarData = self.gvarData[ - self.offsetToData + offsets[0] : self.offsetToData + offsets[1] - ] - if not gvarData: - return [] - glyph = self._glyf[glyphName] - numPointsInGlyph = self._gvar.getNumPoints_(glyph) - return decompileGlyph_(numPointsInGlyph, self.sharedCoords, self.axisTags, gvarData) - - class table__g_v_a_r(DefaultTable.DefaultTable): dependencies = ["fvar", "glyf"] @@ -131,17 +111,33 @@ class table__g_v_a_r(DefaultTable.DefaultTable): offsetToData = self.offsetToGlyphVariationData glyf = ttFont["glyf"] - l = LazyDict( - {glyphs[gid]: _decompileVarGlyph for gid in range(self.glyphCount)} - ) - l.reverseGlyphMap = ttFont.getReverseGlyphMap() - l._glyf = glyf - l._gvar = self - l.gvarData = data - l.offsetToData = offsetToData - l.sharedCoords = sharedCoords - l.axisTags = axisTags - l.tableFormat = self.flags & 1 + def get_read_item(): + reverseGlyphMap = ttFont.getReverseGlyphMap() + tableFormat = self.flags & 1 + + 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 diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index aee2de547..a66c9e858 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -109,11 +109,6 @@ def buildConverters(tableSpec, tableNamespace): return converters, convertersByName -def _base_converter_read_item(self, i): - self.reader.seek(self.pos + i * self.recordSize) - return self.conv.read(self.reader, self.font, {}) - - class BaseConverter(object): """Base class for converter objects. Apart from the constructor, this is an abstract class.""" @@ -162,13 +157,19 @@ class BaseConverter(object): l.append(self.read(reader, font, tableDict)) return l else: - l = LazyList(_base_converter_read_item for i in range(count)) - l.reader = reader.copy() - l.pos = l.reader.pos - l.font = font - l.conv = self - l.recordSize = recordSize + 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 @@ -1819,21 +1820,6 @@ class TupleValues: xmlWriter.newline() -def cff2_index_read_item(self, i): - self.reader.seek(self.offset_pos + i * self.offSize) - offsets = self.readArray(2) - self.reader.seek(self.data_pos + offsets[0]) - item = self.reader.readData(offsets[1] - offsets[0]) - - if self._itemClass is not None: - obj = self._itemClass() - obj.decompile(item, self.font, self.reader.localState) - item = obj - elif self._converter is not None: - item = self._converter.read(item, self.font) - return item - - class CFF2Index(BaseConverter): def __init__( self, @@ -1892,15 +1878,31 @@ class CFF2Index(BaseConverter): lastOffset = offset return items else: - l = LazyList([cff2_index_read_item] * count) - l.reader = reader.copy() - l.offset_pos = l.reader.pos - l.data_pos = l.offset_pos + (count + 1) * offSize - l.font = font - l._itemClass = self._itemClass - l.offSize = offSize - l.readArray = getReadArray(l.reader, offSize) - l._converter = self._converter + + def get_read_item(): + reader_copy = reader.copy() + offset_pos = reader.pos + data_pos = offset_pos + (count + 1) * offSize + 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, self.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 From 5ad4045ceccb077dcdec300d15f9aba0e62fdd7a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 17 Jan 2024 17:27:06 -0700 Subject: [PATCH 098/114] [VarComponent] Nicer XML output https://github.com/fonttools/fonttools/pull/3395#issuecomment-1895944021 --- Lib/fontTools/ttLib/tables/otTables.py | 71 ++++++++++--------- Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 75 ++++++++++++++++++--- 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index bc7f64ced..b5d76aaf3 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -151,6 +151,9 @@ def _write_uint32var(v): class VarComponent: def __init__(self): + self.populateDefaults() + + def populateDefaults(self, propagator=None): self.flags = 0 self.glyphName = None self.axisIndicesIndex = None @@ -266,55 +269,63 @@ class VarComponent: return _write_uint32var(flags) + bytesjoin(data) def toXML(self, writer, ttFont, attrs): - attrs.append(("glyphName", self.glyphName)) - attrs.append(("flags", self.flags)) + writer.begintag("VarComponent", attrs) + writer.newline() + + def write(name, value): + if value is not None: + writer.simpletag(name, value=value) + writer.newline() + + write("glyphName", self.glyphName) + write("flags", hex(self.flags)) if self.axisIndicesIndex is not None: - attrs.append(("axisIndicesIndex", self.axisIndicesIndex)) - attrs.append(("axisValues", self.axisValues)) + write("axisIndicesIndex", self.axisIndicesIndex) + write("axisValues", self.axisValues) if self.axisValuesVarIndex != NO_VARIATION_INDEX: - attrs.append(("axisValuesVarIndex", self.axisValuesVarIndex)) + write("axisValuesVarIndex", self.axisValuesVarIndex) if self.transformVarIndex != NO_VARIATION_INDEX: - attrs.append(("transformVarIndex", self.transformVarIndex)) + write("transformVarIndex", self.transformVarIndex) for attr_name, mapping in VAR_TRANSFORM_MAPPING.items(): v = getattr(self.transform, attr_name) if v != mapping.defaultValue: - attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) + write(attr_name, fl2str(v, mapping.fractionalBits)) - writer.simpletag("VarComponent", attrs) + writer.endtag("VarComponent") writer.newline() def fromXML(self, name, attrs, content, ttFont): - self.flags = safeEval(attrs["flags"]) - self.glyphName = attrs["glyphName"] + content = [c for c in content if isinstance(c, tuple)] - assert ("axisIndicesIndex" in attrs) == ("axisValues" in attrs) - if "axisIndicesIndex" in attrs: - self.axisIndicesIndex = safeEval(attrs["axisIndicesIndex"]) - self.axisValues = safeEval(attrs["axisValues"]) - else: - self.axisIndicesIndex = None - self.axisValues = () + self.populateDefaults() - if "axisValuesVarIndex" in attrs: - self.axisValuesVarIndex = safeEval(attrs["axisValuesVarIndex"]) - else: - self.axisValuesVarIndex = NO_VARIATION_INDEX - if "transformVarIndex" in attrs: - self.transformVarIndex = safeEval(attrs["transformVarIndex"]) - else: - self.transformVarIndex = NO_VARIATION_INDEX + for name, attrs, content in content: + assert not content + v = attrs["value"] - self.transform = DecomposedTransform() - for attr_name, mapping in VAR_TRANSFORM_MAPPING.items(): - if attr_name in attrs: + if name == "glyphName": + self.glyphName = v + elif name == "flags": + self.flags = safeEval(v) + elif name == "axisIndicesIndex": + self.axisIndicesIndex = safeEval(v) + elif name == "axisValues": + self.axisValues = safeEval(v) + elif name == "axisValuesVarIndex": + self.axisValuesVarIndex = safeEval(v) + elif name == "transformVarIndex": + self.transformVarIndex = safeEval(v) + elif name in VAR_TRANSFORM_MAPPING: setattr( self.transform, - attr_name, - safeEval(attrs[attr_name]), + name, + safeEval(v), ) + else: + assert False, name def applyTransformDeltas(self, deltas): i = 0 diff --git a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx index 1c1d49ee2..360807b7c 100644 --- a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx +++ b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx @@ -304,28 +304,83 @@ - - + + + + + + + + - - - + + + + + + + + + + + + - + + + + + + + - + + + + + + + + - + + + + + + + + + + - + + + + + + + + + + - + + + + + + + + + + From 037bbe103005241e08d7d5da7cbd36624a93b7b5 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Thu, 18 Jan 2024 06:25:52 -0700 Subject: [PATCH 099/114] [VARC] Load axisValues as float https://github.com/fonttools/fonttools/pull/3395#issuecomment-1897696665 --- Lib/fontTools/ttLib/tables/otTables.py | 12 ++++++++---- Lib/fontTools/ttLib/ttGlyphSet.py | 5 ++--- Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 10 +++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index b5d76aaf3..595707656 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -185,7 +185,7 @@ class VarComponent: axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i) assert len(axisValues) == numAxes - self.axisValues = tuple(axisValues) + self.axisValues = tuple(fi2fl(v, 14) for v in axisValues) if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: self.axisValuesVarIndex, i = _read_uint32var(data, i) @@ -239,7 +239,11 @@ class VarComponent: if numAxes: flags |= VarComponentFlags.HAVE_AXES data.append(_write_uint32var(self.axisIndicesIndex)) - data.append(TupleVariation.compileDeltaValues_(self.axisValues)) + data.append( + TupleVariation.compileDeltaValues_( + [fl2fi(v, 14) for v in self.axisValues] + ) + ) else: flags &= ~VarComponentFlags.HAVE_AXES @@ -282,7 +286,7 @@ class VarComponent: if self.axisIndicesIndex is not None: write("axisIndicesIndex", self.axisIndicesIndex) - write("axisValues", self.axisValues) + write("axisValues", [float(fl2str(v, 14)) for v in self.axisValues]) if self.axisValuesVarIndex != NO_VARIATION_INDEX: write("axisValuesVarIndex", self.axisValuesVarIndex) @@ -313,7 +317,7 @@ class VarComponent: elif name == "axisIndicesIndex": self.axisIndicesIndex = safeEval(v) elif name == "axisValues": - self.axisValues = safeEval(v) + self.axisValues = tuple(str2fl(v, 14) for v in safeEval(v)) elif name == "axisValuesVarIndex": self.axisValuesVarIndex = safeEval(v) elif name == "transformVarIndex": diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 4a5af3b8e..e458466af 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -304,14 +304,13 @@ class _TTGlyphVARC(_TTGlyph): axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] axisValues = Vector(comp.axisValues) if comp.axisValuesVarIndex != NO_VARIATION_INDEX: - axisValues += instancer[comp.axisValuesVarIndex] + axisValues += fi2fl(instancer[comp.axisValuesVarIndex], 14) assert len(axisIndices) == len(axisValues), ( len(axisIndices), len(axisValues), ) location = { - fvarAxes[i].axisTag: fi2fl(v, 14) - for i, v in zip(axisIndices, axisValues) + fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues) } if comp.transformVarIndex != NO_VARIATION_INDEX: diff --git a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx index 360807b7c..1c60b923b 100644 --- a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx +++ b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx @@ -332,7 +332,7 @@ - + @@ -341,7 +341,7 @@ - + @@ -351,7 +351,7 @@ - + @@ -363,7 +363,7 @@ - + @@ -375,7 +375,7 @@ - + From 5b2df6b90b2e3d7705772d14e25d75040b303611 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 20 Jan 2024 05:29:33 -0800 Subject: [PATCH 100/114] [VARC] Compute flags from XML transform components --- Lib/fontTools/ttLib/tables/otTables.py | 13 ++++++++++--- Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 10 ---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 595707656..c97c866c2 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -282,7 +282,8 @@ class VarComponent: writer.newline() write("glyphName", self.glyphName) - write("flags", hex(self.flags)) + if self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES: + write("resetUnspecifiedAxes", True) if self.axisIndicesIndex is not None: write("axisIndicesIndex", self.axisIndicesIndex) @@ -293,10 +294,13 @@ class VarComponent: 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) - if v != mapping.defaultValue: - write(attr_name, fl2str(v, mapping.fractionalBits)) + write(attr_name, fl2str(v, mapping.fractionalBits)) writer.endtag("VarComponent") writer.newline() @@ -312,6 +316,8 @@ class VarComponent: if name == "glyphName": self.glyphName = v + elif name == "resetUnspecifiedAxes": + self.flags = VarComponentFlags.RESET_UNSPECIFIED_AXES if v else 0 elif name == "flags": self.flags = safeEval(v) elif name == "axisIndicesIndex": @@ -328,6 +334,7 @@ class VarComponent: name, safeEval(v), ) + self.flags |= VAR_TRANSFORM_MAPPING[name].flag else: assert False, name diff --git a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx index 1c60b923b..bda8071af 100644 --- a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx +++ b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx @@ -306,31 +306,25 @@ - - - - - - @@ -339,7 +333,6 @@ - @@ -349,7 +342,6 @@ - @@ -361,7 +353,6 @@ - @@ -373,7 +364,6 @@ - From 7d6df04d44d1a47ed5cde3eb7bcdb2e326fe258f Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 22 Jan 2024 11:33:49 -0800 Subject: [PATCH 101/114] [VARC] Minor, match spec better No functional change. --- Lib/fontTools/ttLib/tables/otTables.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index c97c866c2..9b7444a62 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -183,9 +183,12 @@ class VarComponent: axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex] numAxes = len(axisIndices) - axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i) - assert len(axisValues) == numAxes - self.axisValues = tuple(fi2fl(v, 14) for v in axisValues) + 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) From 40584ad489193b0f8d6b5fd39b0cbedfd847e37d Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 22 Jan 2024 14:20:35 -0800 Subject: [PATCH 102/114] [VARC] Adjust XML output based on review feedback --- Lib/fontTools/ttLib/tables/otTables.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 9b7444a62..7ac56720f 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -279,18 +279,24 @@ class VarComponent: writer.begintag("VarComponent", attrs) writer.newline() - def write(name, value): + def write(name, value, attrs=()): if value is not None: - writer.simpletag(name, value=value) + writer.simpletag(name, (("value", value),) + attrs) writer.newline() write("glyphName", self.glyphName) - if self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES: - write("resetUnspecifiedAxes", True) if self.axisIndicesIndex is not None: write("axisIndicesIndex", self.axisIndicesIndex) - write("axisValues", [float(fl2str(v, 14)) for v in self.axisValues]) + 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) @@ -319,14 +325,12 @@ class VarComponent: if name == "glyphName": self.glyphName = v - elif name == "resetUnspecifiedAxes": - self.flags = VarComponentFlags.RESET_UNSPECIFIED_AXES if v else 0 - elif name == "flags": - self.flags = 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": From 1acc80eba57c4d7a5510cd4b2ae8460138bc0301 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 26 Jan 2024 09:42:55 -0800 Subject: [PATCH 103/114] [VARC] Deepcopy the component Since we modify comp.transform. --- Lib/fontTools/ttLib/ttGlyphSet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index e458466af..0e1bcd6d6 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -3,7 +3,7 @@ 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.vector import Vector from fontTools.misc.fixedTools import otRound, fixedToFloat as fi2fl @@ -315,7 +315,7 @@ class _TTGlyphVARC(_TTGlyph): if comp.transformVarIndex != NO_VARIATION_INDEX: deltas = instancer[comp.transformVarIndex] - comp = copy(comp) # Shallow copy + comp = deepcopy(comp) comp.applyTransformDeltas(deltas) transform = comp.transform From 3bada5de806dc6642d06955915bdd29767e52fd3 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 26 Jan 2024 10:26:31 -0800 Subject: [PATCH 104/114] Fix otConverters lazy reader --- Lib/fontTools/ttLib/tables/otConverters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index a66c9e858..cffe33302 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1893,7 +1893,7 @@ class CFF2Index(BaseConverter): if self._itemClass is not None: obj = self._itemClass() - obj.decompile(item, self.font, reader_copy.localState) + obj.decompile(item, font, reader_copy.localState) item = obj elif self._converter is not None: item = self._converter.read(item, font) From a1641d91b28d4e391d9567dad7693954939faef4 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 6 Feb 2024 15:42:21 -0700 Subject: [PATCH 105/114] Newer black --- Lib/fontTools/ttLib/tables/otConverters.py | 6 +----- Lib/fontTools/varLib/instancer/__init__.py | 8 +++++--- Lib/fontTools/varLib/mutator.py | 14 ++++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index cffe33302..45431c3a0 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1930,11 +1930,7 @@ class CFF2Index(BaseConverter): offSize = ( 1 if lastOffset < 0x100 - else 2 - if lastOffset < 0x10000 - else 3 - if lastOffset < 0x1000000 - else 4 + else 2 if lastOffset < 0x10000 else 3 if lastOffset < 0x1000000 else 4 ) writer.writeUInt8(offSize) diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index 92e9c4737..f8c43187c 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -928,9 +928,11 @@ def instantiateGvar(varfont, axisLimits, optimize=True): glyphnames = sorted( glyf.glyphOrder, key=lambda name: ( - glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() - else 0, + ( + glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth + if glyf[name].isComposite() + else 0 + ), name, ), ) diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py index ae848cc4e..6c327f945 100644 --- a/Lib/fontTools/varLib/mutator.py +++ b/Lib/fontTools/varLib/mutator.py @@ -199,9 +199,11 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True): glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( - glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() - else 0, + ( + glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth + if glyf[name].isComposite() + else 0 + ), name, ), ) @@ -305,9 +307,9 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True): if applies: assert record.FeatureTableSubstitution.Version == 0x00010000 for rec in record.FeatureTableSubstitution.SubstitutionRecord: - table.FeatureList.FeatureRecord[ - rec.FeatureIndex - ].Feature = rec.Feature + table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = ( + rec.Feature + ) break del table.FeatureVariations From 88828e0082816c6ebbc5f45a1bd2e658f6e230ef Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 16 Mar 2024 10:42:01 -0600 Subject: [PATCH 106/114] [CFF2IndexOf] Fix data_base From the spec: "Offsets in the offset array are relative to the byte that precedes the object data." --- Lib/fontTools/ttLib/tables/otConverters.py | 6 +- Tests/ttLib/data/varc-6868.ttf | Bin 9836 -> 9836 bytes Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 357 ++++++++++---------- Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 3372 -> 3284 bytes 4 files changed, 175 insertions(+), 188 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 45431c3a0..656836bd3 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1861,7 +1861,7 @@ class CFF2Index(BaseConverter): offsets = readArray(count + 1) items = [] lastOffset = offsets.pop(0) - reader.readData(lastOffset) # In case first offset is not 0 + reader.readData(lastOffset - 1) # In case first offset is not 1 for offset in offsets: assert lastOffset <= offset @@ -1882,7 +1882,7 @@ class CFF2Index(BaseConverter): def get_read_item(): reader_copy = reader.copy() offset_pos = reader.pos - data_pos = offset_pos + (count + 1) * offSize + data_pos = offset_pos + (count + 1) * offSize - 1 readArray = getReadArray(reader_copy, offSize) def read_item(i): @@ -1924,7 +1924,7 @@ class CFF2Index(BaseConverter): ] offsets = [len(item) for item in items] - offsets = [0] + list(accumulate(offsets)) + offsets = list(accumulate(offsets, initial=1)) lastOffset = offsets[-1] offSize = ( diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index 47f85e9b7ba7760f8ffd8046b312bf13e83aa18c..bb2c518b72e14b98e4e667857aa8f6c093e69b24 100644 GIT binary patch delta 157 zcmaFk^TtPnfsuiMfrp`iftkS}%rVG$rSAG;8%1I`S*~~8J-WGs(|}ouk(KekVlyL9 z1p`nu6Od$OWVCjTPMkk^AI~l}M#<2IZC5te@yapsGBUI=++&=;xQy`=Q_$v>d_7Ep zj0{B#w-`ehS212?VrLR>-bbXwAgS$jl|Bp=;sb862NqH@Q!^nUj&B5~w$U@zZ80 Hksu)e+G;6Q delta 157 zcmaFk^TtPnfsuiMfrp`iftkS}%rVG$uFUe?8%1I`S>CUCdwX*UrvbAR0}JDS#b!pJ z3I?ERCLqbm$YA9Xl`wDeKAv4{3=$#rTQ6^} - + @@ -215,9 +215,6 @@ 0013 - - 0014 - @@ -536,22 +533,27 @@ 1.0 271 - - - - 0014 - 0x0 - -1.0 - 0.0 - 1.0 - 272 - + + + + + + + + + + + + + + + @@ -582,21 +584,6 @@ - - - - - - - - - - - - - - - @@ -661,6 +648,29 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -730,29 +740,6 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -822,54 +809,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -894,30 +833,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - @@ -968,24 +883,71 @@ - - - - - - - + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1066,6 +1028,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1094,6 +1081,21 @@ + + + + + + + + + + + + + + + @@ -1124,21 +1126,6 @@ - - - - - - - - - - - - - - - @@ -1169,6 +1156,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1185,38 +1204,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index 4b81eb6a8d4b4da76b5d169d3ad018775ca1ce36..3134c0ad3eeaf37f9f23f9ac49fc69e9c2ee59c4 100644 GIT binary patch delta 732 zcmZ8eO-NKx6h8O;zj<$Rv@7Wh+Wfq5DmSJC!32sD{TNAEkqPhH@E+Pn87%GCbY3Cv-CWwIwD#A^hM~fClLQd!Pv*|w0x#v6Iz2|-Bp2cXXb_y5( znBag2tryzPw(}L|G=Q82Ua5hrG#0L`j;5N9;=0^c$7CZc2%;pB$qGnx zNo7op)HWPB(Su3lN<{<|N;ZN)9>7Oi+AVhqCxs21HRIT;Rbx&v&{q!pjrIR_Ip(O) zM{*bhdJX!A0x~F0P!22gdR$V|#~p=pC~{tO)y>WNdf(^V_1reL!8QtBJHtQUJ$?s^wVt8_?MGNMxRDh*Gj3CRw$;+#tDn=%fV x30yHPu5!+Of2&B(-~;ESQqgzut-06oTp^Rm>^T1GQ)m`Q+MHVl$4?5VjD@j zvbW38p9~^|1R=TzBPdD;FTx88p)Mmvmr@a}_Z0LVp7(su`+V>FJs;m@!*b(11OO8` zIM914exlF4JevTN0YDra%_Orhmj?m)7w?LWUrAni@yYuLkS?&EnwY#ce&|_$1#rA# zePM!ix&K@97rU4rW8QKxnHg)(p0*Er#|G|Xr!G&kaSkP7>kJ>hF24U@I3Q%F z@Ic2ydMuXqL=fS@QP{WjKtdfXUL{sXq+Ve~B3;p1jkz864hRpG9dG5OO3k;Lmcv4a z>9HExA~7l}@x72;rX)FJnW3nVxj_?9-TuJg#4!C-&b#ZNaxo0U9)|A}mwoDSX;|6@ zy;NK2zS=@-YMAb-0s5vk^1s=W(~x4j;2xRH2^!EsbX7Z050vt8u`bq&r9MIbzMJ$w z+wB#mSF3qm7OlgS2XNh!xs4LSER8 zp|Nv~-$TjqfU{-`pJJLJ8Z!S*Tr~IfXqIJFt2-MTd94b|+T7fz=1X}g7K;@McFvay N_+8|*^*;rRr9YSMq+ Date: Wed, 20 Mar 2024 15:10:17 -0600 Subject: [PATCH 107/114] [varc] Skip reserved records --- Lib/fontTools/ttLib/tables/otTables.py | 37 +++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 7ac56720f..95fa83f8a 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -49,28 +49,30 @@ log = logging.getLogger(__name__) class VarComponentFlags(IntFlag): - RESET_UNSPECIFIED_AXES = 0x0001 + RESET_UNSPECIFIED_AXES = 1 << 0 - HAVE_AXES = 0x0002 + HAVE_AXES = 1 << 1 - AXIS_VALUES_HAVE_VARIATION = 0x0004 - TRANSFORM_HAS_VARIATION = 0x0008 + AXIS_VALUES_HAVE_VARIATION = 1 << 2 + TRANSFORM_HAS_VARIATION = 1 << 3 - HAVE_TRANSLATE_X = 0x0010 - HAVE_TRANSLATE_Y = 0x0020 - HAVE_ROTATION = 0x0040 + HAVE_TRANSLATE_X = 1 << 4 + HAVE_TRANSLATE_Y = 1 << 5 + HAVE_ROTATION = 1 << 6 - USE_MY_METRICS = 0x0080 + USE_MY_METRICS = 1 << 7 - HAVE_SCALE_X = 0x0100 - HAVE_SCALE_Y = 0x0200 - HAVE_TCENTER_X = 0x0400 - HAVE_TCENTER_Y = 0x0800 + HAVE_SCALE_X = 1 << 8 + HAVE_SCALE_Y = 1 << 9 + HAVE_TCENTER_X = 1 << 10 + HAVE_TCENTER_Y = 1 << 11 - GID_IS_24BIT = 0x4000 + GID_IS_24BIT = 1 << 12 - HAVE_SKEW_X = 0x1000 - HAVE_SKEW_Y = 0x2000 + HAVE_SKEW_X = 1 << 13 + HAVE_SKEW_Y = 1 << 14 + + RESERVED_MASK = (1 << 32) - (1 << 15) VarTransformMappingValues = namedtuple( @@ -222,6 +224,11 @@ class VarComponent: 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): From 74f870f4eff8d722708e7ecb65f89891c2ac4f57 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 20 Mar 2024 15:36:32 -0600 Subject: [PATCH 108/114] [varc] Add ConditionSets --- Lib/fontTools/subset/__init__.py | 16 + Lib/fontTools/ttLib/tables/otData.py | 20 + Lib/fontTools/ttLib/tables/otTables.py | 14 +- Lib/fontTools/ttLib/ttGlyphSet.py | 22 + Tests/ttLib/data/varc-6868.ttf | Bin 9836 -> 8784 bytes Tests/ttLib/data/varc-ac00-ac01-500upem.ttx | 490 +++++++++----------- Tests/ttLib/data/varc-ac00-ac01.ttf | Bin 3284 -> 2748 bytes Tests/ttLib/tables/V_A_R_C_test.py | 12 +- Tests/ttLib/ttGlyphSet_test.py | 4 +- 9 files changed, 287 insertions(+), 291 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index ed296ed65..c924d574b 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -2693,6 +2693,22 @@ def prune_post_subset(self, font, options): if comp.axisIndicesIndex is not None: comp.axisIndicesIndex = mapping[comp.axisIndicesIndex] + conditionSetList = table.ConditionSetList + if conditionSetList is not None: + conditionSets = conditionSetList.ConditionSet + usedIndices = set() + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.conditionSetIndex is not None: + usedIndices.add(comp.conditionSetIndex) + usedIndices = sorted(usedIndices) + conditionSetList.ConditionSet = _list_subset(conditionSets, usedIndices) + mapping = {old: new for new, old in enumerate(usedIndices)} + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.conditionSetIndex is not None: + comp.conditionSetIndex = mapping[comp.conditionSetIndex] + return True diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index cb7ad5de9..a2071b1f5 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3168,6 +3168,25 @@ otData = [ ), ], ), + ( + "ConditionSetList", + [ + ( + "uint32", + "ConditionSetCount", + None, + None, + "Number of condition-set tables in the ConditionSet array", + ), + ( + "LOffset", + "ConditionSet", + "ConditionSetCount", + 0, + "Array of condition-set tables.", + ), + ], + ), ( "ConditionSet", [ @@ -3377,6 +3396,7 @@ otData = [ ), ("LOffset", "Coverage", None, None, ""), ("LOffset", "MultiVarStore", None, None, "(may be NULL)"), + ("LOffset", "ConditionSetList", None, None, "(may be NULL)"), ("LOffset", "AxisIndicesList", None, None, "(may be NULL)"), ("LOffset", "VarCompositeGlyphs", None, None, ""), ], diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 95fa83f8a..15797fbed 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -60,7 +60,7 @@ class VarComponentFlags(IntFlag): HAVE_TRANSLATE_Y = 1 << 5 HAVE_ROTATION = 1 << 6 - USE_MY_METRICS = 1 << 7 + HAVE_CONDITION = 1 << 7 HAVE_SCALE_X = 1 << 8 HAVE_SCALE_Y = 1 << 9 @@ -158,6 +158,7 @@ class VarComponent: def populateDefaults(self, propagator=None): self.flags = 0 self.glyphName = None + self.conditionSetIndex = None self.axisIndicesIndex = None self.axisValues = () self.axisValuesVarIndex = NO_VARIATION_INDEX @@ -174,6 +175,9 @@ class VarComponent: i += gidSize self.glyphName = font.glyphOrder[glyphID] + if flags & VarComponentFlags.HAVE_CONDITION: + self.conditionSetIndex, i = _read_uint32var(data, i) + if flags & VarComponentFlags.HAVE_AXES: self.axisIndicesIndex, i = _read_uint32var(data, i) else: @@ -244,6 +248,10 @@ class VarComponent: flags &= ~VarComponentFlags.GID_IS_24BIT data.append(_packer[2](glyphID)) + if self.conditionSetIndex is not None: + flags |= VarComponentFlags.HAVE_CONDITION + data.append(_write_uint32var(self.conditionSetIndex)) + numAxes = len(self.axisValues) if numAxes: @@ -293,6 +301,8 @@ class VarComponent: write("glyphName", self.glyphName) + if self.conditionSetIndex is not None: + write("conditionSetIndex", self.conditionSetIndex) if self.axisIndicesIndex is not None: write("axisIndicesIndex", self.axisIndicesIndex) if ( @@ -332,6 +342,8 @@ class VarComponent: if name == "glyphName": self.glyphName = v + elif name == "conditionSetIndex": + self.conditionSetIndex = safeEval(v) elif name == "axisIndicesIndex": self.axisIndicesIndex = safeEval(v) elif name == "axisValues": diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 0e1bcd6d6..9772ae813 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -299,6 +299,28 @@ class _TTGlyphVARC(_TTGlyph): ) for comp in glyph.components: + + if comp.flags & VarComponentFlags.HAVE_CONDITION: + conditionSet = varc.ConditionSetList.ConditionSet[ + comp.conditionSetIndex + ] + # Evaluate condition + show = True + for condition in conditionSet.ConditionTable: + if condition.Format == 1: + axisIndex = condition.AxisIndex + axisTag = fvarAxes[axisIndex].axisTag + axisValue = self.glyphSet.location[axisTag] + minValue = condition.FilterRangeMinValue + maxValue = condition.FilterRangeMaxValue + if not (minValue <= axisValue <= maxValue): + show = False + break + else: + show = False # Unkonwn condition format + if not show: + continue + location = {} if comp.axisIndicesIndex is not None: axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] diff --git a/Tests/ttLib/data/varc-6868.ttf b/Tests/ttLib/data/varc-6868.ttf index bb2c518b72e14b98e4e667857aa8f6c093e69b24..6041d6cc1f179e0354957e4c0a978e6e77281839 100644 GIT binary patch delta 2399 zcmZXWd2Ccw6o=2f_sz_krPF1m(@v*!Wfx>-N-2A%E0ri^Q3w#lp|%ugZ3|VXs8ehR z5@Jg7Tq03%Ax0B|n(9ADkXjUDL`X0pBqmBEQWRVgS)zvVcV`aJ#QS>Qd}n#*-FxTU z)46j0nfe3?BJxl_DT=MESzdSDc6|+zV=0mQL`!G9n>_8G5{X9_?Go+2J47? zej@qwhK|0r=DE$w;6DWa(GBn)Z1~_9QSLq@J=WeDe{5~I9=Yfn$gFl4l5oVtGBc(>QckH+sPClG<^KL7Cnuco*u4a0K$m%txS{Kc*^|e`Gy{zPk7L$P zmNMHYVbBCt>~KtBc7Ufdo8Snu6I{vc0?%P~gBLJ+z%|Sn;09(d_&(-L@B^B$evt)b z89TDU4>9L}S2O#-4>SA0apqibD{}z6kvR|C$(#@FVGe?yU@ibB`dA4;d4{p$q|@xMfzL7PZ}kUe z2lx`R3I3hg3BJMX0wWC{nf2b{^A0nTIgf{Q7US`7kBF^L^n;2F%>;40=Ea5b|J zyolKkZf4E}uVM~>*D>dTH!$aeyO@IsDBD;m04JD3;N8rH;6dhb;8&Q7zz4RpY}$-2 z)Uv8|IdFin!?Pp&}I$me)0t0CW_ zV)T)fIOvX4J?XL}Sr7S#$aWCt_|>6Jt}+okPRI3Q`Y=-r7s!($Ys6h+OL?uNJILho zR53Mp%B&ubKWyAdkap+<_Owl4>$IwSnO%r^4Qt~E-m#8(4ya`5fVIpUnBk!ew2(~7 zqHM}R@AIP>2Plv7(M$>`M1^Q1MN~{BXd-1)KJ>D;O=h}~*1&RYXeM@=Xmw`%ToFv! zY|wS}I?09Q{6D!)eJAnIjO+*HGGD=zszuA!v~>18OVTTqVY55jnZDdWupm@uOY0fZ z=5S_svvPd?+(3TNleU^pm)n!!+s*BiIwIMJ?Tbx5jP7wApnf^oQ-9LnnQhZ2U3Wb(B^)T$AiOgn45 zjq1{KF>$3oW^Ua%Of*a)StWan1FAw*$OKJ9Qp46*u`ACNT~hh>q{BIbqG_Nm|GK@+ z{o1)7}$aB zjg`>nD5B!z5tGz@TA&j+tV8|@Qkm9S|D=TJZ1hf0_2s)}?XLVO@Sff1kHu(&@F#A~ zmiPX;O%AG4>J+&Ka2j1r-t(d_BrBIz$iQX8l+$C>A9I`@Avr=yT$FWx?U6gw;emPS zSC5b(&d9mcBwTk*n5Ues+NcQ&cb2#ZM#w%w4siyB&?NU`vkCPjQAPMwLH$Et869$J z%36%dBbth0!BF})L}~~n^~jMq#%iO_`Y^B7`Z2!|GM{Dn7`|;A`?-|;B}RWH9kq^z zwpz`FjYFRlE*2`APFR1GueCOhUq5tp{6b-!Dlbt%`JE+;64qNqwU#QbwOS!3AyZ0f Zt-X+|kmk}_>!Z@zAz#@MVcj#a=06{ma1Q_g delta 2612 zcma)+Yiv|S6vxlZ+`Et4?e2ElExX-ryM0nx3ftZ73)F5~Ahlv7pcugbTck9kwS^L; zVBA7A1Y!bmgCB?nVvG?(5Unu=iD_-cC!tysgZM&;ROA!Uq%oxRfA>x+?kA_c_x#R( z&YYQZXLcr5+*+L^Mnn!8Bth}@&1+jvSHE?UNDC8XT;1A}=p#qh2SoPo5f$rvDzR%@ z&y^}7<$WS?VSD#LXCij*dLr9y^k3MH{!fcv>>u9<~I8A)bO=Xm|{m^HuxrUu+3x)c92^#OHwZ7OUDR`NEYB_k`-`^ zWEI>l*#ef6F)Mh3)Qzb+B-_C|Bx~SZk~6@2!AY!?Ip7?S9hu<6lAYjVl3m~vlHK4} zC1-)(lIZ3{x1#qY>{gMN@CX)<{1ht=SXB znKk2TrLB{;UfKp}&3tBb7^BR5G0bF)kI9*06m#Q1<3~lDmJ)tZG%{S8-OZEXaHMf| z7QWGp2x5ida5S8zg`m}>X+_W?X<9L~Xqr|6EtaO0LaT+AG-jNexC~-lT3|V}`ZR4J zw1zaT0$O?@i^AbpdLfn2?kc1uHG37r^hy>(ORuCFT6!f*^fRnd5AnsmY1}blN=#wv zsiue(iK*%E9iN!ezhM!5H4nOs&#Pf)xG^blebgIF=1?Wo(?hfgiqVhf+rvM6yxDcn zUnGzV?U}MyM&|5vc;+?bxW%}wL@E#B%_t+nY|~-mMnp6ELbDCH{oaPQPC0IFuo-1W z=v!E=USlnf8b_i4k*o1sMP`OS#ms>VPQ`ND@H~!;PF&xp&~j$@1#2;@CyL__(YUfD zsUtLkq0lVzq|8%Biu6xx&AP{)qlfJgz1tpqG;cm2IG+d$k}19Oz1b!)<11qZZoC;& z*=*6!-I~hS@|7DCC-qDAlWu%5DRd(|2k)OS7=*O!81ReiB|>Qz3<@h)Sq*_+71&`~9h8DiRsDwjIecD3wS*ss)Byo$GkLmRC{T z5Wj!j=H6$Ct6V6m#p*PT&8}rQGMz5B<3GOOLbd2O+>xY=%W!3TbNzXN{7kM|?U_!u zCp*WR>+_>t#n_$!`jKs6w}+ZYXmRJkLF#`%yu^F$hiv`iRwuP{)__&Bb!1L@n?{L7 zG2$ZK6emO!BRCseq0I0g>k_;vFiL!s#LP81rCd|aDovzh=zFsQj#1+P@Y$D#_K{UD z^5k$Vu-;RK<>;F{3#(NTub}ZfjGN07`OHBA0rwgHe1hkDknu$M0t|RF`td= zUu5}{W5maBsC)VA3Q`|QlB%{^HF3jo#Tc$m4#t&6q)^7NPwgz~xXwDoPBuwp2({Q& zIm^U~lIQCV*8ZCNhD8~S$7!7K&t@9 zc&#aa!t0Q>IL)8a|DQeT>qAZ>Hm(f1=#IX{-wz?APx@CGcG#cS&V*md(Vq(@Qyrq9 zC1qDidr;cXq|GQa`yZ6{2<&Af{7TyFP|6>EBy^tXiC{=%F-7kTY)mB`4RoYtEy=%+ J>8|4De*k~Gu#Nx# diff --git a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx index 49b1696d6..9976b5bf7 100644 --- a/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx +++ b/Tests/ttLib/data/varc-ac00-ac01-500upem.ttx @@ -58,12 +58,12 @@ - + - + - - + + @@ -94,31 +94,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -140,6 +116,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -161,6 +161,36 @@ + + rcjk + + + varc + + + Weight + + + OpticalSize + + + 0000 + + + 0001 + + + 0002 + + + 0003 + + + 0004 + + + 0005 + rcjk @@ -191,30 +221,6 @@ 0005 - - 0006 - - - 0007 - - - 0008 - - - 0009 - - - 0010 - - - 0011 - - - 0012 - - - 0013 - @@ -235,10 +241,10 @@ - - + + @@ -285,88 +291,88 @@ - - - - - - - + + + + + + + - - + + - + - + - + - + - + - - - - + + + + + + + - - - - - + + + + - - + + - - + + - + - - - - - + + + - + - + - - + + @@ -453,201 +459,12 @@ 1.0 263 - - - - 0006 - 0x0 - -1.0 - 0.0 - 1.0 - 264 - - - - - 0007 - 0x0 - -1.0 - 0.0 - 1.0 - 265 - - - - - 0008 - 0x0 - -1.0 - 0.0 - 1.0 - 266 - - - - - 0009 - 0x0 - -1.0 - 0.0 - 1.0 - 267 - - - - - 0010 - 0x0 - -1.0 - 0.0 - 1.0 - 268 - - - - - 0011 - 0x0 - -1.0 - 0.0 - 1.0 - 269 - - - - - 0012 - 0x0 - -1.0 - 0.0 - 1.0 - 270 - - - - - 0013 - 0x0 - -1.0 - 0.0 - 1.0 - 271 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1080,6 +897,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/data/varc-ac00-ac01.ttf b/Tests/ttLib/data/varc-ac00-ac01.ttf index 3134c0ad3eeaf37f9f23f9ac49fc69e9c2ee59c4..fb3a08780ef5b927454ff894cbb1819300d3a6c2 100644 GIT binary patch delta 1186 zcmYk4TS$~a6vxlZeAo5MHrdQ_wUt(~(D2#?EJl-vAc~5jC~8|TYigQGbRqn_JS0Uy z)Ab>`Kok{S=%t_siFybmD0(Qcdg-Bp>LHShaQlDXevxN5^Zm{_Gv~~AW?s*JJhP7& z5jo_NAi242!^&{Ui7F!Tk%%oxb|kvUxxES8h*j0i`xD*S@ePtlkHWs!(tfb{^!=~5 z5WffeXba-~@%RqJM~Sp=sis8Z?lUXzBLgpo_982X+bU*xp1(Q}wxBSBdyi_=dYWdwOwjh`=6-`9Fgn%Dp?I zaOd>epCrU5FFIAy6X`o*NX&yz5U<9^>i|^5(nu;kr6(vNhJ5eTecbnCQJA<|q~%7v z%)&b*1Y3w-&oFHxQm?g|S%Ha9Ax1?o8q^c9TwN6NPie({L=hSfDlzhU;++M#z*>gk zFvtp+Q5M?auVLPr*#Qf1LBQV5qJVX9ali(6LBJ-sj07%Uv*EIEHsLb|FUv5hI8+xf zqg+}Wu(u`|umHCOtbuz1*1?AZ_KrUoun9iao!rxgmy+C4($w0L>Rs2>+nP+YZ)`o( zRGyIvd@7+;&}wLny2+AWcZ9lP%w3S_Q6B7tUb7&Np)c_xCX}O;4zkErtMqk!q3md^ zbm?+ePCjy_abswJ!u@rVZ(-GI4QvJ*J~}{7zx-3{N_N(jk_P4ELsweESRER4+4$%H zvJ1v^a~kIY?bR^r2An2iX1Hi!?HXJ;UZD;Tp)T1g(BJy+M?}^8vGv5#}29wiX zrhg#B>)-9;C*P0tbAeEKE-qM(x~?qzrKNwbpb*u)*!;Q7MTtF=m6<1jM~B1_)Qqd` z{BnhVj}|r(mEui!a@?Q&iJirGl^MLkbB(AEM!p&`=BUd?w)*00NON;oRE?Ph%C@3$ z!}a(6o1_Q%D4Xw+&oD>SK`W}RSrPTziZ~@MT%Je-Y&)uI?1i)&?C;4zovl4#Q*>R delta 1491 zcmZ{jUuYCZ9LImNvpajYdzW02i-}1T4~-2NVJ|81(x$RW^HQrONU$XcdQFT$OiisV zTH4$u_96B~a7rzqg}#&uw)7u}f)D9K9txo#1d6Q&9~A<9C=FHQ*6-ZS-qC~3GCQB& z_xET1?cQl#+gcz-L{A2LMhN2@M+Bs_?%`J`~i3`R>~4E7PKP?{zP*f_^Rf5@RB^vCq_I{PhaWn z(LO!q^w?h-v)anyG*xsK8!L~!I2XeH4fHfpEttw2Tb&s9XV z8Q#&q~UEc`(DAsZ=X_{qEB-&<%bM*eCL_}l-b z@acYvv!Ms?7+2Qs&s#>gN#1^PXz;hAkM{ zT)c@ka1l+;LF0h&t1_1>_u`3^T<3OlKe9!%S6JQKA>qnDxT`f&9v063*=AtV3Aq^W zl)v*jc_ZE?@9_i@O}P@!@kX{GkBMy+?V{+Au1I40Bmy}oT2$FoJFcQVAYUyKEn253 ze-UZwG+bRV*oJ_(VoQ!X&5>VDI&VG)Dl{wp diff --git a/Tests/ttLib/tables/V_A_R_C_test.py b/Tests/ttLib/tables/V_A_R_C_test.py index 9ddc107f1..dcd35da8e 100644 --- a/Tests/ttLib/tables/V_A_R_C_test.py +++ b/Tests/ttLib/tables/V_A_R_C_test.py @@ -9,7 +9,7 @@ DATA_DIR = os.path.join(CURR_DIR, "data") class VarCompositeTest(unittest.TestCase): - def test_trim_varComposite_glyph(self): + def test_basic(self): font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf") font = TTFont(font_path) varc = font["VARC"] @@ -18,10 +18,10 @@ class VarCompositeTest(unittest.TestCase): "uniAC00", "uniAC01", "glyph00003", - "glyph00004", "glyph00005", - "glyph00006", "glyph00007", + "glyph00008", + "glyph00009", ] font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf") @@ -31,11 +31,11 @@ class VarCompositeTest(unittest.TestCase): assert varc.table.Coverage.glyphs == [ "uni6868", "glyph00002", - "glyph00003", - "glyph00004", + "glyph00005", + "glyph00007", ] - def test_varComposite_basic(self): + def test_roundtrip(self): font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf") font = TTFont(font_path) tables = [ diff --git a/Tests/ttLib/ttGlyphSet_test.py b/Tests/ttLib/ttGlyphSet_test.py index c9c5150d2..d4bc5ad85 100644 --- a/Tests/ttLib/ttGlyphSet_test.py +++ b/Tests/ttLib/ttGlyphSet_test.py @@ -226,7 +226,7 @@ class TTGlyphSetTest(object): ( "addVarComponent", ( - "glyph00007", + "glyph00003", DecomposedTransform( translateX=0, translateY=0, @@ -244,7 +244,7 @@ class TTGlyphSetTest(object): ( "addVarComponent", ( - "glyph00003", + "glyph00005", DecomposedTransform( translateX=0, translateY=0, From d53c08a497d432ea3e494c70d24a4b493e79b99f Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Thu, 21 Mar 2024 17:25:14 -0600 Subject: [PATCH 109/114] [varc] Conditionals tested! --- Lib/fontTools/ttLib/ttGlyphSet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 9772ae813..8b276aa37 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -310,7 +310,7 @@ class _TTGlyphVARC(_TTGlyph): if condition.Format == 1: axisIndex = condition.AxisIndex axisTag = fvarAxes[axisIndex].axisTag - axisValue = self.glyphSet.location[axisTag] + axisValue = self.glyphSet.location.get(axisTag, 0) minValue = condition.FilterRangeMinValue maxValue = condition.FilterRangeMaxValue if not (minValue <= axisValue <= maxValue): From 9cb73dafeb78f5c72deccd51ff469801f26b9ca6 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 22 Mar 2024 10:46:22 -0600 Subject: [PATCH 110/114] [varc] Add a conditional VarComponent test --- Tests/ttLib/data/varc-ac01-conditional.ttf | Bin 0 -> 2692 bytes Tests/ttLib/ttGlyphSet_test.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 Tests/ttLib/data/varc-ac01-conditional.ttf diff --git a/Tests/ttLib/data/varc-ac01-conditional.ttf b/Tests/ttLib/data/varc-ac01-conditional.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5de72506d6b930b17a63096be220feff70467de4 GIT binary patch literal 2692 zcmZ`)du&uy8vo8Y_ugsC*caCaw3fcWx{I-|!PiJ`OG6riAyF5yy3w7K+7`=F+O7{Y z@lMYj3K@+L@R5iyF&d-z%>DzLWiwG+G2(1O$ZmarilP`tEC>S>X0E?;@0}K!_??_{ ze&6r=&iTH3&Ua>r5mA_I5@fCFxNX^$AAEN!k(eN2X3x6V5QX2o1NblSYQOjX*l=m% zZ5EN1f&Oma;6wLrd++BRu)hL*b06&WH$D0sk-i;1NBetYYf=Z-oPlj0`jUPS#!KQl z_-oJ```3*=7>e^L=-trG!Sy{c+Da>-KMB2ZUF^XjnnRV)o1oYJEw-+={kgxsOvD$1 z+cLC%WE6Qr#7<==^)2W;+4qlo%A%)8h@;B-$ijDRH6N}Y`p7-&zAYTl096pj7_1Ss z?)9LKx@XB0j>oDc1>qi3MCINZgeVvavx+31R~mL8{u;oY=*mxJXkt{6Owy=vT1fwZMLrwORr2@i z@BVlGt33^u55M!nW39`>BOksnSWTS27JRXw5VmTIG>Eo}2L8#eU5zVNup5@=uN8ks z>^p-9@X;&0-77>lcHO(Ef$wNtUY`52%FU!~kSD_z6msT;@9_wOihLi&aC@ZzRzC9r zEP$^JusVsl0PDaD18e|a9pDh~HR=SlVlYKG6(4ip8+?qz3sXmc8C^!V23Yy@1Xuvy z7hnx|B)~fG!vR*=%K(Rflfyl0??YkttnMA?>mOY?G&;}|8@zpBV{fxBEtp!-+tAz5 zBQ%VFYZ2&vig|c7_0j7*&3Tv7u_p9Rk^Gm8rcY&FWwn4 zhD&W$@6;2Y92lZ1 z+(4D>Uo+H{;*`uQaF(ZfLyhfCDU7OnGR;{5fvL$3ur=dX>+hF>MRiYtxT)#rVCT=o zP5E9#b+(6;-!wP9-vwN0$t4wYP1DR|GU@bzuYYyQGCxnAI!~r~-~biRzpA-aia)Jh zTICr0#N^rX8}E|K*hzMnJ~r?3WFvdK@;h^&kJfUgflC$5-PSMF9Ww(e8uIt8WGER4QvTA;_VDM{S)m@#?N~$jXX}3(` z88K%(6pcm~PEG9DBRA`Skws-r5^sw;TgO>6x<&kO>~zjCOk(+|6B%rQLUcD_DR<)J zN@kj4;{5SLX&J|E2|e@Uk?#&oNKq0JY+Oto`ywrE&II42ojUYs>R(bk{>-j#9V1^_ zUVQSCH>9plesg51#L>>3nkro&#g5nCKjK+KlmB^D>e>CfKl(c3Xus^=`^9P5nXAj~ z%YP|eri#c=anxrwjc6qfr zQNukkc{&_fCG~UV3#yE)qlsee_%~7<&rapXig4Y;&K>*z9(7ds;X%|2aq`;m2VlRwu)+C9j|g)x-&{P`jPQ-d^3nUo#uWA6v6 zkXP&3H1f-MMV`(`2Y1D##fuW?xWNh4;=wGFNy$-jj-y7IbZ8<`81f1=C6d1Z^>iUp literal 0 HcmV?d00001 diff --git a/Tests/ttLib/ttGlyphSet_test.py b/Tests/ttLib/ttGlyphSet_test.py index d4bc5ad85..7c95a847b 100644 --- a/Tests/ttLib/ttGlyphSet_test.py +++ b/Tests/ttLib/ttGlyphSet_test.py @@ -263,6 +263,21 @@ class TTGlyphSetTest(object): 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}) From 973dc5c9a7a20c540252a51a7da608cdf6f0d756 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 22 Apr 2024 14:30:30 -0600 Subject: [PATCH 111/114] [varc] Use Condition instead of ConditionSet With more Condition formats. https://github.com/harfbuzz/boring-expansion-spec/commit/df7dcc4618b9dc003740d25c53afe30a8fc03fe2 https://github.com/harfbuzz/boring-expansion-spec/issues/147 --- Lib/fontTools/subset/__init__.py | 16 ++--- Lib/fontTools/ttLib/tables/otData.py | 69 ++++++++++++++++++--- Lib/fontTools/ttLib/tables/otTables.py | 16 ++--- Lib/fontTools/ttLib/ttGlyphSet.py | 47 ++++++++------ Tests/ttLib/data/varc-ac01-conditional.ttf | Bin 2692 -> 2688 bytes 5 files changed, 106 insertions(+), 42 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index c924d574b..4aa60ad84 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -2693,21 +2693,21 @@ def prune_post_subset(self, font, options): if comp.axisIndicesIndex is not None: comp.axisIndicesIndex = mapping[comp.axisIndicesIndex] - conditionSetList = table.ConditionSetList - if conditionSetList is not None: - conditionSets = conditionSetList.ConditionSet + 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.conditionSetIndex is not None: - usedIndices.add(comp.conditionSetIndex) + if comp.conditionIndex is not None: + usedIndices.add(comp.conditionIndex) usedIndices = sorted(usedIndices) - conditionSetList.ConditionSet = _list_subset(conditionSets, 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.conditionSetIndex is not None: - comp.conditionSetIndex = mapping[comp.conditionSetIndex] + if comp.conditionIndex is not None: + comp.conditionIndex = mapping[comp.conditionIndex] return True diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index a2071b1f5..8e4800a97 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3169,21 +3169,21 @@ otData = [ ], ), ( - "ConditionSetList", + "ConditionList", [ ( "uint32", - "ConditionSetCount", + "ConditionCount", None, None, - "Number of condition-set tables in the ConditionSet array", + "Number of condition tables in the ConditionTable array", ), ( "LOffset", - "ConditionSet", - "ConditionSetCount", + "ConditionTable", + "ConditionCount", 0, - "Array of condition-set tables.", + "Array of offset to condition tables, from the beginning of the ConditionList table.", ), ], ), @@ -3202,7 +3202,7 @@ otData = [ "ConditionTable", "ConditionCount", 0, - "Array of condition tables.", + "Array of offset to condition tables, from the beginning of the ConditionSet table.", ), ], ), @@ -3233,6 +3233,59 @@ otData = [ ), ], ), + ( + "ConditionTableFormat2", + [ + ("uint16", "Format", None, None, "Format, = 2"), + ( + "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.", + ), + ], + ), + ( + "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 disjunction (OR) expression.", + ), + ], + ), + ( + "ConditionTableFormat4", + [ + ("uint16", "Format", None, None, "Format, = 4"), + ( + "Offset24", + "ConditionTable", + None, + None, + "Condition to negate.", + ), + ], + ), ( "FeatureTableSubstitution", [ @@ -3396,7 +3449,7 @@ otData = [ ), ("LOffset", "Coverage", None, None, ""), ("LOffset", "MultiVarStore", None, None, "(may be NULL)"), - ("LOffset", "ConditionSetList", None, None, "(may be NULL)"), + ("LOffset", "ConditionList", None, None, "(may be NULL)"), ("LOffset", "AxisIndicesList", None, None, "(may be NULL)"), ("LOffset", "VarCompositeGlyphs", None, None, ""), ], diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 15797fbed..bc7fbad91 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -158,7 +158,7 @@ class VarComponent: def populateDefaults(self, propagator=None): self.flags = 0 self.glyphName = None - self.conditionSetIndex = None + self.conditionIndex = None self.axisIndicesIndex = None self.axisValues = () self.axisValuesVarIndex = NO_VARIATION_INDEX @@ -176,7 +176,7 @@ class VarComponent: self.glyphName = font.glyphOrder[glyphID] if flags & VarComponentFlags.HAVE_CONDITION: - self.conditionSetIndex, i = _read_uint32var(data, i) + self.conditionIndex, i = _read_uint32var(data, i) if flags & VarComponentFlags.HAVE_AXES: self.axisIndicesIndex, i = _read_uint32var(data, i) @@ -248,9 +248,9 @@ class VarComponent: flags &= ~VarComponentFlags.GID_IS_24BIT data.append(_packer[2](glyphID)) - if self.conditionSetIndex is not None: + if self.conditionIndex is not None: flags |= VarComponentFlags.HAVE_CONDITION - data.append(_write_uint32var(self.conditionSetIndex)) + data.append(_write_uint32var(self.conditionIndex)) numAxes = len(self.axisValues) @@ -301,8 +301,8 @@ class VarComponent: write("glyphName", self.glyphName) - if self.conditionSetIndex is not None: - write("conditionSetIndex", self.conditionSetIndex) + if self.conditionIndex is not None: + write("conditionIndex", self.conditionIndex) if self.axisIndicesIndex is not None: write("axisIndicesIndex", self.axisIndicesIndex) if ( @@ -342,8 +342,8 @@ class VarComponent: if name == "glyphName": self.glyphName = v - elif name == "conditionSetIndex": - self.conditionSetIndex = safeEval(v) + elif name == "conditionIndex": + self.conditionIndex = safeEval(v) elif name == "axisIndicesIndex": self.axisIndicesIndex = safeEval(v) elif name == "axisValues": diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 8b276aa37..6267e7880 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -276,6 +276,33 @@ class _TTGlyphCFF(_TTGlyph): self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) +def _evaluateCondition(condition, fvarAxes, location): + if condition.Format == 1: + 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: + # ConditionAnd + for subcondition in condition.ConditionTable: + if not _evaluateCondition(subcondition, fvarAxes, location): + return False + return True + elif condition.Format == 3: + # ConditionOr + for subcondition in condition.ConditionTable: + if _evaluateCondition(subcondition, fvarAxes, location): + return True + return False + elif condition.Format == 4: + # ConditionNegate + return not _evaluateCondition(condition.conditionTable, fvarAxes, location) + 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 @@ -301,24 +328,8 @@ class _TTGlyphVARC(_TTGlyph): for comp in glyph.components: if comp.flags & VarComponentFlags.HAVE_CONDITION: - conditionSet = varc.ConditionSetList.ConditionSet[ - comp.conditionSetIndex - ] - # Evaluate condition - show = True - for condition in conditionSet.ConditionTable: - if condition.Format == 1: - axisIndex = condition.AxisIndex - axisTag = fvarAxes[axisIndex].axisTag - axisValue = self.glyphSet.location.get(axisTag, 0) - minValue = condition.FilterRangeMinValue - maxValue = condition.FilterRangeMaxValue - if not (minValue <= axisValue <= maxValue): - show = False - break - else: - show = False # Unkonwn condition format - if not show: + condition = varc.ConditionList.ConditionTable[comp.conditionIndex] + if not _evaluateCondition(condition, fvarAxes, self.glyphSet.location): continue location = {} diff --git a/Tests/ttLib/data/varc-ac01-conditional.ttf b/Tests/ttLib/data/varc-ac01-conditional.ttf index 5de72506d6b930b17a63096be220feff70467de4..912a146e259a679674aa70442c921f9eba61a20f 100644 GIT binary patch delta 112 zcmZn>Z4i}VU}Rum;9+QBU}kU#a}08>d0HpOz`%Tifq{{4qKq`-i-{KUyfYXWSYBkL zCZ-%@&-p(wr9j5{i;7@8zs*+$X66?_{R}m#8yZ0LBcFo8Pd^UXzuHxn)7dFL=Nu)N4f zO-wm(H|58~lmZ#1?)1&^{5D@1n3-Py^)u9{ZfF3}kCY0IZ~X9^iFGfKbAIv%=GjaP PY?Iqr5;nhNna&CT-L)dv From 77add05f7fbce736b1ad8ee834c17e227c034372 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 23 Apr 2024 08:51:01 +0200 Subject: [PATCH 112/114] Don't emit addVarComponent() if the component references the parent glyph (special case) --- Lib/fontTools/ttLib/ttGlyphSet.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 6267e7880..e4f035ad6 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -355,11 +355,17 @@ class _TTGlyphVARC(_TTGlyph): reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES with self.glyphSet.glyphSet.pushLocation(location, reset): with self.glyphSet.pushLocation(location, reset): - try: - pen.addVarComponent( - comp.glyphName, transform, self.glyphSet.rawLocation - ) - except AttributeError: + 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 From 973072b25932b5cb455558ab979bd09f070338fc Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 23 Apr 2024 12:57:14 -0600 Subject: [PATCH 113/114] [Condition] Implement ConditionValue Shift other Condition format numbers. Implements https://github.com/adobe-type-tools/opentype-spec-drafts/blob/main/condvalue_spec.md --- Lib/fontTools/ttLib/tables/otData.py | 38 +++++++++++++++++++++------- Lib/fontTools/ttLib/ttGlyphSet.py | 31 ++++++++++++++++++----- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 8e4800a97..3a01f5934 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -3238,18 +3238,18 @@ otData = [ [ ("uint16", "Format", None, None, "Format, = 2"), ( - "uint8", - "ConditionCount", + "int16", + "DefaultValue", None, None, - "Index for the variation axis within the fvar table, base 0.", + "Value at default instance.", ), ( - "Offset24", - "ConditionTable", - "ConditionCount", - 0, - "Array of condition tables for this conjunction (AND) expression.", + "uint32", + "VarIdx", + None, + None, + "Variation index to vary the value based on current designspace location.", ), ], ), @@ -3269,7 +3269,7 @@ otData = [ "ConditionTable", "ConditionCount", 0, - "Array of condition tables for this disjunction (OR) expression.", + "Array of condition tables for this conjunction (AND) expression.", ), ], ), @@ -3277,6 +3277,26 @@ otData = [ "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", diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index e4f035ad6..0c565bcea 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -276,8 +276,9 @@ class _TTGlyphCFF(_TTGlyph): self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) -def _evaluateCondition(condition, fvarAxes, location): +def _evaluateCondition(condition, fvarAxes, location, instancer): if condition.Format == 1: + # ConditionAxisRange axisIndex = condition.AxisIndex axisTag = fvarAxes[axisIndex].axisTag axisValue = location.get(axisTag, 0) @@ -285,20 +286,27 @@ def _evaluateCondition(condition, fvarAxes, location): maxValue = condition.FilterRangeMaxValue return minValue <= axisValue <= maxValue elif condition.Format == 2: + # ConditionValue + value = condition.DefaultValue + value += instancer[condition.VarIdx] + return value > 0 + elif condition.Format == 3: # ConditionAnd for subcondition in condition.ConditionTable: - if not _evaluateCondition(subcondition, fvarAxes, location): + if not _evaluateCondition(subcondition, fvarAxes, location, instancer): return False return True - elif condition.Format == 3: + elif condition.Format == 4: # ConditionOr for subcondition in condition.ConditionTable: - if _evaluateCondition(subcondition, fvarAxes, location): + if _evaluateCondition(subcondition, fvarAxes, location, instancer): return True return False - elif condition.Format == 4: + elif condition.Format == 5: # ConditionNegate - return not _evaluateCondition(condition.conditionTable, fvarAxes, location) + return not _evaluateCondition( + condition.conditionTable, fvarAxes, location, instancer + ) else: return False # Unkonwn condition format @@ -319,17 +327,26 @@ class _TTGlyphVARC(_TTGlyph): 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 ) + gdef = glyphSet.font.get("GDEF") if "GDEF" in glyphSet.font else None + gdefInstancer = VarStoreInstancer( + getattr(gdef.table, "VarStore") if gdef is not None else None, + 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): + if not _evaluateCondition( + condition, fvarAxes, self.glyphSet.location, gdefInstancer + ): continue location = {} From 753197e01783c3418aa7a0bb46e539fa9f20dd4f Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 23 Apr 2024 13:34:30 -0600 Subject: [PATCH 114/114] [varc] Use multiVarStore instead of GDEF varStore That's what we agreed upon. --- Lib/fontTools/ttLib/ttGlyphSet.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 0c565bcea..446c81e7d 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -288,7 +288,7 @@ def _evaluateCondition(condition, fvarAxes, location, instancer): elif condition.Format == 2: # ConditionValue value = condition.DefaultValue - value += instancer[condition.VarIdx] + value += instancer[condition.VarIdx][0] return value > 0 elif condition.Format == 3: # ConditionAnd @@ -333,19 +333,13 @@ class _TTGlyphVARC(_TTGlyph): instancer = MultiVarStoreInstancer( varc.MultiVarStore, fvarAxes, self.glyphSet.location ) - gdef = glyphSet.font.get("GDEF") if "GDEF" in glyphSet.font else None - gdefInstancer = VarStoreInstancer( - getattr(gdef.table, "VarStore") if gdef is not None else None, - 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, gdefInstancer + condition, fvarAxes, self.glyphSet.location, instancer ): continue