diff --git a/Lib/ufo2ft/__init__.py b/Lib/ufo2ft/__init__.py index 89f4ba4a..5f9e61d7 100644 --- a/Lib/ufo2ft/__init__.py +++ b/Lib/ufo2ft/__init__.py @@ -18,6 +18,11 @@ "compileVariableCFF2s", ] +try: + from ._version import version as __version__ +except ImportError: + __version__ = "0.0.0+unknown" + def compileTTF(ufo, **kwargs): """Create FontTools TrueType font from a UFO. @@ -168,6 +173,30 @@ def compileVariableTTFs(designSpaceDoc, **kwargs): def compileInterpolatableTTFsFromDS(designSpaceDoc, **kwargs): + """Create FontTools TrueType fonts from the DesignSpaceDocument UFO sources + with interpolatable outlines. Cubic curves are converted compatibly to + quadratic curves using the Cu2Qu conversion algorithm. + + If the Designspace contains a "public.skipExportGlyphs" lib key, these + glyphs will not be exported to the final font. If these glyphs are used as + components in any other glyph, those components get decomposed. If the lib + key doesn't exist in the Designspace, all glyphs are exported (keys in + individual UFOs are ignored). UFO groups and kerning will be pruned of + skipped glyphs. + + The DesignSpaceDocument should contain SourceDescriptor objects with 'font' + attribute set to an already loaded defcon.Font object (or compatible UFO + Font class). If 'font' attribute is unset or None, an AttributeError exception + is thrown. + + Return a copy of the DesignSpaceDocument object (or the same one if + inplace=True) with the source's 'font' attribute set to the corresponding + TTFont instance. + + For sources that have the 'layerName' attribute defined, the corresponding TTFont + object will contain only a minimum set of tables ("head", "hmtx", "glyf", "loca", + "maxp", "post" and "vmtx"), and no OpenType layout tables. + """ return InterpolatableTTFCompiler(**kwargs).compile_designspace(designSpaceDoc) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index b2704f8f..4175cd0f 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -4,13 +4,19 @@ from dataclasses import dataclass from typing import Callable, Optional, Type +from fontTools import varLib from fontTools.designspaceLib.split import splitInterpolable, splitVariableFonts from fontTools.misc.loggingTools import Timer from fontTools.otlLib.optimize.gpos import GPOS_COMPACT_MODE_ENV_KEY from ufo2ft.constants import MTI_FEATURES_PREFIX from ufo2ft.errors import InvalidDesignSpaceData -from ufo2ft.featureCompiler import FeatureCompiler, MtiFeatureCompiler +from ufo2ft.featureCompiler import ( + FeatureCompiler, + MtiFeatureCompiler, + VariableFeatureCompiler, + _featuresCompatible, +) from ufo2ft.postProcessor import PostProcessor from ufo2ft.util import ( _notdefGlyphFallback, @@ -51,6 +57,7 @@ def compile(self, ufo): with self.timer("preprocess UFO"): glyphSet = self.preprocess(ufo) with self.timer("compile a basic TTF"): + self.logger.info("Building OpenType tables") font = self.compileOutlines(ufo, glyphSet) if self.layerName is None and not self.skipFeatureCompilation: self.compileFeatures(ufo, font, glyphSet=glyphSet) @@ -169,8 +176,13 @@ class BaseInterpolatableCompiler(BaseCompiler): "maxp", "post" and "vmtx"), and no OpenType layout tables. """ + def compile_designspace(self, designSpaceDoc): + ufos = self._pre_compile_designspace(designSpaceDoc) + ttfs = self.compile(ufos) + return self._post_compile_designspace(designSpaceDoc, ttfs) + def _pre_compile_designspace(self, designSpaceDoc): - ufos, self.layerNames = [], [] + ufos, self.glyphSets, self.layerNames = [], [], [] for source in designSpaceDoc.sources: if source.font is None: raise AttributeError( @@ -246,7 +258,16 @@ def _compileNeededSources(self, designSpaceDoc): if source.name in sourcesToCompile: sourcesByName[source.name] = source + # If the feature files are compatible between the sources, we can save + # time by building a variable feature file right at the end. + can_optimize_features = self.variableFeatures and _featuresCompatible( + designSpaceDoc + ) + if can_optimize_features: + self.logger.info("Features are compatible across masters; building later") + originalSources = {} + originalGlyphsets = {} # Compile all needed sources in each interpolable subspace to make sure # they're all compatible; that also ensures that sub-vfs within the same @@ -265,6 +286,7 @@ def _compileNeededSources(self, designSpaceDoc): self.useProductionNames = False save_postprocessor = self.postProcessorClass self.postProcessorClass = None + self.skipFeatureCompilation = can_optimize_features try: ttfDesignSpace = self.compile_designspace(subDoc) finally: @@ -274,10 +296,18 @@ def _compileNeededSources(self, designSpaceDoc): self.useProductionNames = save_production_names # Stick TTFs back into original big DS - for ttfSource in ttfDesignSpace.sources: + for ttfSource, glyphSet in zip(ttfDesignSpace.sources, self.glyphSets): + if can_optimize_features: + originalSources[ttfSource.name] = sourcesByName[ttfSource.name].font sourcesByName[ttfSource.name].font = ttfSource.font + originalGlyphsets[ttfSource.name] = glyphSet - return vfNameToBaseUfo, originalSources + return ( + vfNameToBaseUfo, + can_optimize_features, + originalSources, + originalGlyphsets, + ) def compile_variable(self, designSpaceDoc): if not self.inplace: @@ -285,22 +315,77 @@ def compile_variable(self, designSpaceDoc): ( vfNameToBaseUfo, + buildVariableFeatures, originalSources, + originalGlyphsets, ) = self._compileNeededSources(designSpaceDoc) if not vfNameToBaseUfo: return {} - self.logger.info("Building variable TTF fonts: %s", ", ".join(vfNameToBaseUfo)) + vfNames = list(vfNameToBaseUfo.keys()) + self.logger.info( + "Building variable font%s: %s", + "s" if len(vfNames) > 1 else "", + ", ".join(vfNames), + ) excludeVariationTables = self.excludeVariationTables + if buildVariableFeatures: + # Skip generating feature variations in varLib; we are handling + # the feature variations as part of compiling variable features, + # which we'll do later, so we don't need to produce them here. + excludeVariationTables = set(excludeVariationTables) | {"GSUB"} with self.timer("merge fonts to variable"): vfNameToTTFont = self._merge(designSpaceDoc, excludeVariationTables) + if buildVariableFeatures: + self.compile_all_variable_features( + designSpaceDoc, vfNameToTTFont, originalSources, originalGlyphsets + ) for vfName, varfont in list(vfNameToTTFont.items()): vfNameToTTFont[vfName] = self.postprocess( varfont, vfNameToBaseUfo[vfName], glyphSet=None ) return vfNameToTTFont + + def compile_all_variable_features( + self, + designSpaceDoc, + vfNameToTTFont, + originalSources, + originalGlyphsets, + debugFeatureFile=False, + ): + interpolableSubDocs = [ + subDoc for _location, subDoc in splitInterpolable(designSpaceDoc) + ] + for subDoc in interpolableSubDocs: + for vfName, vfDoc in splitVariableFonts(subDoc): + if vfName not in vfNameToTTFont: + continue + ttFont = vfNameToTTFont[vfName] + # vfDoc is now full of TTFs, create a UFO-sourced equivalent + ufoDoc = vfDoc.deepcopyExceptFonts() + for ttfSource, ufoSource in zip(vfDoc.sources, ufoDoc.sources): + ufoSource.font = originalSources[ttfSource.name] + defaultGlyphset = originalGlyphsets[ufoDoc.findDefault().name] + self.logger.info(f"Compiling variable features for {vfName}") + self.compile_variable_features(ufoDoc, ttFont, defaultGlyphset) + + def compile_variable_features(self, designSpaceDoc, ttFont, glyphSet): + default_ufo = designSpaceDoc.findDefault().font + + featureCompiler = VariableFeatureCompiler( + default_ufo, designSpaceDoc, ttFont=ttFont, glyphSet=glyphSet + ) + featureCompiler.compile() + + if self.debugFeatureFile: + if hasattr(featureCompiler, "writeFeatures"): + featureCompiler.writeFeatures(self.debugFeatureFile) + + # Add back feature variations, as the code above would overwrite them. + varLib.addGSUBFeatureVariations(ttFont, designSpaceDoc) diff --git a/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py b/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py index c9c668e1..9ebdde9d 100644 --- a/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py +++ b/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py @@ -7,6 +7,7 @@ from ufo2ft.constants import SPARSE_OTF_MASTER_TABLES, CFFOptimization from ufo2ft.outlineCompiler import OutlineOTFCompiler from ufo2ft.preProcessor import OTFPreProcessor +from ufo2ft.util import prune_unknown_kwargs from .baseCompiler import BaseInterpolatableCompiler from .otfCompiler import OTFCompiler @@ -28,10 +29,13 @@ class InterpolatableOTFCompiler(OTFCompiler, BaseInterpolatableCompiler): skipFeatureCompilation: bool = False excludeVariationTables: tuple = () - def compile_designspace(self, designSpaceDoc): - self._pre_compile_designspace(designSpaceDoc) + # We can't use the same compile method as interpolatableTTFCompiler + # because that has a TTFInterpolatablePreProcessor which preprocesses + # all UFOs together, whereas we need to do the preprocessing one at + # at a time. + def compile(self, ufos): otfs = [] - for source in designSpaceDoc.sources: + for ufo, layerName in zip(ufos, self.layerNames): # There's a Python bug where dataclasses.asdict() doesn't work with # dataclasses that contain a defaultdict. save_extraSubstitutions = self.extraSubstitutions @@ -39,20 +43,20 @@ def compile_designspace(self, designSpaceDoc): args = { **dataclasses.asdict(self), **dict( - layerName=source.layerName, + layerName=layerName, removeOverlaps=False, overlapsBackend=None, optimizeCFF=CFFOptimization.NONE, - _tables=SPARSE_OTF_MASTER_TABLES if source.layerName else None, + _tables=SPARSE_OTF_MASTER_TABLES if layerName else None, ), } # Remove interpolatable-specific args - del args["variableFontNames"] - del args["excludeVariationTables"] + args = prune_unknown_kwargs(args, OTFCompiler) compiler = OTFCompiler(**args) self.extraSubstitutions = save_extraSubstitutions - otfs.append(compiler.compile(source.font)) - return self._post_compile_designspace(designSpaceDoc, otfs) + otfs.append(compiler.compile(ufo)) + self.glyphSets.append(compiler._glyphSet) + return otfs def _merge(self, designSpaceDoc, excludeVariationTables): return varLib.build_many( diff --git a/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py b/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py index a15e2e07..a5d8b0fb 100644 --- a/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py +++ b/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py @@ -31,9 +31,9 @@ def compile(self, ufos): if self.layerNames is None: self.layerNames = [None] * len(ufos) assert len(ufos) == len(self.layerNames) - glyphSets = self.preprocess(ufos) + self.glyphSets = self.preprocess(ufos) - for ufo, glyphSet, layerName in zip(ufos, glyphSets, self.layerNames): + for ufo, glyphSet, layerName in zip(ufos, self.glyphSets, self.layerNames): yield self.compile_one(ufo, glyphSet, layerName) def compile_one(self, ufo, glyphSet, layerName): @@ -78,11 +78,6 @@ def compileOutlines(self, ufo, glyphSet, layerName=None): outlineCompiler = self.outlineCompilerClass(ufo, glyphSet=glyphSet, **kwargs) return outlineCompiler.compile() - def compile_designspace(self, designSpaceDoc): - ufos = self._pre_compile_designspace(designSpaceDoc) - ttfs = self.compile(ufos) - return self._post_compile_designspace(designSpaceDoc, ttfs) - def _merge(self, designSpaceDoc, excludeVariationTables): return varLib.build_many( designSpaceDoc, diff --git a/Lib/ufo2ft/_compilers/otfCompiler.py b/Lib/ufo2ft/_compilers/otfCompiler.py index 0e3acfb8..6c76bce7 100644 --- a/Lib/ufo2ft/_compilers/otfCompiler.py +++ b/Lib/ufo2ft/_compilers/otfCompiler.py @@ -33,4 +33,5 @@ def postprocess(self, font, ufo, glyphSet): kwargs = prune_unknown_kwargs(self.__dict__, postProcessor.process) kwargs["optimizeCFF"] = self.optimizeCFF >= CFFOptimization.SUBROUTINIZE font = postProcessor.process(**kwargs) + self._glyphSet = glyphSet return font diff --git a/Lib/ufo2ft/_compilers/ttfCompiler.py b/Lib/ufo2ft/_compilers/ttfCompiler.py index a5c32f4c..05bd2653 100644 --- a/Lib/ufo2ft/_compilers/ttfCompiler.py +++ b/Lib/ufo2ft/_compilers/ttfCompiler.py @@ -21,7 +21,7 @@ class TTFCompiler(BaseCompiler): dropImpliedOnCurves: bool = False allQuadratic: bool = True - def compileOutlines(self, ufo, glyphSet, layerName=None): + def compileOutlines(self, ufo, glyphSet): kwargs = prune_unknown_kwargs(self.__dict__, self.outlineCompilerClass) kwargs["glyphDataFormat"] = 0 if self.allQuadratic else 1 outlineCompiler = self.outlineCompilerClass(ufo, glyphSet=glyphSet, **kwargs) diff --git a/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py b/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py index 83c8044e..b79cb543 100644 --- a/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py +++ b/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py @@ -17,3 +17,4 @@ class VariableCFF2sCompiler(InterpolatableOTFCompiler): cffVersion: int = 2 optimizeCFF: CFFOptimization = CFFOptimization.SPECIALIZE excludeVariationTables: tuple = () + variableFeatures: bool = True diff --git a/Lib/ufo2ft/_compilers/variableTTFsCompiler.py b/Lib/ufo2ft/_compilers/variableTTFsCompiler.py index 50ca7ac9..e9e75b7c 100644 --- a/Lib/ufo2ft/_compilers/variableTTFsCompiler.py +++ b/Lib/ufo2ft/_compilers/variableTTFsCompiler.py @@ -21,4 +21,4 @@ class VariableTTFsCompiler(InterpolatableTTFCompiler): autoUseMyMetrics: bool = True dropImpliedOnCurves: bool = False allQuadratic: bool = True - pass + variableFeatures: bool = True diff --git a/Lib/ufo2ft/featureCompiler.py b/Lib/ufo2ft/featureCompiler.py index 788e21da..e1c6bc98 100644 --- a/Lib/ufo2ft/featureCompiler.py +++ b/Lib/ufo2ft/featureCompiler.py @@ -9,6 +9,7 @@ from tempfile import NamedTemporaryFile from fontTools import mtiLib +from fontTools.designspaceLib import DesignSpaceDocument, SourceDescriptor from fontTools.feaLib.builder import addOpenTypeFeaturesFromString from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound from fontTools.feaLib.parser import Parser @@ -102,6 +103,10 @@ def __init__(self, ufo, ttFont=None, glyphSet=None, extraSubstitutions=None): glyphOrder = ttFont.getGlyphOrder() if glyphSet is not None: + if set(glyphOrder) != set(glyphSet.keys()): + print("Glyph order incompatible") + print("In UFO but not in font:", set(glyphSet.keys()) - set(glyphOrder)) + print("In font but not in UFO:", set(glyphOrder) - set(glyphSet.keys())) assert set(glyphOrder) == set(glyphSet.keys()) else: glyphSet = ufo @@ -407,3 +412,56 @@ def warn_about_miscased_insertion_markers( text, pattern_case.pattern, ) + + +class VariableFeatureCompiler(FeatureCompiler): + """Generate a variable feature file and compile OpenType tables from a + designspace file. + """ + + def __init__( + self, + ufo, + designspace, + ttFont=None, + glyphSet=None, + featureWriters=None, + **kwargs, + ): + self.designspace = designspace + super().__init__(ufo, ttFont, glyphSet, featureWriters, **kwargs) + + def setupFeatures(self): + if self.featureWriters: + featureFile = parseLayoutFeatures(self.ufo) + + for writer in self.featureWriters: + writer.write(self.designspace, featureFile, compiler=self) + + # stringify AST to get correct line numbers in error messages + self.features = featureFile.asFea() + else: + # no featureWriters, simply read existing features' text + self.features = self.ufo.features.text or "" + + +def _featuresCompatible(designSpaceDoc: DesignSpaceDocument) -> bool: + """Returns whether the features of the individual source UFOs are the same. + + NOTE: Only compares the feature file text inside the source UFO and does not + follow imports. This will suffice as long as no external feature file is + using variable syntax and all sources are stored n the same parent folder + (so the same includes point to the same files). + """ + + assert all(hasattr(source.font, "features") for source in designSpaceDoc.sources) + + def transform(f: SourceDescriptor) -> str: + # Strip comments + text = re.sub("(?m)#.*$", "", f.font.features.text or "") + # Strip extraneous whitespace + text = re.sub(r"\s+", " ", text) + return text + + first = transform(designSpaceDoc.sources[0]) + return all(transform(s) == first for s in designSpaceDoc.sources[1:]) diff --git a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py index b69f834e..32a15e4b 100644 --- a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py @@ -2,10 +2,19 @@ from collections import OrderedDict, namedtuple from types import SimpleNamespace +from fontTools.designspaceLib import DesignSpaceDocument +from fontTools.feaLib.variableScalar import VariableScalar +from fontTools.misc.fixedTools import otRound + from ufo2ft.constants import OPENTYPE_CATEGORIES_KEY from ufo2ft.errors import InvalidFeaturesData from ufo2ft.featureWriters import ast -from ufo2ft.util import unicodeScriptExtensions +from ufo2ft.util import ( + collapse_varscalar, + get_userspace_location, + quantize, + unicodeScriptExtensions, +) INSERT_FEATURE_MARKER = r"\s*# Automatic Code.*" @@ -107,6 +116,7 @@ def setContext(self, font, feaFile, compiler=None): todo=todo, insertComments=insertComments, existingFeatures=existing, + isVariable=isinstance(font, DesignSpaceDocument), ) return self.context @@ -312,6 +322,8 @@ def compileGSUB(self): from ufo2ft.util import compileGSUB compiler = self.context.compiler + fvar = None + feafile = self.context.feaFile if compiler is not None: # The result is cached in the compiler instance, so if another # writer requests one it is not compiled again. @@ -319,12 +331,13 @@ def compileGSUB(self): return compiler._gsub glyphOrder = compiler.ttFont.getGlyphOrder() + fvar = compiler.ttFont.get("fvar") else: # the 'real' glyph order doesn't matter because the table is not # compiled to binary, only the glyph names are used glyphOrder = sorted(self.context.font.keys()) - gsub = compileGSUB(self.context.feaFile, glyphOrder) + gsub = compileGSUB(feafile, glyphOrder, fvar=fvar) if compiler and not hasattr(compiler, "_gsub"): compiler._gsub = gsub @@ -347,6 +360,10 @@ def getOpenTypeCategories(self): set(), ) openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {}) + # Handle case where we are a variable feature writer + if not openTypeCategories and isinstance(font, DesignSpaceDocument): + font = font.sources[0].font + openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {}) for glyphName, category in openTypeCategories.items(): if category == "unassigned": @@ -414,6 +431,10 @@ def guessFontScripts(self): feaFile = self.context.feaFile single_scripts = set() + # If we're dealing with a Designspace, look at the default source. + if hasattr(font, "findDefault"): + font = font.findDefault().font + # First, detect scripts from the codepoints. for glyph in font: if glyph.name not in glyphSet or glyph.unicodes is None: @@ -428,3 +449,41 @@ def guessFontScripts(self): single_scripts.update(feaScripts.keys()) return single_scripts + + def _getAnchor(self, glyphName, anchorName, anchor=None): + if self.context.isVariable: + designspace = self.context.font + x_value = VariableScalar() + y_value = VariableScalar() + found = False + for source in designspace.sources: + if glyphName not in source.font: + return None + glyph = source.font[glyphName] + for anchor in glyph.anchors: + if anchor.name == anchorName: + location = get_userspace_location(designspace, source.location) + x_value.add_value(location, otRound(anchor.x)) + y_value.add_value(location, otRound(anchor.y)) + found = True + if not found: + return None + x, y = collapse_varscalar(x_value), collapse_varscalar(y_value) + else: + if anchor is None: + if glyphName not in self.context.font: + return None + glyph = self.context.font[glyphName] + anchors = [ + anchor for anchor in glyph.anchors if anchor.name == anchorName + ] + if not anchors: + return None + anchor = anchors[0] + + x = anchor.x + y = anchor.y + if hasattr(self.options, "quantization"): + x = quantize(x, self.options.quantization) + y = quantize(y, self.options.quantization) + return x, y diff --git a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py index 020b4d8a..70f4ae4d 100644 --- a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py @@ -1,7 +1,5 @@ -from fontTools.misc.fixedTools import otRound - from ufo2ft.featureWriters import BaseFeatureWriter, ast -from ufo2ft.util import classifyGlyphs, unicodeScriptDirection +from ufo2ft.util import classifyGlyphs, otRoundIgnoringVariable, unicodeScriptDirection class CursFeatureWriter(BaseFeatureWriter): @@ -90,19 +88,28 @@ def _makeCursiveLookup(self, glyphs, direction=None): return lookup + def _getAnchors(self, glyphName, glyph=None): + entryAnchor = None + exitAnchor = None + entryAnchorXY = self._getAnchor(glyphName, "entry") + exitAnchorXY = self._getAnchor(glyphName, "exit") + if entryAnchorXY: + entryAnchor = ast.Anchor( + x=otRoundIgnoringVariable(entryAnchorXY[0]), + y=otRoundIgnoringVariable(entryAnchorXY[1]), + ) + if exitAnchorXY: + exitAnchor = ast.Anchor( + x=otRoundIgnoringVariable(exitAnchorXY[0]), + y=otRoundIgnoringVariable(exitAnchorXY[1]), + ) + return entryAnchor, exitAnchor + def _makeCursiveStatements(self, glyphs): cursiveAnchors = dict() statements = [] for glyph in glyphs: - entryAnchor = exitAnchor = None - for anchor in glyph.anchors: - if entryAnchor and exitAnchor: - break - if anchor.name == "entry": - entryAnchor = ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)) - elif anchor.name == "exit": - exitAnchor = ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)) - + entryAnchor, exitAnchor = self._getAnchors(glyph.name, glyph) # A glyph can have only one of the cursive anchors (e.g. if it # attaches on one side only) if entryAnchor or exitAnchor: diff --git a/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py b/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py index 14527590..fcd41f19 100644 --- a/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py @@ -1,3 +1,5 @@ +from fontTools.misc.fixedTools import otRound + from ufo2ft.featureWriters import BaseFeatureWriter, ast @@ -55,16 +57,21 @@ def _getLigatureCarets(self): and anchor.name.startswith("caret_") and anchor.x is not None ): - glyphCarets.add(round(anchor.x)) + glyphCarets.add(self._getAnchor(glyphName, anchor.name)[0]) elif ( anchor.name and anchor.name.startswith("vcaret_") and anchor.y is not None ): - glyphCarets.add(round(anchor.y)) + glyphCarets.add(self._getAnchor(glyphName, anchor.name)[1]) if glyphCarets: - carets[glyphName] = sorted(glyphCarets) + if self.context.isVariable: + carets[glyphName] = sorted( + glyphCarets, key=lambda caret: list(caret.values.values())[0] + ) + else: + carets[glyphName] = [otRound(c) for c in sorted(glyphCarets)] return carets diff --git a/Lib/ufo2ft/featureWriters/kernFeatureWriter.py b/Lib/ufo2ft/featureWriters/kernFeatureWriter.py index 0ce7c7e8..0713bb8f 100644 --- a/Lib/ufo2ft/featureWriters/kernFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/kernFeatureWriter.py @@ -4,9 +4,13 @@ import logging from dataclasses import dataclass from types import SimpleNamespace -from typing import Iterator, Mapping +from typing import Any, Iterator, Mapping from fontTools import unicodedata +from fontTools.designspaceLib import DesignSpaceDocument +from fontTools.feaLib.variableScalar import Location as VariableScalarLocation +from fontTools.feaLib.variableScalar import VariableScalar +from fontTools.ufoLib.kerning import lookupKerningValue from fontTools.unicodedata import script_horizontal_direction from ufo2ft.constants import COMMON_SCRIPT, INDIC_SCRIPTS, USE_SCRIPTS @@ -14,7 +18,9 @@ from ufo2ft.util import ( DFLT_SCRIPTS, classifyGlyphs, + collapse_varscalar, describe_ufo, + get_userspace_location, quantize, unicodeScriptExtensions, ) @@ -60,7 +66,7 @@ class KerningPair: side1: str | tuple[str, ...] side2: str | tuple[str, ...] - value: float + value: float | VariableScalar def __lt__(self, other: KerningPair) -> bool: if not isinstance(other, KerningPair): @@ -205,20 +211,25 @@ def setContext(self, font, feaFile, compiler=None): ctx.glyphSet = self.getOrderedGlyphSet() # Unless we use the legacy append mode (which ignores insertion - # markers), if the font contains kerning and the feaFile contains `kern` - # or `dist` feature blocks, but we have no insertion markers (or they - # were misspelt and ignored), warn the user that the kerning blocks in - # the feaFile take precedence and other kerning is dropped. + # markers), if the font (Designspace: default source) contains kerning + # and the feaFile contains `kern` or `dist` feature blocks, but we have + # no insertion markers (or they were misspelt and ignored), warn the + # user that the kerning blocks in the feaFile take precedence and other + # kerning is dropped. + if hasattr(font, "findDefault"): + default_source = font.findDefault().font + else: + default_source = font if ( self.mode == "skip" - and font.kerning + and default_source.kerning and ctx.existingFeatures & self.features and not ctx.insertComments ): LOGGER.warning( "%s: font has kerning, but also manually written kerning features " "without an insertion comment. Dropping the former.", - describe_ufo(font), + describe_ufo(default_source), ) # Remember which languages are defined for which OT tag, as all @@ -299,41 +310,96 @@ def getKerningData(self): side1Classes={}, side2Classes={}, classDefs={}, pairs=pairs ) - def getKerningGroups(self): - font = self.context.font + def getKerningGroups( + self, + ) -> tuple[Mapping[str, tuple[str, ...]], Mapping[str, tuple[str, ...]]]: allGlyphs = self.context.glyphSet - side1Groups = {} - side2Groups = {} - side1Membership = {} - side2Membership = {} - for name, members in font.groups.items(): - # prune non-existent or skipped glyphs - members = {g for g in members if g in allGlyphs} - if not members: + + side1Groups: dict[str, tuple[str, ...]] = {} + side1Membership: dict[str, str] = {} + side2Groups: dict[str, tuple[str, ...]] = {} + side2Membership: dict[str, str] = {} + + if isinstance(self.context.font, DesignSpaceDocument): + fonts = [source.font for source in self.context.font.sources] + else: + fonts = [self.context.font] + + for font in fonts: + assert font is not None + for name, members in font.groups.items(): + # prune non-existent or skipped glyphs + members = {g for g in members if g in allGlyphs} # skip empty groups - continue - # skip groups without UFO3 public.kern{1,2} prefix - if name.startswith(SIDE1_PREFIX): - side1Groups[name] = tuple(sorted(members)) - name_truncated = name[len(SIDE1_PREFIX) :] - for member in members: - side1Membership[member] = name_truncated - elif name.startswith(SIDE2_PREFIX): - side2Groups[name] = tuple(sorted(members)) - name_truncated = name[len(SIDE2_PREFIX) :] - for member in members: - side2Membership[member] = name_truncated + if not members: + continue + # skip groups without UFO3 public.kern{1,2} prefix + if name.startswith(SIDE1_PREFIX): + name_truncated = name[len(SIDE1_PREFIX) :] + known_members = members.intersection(side1Membership.keys()) + if known_members: + for glyph_name in known_members: + original_name_truncated = side1Membership[glyph_name] + if name_truncated != original_name_truncated: + log_regrouped_glyph( + "first", + name, + original_name_truncated, + font, + glyph_name, + ) + # Skip the whole group definition if there is any + # overlap problem. + continue + group = side1Groups.get(name) + if group is None: + side1Groups[name] = tuple(sorted(members)) + for member in members: + side1Membership[member] = name_truncated + elif set(group) != members: + log_redefined_group("left", name, group, font, members) + elif name.startswith(SIDE2_PREFIX): + name_truncated = name[len(SIDE2_PREFIX) :] + known_members = members.intersection(side2Membership.keys()) + if known_members: + for glyph_name in known_members: + original_name_truncated = side2Membership[glyph_name] + if name_truncated != original_name_truncated: + log_regrouped_glyph( + "second", + name, + original_name_truncated, + font, + glyph_name, + ) + # Skip the whole group definition if there is any + # overlap problem. + continue + group = side2Groups.get(name) + if group is None: + side2Groups[name] = tuple(sorted(members)) + for member in members: + side2Membership[member] = name_truncated + elif set(group) != members: + log_redefined_group("right", name, group, font, members) self.context.side1Membership = side1Membership self.context.side2Membership = side2Membership return side1Groups, side2Groups - def getKerningPairs(self, side1Classes, side2Classes): + def getKerningPairs( + self, + side1Classes: Mapping[str, tuple[str, ...]], + side2Classes: Mapping[str, tuple[str, ...]], + ) -> list[KerningPair]: + if self.context.isVariable: + return self.getVariableKerningPairs(side1Classes, side2Classes) + glyphSet = self.context.glyphSet font = self.context.font kerning = font.kerning quantization = self.options.quantization - kerning = font.kerning + kerning: Mapping[tuple[str, str], float] = font.kerning result = [] for (side1, side2), value in kerning.items(): firstIsClass, secondIsClass = (side1 in side1Classes, side2 in side2Classes) @@ -356,6 +422,122 @@ def getKerningPairs(self, side1Classes, side2Classes): return result + def getVariableKerningPairs( + self, + side1Classes: Mapping[str, tuple[str, ...]], + side2Classes: Mapping[str, tuple[str, ...]], + ) -> list[KerningPair]: + designspace: DesignSpaceDocument = self.context.font + glyphSet = self.context.glyphSet + quantization = self.options.quantization + + # Gather utility variables for faster kerning lookups. + # TODO: Do we construct these in code elsewhere? + assert not (set(side1Classes) & set(side2Classes)) + unified_groups = {**side1Classes, **side2Classes} + + glyphToFirstGroup = { + glyph_name: group_name # TODO: Is this overwrite safe? User input is adversarial + for group_name, glyphs in side1Classes.items() + for glyph_name in glyphs + } + glyphToSecondGroup = { + glyph_name: group_name + for group_name, glyphs in side2Classes.items() + for glyph_name in glyphs + } + + # Collate every kerning pair in the designspace, as even UFOs that + # provide no entry for the pair must contribute a value at their + # source's location in the VariableScalar. + # NOTE: This is required as the DS+UFO kerning model and the OpenType + # variation model handle the absence of a kerning value at a + # given location differently: + # - DS+UFO: + # If the missing pair excepts another pair, take its value; + # Otherwise, take a value of 0. + # - OpenType: + # Always interpolate from other locations, ignoring more + # general pairs that this one excepts. + # See discussion: https://github.com/googlefonts/ufo2ft/pull/635 + all_pairs: set[tuple[str, str]] = set() + for source in designspace.sources: + if source.layerName is not None: + continue + assert source.font is not None + all_pairs |= set(source.font.kerning) + + kerning_pairs_in_progress: dict[ + tuple[str | tuple[str], str | tuple[str]], VariableScalar + ] = {} + for source in designspace.sources: + # Skip sparse sources, because they can have no kerning. + if source.layerName is not None: + continue + assert source.font is not None + + location = VariableScalarLocation( + get_userspace_location(designspace, source.location) + ) + + kerning: Mapping[tuple[str, str], float] = source.font.kerning + for pair in all_pairs: + side1, side2 = pair + firstIsClass = side1 in side1Classes + secondIsClass = side2 in side2Classes + + # Filter out pairs that reference missing groups or glyphs. + # TODO: Can we do this outside of the loop? We know the pairs already. + if not firstIsClass and side1 not in glyphSet: + continue + if not secondIsClass and side2 not in glyphSet: + continue + + # Get the kerning value for this source and quantize, following + # the DS+UFO semantics described above. + value = quantize( + lookupKerningValue( + pair, + kerning, + unified_groups, + glyphToFirstGroup=glyphToFirstGroup, + glyphToSecondGroup=glyphToSecondGroup, + ), + quantization, + ) + + if firstIsClass: + side1 = side1Classes[side1] + if secondIsClass: + side2 = side2Classes[side2] + + # TODO: Can we instantiate these outside of the loop? We know the pairs already. + var_scalar = kerning_pairs_in_progress.setdefault( + (side1, side2), VariableScalar() + ) + # NOTE: Avoid using .add_value because it instantiates a new + # VariableScalarLocation on each call. + var_scalar.values[location] = value + + # We may need to provide a default location value to the variation + # model, find out where that is. + default_source = designspace.findDefault() + assert default_source is not None + default_location = VariableScalarLocation( + get_userspace_location(designspace, default_source.location) + ) + + result = [] + for (side1, side2), value in kerning_pairs_in_progress.items(): + # TODO: Should we interpolate a default value if it's not in the + # sources, rather than inserting a zero? What would varLib do? + if default_location not in value.values: + value.values[default_location] = 0 + value = collapse_varscalar(value) + result.append(KerningPair(side1, side2, value)) + + return result + def _makePairPosRule(self, pair, side1Classes, side2Classes, rtl=False): enumerated = pair.firstIsClass ^ pair.secondIsClass valuerecord = ast.ValueRecord( @@ -382,6 +564,18 @@ def _makePairPosRule(self, pair, side1Classes, side2Classes, rtl=False): enumerated=enumerated, ) + def _filterSpacingMarks(self, marks): + if self.context.isVariable: + spacing = [] + for mark in marks: + if all( + source.font[mark].width != 0 for source in self.context.font.sources + ): + spacing.append(mark) + return spacing + + return [mark for mark in marks if self.context.font[mark].width != 0] + def _makeKerningLookup(self, name, ignoreMarks=True): lookup = ast.LookupBlock(name) if ignoreMarks and self.options.ignoreMarks: @@ -392,7 +586,7 @@ def _makeKerningLookup(self, name, ignoreMarks=True): spacing = [] if marks: - spacing = [mark for mark in marks if self.context.font[mark].width != 0] + spacing = self._filterSpacingMarks(marks) if not spacing: # Simple case, there are no spacing ("Spacing Combining") marks, # do what we've always done. @@ -816,3 +1010,30 @@ def addClassDefinition( classNames.add(className) classDef = ast.makeGlyphClassDefinition(className, group) classes[group] = classDefs[className] = classDef + + +def log_redefined_group( + side: str, name: str, group: tuple[str, ...], font: Any, members: set[str] +) -> None: + LOGGER.warning( + "incompatible %s groups: %s was previously %s, %s tried to make it %s", + side, + name, + sorted(group), + font, + sorted(members), + ) + + +def log_regrouped_glyph( + side: str, name: str, original_name: str, font: Any, member: str +) -> None: + LOGGER.warning( + "incompatible %s groups: %s tries to put glyph %s in group %s, but it's already in %s, " + "discarding", + side, + font, + member, + name, + original_name, + ) diff --git a/Lib/ufo2ft/featureWriters/markFeatureWriter.py b/Lib/ufo2ft/featureWriters/markFeatureWriter.py index d8e620ad..22fbc16b 100644 --- a/Lib/ufo2ft/featureWriters/markFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/markFeatureWriter.py @@ -3,13 +3,11 @@ from collections import OrderedDict, defaultdict from functools import partial -from fontTools.misc.fixedTools import otRound - from ufo2ft.constants import INDIC_SCRIPTS, USE_SCRIPTS from ufo2ft.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import ( classifyGlyphs, - quantize, + otRoundIgnoringVariable, unicodeInScripts, unicodeScriptExtensions, ) @@ -34,7 +32,13 @@ def _filterMarks(self, include): def _marksAsAST(self): return [ - (ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)), anchor.markClass) + ( + ast.Anchor( + x=otRoundIgnoringVariable(anchor.x), + y=otRoundIgnoringVariable(anchor.y), + ), + anchor.markClass, + ) for anchor in sorted(self.marks, key=lambda a: a.name) ] @@ -78,7 +82,13 @@ def _filterMarks(self, include): def _marksAsAST(self): return [ [ - (ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)), anchor.markClass) + ( + ast.Anchor( + x=otRoundIgnoringVariable(anchor.x), + y=otRoundIgnoringVariable(anchor.y), + ), + anchor.markClass, + ) for anchor in sorted(component, key=lambda a: a.name) ] for component in self.marks @@ -334,8 +344,7 @@ def _getAnchorLists(self): self.log.warning( "duplicate anchor '%s' in glyph '%s'", anchorName, glyphName ) - x = quantize(anchor.x, self.options.quantization) - y = quantize(anchor.y, self.options.quantization) + x, y = self._getAnchor(glyphName, anchorName, anchor=anchor) a = self.NamedAnchor(name=anchorName, x=x, y=y) anchorDict[anchorName] = a if anchorDict: @@ -411,7 +420,7 @@ def _makeMarkClassDefinitions(self): return newDefs def _defineMarkClass(self, glyphName, x, y, className, markClasses): - anchor = ast.Anchor(x=otRound(x), y=otRound(y)) + anchor = ast.Anchor(x=otRoundIgnoringVariable(x), y=otRoundIgnoringVariable(y)) markClass = markClasses.get(className) if markClass is None: markClass = ast.MarkClass(className) diff --git a/Lib/ufo2ft/util.py b/Lib/ufo2ft/util.py index 48d56a79..d034f0bf 100644 --- a/Lib/ufo2ft/util.py +++ b/Lib/ufo2ft/util.py @@ -243,12 +243,14 @@ def makeUnicodeToGlyphNameMapping(font, glyphOrder=None): return mapping -def compileGSUB(featureFile, glyphOrder): +def compileGSUB(featureFile, glyphOrder, fvar=None): """Compile and return a GSUB table from `featureFile` (feaLib FeatureFile), using the given `glyphOrder` (list of glyph names). """ font = ttLib.TTFont() font.setGlyphOrder(glyphOrder) + if fvar: + font["fvar"] = fvar addOpenTypeFeatures(font, featureFile, tables={"GSUB"}) return font.get("GSUB") @@ -549,9 +551,18 @@ def _loadPluginFromString(spec, moduleName, isValidFunc): def quantize(number, factor): """Round to a multiple of the given parameter""" + if not isinstance(number, (float, int)): + # Some kind of variable scalar + return number return factor * otRound(number / factor) +def otRoundIgnoringVariable(number): + if not isinstance(number, (float, int)): + return number + return otRound(number) + + def init_kwargs(kwargs, defaults): """Initialise kwargs default values. @@ -673,3 +684,18 @@ def colrClipBoxQuantization(ufo: Any) -> int: """ upem = getAttrWithFallback(ufo.info, "unitsPerEm") return int(round(upem / 10, -1)) + + +def get_userspace_location(designspace, location): + """Map a location from designspace to userspace across all axes.""" + location_user = designspace.map_backward(location) + return {designspace.getAxis(k).tag: v for k, v in location_user.items()} + + +def collapse_varscalar(varscalar, threshold=0): + """Collapse a variable scalar to a plain scalar if all values are similar""" + # This should eventually be a method on the VariableScalar object + values = list(varscalar.values.values()) + if not any(abs(v - values[0]) > threshold for v in values[1:]): + return list(varscalar.values.values())[0] + return varscalar diff --git a/setup.py b/setup.py index fa193fd4..10f97610 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup_requires=pytest_runner + wheel + ["setuptools_scm"], tests_require=["pytest>=2.8"], install_requires=[ - "fonttools[ufo]>=4.44.3", + "fonttools[ufo]>=4.46.0", "cffsubr>=0.2.8", "booleanOperations>=0.9.0", ], diff --git a/tests/data/TestVarfea-Bold.ufo/fontinfo.plist b/tests/data/TestVarfea-Bold.ufo/fontinfo.plist new file mode 100644 index 00000000..d6b5d796 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/fontinfo.plist @@ -0,0 +1,36 @@ + + + + + ascender + 600 + capHeight + 700 + descender + -400 + familyName + TestVarfea + italicAngle + 0 + openTypeHeadCreated + 2022/07/26 14:49:29 + openTypeOS2Type + + 3 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleName + Bold + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 0 + xHeight + 500 + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/alef-ar.fina.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/alef-ar.fina.glif new file mode 100644 index 00000000..873945f8 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/alef-ar.fina.glif @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2023/11/20 16:51:14 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/contents.plist b/tests/data/TestVarfea-Bold.ufo/glyphs/contents.plist new file mode 100644 index 00000000..4572eaba --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/contents.plist @@ -0,0 +1,16 @@ + + + + + alef-ar.fina + alef-ar.fina.glif + dotabove-ar + dotabove-ar.glif + peh-ar.init + peh-ar.init.glif + peh-ar.init.BRACKET.varAlt01 + peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif + space + space.glif + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/dotabove-ar.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/dotabove-ar.glif new file mode 100644 index 00000000..05dde66a --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/dotabove-ar.glif @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2023/11/20 16:50:50 + com.schriftgestaltung.Glyphs.originalWidth + 300 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/layerinfo.plist b/tests/data/TestVarfea-Bold.ufo/glyphs/layerinfo.plist new file mode 100644 index 00000000..82fbc0d8 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/layerinfo.plist @@ -0,0 +1,19 @@ + + + + + lib + + com.schriftgestaltung.layerId + B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2 + com.schriftgestaltung.layerOrderInGlyph.alef-ar.fina + 1 + com.schriftgestaltung.layerOrderInGlyph.dotabove-ar + 1 + com.schriftgestaltung.layerOrderInGlyph.peh-ar.init + 1 + com.schriftgestaltung.layerOrderInGlyph.space + 1 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif new file mode 100644 index 00000000..82cc961e --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs._originalLayerName + 26 Jul 22, 15:59 + com.schriftgestaltung.Glyphs.lastChange + 2022/07/27 08:01:34 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.glif new file mode 100644 index 00000000..a8e87507 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.glif @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2022/07/27 08:01:34 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/space.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/space.glif new file mode 100644 index 00000000..fa922f49 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/space.glif @@ -0,0 +1,13 @@ + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2022/07/26 15:00:45 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/kerning.plist b/tests/data/TestVarfea-Bold.ufo/kerning.plist new file mode 100644 index 00000000..6bf0bd2a --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/kerning.plist @@ -0,0 +1,11 @@ + + + + + alef-ar.fina + + alef-ar.fina + 35 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/layercontents.plist b/tests/data/TestVarfea-Bold.ufo/layercontents.plist new file mode 100644 index 00000000..b9c1a4f2 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/tests/data/TestVarfea-Bold.ufo/lib.plist b/tests/data/TestVarfea-Bold.ufo/lib.plist new file mode 100644 index 00000000..d4c4a8cc --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/lib.plist @@ -0,0 +1,80 @@ + + + + + GSCornerRadius + 15 + GSOffsetHorizontal + -30 + GSOffsetVertical + -25 + com.github.googlei18n.ufo2ft.filters + + + name + eraseOpenCorners + namespace + glyphsLib.filters + pre + + + + com.schriftgestaltung.appVersion + 3217 + com.schriftgestaltung.customParameter.GSFont.DisplayStrings + + اا + /dotabove-ar + + com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment + + com.schriftgestaltung.customParameter.GSFont.useNiceNames + 1 + com.schriftgestaltung.customParameter.GSFontMaster.customValue + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue1 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue2 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue3 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.iconName + Bold + com.schriftgestaltung.customParameter.GSFontMaster.weightValue + 1000 + com.schriftgestaltung.customParameter.GSFontMaster.widthValue + 100 + com.schriftgestaltung.fontMasterID + B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2 + com.schriftgestaltung.fontMasterOrder + 1 + com.schriftgestaltung.keyboardIncrement + 1 + com.schriftgestaltung.weight + Regular + com.schriftgestaltung.weightValue + 1000 + com.schriftgestaltung.width + Regular + com.schriftgestaltung.widthValue + 100 + public.glyphOrder + + alef-ar.fina + peh-ar.init + space + dotabove-ar + + public.postscriptNames + + alef-ar.fina + uniFE8E + dotabove-ar + dotabovear + peh-ar.init + uniFB58 + peh-ar.init.BRACKET.varAlt01 + uniFB58.BRACKET.varAlt01 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/metainfo.plist b/tests/data/TestVarfea-Bold.ufo/metainfo.plist new file mode 100644 index 00000000..7b8b34ac --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + com.github.fonttools.ufoLib + formatVersion + 3 + + diff --git a/tests/data/TestVarfea-Regular.ufo/fontinfo.plist b/tests/data/TestVarfea-Regular.ufo/fontinfo.plist new file mode 100644 index 00000000..9f3eadf5 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/fontinfo.plist @@ -0,0 +1,48 @@ + + + + + ascender + 600 + capHeight + 0 + descender + -400 + familyName + TestVarfea + italicAngle + 0 + openTypeHeadCreated + 2022/07/26 14:49:29 + openTypeOS2Type + + 3 + + postscriptBlueValues + + -16 + 0 + 600 + 616 + + postscriptOtherBlues + + -416 + -400 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleName + Regular + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 0 + xHeight + 0 + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/alef-ar.fina.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/alef-ar.fina.glif new file mode 100644 index 00000000..dfd59957 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/alef-ar.fina.glif @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2023/11/20 16:51:14 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/contents.plist b/tests/data/TestVarfea-Regular.ufo/glyphs/contents.plist new file mode 100644 index 00000000..4572eaba --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/contents.plist @@ -0,0 +1,16 @@ + + + + + alef-ar.fina + alef-ar.fina.glif + dotabove-ar + dotabove-ar.glif + peh-ar.init + peh-ar.init.glif + peh-ar.init.BRACKET.varAlt01 + peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif + space + space.glif + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/dotabove-ar.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/dotabove-ar.glif new file mode 100644 index 00000000..d9ec3589 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/dotabove-ar.glif @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2023/11/20 16:50:50 + com.schriftgestaltung.Glyphs.originalWidth + 300 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/layerinfo.plist b/tests/data/TestVarfea-Regular.ufo/glyphs/layerinfo.plist new file mode 100644 index 00000000..e4d188f0 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/layerinfo.plist @@ -0,0 +1,19 @@ + + + + + lib + + com.schriftgestaltung.layerId + m01 + com.schriftgestaltung.layerOrderInGlyph.alef-ar.fina + 0 + com.schriftgestaltung.layerOrderInGlyph.dotabove-ar + 0 + com.schriftgestaltung.layerOrderInGlyph.peh-ar.init + 0 + com.schriftgestaltung.layerOrderInGlyph.space + 0 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif new file mode 100644 index 00000000..26c13c38 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs._originalLayerName + 26 Jul 22, 15:58 + com.schriftgestaltung.Glyphs.lastChange + 2022/07/27 08:01:34 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.glif new file mode 100644 index 00000000..03c6ba3a --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.glif @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2022/07/27 08:01:34 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/space.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/space.glif new file mode 100644 index 00000000..ad61b901 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/space.glif @@ -0,0 +1,13 @@ + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2022/07/26 15:00:45 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/kerning.plist b/tests/data/TestVarfea-Regular.ufo/kerning.plist new file mode 100644 index 00000000..696a1f40 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/kerning.plist @@ -0,0 +1,11 @@ + + + + + alef-ar.fina + + alef-ar.fina + 15 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/layercontents.plist b/tests/data/TestVarfea-Regular.ufo/layercontents.plist new file mode 100644 index 00000000..b9c1a4f2 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/tests/data/TestVarfea-Regular.ufo/lib.plist b/tests/data/TestVarfea-Regular.ufo/lib.plist new file mode 100644 index 00000000..0b5415f3 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/lib.plist @@ -0,0 +1,74 @@ + + + + + com.github.googlei18n.ufo2ft.filters + + + name + eraseOpenCorners + namespace + glyphsLib.filters + pre + + + + com.schriftgestaltung.appVersion + 3217 + com.schriftgestaltung.customParameter.GSFont.DisplayStrings + + اا + /dotabove-ar + + com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment + + com.schriftgestaltung.customParameter.GSFont.useNiceNames + 1 + com.schriftgestaltung.customParameter.GSFontMaster.customValue + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue1 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue2 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue3 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.iconName + + com.schriftgestaltung.customParameter.GSFontMaster.weightValue + 100 + com.schriftgestaltung.customParameter.GSFontMaster.widthValue + 100 + com.schriftgestaltung.fontMasterID + m01 + com.schriftgestaltung.fontMasterOrder + 0 + com.schriftgestaltung.keyboardIncrement + 1 + com.schriftgestaltung.weight + Regular + com.schriftgestaltung.weightValue + 100 + com.schriftgestaltung.width + Regular + com.schriftgestaltung.widthValue + 100 + public.glyphOrder + + alef-ar.fina + peh-ar.init + space + dotabove-ar + + public.postscriptNames + + alef-ar.fina + uniFE8E + dotabove-ar + dotabovear + peh-ar.init + uniFB58 + peh-ar.init.BRACKET.varAlt01 + uniFB58.BRACKET.varAlt01 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/metainfo.plist b/tests/data/TestVarfea-Regular.ufo/metainfo.plist new file mode 100644 index 00000000..7b8b34ac --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + com.github.fonttools.ufoLib + formatVersion + 3 + + diff --git a/tests/data/TestVarfea.designspace b/tests/data/TestVarfea.designspace new file mode 100644 index 00000000..aa7a1a61 --- /dev/null +++ b/tests/data/TestVarfea.designspace @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/TestVarfea.glyphs b/tests/data/TestVarfea.glyphs new file mode 100644 index 00000000..5c0e945d --- /dev/null +++ b/tests/data/TestVarfea.glyphs @@ -0,0 +1,646 @@ +{ +.appVersion = "3217"; +.formatVersion = 3; +DisplayStrings = ( +"اا", +"/dotabove-ar" +); +axes = ( +{ +name = Weight; +tag = wght; +} +); +date = "2022-07-26 14:49:29 +0000"; +familyName = TestVarfea; +fontMaster = ( +{ +axesValues = ( +100 +); +id = m01; +metricValues = ( +{ +over = 16; +pos = 600; +}, +{ +over = -16; +}, +{ +over = -16; +pos = -400; +}, +{ +}, +{ +} +); +name = Regular; +}, +{ +axesValues = ( +1000 +); +iconName = Bold; +id = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +metricValues = ( +{ +pos = 600; +}, +{ +}, +{ +pos = -400; +}, +{ +pos = 700; +}, +{ +pos = 500; +} +); +name = Bold; +userData = { +GSCornerRadius = 15; +GSOffsetHorizontal = -30; +GSOffsetVertical = -25; +}; +} +); +glyphs = ( +{ +glyphname = "alef-ar.fina"; +lastChange = "2023-11-20 16:51:14 +0000"; +layers = ( +{ +anchors = ( +{ +name = entry; +pos = (299,97); +}, +{ +name = top; +pos = (211,730); +} +); +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(270,173,o), +(283,133,o), +(321,118,cs), +(375,97,o), +(403,105,o), +(466,133,c), +(491,13,l), +(427,-19,o), +(381,-25,o), +(336,-25,cs), +(139,-25,o), +(160,121,o), +(160,569,c), +(270,601,l) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = entry; +pos = (330,115); +}, +{ +name = top; +pos = (214,797); +} +); +layerId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +shapes = ( +{ +closed = 1; +nodes = ( +(297,173,o), +(310,133,o), +(348,118,cs), +(402,97,o), +(433,86,o), +(487,123,c), +(565,-41,l), +(501,-73,o), +(400,-76,o), +(355,-76,cs), +(108,-76,o), +(137,121,o), +(137,569,c), +(297,601,l) +); +} +); +width = 600; +} +); +unicode = 1575; +}, +{ +glyphname = "peh-ar.init"; +lastChange = "2022-07-27 08:01:34 +0000"; +layers = ( +{ +anchors = ( +{ +name = exit; +pos = (161,54); +} +); +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(466,268,l), +(450,61,o), +(400,-33,o), +(291,-56,cs), +(165,-84,o), +(107,-64,o), +(67,22,c), +(67,130,l), +(124,89,o), +(185,67,o), +(241,67,cs), +(332,67,o), +(370,122,o), +(378,268,c) +); +}, +{ +closed = 1; +nodes = ( +(164,-235,ls), +(158,-241,o), +(158,-250,o), +(164,-257,cs), +(250,-347,ls), +(256,-354,o), +(265,-354,o), +(272,-347,cs), +(362,-261,ls), +(368,-255,o), +(368,-246,o), +(362,-239,cs), +(276,-149,ls), +(270,-142,o), +(261,-142,o), +(254,-149,cs) +); +}, +{ +closed = 1; +nodes = ( +(384,-235,ls), +(378,-241,o), +(378,-250,o), +(384,-257,cs), +(470,-347,ls), +(476,-354,o), +(485,-354,o), +(492,-347,cs), +(582,-261,ls), +(588,-255,o), +(588,-246,o), +(582,-239,cs), +(496,-149,ls), +(490,-142,o), +(481,-142,o), +(474,-149,cs) +); +}, +{ +closed = 1; +nodes = ( +(264,-435,ls), +(258,-441,o), +(258,-450,o), +(264,-457,cs), +(350,-547,ls), +(356,-554,o), +(365,-554,o), +(372,-547,cs), +(462,-461,ls), +(468,-455,o), +(468,-446,o), +(462,-439,cs), +(376,-349,ls), +(370,-342,o), +(361,-342,o), +(354,-349,cs) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = exit; +pos = (73,89); +} +); +layerId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +shapes = ( +{ +closed = 1; +nodes = ( +(525,322,l), +(509,19,o), +(513,14,o), +(342,-63,cs), +(232,-113,o), +(142,-102,o), +(61,-87,c), +(61,104,l), +(104,84,o), +(139,75,o), +(167,75,cs), +(252,75,o), +(277,161,o), +(291,322,c) +); +}, +{ +closed = 1; +nodes = ( +(115,-208,ls), +(107,-215,o), +(107,-228,o), +(115,-236,cs), +(221,-347,ls), +(229,-355,o), +(241,-354,o), +(248,-347,cs), +(359,-241,ls), +(367,-233,o), +(367,-222,o), +(359,-213,cs), +(253,-102,ls), +(246,-94,o), +(233,-95,o), +(226,-102,cs) +); +}, +{ +closed = 1; +nodes = ( +(387,-208,ls), +(379,-216,o), +(379,-227,o), +(387,-236,cs), +(493,-347,ls), +(500,-355,o), +(513,-354,o), +(520,-347,cs), +(631,-241,ls), +(639,-234,o), +(639,-221,o), +(631,-213,cs), +(525,-102,ls), +(517,-94,o), +(505,-95,o), +(498,-102,cs) +); +}, +{ +closed = 1; +nodes = ( +(238,-455,ls), +(230,-462,o), +(230,-475,o), +(238,-483,cs), +(345,-594,ls), +(352,-602,o), +(364,-601,o), +(372,-594,cs), +(483,-488,ls), +(491,-481,o), +(491,-468,o), +(483,-460,cs), +(377,-349,ls), +(369,-341,o), +(357,-342,o), +(350,-349,cs) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = exit; +pos = (89,53); +} +); +associatedMasterId = m01; +attr = { +axisRules = ( +{ +min = 600; +} +); +}; +layerId = "8B3F4CCE-5E0D-437E-916C-4646A5030CF3"; +name = "26 Jul 22, 15:58"; +shapes = ( +{ +closed = 1; +nodes = ( +(490,215,l), +(469,-10,o), +(412,-54,o), +(266,-54,cs), +(161,-54,o), +(90,-27,o), +(67,22,c), +(67,130,l), +(137,80,o), +(173,67,o), +(241,67,cs), +(291,67,o), +(315,118,o), +(325,290,c) +); +}, +{ +closed = 1; +nodes = ( +(194,-235,ls), +(188,-241,o), +(188,-250,o), +(194,-257,cs), +(280,-347,ls), +(286,-354,o), +(296,-353,o), +(302,-347,cs), +(369,-283,l), +(430,-347,ls), +(436,-354,o), +(446,-353,o), +(452,-347,cs), +(542,-261,ls), +(548,-255,o), +(548,-246,o), +(542,-239,cs), +(456,-149,ls), +(450,-142,o), +(440,-143,o), +(434,-149,cs), +(367,-213,l), +(306,-149,ls), +(300,-142,o), +(290,-143,o), +(284,-149,cs) +); +}, +{ +closed = 1; +nodes = ( +(264,-435,ls), +(258,-441,o), +(258,-450,o), +(264,-457,cs), +(350,-547,ls), +(356,-554,o), +(366,-553,o), +(372,-547,cs), +(462,-461,ls), +(468,-455,o), +(468,-446,o), +(462,-439,cs), +(376,-349,ls), +(370,-342,o), +(360,-343,o), +(354,-349,cs) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = exit; +pos = (73,85); +} +); +associatedMasterId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +attr = { +axisRules = ( +{ +min = 600; +} +); +}; +layerId = "3CC1DB06-AB7B-409C-841A-4209D835026A"; +name = "26 Jul 22, 15:59"; +shapes = ( +{ +closed = 1; +nodes = ( +(525,322,l), +(509,19,o), +(513,14,o), +(342,-63,cs), +(232,-113,o), +(142,-102,o), +(61,-87,c), +(61,104,l), +(104,84,o), +(139,75,o), +(167,75,cs), +(252,75,o), +(277,161,o), +(291,322,c) +); +}, +{ +closed = 1; +nodes = ( +(153,-223,ls), +(146,-230,o), +(145,-242,o), +(153,-250,cs), +(259,-361,ls), +(267,-370,o), +(279,-369,o), +(287,-361,cs), +(369,-282,l), +(445,-361,ls), +(453,-369,o), +(464,-369,o), +(472,-361,cs), +(583,-255,ls), +(590,-248,o), +(590,-236,o), +(583,-228,cs), +(477,-117,ls), +(469,-108,o), +(457,-109,o), +(449,-117,cs), +(367,-196,l), +(291,-117,ls), +(283,-109,o), +(272,-109,o), +(264,-117,cs) +); +}, +{ +closed = 1; +nodes = ( +(208,-495,ls), +(199,-504,o), +(198,-519,o), +(208,-529,cs), +(339,-666,ls), +(348,-676,o), +(364,-675,o), +(373,-666,cs), +(510,-535,ls), +(520,-526,o), +(520,-511,o), +(510,-501,cs), +(379,-364,ls), +(369,-354,o), +(354,-355,o), +(345,-364,cs) +); +} +); +width = 600; +} +); +unicode = 1662; +}, +{ +glyphname = space; +lastChange = "2022-07-26 15:00:45 +0000"; +layers = ( +{ +layerId = m01; +width = 200; +}, +{ +layerId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +width = 600; +} +); +unicode = 32; +}, +{ +glyphname = "dotabove-ar"; +lastChange = "2023-11-20 16:50:50 +0000"; +layers = ( +{ +anchors = ( +{ +name = _top; +pos = (100,320); +} +); +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(104,232,l), +(160,271,o), +(187,303,o), +(187,326,cs), +(187,349,o), +(170,372,o), +(109,411,c), +(100,411,l), +(83,400,o), +(30,341,o), +(13,315,c), +(13,306,l), +(40,285,o), +(68,260,o), +(96,232,c) +); +} +); +width = 300; +}, +{ +anchors = ( +{ +name = _top; +pos = (125,416); +} +); +layerId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +shapes = ( +{ +closed = 1; +nodes = ( +(129,292,l), +(180,328,o), +(231,372,o), +(231,409,cs), +(231,445,o), +(196,472,o), +(135,511,c), +(124,511,l), +(105,499,o), +(38,425,o), +(18,393,c), +(18,382,l), +(50,359,o), +(60,350,o), +(120,292,c) +); +} +); +width = 300; +} +); +} +); +kerningLTR = { +m01 = { +"alef-ar.fina" = { +"alef-ar.fina" = 15; +}; +}; +"B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2" = { +"alef-ar.fina" = { +"alef-ar.fina" = 35; +}; +}; +}; +metrics = ( +{ +type = ascender; +}, +{ +type = baseline; +}, +{ +type = descender; +}, +{ +type = "cap height"; +}, +{ +type = "x-height"; +} +); +unitsPerEm = 1000; +versionMajor = 1; +versionMinor = 0; +} diff --git a/tests/data/TestVariableFont-CFF2-cffsubr.ttx b/tests/data/TestVariableFont-CFF2-cffsubr.ttx index a7ad531d..b2a8804a 100644 --- a/tests/data/TestVariableFont-CFF2-cffsubr.ttx +++ b/tests/data/TestVariableFont-CFF2-cffsubr.ttx @@ -328,7 +328,7 @@ - + diff --git a/tests/data/TestVariableFont-CFF2-post3.ttx b/tests/data/TestVariableFont-CFF2-post3.ttx index 163c07fd..d8f17e02 100644 --- a/tests/data/TestVariableFont-CFF2-post3.ttx +++ b/tests/data/TestVariableFont-CFF2-post3.ttx @@ -305,7 +305,7 @@ - + diff --git a/tests/data/TestVariableFont-CFF2-useProductionNames.ttx b/tests/data/TestVariableFont-CFF2-useProductionNames.ttx index b78c88cb..9bf82105 100644 --- a/tests/data/TestVariableFont-CFF2-useProductionNames.ttx +++ b/tests/data/TestVariableFont-CFF2-useProductionNames.ttx @@ -322,7 +322,7 @@ - + diff --git a/tests/data/TestVariableFont-CFF2.ttx b/tests/data/TestVariableFont-CFF2.ttx index cfb50b2e..c53ebc96 100644 --- a/tests/data/TestVariableFont-CFF2.ttx +++ b/tests/data/TestVariableFont-CFF2.ttx @@ -319,7 +319,7 @@ - + diff --git a/tests/data/TestVariableFont-TTF-post3.ttx b/tests/data/TestVariableFont-TTF-post3.ttx index 6b5fa50d..aa21f3ef 100644 --- a/tests/data/TestVariableFont-TTF-post3.ttx +++ b/tests/data/TestVariableFont-TTF-post3.ttx @@ -308,7 +308,7 @@ - + diff --git a/tests/data/TestVariableFont-TTF-useProductionNames.ttx b/tests/data/TestVariableFont-TTF-useProductionNames.ttx index fff0a018..57466c5f 100644 --- a/tests/data/TestVariableFont-TTF-useProductionNames.ttx +++ b/tests/data/TestVariableFont-TTF-useProductionNames.ttx @@ -325,7 +325,7 @@ - + diff --git a/tests/data/TestVariableFont-TTF.ttx b/tests/data/TestVariableFont-TTF.ttx index 63ff72b0..a7504b6c 100644 --- a/tests/data/TestVariableFont-TTF.ttx +++ b/tests/data/TestVariableFont-TTF.ttx @@ -322,7 +322,7 @@ - + diff --git a/tests/featureWriters/variableFeatureWriter_test.py b/tests/featureWriters/variableFeatureWriter_test.py new file mode 100644 index 00000000..92092c65 --- /dev/null +++ b/tests/featureWriters/variableFeatureWriter_test.py @@ -0,0 +1,54 @@ +import io +from textwrap import dedent + +from fontTools import designspaceLib + +from ufo2ft import compileVariableTTF + + +def test_variable_features(FontClass): + tmp = io.StringIO() + designspace = designspaceLib.DesignSpaceDocument.fromfile( + "tests/data/TestVarfea.designspace" + ) + designspace.loadSourceFonts(FontClass) + _ = compileVariableTTF(designspace, debugFeatureFile=tmp) + + assert dedent("\n" + tmp.getvalue()) == dedent( + """ + markClass dotabove-ar @MC_top; + + lookup kern_Arab { + lookupflag IgnoreMarks; + pos alef-ar.fina alef-ar.fina <(wght=100:15 wght=1000:35) 0 (wght=100:15 wght=1000:35) 0>; + } kern_Arab; + + feature kern { + script DFLT; + language dflt; + lookup kern_Arab; + + script arab; + language dflt; + lookup kern_Arab; + } kern; + + feature mark { + lookup mark2base { + pos base alef-ar.fina + mark @MC_top; + } mark2base; + + } mark; + + feature curs { + lookup curs { + lookupflag RightToLeft IgnoreMarks; + pos cursive alef-ar.fina ; + pos cursive peh-ar.init ; + pos cursive peh-ar.init.BRACKET.varAlt01 ; + } curs; + + } curs; +""" # noqa: B950 + ) diff --git a/tests/integration_test.py b/tests/integration_test.py index 05c2ba0d..0eb2a038 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -4,6 +4,7 @@ import re import sys from pathlib import Path +from textwrap import dedent import pytest from fontTools.pens.boundsPen import BoundsPen @@ -241,9 +242,23 @@ def test_debugFeatureFile(self, designspace): tmp = io.StringIO() _ = compileVariableTTF(designspace, debugFeatureFile=tmp) - - assert "### LayerFont-Regular ###" in tmp.getvalue() - assert "### LayerFont-Bold ###" in tmp.getvalue() + assert "\n" + tmp.getvalue() == dedent( + """ + markClass dotabovecomb @MC_top; + + feature liga { + sub a e s s by s; + } liga; + + feature mark { + lookup mark2base { + pos base e + mark @MC_top; + } mark2base; + + } mark; + """ # noqa: B950 + ) @pytest.mark.parametrize( "output_format, options, expected_ttx",