diff --git a/Lib/ufo2ft/constants.py b/Lib/ufo2ft/constants.py
index 6f2673949..da8c5a22b 100644
--- a/Lib/ufo2ft/constants.py
+++ b/Lib/ufo2ft/constants.py
@@ -1,7 +1,7 @@
from types import MappingProxyType
SPARSE_TTF_MASTER_TABLES = frozenset(
- ["glyf", "head", "hmtx", "loca", "maxp", "post", "vmtx"]
+ ["glyf", "head", "hmtx", "loca", "maxp", "post", "vmtx", "cvt ", "fpgm", "prep"]
)
SPARSE_OTF_MASTER_TABLES = frozenset(["CFF ", "VORG", "head", "hmtx", "maxp", "vmtx"])
@@ -35,9 +35,13 @@
# ]
COLR_CLIP_BOXES_KEY = UFO2FT_PREFIX + "colrClipBoxes"
+OBJECT_LIBS_KEY = "public.objectLibs"
OPENTYPE_CATEGORIES_KEY = "public.openTypeCategories"
OPENTYPE_META_KEY = "public.openTypeMeta"
-
+TRUETYPE_INSTRUCTIONS_KEY = "public.truetype.instructions"
+TRUETYPE_METRICS_KEY = "public.truetype.useMyMetrics"
+TRUETYPE_OVERLAP_KEY = "public.truetype.overlap"
+TRUETYPE_ROUND_KEY = "public.truetype.roundOffsetToGrid"
UNICODE_VARIATION_SEQUENCES_KEY = "public.unicodeVariationSequences"
COMMON_SCRIPT = "Zyyy"
diff --git a/Lib/ufo2ft/instructionCompiler.py b/Lib/ufo2ft/instructionCompiler.py
new file mode 100644
index 000000000..989a5159b
--- /dev/null
+++ b/Lib/ufo2ft/instructionCompiler.py
@@ -0,0 +1,324 @@
+from __future__ import annotations
+
+import array
+import logging
+from typing import TYPE_CHECKING, Optional
+
+from fontTools import ttLib
+from fontTools.pens.hashPointPen import HashPointPen
+from fontTools.ttLib import newTable
+from fontTools.ttLib.tables._g_l_y_f import (
+ OVERLAP_COMPOUND,
+ ROUND_XY_TO_GRID,
+ USE_MY_METRICS,
+)
+
+from ufo2ft.constants import (
+ OBJECT_LIBS_KEY,
+ TRUETYPE_INSTRUCTIONS_KEY,
+ TRUETYPE_METRICS_KEY,
+ TRUETYPE_OVERLAP_KEY,
+ TRUETYPE_ROUND_KEY,
+)
+from ufo2ft.fontInfoData import intListToNum
+
+if TYPE_CHECKING:
+ from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
+ from ufoLib2 import Font, Glyph
+
+
+logger = logging.getLogger(__name__)
+
+
+class InstructionCompiler:
+ def __init__(
+ self, ufo: Font, otf: ttLib.TTFont, autoUseMyMetrics: bool = True
+ ) -> None:
+ self.ufo = ufo
+ self.otf = otf
+ if not autoUseMyMetrics:
+ # If autoUseMyMetrics is False, replace the method with a no-op
+ self.autoUseMyMetrics = lambda ttGlyph, glyphName: None
+
+ def _check_glyph_hash(
+ self, glyphName: str, ttglyph: TTGlyph, glyph_hash: Optional[str]
+ ) -> bool:
+ """Check if the supplied glyph hash from the ufo matches the current outlines."""
+ if glyph_hash is None:
+ # The glyph hash is required
+ logger.error(
+ f"Glyph hash missing, glyph '{glyphName}' will have "
+ "no instructions in font."
+ )
+ return False
+
+ # Check the glyph hash against the TTGlyph that is being built
+
+ ttwidth = self.otf["hmtx"][glyphName][0]
+ hash_pen = HashPointPen(ttwidth, self.otf.getGlyphSet())
+ ttglyph.drawPoints(hash_pen, self.otf["glyf"])
+
+ if glyph_hash != hash_pen.hash:
+ logger.error(
+ f"The stored hash for glyph '{glyphName}' does not match the TrueType "
+ "output glyph. Glyph will have no instructions in the font."
+ )
+ return False
+ return True
+
+ @staticmethod
+ def _check_tt_data_format(ttdata: dict, name: str) -> None:
+ """Make sure we understand the format version, currently only version 1
+ is supported."""
+ formatVersion = ttdata.get("formatVersion", None)
+ if not isinstance(formatVersion, str):
+ raise TypeError(
+ f"Illegal type '{type(formatVersion).__name__}' instead of 'str' for "
+ f"formatVersion for instructions in {name}."
+ )
+ if formatVersion != "1":
+ raise NotImplementedError(
+ f"Unknown formatVersion {formatVersion} for instructions in {name}."
+ )
+
+ def _compile_program(self, key: str, table_tag: str) -> None:
+ """Compile the program for prep or fpgm."""
+ assert key in ("controlValueProgram", "fontProgram")
+ assert table_tag in ("prep", "fpgm")
+ ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
+ if ttdata:
+ self._check_tt_data_format(ttdata, f"lib key '{key}'")
+ asm = ttdata.get(key, None)
+ if asm is None:
+ # The optional key is not there, quit right here
+ return
+ if not asm:
+ # If assembly code is empty, don't bother to add the table
+ logger.debug(
+ f"Assembly for table '{table_tag}' is empty, "
+ "table not added to font."
+ )
+ return
+
+ self.otf[table_tag] = table = ttLib.newTable(table_tag)
+ table.program = ttLib.tables.ttProgram.Program()
+ table.program.fromAssembly(asm.splitlines())
+
+ def compileGlyphInstructions(self, ttGlyph, name) -> None:
+ """Compile the glyph instructions from the UFO glyph `name` to bytecode
+ and add it to `ttGlyph`."""
+ if name not in self.ufo:
+ # Skip glyphs that are not in the UFO, e.g. '.notdef'
+ logger.info(
+ f"Skipping compilation of instructions for glyph '{name}' because it "
+ "is not in the input UFO."
+ )
+ return
+
+ glyph = self.ufo[name]
+ ttdata = glyph.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
+ if ttdata is not None:
+ self._compile_tt_glyph_program(glyph, ttGlyph, ttdata)
+ if ttGlyph.isComposite():
+ self._set_composite_flags(glyph, ttGlyph)
+
+ def _compile_tt_glyph_program(
+ self, glyph: Glyph, ttglyph: TTGlyph, ttdata: dict
+ ) -> None:
+ self._check_tt_data_format(ttdata, f"glyph '{glyph.name}'")
+ glyph_hash = ttdata.get("id", None)
+ if not self._check_glyph_hash(glyph.name, ttglyph, glyph_hash):
+ return
+
+ # Compile the glyph program
+ asm = ttdata.get("assembly", None)
+ if asm is None:
+ # The "assembly" key is required.
+ logger.error(
+ f"Glyph assembly missing, glyph '{glyph.name}' will have "
+ "no instructions in font."
+ )
+ return
+
+ if not asm:
+ # If the assembly code is empty, don't bother adding a program
+ logger.debug(f"Glyph '{glyph.name}' has no instructions.")
+ return
+
+ ttglyph.program = ttLib.tables.ttProgram.Program()
+ ttglyph.program.fromAssembly(asm.splitlines())
+
+ def autoUseMyMetrics(self, ttGlyph, glyphName):
+ """Set the "USE_MY_METRICS" flag on the first component having the
+ same advance width as the composite glyph, no transform and no
+ horizontal shift (but allow it to shift vertically).
+ This forces the composite glyph to use the possibly hinted horizontal
+ metrics of the sub-glyph, instead of those from the "hmtx" table.
+ """
+ hmtx = self.otf["hmtx"]
+ width = hmtx[glyphName][0]
+ for component in ttGlyph.components:
+ try:
+ baseName, transform = component.getComponentInfo()
+ except AttributeError:
+ # component uses '{first,second}Pt' instead of 'x' and 'y'
+ continue
+ try:
+ baseMetrics = hmtx[baseName]
+ except KeyError:
+ continue # ignore missing components
+ else:
+ if baseMetrics[0] == width and transform[:-1] == (1, 0, 0, 1, 0):
+ component.flags |= USE_MY_METRICS
+ break
+
+ def _set_composite_flags(self, glyph: Glyph, ttglyph: TTGlyph) -> None:
+ # Set component flags
+
+ if len(ttglyph.components) != len(glyph.components):
+ # May happen if nested components have been flattened by a filter
+ logger.error(
+ "Number of components differ between UFO and TTF "
+ f"in glyph '{glyph.name}' ({len(glyph.components)} vs. "
+ f"{len(ttglyph.components)}, not setting component flags from"
+ "UFO. They may still be set heuristically."
+ )
+ self.autoUseMyMetrics(ttglyph, glyph.name)
+ return
+
+ # We need to decide when to set the flags.
+ # Let's assume if any lib key is not there, or the component
+ # doesn't have an identifier, we should leave the flags alone.
+
+ # Keep track of which component has the USE_MY_METRICS flag
+ # and whether any component lib contains the useMyMetrics key
+ use_my_metrics_comp = None
+ lib_contains_use_my_metrics_key = False
+
+ for i, c in enumerate(ttglyph.components):
+ # Set OVERLAP_COMPOUND on the first component only
+ if i == 0 and TRUETYPE_OVERLAP_KEY in glyph.lib:
+ if glyph.lib.get(TRUETYPE_OVERLAP_KEY, False):
+ c.flags |= OVERLAP_COMPOUND
+ else:
+ c.flags &= ~OVERLAP_COMPOUND
+
+ # Check if we have information about the current component in the glyph lib
+ ufo_component_id = glyph.components[i].identifier
+ if ufo_component_id is None:
+ # No information about component flags is stored in the UFO.
+ # We don’t modify the flags. Two flags have already been set elsewhere:
+ # - ROUND_XY_TO_GRID is set in TTGlyphPointPen.glyph() called from
+ # OutlineTTFCompiler.compileGlyphs()
+ # - USE_MY_METRICS is set in OutlineTTFCompiler.setupTable_glyf()
+ continue
+
+ if (
+ OBJECT_LIBS_KEY in glyph.lib
+ and ufo_component_id in glyph.lib[OBJECT_LIBS_KEY]
+ and (
+ TRUETYPE_ROUND_KEY in glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]
+ or TRUETYPE_METRICS_KEY
+ in glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]
+ )
+ ):
+ component_lib = glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]
+
+ # ROUND_XY_TO_GRID
+
+ # https://github.com/googlefonts/ufo2ft/pull/425 recommends
+ # to always set the ROUND_XY_TO_GRID flag, so we only
+ # unset it if explicitly done so in the lib
+ if not component_lib.get(TRUETYPE_ROUND_KEY, True):
+ c.flags &= ~ROUND_XY_TO_GRID
+
+ # USE_MY_METRICS
+ if component_lib.get(TRUETYPE_METRICS_KEY, False):
+ if use_my_metrics_comp is None:
+ c.flags |= USE_MY_METRICS
+ use_my_metrics_comp = ufo_component_id
+ else:
+ logger.warning(
+ f"Ignoring USE_MY_METRICS flag on component {i}, "
+ f"'{ufo_component_id}' because it has been set on "
+ f"component '{use_my_metrics_comp}' already "
+ f"in glyph {glyph.name}."
+ )
+ c.flags &= ~USE_MY_METRICS
+ else:
+ c.flags &= ~USE_MY_METRICS
+ lib_contains_use_my_metrics_key |= TRUETYPE_METRICS_KEY in component_lib
+
+ # If no UFO component has the 'public.truetype.useMyMetrics' key defined
+ # we try to automatically set it
+ if not lib_contains_use_my_metrics_key:
+ self.autoUseMyMetrics(ttglyph, glyph.name)
+
+ def update_maxp(self) -> None:
+ """Update the maxp table with relevant values from the UFO and compiled
+ font.
+ """
+ maxp = self.otf["maxp"]
+ ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
+ if ttdata:
+ for name in (
+ "maxStorage",
+ "maxFunctionDefs",
+ "maxInstructionDefs",
+ "maxStackElements",
+ # "maxSizeOfInstructions", # Is recalculated below
+ "maxZones",
+ "maxTwilightPoints",
+ ):
+ value = ttdata.get(name, None)
+ if value is not None:
+ setattr(maxp, name, value)
+
+ # Recalculate maxp.maxSizeOfInstructions
+ sizes = [
+ len(ttglyph.program.getBytecode())
+ for ttglyph in self.otf["glyf"].glyphs.values()
+ if hasattr(ttglyph, "program")
+ ]
+ maxp.maxSizeOfInstructions = max(sizes, default=0)
+
+ def setupTable_cvt(self) -> None:
+ """Make the cvt table."""
+ cvts = []
+ ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
+ if ttdata:
+ self._check_tt_data_format(ttdata, "key 'controlValue'")
+ cvt_dict = ttdata.get("controlValue", None)
+ if cvt_dict:
+ # Convert string keys to int
+ cvt_dict = {int(k): v for k, v in cvt_dict.items()}
+ # Find the maximum cvt index.
+ # We can't just use the dict keys because the cvt must be
+ # filled consecutively.
+ max_cvt = max(cvt_dict.keys())
+ # Make value list, filling entries for missing keys with 0
+ cvts = [cvt_dict.get(i, 0) for i in range(max_cvt + 1)]
+
+ if cvts:
+ # Only write cvt to font if it contains any values
+ self.otf["cvt "] = cvt = newTable("cvt ")
+ cvt.values = array.array("h", cvts)
+
+ def setupTable_fpgm(self) -> None:
+ self._compile_program("fontProgram", "fpgm")
+
+ def setupTable_gasp(self):
+ if not self.ufo.info.openTypeGaspRangeRecords:
+ return
+
+ self.otf["gasp"] = gasp = newTable("gasp")
+ gasp_ranges = dict()
+ for record in self.ufo.info.openTypeGaspRangeRecords:
+ rangeMaxPPEM = record["rangeMaxPPEM"]
+ behavior_bits = record["rangeGaspBehavior"]
+ rangeGaspBehavior = intListToNum(behavior_bits, 0, 4)
+ gasp_ranges[rangeMaxPPEM] = rangeGaspBehavior
+ gasp.gaspRange = gasp_ranges
+
+ def setupTable_prep(self) -> None:
+ self._compile_program("controlValueProgram", "prep")
diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py
index c371bfff9..16615a6bb 100644
--- a/Lib/ufo2ft/outlineCompiler.py
+++ b/Lib/ufo2ft/outlineCompiler.py
@@ -22,7 +22,7 @@
from fontTools.pens.ttGlyphPen import TTGlyphPointPen
from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.standardGlyphOrder import standardGlyphOrder
-from fontTools.ttLib.tables._g_l_y_f import USE_MY_METRICS, Glyph
+from fontTools.ttLib.tables._g_l_y_f import Glyph
from fontTools.ttLib.tables._h_e_a_d import mac_epoch_diff
from fontTools.ttLib.tables.O_S_2f_2 import Panose
@@ -41,10 +41,12 @@
intListToNum,
normalizeStringForPostscript,
)
+from ufo2ft.instructionCompiler import InstructionCompiler
from ufo2ft.util import (
_copyGlyph,
calcCodePageRanges,
colrClipBoxQuantization,
+ getMaxComponentDepth,
makeOfficialGlyphOrder,
makeUnicodeToGlyphNameMapping,
)
@@ -127,6 +129,7 @@ def __init__(
self._glyphBoundingBoxes = None
self._fontBoundingBox = None
self._compiledGlyphs = None
+ self._maxComponentDepths = None
def compile(self):
"""
@@ -290,19 +293,6 @@ def makeOfficialGlyphOrder(self, glyphOrder):
# Table Builders
# --------------
- def setupTable_gasp(self):
- if "gasp" not in self.tables:
- return
-
- self.otf["gasp"] = gasp = newTable("gasp")
- gasp_ranges = dict()
- for record in self.ufo.info.openTypeGaspRangeRecords:
- rangeMaxPPEM = record["rangeMaxPPEM"]
- behavior_bits = record["rangeGaspBehavior"]
- rangeGaspBehavior = intListToNum(behavior_bits, 0, 4)
- gasp_ranges[rangeMaxPPEM] = rangeGaspBehavior
- gasp.gaspRange = gasp_ranges
-
def setupTable_head(self):
"""
Make the head table.
@@ -1415,7 +1405,14 @@ class OutlineTTFCompiler(BaseOutlineCompiler):
"""Compile a .ttf font with TrueType outlines."""
sfntVersion = "\000\001\000\000"
- tables = BaseOutlineCompiler.tables | {"loca", "gasp", "glyf"}
+ tables = BaseOutlineCompiler.tables | {
+ "cvt ",
+ "fpgm",
+ "gasp",
+ "glyf",
+ "loca",
+ "prep",
+ }
def compileGlyphs(self):
"""Compile and return the TrueType glyphs for this font."""
@@ -1452,6 +1449,23 @@ def makeGlyphsBoundingBoxes(self):
glyphBoxes[glyphName] = bounds
return glyphBoxes
+ def getMaxComponentDepths(self):
+ """Collect glyphs max components depths.
+
+ Return a dictionary of non zero max components depth keyed by glyph names.
+ The max component depth of composite glyphs is 1 or more.
+ Simple glyphs are not keyed.
+ """
+ if self._maxComponentDepths:
+ return self._maxComponentDepths
+ maxComponentDepths = dict()
+ for name, glyph in self.allGlyphs.items():
+ depth = getMaxComponentDepth(glyph, self.allGlyphs)
+ if depth > 0:
+ maxComponentDepths[name] = depth
+ self._maxComponentDepths = maxComponentDepths
+ return self._maxComponentDepths
+
def setupTable_maxp(self):
"""Make the maxp table."""
if "maxp" not in self.tables:
@@ -1470,6 +1484,7 @@ def setupTable_maxp(self):
maxp.maxComponentElements = max(
len(g.components) for g in self.allGlyphs.values()
)
+ maxp.maxComponentDepth = max(self.getMaxComponentDepths().values(), default=0)
def setupTable_post(self):
"""Make a format 2 post table with the compiler's glyph order."""
@@ -1487,9 +1502,22 @@ def setupTable_post(self):
post.glyphOrder = self.glyphOrder
def setupOtherTables(self):
+ self.instructionCompiler = InstructionCompiler(
+ self.ufo, self.otf, autoUseMyMetrics=self.autoUseMyMetrics
+ )
+
self.setupTable_glyf()
- if self.ufo.info.openTypeGaspRangeRecords:
- self.setupTable_gasp()
+
+ if "cvt " in self.tables:
+ self.instructionCompiler.setupTable_cvt()
+ if "fpgm" in self.tables:
+ self.instructionCompiler.setupTable_fpgm()
+ if "gasp" in self.tables:
+ self.instructionCompiler.setupTable_gasp()
+ if "prep" in self.tables:
+ self.instructionCompiler.setupTable_prep()
+
+ self.instructionCompiler.update_maxp()
def setupTable_glyf(self):
"""Make the glyf table."""
@@ -1501,41 +1529,32 @@ def setupTable_glyf(self):
glyf.glyphs = {}
glyf.glyphOrder = self.glyphOrder
- hmtx = self.otf.get("hmtx")
ttGlyphs = self.getCompiledGlyphs()
- for name in self.glyphOrder:
+ # Sort the glyphs so that simple glyphs are compiled first, and composite
+ # glyphs are compiled later. Otherwise the glyph hashes may not be ready
+ # to calculate when a base glyph of a composite glyph is not in the font yet.
+ maxComponentDepths = self.getMaxComponentDepths()
+ for name in sorted(self.glyphOrder, key=lambda n: maxComponentDepths.get(n, 0)):
ttGlyph = ttGlyphs[name]
- if ttGlyph.isComposite() and hmtx is not None and self.autoUseMyMetrics:
- self.autoUseMyMetrics(ttGlyph, name, hmtx)
+ self.instructionCompiler.compileGlyphInstructions(ttGlyph, name)
glyf[name] = ttGlyph
# update various maxp fields based on glyf without needing to compile the font
if "maxp" in self.otf:
self.otf["maxp"].recalc(self.otf)
- @staticmethod
- def autoUseMyMetrics(ttGlyph, glyphName, hmtx):
- """Set the "USE_MY_METRICS" flag on the first component having the
- same advance width as the composite glyph, no transform and no
- horizontal shift (but allow it to shift vertically).
- This forces the composite glyph to use the possibly hinted horizontal
- metrics of the sub-glyph, instead of those from the "hmtx" table.
- """
- width = hmtx[glyphName][0]
- for component in ttGlyph.components:
- try:
- baseName, transform = component.getComponentInfo()
- except AttributeError:
- # component uses '{first,second}Pt' instead of 'x' and 'y'
- continue
- try:
- baseMetrics = hmtx[baseName]
- except KeyError:
- continue # ignore missing components
- else:
- if baseMetrics[0] == width and transform[:-1] == (1, 0, 0, 1, 0):
- component.flags |= USE_MY_METRICS
- break
+ # NOTE: the previous 'autoUseMyMetrics' method was moved to the InstructionCompiler
+ # This property setter is kept for backward compatibility to support the relatively
+ # obscure use-case (present in tests) of setting compiler.autoUseMyMetrics = None
+ # in order to disable the feature. It seems to me it's unlikely that one would like
+ # actually disable this at all...
+ @property
+ def autoUseMyMetrics(self) -> bool:
+ return getattr(self, "_autoUseMyMetrics", True)
+
+ @autoUseMyMetrics.setter
+ def autoUseMyMetrics(self, value):
+ self._autoUseMyMetrics = bool(value)
class StubGlyph:
diff --git a/tests/conftest.py b/tests/conftest.py
index 103618523..fbf93b55f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -102,6 +102,35 @@ def draw_rectangle(pen, x_offset, y_offset):
pen.lineTo((0 + x_offset, 10 + y_offset))
pen.closePath()
+ def add_cvt(font, index):
+ font.lib["public.truetype.instructions"] = {
+ "controlValue": {0: 0, 2: 30 + 10 * index, 3: 100 - index**2},
+ "formatVersion": "1",
+ "maxFunctionDefs": 1,
+ "maxInstructionDefs": 0,
+ "maxStackElements": 2,
+ "maxStorage": 0,
+ "maxTwilightPoints": 0,
+ "maxZones": 1,
+ }
+
+ def add_programs(font):
+ font.lib["public.truetype.instructions"][
+ "controlValueProgram"
+ ] = "PUSHB[ ]\n4 3\nINSTCTRL[ ]"
+ font.lib["public.truetype.instructions"][
+ "fontProgram"
+ ] = "PUSHB[ ]\n0\nFDEF[ ]\nPOP[ ]\nENDF[ ]"
+
+ def add_glyph_program(glyph, hash):
+ # The hash must be passed as an argument. We could probably calculate it here,
+ # but it must match the outline after it has been passed through cu2qu.
+ glyph.lib["public.truetype.instructions"] = {
+ "assembly": "PUSHB[ ]\n0 0\nSVTCA[0]\nMDRP[01100]",
+ "formatVersion": "1",
+ "id": hash,
+ }
+
def draw_something(glyph, number, is_sans):
# Ensure Sans and Serif sources are incompatible to make sure that the
# DS5 code treats them separately when using e.g. cu2qu. Use some number
@@ -122,9 +151,15 @@ def draw_something(glyph, number, is_sans):
if source.layerName is not None:
continue
font = FontClass()
+ add_cvt(font, index)
+ if index == 0:
+ # Add some instructions to the default source
+ add_programs(font)
for name in ("I", "S", "I.narrow", "S.closed", "a"):
glyph = font.newGlyph(name)
draw_something(glyph, index, "Serif" not in source.filename)
+ if index == 0:
+ add_glyph_program(font["a"], "w0l0+0l0+10l10+10l10+0|")
font.lib["public.glyphOrder"] = sorted(font.keys())
sources[source.filename] = font
diff --git a/tests/data/DSv5/MutatorSansVariable_Weight-TTF.ttx b/tests/data/DSv5/MutatorSansVariable_Weight-TTF.ttx
index a2d8d3d14..424f48e43 100644
--- a/tests/data/DSv5/MutatorSansVariable_Weight-TTF.ttx
+++ b/tests/data/DSv5/MutatorSansVariable_Weight-TTF.ttx
@@ -63,10 +63,10 @@
-
+
-
-
+
+
@@ -141,6 +141,31 @@
+
+
+ PUSHB[ ]
+ 0
+ FDEF[ ]
+ POP[ ]
+ ENDF[ ]
+
+
+
+
+
+ PUSHB[ ]
+ 4 3
+ INSTCTRL[ ]
+
+
+
+
+
+
+
+
+
+
@@ -213,7 +238,14 @@
-
+
+
+ PUSHB[ ]
+ 0 0
+ SVTCA[0]
+ MDRP[01100]
+
+
@@ -555,6 +587,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/DSv5/MutatorSansVariable_Weight_Width-TTF.ttx b/tests/data/DSv5/MutatorSansVariable_Weight_Width-TTF.ttx
index 3dee6e6dd..09a64efa3 100644
--- a/tests/data/DSv5/MutatorSansVariable_Weight_Width-TTF.ttx
+++ b/tests/data/DSv5/MutatorSansVariable_Weight_Width-TTF.ttx
@@ -63,10 +63,10 @@
-
+
-
-
+
+
@@ -141,6 +141,31 @@
+
+
+ PUSHB[ ]
+ 0
+ FDEF[ ]
+ POP[ ]
+ ENDF[ ]
+
+
+
+
+
+ PUSHB[ ]
+ 4 3
+ INSTCTRL[ ]
+
+
+
+
+
+
+
+
+
+
@@ -213,7 +238,14 @@
-
+
+
+ PUSHB[ ]
+ 0 0
+ SVTCA[0]
+ MDRP[01100]
+
+
@@ -822,6 +854,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/DSv5/MutatorSansVariable_Width-TTF.ttx b/tests/data/DSv5/MutatorSansVariable_Width-TTF.ttx
index 9e4a525d0..6d7354527 100644
--- a/tests/data/DSv5/MutatorSansVariable_Width-TTF.ttx
+++ b/tests/data/DSv5/MutatorSansVariable_Width-TTF.ttx
@@ -63,10 +63,10 @@
-
+
-
-
+
+
@@ -141,6 +141,31 @@
+
+
+ PUSHB[ ]
+ 0
+ FDEF[ ]
+ POP[ ]
+ ENDF[ ]
+
+
+
+
+
+ PUSHB[ ]
+ 4 3
+ INSTCTRL[ ]
+
+
+
+
+
+
+
+
+
+
@@ -213,7 +238,14 @@
-
+
+
+ PUSHB[ ]
+ 0 0
+ SVTCA[0]
+ MDRP[01100]
+
+
@@ -548,6 +580,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/DSv5/MutatorSerifVariable_Width-TTF.ttx b/tests/data/DSv5/MutatorSerifVariable_Width-TTF.ttx
index 090c5ebea..c911761d3 100644
--- a/tests/data/DSv5/MutatorSerifVariable_Width-TTF.ttx
+++ b/tests/data/DSv5/MutatorSerifVariable_Width-TTF.ttx
@@ -63,9 +63,9 @@
-
+
-
+
@@ -141,6 +141,13 @@
+
+
+
+
+
+
+
@@ -472,6 +479,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/instructionCompiler_test.py b/tests/instructionCompiler_test.py
new file mode 100644
index 000000000..e02f5101f
--- /dev/null
+++ b/tests/instructionCompiler_test.py
@@ -0,0 +1,771 @@
+import logging
+
+import pytest
+from cu2qu.ufo import font_to_quadratic
+from fontTools.pens.hashPointPen import HashPointPen
+from fontTools.ttLib.tables._g_l_y_f import (
+ OVERLAP_COMPOUND,
+ ROUND_XY_TO_GRID,
+ USE_MY_METRICS,
+)
+from fontTools.ttLib.ttFont import TTFont
+
+from ufo2ft.instructionCompiler import InstructionCompiler
+
+from .outlineCompiler_test import getpath
+
+TRUETYPE_INSTRUCTIONS_KEY = "public.truetype.instructions"
+
+
+def expect_maxp(
+ font,
+ maxStorage=0,
+ maxFunctionDefs=0,
+ maxInstructionDefs=0,
+ maxStackElements=0,
+ maxSizeOfInstructions=0,
+ maxZones=1,
+ maxTwilightPoints=0,
+):
+ maxp = font["maxp"]
+ assert maxp.maxStorage == maxStorage
+ assert maxp.maxFunctionDefs == maxFunctionDefs
+ assert maxp.maxInstructionDefs == maxInstructionDefs
+ assert maxp.maxStackElements == maxStackElements
+ assert maxp.maxSizeOfInstructions == maxSizeOfInstructions
+ assert maxp.maxZones == maxZones
+ assert maxp.maxTwilightPoints == maxTwilightPoints
+
+
+def get_hash_ufo(glyph, ufo):
+ hash_pen = HashPointPen(glyph.width, ufo)
+ glyph.drawPoints(hash_pen)
+ return hash_pen.hash
+
+
+def get_hash_ttf(glyph_name, ttf):
+ aw, _lsb = ttf["hmtx"][glyph_name]
+ gs = ttf.getGlyphSet()
+ hash_pen = HashPointPen(aw, gs)
+ ttf["glyf"][glyph_name].drawPoints(hash_pen, ttf["glyf"])
+ return hash_pen.hash
+
+
+@pytest.fixture
+def quadfont():
+ font = TTFont()
+ font.importXML(getpath("TestFont-TTF-post3.ttx"))
+ return font
+
+
+@pytest.fixture
+def quadufo(FontClass):
+ font = FontClass(getpath("TestFont.ufo"))
+ font_to_quadratic(font)
+ return font
+
+
+@pytest.fixture
+def quaduforeversed(FontClass):
+ font = FontClass(getpath("TestFont.ufo"))
+ font_to_quadratic(font=font, reverse_direction=True)
+ return font
+
+
+@pytest.fixture
+def testufo(FontClass):
+ font = FontClass(getpath("TestFont.ufo"))
+ del font.lib["public.postscriptNames"]
+ return font
+
+
+class InstructionCompilerTest:
+ # _check_glyph_hash
+
+ def test_check_glyph_hash_match(self, quaduforeversed, quadfont):
+ glyph = quaduforeversed["a"]
+ ufo_hash = get_hash_ufo(glyph, quaduforeversed)
+ ttglyph = quadfont["glyf"]["a"]
+
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ result = ic._check_glyph_hash(glyph.name, ttglyph, ufo_hash)
+ assert result
+
+ def test_check_glyph_hash_missing(self, quaduforeversed, quadfont):
+ glyph = quaduforeversed["a"]
+
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ result = ic._check_glyph_hash(
+ glyph.name,
+ quadfont["glyf"]["a"],
+ None,
+ )
+ assert not result
+
+ def test_check_glyph_hash_mismatch(self, testufo, quadfont):
+ glyph = testufo["a"]
+ ufo_hash = get_hash_ufo(glyph, testufo)
+ ttglyph = quadfont["glyf"]["a"]
+
+ # The contour direction is reversed in testufo vs. quadfont, so the
+ # hash should not match
+
+ ic = InstructionCompiler(testufo, quadfont)
+ result = ic._check_glyph_hash(
+ glyph.name,
+ ttglyph,
+ ufo_hash,
+ )
+ assert not result
+
+ def test_check_glyph_hash_mismatch_composite(self, testufo, quadfont):
+ glyph = testufo["h"]
+ ufo_hash = get_hash_ufo(glyph, testufo)
+ ttglyph = quadfont["glyf"]["h"]
+
+ # The contour direction is reversed in testufo vs. quadfont, so the
+ # hash should not match
+
+ ic = InstructionCompiler(testufo, quadfont)
+ result = ic._check_glyph_hash(
+ glyph.name,
+ ttglyph,
+ ufo_hash,
+ )
+ assert not result
+
+ def test_check_glyph_hash_mismatch_width(self, quaduforeversed, quadfont):
+ glyph = quaduforeversed["a"]
+
+ # Modify the glyph width in the UFO to trigger the mismatch
+ glyph.width += 10
+
+ ufo_hash = get_hash_ufo(glyph, quaduforeversed)
+ ttglyph = quadfont["glyf"]["a"]
+
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ result = ic._check_glyph_hash(
+ glyph.name,
+ ttglyph,
+ ufo_hash,
+ )
+ assert not result
+
+ # _check_tt_data_format
+
+ def test_check_tt_data_format_match_str(self):
+ result = InstructionCompiler._check_tt_data_format(
+ ttdata={"formatVersion": "1"},
+ name="",
+ )
+ assert result is None
+
+ def test_check_tt_data_format_type_error(self):
+ with pytest.raises(
+ TypeError,
+ match=(
+ "Illegal type 'int' instead of 'str' for formatVersion "
+ "for instructions in location."
+ ),
+ ):
+ InstructionCompiler._check_tt_data_format(
+ ttdata={"formatVersion": 1}, # Spec requires a str
+ name="location",
+ )
+
+ def test_check_tt_data_format_mismatch_str(self):
+ with pytest.raises(
+ NotImplementedError,
+ match="Unknown formatVersion 1.5 for instructions in location.",
+ ):
+ InstructionCompiler._check_tt_data_format(
+ ttdata={"formatVersion": "1.5"}, # Maps to the correct int
+ name="location",
+ )
+
+ # _compile_program
+
+ def test_compile_program_no_ttdata(self, quadufo):
+ # UFO contains no "public.truetype.instructions" lib key
+ ic = InstructionCompiler(quadufo, TTFont())
+ for key, tag in (
+ ("controlValueProgram", "prep"),
+ ("fontProgram", "fpgm"),
+ ):
+ ic._compile_program(key=key, table_tag=tag)
+ assert "fpgm" not in ic.otf
+ assert "prep" not in ic.otf
+
+ def test_compile_program_no_programs(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are not there. (They are optional)
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ }
+ for key, tag in (
+ ("controlValueProgram", "prep"),
+ ("fontProgram", "fpgm"),
+ ):
+ ic._compile_program(key=key, table_tag=tag)
+ assert "fpgm" not in ic.otf
+ assert "prep" not in ic.otf
+
+ def test_compile_program_none(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are None.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValueProgram": None,
+ "fontProgram": None,
+ }
+ for key, tag in (
+ ("controlValueProgram", "prep"),
+ ("fontProgram", "fpgm"),
+ ):
+ ic._compile_program(key=key, table_tag=tag)
+ assert "fpgm" not in ic.otf
+ assert "prep" not in ic.otf
+
+ def test_compile_program_empty(self, quadufo, caplog):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are empty.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValueProgram": "",
+ "fontProgram": "",
+ }
+ with caplog.at_level(logging.DEBUG, logger="ufo2ft.instructionCompiler"):
+ for key, tag in (
+ ("controlValueProgram", "prep"),
+ ("fontProgram", "fpgm"),
+ ):
+ ic._compile_program(key=key, table_tag=tag)
+ assert (
+ "Assembly for table 'fpgm' is empty, table not added to font."
+ in caplog.text
+ )
+ assert (
+ "Assembly for table 'prep' is empty, table not added to font."
+ in caplog.text
+ )
+ assert "fpgm" not in ic.otf
+ assert "prep" not in ic.otf
+
+ def test_compile_program(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, and the font and
+ # control value programs are present.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValueProgram": "PUSHW[]\n511\nSCANCTRL[]",
+ "fontProgram": "PUSHB[]\n0\nFDEF[]\nPOP[]\nENDF[]",
+ }
+ for key, tag in (
+ ("controlValueProgram", "prep"),
+ ("fontProgram", "fpgm"),
+ ):
+ ic._compile_program(key=key, table_tag=tag)
+
+ assert "fpgm" in ic.otf
+ assert "prep" in ic.otf
+
+ # Check if the bytecode is correct, though this may be out of scope
+ assert ic.otf["fpgm"].program.getBytecode() == b"\xb0\x00\x2C\x21\x2D"
+ assert ic.otf["prep"].program.getBytecode() == b"\xb8\x01\xff\x85"
+
+ # compileGlyphInstructions
+
+ def test_compileGlyphInstructions_missing_glyph(self, caplog):
+ # The method logs an info when trying to compile a glyph which is
+ # missing in the UFO, e.g. '.notdef'
+ ic = InstructionCompiler(dict(), None)
+ with caplog.at_level(logging.INFO, logger="ufo2ft.instructionCompiler"):
+ ic.compileGlyphInstructions(None, "A")
+ assert "Skipping compilation of instructions for glyph 'A'" in caplog.text
+
+ # _compile_tt_glyph_program
+
+ def test_compile_tt_glyph_program_empty(self, quaduforeversed, quadfont):
+ # UFO glyph contains no "public.truetype.instructions" lib key
+ with pytest.raises(
+ TypeError,
+ match=(
+ "Illegal type 'NoneType' instead of 'str' for formatVersion "
+ "for instructions in glyph 'a'."
+ ),
+ ):
+ InstructionCompiler(quaduforeversed, quadfont)._compile_tt_glyph_program(
+ glyph=quaduforeversed["a"],
+ ttglyph=quadfont["glyf"]["a"],
+ ttdata={},
+ )
+
+ def test_compile_tt_glyph_program_no_asm(self, quaduforeversed, quadfont, caplog):
+ # UFO glyph contains "public.truetype.instructions" lib key, but no
+ # assembly code entry
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+
+ assert not ic.otf["glyf"]["a"].isComposite()
+
+ glyph = ic.ufo["a"]
+ glyph_hash = get_hash_ufo(glyph, ic.ufo)
+
+ with caplog.at_level(logging.ERROR, logger="ufo2ft.instructionCompiler"):
+ ic._compile_tt_glyph_program(
+ glyph=ic.ufo["a"],
+ ttglyph=ic.otf["glyf"]["a"],
+ ttdata={
+ "formatVersion": "1",
+ "id": glyph_hash,
+ # "assembly": "",
+ },
+ )
+ assert (
+ "Glyph assembly missing, glyph 'a' will have no instructions in font."
+ in caplog.text
+ )
+
+ def test_compile_tt_glyph_program_empty_asm(
+ self, quaduforeversed, quadfont, caplog
+ ):
+ # UFO glyph contains "public.truetype.instructions" lib key, but the
+ # assembly code entry is empty
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+
+ assert not ic.otf["glyf"]["a"].isComposite()
+
+ glyph = ic.ufo["a"]
+ glyph_hash = get_hash_ufo(glyph, ic.ufo)
+
+ with caplog.at_level(logging.DEBUG, logger="ufo2ft.instructionCompiler"):
+ ic._compile_tt_glyph_program(
+ glyph=ic.ufo["a"],
+ ttglyph=ic.otf["glyf"]["a"],
+ ttdata={
+ "formatVersion": "1",
+ "id": glyph_hash,
+ "assembly": "",
+ },
+ )
+ assert "Glyph 'a' has no instructions." in caplog.text
+ assert not hasattr(ic.otf["glyf"]["h"], "program")
+
+ def test_compile_tt_glyph_program_empty_asm_composite(
+ self, quaduforeversed, quadfont
+ ):
+ # UFO glyph contains "public.truetype.instructions" lib key, but the
+ # assembly code entry is empty. The glyph is a composite.
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+
+ glyph = ic.ufo["h"]
+ glyph_hash = get_hash_ufo(glyph, ic.ufo)
+
+ assert ic.otf["glyf"]["h"].isComposite()
+
+ ic._compile_tt_glyph_program(
+ glyph=ic.ufo["h"],
+ ttglyph=ic.otf["glyf"]["h"],
+ ttdata={
+ "formatVersion": "1",
+ "id": glyph_hash,
+ "assembly": "",
+ },
+ )
+ # Components must not have an empty program
+ assert not hasattr(ic.otf["glyf"]["h"], "program")
+
+ def test_compile_tt_glyph_program(self, quaduforeversed, quadfont):
+ # UFO glyph contains "public.truetype.instructions" lib key, and the
+ # assembly code entry is present.
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+
+ assert not ic.otf["glyf"]["a"].isComposite()
+
+ glyph = ic.ufo["a"]
+ glyph_hash = get_hash_ufo(glyph, ic.ufo)
+
+ ic._compile_tt_glyph_program(
+ glyph=ic.ufo["a"],
+ ttglyph=ic.otf["glyf"]["a"],
+ ttdata={
+ "formatVersion": "1",
+ "id": glyph_hash,
+ "assembly": "PUSHB[]\n0\nMDAP[1]",
+ },
+ )
+ assert ic.otf["glyf"]["a"].program.getBytecode() == b"\xb0\x00\x2f"
+
+ def test_compile_tt_glyph_program_composite(self, quaduforeversed, quadfont):
+ # UFO glyph contains "public.truetype.instructions" lib key, and the
+ # assembly code entry is present. The glyph is a composite.
+ name = "k" # Name of the composite glyph
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+
+ assert ic.otf["glyf"][name].isComposite()
+
+ glyph_hash = get_hash_ufo(ic.ufo[name], ic.ufo)
+
+ ic._compile_tt_glyph_program(
+ glyph=ic.ufo[name],
+ ttglyph=ic.otf["glyf"][name],
+ ttdata={
+ "formatVersion": "1",
+ "id": glyph_hash,
+ "assembly": "PUSHB[]\n0\nMDAP[1]",
+ },
+ )
+ ttglyph = ic.otf["glyf"][name]
+ assert hasattr(ttglyph, "program")
+ assert ttglyph.program.getBytecode() == b"\xb0\x00\x2f"
+
+ # _set_composite_flags
+
+ def test_set_composite_flags_no_ttdata(self, quadufo, quadfont):
+ name = "h" # Name of the composite glyph
+ ic = InstructionCompiler(quadufo, quadfont)
+
+ glyph = quadufo[name]
+ ttglyph = quadfont["glyf"][name]
+
+ ic._set_composite_flags(
+ glyph=glyph,
+ ttglyph=ttglyph,
+ )
+
+ # Flags have been set by heuristics
+ assert not ttglyph.components[0].flags & OVERLAP_COMPOUND
+ assert ttglyph.components[0].flags & ROUND_XY_TO_GRID
+ assert not ttglyph.components[0].flags & USE_MY_METRICS
+ assert not ttglyph.components[1].flags & OVERLAP_COMPOUND
+ assert ttglyph.components[1].flags & ROUND_XY_TO_GRID
+ assert ttglyph.components[1].flags & USE_MY_METRICS
+
+ def test_set_composite_flags_compound(self, quadufo, quadfont):
+ name = "k" # Name of the composite glyph
+ ic = InstructionCompiler(quadufo, quadfont)
+
+ glyph = quadufo[name]
+ glyph.components[0].identifier = "component0"
+ glyph.components[1].identifier = "component1"
+ glyph.lib = {"public.truetype.overlap": True}
+ ttglyph = quadfont["glyf"][name]
+
+ ic._set_composite_flags(
+ glyph=glyph,
+ ttglyph=ttglyph,
+ )
+ # The OVERLAP_COMPOUND flag is only set on 1st component
+ assert ttglyph.components[0].flags & OVERLAP_COMPOUND
+ assert not ttglyph.components[1].flags & OVERLAP_COMPOUND
+
+ def test_set_composite_flags_no_compound(self, quadufo, quadfont):
+ name = "k" # Name of the composite glyph
+ ic = InstructionCompiler(quadufo, quadfont)
+
+ glyph = quadufo[name]
+ glyph.components[0].identifier = "component0"
+ glyph.components[1].identifier = "component1"
+ glyph.lib = {"public.truetype.overlap": False}
+ ttglyph = quadfont["glyf"][name]
+
+ ic._set_composite_flags(
+ glyph=glyph,
+ ttglyph=ttglyph,
+ )
+ assert not ttglyph.components[0].flags & OVERLAP_COMPOUND
+ assert not ttglyph.components[1].flags & OVERLAP_COMPOUND
+
+ def test_set_composite_flags(self, quadufo, quadfont):
+ name = "h" # Name of the composite glyph
+ ic = InstructionCompiler(quadufo, quadfont)
+
+ glyph = quadufo[name]
+ glyph.components[0].identifier = "component0"
+ glyph.components[1].identifier = "component1"
+ glyph.lib = {
+ "public.objectLibs": {
+ "component0": {
+ "public.truetype.roundOffsetToGrid": False,
+ "public.truetype.useMyMetrics": False,
+ },
+ "component1": {
+ "public.truetype.roundOffsetToGrid": True,
+ "public.truetype.useMyMetrics": True,
+ },
+ },
+ }
+ ttglyph = quadfont["glyf"][name]
+
+ ic._set_composite_flags(
+ glyph=glyph,
+ ttglyph=ttglyph,
+ )
+
+ assert not ttglyph.components[0].flags & OVERLAP_COMPOUND
+ assert not ttglyph.components[0].flags & ROUND_XY_TO_GRID
+ assert not ttglyph.components[0].flags & USE_MY_METRICS
+
+ assert not ttglyph.components[1].flags & OVERLAP_COMPOUND
+ assert ttglyph.components[1].flags & ROUND_XY_TO_GRID
+ assert ttglyph.components[1].flags & USE_MY_METRICS
+
+ def test_set_composite_flags_metrics_first_only(self, quadufo, quadfont):
+ name = "h" # Name of the composite glyph
+ ic = InstructionCompiler(quadufo, quadfont)
+
+ glyph = quadufo[name]
+ glyph.components[0].identifier = "component0"
+ glyph.components[1].identifier = "component1"
+ glyph.lib = {
+ "public.objectLibs": {
+ "component0": {
+ "public.truetype.useMyMetrics": True,
+ },
+ "component1": {
+ "public.truetype.useMyMetrics": True,
+ },
+ },
+ }
+ ttglyph = quadfont["glyf"][name]
+
+ ic._set_composite_flags(
+ glyph=glyph,
+ ttglyph=ttglyph,
+ )
+
+ # Flag on component 1 should have been ignored
+ assert ttglyph.components[0].flags & USE_MY_METRICS
+ assert not ttglyph.components[1].flags & USE_MY_METRICS
+
+ def test_set_composite_flags_metrics_no_id(self, quadufo, quadfont):
+ name = "h" # Name of the composite glyph
+ ic = InstructionCompiler(quadufo, quadfont)
+
+ glyph = quadufo[name]
+ # First component has no identifier
+ glyph.components[0].identifier = None
+ glyph.components[1].identifier = "component1"
+ glyph.lib = {
+ "public.objectLibs": {
+ "component1": {
+ "public.truetype.useMyMetrics": False,
+ },
+ },
+ }
+ ttglyph = quadfont["glyf"][name]
+
+ ic._set_composite_flags(
+ glyph=glyph,
+ ttglyph=ttglyph,
+ )
+
+ # Flag on both components should have been unset
+ assert not ttglyph.components[0].flags & USE_MY_METRICS
+ assert not ttglyph.components[1].flags & USE_MY_METRICS
+
+ # update_maxp
+
+ def test_update_maxp_no_ttdata(self, quaduforeversed, quadfont):
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+
+ ic.update_maxp()
+ expect_maxp(ic.otf)
+
+ def test_update_maxp(self, quaduforeversed, quadfont):
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "maxStorage": 1,
+ "maxFunctionDefs": 1,
+ "maxInstructionDefs": 1,
+ "maxStackElements": 1,
+ "maxSizeOfInstructions": 1,
+ "maxZones": 2,
+ "maxTwilightPoints": 1,
+ }
+ # Make a glyph program of size 3 in "a"
+ self.test_compile_tt_glyph_program(quaduforeversed, quadfont)
+ ic.update_maxp()
+ # maxSizeOfInstructions should be 3 because it is calculated from the font
+ expect_maxp(ic.otf, 1, 1, 1, 1, 3, 2, 1)
+
+ # setupTable_cvt
+
+ def test_setupTable_cvt(self, quaduforeversed, quadfont):
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValue": {
+ "1": 500,
+ "2": 750,
+ "3": -250,
+ },
+ }
+ ic.setupTable_cvt()
+ assert "cvt " in ic.otf
+ assert list(ic.otf["cvt "].values) == [0, 500, 750, -250]
+
+ def test_setupTable_cvt_empty(self, quaduforeversed, quadfont):
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValue": {},
+ }
+ ic.setupTable_cvt()
+ assert "cvt " not in ic.otf
+
+ def test_setupTable_cvt_none(self, quaduforeversed, quadfont):
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValue": None,
+ }
+ ic.setupTable_cvt()
+ assert "cvt " not in ic.otf
+
+ def test_setupTable_cvt_missing(self, quaduforeversed, quadfont):
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ }
+ ic.setupTable_cvt()
+ assert "cvt " not in ic.otf
+
+ def test_setupTable_cvt_no_ttdata(self, quaduforeversed, quadfont):
+ ic = InstructionCompiler(quaduforeversed, quadfont)
+ ic.setupTable_cvt()
+ assert "cvt " not in ic.otf
+
+ # setupTable_fpgm
+
+ def test_setupTable_fpgm_no_ttdata(self, quadufo):
+ # UFO contains no "public.truetype.instructions" lib key
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.setupTable_fpgm()
+ assert "fpgm" not in ic.otf
+
+ def test_setupTable_fpgm_no_program(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are not there. (They are optional)
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ }
+ ic.setupTable_fpgm()
+ assert "fpgm" not in ic.otf
+
+ def test_setupTable_fpgm_none(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are None.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "fontProgram": None,
+ }
+ ic.setupTable_fpgm()
+ assert "fpgm" not in ic.otf
+
+ def test_setupTable_fpgm_empty(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are empty.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "fontProgram": "",
+ }
+ ic.setupTable_fpgm()
+ assert "fpgm" not in ic.otf
+
+ def test_setupTable_fpgm(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, and the font and
+ # control value programs are present.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "fontProgram": "PUSHB[]\n0\nFDEF[]\nPOP[]\nENDF[]",
+ }
+ ic.setupTable_fpgm()
+
+ assert "fpgm" in ic.otf
+
+ # Check if the bytecode is correct, though this may be out of scope
+ assert ic.otf["fpgm"].program.getBytecode() == b"\xb0\x00\x2C\x21\x2D"
+
+ # setupTable_gasp
+
+ def test_setupTable_gasp(self, testufo):
+ ic = InstructionCompiler(testufo, TTFont())
+ ic.setupTable_gasp()
+ assert "gasp" in ic.otf
+ assert ic.otf["gasp"].gaspRange == {7: 10, 65535: 15}
+
+ def test_compile_without_gasp(self, testufo):
+ testufo.info.openTypeGaspRangeRecords = None
+ ic = InstructionCompiler(testufo, TTFont())
+ ic.setupTable_gasp()
+ assert "gasp" not in ic.otf
+
+ def test_compile_empty_gasp(self, testufo):
+ # ignore empty gasp
+ testufo.info.openTypeGaspRangeRecords = []
+ ic = InstructionCompiler(testufo, TTFont())
+ ic.setupTable_gasp()
+ assert "gasp" not in ic.otf
+
+ # setupTable_prep
+
+ def test_setupTable_prep_no_ttdata(self, quadufo):
+ # UFO contains no "public.truetype.instructions" lib key
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.setupTable_prep()
+ assert "prep" not in ic.otf
+
+ def test_setupTable_prep_no_program(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are not there. (They are optional)
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ }
+ ic.setupTable_prep()
+ assert "prep" not in ic.otf
+
+ def test_setupTable_prep_none(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are None.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValueProgram": None,
+ }
+ ic.setupTable_prep()
+ assert "prep" not in ic.otf
+
+ def test_setupTable_prep_empty(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, but the font and
+ # control value programs are empty.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValueProgram": "",
+ }
+ ic.setupTable_prep()
+ assert "prep" not in ic.otf
+
+ def test_setupTable_prep(self, quadufo):
+ # UFO contains the "public.truetype.instructions" lib key, and the font and
+ # control value programs are present.
+ ic = InstructionCompiler(quadufo, TTFont())
+ ic.ufo.lib[TRUETYPE_INSTRUCTIONS_KEY] = {
+ "formatVersion": "1",
+ "controlValueProgram": "PUSHW[]\n511\nSCANCTRL[]",
+ }
+ ic.setupTable_prep()
+
+ assert "prep" in ic.otf
+
+ # Check if the bytecode is correct, though this may be out of scope
+ assert ic.otf["prep"].program.getBytecode() == b"\xb8\x01\xff\x85"
diff --git a/tests/outlineCompiler_test.py b/tests/outlineCompiler_test.py
index 4dd5f0d09..bb357c34e 100644
--- a/tests/outlineCompiler_test.py
+++ b/tests/outlineCompiler_test.py
@@ -44,6 +44,12 @@ def quadufo(FontClass):
return font
+@pytest.fixture
+def nestedcomponentsufo(FontClass):
+ font = FontClass(getpath("NestedComponents-Regular.ufo"))
+ return font
+
+
@pytest.fixture
def use_my_metrics_ufo(FontClass):
return FontClass(getpath("UseMyMetrics.ufo"))
@@ -63,13 +69,6 @@ def emptyufo(FontClass):
class OutlineTTFCompilerTest:
- def test_setupTable_gasp(self, testufo):
- compiler = OutlineTTFCompiler(testufo)
- compiler.otf = TTFont()
- compiler.setupTable_gasp()
- assert "gasp" in compiler.otf
- assert compiler.otf["gasp"].gaspRange == {7: 10, 65535: 15}
-
def test_compile_with_gasp(self, testufo):
compiler = OutlineTTFCompiler(testufo)
compiler.compile()
@@ -97,6 +96,14 @@ def test_makeGlyphsBoundingBoxes(self, quadufo):
# float coordinates are rounded, so is the bbox
assert compiler.glyphBoundingBoxes["d"] == (90, 77, 211, 197)
+ def test_getMaxComponentDepths(self, nestedcomponentsufo):
+ compiler = OutlineTTFCompiler(nestedcomponentsufo)
+ assert "a" not in compiler.getMaxComponentDepths()
+ assert "b" not in compiler.getMaxComponentDepths()
+ assert compiler.getMaxComponentDepths()["c"] == 1
+ assert compiler.getMaxComponentDepths()["d"] == 1
+ assert compiler.getMaxComponentDepths()["e"] == 2
+
def test_autoUseMyMetrics(self, use_my_metrics_ufo):
compiler = OutlineTTFCompiler(use_my_metrics_ufo)
ttf = compiler.compile()