pax_global_header00006660000000000000000000000064147017526270014525gustar00rootroot0000000000000052 comment=862a390f7958b2a0b9af70d02437232d80e5d260 ufo2ft-3.3.1/000077500000000000000000000000001470175262700127365ustar00rootroot00000000000000ufo2ft-3.3.1/.codecov.yml000066400000000000000000000001211470175262700151530ustar00rootroot00000000000000comment: false coverage: status: project: false patch: false ufo2ft-3.3.1/.coveragerc000066400000000000000000000016011470175262700150550ustar00rootroot00000000000000[run] # measure 'branch' coverage in addition to 'statement' coverage # See: http://coverage.readthedocs.org/en/coverage-4.0.3/branch.html#branch branch = True # list of directories or packages to measure source = ufo2ft # these are treated as equivalent when combining data [paths] source = Lib/ufo2ft .tox/*/lib/python*/site-packages/ufo2ft .tox/pypy*/site-packages/ufo2ft [report] # Regexes for lines to exclude from consideration exclude_lines = # keywords to use in inline comments to skip coverage pragma: no cover # don't complain if tests don't hit defensive assertion code raise AssertionError raise NotImplementedError # don't complain if non-runnable code isn't run if 0: if __name__ == .__main__.: # ignore source code that can’t be found ignore_errors = True # when running a summary report, show missing lines show_missing = True ufo2ft-3.3.1/.gitattributes000066400000000000000000000004411470175262700156300ustar00rootroot00000000000000# Set the default behavior, in case people don't have core.autocrlf set. * text=lf # Explicitly declare text files you want to always be normalized and converted # to native line endings on checkout. *.cfg text *.ini text *.md text *.py text *.toml text *.txt text *.yaml text *.yml text ufo2ft-3.3.1/.github/000077500000000000000000000000001470175262700142765ustar00rootroot00000000000000ufo2ft-3.3.1/.github/workflows/000077500000000000000000000000001470175262700163335ustar00rootroot00000000000000ufo2ft-3.3.1/.github/workflows/ci.yml000066400000000000000000000077251470175262700174640ustar00rootroot00000000000000name: Test + Deploy on: push: branches: [main] tags: ["v*.*.*"] pull_request: branches: [main] jobs: lint: runs-on: ubuntu-latest # https://github.community/t/github-actions-does-not-respect-skip-ci/17325/8 if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.x" - name: Install dependencies run: pip install tox - name: Run style and static checks run: tox -e lint test: runs-on: ${{ matrix.platform }} if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" strategy: matrix: python-version: ["3.8", "3.12"] platform: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install tox - name: Test with tox run: tox -e py-cov - name: Produce coverage files run: tox -e htmlcov - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: coverage.xml flags: unittests name: codecov-umbrella fail_ci_if_error: false # see https://github.com/codecov/codecov-action/issues/557 token: ${{ secrets.CODECOV_TOKEN }} deploy: # only run if the commit is tagged... if: startsWith(github.ref, 'refs/tags/v') # ... and both the lint and test jobs completed successfully needs: - lint - test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # setuptools_scm requires the git clone to not be 'shallow' fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.x" - name: Extract release notes from annotated tag message id: release_notes env: # e.g. v0.1.0a1, v1.2.0b2 or v2.3.0rc3, but not v1.0.0 PRERELEASE_TAG_PATTERN: "v[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+([ab]|rc)[[:digit:]]+" run: | # GH checkout action doesn't preserve tag annotations, we must fetch them # https://github.com/actions/checkout/issues/290 git fetch --tags --force # strip leading 'refs/tags/' to get the tag name TAG_NAME="${GITHUB_REF##*/}" # Dump tag message to temporary .md file (excluding the PGP signature at the bottom) TAG_MESSAGE=$(git tag -l --format='%(contents)' $TAG_NAME | sed -n '/-----BEGIN PGP SIGNATURE-----/q;p') echo "$TAG_MESSAGE" > "${{ runner.temp }}/release_notes.md" # if the tag has a pre-release suffix mark the Github Release accordingly if egrep -q "$PRERELEASE_TAG_PATTERN" <<< "$TAG_NAME"; then echo "Tag contains a pre-release suffix" echo "IS_PRERELEASE=true" >> "$GITHUB_ENV" else echo "Tag does not contain pre-release suffix" echo "IS_PRERELEASE=false" >> "$GITHUB_ENV" fi - name: Create GitHub release id: create_release uses: actions/create-release@v1 env: # This token is provided by Actions, you do not need to create your own token GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} body_path: "${{ runner.temp }}/release_notes.md" draft: false prerelease: ${{ env.IS_PRERELEASE }} - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | if [ "$IS_PRERELEASE" == true ]; then echo "DEBUG: This is a pre-release" else echo "DEBUG: This is a final release" fi pipx run build pipx run twine check dist/* pipx run twine upload dist/* ufo2ft-3.3.1/.gitignore000066400000000000000000000007301470175262700147260ustar00rootroot00000000000000# Byte-compiled / optimized files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / Packaging *.egg *.egg-info *.eggs MANIFEST build dist # Unit test / coverage files .tox/* .cache/ .coverage .coverage.* htmlcov/ .pytest_cache/ # OSX Finder .DS_Store # pyenv python configuration file .python-version # autosaved emacs files *~ # virtual environments dirs venv/ .venv/ venv3*/ .venv3*/ # autogenerated by setuptools-scm Lib/ufo2ft/_version.py ufo2ft-3.3.1/.pyup.yml000066400000000000000000000002501470175262700145310ustar00rootroot00000000000000# controls the frequency of updates (undocumented beta feature) schedule: every week # do not pin dependencies unless they have explicit version specifiers pin: False ufo2ft-3.3.1/LICENSE000066400000000000000000000020631470175262700137440ustar00rootroot00000000000000The MIT License Copyright (c) 2009 Type Supply LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.ufo2ft-3.3.1/Lib/000077500000000000000000000000001470175262700134445ustar00rootroot00000000000000ufo2ft-3.3.1/Lib/ufo2ft/000077500000000000000000000000001470175262700146515ustar00rootroot00000000000000ufo2ft-3.3.1/Lib/ufo2ft/__init__.py000066400000000000000000000326671470175262700170000ustar00rootroot00000000000000from ufo2ft._compilers.interpolatableOTFCompiler import InterpolatableOTFCompiler from ufo2ft._compilers.interpolatableTTFCompiler import InterpolatableTTFCompiler from ufo2ft._compilers.otfCompiler import OTFCompiler from ufo2ft._compilers.ttfCompiler import TTFCompiler from ufo2ft._compilers.variableCFF2sCompiler import VariableCFF2sCompiler from ufo2ft._compilers.variableTTFsCompiler import VariableTTFsCompiler from ufo2ft.constants import CFFOptimization # noqa: F401 (fontmake uses it) __all__ = [ "compileTTF", "compileOTF", "compileInterpolatableTTFs", "compileVariableTTFs", "compileInterpolatableTTFsFromDS", "compileInterpolatableOTFsFromDS", "compileVariableTTF", "compileVariableCFF2", "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. *removeOverlaps* performs a union operation on all the glyphs' contours. *flattenComponents* un-nests glyphs so that they have at most one level of components. *convertCubics* and *cubicConversionError* specify how the conversion from cubic to quadratic curves should be handled. *layerName* specifies which layer should be compiled. When compiling something other than the default layer, feature compilation is skipped. *skipExportGlyphs* is a list or set of glyph names to not be exported to the final font. If these glyphs are used as components in any other glyph, those components get decomposed. If the parameter is not passed in, the UFO's "public.skipExportGlyphs" lib key will be consulted. If it doesn't exist, all glyphs are exported. UFO groups and kerning will be pruned of skipped glyphs. *dropImpliedOnCurves* (bool) specifies whether on-curve points that are exactly in between two off-curves can be dropped when building glyphs (default: False). *allQuadratic* (bool) specifies whether to convert all curves to quadratic - True by default, builds traditional glyf v0 table. If False, quadratic curves or cubic curves are generated depending on which has fewer points; a glyf v1 is generated. """ return TTFCompiler(**kwargs).compile(ufo) def compileOTF(ufo, **kwargs): """Create FontTools CFF font from a UFO. *removeOverlaps* performs a union operation on all the glyphs' contours. *optimizeCFF* (int) defines whether the CFF charstrings should be specialized and subroutinized. By default both optimization are enabled. A value of 0 disables both; 1 only enables the specialization; 2 (default) does both specialization and subroutinization. *roundTolerance* (float) controls the rounding of point coordinates. It is defined as the maximum absolute difference between the original float and the rounded integer value. By default, all floats are rounded to integer (tolerance 0.5); a value of 0 completely disables rounding; values in between only round floats which are close to their integral part within the tolerated range. *featureWriters* argument is a list of BaseFeatureWriter subclasses or pre-initialized instances. Features will be written by each feature writer in the given order. If featureWriters is None, the default feature writers [KernFeatureWriter, MarkFeatureWriter] are used. *filters* argument is a list of BaseFilters subclasses or pre-initialized instances. Filters with 'pre' attribute set to True will be pre-filters called before the default filters, otherwise they will be post-filters, called after the default filters. Filters will modify glyphs or the glyph set. The default filters cannot be disabled. *useProductionNames* renames glyphs in TrueType 'post' or OpenType 'CFF ' tables based on the 'public.postscriptNames' mapping in the UFO lib, if present. Otherwise, uniXXXX names are generated from the glyphs' unicode values. The default value (None) will first check if the UFO lib has the 'com.github.googlei18n.ufo2ft.useProductionNames' key. If this is missing or True (default), the glyphs are renamed. Set to False to keep the original names. **inplace** (bool) specifies whether the filters should modify the input UFO's glyphs, a copy should be made first. *layerName* specifies which layer should be compiled. When compiling something other than the default layer, feature compilation is skipped. *skipExportGlyphs* is a list or set of glyph names to not be exported to the final font. If these glyphs are used as components in any other glyph, those components get decomposed. If the parameter is not passed in, the UFO's "public.skipExportGlyphs" lib key will be consulted. If it doesn't exist, all glyphs are exported. UFO groups and kerning will be pruned of skipped glyphs. *cffVersion* (int) is the CFF format, choose between 1 (default) and 2. *subroutinizer* (Optional[str]) is the name of the library to use for compressing CFF charstrings, if subroutinization is enabled by optimizeCFF parameter. Choose between "cffsubr" or "compreffor". By default "cffsubr" is used for both CFF 1 and CFF 2. NOTE: cffsubr is required for subroutinizing CFF2 tables, as compreffor currently doesn't support it. """ return OTFCompiler(**kwargs).compile(ufo) def compileInterpolatableTTFs(ufos, **kwargs): """Create FontTools TrueType fonts from a list of UFOs with interpolatable outlines. Cubic curves are converted compatibly to quadratic curves using the Cu2Qu conversion algorithm. Return an iterator object that yields a TTFont instance for each UFO. *layerNames* refers to the layer names to use glyphs from in the order of the UFOs in *ufos*. By default, this is a list of `[None]` times the number of UFOs, i.e. using the default layer from all the UFOs. When the layerName is not None for a given UFO, 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. *skipExportGlyphs* is a list or set of glyph names to not be exported to the final font. If these glyphs are used as components in any other glyph, those components get decomposed. If the parameter is not passed in, the union of all UFO's "public.skipExportGlyphs" lib keys will be used. If they don't exist, all glyphs are exported. UFO groups and kerning will be pruned of skipped glyphs. """ return InterpolatableTTFCompiler(**kwargs).compile(ufos) def compileVariableTTFs(designSpaceDoc, **kwargs): """Create FontTools TrueType variable fonts for each variable font defined in the given DesignSpaceDocument, using their UFO sources with interpolatable outlines, using fontTools.varLib.build. *optimizeGvar*, if set to False, will not perform IUP optimization on the generated 'gvar' table. *excludeVariationTables* is a list of sfnt table tags (str) that is passed on to fontTools.varLib.build, to skip building some variation tables. *variableFontNames* is an optional list of names of variable fonts to build. If not provided, all variable fonts listed in the given designspace will by built. *allQuadratic* (bool) specifies whether to convert all curves to quadratic - True by default, builds traditional glyf v0 table. If False, quadratic curves or cubic curves are generated depending on which has fewer points; a glyf v1 is generated. The rest of the arguments works the same as in the other compile functions. Returns a dictionary that maps each variable font filename to a new variable TTFont object. If no variable fonts are defined in the Designspace, returns an empty dictionary. .. versionadded:: 2.28.0 """ return VariableTTFsCompiler(**kwargs).compile_variable(designSpaceDoc) 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) def compileInterpolatableOTFsFromDS(designSpaceDoc, **kwargs): """Create FontTools CFF fonts from the DesignSpaceDocument UFO sources with interpolatable outlines. Interpolatable means without subroutinization and specializer optimizations and no removal of overlaps. 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", "CFF ", "maxp", "vmtx" and "VORG"), and no OpenType layout tables. """ return InterpolatableOTFCompiler(**kwargs).compile_designspace(designSpaceDoc) def compileVariableTTF(designSpaceDoc, **kwargs): """Create FontTools TrueType variable font from the DesignSpaceDocument UFO sources with interpolatable outlines, using fontTools.varLib.build. *optimizeGvar*, if set to False, will not perform IUP optimization on the generated 'gvar' table. *excludeVariationTables* is a list of sfnt table tags (str) that is passed on to fontTools.varLib.build, to skip building some variation tables. The rest of the arguments works the same as in the other compile functions. Returns a new variable TTFont object. """ fonts = VariableTTFsCompiler(**kwargs).compile_variable(designSpaceDoc) if len(fonts) != 1: raise ValueError( "Tried to build a DesignSpace version 5 with multiple variable " "fonts using the old ufo2ft API `compileVariableTTF`. " "Use the new API instead `compileVariableTTFs`" ) return next(iter(fonts.values())) def compileVariableCFF2(designSpaceDoc, **kwargs): fonts = VariableCFF2sCompiler(**kwargs).compile_variable(designSpaceDoc) if len(fonts) != 1: raise ValueError( "Tried to build a DesignSpace version 5 with multiple variable " "fonts using the old ufo2ft API `compileVariableCFF2`. " "Use the new API instead `compileVariableCFF2s`" ) return next(iter(fonts.values())) def compileVariableCFF2s(designSpaceDoc, **kwargs): """Create FontTools TrueType variable fonts for each variable font defined in the given DesignSpaceDocument, using their UFO sources with interpolatable outlines, using fontTools.varLib.build. *optimizeGvar*, if set to False, will not perform IUP optimization on the generated 'gvar' table. *excludeVariationTables* is a list of sfnt table tags (str) that is passed on to fontTools.varLib.build, to skip building some variation tables. *variableFontNames* is an optional list of names of variable fonts to build. If not provided, all variable fonts listed in the given designspace will by built. *allQuadratic* (bool) specifies whether to convert all curves to quadratic - True by default, builds traditional glyf v0 table. If False, quadratic curves or cubic curves are generated depending on which has fewer points; a glyf v1 is generated. The rest of the arguments works the same as in the other compile functions. Returns a dictionary that maps each variable font filename to a new variable TTFont object. If no variable fonts are defined in the Designspace, returns an empty dictionary. .. versionadded:: 2.28.0 """ return VariableCFF2sCompiler(**kwargs).compile_variable(designSpaceDoc) ufo2ft-3.3.1/Lib/ufo2ft/_compilers/000077500000000000000000000000001470175262700170055ustar00rootroot00000000000000ufo2ft-3.3.1/Lib/ufo2ft/_compilers/__init__.py000066400000000000000000000000001470175262700211040ustar00rootroot00000000000000ufo2ft-3.3.1/Lib/ufo2ft/_compilers/baseCompiler.py000066400000000000000000000477071470175262700220030ustar00rootroot00000000000000import logging from collections import defaultdict from dataclasses import dataclass, field 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 COMPRESSION_LEVEL as GPOS_COMPRESSION_LEVEL from ufo2ft.constants import MTI_FEATURES_PREFIX from ufo2ft.errors import InvalidDesignSpaceData from ufo2ft.featureCompiler import ( FeatureCompiler, MtiFeatureCompiler, VariableFeatureCompiler, _featuresCompatible, ) from ufo2ft.instantiator import Instantiator from ufo2ft.postProcessor import PostProcessor from ufo2ft.util import ( _LazyFontName, _notdefGlyphFallback, colrClipBoxQuantization, ensure_all_sources_have_names, getDefaultMasterFont, location_to_string, prune_unknown_kwargs, ) @dataclass class BaseCompiler: postProcessorClass: Type = PostProcessor featureCompilerClass: Optional[Type] = None featureWriters: Optional[list] = None filters: Optional[list] = None glyphOrder: Optional[list] = None useProductionNames: Optional[bool] = None removeOverlaps: bool = False overlapsBackend: Optional[str] = None inplace: bool = False layerName: Optional[str] = None skipExportGlyphs: Optional[bool] = None debugFeatureFile: Optional[str] = None notdefGlyph: Optional[str] = None colrLayerReuse: bool = True colrAutoClipBoxes: bool = True colrClipBoxQuantization: Callable[[object], int] = colrClipBoxQuantization feaIncludeDir: Optional[str] = None skipFeatureCompilation: bool = False ftConfig: dict = field(default_factory=dict) def __post_init__(self): self.logger = logging.getLogger("ufo2ft") self.timer = Timer(logging.getLogger("ufo2ft.timer"), level=logging.DEBUG) 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) with self.timer("postprocess TTF"): font = self.postprocess(font, ufo, glyphSet) return font def preprocess(self, ufo_or_ufos): self.logger.info("Pre-processing glyphs") if self.skipExportGlyphs is None: if isinstance(ufo_or_ufos, (list, tuple)): self.skipExportGlyphs = set() for ufo in ufo_or_ufos: self.skipExportGlyphs.update( ufo.lib.get("public.skipExportGlyphs", []) ) else: self.skipExportGlyphs = ufo_or_ufos.lib.get( "public.skipExportGlyphs", [] ) callables = [self.preProcessorClass] if hasattr(self.preProcessorClass, "initDefaultFilters"): callables.append(self.preProcessorClass.initDefaultFilters) preprocessor_args = prune_unknown_kwargs(self.__dict__, *callables) # Preprocessors expect this parameter under a different name. if hasattr(self, "cubicConversionError"): preprocessor_args["conversionError"] = self.cubicConversionError preProcessor = self.preProcessorClass(ufo_or_ufos, **preprocessor_args) return preProcessor.process() def compileOutlines(self, ufo, glyphSet): kwargs = prune_unknown_kwargs(self.__dict__, self.outlineCompilerClass) outlineCompiler = self.outlineCompilerClass(ufo, glyphSet=glyphSet, **kwargs) return outlineCompiler.compile() def postprocess(self, ttf, ufo, glyphSet, info=None): if self.postProcessorClass is not None: postProcessor = self.postProcessorClass( ttf, ufo, glyphSet=glyphSet, info=info ) kwargs = prune_unknown_kwargs(self.__dict__, postProcessor.process) ttf = postProcessor.process(**kwargs) return ttf def compileFeatures( self, ufo, ttFont=None, glyphSet=None, ): """Compile OpenType Layout features from `ufo` into FontTools OTL tables. If `ttFont` is None, a new TTFont object is created containing the new tables, else the provided `ttFont` is updated with the new tables. If no explicit `featureCompilerClass` is provided, the one used will depend on whether the ufo contains any MTI feature files in its 'data' directory (thus the `MTIFeatureCompiler` is used) or not (then the default FeatureCompiler for Adobe FDK features is used). If skipExportGlyphs is provided (see description in the ``compile*`` functions), the feature compiler will prune groups (removing them if empty) and kerning of the UFO of these glyphs. The feature file is left untouched. `debugFeatureFile` can be a file or file-like object opened in text mode, in which to dump the text content of the feature file, useful for debugging auto-generated OpenType features like kern, mark, mkmk etc. """ if self.featureCompilerClass is None: if any( fn.startswith(MTI_FEATURES_PREFIX) and fn.endswith(".mti") for fn in ufo.data.fileNames ): self.featureCompilerClass = MtiFeatureCompiler else: self.featureCompilerClass = FeatureCompiler kwargs = prune_unknown_kwargs(self.__dict__, self.featureCompilerClass) featureCompiler = self.featureCompilerClass( ufo, ttFont, glyphSet=glyphSet, **kwargs ) otFont = featureCompiler.compile() if self.debugFeatureFile: if hasattr(featureCompiler, "writeFeatures"): featureCompiler.writeFeatures(self.debugFeatureFile) return otFont @dataclass class BaseInterpolatableCompiler(BaseCompiler): """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. """ extraSubstitutions: Optional[dict] = None variableFontNames: Optional[list] = None # used to generate glyph instances on-the-fly (e.g. decomposing sparse composites) instantiator: Optional[Instantiator] = field(init=False, default=None) # We may need to compile things differently based on whether the source is default # or not: e.g. handling of composite glyphs pointing to missing components. compilingVFDefaultSource: bool = field(init=False, default=True) def compile(self, ufos): if self.layerNames is None: self.layerNames = [None] * len(ufos) assert len(ufos) == len(self.layerNames) self.glyphSets = self.preprocess(ufos) default_idx = ( self.instantiator.default_source_idx if self.instantiator else None ) for i, (ufo, glyphSet, layerName) in enumerate( zip(ufos, self.glyphSets, self.layerNames) ): if default_idx is not None: self.compilingVFDefaultSource = i == default_idx yield self.compile_one(ufo, glyphSet, layerName) def compile_one(self, ufo, glyphSet, layerName): fontName = _LazyFontName(ufo) if layerName is not None: self.logger.info("Building OpenType tables for %s-%s", fontName, layerName) else: self.logger.info("Building OpenType tables for %s", fontName) ttf = self.compileOutlines(ufo, glyphSet, layerName) # Only the default layer is likely to have all glyphs used in feature # code. if layerName is None and not self.skipFeatureCompilation: if self.debugFeatureFile: self.debugFeatureFile.write("\n### %s ###\n" % fontName) self.compileFeatures(ufo, ttf, glyphSet=glyphSet) ttf = self.postprocess(ttf, ufo, glyphSet) if layerName is not None and "post" in ttf: # for sparse masters (i.e. containing only a subset of the glyphs), we # need to include the post table in order to store glyph names, so that # fontTools.varLib can interpolate glyphs with same name across masters. # However we want to prevent the underlinePosition/underlineThickness # fields in such sparse masters to be included when computing the deltas # for the MVAR table. Thus, we set them to this unlikely, limit value # (-36768) which is a signal varLib should ignore them when building MVAR. ttf["post"].underlinePosition = -0x8000 ttf["post"].underlineThickness = -0x8000 return ttf 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.glyphSets, self.layerNames = [], [], [] for source in designSpaceDoc.sources: if source.font is None: raise AttributeError( "designspace source '%s' is missing required 'font' attribute" % getattr(source, "name", "") ) ufos.append(source.font) # 'layerName' is None for the default layer self.layerNames.append(source.layerName) self.skipExportGlyphs = designSpaceDoc.lib.get("public.skipExportGlyphs", []) if self.notdefGlyph is None: self.notdefGlyph = _notdefGlyphFallback(designSpaceDoc) self.extraSubstitutions = defaultdict(set) for rule in designSpaceDoc.rules: for left, right in rule.subs: self.extraSubstitutions[left].add(right) # used to interpolate glyphs on-the-fly in filters (e.g. DecomposeComponents) self.instantiator = Instantiator.from_designspace( designSpaceDoc, round_geometry=False, do_info=False, do_kerning=False ) return ufos def _post_compile_designspace(self, designSpaceDoc, fonts): if self.inplace: result = designSpaceDoc else: result = designSpaceDoc.deepcopyExceptFonts() for source, font in zip(result.sources, fonts): source.font = font return result def _compileNeededSources(self, designSpaceDoc): # We'll need to map elements to TTFonts, to do so make sure that # each has a name. ensure_all_sources_have_names(designSpaceDoc) # Go through VFs to build and gather list of needed sources to compile interpolableSubDocs = [ subDoc for _location, subDoc in splitInterpolable(designSpaceDoc) ] vfNameToBaseUfo = {} sourcesToCompile = set() for subDoc in interpolableSubDocs: for vfName, vfDoc in splitVariableFonts(subDoc): if ( self.variableFontNames is not None and vfName not in self.variableFontNames ): # This VF is not needed so we don't need to compile its sources continue default_source = vfDoc.findDefault() if default_source is None: default_location = location_to_string(vfDoc.newDefaultLocation()) master_locations = [] for sourceDescriptor in vfDoc.sources: master_location = sourceDescriptor.name + " at " master_location += location_to_string( sourceDescriptor.getFullDesignLocation(vfDoc) ) master_locations.append(master_location) master_location_descriptions = "\n".join(master_locations) raise InvalidDesignSpaceData( f"No default source; expected default master at {default_location}." f" Found master locations:\n{master_location_descriptions}" ) vfNameToBaseUfo[vfName] = ( default_source.font, vfDoc.lib.get("public.fontInfo"), ) for source in vfDoc.sources: sourcesToCompile.add(source.name) # Match sources to compile to their Descriptor in the original designspace sourcesByName = {} for source in designSpaceDoc.sources: 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 all( _featuresCompatible(doc) for doc in interpolableSubDocs ) if can_optimize_features: self.logger.info("Features are compatible across masters; building later") originalSources = {} originalGlyphsets = {} # Disable GPOS compaction while building masters because the compaction # will be undone anyway by varLib merge and then done again on the final VF gpos_compact_value = self.ftConfig.pop(GPOS_COMPRESSION_LEVEL, None) # we want to rename glyphs only on the final VF and skip postprocessing masters save_production_names, self.useProductionNames = self.useProductionNames, False save_postprocessor, self.postProcessorClass = self.postProcessorClass, None # skip per-master feature compilation if we are building variable features save_skip_features, self.skipFeatureCompilation = ( self.skipFeatureCompilation, can_optimize_features, ) try: # Compile all needed sources in each interpolable subspace to make sure # they're all compatible; that also ensures that sub-vfs within the same # interpolable sub-space are compatible too. for subDoc in interpolableSubDocs: # Only keep the sources that we've identified earlier as need-to-compile subDoc.sources = [ s for s in subDoc.sources if s.name in sourcesToCompile ] if not subDoc.sources: continue ttfDesignSpace = self.compile_designspace(subDoc) if gpos_compact_value is not None: # the VF will inherit the config from the base TTF master baseTtf = getDefaultMasterFont(ttfDesignSpace) baseTtf.cfg[GPOS_COMPRESSION_LEVEL] = gpos_compact_value # Stick TTFs back into original big DS 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 finally: # can restore self to its original state if gpos_compact_value is not None: self.ftConfig[GPOS_COMPRESSION_LEVEL] = gpos_compact_value self.postProcessorClass = save_postprocessor self.useProductionNames = save_production_names self.skipFeatureCompilation = save_skip_features return ( vfNameToBaseUfo, can_optimize_features, originalSources, originalGlyphsets, ) def compile_variable(self, designSpaceDoc): if not self.inplace: designSpaceDoc = designSpaceDoc.deepcopyExceptFonts() ( vfNameToBaseUfo, buildVariableFeatures, originalSources, originalGlyphsets, ) = self._compileNeededSources(designSpaceDoc) if not vfNameToBaseUfo: return {} 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()): ufo, info = vfNameToBaseUfo[vfName] vfNameToTTFont[vfName] = self.postprocess( varfont, ufo, glyphSet=None, info=info ) 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) ufo2ft-3.3.1/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py000066400000000000000000000031021470175262700244240ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional, Type from fontTools import varLib from ufo2ft.constants import SPARSE_OTF_MASTER_TABLES, CFFOptimization from ufo2ft.outlineCompiler import OutlineOTFCompiler from ufo2ft.preProcessor import OTFInterpolatablePreProcessor from ufo2ft.util import prune_unknown_kwargs from .baseCompiler import BaseInterpolatableCompiler @dataclass class InterpolatableOTFCompiler(BaseInterpolatableCompiler): preProcessorClass: Type = OTFInterpolatablePreProcessor outlineCompilerClass: Type = OutlineOTFCompiler roundTolerance: Optional[float] = None optimizeCFF: CFFOptimization = CFFOptimization.NONE colrLayerReuse: bool = False colrAutoClipBoxes: bool = False skipFeatureCompilation: bool = False def compileOutlines(self, ufo, glyphSet, layerName=None): kwargs = prune_unknown_kwargs(self.__dict__, self.outlineCompilerClass) kwargs["tables"] = SPARSE_OTF_MASTER_TABLES if layerName is not None else None kwargs["optimizeCFF"] = CFFOptimization.NONE outlineCompiler = self.outlineCompilerClass(ufo, glyphSet=glyphSet, **kwargs) return outlineCompiler.compile() def _merge(self, designSpaceDoc, excludeVariationTables): return varLib.build_many( designSpaceDoc, exclude=excludeVariationTables, optimize=self.optimizeCFF >= CFFOptimization.SPECIALIZE, skip_vf=lambda vf_name: self.variableFontNames and vf_name not in self.variableFontNames, colr_layer_reuse=self.colrLayerReuse, ) ufo2ft-3.3.1/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py000066400000000000000000000042171470175262700244410ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional, Type from fontTools import varLib from ufo2ft.constants import SPARSE_TTF_MASTER_TABLES from ufo2ft.outlineCompiler import OutlineTTFCompiler from ufo2ft.preProcessor import TTFInterpolatablePreProcessor from ufo2ft.util import prune_unknown_kwargs from .baseCompiler import BaseInterpolatableCompiler @dataclass class InterpolatableTTFCompiler(BaseInterpolatableCompiler): preProcessorClass: Type = TTFInterpolatablePreProcessor outlineCompilerClass: Type = OutlineTTFCompiler convertCubics: bool = True cubicConversionError: Optional[float] = None reverseDirection: bool = True flattenComponents: bool = False layerNames: Optional[str] = None colrLayerReuse: bool = False colrAutoClipBoxes: bool = False autoUseMyMetrics: bool = True allQuadratic: bool = True skipFeatureCompilation: bool = False def compileOutlines(self, ufo, glyphSet, layerName=None): kwargs = prune_unknown_kwargs(self.__dict__, self.outlineCompilerClass) kwargs["glyphDataFormat"] = 0 if self.allQuadratic else 1 kwargs["tables"] = SPARSE_TTF_MASTER_TABLES if layerName else None # we want to keep coordinates as floats in glyf masters so that fonttools # can compute impliable on-curve points from unrounded coordinates before # building the VF kwargs["roundCoordinates"] = False # keep impliable oncurve points in the interpolatable master TTFs, they will # be pruned at the end by varLib in the final VF. kwargs["dropImpliedOnCurves"] = False outlineCompiler = self.outlineCompilerClass(ufo, glyphSet=glyphSet, **kwargs) return outlineCompiler.compile() def _merge(self, designSpaceDoc, excludeVariationTables): return varLib.build_many( designSpaceDoc, exclude=excludeVariationTables, optimize=self.optimizeGvar, skip_vf=lambda vf_name: self.variableFontNames and vf_name not in self.variableFontNames, colr_layer_reuse=self.colrLayerReuse, drop_implied_oncurves=self.dropImpliedOnCurves, ) ufo2ft-3.3.1/Lib/ufo2ft/_compilers/otfCompiler.py000066400000000000000000000010731470175262700216430ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional, Type from ufo2ft.constants import CFFOptimization from ufo2ft.outlineCompiler import OutlineOTFCompiler from ufo2ft.preProcessor import OTFPreProcessor from .baseCompiler import BaseCompiler @dataclass class OTFCompiler(BaseCompiler): preProcessorClass: Type = OTFPreProcessor outlineCompilerClass: Type = OutlineOTFCompiler optimizeCFF: CFFOptimization = CFFOptimization.SUBROUTINIZE roundTolerance: Optional[float] = None cffVersion: int = 1 subroutinizer: Optional[str] = None ufo2ft-3.3.1/Lib/ufo2ft/_compilers/ttfCompiler.py000066400000000000000000000017571470175262700216610ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional, Type from ufo2ft.outlineCompiler import OutlineTTFCompiler from ufo2ft.preProcessor import TTFPreProcessor from ufo2ft.util import prune_unknown_kwargs from .baseCompiler import BaseCompiler @dataclass class TTFCompiler(BaseCompiler): preProcessorClass: Type = TTFPreProcessor outlineCompilerClass: Type = OutlineTTFCompiler convertCubics: bool = True cubicConversionError: Optional[float] = None reverseDirection: bool = True rememberCurveType: bool = True flattenComponents: bool = False autoUseMyMetrics: bool = True dropImpliedOnCurves: bool = False allQuadratic: bool = True 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) return outlineCompiler.compile() ufo2ft-3.3.1/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py000066400000000000000000000013131470175262700234210ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional, Type from ufo2ft.constants import CFFOptimization from ufo2ft.outlineCompiler import OutlineOTFCompiler from ufo2ft.preProcessor import OTFInterpolatablePreProcessor from .interpolatableOTFCompiler import InterpolatableOTFCompiler @dataclass class VariableCFF2sCompiler(InterpolatableOTFCompiler): preProcessorClass: Type = OTFInterpolatablePreProcessor outlineCompilerClass: Type = OutlineOTFCompiler roundTolerance: Optional[float] = None colrAutoClipBoxes: bool = False cffVersion: int = 2 optimizeCFF: CFFOptimization = CFFOptimization.SPECIALIZE excludeVariationTables: tuple = () variableFeatures: bool = True ufo2ft-3.3.1/Lib/ufo2ft/_compilers/variableTTFsCompiler.py000066400000000000000000000014661470175262700234070ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional, Type from ufo2ft.outlineCompiler import OutlineTTFCompiler from ufo2ft.preProcessor import TTFInterpolatablePreProcessor from .interpolatableTTFCompiler import InterpolatableTTFCompiler @dataclass class VariableTTFsCompiler(InterpolatableTTFCompiler): preProcessorClass: Type = TTFInterpolatablePreProcessor outlineCompilerClass: Type = OutlineTTFCompiler convertCubics: bool = True cubicConversionError: Optional[float] = None reverseDirection: bool = True flattenComponents: bool = False excludeVariationTables: tuple = () optimizeGvar: bool = True colrAutoClipBoxes: bool = False autoUseMyMetrics: bool = True dropImpliedOnCurves: bool = False allQuadratic: bool = True variableFeatures: bool = True ufo2ft-3.3.1/Lib/ufo2ft/constants.py000066400000000000000000000106001470175262700172340ustar00rootroot00000000000000from enum import IntEnum from types import MappingProxyType class CFFOptimization(IntEnum): NONE = 0 SPECIALIZE = 1 SUBROUTINIZE = 2 SPARSE_TTF_MASTER_TABLES = frozenset( ["glyf", "head", "hmtx", "loca", "maxp", "post", "vmtx", "cvt ", "fpgm", "prep"] ) SPARSE_OTF_MASTER_TABLES = frozenset(["CFF ", "VORG", "head", "hmtx", "maxp", "vmtx"]) UFO2FT_PREFIX = "com.github.googlei18n.ufo2ft." GLYPHS_PREFIX = "com.schriftgestaltung." FILTERS_KEY = UFO2FT_PREFIX + "filters" MTI_FEATURES_PREFIX = UFO2FT_PREFIX + "mtiFeatures" FEATURE_WRITERS_KEY = UFO2FT_PREFIX + "featureWriters" USE_PRODUCTION_NAMES = UFO2FT_PREFIX + "useProductionNames" GLYPHS_DONT_USE_PRODUCTION_NAMES = GLYPHS_PREFIX + "Don't use Production Names" KEEP_GLYPH_NAMES = UFO2FT_PREFIX + "keepGlyphNames" COLOR_LAYERS_KEY = UFO2FT_PREFIX + "colorLayers" COLOR_PALETTES_KEY = UFO2FT_PREFIX + "colorPalettes" COLOR_LAYER_MAPPING_KEY = UFO2FT_PREFIX + "colorLayerMapping" # sequence of [glyphs, clipBox], where 'glyphs' is in turn a sequence of # glyph names, and 'clipBox' a 5- or 4-item sequence of numbers: # Sequence[ # Sequence[ # Sequence[str, ...], # glyph names # Union[ # Sequence[float, float, float, float, float], # variable box # Sequence[float, float, float, float], # non-variable box # ] # ], # ... # ] COLR_CLIP_BOXES_KEY = UFO2FT_PREFIX + "colrClipBoxes" GLYPHS_MATH_PREFIX = "com.nagwa.MATHPlugin." GLYPHS_MATH_CONSTANTS_KEY = GLYPHS_MATH_PREFIX + "constants" GLYPHS_MATH_VARIANTS_KEY = GLYPHS_MATH_PREFIX + "variants" GLYPHS_MATH_EXTENDED_SHAPE_KEY = GLYPHS_MATH_PREFIX + "extendedShape" OBJECT_LIBS_KEY = "public.objectLibs" OPENTYPE_CATEGORIES_KEY = "public.openTypeCategories" OPENTYPE_META_KEY = "public.openTypeMeta" OPENTYPE_POST_UNDERLINE_POSITION_KEY = "public.openTypePostUnderlinePosition" 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" UNICODE_SCRIPT_ALIASES = MappingProxyType({"Hira": "Hrkt", "Kana": "Hrkt"}) # HarfBuzz passes Sinhala to the Indic shaper, while OpenType moved it to the USE shaper. INDIC_SCRIPTS = [ "Beng", # Bengali "Deva", # Devanagari "Gujr", # Gujarati "Guru", # Gurmukhi "Knda", # Kannada "Mlym", # Malayalam "Orya", # Oriya "Sinh", # Sinhala "Taml", # Tamil "Telu", # Telugu ] USE_SCRIPTS = [ # Correct as at Unicode 15.0 "Adlm", # Adlam "Ahom", # Ahom "Bali", # Balinese "Batk", # Batak "Brah", # Brahmi "Bugi", # Buginese "Buhd", # Buhid "Cakm", # Chakma "Cham", # Cham "Chrs", # Chorasmian "Cpmn", # Cypro Minoan "Diak", # Dives Akuru "Dogr", # Dogra "Dupl", # Duployan "Egyp", # Egyptian Hieroglyphs "Elym", # Elymaic "Gong", # Gunjala Gondi "Gonm", # Masaram Gondi "Gran", # Grantha "Hano", # Hanunoo "Hmng", # Pahawh Hmong "Hmnp", # Nyiakeng Puachue Hmong "Java", # Javanese "Kali", # Kayah Li "Kawi", # Kawi "Khar", # Kharosthi "Khoj", # Khojki "Kits", # Khitan Small Script "Kthi", # Kaithi "Lana", # Tai Tham "Lepc", # Lepcha "Limb", # Limbu "Mahj", # Mahajani "Maka", # Makasar "Mand", # Mandaic "Mani", # Manichaean "Marc", # Marchen "Medf", # Medefaidrin "Modi", # Modi "Mong", # Mongolian "Mtei", # Meetei Mayek "Mult", # Multani "Nagm", # Nag Mundari "Nand", # Nandinagari "Newa", # Newa "Nhks", # Bhaiksuki "Nko ", # Nko "Ougr", # Old Uyghur "Phag", # Phags Pa "Phlp", # Psalter Pahlavi "Plrd", # Miao "Rjng", # Rejang "Rohg", # Hanifi Rohingya "Saur", # Saurashtra "Shrd", # Sharada "Sidd", # Siddham "Sind", # Khudawadi "Sogd", # Sogdian "Sogo", # Old Sogdian "Soyo", # Soyombo "Sund", # Sundanese "Sylo", # Syloti Nagri "Tagb", # Tagbanwa "Takr", # Takri "Tale", # Tai Le "Tavt", # Tai Viet "Tfng", # Tifinagh "Tglg", # Tagalog "Tibt", # Tibetan "Tirh", # Tirhuta "Tnsa", # Tangsa "Toto", # Toto "Vith", # Vithkuqi "Wcho", # Wancho "Yezi", # Yezidi "Zanb", # Zanabazar Square ] ufo2ft-3.3.1/Lib/ufo2ft/errors.py000066400000000000000000000006421470175262700165410ustar00rootroot00000000000000class Error(Exception): """Base exception class for all ufo2ft errors.""" pass class InvalidFontData(Error): """Raised when input font contains invalid data.""" pass class InvalidFeaturesData(Error): """Raised when input font contains invalid features data.""" pass class InvalidDesignSpaceData(Error): """Raised when input DesignSpace document contains invalid data.""" pass ufo2ft-3.3.1/Lib/ufo2ft/featureCompiler.py000066400000000000000000000433531470175262700203610ustar00rootroot00000000000000from __future__ import annotations import logging import os import re from collections import OrderedDict from inspect import isclass from io import StringIO 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 from fontTools.misc.loggingTools import Timer from ufo2ft.constants import MTI_FEATURES_PREFIX from ufo2ft.featureWriters import ( CursFeatureWriter, GdefFeatureWriter, KernFeatureWriter, MarkFeatureWriter, ast, isValidFeatureWriter, loadFeatureWriters, ) from ufo2ft.util import describe_ufo logger = logging.getLogger(__name__) timer = Timer(logging.getLogger("ufo2ft.timer"), level=logging.DEBUG) def parseLayoutFeatures(font, includeDir=None): """Parse OpenType layout features in the UFO and return a feaLib.ast.FeatureFile instance. includeDir is an optional directory path to search for included feature files, if omitted the font.path is used. If the latter is also not set, the feaLib Lexer uses the current working directory. """ featxt = font.features.text or "" if not featxt: return ast.FeatureFile() buf = StringIO(featxt) ufoPath = font.path if includeDir is None and ufoPath is not None: # The UFO v3 specification says "Any include() statements must be relative to # the UFO path, not to the features.fea file itself". We set the `name` # attribute on the buffer to the actual feature file path, which feaLib will # pick up and use to attribute errors to the correct file, and explicitly set # the include directory to the parent of the UFO. ufoPath = os.path.normpath(ufoPath) buf.name = os.path.join(ufoPath, "features.fea") includeDir = os.path.dirname(ufoPath) or "." glyphNames = set(font.keys()) includeDir = os.path.normpath(includeDir) if includeDir else None try: parser = Parser(buf, glyphNames, includeDir=includeDir) doc = parser.parse() except IncludedFeaNotFound as e: if ufoPath and os.path.exists(os.path.join(ufoPath, e.args[0])): logger.warning( "Please change the file name in the include(...); " "statement to be relative to the UFO itself, " "instead of relative to the 'features.fea' file " "contained in it." ) raise return doc class BaseFeatureCompiler: """Base class for generating OpenType features and compiling OpenType layout tables from these. """ def __init__(self, ufo, ttFont=None, glyphSet=None, extraSubstitutions=None): """ Args: ufo: an object representing a UFO (defcon.Font or equivalent) containing the features source data. ttFont: a fontTools TTFont object where the generated OpenType tables are added. If None, an empty TTFont is used, with the same glyph order as the ufo object. glyphSet: a (optional) dict containing pre-processed copies of the UFO glyphs. extraSubstitutions: an optional dictionary mapping glyph names to a set of other glyphs which should be considered reachable from them (for example when using designspace rules to effect substitutions). """ self.ufo = ufo if ttFont is None: from fontTools.ttLib import TTFont from ufo2ft.util import makeOfficialGlyphOrder ttFont = TTFont() ttFont.setGlyphOrder(makeOfficialGlyphOrder(ufo)) self.ttFont = ttFont 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 self.glyphSet = OrderedDict((gn, glyphSet[gn]) for gn in glyphOrder) self.extraSubstitutions = extraSubstitutions def setupFeatures(self): """Make the features source. **This should not be called externally.** Subclasses must override this method. """ raise NotImplementedError def buildTables(self): """Compile OpenType feature tables from the source. **This should not be called externally.** Subclasses must override this method. """ raise NotImplementedError def setupFile_features(self): """DEPRECATED. Use 'setupFeatures' instead.""" _deprecateMethod("setupFile_features", "setupFeatures") self.setupFeatures() def setupFile_featureTables(self): """DEPRECATED. Use 'setupFeatures' instead.""" _deprecateMethod("setupFile_featureTables", "buildTables") self.buildTables() def compile(self): if "setupFile_features" in self.__class__.__dict__: _deprecateMethod("setupFile_features", "setupFeatures") self.setupFile_features() else: self.setupFeatures() if "setupFile_featureTables" in self.__class__.__dict__: _deprecateMethod("setupFile_featureTables", "buildTables") self.setupFile_featureTables() else: self.buildTables() return self.ttFont def _deprecateMethod(arg, repl): import warnings warnings.warn( f"{arg!r} method is deprecated; use {repl!r} instead", category=UserWarning, stacklevel=3, ) class FeatureCompiler(BaseFeatureCompiler): """Generate automatic features and compile OpenType tables from Adobe Feature File stored in the UFO, using fontTools.feaLib as compiler. """ defaultFeatureWriters = [ CursFeatureWriter, KernFeatureWriter, MarkFeatureWriter, GdefFeatureWriter, ] def __init__( self, ufo, ttFont=None, glyphSet=None, featureWriters=None, feaIncludeDir=None, extraSubstitutions=None, **kwargs, ): """ Args: featureWriters: a list of BaseFeatureWriter subclasses or pre-initialized instances. The default value (None) means that: - first, the UFO lib will be searched for a list of featureWriters under the key "com.github.googlei18n.ufo2ft.featureWriters" (see loadFeatureWriters). - if that is not found, the default list of writers will be used: (see FeatureCompiler.defaultFeatureWriters, and the individual feature writer classes for the list of features generated). If the featureWriters list is empty, no automatic feature is generated and only pre-existing features are compiled. The ``featureWriters`` parameter overrides both the writers from the UFO lib and the default writers list. To extend instead of replace the latter, the list can contain a special value ``...`` (i.e. the ``ellipsis`` singleton, not the str literal '...') which gets replaced by either the UFO.lib writers or the default ones; thus one can insert additional writers either before or after these. feaIncludeDir: a directory to be used as the include directory for the feature file. If None, the include directory is set to the parent directory of the UFO, provided the UFO has a path. """ BaseFeatureCompiler.__init__( self, ufo, ttFont, glyphSet, extraSubstitutions=extraSubstitutions ) self.feaIncludeDir = feaIncludeDir self.initFeatureWriters(featureWriters) if kwargs.get("mtiFeatures") is not None: import warnings warnings.warn( "mtiFeatures argument is ignored; " "you should use MtiLibFeatureCompiler", category=UserWarning, stacklevel=2, ) def _load_custom_feature_writers(self, featureWriters=None): # Args: # ufo: Font # featureWriters: Optional[List[Union[FeatureWriter, EllipsisType]]]) # Returns: List[FeatureWriter] # by default, load the feature writers from the lib or the default ones; # ellipsis is used as a placeholder so one can optionally insert additional # featureWriters=[w1, ..., w2] either before or after these, or override # them by omitting the ellipsis. if featureWriters is None: featureWriters = [...] result = [] seen_ellipsis = False for writer in featureWriters: if writer is ...: if seen_ellipsis: raise ValueError("ellipsis not allowed more than once") writers = loadFeatureWriters(self.ufo) if writers is not None: result.extend(writers) else: result.extend(self.defaultFeatureWriters) seen_ellipsis = True else: klass = writer if isclass(writer) else type(writer) if not isValidFeatureWriter(klass): raise TypeError(f"Invalid feature writer: {writer!r}") result.append(writer) return result def initFeatureWriters(self, featureWriters=None): """Initialize feature writer classes as specified in the UFO lib. If none are defined in the UFO, the default feature writers are used (see FeatureCompiler.defaultFeatureWriters). The 'featureWriters' argument can be used to override these. The method sets the `self.featureWriters` attribute with the list of writers. Note that the writers that generate GSUB features are placed first in this list, before all others. This is because the GSUB table may be used in the subsequent feature writers to resolve substitutions from glyphs with unicodes to their alternates. """ featureWriters = self._load_custom_feature_writers(featureWriters) gsubWriters = [] others = [] for writer in featureWriters: if isclass(writer): writer = writer() if writer.tableTag == "GSUB": gsubWriters.append(writer) else: others.append(writer) self.featureWriters = gsubWriters + others def setupFeatures(self): """ Make the features source. **This should not be called externally.** Subclasses may override this method to handle the file creation in a different way if desired. """ with timer("run feature writers"): if self.featureWriters: featureFile = parseLayoutFeatures(self.ufo, self.feaIncludeDir) # Insertion markers are only considered in "skip" mode. if any(writer.mode == "skip" for writer in self.featureWriters): markers = { writer.insertFeatureMarker for writer in self.featureWriters if writer.insertFeatureMarker is not None } warn_about_miscased_insertion_markers( describe_ufo(self.ufo), featureFile, markers ) path = self.ufo.path for writer in self.featureWriters: try: writer.write(self.ufo, featureFile, compiler=self) except FeatureLibError: if path is None: self._write_temporary_feature_file(featureFile.asFea()) raise # 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 writeFeatures(self, outfile): if hasattr(self, "features"): outfile.write(self.features) def buildTables(self): """ Compile OpenType feature tables from the source. Raises a FeaLibError if the feature compilation was unsuccessful. **This should not be called externally.** Subclasses may override this method to handle the table compilation in a different way if desired. """ if not self.features: return # the path is used by the lexer to follow 'include' statements; # if we generated some automatic features, includes have already been # resolved, and we work from a string which does't exist on disk path = self.ufo.path if not self.featureWriters else None with timer("build OpenType features"): try: addOpenTypeFeaturesFromString(self.ttFont, self.features, filename=path) except FeatureLibError: if path is None: self._write_temporary_feature_file(self.features) raise def _write_temporary_feature_file(self, features: str) -> None: # if compilation fails, create temporary file for inspection data = features.encode("utf-8") with NamedTemporaryFile(delete=False) as tmp: tmp.write(data) logger.error("Compilation failed! Inspect temporary file: %r", tmp.name) class MtiFeatureCompiler(BaseFeatureCompiler): """Compile OpenType layout tables from MTI feature files using fontTools.mtiLib. """ def setupFeatures(self): ufo = self.ufo features = {} # includes the length of the "/" separator prefixLength = len(MTI_FEATURES_PREFIX) + 1 for fn in ufo.data.fileNames: if fn.startswith(MTI_FEATURES_PREFIX) and fn.endswith(".mti"): content = ufo.data[fn].decode("utf-8") features[fn[prefixLength:-4]] = content self.mtiFeatures = features def buildTables(self): for tag, features in self.mtiFeatures.items(): table = mtiLib.build(features.splitlines(), self.ttFont) assert table.tableTag == tag self.ttFont[tag] = table def warn_about_miscased_insertion_markers( ufo_description: str, feaFile: ast.FeatureFile, patterns: set[str] ) -> None: """Warn the user about potentially mistyped feature insertion markers.""" patterns_compiled = tuple( (re.compile(pattern), re.compile(pattern, re.IGNORECASE)) for pattern in patterns ) # NOTE: Insertion markers can only meaningfully occur in top-level feature # blocks. for block in ast.iterFeatureBlocks(feaFile): for statement in block.statements: if not isinstance(statement, ast.Comment): continue for pattern_case, pattern_ignore_case in patterns_compiled: text = str(statement) match_case = re.match(pattern_case, text) match_ignore_case = re.match(pattern_ignore_case, text) if match_ignore_case and not match_case: logger.warning( "%s: The insertion comment '%s' in the feature file is " "miscased (search pattern: %s), ignoring it.", ufo_description, 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 True when the features of the individual source UFOs are the same, or when only the default source has features. 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 sources = sorted( designSpaceDoc.sources, key=lambda source: source != designSpaceDoc.default ) assert sources[0] == designSpaceDoc.default transformed = [transform(s) for s in sources] first = transformed[0] return all(s == first for s in transformed[1:]) or all( not s for s in transformed[1:] ) ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/000077500000000000000000000000001470175262700176645ustar00rootroot00000000000000ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/__init__.py000066400000000000000000000112361470175262700220000ustar00rootroot00000000000000import importlib import logging from inspect import getfullargspec, isclass from ufo2ft.constants import FEATURE_WRITERS_KEY from ufo2ft.util import _loadPluginFromString from .baseFeatureWriter import BaseFeatureWriter from .cursFeatureWriter import CursFeatureWriter from .gdefFeatureWriter import GdefFeatureWriter from .kernFeatureWriter import KernFeatureWriter from .markFeatureWriter import MarkFeatureWriter __all__ = [ "BaseFeatureWriter", "CursFeatureWriter", "GdefFeatureWriter", "KernFeatureWriter", "MarkFeatureWriter", "loadFeatureWriters", ] logger = logging.getLogger(__name__) def isValidFeatureWriter(klass): """Return True if 'klass' is a valid feature writer class. A valid feature writer class is a class (of type 'type'), that has two required attributes: 1) 'tableTag' (str), which can be "GSUB", "GPOS", or other similar tags. 2) 'write' (bound method), with the signature matching the same method from the BaseFeatureWriter class: def write(self, font, feaFile, compiler=None) """ if not isclass(klass): logger.error("%r is not a class", klass) return False if not hasattr(klass, "tableTag"): logger.error("%r does not have required 'tableTag' attribute", klass) return False if not hasattr(klass, "write"): logger.error("%r does not have a required 'write' method", klass) return False if getfullargspec(klass.write).args != getfullargspec(BaseFeatureWriter.write).args: logger.error("%r 'write' method has incorrect signature", klass) return False return True def loadFeatureWriters(ufo, ignoreErrors=True): """Check UFO lib for key "com.github.googlei18n.ufo2ft.featureWriters", containing a list of dicts, each having the following key/value pairs: For example: { "module": "myTools.featureWriters", # default: ufo2ft.featureWriters "class": "MyKernFeatureWriter", # required "options": {"doThis": False, "doThat": True}, } Import each feature writer class from the specified module (default is the built-in ufo2ft.featureWriters), and instantiate it with the given 'options' dict. Return the list of feature writer objects. If the 'featureWriters' key is missing from the UFO lib, return None. If an exception occurs and 'ignoreErrors' is True, the exception message is logged and the invalid writer is skipped, otrherwise it's propagated. """ if FEATURE_WRITERS_KEY not in ufo.lib: return None writers = [] for wdict in ufo.lib[FEATURE_WRITERS_KEY]: try: moduleName = wdict.get("module", __name__) className = wdict["class"] options = wdict.get("options", {}) if not isinstance(options, dict): raise TypeError(type(options)) module = importlib.import_module(moduleName) klass = getattr(module, className) if not isValidFeatureWriter(klass): raise TypeError(klass) writer = klass(**options) except Exception: if ignoreErrors: logger.exception("failed to load feature writer: %r", wdict) continue raise writers.append(writer) return writers def loadFeatureWriterFromString(spec): """Take a string specifying a feature writer class to load (either a built-in writer or one defined in an external, user-defined module), initialize it with given options and return the writer object. The string must conform to the following notation: - an optional python module, followed by '::' - a required class name; the class must have a method call 'write' with the same signature as the BaseFeatureWriter. - an optional list of keyword-only arguments enclosed by parentheses Raises ValueError if the string doesn't conform to this specification; TypeError if imported name is not a feature writer class; and ImportError if the user-defined module cannot be imported. Examples: >>> loadFeatureWriterFromString("KernFeatureWriter") >>> w = loadFeatureWriterFromString("KernFeatureWriter(ignoreMarks=False)") >>> w.options.ignoreMarks False >>> w = loadFeatureWriterFromString("MarkFeatureWriter(features=['mkmk'])") >>> w.features == frozenset(['mkmk']) True >>> loadFeatureWriterFromString("ufo2ft.featureWriters::KernFeatureWriter") """ return _loadPluginFromString(spec, "ufo2ft.featureWriters", isValidFeatureWriter) ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/__main__.py000066400000000000000000000023151470175262700217570ustar00rootroot00000000000000import argparse import logging from io import StringIO from fontTools.misc.cliTools import makeOutputFileName from ufo2ft.featureCompiler import FeatureCompiler from ufo2ft.featureWriters import loadFeatureWriterFromString, logger try: import ufoLib2 loader = ufoLib2.Font.open except ImportError: import defcon loader = defcon.Font logging.basicConfig(level=logging.INFO) parser = argparse.ArgumentParser(description="Apply feature writers to a UFO file") parser.add_argument("--output", "-o", metavar="OUTPUT", help="output file name") parser.add_argument("ufo", metavar="UFO", help="UFO file") parser.add_argument( "writers", metavar="WRITER", nargs="*", help="list of feature writers to enable", ) args = parser.parse_args() if not args.output: args.output = makeOutputFileName(args.ufo) ufo = loader(args.ufo) writers = [loadFeatureWriterFromString(w) for w in args.writers] compiler = FeatureCompiler(ufo, featureWriters=writers or None) compiler.setupFeatures() buf = StringIO() compiler.writeFeatures(buf) ufo.features.text = buf.getvalue() logger.info("Written on %s" % args.output) try: ufo.save(args.output, overwrite=True) except TypeError: ufo.save(args.output) ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/ast.py000066400000000000000000000200411470175262700210220ustar00rootroot00000000000000"""Helpers to build or extract data from feaLib AST objects.""" import collections import functools import operator import re # we re-export here all the feaLib AST classes so they can be used from # writer modules with a single `from ufo2ft.featureWriters import ast` import sys from fontTools import unicodedata from fontTools.feaLib import ast self = sys.modules[__name__] for name in getattr(ast, "__all__", dir(ast)): if isinstance(getattr(ast, name), type): setattr(self, name, getattr(ast, name)) del sys, self, name def getScriptLanguageSystems(feaFile, excludeDflt=True): """Return dictionary keyed by Unicode script code containing lists of (OT_SCRIPT_TAG, [OT_LANGUAGE_TAG, ...]) tuples (excluding "DFLT" by default, unless excludeDflt is False). """ languagesByScript = collections.OrderedDict() for ls in [ st for st in feaFile.statements if isinstance(st, ast.LanguageSystemStatement) ]: if ls.script == "DFLT" and excludeDflt: continue languagesByScript.setdefault(ls.script, []).append(ls.language) langSysMap = collections.OrderedDict() for script, languages in languagesByScript.items(): sc = unicodedata.ot_tag_to_script(script) langSysMap.setdefault(sc, []).append((script, languages)) return langSysMap def iterFeatureBlocks(feaFile, tag=None): for statement in feaFile.statements: if isinstance(statement, ast.FeatureBlock): if tag is not None and statement.name != tag: continue yield statement def findFeatureTags(feaFile): return {f.name for f in iterFeatureBlocks(feaFile)} def findCommentPattern(feaFile, pattern): """ Yield a tuple of statements, starting with the parent block, followed by nested blocks if present, ending with the comment matching a given pattern. There is not parent block if the matched comment is a the root level. """ for statement in feaFile.statements: if hasattr(statement, "statements"): for res in findCommentPattern(statement, pattern): yield (statement, *res) elif isinstance(statement, ast.Comment): if re.match(pattern, str(statement)): yield (statement,) def findTable(feaLib, tag): for statement in feaLib.statements: if isinstance(statement, ast.TableBlock) and statement.name == tag: return statement def iterClassDefinitions(feaFile): for s in feaFile.statements: if isinstance(s, ast.GlyphClassDefinition): yield s elif isinstance(s, (ast.Block)): yield from iterClassDefinitions(s) LOOKUP_FLAGS = { "RightToLeft": 1, "IgnoreBaseGlyphs": 2, "IgnoreLigatures": 4, "IgnoreMarks": 8, } def makeLookupFlag(flags=None, markAttachment=None, markFilteringSet=None): if isinstance(flags, str): value = LOOKUP_FLAGS[flags] elif flags is not None: value = functools.reduce(operator.or_, [LOOKUP_FLAGS[n] for n in flags], 0) else: value = 0 if markAttachment is not None: assert isinstance(markAttachment, ast.GlyphClassDefinition) markAttachment = ast.GlyphClassName(markAttachment) if markFilteringSet is not None: assert isinstance(markFilteringSet, ast.GlyphClassDefinition) markFilteringSet = ast.GlyphClassName(markFilteringSet) return ast.LookupFlagStatement( value, markAttachment=markAttachment, markFilteringSet=markFilteringSet ) def makeGlyphClassDefinitions(groups, feaFile=None, stripPrefix=""): """Given a groups dictionary ({str: list[str]}), create feaLib GlyphClassDefinition objects for each group. Return a dict keyed by the original group name. If `stripPrefix` (str) is provided and a group name starts with it, the string will be stripped from the beginning of the class name. """ classDefs = {} if feaFile is not None: classNames = {cdef.name for cdef in iterClassDefinitions(feaFile)} else: classNames = set() lengthPrefix = len(stripPrefix) for groupName, members in sorted(groups.items()): originalGroupName = groupName if stripPrefix and groupName.startswith(stripPrefix): groupName = groupName[lengthPrefix:] className = makeFeaClassName(groupName, classNames) classNames.add(className) classDef = makeGlyphClassDefinition(className, members) classDefs[originalGroupName] = classDef return classDefs def makeGlyphClassDefinition(className, members): glyphNames = [ast.GlyphName(g) for g in members] glyphClass = ast.GlyphClass(glyphNames) classDef = ast.GlyphClassDefinition(className, glyphClass) return classDef def makeFeaClassName(name, existingClassNames=None): """Make a glyph class name which is legal to use in feature text. Ensures the name only includes characters in "A-Za-z0-9._", and isn't already defined. """ name = re.sub(r"[^A-Za-z0-9._]", r"", name) if existingClassNames is None: return name i = 1 origName = name while name in existingClassNames: name = "%s_%d" % (origName, i) i += 1 return name def addLookupReferences( feature, lookups, script=None, languages=None, exclude_dflt=False ): """Add references to named lookups to the feature's statements. If `script` (str) and `languages` (sequence of str) are provided, only register the lookup for the given script and languages, optionally with `exclude_dflt` directive. Otherwise add a global reference which will be registered for all the scripts and languages in the feature file's `languagesystems` statements. """ assert lookups if not script: for lookup in lookups: feature.statements.append(ast.LookupReferenceStatement(lookup)) return feature.statements.append(ast.ScriptStatement(script)) if exclude_dflt: for language in languages or ("dflt",): feature.statements.append( ast.LanguageStatement(language, include_default=False) ) for lookup in lookups: feature.statements.append(ast.LookupReferenceStatement(lookup)) else: feature.statements.append(ast.LanguageStatement("dflt", include_default=True)) for lookup in lookups: feature.statements.append(ast.LookupReferenceStatement(lookup)) for language in languages or (): if language == "dflt": continue feature.statements.append( ast.LanguageStatement(language, include_default=True) ) _GDEFGlyphClasses = collections.namedtuple( "_GDEFGlyphClasses", "base ligature mark component" ) def getGDEFGlyphClasses(feaLib): """Return GDEF GlyphClassDef base/mark/ligature/component glyphs, or None if no GDEF table is defined in the feature file. """ for s in feaLib.statements: if isinstance(s, ast.TableBlock) and s.name == "GDEF": for st in s.statements: if isinstance(st, ast.GlyphClassDefStatement): return _GDEFGlyphClasses( ( frozenset(st.baseGlyphs.glyphSet()) if st.baseGlyphs is not None else frozenset() ), ( frozenset(st.ligatureGlyphs.glyphSet()) if st.ligatureGlyphs is not None else frozenset() ), ( frozenset(st.markGlyphs.glyphSet()) if st.markGlyphs is not None else frozenset() ), ( frozenset(st.componentGlyphs.glyphSet()) if st.componentGlyphs is not None else frozenset() ), ) return _GDEFGlyphClasses(None, None, None, None) ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/baseFeatureWriter.py000066400000000000000000000425401470175262700236660ustar00rootroot00000000000000import logging from collections import OrderedDict from types import SimpleNamespace from fontTools.designspaceLib import DesignSpaceDocument from fontTools.feaLib.variableScalar import VariableScalar from fontTools.misc.fixedTools import otRound from ufo2ft.featureWriters import ast from ufo2ft.util import ( OpenTypeCategories, collapse_varscalar, get_userspace_location, quantize, unicodeScriptExtensions, ) INSERT_FEATURE_MARKER = r"\s*# Automatic Code.*" class BaseFeatureWriter: """Abstract features writer. The `tableTag` class attribute (str) states the tag of the OpenType Layout table which the generated features are intended for. For example: "GPOS", "GSUB", "BASE", etc. The `features` class attribute defines the set of all the features that this writer supports. If you want to only write some of the available features you can provide a smaller sequence to 'features' constructor argument. By the default all the features supported by this writer will be outputted. Two writing modes are defined here: 1) "skip" (default) will not write features if already present; 2) "append" will add additional lookups to an existing feature, if present, or it will add a new one at the end of all features. Subclasses can set a different default mode or define a different set of `_SUPPORTED_MODES`. The `options` class attribute contains a mapping of option names with their default values. These can be overridden on an instance by passing keyword arguments to the constructor. Combining manually written and automatically generated feature code can be achieved by using the `# Automatic Code` insertion marker inside the feature code. Automatically generated code for the respective feature is added in that spot. """ tableTag = None features = frozenset() mode = "skip" insertFeatureMarker = INSERT_FEATURE_MARKER options = {} _SUPPORTED_MODES = frozenset(["skip", "append"]) def __init__(self, features=None, mode=None, **kwargs): if features is not None: features = frozenset(features) assert features, "features cannot be empty" unsupported = features.difference(self.__class__.features) if unsupported: raise ValueError("unsupported: %s" % ", ".join(unsupported)) self.features = features if mode is not None: self.mode = mode if self.mode not in self._SUPPORTED_MODES: raise ValueError(self.mode) options = dict(self.__class__.options) for k in kwargs: if k not in options: raise TypeError("unsupported keyword argument: %r" % k) options[k] = kwargs[k] self.options = SimpleNamespace(**options) logger = ".".join([self.__class__.__module__, self.__class__.__name__]) self.log = logging.getLogger(logger) def setContext(self, font, feaFile, compiler=None): """Populate a temporary `self.context` namespace, which is reset after each new call to `_write` method. Subclasses can override this to provide contextual information which depends on other data, or set any temporary attributes. The default implementation sets: - the current font; - the current FeatureFile object; - the current compiler instance (only present when this writer was instantiated from a FeatureCompiler); - a set of features (tags) to be generated. If self.mode is "skip", these are all the features which are _not_ already present. - a set of all existing features (tags) in the feaFile. Returns the context namespace instance. """ todo = set(self.features) insertComments = None existing = set() if self.mode == "skip": if self.insertFeatureMarker is not None: insertComments = self.collectInsertMarkers( feaFile, self.insertFeatureMarker, todo ) # find existing feature blocks existing = ast.findFeatureTags(feaFile) # ignore features with insert marker if insertComments: existing.difference_update(insertComments.keys()) # remove existing feature without insert marker from todo list todo.difference_update(existing) self.context = SimpleNamespace( font=font, feaFile=feaFile, compiler=compiler, todo=todo, insertComments=insertComments, existingFeatures=existing, isVariable=isinstance(font, DesignSpaceDocument), ) return self.context def shouldContinue(self): """Decide whether to start generating features or return early. Returns a boolean: True to proceed, False to skip. Sublcasses may override this to skip generation based on the presence or lack of other required pieces of font data. """ if not self.context.todo: self.log.debug("No features to be generated; skipped") return False return True def write(self, font, feaFile, compiler=None): """Write features and class definitions for this font to a feaLib FeatureFile object. The main entry point for the FeatureCompiler to any of the FeatureWriters. Returns True if feature file was modified, False if no new features were generated. """ self.setContext(font, feaFile, compiler=compiler) try: if self.shouldContinue(): return self._write() else: return False finally: del self.context def _write(self): """Subclasses must override this.""" raise NotImplementedError def _insert( self, feaFile, classDefs=None, anchorDefs=None, markClassDefs=None, lookups=None, features=None, ): """ Insert feature, its classDefs or markClassDefs and lookups at insert marker comment. If the insert marker is at the top of a feature block, the feature is inserted before that block, and after if the insert marker is at the bottom. """ statements = feaFile.statements inserted = {} # First handle those with a known location, i.e. insert markers insertComments = self.context.insertComments indices = [] for ix, feature in enumerate(features): if insertComments and feature.name in insertComments: block, comment = insertComments[feature.name] markerIndex = block.statements.index(comment) onlyCommentsBefore = all( isinstance(s, ast.Comment) for s in block.statements[:markerIndex] ) onlyCommentsAfter = all( isinstance(s, ast.Comment) for s in block.statements[markerIndex:] ) # Remove insert marker(s) from feature block. del block.statements[markerIndex] # insertFeatureMarker is in a block with only comments. # Replace that block with new feature block. if onlyCommentsBefore and onlyCommentsAfter: index = statements.index(block) statements.remove(block) # insertFeatureMarker is at the top of a feature block # or only preceded by other comments. elif onlyCommentsBefore: index = statements.index(block) # insertFeatureMarker is at the bottom of a feature block # or only followed by other comments elif onlyCommentsAfter: index = statements.index(block) + 1 # insertFeatureMarker is in the middle of a feature block # preceded and followed by statements that are not comments else: index = statements.index(block) + 1 # Split statements after the insertFeatureMarker into a new block afterBlock = ast.FeatureBlock(block.name) afterBlock.statements = block.statements[markerIndex:] statements.insert(index, afterBlock) # And remove them from the original block block.statements = block.statements[:markerIndex] statements.insert(index, feature) indices.append(index) inserted[id(feature)] = True # Now walk feature list backwards and insert any dependent features for i in range(ix - 1, -1, -1): if id(features[i]) in inserted: break # Insert this before the current one i.e. at same array index statements.insert(index, features[i]) # All the indices recorded previously have now shifted up by one indices = [index] + [j + 1 for j in indices] inserted[id(features[i])] = True # Finally, deal with any remaining features for feature in features: if id(feature) in inserted: continue index = len(statements) statements.insert(index, feature) indices.append(index) minindex = min(indices) if lookups: feaFile.statements = statements = ( statements[:minindex] + lookups + statements[minindex:] ) # Write classDefs, anchorsDefs, markClassDefs, lookups at # the very top of the feature file. others = [] for defs in [classDefs, anchorDefs, markClassDefs]: if defs: others.extend(defs) others.append(ast.Comment("")) feaFile.statements = statements = others + statements @staticmethod def collectInsertMarkers(feaFile, insertFeatureMarker, featureTags): """ Returns a dictionary of tuples (block, comment) keyed by feature tag with the block that contains the comment matching the insert feature marker, for given feature tags. """ insertComments = dict() for match in ast.findCommentPattern(feaFile, insertFeatureMarker): blocks, comment = match[:-1], match[-1] if len(blocks) == 1 and isinstance(blocks[0], ast.FeatureBlock): block = blocks[0] if block.name in featureTags and block.name not in insertComments: insertComments[block.name] = (block, comment) return insertComments def makeUnicodeToGlyphNameMapping(self): """Return the Unicode to glyph name mapping for the current font.""" # Try to get the "best" Unicode cmap subtable if this writer is running # in the context of a FeatureCompiler, else create a new mapping from # the UFO glyphs compiler = self.context.compiler cmap = None if compiler is not None: table = compiler.ttFont.get("cmap") if table is not None: cmap = table.getBestCmap() if cmap is None: from ufo2ft.util import makeUnicodeToGlyphNameMapping if compiler is not None: glyphSet = compiler.glyphSet else: glyphSet = self.context.font cmap = makeUnicodeToGlyphNameMapping(glyphSet) return cmap def getOrderedGlyphSet(self): """Return OrderedDict[glyphName, glyph] sorted by glyphOrder.""" compiler = self.context.compiler if compiler is not None: return compiler.glyphSet from ufo2ft.util import _GlyphSet, makeOfficialGlyphOrder font = self.context.font # subset glyphSet by skipExportGlyphs if any glyphSet = _GlyphSet.from_layer( font, skipExportGlyphs=set(font.lib.get("public.skipExportGlyphs", [])), ) glyphOrder = makeOfficialGlyphOrder(glyphSet, font.glyphOrder) return OrderedDict((gn, glyphSet[gn]) for gn in glyphOrder) def compileGSUB(self): """Compile a temporary GSUB table from the current feature file.""" 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. if hasattr(compiler, "_gsub"): 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(feafile, glyphOrder, fvar=fvar) if compiler and not hasattr(compiler, "_gsub"): compiler._gsub = gsub return gsub def extraSubstitutions(self): compiler = self.context.compiler if compiler is not None: return compiler.extraSubstitutions def getOpenTypeCategories(self): """Return 'public.openTypeCategories' values as a tuple of sets of unassigned, bases, ligatures, marks, components.""" return OpenTypeCategories.load(self.context.font) def getGDEFGlyphClasses(self): """Return a tuple of GDEF GlyphClassDef base, ligature, mark, component glyph names. Sets are `None` if no 'public.openTypeCategories' values are defined or if no GDEF table is defined in the feature file. """ feaFile = self.context.feaFile if ast.findTable(feaFile, "GDEF") is not None: return ast.getGDEFGlyphClasses(feaFile) unassigned, bases, ligatures, marks, components = self.getOpenTypeCategories() if not any((unassigned, bases, ligatures, marks, components)): return ast._GDEFGlyphClasses(None, None, None, None) return ast._GDEFGlyphClasses( frozenset(bases), frozenset(ligatures), frozenset(marks), frozenset(components), ) def guessFontScripts(self): """Returns a set of scripts the font probably supports. This is done by: 1. Looking at all defined codepoints in a font and remembering the script of any of the codepoints if it is associated with just one script. This would remember the script of U+0780 THAANA LETTER HAA (Thaa) but not U+061F ARABIC QUESTION MARK (multiple scripts). 2. Adding explicitly declared `languagesystem` scripts on top. """ font = self.context.font glyphSet = self.context.glyphSet 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: continue for codepoint in glyph.unicodes: scripts = unicodeScriptExtensions(codepoint) if len(scripts) == 1: single_scripts.update(scripts) # Then, add explicitly declared languagesystems on top. feaScripts = ast.getScriptLanguageSystems(feaFile) 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 source.layerName is None: layer = source.font else: layer = source.font.layers[source.layerName] if glyphName not in layer: continue glyph = layer[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 ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/cursFeatureWriter.py000066400000000000000000000137221470175262700237300ustar00rootroot00000000000000from ufo2ft.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import classifyGlyphs, otRoundIgnoringVariable, unicodeScriptDirection class CursFeatureWriter(BaseFeatureWriter): """Generate a curs feature base on glyph anchors. The default mode is 'skip': i.e. if the 'curs' feature is already present in the feature file, it is not generated again. The optional 'append' mode will add extra lookups to an already existing features, if any. By default, anchors names 'entry' and 'exit' will be used to connect the 'entry' anchor of a glyph with the 'exit' anchor of the preceding glyph. """ tableTag = "GPOS" features = frozenset(["curs"]) @staticmethod def _getCursiveAnchorPairs(glyphs): anchors = set() for _, glyph in glyphs: anchors.update(a.name for a in glyph.anchors) anchorPairs = [] if "entry" in anchors and "exit" in anchors: anchorPairs.append(("entry", "exit")) for anchor in anchors: if anchor.startswith("entry.") and f"exit.{anchor[6:]}" in anchors: anchorPairs.append((anchor, f"exit.{anchor[6:]}")) return sorted(anchorPairs) @staticmethod def _hasAnchor(glyph, anchorName): return any(a.name == anchorName for a in glyph.anchors) def _makeCursiveFeature(self): cmap = self.makeUnicodeToGlyphNameMapping() if any(unicodeScriptDirection(uv) == "LTR" for uv in cmap): gsub = self.compileGSUB() extras = self.extraSubstitutions() dirGlyphs = classifyGlyphs(unicodeScriptDirection, cmap, gsub, extras) shouldSplit = "LTR" in dirGlyphs else: shouldSplit = False lookups = [] orderedGlyphSet = self.getOrderedGlyphSet().items() cursiveAnchorsPairs = self._getCursiveAnchorPairs(orderedGlyphSet) for entryName, exitName in cursiveAnchorsPairs: # If the anchors have an explicit direction suffix, don’t set # direction based on the script of the glyphs. if not entryName.endswith((".LTR", ".RTL")) and shouldSplit: # Make LTR lookup LTRlookup = self._makeCursiveLookup( ( glyph for (glyphName, glyph) in orderedGlyphSet if glyphName in dirGlyphs["LTR"] ), entryName, exitName, direction="LTR", ) if LTRlookup: lookups.append(LTRlookup) # Make RTL lookup with other glyphs RTLlookup = self._makeCursiveLookup( ( glyph for (glyphName, glyph) in orderedGlyphSet if glyphName not in dirGlyphs["LTR"] ), entryName, exitName, direction="RTL", ) if RTLlookup: lookups.append(RTLlookup) else: lookup = self._makeCursiveLookup( (glyph for (glyphName, glyph) in orderedGlyphSet), entryName, exitName, ) if lookup: lookups.append(lookup) if lookups: feature = ast.FeatureBlock("curs") feature.statements.extend(lookups) return feature def _makeCursiveLookup(self, glyphs, entryName, exitName, direction=None): statements = self._makeCursiveStatements(glyphs, entryName, exitName) if not statements: return suffix = "" if entryName != "entry": suffix = f"_{entryName[6:]}" if direction == "LTR": suffix += "_ltr" elif direction == "RTL": suffix += "_rtl" lookup = ast.LookupBlock(name=f"curs{suffix}") if entryName.endswith(".RTL"): direction = "RTL" elif entryName.endswith(".LTR"): direction = "LTR" if direction != "LTR": lookup.statements.append(ast.makeLookupFlag(("IgnoreMarks", "RightToLeft"))) else: lookup.statements.append(ast.makeLookupFlag("IgnoreMarks")) lookup.statements.extend(statements) return lookup def _getAnchors(self, glyphName, entryName, exitName): entryAnchor = None exitAnchor = None entryAnchorXY = self._getAnchor(glyphName, entryName) exitAnchorXY = self._getAnchor(glyphName, exitName) 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, entryName, exitName): cursiveAnchors = dict() statements = [] for glyph in glyphs: entryAnchor, exitAnchor = self._getAnchors(glyph.name, entryName, exitName) # A glyph can have only one of the cursive anchors (e.g. if it # attaches on one side only) if entryAnchor or exitAnchor: cursiveAnchors[ast.GlyphName(glyph.name)] = (entryAnchor, exitAnchor) if cursiveAnchors: for glyphName, anchors in cursiveAnchors.items(): statement = ast.CursivePosStatement(glyphName, *anchors) statements.append(statement) return statements def _write(self): feaFile = self.context.feaFile feature = self._makeCursiveFeature() if not feature: return False self._insert(feaFile=feaFile, features=[feature]) return True ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py000066400000000000000000000101111470175262700236460ustar00rootroot00000000000000from fontTools.feaLib.variableScalar import VariableScalar from fontTools.misc.fixedTools import otRound from ufo2ft.featureWriters import BaseFeatureWriter, ast def caretSortKey(caret): if isinstance(caret, VariableScalar): return list(caret.values.values())[0] return caret class GdefFeatureWriter(BaseFeatureWriter): """Generates a GDEF table based on OpenType Category and glyph anchors. It skips generating the GDEF if a GDEF is defined in the features. It uses the 'public.openTypeCategories' values to create the GDEF ClassDefs and the ligature caret anchors to create the GDEF ligature carets. """ tableTag = "GDEF" features = frozenset(["GlyphClassDefs", "LigatureCarets"]) insertFeatureMarker = None def setContext(self, font, feaFile, compiler=None): ctx = super().setContext(font, feaFile, compiler=compiler) ctx.gdefTableBlock = ast.findTable(self.context.feaFile, "GDEF") if ctx.gdefTableBlock: for fea in ctx.gdefTableBlock.statements: if isinstance(fea, ast.GlyphClassDefStatement): ctx.todo.discard("GlyphClassDefs") elif isinstance(fea, ast.LigatureCaretByIndexStatement) or isinstance( fea, ast.LigatureCaretByPosStatement ): ctx.todo.discard("LigatureCarets") if not ctx.todo: break ctx.orderedGlyphSet = self.getOrderedGlyphSet() if "GlyphClassDefs" in ctx.todo: ctx.openTypeCategories = self.getOpenTypeCategories() if not any(ctx.openTypeCategories): ctx.todo.remove("GlyphClassDefs") if "LigatureCarets" in ctx.todo: ctx.ligatureCarets = self._getLigatureCarets() if not ctx.ligatureCarets: ctx.todo.remove("LigatureCarets") return ctx def _getLigatureCarets(self): carets = dict() for glyphName, glyph in self.context.orderedGlyphSet.items(): glyphCarets = set() for anchor in glyph.anchors: if ( anchor.name and anchor.name.startswith("caret_") and anchor.x is not None ): glyphCarets.add(self._getAnchor(glyphName, anchor.name)[0]) elif ( anchor.name and anchor.name.startswith("vcaret_") and anchor.y is not None ): glyphCarets.add(self._getAnchor(glyphName, anchor.name)[1]) if glyphCarets: if self.context.isVariable: carets[glyphName] = sorted(glyphCarets, key=caretSortKey) else: carets[glyphName] = [otRound(c) for c in sorted(glyphCarets)] return carets def _sortedGlyphClass(self, glyphNames): return sorted(n for n in self.context.orderedGlyphSet if n in glyphNames) def _write(self): feaFile = self.context.feaFile gdefTableBlock = self.context.gdefTableBlock if not gdefTableBlock: gdefTableBlock = ast.TableBlock("GDEF") feaFile.statements.append(gdefTableBlock) if "GlyphClassDefs" in self.context.todo: categories = self.context.openTypeCategories glyphClassDefs = ast.GlyphClassDefStatement( ast.GlyphClass(self._sortedGlyphClass(categories.base)), ast.GlyphClass(self._sortedGlyphClass(categories.mark)), ast.GlyphClass(self._sortedGlyphClass(categories.ligature)), ast.GlyphClass(self._sortedGlyphClass(categories.component)), ) gdefTableBlock.statements.append(glyphClassDefs) if "LigatureCarets" in self.context.todo: ligatureCarets = [ ast.LigatureCaretByPosStatement(ast.GlyphName(glyphName), carets) for glyphName, carets in self.context.ligatureCarets.items() ] gdefTableBlock.statements.extend(ligatureCarets) return True ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/kernFeatureWriter.py000066400000000000000000001353401470175262700237140ustar00rootroot00000000000000from __future__ import annotations import itertools import logging from dataclasses import dataclass from types import SimpleNamespace 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 from ufo2ft.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import ( DFLT_SCRIPTS, classifyGlyphs, collapse_varscalar, describe_ufo, get_userspace_location, quantize, unicodeScriptExtensions, ) LOGGER = logging.getLogger(__name__) SIDE1_PREFIX = "public.kern1." SIDE2_PREFIX = "public.kern2." # In HarfBuzz the 'dist' feature is automatically enabled for these shapers: # src/hb-ot-shape-complex-myanmar.cc # src/hb-ot-shape-complex-use.cc # src/hb-ot-shape-complex-indic.cc # src/hb-ot-shape-complex-khmer.cc # We derived the list of scripts associated to each dist-enabled shaper from # `hb_ot_shape_complex_categorize` in src/hb-ot-shape-complex-private.hh DIST_ENABLED_SCRIPTS = set(INDIC_SCRIPTS) | set(["Khmr", "Mymr"]) | set(USE_SCRIPTS) RTL_BIDI_TYPES = {"R", "AL"} LTR_BIDI_TYPES = {"L", "AN", "EN"} AMBIGUOUS_BIDIS = {"R", "L"} COMMON_SCRIPTS_SET = {COMMON_SCRIPT} COMMON_CLASS_NAME = "Default" def unicodeBidiType(uv): """Return "R" for characters with RTL direction, or "L" for LTR (whether 'strong' or 'weak'), or None for neutral direction. """ char = chr(uv) bidiType = unicodedata.bidirectional(char) if bidiType in RTL_BIDI_TYPES: return "R" elif bidiType in LTR_BIDI_TYPES: return "L" else: return None def script_direction(script: str) -> str: if script == COMMON_SCRIPT: return "Auto" return script_horizontal_direction(script, "LTR") @dataclass(frozen=True, order=False) class KerningPair: __slots__ = ("side1", "side2", "value") side1: str | tuple[str, ...] side2: str | tuple[str, ...] value: float | VariableScalar def __lt__(self, other: KerningPair) -> bool: if not isinstance(other, KerningPair): return NotImplemented # Sort Kerning pairs so that glyph to glyph comes first, then glyph to # class, class to glyph, and finally class to class. This makes "kerning # exceptions" work, where more specific glyph pair values override less # specific class kerning. NOTE: Since comparisons terminate early, this # is never going to compare a str to a tuple. selfTuple = (self.firstIsClass, self.secondIsClass, self.side1, self.side2) otherTuple = (other.firstIsClass, other.secondIsClass, other.side1, other.side2) return selfTuple < otherTuple @property def firstIsClass(self) -> bool: return isinstance(self.side1, tuple) @property def secondIsClass(self) -> bool: return isinstance(self.side2, tuple) @property def firstGlyphs(self) -> tuple[str, ...]: if isinstance(self.side1, tuple): return self.side1 else: return (self.side1,) @property def secondGlyphs(self) -> tuple[str, ...]: if isinstance(self.side2, tuple): return self.side2 else: return (self.side2,) @property def glyphs(self) -> tuple[str, ...]: return (*self.firstGlyphs, *self.secondGlyphs) class KernFeatureWriter(BaseFeatureWriter): """Generates a kerning feature based on groups and rules contained in an UFO's kerning data. If the `quantization` argument is given in the filter options, the resulting anchors are rounded to the nearest multiple of the quantization value. ## Implementation Notes The algorithm works like this: * Parse GDEF GlyphClassDefinition from UFO features.fea to get the set of "Mark" glyphs (this will be used later to decide whether to add ignoreMarks flag to kern lookups containing pairs between base and mark glyphs). * Get the ordered glyphset for the font, for filtering kerning groups and kernings that reference unknown glyphs. * Determine which scripts the kerning affects (read: "the font most probably supports"), to know which lookups to generate later: * First, determine the unambiguous script associations for each (Unicoded) glyph in the glyphset, as in, glyphs that have a single entry for their Unicode script extensions property; * then, parse the `languagesystem` statements in the provided feature file to add on top. * Compile a Unicode cmap from the UFO and a GSUB table from the features so far, so we can determine: * the script (extensions) for each glyph in the glyphset, including glyphs reachable via substitution, using the fontTools subsetter with its `closure_glyphs` machinery; the scripts are cut down to the ones we think the font supports; * and the bidirectionality class, so we can later filter out kerning pairs that would mix RTL and LTR glyphs, which will not occur in applications. Unicode BiDi classes L, AN and EN are considered L, R and AL are considered R. * Note: the glyph script determination has the quirk of declaring "Hira" and "Kana" scripts as "Hrkt" so that they are considered one script and can be kerned against each other. * Get the kerning groups from the UFO and filter out glyphs not in the glyphset and empty groups. Remember which group a glyph is a member of, for kern1 and kern2, so we can later reconstruct per-script groups. * Get the bare kerning pairs from the UFO, filtering out pairs with unknown groups or glyphs not in the glyphset and (redundant) zero class-to-class kernings and optionally quantizing kerning values. * Start generating lookups. By default, the ignore marks flag is added to each lookup. Kerning pairs that kern bases against marks or marks against marks, according to the glyphs' GDEF category, then get split off into a second lookup without the ignore marks flag. * Go through all kerning pairs and split them up by script, to put them in different lookups. This reduces the size of each lookup compared to splitting by direction, as previously done. If there are kerning pairs with different scripts on each side, these scripts are all kept together to allow for cross-script kerning (in implementations that apply it). Scripts with different direction are always split. * Partition the first and second side of a pair by script and emit only those with the same script (e.g. `a` and `b` are both "Latn", `period` and `period` are both "Default", but `a` and `a-cy` would mix "Latn" and "Cyrl" and are dropped), or those with kerning across them, or those that kern an explicit against a "common" or "inherited" script (e.g. `a` and `period`). * Glyphs can have multiple scripts assigned to them (legitimately, e.g. U+0951 DEVANAGARI STRESS SIGN UDATTA, or for random reasons like having both `sub h by h.sc` and `sub Etaprosgegrammeni by h.sc;`). Only scripts that were determined earlier to be supported by the font will be considered. Usually, we will emit pairs where both sides have the same script and no splitting is necessary. A glyph can be part of both for weird reasons, so we always treat any glyph with a common or inherited script as a purely common (not inherited) glyph for bucketing purposes. This avoids creating overlapping groups with the multi-script glyph in a lookup. * Some glyphs may have a script of Zyyy or Zinh but have a disjoint set of explicit scripts as their script extension. By looking only at the script extension, we treat many of them as being part of an explicit script rather than as a common or inherited glyph. * Preserve the type of the kerning pair, so class-to-class kerning stays that way, even when there's only one glyph on each side. * Reconstruct kerning group names for the newly split classes. This is done for debuggability; it makes no difference for the final font binary. * This first looks at the common lookups and then all others, assigning new group names are it goes. A class like `@kern1.A = [A A-cy increment]` may be split up into `@kern1.Latn.A = [A]`, `@kern1.Cyrl.A = [A-cy]` and `@kern1.Default.A = [increment]`. Note: If there is no dedicated Default lookup, common glyph classes like `[period]` might carry the name `@kern1.Grek.foo` if the class was first encountered while going over the Grek lookup. * Discard pairs that mix RTL and LTR BiDi types, because they won't show up in applications due to how Unicode text is split into runs. * Discard empty lookups, if they were created but all their pairs were discarded. * Make a `kern` (and potentially `dist`) feature block and register the lookups for each script. Some scripts need to be registered in the `dist` feature for some shapers to discover them, e.g. Yezi. * Write the new glyph class definitions and then the lookups and feature blocks to the feature file. """ tableTag = "GPOS" features = frozenset(["kern", "dist"]) options = dict(ignoreMarks=True, quantization=1) def setContext(self, font, feaFile, compiler=None): ctx = super().setContext(font, feaFile, compiler=compiler) ctx.gdefClasses = self.getGDEFGlyphClasses() ctx.glyphSet = self.getOrderedGlyphSet() # Unless we use the legacy append mode (which ignores insertion # 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 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(default_source), ) # Remember which languages are defined for which OT tag, as all # generated kerning needs to be registered for the script's `dflt` # language, but also all those the designer defined manually. Otherwise, # setting any language for a script would deactivate kerning. feaLanguagesByScript = ast.getScriptLanguageSystems(feaFile, excludeDflt=False) ctx.feaLanguagesByScript = { otTag: languages for _, languageSystems in feaLanguagesByScript.items() for otTag, languages in languageSystems } # TODO: Also include substitution information from Designspace rules to # correctly set the scripts of variable substitution glyphs, maybe add # `glyphUnicodeMapping: dict[str, int] | None` to `BaseFeatureCompiler`? cmap = self.makeUnicodeToGlyphNameMapping() gsub = self.compileGSUB() extras = self.extraSubstitutions() ctx.knownScripts = self.guessFontScripts() scriptGlyphs = classifyGlyphs(self.knownScriptsPerCodepoint, cmap, gsub, extras) bidiGlyphs = classifyGlyphs(unicodeBidiType, cmap, gsub, extras) ctx.bidiGlyphs = bidiGlyphs glyphScripts = {} for script, glyphs in scriptGlyphs.items(): for g in glyphs: glyphScripts.setdefault(g, set()).add(script) ctx.glyphScripts = glyphScripts ctx.kerning = self.getKerningData() return ctx def shouldContinue(self): if not self.context.kerning.pairs: self.log.debug("No kerning data; skipped") return False return super().shouldContinue() def _write(self): lookups = self._makeKerningLookups() if not lookups: self.log.debug("kerning lookups empty; skipped") return False features = self._makeFeatureBlocks(lookups) if not features: self.log.debug("kerning features empty; skipped") return False # extend feature file with the new generated statements feaFile = self.context.feaFile # first add the glyph class definitions classDefs = self.context.kerning.classDefs newClassDefs = [c for _, c in sorted(classDefs.items())] lookupGroups = [] for _, lookupGroup in sorted(lookups.items()): lookupGroups.extend( lkp for lkp in lookupGroup.values() if lkp not in lookupGroups ) # NOTE: We don't write classDefs because we literalise all classes. self._insert( feaFile=feaFile, classDefs=newClassDefs, lookups=lookupGroups, features=[features[tag] for tag in ["kern", "dist"] if tag in features], ) return True def getKerningData(self): side1Groups, side2Groups = self.getKerningGroups() pairs = self.getKerningPairs(side1Groups, side2Groups) # side(1|2)Classes and classDefs will hold the feaLib AST to write out. return SimpleNamespace( side1Classes={}, side2Classes={}, classDefs={}, pairs=pairs ) def getKerningGroups( self, ) -> tuple[Mapping[str, tuple[str, ...]], Mapping[str, tuple[str, ...]]]: allGlyphs = self.context.glyphSet 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 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: Mapping[str, tuple[str, ...]], side2Classes: Mapping[str, tuple[str, ...]], ) -> list[KerningPair]: if self.context.isVariable: return self.getVariableKerningPairs( self.context.font, side1Classes, side2Classes, self.context.glyphSet, self.options, ) glyphSet = self.context.glyphSet font = self.context.font kerning = font.kerning quantization = self.options.quantization kerning: Mapping[tuple[str, str], float] = font.kerning result = [] for (side1, side2), value in kerning.items(): firstIsClass, secondIsClass = (side1 in side1Classes, side2 in side2Classes) # Filter out pairs that reference missing groups or glyphs. if not firstIsClass and side1 not in glyphSet: continue if not secondIsClass and side2 not in glyphSet: continue # Ignore zero-valued class kern pairs. They are the most general # kerns, so they don't override anything else like glyph kerns would # and zero is the default. if firstIsClass and secondIsClass and value == 0: continue if firstIsClass: side1 = side1Classes[side1] if secondIsClass: side2 = side2Classes[side2] value = quantize(value, quantization) result.append(KerningPair(side1, side2, value)) return result @staticmethod def getVariableKerningPairs( designspace: DesignSpaceDocument, side1Classes: Mapping[str, tuple[str, ...]], side2Classes: Mapping[str, tuple[str, ...]], glyphSet: Mapping[str, str], options: SimpleNamespace, ) -> list[KerningPair]: quantization = 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) pair = KerningPair(side1, side2, value) # Ignore zero-valued class kern pairs. They are the most general # kerns, so they don't override anything else like glyph kerns would # and zero is the default. if pair.firstIsClass and pair.secondIsClass and pair.value == 0: continue result.append(pair) return result def _makePairPosRule(self, pair, side1Classes, side2Classes, rtl=False): enumerated = pair.firstIsClass ^ pair.secondIsClass valuerecord = ast.ValueRecord( xPlacement=pair.value if rtl else None, yPlacement=0 if rtl else None, xAdvance=pair.value, yAdvance=0 if rtl else None, ) if pair.firstIsClass: glyphs1 = ast.GlyphClassName(side1Classes[pair.side1]) else: glyphs1 = ast.GlyphName(pair.side1) if pair.secondIsClass: glyphs2 = ast.GlyphClassName(side2Classes[pair.side2]) else: glyphs2 = ast.GlyphName(pair.side2) return ast.PairPosStatement( glyphs1=glyphs1, valuerecord1=valuerecord, glyphs2=glyphs2, valuerecord2=None, 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 if mark in source.font ): 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: # We only want to filter the spacing marks marks = set(self.context.gdefClasses.mark or []) & set( self.context.glyphSet.keys() ) spacing = [] if marks: spacing = self._filterSpacingMarks(marks) if not spacing: # Simple case, there are no spacing ("Spacing Combining") marks, # do what we've always done. lookup.statements.append(ast.makeLookupFlag("IgnoreMarks")) else: # We want spacing marks to block kerns. className = "MFS_%s" % name filteringClass = ast.makeGlyphClassDefinitions( {className: spacing}, feaFile=self.context.feaFile )[className] lookup.statements.append(filteringClass) lookup.statements.append( ast.makeLookupFlag(markFilteringSet=filteringClass) ) return lookup def knownScriptsPerCodepoint(self, uv: int) -> set[str]: if not self.context.knownScripts: # If there are no languagesystems and nothing to derive from Unicode # codepoints, consider everything common; it'll all end in DFLT/dflt # anyway. return {COMMON_SCRIPT} else: script_extension = unicodeScriptExtensions(uv) return script_extension & (self.context.knownScripts | DFLT_SCRIPTS) def _makeKerningLookups(self): marks = self.context.gdefClasses.mark lookups = {} pairs = self.context.kerning.pairs if self.options.ignoreMarks: basePairs, markPairs = self._splitBaseAndMarkPairs( self.context.kerning.pairs, marks ) if basePairs: self._makeSplitScriptKernLookups(lookups, basePairs) if markPairs: self._makeSplitScriptKernLookups( lookups, markPairs, ignoreMarks=False, suffix="_marks" ) else: self._makeSplitScriptKernLookups(lookups, pairs) return lookups def _splitBaseAndMarkPairs( self, pairs: list[KerningPair], marks: set[str] ) -> tuple[list[KerningPair], list[KerningPair]]: if not marks: return list(pairs), [] basePairs: list[KerningPair] = [] markPairs: list[KerningPair] = [] for pair in pairs: # Disentangle kerning between bases and marks by splitting a pair # into a list of base-to-base pairs (basePairs) and a list of # base-to-mark, mark-to-base and mark-to-mark pairs (markPairs). # This ensures that "kerning exceptions" (a kerning pair modifying # the effect of another) work as intended because these related # pairs end up in the same list together. side1Bases: tuple[str, ...] | str | None = None side1Marks: tuple[str, ...] | str | None = None if pair.firstIsClass: side1Bases = tuple(glyph for glyph in pair.side1 if glyph not in marks) side1Marks = tuple(glyph for glyph in pair.side1 if glyph in marks) else: if pair.side1 in marks: side1Marks = pair.side1 else: side1Bases = pair.side1 side2Bases: tuple[str, ...] | str | None = None side2Marks: tuple[str, ...] | str | None = None if pair.secondIsClass: side2Bases = tuple(glyph for glyph in pair.side2 if glyph not in marks) side2Marks = tuple(glyph for glyph in pair.side2 if glyph in marks) else: if pair.side2 in marks: side2Marks = pair.side2 else: side2Bases = pair.side2 if side1Bases and side2Bases: # base-to-base basePairs.append(KerningPair(side1Bases, side2Bases, value=pair.value)) if side1Bases and side2Marks: # base-to-mark markPairs.append(KerningPair(side1Bases, side2Marks, value=pair.value)) if side1Marks and side2Bases: # mark-to-base markPairs.append(KerningPair(side1Marks, side2Bases, value=pair.value)) if side1Marks and side2Marks: # mark-to-mark markPairs.append(KerningPair(side1Marks, side2Marks, value=pair.value)) return basePairs, markPairs def _makeSplitScriptKernLookups(self, lookups, pairs, ignoreMarks=True, suffix=""): bidiGlyphs = self.context.bidiGlyphs glyphScripts = self.context.glyphScripts kerningPerScript = splitKerning(pairs, glyphScripts) side1Classes = self.context.kerning.side1Classes side2Classes = self.context.kerning.side2Classes newClassDefs, newSide1Classes, newSide2Classes = makeAllGlyphClassDefinitions( kerningPerScript, self.context, self.context.feaFile ) # NOTE: Consider duplicate names a bug, even if the classes would carry # the same glyphs. assert not self.context.kerning.classDefs.keys() & newClassDefs.keys() self.context.kerning.classDefs.update(newClassDefs) assert not side1Classes.keys() & newSide1Classes.keys() side1Classes.update(newSide1Classes) assert not side2Classes.keys() & newSide2Classes.keys() side2Classes.update(newSide2Classes) for scripts, pairs in kerningPerScript.items(): lookupName = f"kern_{'_'.join(scripts)}{suffix}".replace( COMMON_SCRIPT, COMMON_CLASS_NAME ) lookup = self._makeKerningLookup(lookupName, ignoreMarks=ignoreMarks) for pair in pairs: bidiTypes = { direction for direction, glyphs in bidiGlyphs.items() if not set(pair.glyphs).isdisjoint(glyphs) } if bidiTypes.issuperset(AMBIGUOUS_BIDIS): LOGGER.info( "Skipping kerning pair <%s %s %s> with ambiguous direction", pair.side1, pair.side2, pair.value, ) continue directions = {script_direction(script) for script in scripts} assert len(directions) == 1 scriptIsRtl = directions == {"RTL"} # Numbers are always shaped LTR even in RTL scripts: pairIsRtl = scriptIsRtl and "L" not in bidiTypes rule = self._makePairPosRule( pair, side1Classes, side2Classes, pairIsRtl ) lookup.statements.append(rule) for script in scripts: lookups.setdefault(script, {})[lookupName] = lookup # Clean out empty lookups. for script, scriptLookups in list(lookups.items()): for lookup_name, lookup in list(scriptLookups.items()): if not any( stmt for stmt in lookup.statements if not isinstance(stmt, ast.LookupFlagStatement) ): del scriptLookups[lookup_name] if not scriptLookups: del lookups[script] def _makeFeatureBlocks(self, lookups): features = {} feaLanguagesByScript = self.context.feaLanguagesByScript if "kern" in self.context.todo: kern = ast.FeatureBlock("kern") self._registerLookups(kern, lookups, feaLanguagesByScript) if kern.statements: features["kern"] = kern if "dist" in self.context.todo: dist = ast.FeatureBlock("dist") self._registerLookups(dist, lookups, feaLanguagesByScript) if dist.statements: features["dist"] = dist return features @staticmethod def _registerLookups( feature: ast.FeatureBlock, lookups: dict[str, dict[str, ast.LookupBlock]], feaLanguagesByScript: Mapping[str, list[str]], ) -> None: # Ensure we have kerning for pure common script runs (e.g. ">1") isKernBlock = feature.name == "kern" dfltLookups: list[ast.LookupBlock] = [] if isKernBlock and COMMON_SCRIPT in lookups: dfltLookups.extend( lkp for lkp in lookups[COMMON_SCRIPT].values() if lkp not in dfltLookups ) # InDesign bugfix: register kerning lookups for all LTR scripts under DFLT # so that the basic composer, without a language selected, will still kern. # Register LTR lookups if any, otherwise RTL lookups. if isKernBlock: lookupsLTR: list[ast.LookupBlock] = [] lookupsRTL: list[ast.LookupBlock] = [] for script, scriptLookups in sorted(lookups.items()): if script not in DIST_ENABLED_SCRIPTS: if script_direction(script) == "LTR": lookupsLTR.extend(scriptLookups.values()) elif script_direction(script) == "RTL": lookupsRTL.extend(scriptLookups.values()) dfltLookups.extend( lkp for lkp in (lookupsLTR or lookupsRTL) if lkp not in dfltLookups ) if dfltLookups: languages = feaLanguagesByScript.get("DFLT", ["dflt"]) ast.addLookupReferences(feature, dfltLookups, "DFLT", languages) # Feature blocks use script tags to distinguish what to run for a # Unicode script. # # "Script tags generally correspond to a Unicode script. However, the # associations between them may not always be one-to-one, and the # OpenType script tags are not guaranteed to be the same as Unicode # Script property-value aliases or ISO 15924 script IDs." # # E.g. {"latn": "Latn", "telu": "Telu", "tel2": "Telu"} # # Skip DFLT script because we always take care of it above for `kern`. # It never occurs in `dist`. if isKernBlock: scriptsToReference = lookups.keys() - DIST_ENABLED_SCRIPTS else: scriptsToReference = DIST_ENABLED_SCRIPTS.intersection(lookups.keys()) for script in sorted(scriptsToReference - DFLT_SCRIPTS): for tag in unicodedata.ot_tags_from_script(script): # Insert line breaks between statements for niceness :). if feature.statements: feature.statements.append(ast.Comment("")) # We have something for this script. First add the default # lookups, then the script-specific ones lookupsForThisScript = {} for dfltScript in DFLT_SCRIPTS: if dfltScript in lookups: lookupsForThisScript.update(lookups[dfltScript]) lookupsForThisScript.update(lookups[script]) # Register the lookups for all languages defined in the feature # file for the script, otherwise kerning is not applied if any # language is set at all. languages = feaLanguagesByScript.get(tag, ["dflt"]) ast.addLookupReferences( feature, lookupsForThisScript.values(), tag, languages ) def splitKerning(pairs, glyphScripts): # Split kerning into per-script buckets, so we can post-process them before # continuing. Scripts that have cross-script kerning pairs will be put in # the same bucket. kerningPerScript = {} for pair in pairs: for scripts, splitPair in partitionByScript(pair, glyphScripts): scripts = tuple(sorted(scripts)) kerningPerScript.setdefault(scripts, []).append(splitPair) kerningPerScript = mergeScripts(kerningPerScript) for scripts, pairs in kerningPerScript.items(): if len(scripts) > 1: LOGGER.info( "Merging kerning lookups from the following scripts: %s", ", ".join(scripts), ) pairs.sort() return kerningPerScript def partitionByScript( pair: KerningPair, glyphScripts: Mapping[str, set[str]], ) -> Iterator[tuple[str, KerningPair]]: """Split a potentially mixed-script pair into pairs that make sense based on the dominant script, and yield each combination with its dominant script.""" side1Directions: dict[str, set[str]] = {} side2Directions: dict[str, set[str]] = {} resolvedScripts: dict[str, set[str]] = {} for glyph in pair.firstGlyphs: scripts = glyphScripts.get(glyph, DFLT_SCRIPTS) # If a glyph is both common or inherited *and* another script, treat it # as just common (throwing Zyyy and Zinh into the same bucket for # simplicity). This ensures that a pair appears to the shaper exactly # once, as long as every script sees at most 2 lookups (or 3 with mark # lookups, but they contain distinct pairs), the common one and the # script-specific one. if scripts & DFLT_SCRIPTS: scripts = COMMON_SCRIPTS_SET resolvedScripts[glyph] = scripts for direction in (script_direction(script) for script in sorted(scripts)): side1Directions.setdefault(direction, set()).add(glyph) for glyph in pair.secondGlyphs: scripts = glyphScripts.get(glyph, DFLT_SCRIPTS) if scripts & DFLT_SCRIPTS: scripts = COMMON_SCRIPTS_SET resolvedScripts[glyph] = scripts for direction in (script_direction(script) for script in sorted(scripts)): side2Directions.setdefault(direction, set()).add(glyph) for side1Direction, side2Direction in itertools.product( side1Directions, side2Directions ): localSide1: str | tuple[str, ...] localSide2: str | tuple[str, ...] side1Scripts: set[str] = set() side2Scripts: set[str] = set() if pair.firstIsClass: localSide1 = tuple(sorted(side1Directions[side1Direction])) for glyph in localSide1: side1Scripts |= resolvedScripts[glyph] else: assert len(side1Directions[side1Direction]) == 1 (localSide1,) = side1Directions[side1Direction] side1Scripts |= resolvedScripts[localSide1] if pair.secondIsClass: localSide2 = tuple(sorted(side2Directions[side2Direction])) for glyph in localSide2: side2Scripts |= resolvedScripts[glyph] else: assert len(side2Directions[side2Direction]) == 1 (localSide2,) = side2Directions[side2Direction] side2Scripts |= resolvedScripts[localSide2] # Skip pairs with mixed direction. if side1Direction != side2Direction and not any( side == "Auto" for side in (side1Direction, side2Direction) ): LOGGER.info( "Skipping kerning pair <%s %s %s> with mixed direction (%s, %s)", pair.side1, pair.side2, pair.value, side1Direction, side2Direction, ) continue scripts = side1Scripts | side2Scripts # If only one side has Common, drop it if not all(side & COMMON_SCRIPTS_SET for side in (side1Scripts, side2Scripts)): scripts -= COMMON_SCRIPTS_SET yield scripts, KerningPair( localSide1, localSide2, pair.value, ) def mergeScripts(kerningPerScript): """Merge buckets that have common scripts. If we have [A, B], [B, C], and [D] buckets, we want to merge the first two into [A, B, C] and leave [D] so that all kerning pairs of the three scripts are in the same lookup.""" sets = [set(scripts) for scripts in kerningPerScript if scripts] merged = True while merged: merged = False result = [] while sets: common, rest = sets[0], sets[1:] sets = [] for scripts in rest: if scripts.isdisjoint(common): sets.append(scripts) else: merged = True common |= scripts result.append(common) sets = result # Now that we have merged all common-script buckets, we need to re-assign # the kerning pairs to the new buckets. result = {tuple(sorted(scripts)): [] for scripts in sets} for scripts, pairs in kerningPerScript.items(): for scripts2 in sets: if scripts2 & set(scripts): result[tuple(sorted(scripts2))].extend(pairs) break else: # Shouldn't happen, but just in case. raise AssertionError return result def makeAllGlyphClassDefinitions(kerningPerScript, context, feaFile=None): # Note: Refer to the context for existing classDefs and mappings of glyph # class tuples to feaLib AST to avoid overwriting existing class names, # because base and mark kerning pairs might be separate passes. newClassDefs = {} existingSide1Classes = context.kerning.side1Classes existingSide2Classes = context.kerning.side2Classes newSide1Classes = {} newSide2Classes = {} side1Membership = context.side1Membership side2Membership = context.side2Membership if feaFile is not None: classNames = {cdef.name for cdef in ast.iterClassDefinitions(feaFile)} else: classNames = set() classNames.update(context.kerning.classDefs.keys()) # Generate common class names first so that common classes are correctly # named in other lookups. for scripts, pairs in kerningPerScript.items(): if set(scripts) != COMMON_SCRIPTS_SET: continue for pair in pairs: if ( pair.firstIsClass and pair.side1 not in existingSide1Classes and pair.side1 not in newSide1Classes ): addClassDefinition( "kern1", pair.side1, newSide1Classes, side1Membership, newClassDefs, classNames, COMMON_CLASS_NAME, ) if ( pair.secondIsClass and pair.side2 not in existingSide2Classes and pair.side2 not in newSide2Classes ): addClassDefinition( "kern2", pair.side2, newSide2Classes, side2Membership, newClassDefs, classNames, COMMON_CLASS_NAME, ) sortedKerningPerScript = sorted(kerningPerScript.items()) for scripts, pairs in sortedKerningPerScript: if set(scripts) == COMMON_SCRIPTS_SET: continue script = "_".join(scripts).replace(COMMON_SCRIPT, COMMON_CLASS_NAME) for pair in pairs: if ( pair.firstIsClass and pair.side1 not in existingSide1Classes and pair.side1 not in newSide1Classes ): addClassDefinition( "kern1", pair.side1, newSide1Classes, side1Membership, newClassDefs, classNames, script, ) if ( pair.secondIsClass and pair.side2 not in existingSide2Classes and pair.side2 not in newSide2Classes ): addClassDefinition( "kern2", pair.side2, newSide2Classes, side2Membership, newClassDefs, classNames, script, ) return newClassDefs, newSide1Classes, newSide2Classes def addClassDefinition( prefix, group, classes, originalMembership, classDefs, classNames, script ): firstGlyph = next(iter(group)) originalGroupName = originalMembership[firstGlyph] groupName = f"{prefix}.{script}.{originalGroupName}" className = ast.makeFeaClassName(groupName, classNames) 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, ) ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/kernFeatureWriter2.py000066400000000000000000001206561470175262700240020ustar00rootroot00000000000000"""Alternative implementation of KernFeatureWriter. This behaves like the primary kern feature writer, with the important difference of grouping kerning data into lookups by kerning direction, not script, like the feature writer in ufo2ft v2.30 and older did. The original idea for the primary splitter was to generate smaller, easier to pack lookups for each script exclusively, as cross-script kerning dos not work in browsers. However, other applications may allow it, e.g. Adobe's InDesign. Subsequently, it was modified to clump together lookups that cross-reference each other's scripts, negating the size advantages if you design fonts with cross-script kerning for designer ease. As a special edge case, InDesign's default text shaper does not properly itemize text runs, meaning it may group different scripts into the same run unless the user specifically marks some text as being a specific script or language. To make all kerning reachable in that case, it must be put into a single broad LTR, RTL or neutral direction lookup instead of finer script clusters. That will make it work in all cases, including when there is no cross-script kerning to fuse different lookups together. Testing showed that size benefits are clawed back with the use of the HarfBuzz repacker (during compilation) and GPOS compression (after compilation) at acceptable speed. """ from __future__ import annotations import enum import itertools import logging import sys from collections import OrderedDict from types import SimpleNamespace from typing import Any, Iterator, Mapping, cast import fontTools.feaLib.ast as fea_ast 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.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import ( DFLT_SCRIPTS, classifyGlyphs, collapse_varscalar, describe_ufo, get_userspace_location, quantize, ) from .kernFeatureWriter import ( AMBIGUOUS_BIDIS, DIST_ENABLED_SCRIPTS, LTR_BIDI_TYPES, RTL_BIDI_TYPES, SIDE1_PREFIX, SIDE2_PREFIX, KerningPair, addClassDefinition, log_redefined_group, log_regrouped_glyph, ) if sys.version_info < (3, 10): from typing_extensions import TypeAlias else: from typing import TypeAlias LOGGER = logging.getLogger(__name__) KerningGroup: TypeAlias = "Mapping[str, tuple[str, ...]]" class Direction(enum.Enum): Neutral = "dflt" LeftToRight = "ltr" RightToLeft = "rtl" def __lt__(self, other: Direction) -> bool: if not isinstance(other, Direction): return NotImplemented return self.name < other.name class KernContext(SimpleNamespace): bidiGlyphs: dict[Direction, set[str]] compiler: Any default_source: Any existingFeatures: Any feaFile: Any feaLanguagesByTag: dict[str, list[str]] font: Any gdefClasses: Any glyphBidi: dict[str, set[Direction]] glyphDirection: dict[str, set[Direction]] glyphSet: OrderedDict[str, Any] insertComments: Any isVariable: bool kerning: Any knownScripts: set[str] side1Membership: dict[str, str] side2Membership: dict[str, str] todo: Any class KernFeatureWriter(BaseFeatureWriter): """Generates a kerning feature based on groups and rules contained in an UFO's kerning data. If the `quantization` argument is given in the filter options, the resulting anchors are rounded to the nearest multiple of the quantization value. ## Implementation Notes The algorithm works like this: * Parse GDEF GlyphClassDefinition from UFO features.fea to get the set of "Mark" glyphs (this will be used later to decide whether to add ignoreMarks flag to kern lookups containing pairs between base and mark glyphs). * Get the ordered glyphset for the font, for filtering kerning groups and kernings that reference unknown glyphs. * Determine which scripts the kerning affects (read: "the font most probably supports"), to know which lookups to generate later: * First, determine the unambiguous script associations for each (Unicoded) glyph in the glyphset, as in, glyphs that have a single entry for their Unicode script extensions property; * then, parse the `languagesystem` statements in the provided feature file to add on top. * Compile a Unicode cmap from the UFO and a GSUB table from the features so far, so we can determine the bidirectionality class, so we can later filter out kerning pairs that would mix RTL and LTR glyphs, which will not occur in applications, and put the pairs into their correct lookup. Unicode BiDi classes R and AL are considered R. Common characters and numbers are considered neutral even when their BiDi class says otherwise, so they'll end up in the common lookup available to all scripts. * Get the kerning groups from the UFO and filter out glyphs not in the glyphset and empty groups. Remember which group a glyph is a member of, for kern1 and kern2, so we can later reconstruct per-direction groups. * Get the bare kerning pairs from the UFO, filtering out pairs with unknown groups or glyphs not in the glyphset and (redundant) zero class-to-class kernings and optionally quantizing kerning values. * Optionally, split kerning pairs into base (only base against base) and mark (mixed mark and base) pairs, according to the glyphs' GDEF category, so that kerning against marks can be accounted for correctly later. * Go through all kerning pairs and split them up by direction, to put them in different lookups. In pairs with common glyphs, assume the direction of the dominant script, in pairs of common glyphs, assume no direction. Pairs with clashing script directions are dropped. * Partition the first and second side of a pair by BiDi direction (as above) and emit only those with the same direction or a strong direction and neutral one. * Discard pairs that mix RTL and LTR BiDi types, because they won't show up in applications due to how Unicode text is split into runs. * Glyphs will have only one direction assigned to them. * Preserve the type of the kerning pair, so class-to-class kerning stays that way, even when there's only one glyph on each side. * Reconstruct kerning group names for the newly split classes. This is done for debuggability; it makes no difference for the final font binary. * This first looks at the neutral lookups and then all others, assigning new group names are it goes. A class like `@kern1.something = [foo bar baz]` may be split up into `@kern1.dflt.something = [foo]` and `@kern1.ltr.something = [bar baz]`. Note: If there is no dedicated dflt lookup, common glyph classes like `[foo]` might carry the name `@kern1.ltr.foo` if the class was first encountered while going over the ltr lookup. * Make a `kern` (and potentially `dist`) feature block and register the lookups for each script. Some scripts need to be registered in the `dist` feature for some shapers to discover them, e.g. Yezi. * Write the new glyph class definitions and then the lookups and feature blocks to the feature file. """ tableTag = "GPOS" features = frozenset(["kern", "dist"]) options = dict(ignoreMarks=True, quantization=1) def setContext(self, font, feaFile, compiler=None): ctx: KernContext = cast( KernContext, super().setContext(font, feaFile, compiler=compiler) ) if hasattr(font, "findDefault"): ctx.default_source = font.findDefault().font else: ctx.default_source = font # Unless we use the legacy append mode (which ignores insertion # 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 ( self.mode == "skip" and ctx.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(ctx.default_source), ) # Remember which languages are defined for which OT tag, as all # generated kerning needs to be registered for the script's `dflt` # language, but also all those the designer defined manually. Otherwise, # setting any language for a script would deactivate kerning. feaLanguagesByScript = ast.getScriptLanguageSystems(feaFile, excludeDflt=False) ctx.feaLanguagesByTag = { otTag: languages for _, languageSystems in feaLanguagesByScript.items() for otTag, languages in languageSystems } ctx.glyphSet = self.getOrderedGlyphSet() ctx.gdefClasses = self.getGDEFGlyphClasses() ctx.knownScripts = self.guessFontScripts() # We need the direction of a glyph (with common characters considered # neutral or "dflt") to know in which of the three lookups to put the # pair. cmap = self.makeUnicodeToGlyphNameMapping() gsub = self.compileGSUB() extras = self.extraSubstitutions() dirGlyphs = classifyGlyphs(unicodeScriptDirection, cmap, gsub, extras) neutral_glyphs = ( ctx.glyphSet.keys() - dirGlyphs.get(Direction.LeftToRight, set()) - dirGlyphs.get(Direction.RightToLeft, set()) ) dirGlyphs[Direction.Neutral] = neutral_glyphs glyphDirection = {} for direction, glyphs in dirGlyphs.items(): for name in glyphs: glyphDirection.setdefault(name, set()).add(direction) ctx.glyphDirection = glyphDirection # We need the BiDi class of a glyph to reject kerning of RTL glyphs # against LTR glyphs. ctx.bidiGlyphs = classifyGlyphs(unicodeBidiType, cmap, gsub, extras) neutral_glyphs = ( ctx.glyphSet.keys() - ctx.bidiGlyphs.get(Direction.LeftToRight, set()) - ctx.bidiGlyphs.get(Direction.RightToLeft, set()) ) ctx.bidiGlyphs[Direction.Neutral] = neutral_glyphs glyphBidi = {} for direction, glyphs in ctx.bidiGlyphs.items(): for name in glyphs: glyphBidi.setdefault(name, set()).add(direction) ctx.glyphBidi = glyphBidi ctx.kerning = extract_kerning_data(ctx, cast(SimpleNamespace, self.options)) return ctx def shouldContinue(self): if ( not self.context.kerning.base_pairs_by_direction and not self.context.kerning.mark_pairs_by_direction ): self.log.debug("No kerning data; skipped") return False return super().shouldContinue() def _write(self): self.context: KernContext self.options: SimpleNamespace lookups = make_kerning_lookups(self.context, self.options) if not lookups: self.log.debug("kerning lookups empty; skipped") return False features = make_feature_blocks(self.context, lookups) if not features: self.log.debug("kerning features empty; skipped") return False # extend feature file with the new generated statements feaFile = self.context.feaFile # first add the glyph class definitions classDefs = self.context.kerning.classDefs newClassDefs = [c for _, c in sorted(classDefs.items())] lookupGroups = [] for _, lookupGroup in sorted(lookups.items(), key=lambda x: x[0].value): lookupGroups.extend( lkp for lkp in lookupGroup.values() if lkp not in lookupGroups ) self._insert( feaFile=feaFile, classDefs=newClassDefs, lookups=lookupGroups, features=[features[tag] for tag in ["kern", "dist"] if tag in features], ) return True def unicodeBidiType(uv: int) -> Direction | None: """Return Direction.RightToLeft for characters with strong RTL direction, or Direction.LeftToRight for strong LTR and European and Arabic numbers, or None for neutral direction. """ bidiType = unicodedata.bidirectional(chr(uv)) if bidiType in RTL_BIDI_TYPES: return Direction.RightToLeft elif bidiType in LTR_BIDI_TYPES: return Direction.LeftToRight return None def unicodeScriptDirection(uv: int) -> Direction | None: script = unicodedata.script(chr(uv)) if script in DFLT_SCRIPTS: return None direction = unicodedata.script_horizontal_direction(script, "LTR") if direction == "LTR": return Direction.LeftToRight elif direction == "RTL": return Direction.RightToLeft raise ValueError(f"Unknown direction {direction}") def extract_kerning_data(context: KernContext, options: SimpleNamespace) -> Any: side1Groups, side2Groups = get_kerning_groups(context) if context.isVariable: pairs = get_variable_kerning_pairs(context, options, side1Groups, side2Groups) else: pairs = get_kerning_pairs(context, options, side1Groups, side2Groups) if options.ignoreMarks: marks = context.gdefClasses.mark base_pairs, mark_pairs = split_base_and_mark_pairs(pairs, marks) else: base_pairs = pairs mark_pairs = [] base_pairs_by_direction = split_kerning(context, base_pairs) mark_pairs_by_direction = split_kerning(context, mark_pairs) return SimpleNamespace( base_pairs_by_direction=base_pairs_by_direction, mark_pairs_by_direction=mark_pairs_by_direction, side1Classes={}, side2Classes={}, classDefs={}, ) def get_kerning_groups(context: KernContext) -> tuple[KerningGroup, KerningGroup]: allGlyphs = context.glyphSet side1Groups: dict[str, tuple[str, ...]] = {} side1Membership: dict[str, str] = {} side2Groups: dict[str, tuple[str, ...]] = {} side2Membership: dict[str, str] = {} if isinstance(context.font, DesignSpaceDocument): fonts = [source.font for source in context.font.sources] else: fonts = [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 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) context.side1Membership = side1Membership context.side2Membership = side2Membership return side1Groups, side2Groups def get_kerning_pairs( context: KernContext, options: SimpleNamespace, side1Classes: KerningGroup, side2Classes: KerningGroup, ) -> list[KerningPair]: glyphSet = context.glyphSet font = context.font kerning: Mapping[tuple[str, str], float] = font.kerning quantization = options.quantization result = [] for (side1, side2), value in kerning.items(): firstIsClass, secondIsClass = (side1 in side1Classes, side2 in side2Classes) # Filter out pairs that reference missing groups or glyphs. if not firstIsClass and side1 not in glyphSet: continue if not secondIsClass and side2 not in glyphSet: continue # Ignore zero-valued class kern pairs. They are the most general # kerns, so they don't override anything else like glyph kerns would # and zero is the default. if firstIsClass and secondIsClass and value == 0: continue if firstIsClass: side1 = side1Classes[side1] if secondIsClass: side2 = side2Classes[side2] value = quantize(value, quantization) result.append(KerningPair(side1, side2, value)) return result def get_variable_kerning_pairs( context: KernContext, options: SimpleNamespace, side1Classes: KerningGroup, side2Classes: KerningGroup, ) -> list[KerningPair]: designspace: DesignSpaceDocument = context.font glyphSet = context.glyphSet quantization = 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 = context.font.findDefault() 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) pair = KerningPair(side1, side2, value) # Ignore zero-valued class kern pairs. They are the most general # kerns, so they don't override anything else like glyph kerns would # and zero is the default. if pair.firstIsClass and pair.secondIsClass and pair.value == 0: continue result.append(pair) return result def split_base_and_mark_pairs( pairs: list[KerningPair], marks: set[str] ) -> tuple[list[KerningPair], list[KerningPair]]: if not marks: return list(pairs), [] basePairs: list[KerningPair] = [] markPairs: list[KerningPair] = [] for pair in pairs: # Disentangle kerning between bases and marks by splitting a pair # into a list of base-to-base pairs (basePairs) and a list of # base-to-mark, mark-to-base and mark-to-mark pairs (markPairs). # This ensures that "kerning exceptions" (a kerning pair modifying # the effect of another) work as intended because these related # pairs end up in the same list together. side1Bases: tuple[str, ...] | str | None = None side1Marks: tuple[str, ...] | str | None = None if pair.firstIsClass: side1Bases = tuple(glyph for glyph in pair.side1 if glyph not in marks) side1Marks = tuple(glyph for glyph in pair.side1 if glyph in marks) elif pair.side1 in marks: side1Marks = pair.side1 else: side1Bases = pair.side1 side2Bases: tuple[str, ...] | str | None = None side2Marks: tuple[str, ...] | str | None = None if pair.secondIsClass: side2Bases = tuple(glyph for glyph in pair.side2 if glyph not in marks) side2Marks = tuple(glyph for glyph in pair.side2 if glyph in marks) elif pair.side2 in marks: side2Marks = pair.side2 else: side2Bases = pair.side2 if side1Bases and side2Bases: # base-to-base basePairs.append(KerningPair(side1Bases, side2Bases, value=pair.value)) if side1Bases and side2Marks: # base-to-mark markPairs.append(KerningPair(side1Bases, side2Marks, value=pair.value)) if side1Marks and side2Bases: # mark-to-base markPairs.append(KerningPair(side1Marks, side2Bases, value=pair.value)) if side1Marks and side2Marks: # mark-to-mark markPairs.append(KerningPair(side1Marks, side2Marks, value=pair.value)) return basePairs, markPairs def split_kerning( context: KernContext, pairs: list[KerningPair], ) -> dict[Direction, list[KerningPair]]: # Split kerning into per-direction buckets, so we can drop them into their # own lookups. glyph_bidi = context.glyphBidi glyph_direction = context.glyphDirection kerning_per_direction: dict[Direction, list[KerningPair]] = {} for pair in pairs: for direction, split_pair in partition_by_direction( pair, glyph_bidi, glyph_direction ): kerning_per_direction.setdefault(direction, []).append(split_pair) for pairs in kerning_per_direction.values(): pairs.sort() return kerning_per_direction def partition_by_direction( pair: KerningPair, glyph_bidi: Mapping[str, set[Direction]], glyph_direction: Mapping[str, set[Direction]], ) -> Iterator[tuple[Direction, KerningPair]]: """Split a potentially mixed-direction pair into pairs of the same or compatible direction.""" side1Bidis: dict[Direction, set[str]] = {} side2Bidis: dict[Direction, set[str]] = {} side1Directions: dict[Direction, set[str]] = {} side2Directions: dict[Direction, set[str]] = {} for glyph in pair.firstGlyphs: bidis = glyph_bidi[glyph] directions = glyph_direction[glyph] for bidi in bidis: side1Bidis.setdefault(bidi, set()).add(glyph) for direction in directions: side1Directions.setdefault(direction, set()).add(glyph) for glyph in pair.secondGlyphs: bidis = glyph_bidi[glyph] directions = glyph_direction[glyph] for bidi in bidis: side2Bidis.setdefault(bidi, set()).add(glyph) for direction in directions: side2Directions.setdefault(direction, set()).add(glyph) for side1Direction, side2Direction in itertools.product( sorted(side1Directions), sorted(side2Directions) ): localSide1: str | tuple[str, ...] localSide2: str | tuple[str, ...] if pair.firstIsClass: localSide1 = tuple(sorted(side1Directions[side1Direction])) else: assert len(side1Directions[side1Direction]) == 1 (localSide1,) = side1Directions[side1Direction] if pair.secondIsClass: localSide2 = tuple(sorted(side2Directions[side2Direction])) else: assert len(side2Directions[side2Direction]) == 1 (localSide2,) = side2Directions[side2Direction] # Skip pairs with clashing directions (e.g. "a" to "alef-ar"). if side1Direction != side2Direction and not any( side is Direction.Neutral for side in (side1Direction, side2Direction) ): LOGGER.info( "Skipping part of a kerning pair <%s %s %s> with mixed direction (%s, %s)", localSide1, localSide2, pair.value, side1Direction.name, side2Direction.name, ) continue # Skip pairs with clashing BiDi classes (e.g. "alef-ar" to "one-ar"). localSide1Bidis = { bidi for glyph in side1Directions[side1Direction] for bidi in glyph_bidi[glyph] } localSide2Bidis = { bidi for glyph in side2Directions[side2Direction] for bidi in glyph_bidi[glyph] } if localSide1Bidis != localSide2Bidis and not any( Direction.Neutral in side for side in (localSide1Bidis, localSide2Bidis) ): LOGGER.info( "Skipping part of a kerning pair <%s %s %s> with conflicting BiDi classes", localSide1, localSide2, pair.value, ) continue dominant_direction = ( side1Direction if side2Direction is Direction.Neutral else side2Direction ) yield (dominant_direction, KerningPair(localSide1, localSide2, pair.value)) def make_kerning_lookups( context: KernContext, options: SimpleNamespace ) -> dict[Direction, dict[str, fea_ast.LookupBlock]]: lookups: dict[Direction, dict[str, fea_ast.LookupBlock]] = {} if context.kerning.base_pairs_by_direction: make_split_kerning_lookups( context, options, lookups, context.kerning.base_pairs_by_direction ) if context.kerning.mark_pairs_by_direction: make_split_kerning_lookups( context, options, lookups, context.kerning.mark_pairs_by_direction, ignoreMarks=False, suffix="_marks", ) return lookups def make_split_kerning_lookups( context: KernContext, options: SimpleNamespace, lookups: dict[Direction, dict[str, fea_ast.LookupBlock]], kerning_per_direction: dict[Direction, list[KerningPair]], ignoreMarks: bool = True, suffix: str = "", ) -> None: bidiGlyphs = context.bidiGlyphs side1Classes = context.kerning.side1Classes side2Classes = context.kerning.side2Classes newClassDefs, newSide1Classes, newSide2Classes = make_all_glyph_class_definitions( kerning_per_direction, context, context.feaFile ) # NOTE: Consider duplicate names a bug, even if the classes would carry # the same glyphs. assert not context.kerning.classDefs.keys() & newClassDefs.keys() context.kerning.classDefs.update(newClassDefs) assert not side1Classes.keys() & newSide1Classes.keys() side1Classes.update(newSide1Classes) assert not side2Classes.keys() & newSide2Classes.keys() side2Classes.update(newSide2Classes) for direction, pairs in kerning_per_direction.items(): lookupName = f"kern_{direction.value}{suffix}" lookup = make_kerning_lookup( context, options, lookupName, ignoreMarks=ignoreMarks ) for pair in pairs: bidiTypes = { direction for direction, glyphs in bidiGlyphs.items() if not set(pair.glyphs).isdisjoint(glyphs) } if bidiTypes.issuperset(AMBIGUOUS_BIDIS): assert None, "this should have been caught by the splitter" # European and Arabic Numbers are always shaped LTR even in RTL scripts: pairIsRtl = ( direction == Direction.RightToLeft and Direction.LeftToRight not in bidiTypes ) rule = make_pairpos_rule(pair, side1Classes, side2Classes, pairIsRtl) lookup.statements.append(rule) lookups.setdefault(direction, {})[lookupName] = lookup def make_all_glyph_class_definitions( kerning_per_direction: dict[Direction, list[KerningPair]], context: KernContext, feaFile: fea_ast.FeatureFile | None = None, ): # Note: Refer to the context for existing classDefs and mappings of glyph # class tuples to feaLib AST to avoid overwriting existing class names, # because base and mark kerning pairs might be separate passes. newClassDefs = {} existingSide1Classes = context.kerning.side1Classes existingSide2Classes = context.kerning.side2Classes newSide1Classes = {} newSide2Classes = {} side1Membership = context.side1Membership side2Membership = context.side2Membership if feaFile is not None: classNames = {cdef.name for cdef in ast.iterClassDefinitions(feaFile)} else: classNames = set() classNames.update(context.kerning.classDefs.keys()) # Generate common class names first so that common classes are correctly # named in other lookups. for direction in ( Direction.Neutral, Direction.LeftToRight, Direction.RightToLeft, ): for pair in kerning_per_direction.get(direction, []): if ( pair.firstIsClass and pair.side1 not in existingSide1Classes and pair.side1 not in newSide1Classes ): addClassDefinition( "kern1", pair.side1, newSide1Classes, side1Membership, newClassDefs, classNames, direction.value, ) if ( pair.secondIsClass and pair.side2 not in existingSide2Classes and pair.side2 not in newSide2Classes ): addClassDefinition( "kern2", pair.side2, newSide2Classes, side2Membership, newClassDefs, classNames, direction.value, ) return newClassDefs, newSide1Classes, newSide2Classes def make_kerning_lookup( context: KernContext, options: SimpleNamespace, name: str, ignoreMarks: bool = True ) -> fea_ast.LookupBlock: lookup = fea_ast.LookupBlock(name) if ignoreMarks and options.ignoreMarks: # We only want to filter the spacing marks marks = set(context.gdefClasses.mark or []) & set(context.glyphSet.keys()) spacing = [] if marks: spacing = filter_spacing_marks(context, marks) if not spacing: # Simple case, there are no spacing ("Spacing Combining") marks, # do what we've always done. lookup.statements.append(ast.makeLookupFlag("IgnoreMarks")) else: # We want spacing marks to block kerns. className = f"MFS_{name}" filteringClass = ast.makeGlyphClassDefinitions( {className: spacing}, feaFile=context.feaFile )[className] lookup.statements.append(filteringClass) lookup.statements.append( ast.makeLookupFlag(markFilteringSet=filteringClass) ) return lookup def filter_spacing_marks(context: KernContext, marks: set[str]) -> list[str]: if context.isVariable: spacing = [] for mark in marks: if all( source.font[mark].width != 0 for source in context.font.sources if mark in source.font ): spacing.append(mark) return spacing return [mark for mark in marks if context.font[mark].width != 0] def make_pairpos_rule( pair: KerningPair, side1Classes, side2Classes, rtl: bool = False ) -> fea_ast.PairPosStatement: enumerated = pair.firstIsClass ^ pair.secondIsClass valuerecord = fea_ast.ValueRecord( xPlacement=pair.value if rtl else None, yPlacement=0 if rtl else None, xAdvance=pair.value, yAdvance=0 if rtl else None, ) if pair.firstIsClass: glyphs1 = fea_ast.GlyphClassName(side1Classes[pair.side1]) else: glyphs1 = fea_ast.GlyphName(pair.side1) if pair.secondIsClass: glyphs2 = fea_ast.GlyphClassName(side2Classes[pair.side2]) else: glyphs2 = fea_ast.GlyphName(pair.side2) return fea_ast.PairPosStatement( glyphs1=glyphs1, valuerecord1=valuerecord, glyphs2=glyphs2, valuerecord2=None, enumerated=enumerated, ) def make_feature_blocks( context: KernContext, lookups: dict[Direction, dict[str, Any]] ) -> Any: features = {} if "kern" in context.todo: kern = fea_ast.FeatureBlock("kern") register_lookups(context, kern, lookups) if kern.statements: features["kern"] = kern if "dist" in context.todo: dist = fea_ast.FeatureBlock("dist") register_lookups(context, dist, lookups) if dist.statements: features["dist"] = dist return features def register_lookups( context: KernContext, feature: fea_ast.FeatureBlock, lookups: dict[Direction, dict[str, fea_ast.LookupBlock]], ) -> None: # Ensure we have kerning for pure common script runs (e.g. ">1") isKernBlock = feature.name == "kern" lookupsNeutral: list[fea_ast.LookupBlock] = [] if isKernBlock and Direction.Neutral in lookups: lookupsNeutral.extend( lkp for lkp in lookups[Direction.Neutral].values() if lkp not in lookupsNeutral ) # InDesign bugfix: register kerning lookups for all LTR scripts under DFLT # so that the basic composer, without a language selected, will still kern. # Register LTR lookups if any, otherwise RTL lookups. if isKernBlock: lookupsLTR: list[fea_ast.LookupBlock] = ( list(lookups[Direction.LeftToRight].values()) if Direction.LeftToRight in lookups else [] ) lookupsRTL: list[fea_ast.LookupBlock] = ( list(lookups[Direction.RightToLeft].values()) if Direction.RightToLeft in lookups else [] ) lookupsNeutral.extend( lkp for lkp in (lookupsLTR or lookupsRTL) if lkp not in lookupsNeutral ) if lookupsNeutral: languages = context.feaLanguagesByTag.get("DFLT", ["dflt"]) ast.addLookupReferences(feature, lookupsNeutral, "DFLT", languages) # Feature blocks use script tags to distinguish what to run for a # Unicode script. # # "Script tags generally correspond to a Unicode script. However, the # associations between them may not always be one-to-one, and the # OpenType script tags are not guaranteed to be the same as Unicode # Script property-value aliases or ISO 15924 script IDs." # # E.g. {"latn": "Latn", "telu": "Telu", "tel2": "Telu"} # # Skip DFLT script because we always take care of it above for `kern`. # It never occurs in `dist`. if isKernBlock: scriptsToReference: set[str] = context.knownScripts - DIST_ENABLED_SCRIPTS else: scriptsToReference = DIST_ENABLED_SCRIPTS.intersection(context.knownScripts) scriptsToReference -= DFLT_SCRIPTS for script in sorted(scriptsToReference): script_direction = script_horizontal_direction(script, "LTR") for tag in unicodedata.ot_tags_from_script(script): lookupsForThisScript = {} if Direction.Neutral in lookups: lookupsForThisScript.update(lookups[Direction.Neutral]) if script_direction == "LTR" and Direction.LeftToRight in lookups: lookupsForThisScript.update(lookups[Direction.LeftToRight]) if script_direction == "RTL" and Direction.RightToLeft in lookups: lookupsForThisScript.update(lookups[Direction.RightToLeft]) if not lookupsForThisScript: continue if feature.statements: feature.statements.append(fea_ast.Comment("")) # Register the lookups for all languages defined in the feature # file for the script, otherwise kerning is not applied if any # language is set at all. languages = context.feaLanguagesByTag.get(tag, ["dflt"]) ast.addLookupReferences( feature, lookupsForThisScript.values(), tag, languages ) ufo2ft-3.3.1/Lib/ufo2ft/featureWriters/markFeatureWriter.py000066400000000000000000001336621470175262700237140ustar00rootroot00000000000000import itertools import re from collections import OrderedDict, defaultdict from functools import partial from ufo2ft.constants import INDIC_SCRIPTS, OBJECT_LIBS_KEY, USE_SCRIPTS from ufo2ft.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import ( classifyGlyphs, otRoundIgnoringVariable, unicodeInScripts, unicodeScriptExtensions, ) class AbstractMarkPos: """Object containing all the mark attachments for glyph 'name'. The 'marks' is a list of NamedAnchor objects. Provides methods to filter marks given some callable, and convert itself to feaLib AST 'pos' statements for mark2base, mark2liga and mark2mark lookups. """ Statement = None def __init__(self, name, marks): self.name = name self.marks = marks def _filterMarks(self, include): return [anchor for anchor in self.marks if include(anchor)] def _marksAsAST(self): return [ ( ast.Anchor( x=otRoundIgnoringVariable(anchor.x), y=otRoundIgnoringVariable(anchor.y), ), anchor.markClass, ) for anchor in sorted(self.marks, key=lambda a: a.name) ] def asAST(self): marks = self._marksAsAST() return self.Statement(ast.GlyphName(self.name), marks) def __str__(self): return self.asAST().asFea() # pragma: no cover def filter(self, include): marks = self._filterMarks(include) return self.__class__(self.name, marks) if any(marks) else None def getMarkGlyphToMarkClasses(self): """Return a list of pairs (markGlyph, markClasses).""" markGlyphToMarkClasses = defaultdict(set) for namedAnchor in self.marks: for markGlyph in namedAnchor.markClass.glyphs: markGlyphToMarkClasses[markGlyph].add(namedAnchor.markClass.name) return markGlyphToMarkClasses.items() class MarkToBasePos(AbstractMarkPos): Statement = ast.MarkBasePosStatement class MarkToMarkPos(AbstractMarkPos): Statement = ast.MarkMarkPosStatement class MarkToLigaPos(AbstractMarkPos): Statement = ast.MarkLigPosStatement def _filterMarks(self, include): return [ [anchor for anchor in component if include(anchor)] for component in self.marks ] def _marksAsAST(self): return [ [ ( 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 ] def getMarkGlyphToMarkClasses(self): """Return a list of pairs (markGlyph, markClasses).""" markGlyphToMarkClasses = defaultdict(set) for component in self.marks: for namedAnchor in component: for markGlyph in namedAnchor.markClass.glyphs: markGlyphToMarkClasses[markGlyph].add(namedAnchor.markClass.name) return markGlyphToMarkClasses.items() MARK_PREFIX = LIGA_SEPARATOR = "_" LIGA_NUM_RE = re.compile(r".*?(\d+)$") def parseAnchorName( anchorName, markPrefix=MARK_PREFIX, ligaSeparator=LIGA_SEPARATOR, ligaNumRE=LIGA_NUM_RE, ignoreRE=None, ): """Parse anchor name and return a tuple that specifies: 1) whether the anchor is a "mark" anchor (bool); 2) the "key" name of the anchor, i.e. the name after stripping all the prefixes and suffixes, which identifies the class it belongs to (str); 3) An optional number (int), starting from 1, which identifies that index of the ligature component the anchor refers to. The 'ignoreRE' argument is an optional regex pattern (str) identifying sub-strings in the anchor name that should be ignored when parsing the three elements above. """ number = None isContextual = False if ignoreRE is not None: anchorName = re.sub(ignoreRE, "", anchorName) if anchorName[0] == "*": isContextual = True anchorName = anchorName[1:] anchorName = re.sub(r"\..*", "", anchorName) m = ligaNumRE.match(anchorName) if not m: key = anchorName else: number = m.group(1) key = anchorName.rstrip(number) separator = ligaSeparator if key.endswith(separator): assert separator key = key[: -len(separator)] number = int(number) else: # not a valid ligature anchor name key = anchorName number = None if anchorName.startswith(markPrefix) and key: if number is not None: raise ValueError("mark anchor cannot be numbered: %r" % anchorName) isMark = True key = key[len(markPrefix) :] if not key: raise ValueError("mark anchor key is nil: %r" % anchorName) else: isMark = False isIgnorable = key and not key[0].isalpha() return isMark, key, number, isContextual, isIgnorable class NamedAnchor: """A position with a name, and an associated markClass.""" __slots__ = ( "name", "x", "y", "isMark", "key", "number", "markClass", "isContextual", "isIgnorable", "libData", ) # subclasses can customize these to use different anchor naming schemes markPrefix = MARK_PREFIX ignoreRE = None ligaSeparator = LIGA_SEPARATOR ligaNumRE = LIGA_NUM_RE def __init__(self, name, x, y, markClass=None, libData=None): self.name = name self.x = x self.y = y isMark, key, number, isContextual, isIgnorable = parseAnchorName( name, markPrefix=self.markPrefix, ligaSeparator=self.ligaSeparator, ligaNumRE=self.ligaNumRE, ignoreRE=self.ignoreRE, ) if number is not None: if number < 1: raise ValueError("ligature component indexes must start from 1") else: assert key, name self.isMark = isMark self.key = key self.number = number self.markClass = markClass self.isContextual = isContextual self.isIgnorable = isIgnorable self.libData = libData @property def markAnchorName(self): return self.markPrefix + self.key def __repr__(self): items = ("{}={!r}".format(k, getattr(self, k)) for k in ("name", "x", "y")) return "%s(%s)" % (type(self).__name__, ", ".join(items)) def colorGraph(adjacency): """Color the graph defined by the provided adjacency lists. The input is a dict of iterables. Each entry of the dict is one vertex, and the value is a list of neighbours of that vertex. The input graph is expected to be undirected and the input should reflect that (have symmetric adjacency for A -> B and B -> A). Vertices that don't have neighbours should still be present in the input. The output is a list of lists, each list being one color assignment, and its members being vertices. """ # Basic implementation # https://en.wikipedia.org/wiki/Greedy_coloring colors = dict() # Sorted for reproducibility, probably not the optimal vertex order for node in sorted(adjacency): usedNeighbourColors = { colors[neighbour] for neighbour in adjacency[node] if neighbour in colors } colors[node] = firstAvailable(usedNeighbourColors) groups = defaultdict(list) for node, color in colors.items(): groups[color].append(node) return list(groups.values()) def firstAvailable(colorSet): """Return smallest non-negative integer not in the given set of colors.""" count = 0 while True: if count not in colorSet: return count count += 1 class MarkFeatureWriter(BaseFeatureWriter): """Generates a mark, mkmk, abvm and blwm features based on glyph anchors. The default mode is 'skip': i.e. if any of the supported features is already present in the feature file, it is not generated again. The optional 'append' mode will add extra lookups to already existing features, if any. New markClass definitions with unique names are generated when the mark anchors from UFO glyphs are different from those already defined in the feature file, otherwise the existing markClass definitions are reused in the newly appended lookups. Anchors prefixed with "_" are considered mark anchors; any glyph containing those is as such considered a mark glyph, thus added to markClass definitions, and in mark-to-mark lookups (if the glyph also contains other non-underscore-prefixed anchors). Anchors suffixed with a number, e.g. "top_1", "bottom_2", etc., are used for ligature glyphs. The number refers to the index (counting from 1) of the ligature component where the mark is meant to be attached. It is possible that a ligature component has no marks defined, in which case one can have an anchor with an empty name and only the number (e.g. '_3'), which is encoded as '' in the generated 'pos ligature' statement. If the glyph set contains glyphs whose unicode codepoint's script extension property intersects with one of the scripts which are processed by the Indic, USE, or Khmer complex shapers, then the "abvm" and "blwm" features are also generated for those glyphs, as well as for alternate glyphs only accessible via GSUB substitutions. The "abvm" (above-base marks) and "blwm" (below-base marks) features include all mark2base, mark2liga and mark2mark attachments for Indic/USE/Khmer glyphs containing anchors from predefined lists of "above" and "below" anchor names (see below). If these glyphs contain anchors with names not in those lists, the anchors' vertical position relative to the half of the UPEM square is used to decide whether they are considered above or below. If the `quantization` argument is given in the filter options, the resulting anchors are rounded to the nearest multiple of the quantization value. If `groupMarkClases=True`, mark-to-base or mark-to-ligature attachments that reference non-overlapping mark classes will get grouped in the same lookup; and if a mark glyph is in more than one mark class, additional lookups will be generated for those as required. NOTE: this was the default behavior until ufo2ft 2.33.4. The current default behavior was simplified to match other font editors and we now build as many mark-to-base and mark-to-liga lookups as there are mark classes, and lookups are sorted alphabetically by the mark class name so the more specific ('top.alt' instead 'top') would be applied last and wins in case when the same base or ligature glyph can attach to the same mark through multiple mark classes. https://github.com/googlefonts/ufo2ft/issues/591 """ options = dict(quantization=1, groupMarkClasses=False) tableTag = "GPOS" features = frozenset(["mark", "mkmk", "abvm", "blwm"]) # subclasses may override this to use different anchor naming schemes NamedAnchor = NamedAnchor # @MC_top, @MC_bottom, etc. markClassPrefix = "MC" abvmAnchorNames = { "top", "topleft", "topright", "candra", "bindu", "candrabindu", "imatra", } blwmAnchorNames = {"bottom", "bottomleft", "bottomright", "nukta"} scriptsUsingAbvm = set(INDIC_SCRIPTS + USE_SCRIPTS + ["Khmr"]) # Glyphs moves "_bottom" and "_top" (if present) to the top of # the list and then picks the first to use in the mark feature. # https://github.com/googlei18n/noto-source/issues/122#issuecomment-403952188 anchorSortKey = {"_bottom": -2, "_top": -1} def setContext(self, font, feaFile, compiler=None): ctx = super().setContext(font, feaFile, compiler=compiler) ctx.gdefClasses = self.getGDEFGlyphClasses() ctx.anchorLists = self._getAnchorLists() ctx.anchorPairs = self._getAnchorPairs() ctx.feaScripts = set(ast.getScriptLanguageSystems(feaFile).keys()) def shouldContinue(self): if not self.context.anchorPairs: self.log.debug("No mark-attaching anchors found; skipped") return False return super().shouldContinue() def _getAnchorLists(self): gdefClasses = self.context.gdefClasses if gdefClasses.base is not None: # only include the glyphs listed in the GDEF.GlyphClassDef groups include = gdefClasses.base | gdefClasses.ligature | gdefClasses.mark else: # no GDEF table defined in feature file, include all glyphs include = None result = OrderedDict() for glyphName, glyph in self.getOrderedGlyphSet().items(): if include is not None and glyphName not in include: continue anchorDict = OrderedDict() for anchor in glyph.anchors: anchorName = anchor.name if not anchorName: self.log.warning( "unnamed anchor discarded in glyph '%s'", glyphName ) continue if anchorName in anchorDict: self.log.warning( "duplicate anchor '%s' in glyph '%s'", anchorName, glyphName ) x, y = self._getAnchor(glyphName, anchorName, anchor=anchor) libData = None if anchor.identifier: libData = glyph.lib[OBJECT_LIBS_KEY].get(anchor.identifier) a = self.NamedAnchor(name=anchorName, x=x, y=y, libData=libData) if a.isContextual and not libData: continue if a.isIgnorable: continue anchorDict[anchorName] = a if anchorDict: result[glyphName] = list(anchorDict.values()) return result def _getAnchorPairs(self): markAnchorNames = set() for anchors in self.context.anchorLists.values(): markAnchorNames.update(a.name for a in anchors if a.isMark) anchorPairs = {} for anchors in self.context.anchorLists.values(): for anchor in anchors: if anchor.isMark: continue markAnchorName = anchor.markAnchorName if markAnchorName in markAnchorNames: anchorPairs[anchor.name] = markAnchorName return anchorPairs def _pruneUnusedAnchors(self): baseAnchorNames = set(self.context.anchorPairs.keys()) markAnchorNames = set(self.context.anchorPairs.values()) attachingAnchorNames = baseAnchorNames | markAnchorNames for glyphName, anchors in list(self.context.anchorLists.items()): for anchor in list(anchors): if anchor.name not in attachingAnchorNames and anchor.key: anchors.remove(anchor) if not anchors: del self.context.anchorLists[glyphName] def _groupMarkGlyphsByAnchor(self): gdefMarks = self.context.gdefClasses.mark markAnchorNames = set(self.context.anchorPairs.values()) markGlyphNames = set() groups = {} for glyphName, anchors in self.context.anchorLists.items(): # if the feature file has a GDEF table with GlyphClassDef defined, # only include mark glyphs that are referenced in there, otherwise # include any glyphs that contain an "_" prefixed anchor. if gdefMarks is not None and glyphName not in gdefMarks: continue markAnchors = [a for a in anchors if a.name in markAnchorNames] if not markAnchors: continue # Use all mark anchors. The rest of the algorithm will make sure # that the generated lookups will not have overlapping mark classes. for anchor in markAnchors: group = groups.setdefault(anchor.name, OrderedDict()) assert glyphName not in group group[glyphName] = anchor markGlyphNames.add(glyphName) self.context.markGlyphNames = markGlyphNames return groups def _makeMarkClassDefinitions(self): markGlyphSets = self._groupMarkGlyphsByAnchor() currentClasses = self.context.feaFile.markClasses allMarkClasses = self.context.markClasses = {} classPrefix = self.markClassPrefix newDefs = [] for markAnchorName, glyphAnchorPairs in sorted(markGlyphSets.items()): className = ast.makeFeaClassName(classPrefix + markAnchorName) for glyphName, anchor in glyphAnchorPairs.items(): mcd = self._defineMarkClass( glyphName, anchor.x, anchor.y, className, currentClasses ) if mcd is not None: newDefs.append(mcd) # this may be different because of name clashes className = mcd.markClass.name allMarkClasses[anchor.key] = currentClasses[className] return newDefs def _defineMarkClass(self, glyphName, x, y, className, markClasses): anchor = ast.Anchor(x=otRoundIgnoringVariable(x), y=otRoundIgnoringVariable(y)) markClass = markClasses.get(className) if markClass is None: markClass = ast.MarkClass(className) markClasses[className] = markClass else: if glyphName in markClass.glyphs: mcdef = markClass.glyphs[glyphName] if self._anchorsAreEqual(anchor, mcdef.anchor): self.log.debug( "Glyph %s already defined in markClass @%s", glyphName, className, ) return None else: # same mark glyph defined with different anchors for the # same markClass; make a new unique markClass definition newClassName = ast.makeFeaClassName(className, markClasses) markClass = ast.MarkClass(newClassName) markClasses[newClassName] = markClass glyphName = ast.GlyphName(glyphName) mcdef = ast.MarkClassDefinition(markClass, anchor, glyphName) markClass.addDefinition(mcdef) return mcdef @staticmethod def _anchorsAreEqual(a1, a2): # TODO add __eq__ to feaLib AST objects? return all( getattr(a1, attr) == getattr(a2, attr) for attr in ("x", "y", "contourpoint", "xDeviceTable", "yDeviceTable") ) def _setBaseAnchorMarkClasses(self): markClasses = self.context.markClasses for anchors in self.context.anchorLists.values(): for anchor in anchors: if anchor.isMark or not anchor.key or anchor.key not in markClasses: continue anchor.markClass = markClasses[anchor.key] def _groupMarkClasses(self, markGlyphToMarkClasses): # To compute the number of lookups that we need to build, we want # the minimum number of lookups such that, whenever a mark glyph # belongs to several mark classes, these classes are not in the same # lookup. A trivial solution is to make 1 lookup per mark class # but that's a bit wasteful, we might be able to do better by grouping # mark classes that do not conflict. # This is a graph coloring problem: the graph nodes are mark classes, # edges are between classes that would conflict and the colors are # the lookups in which they can go. adjacency = { # We'll get the same markClass several times in the dict # comprehension below but it's ok, only one will be kept. markClass: set() for markClasses in markGlyphToMarkClasses.values() for markClass in markClasses } for _markGlyph, markClasses in markGlyphToMarkClasses.items(): for markClass, other in itertools.combinations(markClasses, 2): adjacency[markClass].add(other) adjacency[other].add(markClass) colorGroups = colorGraph(adjacency) # Sort the groups, because the group that contains MC_top or MC_bottom # needs to go to the end (as specified in self.anchorSortKey) so that # they are applied last and "win" in case of conflict. # We also sort alphabetically for reproducibility, both within each # group and between groups. return sorted( [sorted(group) for group in colorGroups], key=lambda group: ( # The first part sorts _top and _bottom at the end. # There's a minus sign in front of the min because the original # self.anchorSortKey was designed to put the _top and _bottom # at the start (and now we want them at the end). -min( # Remove the MC prefix because that's how the mark classes # are looking at this stage (the original # self.anchorSortKey was applied at a different stage of # the algorithm, on anchors instead of mark classes) self.anchorSortKey.get(self._removeClassPrefix(markClass), 0) for markClass in group ), # Second part of the tuple sorts the groups lexicographically group, ), ) def _logIfAmbiguous(self, attachments, groupedMarkClasses): """Warn about ambiguous situations and log the current resolution. An anchor attachment is ambiguous if for the same mark glyph, more than one mark class can be used to attach it to the base. """ for attachment in attachments: for markGlyph, markClasses in attachment.getMarkGlyphToMarkClasses(): if len(markClasses) > 1: self.log.info( "The base glyph %s and mark glyph %s are ambiguously " "connected by several anchor classes: %s. " "The last one will prevail.", attachment.name, markGlyph, ", ".join( markClass for group in groupedMarkClasses for markClass in group if markClass in markClasses ), ) def _removeClassPrefix(self, markClass): assert markClass.startswith(self.markClassPrefix) return markClass[len(self.markClassPrefix) :] def _groupAttachments(self, attachments): """Group the given attachments so that no group contains conflicting anchor classes for the same glyph. """ # Idea for mark2base: # attachments is a list of mark to base pairs, linked together through # an anchor name We have to put them into one or more lookups with the # constraint that the same mark glyph cannot appear twice in the same # lookup while using different anchor names. # Idea for mark2liga: # attachments is a list of mark to liga positioning. Each links # together a base ligature with several marks, through numbered anchor # names. # We have to put them into one or more lookups with the constraint that # the same mark glyph cannot appear twice in the same lookup while # using different anchor names. # To do so, if a single attachment refers to to the same mark twice # through different anchor names, we may have to split the attachment # into two attachments, using null anchors instead of one or the other # mark class in each split attachment. if self.options.groupMarkClasses: markGlyphToMarkClasses = defaultdict(set) for attachment in attachments: for markGlyph, markClasses in attachment.getMarkGlyphToMarkClasses(): markGlyphToMarkClasses[markGlyph].update(markClasses) groupedMarkClasses = self._groupMarkClasses(markGlyphToMarkClasses) else: # this will generate one lookup per mark class, and sort them # lexicographically by the anchor name, so the lookup for e.g. # '_top.alt01' will occur *after* the one for `_top` (the last wins) thus # allowing some degree of control on potentially ambiguous attachments # https://github.com/googlefonts/ufo2ft/issues/762 # https://github.com/googlefonts/ufo2ft/issues/591 groupedMarkClasses = [ [markClass.name] for _, markClass in sorted(self.context.markClasses.items()) ] self._logIfAmbiguous(attachments, groupedMarkClasses) lookups = [] for markClasses in groupedMarkClasses: lookup = [] # Filter existing attachments for attachment in attachments: # One attachment has one base glyph and many marks, each of # the class NamedAnchor. Each NamedAnchor has one markClass. # We keep the NamedAnchor if the markClass is allowed in the # current lookup. def include(anchor): return anchor.markClass.name in markClasses # noqa: B023 filteredAttachment = attachment.filter(include) if filteredAttachment: lookup.append(filteredAttachment) lookups.append(lookup) return lookups def _makeMarkToBaseAttachments(self): markGlyphNames = self.context.markGlyphNames baseClass = self.context.gdefClasses.base result = [] for glyphName, anchors in self.context.anchorLists.items(): # exclude mark glyphs, or glyphs not listed in GDEF Base if glyphName in markGlyphNames or ( baseClass is not None and glyphName not in baseClass ): continue baseMarks = [] for anchor in anchors: if anchor.markClass is None or anchor.number is not None: # skip anchors for which no mark class is defined; also # skip '_1', '_2', etc. suffixed anchors for this lookup # type; these will be are added in the mark2liga lookup continue if anchor.isContextual: # skip contextual anchors. They are handled separately. continue assert not anchor.isMark baseMarks.append(anchor) if not baseMarks: continue result.append(MarkToBasePos(glyphName, baseMarks)) return result def _makeMarkToMarkAttachments(self): markGlyphNames = self.context.markGlyphNames # we make a dict of lists containing mkmk pos rules keyed by # anchor name, so we can create one mkmk lookup per markClass # each with different mark filtering sets. results = {} for glyphName, anchors in self.context.anchorLists.items(): if glyphName not in markGlyphNames: continue for anchor in anchors: # skip anchors for which no mark class is defined if anchor.markClass is None or anchor.isMark: continue if anchor.isContextual: # skip contextual anchors. They are handled separately. continue if anchor.number is not None: self.log.warning( "invalid ligature anchor '%s' in mark glyph '%s'; " "skipped", anchor.name, glyphName, ) continue pos = MarkToMarkPos(glyphName, [anchor]) results.setdefault(anchor.key, []).append(pos) return results def _makeMarkToLigaAttachments(self): markGlyphNames = self.context.markGlyphNames ligatureClass = self.context.gdefClasses.ligature result = [] for glyphName, anchors in self.context.anchorLists.items(): # exclude mark glyphs, or glyphs not listed in GDEF Ligature if glyphName in markGlyphNames or ( ligatureClass is not None and glyphName not in ligatureClass ): continue componentAnchors = {} for anchor in anchors: if anchor.markClass is None and anchor.key: # skip anchors for which no mark class is defined continue assert not anchor.isMark number = anchor.number if number is None: # we handled these in the mark2base lookup continue if anchor.isContextual: # skip contextual anchors. They are handled separately. continue # unnamed anchors with only a number suffix "_1", "_2", etc. # are understood as the ligature component having if not anchor.key: componentAnchors[number] = [] else: componentAnchors.setdefault(number, []).append(anchor) if not componentAnchors: continue ligatureMarks = [] # ligature components are indexed from 1; any missing intermediate # anchor number means the component has for number in range(1, max(componentAnchors.keys()) + 1): ligatureMarks.append(componentAnchors.get(number, [])) result.append(MarkToLigaPos(glyphName, ligatureMarks)) return result def _makeContextualAttachments(self, glyphClass, liga=False): ctx = self.context result = defaultdict(list) markGlyphNames = ctx.markGlyphNames for glyphName, anchors in sorted(ctx.anchorLists.items()): if glyphName in markGlyphNames: continue if glyphClass and glyphName not in glyphClass: continue for anchor in anchors: # Skip non-contextual anchors if not anchor.isContextual: continue # If we are building the mark2liga lookup, skip anchors without a number if liga and anchor.number is None: continue # If we are building the mark2base lookup, skip anchors with a number if not liga and anchor.number is not None: continue anchor_context = anchor.libData.get("GPOS_Context", "").strip() if not anchor_context: self.log.warning( "contextual anchor '%s' in glyph '%s' has no context data; skipped", anchor.name, glyphName, ) continue result[anchor_context].append((glyphName, anchor)) return result @staticmethod def _iterAttachments(attachments, include=None, marksFilter=None): for pos in attachments: if include is not None and not include(pos.name): continue if marksFilter is not None: pos = pos.filter(marksFilter) if pos is None: continue yield pos def _makeMarkLookup(self, lookupName, attachments, include, marksFilter=None): statements = [ pos.asAST() for pos in self._iterAttachments(attachments, include, marksFilter) ] if statements: lkp = ast.LookupBlock(lookupName) lkp.statements.extend(statements) return lkp def _makeMarkFilteringSetClass(self, lookupName, attachments, markClass, include): markGlyphs = (glyphName for glyphName in markClass.glyphs if include(glyphName)) baseGlyphs = ( pos.name for pos in attachments if pos.name not in markClass.glyphs ) members = itertools.chain(markGlyphs, baseGlyphs) className = "MFS_%s" % lookupName return ast.makeGlyphClassDefinitions( {className: members}, feaFile=self.context.feaFile )[className] def _makeMarkToMarkLookup( self, anchorName, attachments, include, marksFilter=None, featureTag=None ): attachments = list(self._iterAttachments(attachments, include, marksFilter)) if not attachments: return prefix = (featureTag + "_") if featureTag is not None else "" lookupName = f"{prefix}mark2mark_{anchorName}" filteringClass = self._makeMarkFilteringSetClass( lookupName, attachments, markClass=self.context.markClasses[anchorName], include=include, ) lkp = ast.LookupBlock(lookupName) lkp.statements.append(filteringClass) lkp.statements.append(ast.makeLookupFlag(markFilteringSet=filteringClass)) lkp.statements.extend(pos.asAST() for pos in attachments) return lkp def _makeMarkFeature(self, include): # First make the non-contextual lookups baseLkps = [] for attachments in self.context.groupedMarkToBaseAttachments: i = len(baseLkps) lookup = self._makeMarkLookup( f"mark2base{'_' + str(i) if i > 0 else ''}", attachments, include ) if lookup: baseLkps.append(lookup) ligaLkps = [] for attachments in self.context.groupedMarkToLigaAttachments: i = len(ligaLkps) lookup = self._makeMarkLookup( f"mark2liga{'_' + str(i) if i > 0 else ''}", attachments, include ) if lookup: ligaLkps.append(lookup) # Then make the contextual ones refLkps = [] ctxLkps = {} # We sort the full context by longest first. This isn't perfect # but it gives us the best chance that more specific contexts # (typically longer) will take precedence over more general ones. for context, glyph_anchor_pair in sorted( self.context.contextualMarkToBaseAnchors.items(), key=lambda x: -len(x[0]) ): # Group by anchor attachments = defaultdict(list) for glyphName, anchor in glyph_anchor_pair: attachments[anchor.key].append(MarkToBasePos(glyphName, [anchor])) self._makeContextualMarkLookup( attachments, context, refLkps, ctxLkps, ) for context, glyph_anchor_pair in sorted( self.context.contextualMarkToLigaAnchors.items(), key=lambda x: -len(x[0]) ): # Group by anchor attachments = defaultdict(list) for glyphName, anchor in glyph_anchor_pair: marks = [[]] * max( a.number for a in self.context.anchorLists[glyphName] if a.key and a.number is not None ) marks[anchor.number - 1] = [anchor] attachments[anchor.key].append(MarkToLigaPos(glyphName, marks)) self._makeContextualMarkLookup( attachments, context, refLkps, ctxLkps, ) ctxLkps = list(ctxLkps.values()) if not baseLkps and not ligaLkps and not ctxLkps: return None, [] feature = ast.FeatureBlock("mark") if ctxLkps: # When we have contextual lookups, we need to make sure that the # contextual and non-contextual lookups are in the right order # and we can’t use nested lookups inside the feature block for # the referenced lookups, so we put all lookups outside the feature # and use lookup references instead. # We should probably always do this, as nested lookups are full of # gotchas, but this will require updating many test expectations. lookups = baseLkps + ligaLkps + refLkps + ctxLkps for lookup in baseLkps + ligaLkps + ctxLkps: feature.statements.append(ast.LookupReferenceStatement(lookup)) else: lookups = [] for lookup in baseLkps + ligaLkps: feature.statements.append(lookup) return feature, lookups def _makeContextualMarkLookup( self, attachments, fullcontext, refLkps, ctxLkps, ): for anchorKey, statements in attachments.items(): # First make the contextual lookup if ";" in fullcontext: before, after = fullcontext.split(";") else: before, after = "", fullcontext after = after.strip() if before not in ctxLkps: ctxLkps[before] = ast.LookupBlock( f"ContextualMarkDispatch_{len(ctxLkps)}" ) if before: # I know it's not really a comment but this is the easiest way # to get the lookup flag in there without reparsing it. ctxLkps[before].statements.append(ast.Comment(f"{before};")) ctxLkp = ctxLkps[before] ctxLkp.statements.append(ast.Comment(f"# {after}")) # Insert mark glyph names after base glyph names if not specified otherwise. if "&" not in after: after = after.replace("*", "* &") baseGlyphNames = " ".join([s.name for s in statements]) marks = ast.MarkClassName(self.context.markClasses[anchorKey]).asFea() # Replace * with base glyph names contextual = after.replace("*", f"[{baseGlyphNames}]") # Replace & with mark glyph names refLkpName = f"ContextualMark_{len(refLkps)}" contextual = contextual.replace("&", f"{marks}' lookup {refLkpName}") ctxLkp.statements.append(ast.Comment(f"pos {contextual};")) # Then make the non-contextual lookup it references refLkp = ast.LookupBlock(refLkpName) refLkp.statements = [s.asAST() for s in statements] refLkps.append(refLkp) def _makeMkmkFeature(self, include): feature = ast.FeatureBlock("mkmk") for anchorName, attachments in sorted( self.context.markToMarkAttachments.items() ): lkp = self._makeMarkToMarkLookup(anchorName, attachments, include) if lkp is not None: feature.statements.append(lkp) return feature if feature.statements else None def _isAboveMark(self, anchor): if anchor.name in self.abvmAnchorNames: return True if anchor.name in self.blwmAnchorNames or anchor.name.startswith("bottom"): return False # Glyphs uses (used to use?) a heuristic to guess whether an anchor # should go into abvm or blwm. (See # https://github.com/googlefonts/ufo2ft/issues/179#issuecomment-390391382) # However, this causes issues in variable fonts where an anchor in one # master is assigned to a different feature from the same anchor in # another master if the Y-coordinates happen to straddle the threshold # coordinate. For simplicity, we just place all unknown anchors into # the abvm feature. return True def _isBelowMark(self, anchor): return not self._isAboveMark(anchor) def _makeAbvmOrBlwmFeature(self, tag, include): if tag == "abvm": marksFilter = self._isAboveMark elif tag == "blwm": marksFilter = self._isBelowMark else: raise AssertionError(tag) baseLkps = [] for attachments in self.context.groupedMarkToBaseAttachments: i = len(baseLkps) lookup = self._makeMarkLookup( f"{tag}_mark2base{'_' + str(i) if i > 0 else ''}", attachments, include=include, marksFilter=marksFilter, ) if lookup: baseLkps.append(lookup) ligaLkps = [] for attachments in self.context.groupedMarkToLigaAttachments: i = len(ligaLkps) lookup = self._makeMarkLookup( f"{tag}_mark2liga{'_' + str(i) if i > 0 else ''}", attachments, include=include, marksFilter=marksFilter, ) if lookup: ligaLkps.append(lookup) mkmkLookups = [] for anchorName, attachments in sorted( self.context.markToMarkAttachments.items() ): lkp = self._makeMarkToMarkLookup( anchorName, attachments, include=include, marksFilter=marksFilter, featureTag=tag, ) if lkp is not None: mkmkLookups.append(lkp) if not any([baseLkps, ligaLkps, mkmkLookups]): return feature = ast.FeatureBlock(tag) for baseLkp in baseLkps: feature.statements.append(baseLkp) for ligaLkp in ligaLkps: feature.statements.append(ligaLkp) feature.statements.extend(mkmkLookups) return feature def _makeFeatures(self): ctx = self.context # First do non-contextual lookups ctx.groupedMarkToBaseAttachments = self._groupAttachments( self._makeMarkToBaseAttachments() ) ctx.groupedMarkToLigaAttachments = self._groupAttachments( self._makeMarkToLigaAttachments() ) ctx.markToMarkAttachments = self._makeMarkToMarkAttachments() baseClass = self.context.gdefClasses.base ctx.contextualMarkToBaseAnchors = self._makeContextualAttachments(baseClass) ligatureClass = self.context.gdefClasses.ligature ctx.contextualMarkToLigaAnchors = self._makeContextualAttachments( ligatureClass, True, ) abvmGlyphs, notAbvmGlyphs = self._getAbvmGlyphs() def isAbvm(glyphName): return glyphName in abvmGlyphs def isNotAbvm(glyphName): return glyphName in notAbvmGlyphs features = {} lookups = [] todo = ctx.todo if "mark" in todo: mark, markLookups = self._makeMarkFeature(include=isNotAbvm) if mark is not None: features["mark"] = mark lookups.extend(markLookups) if "mkmk" in todo: mkmk = self._makeMkmkFeature(include=isNotAbvm) if mkmk is not None: features["mkmk"] = mkmk if "abvm" in todo or "blwm" in todo: if abvmGlyphs: for tag in ("abvm", "blwm"): if tag not in todo: continue feature = self._makeAbvmOrBlwmFeature(tag, include=isAbvm) if feature is not None: features[tag] = feature return features, lookups def _getAbvmGlyphs(self): glyphSet = set(self.getOrderedGlyphSet().keys()) scriptsUsingAbvm = self.scriptsUsingAbvm if self.context.feaScripts: # https://github.com/googlefonts/ufo2ft/issues/579 Some characters # can be used in multiple scripts and some of these scripts might # need an abvm feature and some might not, so we filter-out the # abvm scripts that the font does not intend to support. scriptsUsingAbvm = scriptsUsingAbvm & self.context.feaScripts if scriptsUsingAbvm: cmap = self.makeUnicodeToGlyphNameMapping() unicodeIsAbvm = partial(unicodeInScripts, scripts=scriptsUsingAbvm) def unicodeIsNotAbvm(uv): return bool(unicodeScriptExtensions(uv) - self.scriptsUsingAbvm) if any(unicodeIsAbvm(uv) for uv in cmap): # If there are any characters from Indic/USE/Khmer scripts in # the cmap, we compile a temporary GSUB table to resolve # substitutions and get the set of all the relevant glyphs, # including alternate glyphs. gsub = self.compileGSUB() extras = self.extraSubstitutions() glyphGroups = classifyGlyphs(unicodeIsAbvm, cmap, gsub, extras) # the 'glyphGroups' dict is keyed by the return value of the # classifying include, so here 'True' means all the # Indic/USE/Khmer glyphs abvmGlyphs = glyphGroups.get(True, set()) # If a character can be used in Indic/USE/Khmer scripts as well # as other scripts, we want to return it in both 'abvmGlyphs' # (done above) and 'notAbvmGlyphs' (done below) sets. glyphGroups = classifyGlyphs(unicodeIsNotAbvm, cmap, gsub, extras) notAbvmGlyphs = glyphGroups.get(True, set()) # Since cmap might not cover all glyphs, we union with the # glyph set. notAbvmGlyphs |= glyphSet - abvmGlyphs return abvmGlyphs, notAbvmGlyphs return set(), glyphSet def _write(self): self._pruneUnusedAnchors() newClassDefs = self._makeMarkClassDefinitions() self._setBaseAnchorMarkClasses() features, lookups = self._makeFeatures() if not features: return False feaFile = self.context.feaFile self._insert( feaFile=feaFile, markClassDefs=newClassDefs, features=[features[tag] for tag in sorted(features.keys())], lookups=lookups, ) return True ufo2ft-3.3.1/Lib/ufo2ft/filters/000077500000000000000000000000001470175262700163215ustar00rootroot00000000000000ufo2ft-3.3.1/Lib/ufo2ft/filters/__init__.py000066400000000000000000000126071470175262700204400ustar00rootroot00000000000000import importlib import logging from inspect import getfullargspec, isclass from ufo2ft.constants import FILTERS_KEY from ufo2ft.util import _loadPluginFromString from .base import BaseFilter, BaseIFilter from .cubicToQuadratic import CubicToQuadraticFilter from .decomposeComponents import DecomposeComponentsFilter, DecomposeComponentsIFilter from .decomposeTransformedComponents import ( DecomposeTransformedComponentsFilter, DecomposeTransformedComponentsIFilter, ) from .dottedCircle import DottedCircleFilter from .explodeColorLayerGlyphs import ExplodeColorLayerGlyphsFilter from .flattenComponents import FlattenComponentsFilter, FlattenComponentsIFilter from .propagateAnchors import PropagateAnchorsFilter, PropagateAnchorsIFilter from .removeOverlaps import RemoveOverlapsFilter from .reverseContourDirection import ReverseContourDirectionFilter from .skipExportGlyphs import SkipExportGlyphsFilter, SkipExportGlyphsIFilter from .sortContours import SortContoursFilter from .transformations import TransformationsFilter __all__ = [ "BaseFilter", "BaseIFilter", "CubicToQuadraticFilter", "DecomposeComponentsFilter", "DecomposeComponentsIFilter", "DecomposeTransformedComponentsFilter", "DecomposeTransformedComponentsIFilter", "DottedCircleFilter", "ExplodeColorLayerGlyphsFilter", "FlattenComponentsFilter", "FlattenComponentsIFilter", "PropagateAnchorsFilter", "PropagateAnchorsIFilter", "RemoveOverlapsFilter", "ReverseContourDirectionFilter", "SkipExportGlyphsFilter", "SkipExportGlyphsIFilter", "SortContoursFilter", "TransformationsFilter", "loadFilters", "loadFilterFromString", ] logger = logging.getLogger(__name__) def getFilterClass(filterName, pkg="ufo2ft.filters"): """Given a filter name, import and return the filter class. By default, filter modules are searched within the ``ufo2ft.filters`` package. """ # TODO add support for third-party plugin discovery? # if filter name is 'Foo Bar', the module should be called 'fooBar' filterName = filterName.replace(" ", "") moduleName = filterName[0].lower() + filterName[1:] module = importlib.import_module(".".join([pkg, moduleName])) # if filter name is 'Foo Bar', the class should be called 'FooBarFilter' className = filterName[0].upper() + filterName[1:] if not className.endswith("Filter"): className += "Filter" return getattr(module, className) def loadFilters(ufo): """Parse custom filters from the ufo's lib.plist. Return two lists, one for the filters that are applied before decomposition of composite glyphs, another for the filters that are applied after decomposition. """ preFilters, postFilters = [], [] for filterDict in ufo.lib.get(FILTERS_KEY, []): namespace = filterDict.get("namespace", "ufo2ft.filters") try: filterClass = getFilterClass(filterDict["name"], namespace) except (ImportError, AttributeError): from pprint import pformat logger.exception("Failed to load filter: %s", pformat(filterDict)) continue filterObj = filterClass( *filterDict.get("args", []), include=filterDict.get("include"), exclude=filterDict.get("exclude"), pre=filterDict.get("pre", False), **filterDict.get("kwargs", {}), ) if filterObj.pre: preFilters.append(filterObj) else: postFilters.append(filterObj) return preFilters, postFilters def isValidFilter(klass, *bases): """Return True if 'klass' is a valid filter class. A valid filter class is a class (of type 'type'), that has a '__call__' (bound method), with the signature matching the same method from the BaseFilter or BaseIFilter classes, respectively: def __call__(self, font, glyphSet=None) def __call__(self, fonts, glyphSets=None, instantiator=None, **kwargs) """ if not isclass(klass): logger.error(f"{klass!r} is not a class") return False if not callable(klass): logger.error(f"{klass!r} is not callable") return False for baseClass in bases or (BaseFilter, BaseIFilter): if ( getfullargspec(klass.__call__).args == getfullargspec(baseClass.__call__).args ): return True logger.error(f"{klass!r} '__call__' method has incorrect signature") return False def loadFilterFromString(spec): """Take a string specifying a filter class to load (either a built-in filter or one defined in an external, user-defined module), initialize it with given options and return the filter object. The string must conform to the following notation: - an optional python module, followed by '::' - a required class name; the class must have a method called 'filter' with the same signature as the BaseFilter. - an optional list of keyword-only arguments enclosed by parentheses Raises ValueError if the string doesn't conform to this specification; TypeError if imported name is not a filter class; and ImportError if the user-defined module cannot be imported. Examples: >>> loadFilterFromString("ufo2ft.filters.removeOverlaps::RemoveOverlapsFilter") """ return _loadPluginFromString(spec, "ufo2ft.filters", isValidFilter) ufo2ft-3.3.1/Lib/ufo2ft/filters/__main__.py000066400000000000000000000030351470175262700204140ustar00rootroot00000000000000import argparse import logging from fontTools.misc.cliTools import makeOutputFileName from ufo2ft.filters import loadFilterFromString, logger try: import ufoLib2 loader = ufoLib2.Font.open except ImportError: import defcon loader = defcon.Font logging.basicConfig(level=logging.INFO) parser = argparse.ArgumentParser(description="Filter a UFO file") parser.add_argument("--output", "-o", metavar="OUTPUT", help="output file name") include_group = parser.add_mutually_exclusive_group(required=False) include_group.add_argument( "--include", metavar="GLYPHS", help="comma-separated list of glyphs to filter" ) include_group.add_argument( "--exclude", metavar="GLYPHS", help="comma-separated list of glyphs to not filter" ) parser.add_argument("ufo", metavar="UFO", help="UFO file") parser.add_argument("filters", metavar="FILTER", nargs="+", help="filter name") args = parser.parse_args() if not args.output: args.output = makeOutputFileName(args.ufo) ufo = loader(args.ufo) if args.include: include_set = set(args.include.split(",")) def include(g): return g.name in include_set elif args.exclude: exclude_set = set(args.exclude.split(",")) def include(g): return g.name not in exclude_set else: include = None for filtername in args.filters: f = loadFilterFromString(filtername) if include is not None: f.include = include f(ufo) logger.info("Written on %s" % args.output) try: ufo.save(args.output, overwrite=True) except TypeError: ufo.save(args.output) ufo2ft-3.3.1/Lib/ufo2ft/filters/base.py000066400000000000000000000450201470175262700176060ustar00rootroot00000000000000from __future__ import annotations import logging import sys from types import SimpleNamespace from typing import TYPE_CHECKING, FrozenSet, Tuple from fontTools.misc.loggingTools import Timer from ufo2ft.util import ( _getNewGlyphFactory, _GlyphSet, _LazyFontName, getMaxComponentDepth, zip_strict, ) if TYPE_CHECKING: from typing import Any, TypeAlias from ufoLib2.objects import Font, Glyph from ufo2ft.instantiator import Instantiator, InterpolatedLayer # reuse the "ufo2ft.filters" logger logger = logging.getLogger("ufo2ft.filters") # library-level logger specialized for timing info which apps like fontmake # can selectively configure timing_logger = logging.getLogger("ufo2ft.timer") class BaseFilter: # tuple of strings listing the names of required positional arguments # which will be set as attributes of the filter instance _args = () # dictionary containing the names of optional keyword arguments and # their default values, which will be set as instance attributes _kwargs = {} # pre-filter when True, post-filter when False, meaning before or after default # filters _pre = False def __init__(self, *args, **kwargs): self.options = options = SimpleNamespace() num_required = len(self._args) num_args = len(args) # process positional arguments as keyword arguments if num_args < num_required: args = ( *args, *(kwargs.pop(a) for a in self._args[num_args:] if a in kwargs), ) num_args = len(args) duplicated_args = [k for k in self._args if k in kwargs] if duplicated_args: num_duplicated = len(duplicated_args) raise TypeError( "got {} duplicated positional argument{}: {}".format( num_duplicated, "s" if num_duplicated > 1 else "", ", ".join(duplicated_args), ) ) # process positional arguments if num_args < num_required: missing = [repr(a) for a in self._args[num_args:]] num_missing = len(missing) raise TypeError( "missing {} required positional argument{}: {}".format( num_missing, "s" if num_missing > 1 else "", ", ".join(missing) ) ) elif num_args > num_required: extra = [repr(a) for a in args[num_required:]] num_extra = len(extra) raise TypeError( "got {} unsupported positional argument{}: {}".format( num_extra, "s" if num_extra > 1 else "", ", ".join(extra) ) ) for key, value in zip(self._args, args): setattr(options, key, value) # process optional keyword arguments for key, default in self._kwargs.items(): setattr(options, key, kwargs.pop(key, default)) # process special pre argument self.pre = kwargs.pop("pre", self._pre) # process special include/exclude arguments include = kwargs.pop("include", None) exclude = kwargs.pop("exclude", None) if include is not None and exclude is not None: raise ValueError("'include' and 'exclude' arguments are mutually exclusive") if callable(include): # 'include' can be a function (e.g. lambda) that takes a # glyph object and returns True/False based on some test self.include = include self._include_repr = lambda: repr(include) elif include is not None: # or it can be a list of glyph names to be included included = set(include) self.include = lambda g: g.name in included self._include_repr = lambda: repr(include) elif exclude is not None: # alternatively one can provide a list of names to not include excluded = set(exclude) self.include = lambda g: g.name not in excluded self._exclude_repr = lambda: repr(exclude) else: # by default, all glyphs are included self.include = lambda g: True # raise if any unsupported keyword arguments if kwargs: num_left = len(kwargs) raise TypeError( "got {}unsupported keyword argument{}: {}".format( "an " if num_left == 1 else "", "s" if len(kwargs) > 1 else "", ", ".join(f"'{k}'" for k in kwargs), ) ) # run the filter's custom initialization code self.start() def __repr__(self): items = [] if self._args: items.append( ", ".join(repr(getattr(self.options, arg)) for arg in self._args) ) if self._kwargs: items.append( ", ".join( "{}={!r}".format(k, getattr(self.options, k)) for k in sorted(self._kwargs) ) ) if hasattr(self, "_include_repr"): items.append(f"include={self._include_repr()}") elif hasattr(self, "_exclude_repr"): items.append(f"exclude={self._exclude_repr()}") return "{}({})".format(type(self).__name__, ", ".join(items)) def start(self): """Subclasses can perform here custom initialization code.""" pass def set_context(self, font, glyphSet): """Populate a `self.context` namespace, which is reset before each new filter call. Subclasses can override this to provide contextual information which depends on other data in the font that is not available in the glyphs objects currently being filtered, or set any other temporary attributes. The default implementation simply sets the current font and glyphSet, and initializes an empty set that keeps track of the names of the glyphs that were modified. Returns the namespace instance. """ self.context = SimpleNamespace(font=font, glyphSet=glyphSet) self.context.modified = set() proto = font.layers.defaultLayer.instantiateGlyphObject() self.context.glyphFactory = _getNewGlyphFactory(proto) return self.context def filter(self, glyph): """This is where the filter is applied to a single glyph. Subclasses must override this method, and return True when the glyph was modified. """ raise NotImplementedError @property def name(self): return self.__class__.__name__ def __call__(self, font, glyphSet=None): """Run this filter on all the included glyphs. Return the set of glyph names that were modified, if any. If `glyphSet` (dict) argument is provided, run the filter on the glyphs contained therein (which may be copies). Otherwise, run the filter in-place on the font's default glyph set. """ fontName = _LazyFontName(font) if glyphSet is not None and getattr(glyphSet, "name", None): logger.info("Running %s on %s-%s", self.name, fontName, glyphSet.name) else: logger.info("Running %s on %s", self.name, fontName) if glyphSet is None: glyphSet = _GlyphSet.from_layer(font) context = self.set_context(font, glyphSet) filter_ = self.filter include = self.include modified = context.modified # process composite glyphs in decreasing component depth order (i.e. composites # with more deeply nested components before shallower ones) to avoid # order-dependent interferences while filtering glyphs with nested components # https://github.com/googlefonts/ufo2ft/issues/621 orderedGlyphs = sorted( glyphSet.keys(), key=lambda g: -getMaxComponentDepth(glyphSet[g], glyphSet) ) with Timer() as t: for glyphName in orderedGlyphs: if glyphName in modified: continue glyph = glyphSet[glyphName] if include(glyph) and filter_(glyph): modified.add(glyphName) num = len(modified) if num > 0: timing_logger.debug( "Took %.3fs to run %s on %d glyph%s", t, self.name, len(modified), "" if num == 1 else "s", ) return modified @classmethod def getInterpolatableFilterClass(cls) -> BaseIFilter | None: """Return interpolatable filter class if one is found in the same module. We search for a class with the same name and the 'IFilter' suffix (where the 'I' stands for "interpolatable"). Subclasses can override this if they wish to use a different class name or module. """ module = sys.modules[cls.__module__] filter_name = cls.__name__ if filter_name.endswith("Filter"): filter_name = filter_name[:-6] ifilter_name = f"{filter_name}IFilter" return getattr(module, ifilter_name, None) HashableLocation: TypeAlias = FrozenSet[Tuple[str, float]] class BaseIFilter(BaseFilter): """Interpolatable variant that zips through mutliple glyphs at a time.""" def set_context( self, fonts: list[Font], glyphSets: list[dict[str, Glyph]], instantiator: Instantiator | None = None, **kwargs: dict[str, Any], ) -> SimpleNamespace: """Populate a `self.context` namespace, which is reset before each new filter call. Subclasses can override this to provide contextual information which depends on other data in the fonts that is not available in the glyphs objects currently being filtered, or set any other temporary attributes. The default implementation simply sets the current fonts, glyphSets, and optional instantiator and initializes an empty set that keeps track of the names of the glyphs that were modified. Any extra keyword arguments are passed to the context namespace. Returns the namespace instance. """ assert len(fonts) == len(glyphSets) self.context = SimpleNamespace( fonts=fonts, glyphSets=glyphSets, instantiator=instantiator, **kwargs, ) self.context.modified = set() # this is used to memoize locationsFromComponentGlyphs method below, to avoid # redoing the same work over and over again (especially when font has loads of # masters and many nested components). self.context.componentLocations = {} proto = fonts[0].layers.defaultLayer.instantiateGlyphObject() self.context.glyphFactory = _getNewGlyphFactory(proto) return self.context def filter(self, glyphName: str, glyphs: list) -> bool: """This is where the filter is applied to a set of interpolatable glyphs. Subclasses must override this method, and return True when the glyph was modified. """ raise NotImplementedError def __call__( self, fonts: list[Font], glyphSets: list[dict[str, Glyph]] | None = None, instantiator: Instantiator | None = None, **kwargs: dict[str, Any], ) -> set[str]: """Run this filter on all the included glyphs from the given glyphSets. Return the set of glyph names that were modified, if any. If `glyphSets` (list[dict]) argument is provided, run the filter on the glyphs contained therein (which may be copies). Otherwise, run the filter in-place on the fonts' default glyph sets. The `instantiator` optional argument allows interpolatable filters to generate glyph instances on demand at any location in the designspace. Any extra keyword arguments are passed on to the `set_context` method. """ logger.info("Running interpolatable %s", self.name) if glyphSets is None: glyphSets = [_GlyphSet.from_layer(font) for font in fonts] context = self.set_context(fonts, glyphSets, instantiator, **kwargs) filter_ = self.filter include = self.include modified = context.modified # process composite glyphs in decreasing component depth order (i.e. composites # with more deeply nested components before shallower ones) to avoid # order-dependent interferences while filtering glyphs with nested components # https://github.com/googlefonts/ufo2ft/issues/621 allGlyphNames = set.union(*(set(glyphSet.keys()) for glyphSet in glyphSets)) def comp_depth(g): for glyphSet in glyphSets: if g in glyphSet: return -getMaxComponentDepth(glyphSet[g], glyphSet) raise AssertionError orderedGlyphs = sorted(allGlyphNames, key=comp_depth) with Timer() as t: for glyphName in orderedGlyphs: if glyphName in modified: continue glyphs = [ glyphSet[glyphName] for glyphSet in glyphSets if glyphName in glyphSet ] if any(include(g) for g in glyphs) and filter_(glyphName, glyphs): modified.add(glyphName) num = len(modified) if num > 0: timing_logger.debug( "Took %.3fs to run %s on %d glyph%s", t, self.name, len(modified), "" if num == 1 else "s", ) return modified @classmethod def getInterpolatableFilterClass(cls) -> "BaseIFilter" | None: """Return the same class as self.""" return cls # no-op def getDefaultFont(self) -> Font: if self.context.instantiator is not None: return self.context.fonts[self.context.instantiator.default_source_idx] else: # as good a guess as any... return self.context.fonts[0] def getDefaultGlyphSet(self) -> dict[str, Glyph]: """Return the current glyphSet corresponding to the default location.""" if self.context.instantiator is not None: default_idx = self.context.instantiator.default_source_idx for i, glyphSet in enumerate(self.context.glyphSets): if i == default_idx: return glyphSet else: raise AssertionError("No default source?!") else: # we don't have enough info to know which glyphSet corresponds # to the default source location so we just guess it's going to # be the larger one given it contains all glyphs by definition. return max(self.context.glyphSets, key=lambda glyphSet: len(glyphSet)) def getInterpolatedLayers(self) -> list[InterpolatedLayer] | list[None]: """Return InterpolatedLayers at source locations or Nones if no Instantiator.""" if self.context.instantiator is not None: return self.context.instantiator.interpolated_layers else: return [None] * len(self.context.glyphSets) @staticmethod def hashableLocation(location: dict[str, float]) -> HashableLocation: """Convert (normalized) location dict to a hashable set of tuples.""" return frozenset((k, v) for k, v in location.items() if v != 0.0) def glyphSourceLocations(self, glyphName) -> set[HashableLocation]: """Return locations of all the sources that have a glyph.""" assert self.context.instantiator is not None return { self.hashableLocation(location) for glyphSet, location in zip_strict( self.context.glyphSets, self.context.instantiator.source_locations ) if glyphName in glyphSet } def locationsFromComponentGlyphs( self, glyphName: str, include: set[str] | None = None, ) -> set[HashableLocation]: """Return locations from all the components' base glyphs, recursively.""" logger.debug("Gathering all locations from component glyphs: %s", glyphName) assert self.context.instantiator is not None locations = set() cache = self.context.componentLocations for glyphSet in self.context.glyphSets: if glyphName in glyphSet: glyph = glyphSet[glyphName] for component in glyph.components: baseGlyph = component.baseGlyph if include is None or baseGlyph in include: locations |= self.glyphSourceLocations(baseGlyph) # using ternary operator instead of cache.setdefault because # the latter always evaluates the second argument, whereas # I want it to be lazy to avoid recursing too often. locations |= ( cache[baseGlyph] if baseGlyph in cache else cache.setdefault( baseGlyph, self.locationsFromComponentGlyphs(baseGlyph, include), ) ) return locations def ensureCompositeDefinedAtComponentLocations( self, glyphName: str, include: set[str] | None = None, ): """Ensure the composite glyph is defined at all its components' locations. The Instantiator is used to interpolate the glyph at the missing locations. If we have no Instantiator, we can't interpolate so this does nothing. """ if self.context.instantiator is None: return haveLocations = self.glyphSourceLocations(glyphName) needLocations = self.locationsFromComponentGlyphs(glyphName, include) locationsToAdd = needLocations - haveLocations if locationsToAdd: for glyphSet, interpolatedLayer in zip_strict( self.context.glyphSets, self.context.instantiator.interpolated_layers ): if self.hashableLocation(interpolatedLayer.location) in locationsToAdd: assert glyphName not in glyphSet logger.debug( "Interpolating composite glyph %r at %s", glyphName, interpolatedLayer.location, ) glyphSet[glyphName] = interpolatedLayer[glyphName] ufo2ft-3.3.1/Lib/ufo2ft/filters/cubicToQuadratic.py000066400000000000000000000046251470175262700221300ustar00rootroot00000000000000import logging from fontTools.cu2qu.ufo import CURVE_TYPE_LIB_KEY, DEFAULT_MAX_ERR from fontTools.pens.cu2quPen import Cu2QuPointPen from ufo2ft.filters import BaseFilter from ufo2ft.fontInfoData import getAttrWithFallback logger = logging.getLogger(__name__) class CubicToQuadraticFilter(BaseFilter): _kwargs = { "conversionError": None, "reverseDirection": True, "rememberCurveType": False, "allQuadratic": True, } def set_context(self, font, glyphSet): ctx = super().set_context(font, glyphSet) relativeError = self.options.conversionError or DEFAULT_MAX_ERR ctx.absoluteError = relativeError * getAttrWithFallback(font.info, "unitsPerEm") ctx.stats = {} return ctx def __call__(self, font, glyphSet=None): if self.options.rememberCurveType: # check first in the global font lib, then in layer lib for lib in (font.lib, getattr(glyphSet, "lib", {})): curve_type = lib.get(CURVE_TYPE_LIB_KEY, "cubic") if curve_type == "quadratic": logger.info("Curves already converted to quadratic") return set() elif curve_type == "cubic": pass # keep converting else: raise NotImplementedError(curve_type) modified = super().__call__(font, glyphSet) if modified: stats = self.context.stats logger.info( "New spline lengths: %s" % (", ".join("%s: %d" % (ln, stats[ln]) for ln in sorted(stats.keys()))) ) if self.options.rememberCurveType: # 'lib' here is the layer's lib, as defined in for loop variable curve_type = lib.get(CURVE_TYPE_LIB_KEY, "cubic") if curve_type != "quadratic": lib[CURVE_TYPE_LIB_KEY] = "quadratic" return modified def filter(self, glyph): if not len(glyph): return False pen = Cu2QuPointPen( glyph.getPointPen(), self.context.absoluteError, reverse_direction=self.options.reverseDirection, stats=self.context.stats, all_quadratic=self.options.allQuadratic, ) contours = list(glyph) glyph.clearContours() for contour in contours: contour.drawPoints(pen) return True ufo2ft-3.3.1/Lib/ufo2ft/filters/decomposeComponents.py000066400000000000000000000024721470175262700227240ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from ufo2ft.filters import BaseFilter, BaseIFilter from ufo2ft.util import decomposeCompositeGlyph, zip_strict if TYPE_CHECKING: from ufoLib2.objects import Glyph class DecomposeComponentsFilter(BaseFilter): # pre=True so by default this is run before the RemoveOverlaps filter, # in case a component overlaps other contours or components, to ensure # the decomposed contours will be merged correctly: # https://github.com/googlefonts/gftools/pull/425 _pre = True def filter(self, glyph: Glyph) -> bool: if not glyph.components: return False decomposeCompositeGlyph(glyph, self.context.glyphSet) return True class DecomposeComponentsIFilter(BaseIFilter): _pre = True def filter(self, glyphName: str, glyphs: list[Glyph]) -> bool: if not any(glyph.components for glyph in glyphs): return False self.ensureCompositeDefinedAtComponentLocations(glyphName) for glyphSet, interpolatedLayer in zip_strict( self.context.glyphSets, self.getInterpolatedLayers() ): glyph = glyphSet.get(glyphName) if glyph is not None: decomposeCompositeGlyph(glyph, interpolatedLayer or glyphSet) return True ufo2ft-3.3.1/Lib/ufo2ft/filters/decomposeTransformedComponents.py000066400000000000000000000016371470175262700251330ustar00rootroot00000000000000from fontTools.misc.transform import Identity from ufo2ft.filters.decomposeComponents import ( DecomposeComponentsFilter, DecomposeComponentsIFilter, ) IDENTITY_2x2 = Identity[:4] def _isTransformed(component): return component.transformation[:4] != IDENTITY_2x2 class DecomposeTransformedComponentsFilter(DecomposeComponentsFilter): def filter(self, glyph): if any(_isTransformed(c) for c in glyph.components): return super().filter(glyph) return False class DecomposeTransformedComponentsIFilter(DecomposeComponentsIFilter): def filter(self, glyphName, glyphs): # We decomposes the glyph in *all* masters if any contains a transformed # component: https://github.com/googlefonts/ufo2ft/issues/507 if not any(any(_isTransformed(c) for c in g.components) for g in glyphs): return False return super().filter(glyphName, glyphs) ufo2ft-3.3.1/Lib/ufo2ft/filters/dottedCircle.py000066400000000000000000000234731470175262700213110ustar00rootroot00000000000000""" Dotted Circle Filter This filter checks whether a font contains a glyph for U+25CC (DOTTED CIRCLE), which is inserted by complex shapers to display mark glyphs which have no associated base glyph, usually as a result of broken clusters but also for pedagogical reasons. (For example, to display the marks in a table of glyphs.) If no dotted circle glyph is present in the font, then one is drawn and added. Next, the filter creates any additional anchors for the dotted circle glyph to ensure that all marks can be attached to it. It does this by gathering a list of anchors, finding the set of base glyphs for each anchor, computing the average position of the anchor on the base glyph (relative to the glyph's width), and then creating an anchor at that average position on the dotted circle glyph. The filter must be run as a "pre" filter. This can be done from the command line like so:: fontmake -o ttf -g MyFont.glyphs --filter "DottedCircleFilter(pre=True)" or in the ``lib.plist`` file of a UFO:: com.github.googlei18n.ufo2ft.filters name dottedCircle pre The filter supports the following options: margin When drawing a dotted circle, the vertical space in units around the dotted circle. sidebearings When drawing a dotted circle, additional horizontal space in units around the dotted circle. dots Number of dots in the circle. """ import logging import math from statistics import mean from fontTools.misc.fixedTools import otRound from ufo2ft.constants import OPENTYPE_CATEGORIES_KEY from ufo2ft.featureCompiler import parseLayoutFeatures from ufo2ft.featureWriters import ast from ufo2ft.filters import BaseFilter from ufo2ft.fontInfoData import getAttrWithFallback from ufo2ft.util import _GlyphSet, _LazyFontName, _setGlyphMargin logger = logging.getLogger(__name__) DO_NOTHING = -1 # Sentinel value (not a valid glyph name) # Length of cubic Bezier handle used when drawing quarter circles. # See https://pomax.github.io/bezierinfo/#circles_cubic CIRCULAR_SUPERNESS = 0.551784777779014 def circle(pen, origin, radius): w = (origin[0] - radius, origin[1]) n = (origin[0], origin[1] + radius) e = (origin[0] + radius, origin[1]) s = (origin[0], origin[1] - radius) pen.moveTo(w) pen.curveTo( (w[0], w[1] + radius * CIRCULAR_SUPERNESS), (n[0] - radius * CIRCULAR_SUPERNESS, n[1]), n, ) pen.curveTo( (n[0] + radius * CIRCULAR_SUPERNESS, n[1]), (e[0], e[1] + radius * CIRCULAR_SUPERNESS), e, ) pen.curveTo( (e[0], e[1] - radius * CIRCULAR_SUPERNESS), (s[0] + radius * CIRCULAR_SUPERNESS, s[1]), s, ) pen.curveTo( (s[0] - radius * CIRCULAR_SUPERNESS, s[1]), (w[0], w[1] - radius * CIRCULAR_SUPERNESS), w, ) pen.closePath() class DottedCircleFilter(BaseFilter): _kwargs = {"margin": 80, "sidebearing": 160, "dots": 12} def __call__(self, font, glyphSet=None): fontName = _LazyFontName(font) if glyphSet is not None and getattr(glyphSet, "name", None): logger.info("Running %s on %s-%s", self.name, fontName, glyphSet.name) else: logger.info("Running %s on %s", self.name, fontName) if glyphSet is None: glyphSet = _GlyphSet.from_layer(font) self.set_context(font, glyphSet) added_glyph = False dotted_circle_glyph = self.check_dotted_circle() if dotted_circle_glyph == DO_NOTHING: return set() if not dotted_circle_glyph: dotted_circle_glyph = self.draw_dotted_circle(glyphSet) added_glyph = True added_anchors = self.check_and_add_anchors(dotted_circle_glyph) if added_anchors: self.ensure_base(dotted_circle_glyph) if added_glyph or added_anchors: return {dotted_circle_glyph.name} else: return set() def check_dotted_circle(self): """Check for the presence of a dotted circle glyph and return it""" font = self.context.font glyphset = self.context.glyphSet dotted_circle = next((g.name for g in font if 0x25CC in g.unicodes), None) if dotted_circle: if dotted_circle not in glyphset: logger.debug( "Found dotted circle glyph %s in font but not in glyphset", dotted_circle, ) return DO_NOTHING logger.debug("Found dotted circle glyph %s", dotted_circle) return glyphset[dotted_circle] def draw_dotted_circle(self, glyphSet): """Add a new dotted circle glyph, drawing its outlines""" font = self.context.font logger.debug("Adding dotted circle glyph") glyph = self.context.glyphFactory(name="uni25CC", unicodes=[0x25CC]) pen = glyph.getPen() xHeight = getAttrWithFallback(font.info, "xHeight") bigradius = (xHeight - 2 * self.options.margin) / 2 littleradius = bigradius / 6 left = self.options.sidebearing + littleradius right = self.options.sidebearing + bigradius * 2 - littleradius middleY = xHeight / 2 middleX = (left + right) / 2 subangle = 2 * math.pi / self.options.dots for t in range(self.options.dots): angle = t * subangle cx = middleX + bigradius * math.cos(angle) cy = middleY + bigradius * math.sin(angle) circle(pen, (cx, cy), littleradius) _setGlyphMargin(glyph, "right", self.options.sidebearing) glyphSet["uni25CC"] = glyph return glyph def check_and_add_anchors(self, dotted_circle_glyph): """Check that all mark-attached anchors are present on the dotted circle glyph, synthesizing a position for any missing anchors.""" font = self.context.font # First we will gather information about all the anchors in the # font at present; for the anchors on marks (starting with "_") # we just want to know their names, so we can match them with # bases later. For the anchors on bases, we also want to store # the position of the anchor so we can average them. all_anchors = {} any_added = False for glyph in font: width = None try: bounds = glyph.getBounds(font) if bounds: width = bounds.xMax - bounds.xMin except AttributeError: bounds = glyph.bounds if bounds: width = bounds[2] - bounds[0] if width is None: width = glyph.width for anchor in glyph.anchors: if anchor.name.startswith("_"): all_anchors[anchor.name] = [] continue if not width: continue x_percentage = anchor.x / width all_anchors.setdefault(anchor.name, []).append((x_percentage, anchor.y)) # Now we move to the dotted circle. What anchors do we have already? dsanchors = set([a.name for a in dotted_circle_glyph.anchors]) for anchor, positions in all_anchors.items(): # Skip existing anchors on the dotted-circle, and any anchors # which don't have a matching mark glyph (mark-to-lig etc.). if anchor in dsanchors or f"_{anchor}" not in all_anchors: continue # And now we're creating a new one anchor_x = dotted_circle_glyph.width * mean([v[0] for v in positions]) anchor_y = mean([v[1] for v in positions]) logger.debug( "Adding anchor %s to dotted circle glyph at %i,%i", anchor, anchor_x, anchor_y, ) dotted_circle_glyph.appendAnchor( {"x": otRound(anchor_x), "y": otRound(anchor_y), "name": anchor} ) any_added = True return any_added # We have added some anchors to the dotted circle glyph. Now we need to # ensure the glyph is a base (and specifically a base glyph, not just # unclassified), or else it won't be in the list of base glyphs when # we come to the mark features writer, and all our work will be for nothing. # Also note that if we had a dotted circle glyph in the font already and # we have come from Glyphs, glyphsLib would only consider the glyph to # be a base if it has anchors, and it might not have had any when glyphsLib # wrote the GDEF table. # So we have to go digging around for a GDEF table and modify it. def ensure_base(self, dotted_circle_glyph): dotted_circle = dotted_circle_glyph.name font = self.context.font feaFile = parseLayoutFeatures(font) if ast.findTable(feaFile, "GDEF") is None: # We have no GDEF table. GDEFFeatureWriter will create one # using the font's lib. if OPENTYPE_CATEGORIES_KEY in font.lib: font.lib[OPENTYPE_CATEGORIES_KEY][dotted_circle] = "base" return # We have GDEF table, so we need to find the GlyphClassDef, and add # ourselves to the baseGlyphs set. for st in feaFile.statements: if isinstance(st, ast.TableBlock) and st.name == "GDEF": for st2 in st.statements: if isinstance(st2, ast.GlyphClassDefStatement): if ( st2.baseGlyphs and dotted_circle not in st2.baseGlyphs.glyphSet() ): st2.baseGlyphs.glyphs.append(dotted_circle) # And then put the modified feature file back into the font font.features.text = feaFile.asFea() ufo2ft-3.3.1/Lib/ufo2ft/filters/dottedCircleFilter.py000066400000000000000000000004401470175262700224440ustar00rootroot00000000000000"""This is a redirection stub, because the original module was misnamed.""" import warnings from .dottedCircle import DottedCircleFilter # noqa warnings.warn( "The dottedCircleFilter module is deprecated, please import dottedCircle instead.", UserWarning, stacklevel=1, ) ufo2ft-3.3.1/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py000066400000000000000000000071101470175262700235150ustar00rootroot00000000000000from ufo2ft.constants import COLOR_LAYER_MAPPING_KEY, COLOR_LAYERS_KEY from ufo2ft.filters import BaseFilter from ufo2ft.util import _GlyphSet class ExplodeColorLayerGlyphsFilter(BaseFilter): """This filter doesn't really filter glyphs, but copies glyphs from UFO layers to alternate glyphs in the default layer, for use in the COLR table. """ def set_context(self, font, glyphSet): context = super().set_context(font, glyphSet) context.globalColorLayerMapping = font.lib.get(COLOR_LAYER_MAPPING_KEY) context.layerGlyphSets = {} context.colorLayerGlyphNames = set() # glyph names that we added if COLOR_LAYERS_KEY not in font.lib: font.lib[COLOR_LAYERS_KEY] = {} else: # if the font already contains an explicit COLOR_LAYERS_KEY, we # assume the color layers have already been 'exploded' once. context.skipCurrentFont = True return context def _getLayer(self, font, layerName): layer = self.context.layerGlyphSets.get(layerName) if layer is None: layer = _GlyphSet.from_layer(font, layerName) self.context.layerGlyphSets[layerName] = layer return layer def _copyGlyph(self, layerGlyphSet, glyphSet, glyphName, layerName): layerGlyph = layerGlyphSet[glyphName] layerGlyphName = f"{glyphName}.{layerName}" if layerGlyphName in glyphSet: if layerGlyphName in self.context.colorLayerGlyphNames: # We've added this glyph already, so we're done return layerGlyphName from ufo2ft.errors import InvalidFontData raise InvalidFontData( f"a glyph named {layerGlyphName} already exists, " "conflicting with a requested color layer glyph." ) for component in layerGlyph.components: baseLayerGlyphName = self._copyGlyph( layerGlyphSet, glyphSet, component.baseGlyph, layerName ) component.baseGlyph = baseLayerGlyphName # The new alternates must have their codepoints stripped lest they # become encoded, which they will be if placed in the default layer. # See: https://github.com/googlefonts/ufo2ft/pull/739#issuecomment-1516075892 layerGlyph.unicodes = [] glyphSet[layerGlyphName] = layerGlyph self.context.colorLayerGlyphNames.add(layerGlyphName) return layerGlyphName def filter(self, glyph): if getattr(self.context, "skipCurrentFont", False): return False font = self.context.font glyphSet = self.context.glyphSet colorLayers = font.lib[COLOR_LAYERS_KEY] colorLayerMapping = glyph.lib.get(COLOR_LAYER_MAPPING_KEY) if colorLayerMapping is None: colorLayerMapping = self.context.globalColorLayerMapping if colorLayerMapping is None: # No color layer info for this glyph return False layers = [] for layerName, colorID in colorLayerMapping: layerGlyphSet = self._getLayer(font, layerName) if glyph.name in layerGlyphSet: if glyph == layerGlyphSet[glyph.name]: layerGlyphName = glyph.name else: layerGlyphName = self._copyGlyph( layerGlyphSet, glyphSet, glyph.name, layerName ) layers.append((layerGlyphName, colorID)) if layers: colorLayers[glyph.name] = layers return True else: return False ufo2ft-3.3.1/Lib/ufo2ft/filters/flattenComponents.py000066400000000000000000000071731470175262700224060ustar00rootroot00000000000000from __future__ import annotations import logging from fontTools.misc.transform import Transform from ufo2ft.filters import BaseFilter, BaseIFilter from ufo2ft.util import zip_strict logger = logging.getLogger(__name__) class FlattenComponentsFilter(BaseFilter): """Replace nested components with their referents so that max depth is 1.""" def __call__(self, font, glyphSet=None): modified = super().__call__(font, glyphSet) if modified: logger.info("Flattened composite glyphs: %i" % len(modified)) return modified def filter(self, glyph): return _flattenGlyphComponents(glyph, self.context.glyphSet) class FlattenComponentsIFilter(BaseIFilter): """Interpolatable variant of FlattenComponentsFilter.""" def __call__(self, fonts, glyphSets=None, instantiator=None, **kwargs): modified = super().__call__(fonts, glyphSets, instantiator, **kwargs) if modified: logger.info("Flattened composite glyphs: %i" % len(modified)) return modified def filter(self, glyphName: str, glyphs: list) -> bool: flattened = False if not any(glyph.components for glyph in glyphs): return flattened defaultGlyphSet = self.getDefaultGlyphSet() if not any(_haveNestedComponents(g, defaultGlyphSet) for g in glyphs): return flattened for glyphSet, interpolatedLayer in zip_strict( self.context.glyphSets, self.getInterpolatedLayers() ): glyph = glyphSet.get(glyphName) if glyph is not None: flattened = _flattenGlyphComponents( glyph, interpolatedLayer or glyphSet ) return flattened def _isSimpleOrMixed(glyph): return not glyph.components or len(glyph) > 0 def _haveNestedComponents(glyph, glyphSet): return not _isSimpleOrMixed(glyph) and any( glyphSet[compo.baseGlyph].components for compo in glyph.components if compo.baseGlyph in glyphSet ) def _flattenGlyphComponents(glyph, glyphSet): flattened = False if not glyph.components: return flattened components = list(glyph.components) glyph.clearComponents() pen = glyph.getPointPen() for comp in components: flattened_tuples = _flattenComponent(glyphSet, comp, found_in=glyph) if flattened_tuples[0] != (comp.baseGlyph, comp.transformation): flattened = True for flattened_tuple in flattened_tuples: pen.addComponent(*flattened_tuple) return flattened def _flattenComponent(glyphSet, component, found_in): """Returns a list of tuples (baseGlyph, transform) of nested component.""" if component.baseGlyph not in glyphSet: raise ValueError( f"Could not find component '{component.baseGlyph}' used in '{found_in.name}'" ) glyph = glyphSet[component.baseGlyph] # Any contour will cause components to be decomposed if _isSimpleOrMixed(glyph): transformation = Transform(*component.transformation) return [(component.baseGlyph, transformation)] all_flattened_components = [] for nested in glyph.components: flattened_components = _flattenComponent(glyphSet, nested, found_in=glyph) for i, (name, tr) in enumerate(flattened_components): flat_tr = Transform(*component.transformation) flat_tr = flat_tr.translate(tr.dx, tr.dy) flat_tr = flat_tr.transform((tr.xx, tr.xy, tr.yx, tr.yy, 0, 0)) flattened_components[i] = (name, flat_tr) all_flattened_components.extend(flattened_components) return all_flattened_components ufo2ft-3.3.1/Lib/ufo2ft/filters/propagateAnchors.py000066400000000000000000000206251470175262700222000ustar00rootroot00000000000000# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import fontTools.pens.boundsPen from fontTools.misc.transform import Transform from ufo2ft.filters import BaseFilter, BaseIFilter from ufo2ft.util import OpenTypeCategories, zip_strict logger = logging.getLogger(__name__) class PropagateAnchorsFilter(BaseFilter): def set_context(self, font, glyphSet): ctx = super().set_context(font, glyphSet) ctx.processed = set() ctx.categories = OpenTypeCategories.load(font) return ctx def __call__(self, font, glyphSet=None): modified = super().__call__(font, glyphSet) if modified: logger.info("Glyphs with propagated anchors: %i" % len(modified)) return modified def filter(self, glyph): if not glyph.components: return False before = len(glyph.anchors) _propagate_glyph_anchors( self.context.glyphSet, glyph, self.context.processed, self.context.modified, self.context.categories, ) return len(glyph.anchors) > before class PropagateAnchorsIFilter(BaseIFilter): def set_context(self, *args, **kwargs): ctx = super().set_context(*args, **kwargs) ctx.processed = [set() for _ in range(len(ctx.glyphSets))] ctx.categories = OpenTypeCategories.load(self.getDefaultFont()) return ctx def __call__(self, fonts, glyphSets=None, instantiator=None, **kwargs): modified = super().__call__(fonts, glyphSets, instantiator, **kwargs) if modified: logger.info("Glyphs with propagated anchors: %i" % len(modified)) return modified def filter(self, glyphName, glyphs): modified = False if not any(glyph.components for glyph in glyphs): return modified before = len(self.context.modified) for i, (glyphSet, interpolatedLayer) in enumerate( zip_strict(self.context.glyphSets, self.getInterpolatedLayers()) ): glyph = glyphSet.get(glyphName) if glyph is not None: _propagate_glyph_anchors( interpolatedLayer or glyphSet, glyph, self.context.processed[i], self.context.modified, self.context.categories, ) return len(self.context.modified) > before def _propagate_glyph_anchors(glyphSet, composite, processed, modified, categories): """ Propagate anchors from base glyphs to a given composite glyph, and to all composite glyphs used in between. """ if composite.name in processed: return processed.add(composite.name) if not composite.components or ( # "If it is a 'mark' and there are anchors, it will not look into components" # Georg said: https://github.com/googlefonts/ufo2ft/issues/802#issuecomment-1904109457 composite.name in categories.mark and composite.anchors ): return base_components = [] mark_components = [] anchor_names = set() to_add = {} for component in composite.components: try: glyph = glyphSet[component.baseGlyph] except KeyError: logger.warning( "Anchors not propagated for inexistent component {} " "in glyph {}".format(component.baseGlyph, composite.name) ) else: _propagate_glyph_anchors(glyphSet, glyph, processed, modified, categories) if any(a.name.startswith("_") for a in glyph.anchors): mark_components.append(component) else: base_components.append(component) anchor_names |= {a.name for a in glyph.anchors} if mark_components and not base_components and _is_ligature_mark(composite): # The composite is a mark that is composed of other marks (E.g. # "circumflexcomb_tildecomb"). Promote the mark that is positioned closest # to the origin to a base. try: component = _component_closest_to_origin(mark_components, glyphSet) except Exception as e: raise Exception( "Error while determining which component of composite " "'{}' is the lowest: {}".format(composite.name, str(e)) ) from e mark_components.remove(component) base_components.append(component) glyph = glyphSet[component.baseGlyph] anchor_names |= {a.name for a in glyph.anchors} for anchor_name in anchor_names: # don't add if composite glyph already contains this anchor OR any # associated ligature anchors (e.g. "top_1, top_2" for "top") if not any(a.name.startswith(anchor_name) for a in composite.anchors): _get_anchor_data(to_add, glyphSet, base_components, anchor_name) for component in mark_components: _adjust_anchors(to_add, glyphSet, component) # we sort propagated anchors to append in a deterministic order for name, (x, y) in sorted(to_add.items()): anchor_dict = {"name": name, "x": x, "y": y} try: composite.appendAnchor(anchor_dict) except TypeError: # pragma: no cover # fontParts API composite.appendAnchor(name, (x, y)) if to_add: modified.add(composite.name) def _get_anchor_data(anchor_data, glyphSet, components, anchor_name): """Get data for an anchor from a list of components.""" anchors = [] for component in components: for anchor in glyphSet[component.baseGlyph].anchors: if anchor.name == anchor_name: anchors.append((anchor, component)) break if len(anchors) > 1: for i, (anchor, component) in enumerate(anchors): t = Transform(*component.transformation) name = "%s_%d" % (anchor.name, i + 1) anchor_data[name] = t.transformPoint((anchor.x, anchor.y)) elif anchors: anchor, component = anchors[0] t = Transform(*component.transformation) anchor_data[anchor.name] = t.transformPoint((anchor.x, anchor.y)) def _adjust_anchors(anchor_data, glyphSet, component): """ Adjust base anchors to which a mark component may have been attached, by moving the base anchor attached to a mark anchor to the position of the mark component's base anchor. """ glyph = glyphSet[component.baseGlyph] t = Transform(*component.transformation) for anchor in glyph.anchors: # only adjust if this anchor has data and the component also contains # the associated mark anchor (e.g. "_top" for "top") if anchor.name in anchor_data and any( a.name == "_" + anchor.name for a in glyph.anchors ): anchor_data[anchor.name] = t.transformPoint((anchor.x, anchor.y)) def _component_closest_to_origin(components, glyph_set): """Return the component whose (xmin, ymin) bounds are closest to origin. This ensures that a component that is moved below another is actually recognized as such. Looking only at the transformation offset can be misleading. """ return min(components, key=lambda comp: _distance((0, 0), _bounds(comp, glyph_set))) def _distance(pos1, pos2): x1, y1 = pos1 x2, y2 = pos2 return (x1 - x2) ** 2 + (y1 - y2) ** 2 def _is_ligature_mark(glyph): return not glyph.name.startswith("_") and "_" in glyph.name def _bounds(component, glyph_set): """Return the (xmin, ymin) of the bounds of `component`.""" if hasattr(component, "bounds"): # e.g. defcon return component.bounds[:2] elif hasattr(component, "draw"): # e.g. ufoLib2 pen = fontTools.pens.boundsPen.BoundsPen(glyphSet=glyph_set) component.draw(pen) return pen.bounds[:2] else: raise ValueError( f"Don't know to to compute the bounds of component '{component}' " ) ufo2ft-3.3.1/Lib/ufo2ft/filters/removeOverlaps.py000066400000000000000000000030601470175262700217030ustar00rootroot00000000000000import logging from enum import Enum from ufo2ft.filters import BaseFilter logger = logging.getLogger(__name__) class RemoveOverlapsFilter(BaseFilter): class Backend(Enum): BOOLEAN_OPERATIONS = "booleanOperations" SKIA_PATHOPS = "pathops" # use booleanOperations by default, unless pathops specified as backend _kwargs = {"backend": Backend.BOOLEAN_OPERATIONS} def start(self): self.options.backend = self.Backend(self.options.backend) if self.options.backend is self.Backend.BOOLEAN_OPERATIONS: from booleanOperations import BooleanOperationsError, union self.union = union self.Error = BooleanOperationsError self.penGetter = "getPointPen" logger.debug("using booleanOperations as RemoveOverlapsFilter backend") elif self.options.backend is self.Backend.SKIA_PATHOPS: from pathops import PathOpsError, union self.union = union self.Error = PathOpsError self.penGetter = "getPen" logger.debug("using skia-pathops as RemoveOverlapsFilter backend") else: raise AssertionError(self.options.backend) def filter(self, glyph): if not len(glyph): return False contours = list(glyph) glyph.clearContours() pen = getattr(glyph, self.penGetter)() try: self.union(contours, pen) except self.Error: logger.error("Failed to remove overlaps for %s", glyph.name) raise return True ufo2ft-3.3.1/Lib/ufo2ft/filters/reverseContourDirection.py000066400000000000000000000006661470175262700235710ustar00rootroot00000000000000from fontTools.pens.pointPen import ReverseContourPointPen from ufo2ft.filters import BaseFilter class ReverseContourDirectionFilter(BaseFilter): def filter(self, glyph): if not len(glyph): return False pen = ReverseContourPointPen(glyph.getPointPen()) contours = list(glyph) glyph.clearContours() for contour in contours: contour.drawPoints(pen) return True ufo2ft-3.3.1/Lib/ufo2ft/filters/skipExportGlyphs.py000066400000000000000000000074561470175262700222460ustar00rootroot00000000000000from __future__ import annotations from ufo2ft.filters import BaseFilter, BaseIFilter from ufo2ft.util import decomposeCompositeGlyph, zip_strict class SkipExportGlyphsFilter(BaseFilter): """Subset a glyphSet while decomposing references to pruned component glyphs.""" _pre = True _args = ("skipExportGlyphs",) def start(self): self.options.skipExportGlyphs = frozenset(self.options.skipExportGlyphs) def filter(self, glyph) -> bool: if not glyph.components or self.options.skipExportGlyphs.isdisjoint( comp.baseGlyph for comp in glyph.components ): return False # decomposeNested=False because at this stage we are only interested # in pruning component references to specific non-export glyphs, not # decomposing entire composite glyphs per se; it's conceivable that # after a component is replaced by its direct referent and the latter # in turn only comprises components, the parent can remain a composite # glyph and need not be fully decomposed to contours; any further # decompositions (e.g. of mixed glyphs) can be performed later. decomposeCompositeGlyph( glyph, self.context.glyphSet, decomposeNested=False, include=self.options.skipExportGlyphs, ) return True def __call__(self, font, glyphSet=None): if not self.options.skipExportGlyphs: return self.context.modified # nothing to do modified = super().__call__(font, glyphSet) # now that all component references to non-export glyphs have been removed, # the glyphSet can be subset in-place glyphSet = self.context.glyphSet for glyphName in self.options.skipExportGlyphs: if glyphName in glyphSet: del glyphSet[glyphName] # technically this glyph was 'removed' rather than 'modified' but # filters only return one set... modified.add(glyphName) return modified class SkipExportGlyphsIFilter(BaseIFilter): """Interpolatable variant of SkipExportGlyphsFilter.""" _pre = True _args = ("skipExportGlyphs",) def start(self): self.options.skipExportGlyphs = frozenset(self.options.skipExportGlyphs) def filter(self, glyphName: str, glyphs: list) -> bool: if not any(glyph.components for glyph in glyphs) or all( self.options.skipExportGlyphs.isdisjoint( comp.baseGlyph for comp in glyph.components ) for glyph in glyphs ): return False self.ensureCompositeDefinedAtComponentLocations( glyphName, include=self.options.skipExportGlyphs ) for glyphSet, interpolatedLayer in zip_strict( self.context.glyphSets, self.getInterpolatedLayers() ): glyph = glyphSet.get(glyphName) if glyph is not None: decomposeCompositeGlyph( glyph, interpolatedLayer or glyphSet, decomposeNested=False, include=self.options.skipExportGlyphs, ) return True def __call__(self, fonts, glyphSets=None, instantiator=None, **kwargs): if not self.options.skipExportGlyphs: return self.context.modified # nothing to do modified = super().__call__( fonts, glyphSets, instantiator=instantiator, **kwargs ) for glyphName in self.options.skipExportGlyphs: for glyphSet in self.context.glyphSets: if glyphName in glyphSet: del glyphSet[glyphName] # mark removed glyphs among the 'modified' ones modified.add(glyphName) return modified ufo2ft-3.3.1/Lib/ufo2ft/filters/sortContours.py000066400000000000000000000030261470175262700214200ustar00rootroot00000000000000import logging import fontTools.pens.boundsPen from ufo2ft.filters import BaseFilter logger = logging.getLogger(__name__) class SortContoursFilter(BaseFilter): """Sort contours by their bounding box. ATTENTION: This filter should be run after decomposition! Mixed contours and components cannot meaningfully be sorted. This is to work around the undefined contour order in pyclipper, see https://sourceforge.net/p/polyclipping/bugs/195/. It only strikes on glyphs that contain a lot of contours on the same height (think word marks or glyphs like U+FFFC OBJECT REPLACEMENT CHARACTER, U+034F COMBINING GRAPHEME JOINER or U+2591 LIGHT SHADE). """ def filter(self, glyph): if len(glyph) == 0: # As in, no contours. return False if glyph.components: logger.warning( "Glyph '%s' contains components which will not be sorted.", glyph.name, ) contours = sorted( (c for c in glyph), key=lambda contour: _control_bounding_box(contour) ) glyph.clearContours() if hasattr(glyph, "appendContour"): # defcon for contour in contours: glyph.appendContour(contour) else: # ufoLib2 glyph.contours.extend(contours) return True def _control_bounding_box(contour): pen = fontTools.pens.boundsPen.ControlBoundsPen(None) p2s_pen = fontTools.pens.pointPen.PointToSegmentPen(pen) contour.drawPoints(p2s_pen) return pen.bounds ufo2ft-3.3.1/Lib/ufo2ft/filters/transformations.py000066400000000000000000000111361470175262700221260ustar00rootroot00000000000000import logging import math from enum import IntEnum from fontTools.misc.fixedTools import otRound from fontTools.misc.transform import Identity, Transform from fontTools.pens.recordingPen import RecordingPointPen from fontTools.pens.transformPen import TransformPointPen as _TransformPointPen from ufo2ft.filters import BaseFilter from ufo2ft.fontInfoData import getAttrWithFallback log = logging.getLogger(__name__) class TransformPointPen(_TransformPointPen): def __init__(self, outPointPen, transformation, modified=None): super().__init__(outPointPen, transformation) self.modified = modified if modified is not None else set() self._inverted = self._transformation.inverse() def addComponent(self, baseGlyph, transformation, identifier=None, **kwargs): if baseGlyph in self.modified: # multiply the component's transformation matrix with the inverse # of the filter's transformation matrix to compensate for the # transformation already applied to the base glyph transformation = Transform(*transformation).transform(self._inverted) super().addComponent(baseGlyph, transformation, identifier=identifier, **kwargs) class TransformationsFilter(BaseFilter): class Origin(IntEnum): CAP_HEIGHT = 0 HALF_CAP_HEIGHT = 1 X_HEIGHT = 2 HALF_X_HEIGHT = 3 BASELINE = 4 _kwargs = { "OffsetX": 0, "OffsetY": 0, "ScaleX": 100, "ScaleY": 100, "Slant": 0, "Origin": 4, # BASELINE } def start(self): self.options.Origin = self.Origin(self.options.Origin) def get_origin_height(self, font, origin): if origin is self.Origin.BASELINE: return 0 elif origin is self.Origin.CAP_HEIGHT: return getAttrWithFallback(font.info, "capHeight") elif origin is self.Origin.HALF_CAP_HEIGHT: return otRound(getAttrWithFallback(font.info, "capHeight") / 2) elif origin is self.Origin.X_HEIGHT: return getAttrWithFallback(font.info, "xHeight") elif origin is self.Origin.HALF_X_HEIGHT: return otRound(getAttrWithFallback(font.info, "xHeight") / 2) else: raise AssertionError(origin) def set_context(self, font, glyphSet): ctx = super().set_context(font, glyphSet) origin_height = self.get_origin_height(font, self.options.Origin) m = Identity dx, dy = self.options.OffsetX, self.options.OffsetY if dx != 0 or dy != 0: m = m.translate(dx, dy) sx, sy = self.options.ScaleX, self.options.ScaleY angle = self.options.Slant # TODO Add support for "Cursify" option # cursify = self.options.SlantCorrection if sx != 100 or sy != 100 or angle != 0: # vertically shift glyph to the specified 'Origin' before # scaling and/or slanting, then move it back if origin_height != 0: m = m.translate(0, origin_height) if sx != 100 or sy != 100: m = m.scale(sx / 100, sy / 100) if angle != 0: m = m.skew(math.radians(angle)) if origin_height != 0: m = m.translate(0, -origin_height) ctx.matrix = m return ctx def filter(self, glyph): matrix = self.context.matrix if matrix == Identity or not (glyph or glyph.components or glyph.anchors): return False # nothing to do modified = self.context.modified glyphSet = self.context.glyphSet for component in glyph.components: base_name = component.baseGlyph if base_name in modified: continue base_glyph = glyphSet[base_name] if self.include(base_glyph) and self.filter(base_glyph): # base glyph is included but was not transformed yet; we # call filter recursively until all the included bases are # transformed, or there are no more components modified.add(base_name) rec = RecordingPointPen() glyph.drawPoints(rec) glyph.clearContours() glyph.clearComponents() outpen = glyph.getPointPen() filterpen = TransformPointPen(outpen, matrix, modified) rec.replay(filterpen) # anchors are not drawn through the pen API, # must be transformed separately for a in glyph.anchors: a.x, a.y = matrix.transformPoint((a.x, a.y)) glyph.width, glyph.height = matrix.transformVector((glyph.width, glyph.height)) return True ufo2ft-3.3.1/Lib/ufo2ft/fontInfoData.py000066400000000000000000000410451470175262700176030ustar00rootroot00000000000000""" This file provides fallback data for info attributes that are required for building OTFs. There are two main functions that are important: * :func:`~getAttrWithFallback` * :func:`~preflightInfo` There are a set of other functions that are used internally for synthesizing values for specific attributes. These can be used externally as well. """ import calendar import logging import math import os import time import unicodedata from datetime import datetime, timezone from fontTools import ufoLib from fontTools.misc.fixedTools import otRound from fontTools.misc.textTools import binary2num logger = logging.getLogger(__name__) # ----------------- # Special Fallbacks # ----------------- # generic _styleMapStyleNames = ["regular", "bold", "italic", "bold italic"] def ascenderFallback(info): upm = getAttrWithFallback(info, "unitsPerEm") return otRound(upm * 0.8) def descenderFallback(info): upm = getAttrWithFallback(info, "unitsPerEm") return -otRound(upm * 0.2) def capHeightFallback(info): upm = getAttrWithFallback(info, "unitsPerEm") return otRound(upm * 0.7) def xHeightFallback(info): upm = getAttrWithFallback(info, "unitsPerEm") return otRound(upm * 0.5) def styleMapFamilyNameFallback(info): """ Fallback to *openTypeNamePreferredFamilyName* if *styleMapStyleName* or, if *styleMapStyleName* isn't defined, *openTypeNamePreferredSubfamilyName* is *regular*, *bold*, *italic* or *bold italic*, otherwise fallback to *openTypeNamePreferredFamilyName openTypeNamePreferredFamilyName*. """ familyName = getAttrWithFallback(info, "openTypeNamePreferredFamilyName") styleName = info.styleMapStyleName if not styleName: styleName = getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName") if styleName is None: styleName = "" elif styleName.lower() in _styleMapStyleNames: styleName = "" return (familyName + " " + styleName).strip() def styleMapStyleNameFallback(info): """ Fallback to *openTypeNamePreferredSubfamilyName* if it is one of *regular*, *bold*, *italic*, *bold italic*, otherwise fallback to *regular*. """ styleName = getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName") if styleName is None: styleName = "regular" elif styleName.strip().lower() not in _styleMapStyleNames: styleName = "regular" else: styleName = styleName.strip().lower() return styleName # head _date_format = "%Y/%m/%d %H:%M:%S" def dateStringForNow(): return time.strftime(_date_format, time.gmtime()) def openTypeHeadCreatedFallback(info): """ Fallback to the environment variable SOURCE_DATE_EPOCH if set, otherwise now. """ if "SOURCE_DATE_EPOCH" in os.environ: t = datetime.fromtimestamp(int(os.environ["SOURCE_DATE_EPOCH"]), timezone.utc) return t.strftime(_date_format) else: return dateStringForNow() # hhea def openTypeHheaAscenderFallback(info): """ Fallback to *ascender + typoLineGap*. """ return getAttrWithFallback(info, "ascender") + getAttrWithFallback( info, "openTypeOS2TypoLineGap" ) def openTypeHheaDescenderFallback(info): """ Fallback to *descender*. """ return getAttrWithFallback(info, "descender") def openTypeHheaCaretSlopeRiseFallback(info): """ Fallback to *openTypeHheaCaretSlopeRise*. If the italicAngle is zero, return UPEM. If italicAngle is non-zero, compute the slope rise from the complementary openTypeHheaCaretSlopeRun, if the latter is defined. Else, default to the font's UPEM. """ italicAngle = getAttrWithFallback(info, "italicAngle") if italicAngle != 0: if ( hasattr(info, "openTypeHheaCaretSlopeRun") and info.openTypeHheaCaretSlopeRun is not None ): slopeRun = info.openTypeHheaCaretSlopeRun return otRound(slopeRun / math.tan(math.radians(-italicAngle))) return getAttrWithFallback(info, "unitsPerEm") def openTypeHheaCaretSlopeRunFallback(info): """ Fallback to *openTypeHheaCaretSlopeRun*. If the italicAngle is zero, return 0. If italicAngle is non-zero, compute the slope run from the complementary openTypeHheaCaretSlopeRise. """ italicAngle = getAttrWithFallback(info, "italicAngle") if italicAngle != 0: slopeRise = getAttrWithFallback(info, "openTypeHheaCaretSlopeRise") return otRound(math.tan(math.radians(-italicAngle)) * slopeRise) return 0 # name def openTypeNameVersionFallback(info): """ Fallback to *versionMajor.versionMinor* in the form 0.000. """ versionMajor = getAttrWithFallback(info, "versionMajor") versionMinor = getAttrWithFallback(info, "versionMinor") return "Version %d.%s" % (versionMajor, str(versionMinor).zfill(3)) def openTypeNameUniqueIDFallback(info): """ Fallback to *openTypeNameVersion;openTypeOS2VendorID;postscriptFontName*. """ version = getAttrWithFallback(info, "openTypeNameVersion").replace("Version ", "") vendor = getAttrWithFallback(info, "openTypeOS2VendorID") fontName = getAttrWithFallback(info, "postscriptFontName") return f"{version};{vendor};{fontName}" def openTypeNamePreferredFamilyNameFallback(info): """ Fallback to *familyName*. """ return getAttrWithFallback(info, "familyName") def openTypeNamePreferredSubfamilyNameFallback(info): """ Fallback to *styleName*. """ return getAttrWithFallback(info, "styleName") def openTypeNameWWSFamilyNameFallback(info): # not yet supported return None def openTypeNameWWSSubfamilyNameFallback(info): # not yet supported return None # OS/2 def openTypeOS2TypoAscenderFallback(info): """ Fallback to *ascender*. """ return getAttrWithFallback(info, "ascender") def openTypeOS2TypoDescenderFallback(info): """ Fallback to *descender*. """ return getAttrWithFallback(info, "descender") def openTypeOS2TypoLineGapFallback(info): """ Fallback to *UPM * 1.2 - ascender + descender*, or zero if that's negative. """ return max( int(getAttrWithFallback(info, "unitsPerEm") * 1.2) - getAttrWithFallback(info, "ascender") + getAttrWithFallback(info, "descender"), 0, ) def openTypeOS2WinAscentFallback(info): """ Fallback to *ascender + typoLineGap*. """ return getAttrWithFallback(info, "ascender") + getAttrWithFallback( info, "openTypeOS2TypoLineGap" ) def openTypeOS2WinDescentFallback(info): """ Fallback to *descender*. """ return abs(getAttrWithFallback(info, "descender")) # postscript _postscriptFontNameExceptions = set("[](){}<>/%") _postscriptFontNameAllowed = {chr(i) for i in range(33, 127)} def normalizeStringForPostscript(s, allowSpaces=True): normalized = [] for c in s: if c == " " and not allowSpaces: continue if c in _postscriptFontNameExceptions: continue if c not in _postscriptFontNameAllowed: # Use compatibility decomposed form, to keep parts in ascii c = unicodedata.normalize("NFKD", c) if not set(c) < _postscriptFontNameAllowed: c = c.encode("ascii", errors="replace").decode() normalized.append(c) return "".join(normalized) def normalizeNameForPostscript(name): return normalizeStringForPostscript(name, allowSpaces=False) def postscriptFontNameFallback(info): """ Fallback to a string containing only valid characters as defined in the specification. This will draw from *openTypeNamePreferredFamilyName* and *openTypeNamePreferredSubfamilyName*. """ name = "{}-{}".format( getAttrWithFallback(info, "openTypeNamePreferredFamilyName"), getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName"), ) return normalizeNameForPostscript(name) def postscriptFullNameFallback(info): """ Fallback to *openTypeNamePreferredFamilyName openTypeNamePreferredSubfamilyName*. """ return "{} {}".format( getAttrWithFallback(info, "openTypeNamePreferredFamilyName"), getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName"), ) def postscriptSlantAngleFallback(info): """ Fallback to *italicAngle*. """ return getAttrWithFallback(info, "italicAngle") def postscriptUnderlineThicknessFallback(info): """Return UPM * 0.05 (50 for 1000 UPM) and warn.""" logger.debug("Underline thickness not set in UFO, defaulting to UPM * 0.05") return getAttrWithFallback(info, "unitsPerEm") * 0.05 def postscriptUnderlinePositionFallback(info): """Return UPM * -0.075 (-75 for 1000 UPM) and warn.""" logger.debug("Underline position not set in UFO, defaulting to UPM * -0.075") return getAttrWithFallback(info, "unitsPerEm") * -0.075 def postscriptBlueScaleFallback(info): """ Fallback to a calculated value: 3/(4 * *maxZoneHeight*) where *maxZoneHeight* is the tallest zone from *postscriptBlueValues* and *postscriptOtherBlues*. If zones are not set, return 0.039625. """ blues = getAttrWithFallback(info, "postscriptBlueValues") otherBlues = getAttrWithFallback(info, "postscriptOtherBlues") maxZoneHeight = 0 blueScale = 0.039625 if blues: assert len(blues) % 2 == 0 for x, y in zip(blues[:-1:2], blues[1::2]): maxZoneHeight = max(maxZoneHeight, abs(y - x)) if otherBlues: assert len(otherBlues) % 2 == 0 for x, y in zip(otherBlues[:-1:2], otherBlues[1::2]): maxZoneHeight = max(maxZoneHeight, abs(y - x)) if maxZoneHeight != 0: blueScale = 3 / (4 * maxZoneHeight) return blueScale # -------------- # Attribute Maps # -------------- staticFallbackData = dict( versionMajor=0, versionMinor=0, copyright=None, trademark=None, familyName="New Font", styleName="Regular", unitsPerEm=1000, italicAngle=0, # not needed year=None, note=None, openTypeHeadLowestRecPPEM=6, openTypeHeadFlags=[0, 1], openTypeHheaLineGap=0, openTypeHheaCaretOffset=0, openTypeNameDesigner=None, openTypeNameDesignerURL=None, openTypeNameManufacturer=None, openTypeNameManufacturerURL=None, openTypeNameLicense=None, openTypeNameLicenseURL=None, openTypeNameDescription=None, openTypeNameCompatibleFullName=None, openTypeNameSampleText=None, openTypeNameRecords=[], openTypeOS2WidthClass=5, openTypeOS2WeightClass=400, openTypeOS2Selection=[], openTypeOS2VendorID="NONE", openTypeOS2Panose=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], openTypeOS2FamilyClass=[0, 0], openTypeOS2UnicodeRanges=None, openTypeOS2CodePageRanges=None, openTypeOS2Type=[2], openTypeOS2SubscriptXSize=None, openTypeOS2SubscriptYSize=None, openTypeOS2SubscriptXOffset=None, openTypeOS2SubscriptYOffset=None, openTypeOS2SuperscriptXSize=None, openTypeOS2SuperscriptYSize=None, openTypeOS2SuperscriptXOffset=None, openTypeOS2SuperscriptYOffset=None, openTypeOS2StrikeoutSize=None, openTypeOS2StrikeoutPosition=None, # fallback to None on these # as the user should be in # complete control openTypeVheaVertTypoAscender=None, openTypeVheaVertTypoDescender=None, openTypeVheaVertTypoLineGap=None, # fallback to horizontal caret: # a value of 0 for the rise # and a value of 1 for the run. openTypeVheaCaretSlopeRise=0, openTypeVheaCaretSlopeRun=1, openTypeVheaCaretOffset=0, postscriptUniqueID=None, postscriptWeightName=None, postscriptIsFixedPitch=False, postscriptBlueValues=[], postscriptOtherBlues=[], postscriptFamilyBlues=[], postscriptFamilyOtherBlues=[], postscriptStemSnapH=[], postscriptStemSnapV=[], postscriptBlueFuzz=0, postscriptBlueShift=7, postscriptForceBold=0, postscriptDefaultWidthX=200, postscriptNominalWidthX=0, # not used in OTF postscriptDefaultCharacter=None, postscriptWindowsCharacterSet=None, # not used in OTF macintoshFONDFamilyID=None, macintoshFONDName=None, ) specialFallbacks = dict( ascender=ascenderFallback, descender=descenderFallback, capHeight=capHeightFallback, xHeight=xHeightFallback, styleMapFamilyName=styleMapFamilyNameFallback, styleMapStyleName=styleMapStyleNameFallback, openTypeHeadCreated=openTypeHeadCreatedFallback, openTypeHheaAscender=openTypeHheaAscenderFallback, openTypeHheaDescender=openTypeHheaDescenderFallback, openTypeHheaCaretSlopeRise=openTypeHheaCaretSlopeRiseFallback, openTypeHheaCaretSlopeRun=openTypeHheaCaretSlopeRunFallback, openTypeNameVersion=openTypeNameVersionFallback, openTypeNameUniqueID=openTypeNameUniqueIDFallback, openTypeNamePreferredFamilyName=openTypeNamePreferredFamilyNameFallback, openTypeNamePreferredSubfamilyName=openTypeNamePreferredSubfamilyNameFallback, openTypeNameWWSFamilyName=openTypeNameWWSFamilyNameFallback, openTypeNameWWSSubfamilyName=openTypeNameWWSSubfamilyNameFallback, openTypeOS2TypoAscender=openTypeOS2TypoAscenderFallback, openTypeOS2TypoDescender=openTypeOS2TypoDescenderFallback, openTypeOS2TypoLineGap=openTypeOS2TypoLineGapFallback, openTypeOS2WinAscent=openTypeOS2WinAscentFallback, openTypeOS2WinDescent=openTypeOS2WinDescentFallback, postscriptFontName=postscriptFontNameFallback, postscriptFullName=postscriptFullNameFallback, postscriptSlantAngle=postscriptSlantAngleFallback, postscriptUnderlineThickness=postscriptUnderlineThicknessFallback, postscriptUnderlinePosition=postscriptUnderlinePositionFallback, postscriptBlueScale=postscriptBlueScaleFallback, ) requiredAttributes = set(ufoLib.fontInfoAttributesVersion2) - ( set(staticFallbackData.keys()) | set(specialFallbacks.keys()) ) recommendedAttributes = { "styleMapFamilyName", "versionMajor", "versionMinor", "copyright", "trademark", "openTypeHeadCreated", "openTypeNameDesigner", "openTypeNameDesignerURL", "openTypeNameManufacturer", "openTypeNameManufacturerURL", "openTypeNameLicense", "openTypeNameLicenseURL", "openTypeNameDescription", "openTypeNameSampleText", "openTypeOS2WidthClass", "openTypeOS2WeightClass", "openTypeOS2VendorID", "openTypeOS2Panose", "openTypeOS2FamilyClass", "openTypeOS2UnicodeRanges", "openTypeOS2CodePageRanges", "openTypeOS2TypoLineGap", "openTypeOS2Type", "postscriptBlueValues", "postscriptOtherBlues", "postscriptFamilyBlues", "postscriptFamilyOtherBlues", "postscriptStemSnapH", "postscriptStemSnapV", } # ------------ # Main Methods # ------------ def getAttrWithFallback(info, attr): """ Get the value for *attr* from the *info* object. If the object does not have the attribute or the value for the atribute is None, this will either get a value from a predefined set of attributes or it will synthesize a value from the available data. """ if hasattr(info, attr) and getattr(info, attr) is not None: value = getattr(info, attr) else: if attr in specialFallbacks: value = specialFallbacks[attr](info) else: value = staticFallbackData[attr] return value def preflightInfo(info): """ Returns a dict containing two items. The value for each item will be a list of info attribute names. ================== === missingRequired Required data that is missing. missingRecommended Recommended data that is missing. ================== === """ missingRequired = set() missingRecommended = set() for attr in requiredAttributes: if not hasattr(info, attr) or getattr(info, attr) is None: missingRequired.add(attr) for attr in recommendedAttributes: if not hasattr(info, attr) or getattr(info, attr) is None: missingRecommended.add(attr) return dict(missingRequired=missingRequired, missingRecommended=missingRecommended) # ----------------- # Low Level Support # ----------------- # these should not be used outside of this package def intListToNum(intList, start, length): all = [] bin = "" for i in range(start, start + length): if i in intList: b = "1" else: b = "0" bin = b + bin if not (i + 1) % 8: all.append(bin) bin = "" if bin: all.append(bin) all.reverse() all = " ".join(all) return binary2num(all) def dateStringToTimeValue(date): try: t = time.strptime(date, "%Y/%m/%d %H:%M:%S") return calendar.timegm(t) except ValueError: return 0 ufo2ft-3.3.1/Lib/ufo2ft/infoCompiler.py000066400000000000000000000121461470175262700176550ustar00rootroot00000000000000""" InfoCompiler is used to apply fontinfo overrides to an already compiled font. This is used to apply fontinfo from a DesignSpace variable-font after merging font sources into final variable font. It builds a temporary font with the only the tables that can be modified with fontinfo, then merge relevant table attributes it into the original font. """ import copy from ufo2ft.outlineCompiler import BaseOutlineCompiler class InfoCompiler(BaseOutlineCompiler): info_tables = frozenset( [ "head", "hhea", "name", "OS/2", "post", "vhea", "gasp", ] ) def __init__(self, otf, ufo, info): self.orig_otf = otf tables = self.info_tables & set(otf.keys()) # Create a temporary UFO and sets its fontinfo to the union of the main # UFO's fontinfo and the DesignSpace variable-font’s info. temp_ufo = type(ufo)() if hasattr(ufo.info, "getDataForSerialization"): # defcon data = ufo.info.getDataForSerialization() data.update(info) temp_ufo.info.setDataFromSerialization(data) else: # ufoLib2 temp_ufo.info = copy.copy(ufo.info) for k, v in info.items(): setattr(temp_ufo.info, k, v) super().__init__(temp_ufo, tables=tables, glyphSet={}, glyphOrder=[]) def compile(self): super().compile() if "gasp" in self.tables: self.setupTable_gasp() return self.orig_otf @staticmethod def makeMissingRequiredGlyphs(*args, **kwargs): return def makeFontBoundingBox(self): from ufo2ft.outlineCompiler import EMPTY_BOUNDING_BOX return EMPTY_BOUNDING_BOX def _set_attrs(self, tag, attrs): temp = self.otf[tag] orig = self.orig_otf[tag] for attr in attrs: if (value := getattr(temp, attr, None)) is not None: setattr(orig, attr, value) def setupTable_head(self): super().setupTable_head() self._set_attrs( "head", { "fontRevision", "unitsPerEm", "created", "macStyle", "flags", "lowestRecPPEM", }, ) def setupTable_hhea(self): super().setupTable_hhea() self._set_attrs( "hhea", { "ascent", "descent", "lineGap", "caretSlopeRise", "caretSlopeRun", "caretOffset", }, ) def setupTable_vhea(self): super().setupTable_vhea() self._set_attrs( "vhea", { "ascent", "descent", "lineGap", "caretSlopeRise", "caretSlopeRun", "caretOffset", }, ) def setupTable_OS2(self): super().setupTable_OS2() self._set_attrs( "OS/2", { "usWeightClass", "usWidthClass", "fsType", "ySubscriptXSize", "ySubscriptYSize", "ySubscriptYOffset", "ySubscriptXOffset", "ySuperscriptXSize", "ySuperscriptYSize", "ySuperscriptYOffset", "ySuperscriptXOffset", "yStrikeoutSize", "yStrikeoutPosition", "sFamilyClass", "panose", "ulUnicodeRange1", "ulUnicodeRange2", "ulUnicodeRange3", "ulUnicodeRange4", "achVendID", "fsSelection", "sTypoAscender", "sTypoDescender", "sTypoLineGap", "usWinAscent", "usWinDescent", "ulCodePageRange1", "ulCodePageRange2", "sxHeight", "sCapHeight", }, ) def setupTable_post(self): super().setupTable_post() self._set_attrs( "post", { "italicAngle", "underlinePosition", "underlineThickness", "isFixedPitch", }, ) def setupTable_name(self): super().setupTable_name() temp = self.otf["name"] orig = self.orig_otf["name"] temp_names = { (n.nameID, n.platformID, n.platEncID, n.langID): n for n in temp.names } orig_names = { (n.nameID, n.platformID, n.platEncID, n.langID): n for n in orig.names } orig_names.update(temp_names) orig.names = list(orig_names.values()) def setupTable_gasp(self): from ufo2ft.instructionCompiler import InstructionCompiler instructionCompiler = InstructionCompiler(self.ufo, self.otf) instructionCompiler.setupTable_gasp() self._set_attrs("gasp", {"gaspRange"}) def setupTable_maxp(self): return ufo2ft-3.3.1/Lib/ufo2ft/instantiator.py000066400000000000000000001162371470175262700177540ustar00rootroot00000000000000# This code is derived from fontmake.instantiator (Apache License 2.0), which in # turn was based on ufoProcessor code. The latter is licensed as follows. # Copyright (c) 2017-2018 LettError and Erik van Blokland # All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """Module for generating static font instances. It is an alternative to ufoProcessor originally meant to be used internally by fontmake. The aim is to be a minimal implementation that is focussed on using ufoLib2 for font data abstraction, varLib for instance computation and fontMath as a font data shell for instance computation directly and exclusively. """ from __future__ import annotations import copy import logging import typing from dataclasses import dataclass, field from functools import cached_property from typing import ( TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Set, Tuple, Union, ) import fontMath import fontTools.misc.fixedTools from fontTools import designspaceLib, varLib from ufo2ft.util import ( _getNewGlyphFactory, importUfoModule, openFontFactory, zip_strict, ) if TYPE_CHECKING: from collections.abc import Iterable, KeysView from ufoLib2.objects import Font, Glyph, Info logger = logging.getLogger(__name__) # Use the same rounding function used by varLib to round things for the variable font # to reduce differences between the variable and static instances. fontMath.mathFunctions.setRoundIntegerFunction(fontTools.misc.fixedTools.otRound) # Stand-in type for any of the fontMath classes we use. FontMathObject = Union[fontMath.MathGlyph, fontMath.MathInfo, fontMath.MathKerning] # MutatorMath-style location mapping type, i.e. # `{"wght": 1.0, "wdth": 0.0, "bleep": 0.5}`. # LocationKey is a Location turned into a tuple so we can use it as a dict key. Location = Mapping[str, float] LocationKey = Tuple[Tuple[str, float], ...] # Type of mapping of axes to their minimum, default and maximum values, i.e. # `{"wght": (100.0, 400.0, 900.0), "wdth": (75.0, 100.0, 100.0)}`. AxisBounds = Dict[str, Tuple[float, float, float]] # A bunch of glyphs at a given location (in design coordinates) SourceLayer = Tuple[Location, Dict[str, "Glyph"]] # For mapping `wdth` axis user values to the OS2 table's width class field. WDTH_VALUE_TO_OS2_WIDTH_CLASS = { 50: 1, 62.5: 2, 75: 3, 87.5: 4, 100: 5, 112.5: 6, 125: 7, 150: 8, 200: 9, } # Font info fields that are not interpolated and should be copied from the # default font to the instance. # # fontMath at the time of this writing handles the following attributes: # https://github.com/robotools/fontMath/blob/0.5.0/Lib/fontMath/mathInfo.py#L360-L422 # # From the attributes that are left, we skip instance-specific ones on purpose: # - guidelines # - postscriptFontName # - styleMapFamilyName # - styleMapStyleName # - styleName # - openTypeNameCompatibleFullName # - openTypeNamePreferredFamilyName # - openTypeNamePreferredSubfamilyName # - openTypeNameUniqueID # - openTypeNameWWSFamilyName # - openTypeNameWWSSubfamilyName # - openTypeOS2Panose # - postscriptFullName # - postscriptUniqueID # - woffMetadataUniqueID # # Some, we skip because they are deprecated: # - macintoshFONDFamilyID # - macintoshFONDName # - year # # This means we implicitly require the `stylename` attribute in the Designspace # `` element. UFO_INFO_ATTRIBUTES_TO_COPY_TO_INSTANCES = { "copyright", "familyName", "note", "openTypeGaspRangeRecords", "openTypeHeadCreated", "openTypeHeadFlags", "openTypeNameDescription", "openTypeNameDesigner", "openTypeNameDesignerURL", "openTypeNameLicense", "openTypeNameLicenseURL", "openTypeNameManufacturer", "openTypeNameManufacturerURL", "openTypeNameRecords", "openTypeNameSampleText", "openTypeNameVersion", "openTypeOS2CodePageRanges", "openTypeOS2FamilyClass", "openTypeOS2Selection", "openTypeOS2Type", "openTypeOS2UnicodeRanges", "openTypeOS2VendorID", "postscriptDefaultCharacter", "postscriptForceBold", "postscriptIsFixedPitch", "postscriptWindowsCharacterSet", "trademark", "versionMajor", "versionMinor", "woffMajorVersion", "woffMetadataCopyright", "woffMetadataCredits", "woffMetadataDescription", "woffMetadataExtensions", "woffMetadataLicense", "woffMetadataLicensee", "woffMetadataTrademark", "woffMetadataVendor", "woffMinorVersion", } # Custom exception for this module class InstantiatorError(Exception): pass def process_rules_swaps(rules, location, glyphNames): """Apply these rules at this location to these glyphnames - rule order matters Return a list of (oldName, newName) in the same order as the rules. """ swaps = [] glyphNames = set(glyphNames) for rule in rules: if designspaceLib.evaluateRule(rule, location): for oldName, newName in rule.subs: # Here I don't check if the new name is also in glyphNames... # I guess it should be, so that we can swap, and if it isn't, # then it's better to error out later when we try to swap, # instead of silently ignoring the rule here. if oldName in glyphNames: swaps.append((oldName, newName)) return swaps @dataclass(frozen=True) class Instantiator: """Data class that holds all necessary information to generate a static font instance object at an arbitary location within the design space.""" axis_bounds: AxisBounds # Design space! source_layers: List[SourceLayer] # at least 1 default layer required (can be empty) copy_feature_text: str = "" copy_nonkerning_groups: Mapping[str, List[str]] = field(default_factory=dict) copy_info: Optional[Info] = None copy_lib: Mapping[str, Any] = field(default_factory=dict) designspace_rules: List[designspaceLib.RuleDescriptor] = field(default_factory=list) glyph_mutators: Mapping[str, Optional["Variator"]] = field(default_factory=dict) info_mutator: Optional["Variator"] = None kerning_mutator: Optional["Variator"] = None round_geometry: bool = False skip_export_glyphs: List[str] = field(default_factory=list) special_axes: Mapping[str, designspaceLib.AxisDescriptor] = field( default_factory=dict ) # computed attributes (see __post_init__ below) default_source_idx: int = field(init=False) default_design_location: Location = field(init=False) def __post_init__(self): default_location = { axis: default for axis, (_, default, _) in self.axis_bounds.items() } default_location_items = default_location.items() default_source_idx = None for i, (location, _) in enumerate(self.source_layers): if location.items() <= default_location_items: default_source_idx = i break else: raise InstantiatorError( f"Missing source layer at default location: {default_location}" ) assert default_source_idx is not None object.__setattr__(self, "default_source_idx", default_source_idx) object.__setattr__( self, "default_design_location", {**default_location, **location} ) @classmethod def from_designspace( cls, designspace: designspaceLib.DesignSpaceDocument, round_geometry: bool = True, do_info=True, do_kerning=True, do_glyphs=True, ): """Instantiates a new data class from a Designspace object.""" if designspace.default is None: raise InstantiatorError(_error_msg_no_default(designspace)) if any(hasattr(axis, "values") for axis in designspace.axes): raise InstantiatorError( "The given designspace has one or more discrete (= non-interpolating) " "axes. You should split this designspace into smaller interpolating " "spaces and use the Instantiator on each. See the method " "`fontTools.designspaceLib.split.splitInterpolable()`" ) if any(anisotropic(instance.location) for instance in designspace.instances): raise InstantiatorError( "The Designspace contains anisotropic instance locations, which are " "not supported by varLib. Look for and remove all 'yvalue=\"...\"' or " "use MutatorMath instead." ) designspace.loadSourceFonts(openFontFactory()) # The default font (default layer) determines which glyphs are interpolated, # because the math behind varLib and MutatorMath uses the default font as the # point of reference for all data. default_font = designspace.default.font default_layer = default_font.layers.defaultLayer non_default_layer_name = designspace.default.layerName if non_default_layer_name is not None: try: layer = default_font.layers[non_default_layer_name] except KeyError as e: raise InstantiatorError( f"Layer {non_default_layer_name!r} not found " f"in {designspace.default.filename}" ) from e default_layer = default_font.layers[non_default_layer_name] logger.info(f"Building from layer {layer.name}") glyph_names: Set[str] = set(default_layer.keys()) for source in designspace.sources: other_names = set(source.font.keys()) diff_names = other_names - glyph_names if diff_names: max_diff_glyphs = 10 logger.warning( "The source %s (%s)%s contains glyphs that are missing from the " "default source, which will be ignored: %s%s; if this is unintended, " "check that these glyphs have the exact same name as the " "corresponding glyphs in the default source.", source.name, source.filename, ( f" [layer: {source.layerName}]" if non_default_layer_name is not None else "" ), ", ".join(sorted(diff_names)[0:max_diff_glyphs]), ( f"... ({len(diff_names)} total)" if len(diff_names) > max_diff_glyphs else "" ), ) # Construct Variators axis_bounds: AxisBounds = {} # Design space! default_design_location = {} axis_order: List[str] = [] special_axes = {} for axis in designspace.axes: axis_order.append(axis.name) axis_bounds[axis.name] = ( axis.map_forward(axis.minimum), axis.map_forward(axis.default), axis.map_forward(axis.maximum), ) default_design_location[axis.name] = axis_bounds[axis.name][1] # Some axes relate to existing OpenType fields and get special attention. if axis.tag in {"wght", "wdth", "slnt"}: special_axes[axis.tag] = axis info_mutator = None if do_info: masters_info = collect_info_masters(designspace, axis_bounds) try: info_mutator = Variator.from_masters(masters_info, axis_order) except varLib.errors.VarLibError as e: raise InstantiatorError( f"Cannot set up fontinfo for interpolation: {e}'" ) from e kerning_mutator = None if do_kerning: masters_kerning = collect_kerning_masters(designspace, axis_bounds) try: kerning_mutator = Variator.from_masters(masters_kerning, axis_order) except varLib.errors.VarLibError as e: raise InstantiatorError( f"Cannot set up kerning for interpolation: {e}'" ) from e # left empty as the glyph sources will be loaded later on-demand glyph_mutators: Dict[str, Variator] = {} source_layers = [] if do_glyphs: # collect (location, source layer) tuples for source in designspace.sources: if source.layerName is None: layer = source.font.layers.defaultLayer else: layer = source.font.layers[source.layerName] source_location = source.location if source is designspace.default: # sanity check that the source marked as the default really matches # the default location (i.e. is a subset of) assert source_location.items() <= default_design_location.items() source_layers.append((source_location, {g.name: g for g in layer})) # Construct defaults to copy over copy_feature_text: str = default_font.features.text if do_info else "" # Kerning groups are taken care of by the kerning Variator. copy_nonkerning_groups: Mapping[str, List[str]] = ( { key: glyph_names for key, glyph_names in default_font.groups.items() if not key.startswith(("public.kern1.", "public.kern2.")) } if do_info else {} ) copy_info: Optional[Info] = default_font.info if do_info else None copy_lib: Mapping[str, Any] = default_font.lib if do_info else {} # The list of glyphs-not-to-export-and-decompose-where-used-as-a-component is # supposed to be taken from the Designspace when a Designspace is used as the # starting point of the compilation process. It should be exported to all # instance libs, where the ufo2ft compilation functions will pick it up. skip_export_glyphs = designspace.lib.get("public.skipExportGlyphs", []) return cls( axis_bounds, source_layers, copy_feature_text, copy_nonkerning_groups, copy_info, copy_lib, designspace.rules, glyph_mutators, info_mutator, kerning_mutator, round_geometry, skip_export_glyphs, special_axes, ) @property def axis_order(self): return list(self.axis_bounds.keys()) @property def default_source_glyphs(self) -> Dict[str, Glyph]: return self.source_layers[self.default_source_idx][1] @property def glyph_names(self) -> KeysView[str]: return self.default_source_glyphs.keys() @property def source_locations(self) -> Iterable[Location]: return ( {**self.default_design_location, **loc} for loc, _ in self.source_layers ) def normalize(self, location: Location) -> Location: return varLib.models.normalizeLocation(location, self.axis_bounds) def generate_instance(self, instance: designspaceLib.InstanceDescriptor) -> Font: """Generate an interpolated instance font object for an InstanceDescriptor.""" if anisotropic(instance.location): raise InstantiatorError( f"Instance {instance.familyName}-" f"{instance.styleName}: Anisotropic location " f"{instance.location} not supported by varLib." ) ufo_module = importUfoModule() font = ufo_module.Font() # Instances may leave out locations that match the default source, so merge # default location with the instance's location. location = {**self.default_design_location, **instance.location} location_normalized = self.normalize(location) # Kerning if self.kerning_mutator: kerning_instance = self.kerning_mutator.instance_at(location_normalized) if self.round_geometry: kerning_instance.round() kerning_instance.extractKerning(font) # Info if self.info_mutator: self._generate_instance_info(instance, location_normalized, location, font) # Non-kerning groups. Kerning groups have been taken care of by the kerning # instance. for key, glyph_names in self.copy_nonkerning_groups.items(): font.groups[key] = [name for name in glyph_names] # Features font.features.text = self.copy_feature_text # Lib # 1. Copy the default lib to the instance. font.lib = typing.cast(dict, copy.deepcopy(self.copy_lib)) # 2. Copy the Designspace's skipExportGlyphs list over to the UFO to # make sure it wins over the default UFO one. font.lib["public.skipExportGlyphs"] = [name for name in self.skip_export_glyphs] # 3. Write _design_ location to instance's lib. font.lib["designspace.location"] = [loc for loc in location.items()] # Glyphs for glyph_name in self.glyph_names: glyph = font.newGlyph(glyph_name) try: self.generate_glyph_instance( glyph_name, location_normalized, output_glyph=glyph ) except Exception as e: # TODO: Figure out what exceptions fontMath/varLib can throw. # By default, explode if we cannot generate a glyph instance for # whatever reason (usually outline incompatibility)... if glyph_name not in self.skip_export_glyphs: raise InstantiatorError( f"Failed to generate instance of glyph {glyph_name!r}: " f"{str(e)}. (Note: the most common cause for an error here is " "that the glyph outlines are not point-for-point compatible or " "have the same starting point or are in the same order in all " "masters.)" ) from e # ...except if the glyph is in public.skipExportGlyphs and would # therefore be removed from the compiled font anyway. There's not much # we can do except leave it empty in the instance and tell the user. logger.warning( "Failed to generate instance of glyph '%s', which is marked as " "non-exportable. Glyph will be left empty. Failure reason: %s", glyph_name, e, ) # Process rules # The order of the swaps below is independent of the order of glyph names. # It depends on the order of the s in the designspace rules. swaps = process_rules_swaps(self.designspace_rules, location, self.glyph_names) for name_old, name_new in swaps: if name_old != name_new: swap_glyph_names(font, name_old, name_new) return font @cached_property def glyph_factory(self) -> Callable[[str], Glyph]: glyphs = self.default_source_glyphs if len(glyphs) > 0: glyph_factory = _getNewGlyphFactory(next(iter(glyphs.values()))) else: raise InstantiatorError("Default source has no glyphs, can't make new ones") return glyph_factory def new_glyph(self, name: str) -> Glyph: return self.glyph_factory(name=name) def generate_glyph_instance( self, glyph_name: str, normalized_location: Location, output_glyph: Glyph | None = None, ) -> Glyph: """Generate an instance of a single glyph at the given location. The location must be specified using normalized coordinates. If output_glyph is None, the instance is generated in a new Glyph object and returned. Otherwise, the instance is extracted to the given Glyph object. """ glyph_mutator = self.glyph_mutators.get(glyph_name) if glyph_mutator is None: sources = collect_glyph_masters( self.source_layers, glyph_name, self.axis_bounds, self.default_source_idx, ) try: glyph_mutator = self.glyph_mutators[glyph_name] = Variator.from_masters( sources, self.axis_order ) except varLib.errors.VarLibError as e: raise InstantiatorError( f"Cannot set up glyph {glyph_name} for interpolation: {e}'" ) from e glyph_instance = glyph_mutator.instance_at(normalized_location) if self.round_geometry: glyph_instance = glyph_instance.round() if output_glyph is None: output_glyph = self.new_glyph(glyph_name) # onlyGeometry=True does not set name and unicodes, in ufoLib2 we can't # modify a glyph's name. Copy unicodes from default layer. glyph_instance.extractGlyph(output_glyph, onlyGeometry=True) output_glyph.unicodes = list(self.default_source_glyphs[glyph_name].unicodes) return output_glyph def _generate_instance_info( self, instance: designspaceLib.InstanceDescriptor, location_normalized: Location, location: Location, font: Font, ) -> None: """Generate fontinfo related attributes. Separate, as fontinfo treatment is more extensive than the rest. """ assert self.info_mutator is not None assert self.copy_info is not None info_instance = self.info_mutator.instance_at(location_normalized) if self.round_geometry: info_instance = info_instance.round() info_instance.extractInfo(font.info) # Copy non-interpolating metadata from the default font. for attribute in UFO_INFO_ATTRIBUTES_TO_COPY_TO_INSTANCES: if hasattr(self.copy_info, attribute): setattr( font.info, attribute, copy.deepcopy(getattr(self.copy_info, attribute)), ) # TODO: multilingual names to replace possibly existing name records. if instance.familyName: font.info.familyName = instance.familyName if instance.styleName is None: logger.warning( "The given instance or instance at location %s is missing the " "stylename attribute, which is required. Copying over the styleName " "from the default font, which is probably wrong.", location, ) font.info.styleName = self.copy_info.styleName else: font.info.styleName = instance.styleName if instance.postScriptFontName: font.info.postscriptFontName = instance.postScriptFontName if instance.styleMapFamilyName: font.info.styleMapFamilyName = instance.styleMapFamilyName if instance.styleMapStyleName: font.info.styleMapStyleName = instance.styleMapStyleName # If the masters haven't set the OS/2 weight and width class, use the # user-space values ("input") of the axis mapping in the Designspace file for # weight and width axes, if they exist. The slnt axis' value maps 1:1 to # italicAngle. Clamp the values to the valid ranges. if info_instance.openTypeOS2WeightClass is None and "wght" in self.special_axes: weight_axis = self.special_axes["wght"] font.info.openTypeOS2WeightClass = weight_class_from_wght_value( weight_axis.map_backward(location[weight_axis.name]) ) if info_instance.openTypeOS2WidthClass is None and "wdth" in self.special_axes: width_axis = self.special_axes["wdth"] font.info.openTypeOS2WidthClass = width_class_from_wdth_value( width_axis.map_backward(location[width_axis.name]) ) if info_instance.italicAngle is None and "slnt" in self.special_axes: slant_axis = self.special_axes["slnt"] font.info.italicAngle = italic_angle_from_slnt_value( slant_axis.map_backward(location[slant_axis.name]) ) @property def interpolated_layers(self) -> list[InterpolatedLayer]: """Return one InterpolatedLayer for each source location.""" default = self.default_design_location return [ InterpolatedLayer(self, {**default, **loc}, source_layer) for loc, source_layer in self.source_layers ] def replace_source_layers(self, new_layers: list[dict[str, Glyph]]): """Replace source layers with `new_layers` and clear the cached glyph models. Raises `ValueError` if len(new_layers) != len(self.source_layers). """ self.source_layers[:] = [ (loc, new_glyphs) for (loc, _), new_glyphs in zip_strict(self.source_layers, new_layers) ] # this forces to reload the glyph variation models when an instance is requested self.glyph_mutators.clear() def _error_msg_no_default(designspace: designspaceLib.DesignSpaceDocument) -> str: if any(axis.map for axis in designspace.axes): bonus_msg = ( "For axes with a mapping, the 'default' values should have an " "'input=\"...\"' map value, where the corresponding 'output=\"...\"' " "value then points to the master source." ) else: bonus_msg = "" default_location = ", ".join( f"{k}: {v}" for k, v in designspace.newDefaultLocation().items() ) return ( "Can't generate UFOs from this Designspace because there is no default " "master source at location {!r}. Check that all 'default' " "values of all axes together point to a single actual master source. " "{!s}".format(default_location, bonus_msg) ) def location_to_key(location: Location) -> LocationKey: """Converts a Location into a sorted tuple so it can be used as a dict key.""" return tuple(sorted(location.items())) def anisotropic(location: Location) -> bool: """Tests if any single location value is a MutatorMath-style anisotropic value, i.e. is a tuple of (x, y).""" return any(isinstance(v, tuple) for v in location.values()) def collect_info_masters( designspace: designspaceLib.DesignSpaceDocument, axis_bounds: AxisBounds ) -> List[Tuple[Location, FontMathObject]]: """Return master Info objects wrapped by MathInfo.""" locations_and_masters = [] for source in designspace.sources: if source.layerName is not None and source is not designspace.default: continue # No font info in non-default source layers. normalized_location = varLib.models.normalizeLocation( source.location, axis_bounds ) locations_and_masters.append( (normalized_location, fontMath.MathInfo(source.font.info)) ) return locations_and_masters def collect_kerning_masters( designspace: designspaceLib.DesignSpaceDocument, axis_bounds: AxisBounds ) -> List[Tuple[Location, FontMathObject]]: """Return master kerning objects wrapped by MathKerning.""" # Always take the groups from the default source. This also avoids fontMath # making a union of all groups it is given. groups = designspace.default.font.groups locations_and_masters = [] for source in designspace.sources: if source.layerName is not None and source is not designspace.default: continue # No kerning in non-default source layers. # If a source has groups, they should match the default's. if source.font.groups and source.font.groups != groups: logger.warning( "The source %s (%s) contains different groups than the default source. " "The default source's groups will be used for the instances.", source.name, source.filename, ) # This assumes that groups of all sources are the same. normalized_location = varLib.models.normalizeLocation( source.location, axis_bounds ) locations_and_masters.append( (normalized_location, fontMath.MathKerning(source.font.kerning, groups)) ) return locations_and_masters def collect_glyph_masters( source_layers: List[SourceLayer], glyph_name: str, axis_bounds: AxisBounds, default_source_idx: int, ) -> List[Tuple[Location, FontMathObject]]: """Return master glyph objects for glyph_name wrapped by MathGlyph. Note: skips empty source glyphs if the default glyph is not empty to almost match what ufoProcessor is doing. In e.g. Mutator Sans, the 'S.closed' glyph is left empty in one source layer. One could treat this as a source error, but ufoProcessor specifically has code to skip that empty glyph and carry on. """ locations_and_masters = [] default_glyph_empty = False other_glyph_empty = False for i, (location, source_layer) in enumerate(source_layers): this_is_default = i == default_source_idx if glyph_name not in source_layer: if this_is_default: # Default layer must contain every glyph by definition. raise InstantiatorError( f"glyph {glyph_name!r} not found in the default source" ) else: # Sparse fonts do not and layers may not contain every glyph. continue source_glyph = source_layer[glyph_name] if not (len(source_glyph) or source_glyph.components): if this_is_default: default_glyph_empty = True else: other_glyph_empty = True normalized_location = varLib.models.normalizeLocation(location, axis_bounds) locations_and_masters.append( (normalized_location, fontMath.MathGlyph(source_glyph, strict=True)) ) # Filter out empty glyphs if the default glyph is not empty. if not default_glyph_empty and other_glyph_empty: locations_and_masters = [ (loc, master) for loc, master in locations_and_masters if master.contours or master.components ] return locations_and_masters def width_class_from_wdth_value(wdth_user_value) -> int: """Return the OS/2 width class from the wdth axis user value. The OpenType 1.8.3 specification states: When mapping from 'wdth' values to usWidthClass, interpolate fractional values between the mapped values and then round, and clamp to the range 1 to 9. "Mapped values" probably means the in-percent numbers layed out for the OS/2 width class, so we are forcing these numerical semantics on the user values of the wdth axis. """ width_user_value = min(max(wdth_user_value, 50), 200) width_user_value_mapped = varLib.models.piecewiseLinearMap( width_user_value, WDTH_VALUE_TO_OS2_WIDTH_CLASS ) return fontTools.misc.fixedTools.otRound(width_user_value_mapped) def weight_class_from_wght_value(wght_user_value) -> int: """Return the OS/2 weight class from the wght axis user value.""" weight_user_value = min(max(wght_user_value, 1), 1000) return fontTools.misc.fixedTools.otRound(weight_user_value) def italic_angle_from_slnt_value(slnt_user_value) -> Union[int, float]: """Return the italic angle from the slnt axis user value.""" slant_user_value = min(max(slnt_user_value, -90), 90) return slant_user_value def swap_glyph_names(font: Any, name_old: str, name_new: str): """Swap two existing glyphs in the default layer of a font (outlines, width, component references, kerning references, group membership). The idea behind swapping instead of overwriting is explained in https://github.com/fonttools/fonttools/tree/main/Doc/source/designspaceLib#ufo-instances. We need to keep the old glyph around in case any other glyph references it; glyphs that are not explicitly substituted by rules should not be affected by the rule application. The .unicodes are not swapped. The rules mechanism is supposed to swap glyphs, not characters. """ if name_old not in font or name_new not in font: raise InstantiatorError( "Cannot swap glyphs {!r} and {!r}, as either or both are missing".format( name_old, name_new ) ) # 1. Swap outlines and glyph width. Ignore lib content and other properties. glyph_old = font[name_old] glyph_new = font[name_new] glyph_swap = _getNewGlyphFactory(glyph_old)(name="temporary_swap_glyph") p = glyph_swap.getPointPen() glyph_old.drawPoints(p) glyph_swap.width = glyph_old.width glyph_old.clearContours() glyph_old.clearComponents() p = glyph_old.getPointPen() glyph_new.drawPoints(p) glyph_old.width = glyph_new.width glyph_new.clearContours() glyph_new.clearComponents() p = glyph_new.getPointPen() glyph_swap.drawPoints(p) glyph_new.width = glyph_swap.width # 2. Swap anchors. glyph_swap.anchors = [dict(a) for a in glyph_old.anchors] glyph_old.anchors = [dict(a) for a in glyph_new.anchors] glyph_new.anchors = [dict(a) for a in glyph_swap.anchors] # 3. Remap components. for g in font: for c in g.components: if c.baseGlyph == name_old: c.baseGlyph = name_new elif c.baseGlyph == name_new: c.baseGlyph = name_old # 4. Swap literal names in kerning. kerning_new = {} for first, second in font.kerning.keys(): value = font.kerning[(first, second)] if first == name_old: first = name_new elif first == name_new: first = name_old if second == name_old: second = name_new elif second == name_new: second = name_old kerning_new[(first, second)] = value font.kerning.clear() font.kerning.update(kerning_new) # 5. Swap names in groups. for group_name, group_members in font.groups.items(): group_members_new = [] for name in group_members: if name == name_old: group_members_new.append(name_new) elif name == name_new: group_members_new.append(name_old) else: group_members_new.append(name) font.groups[group_name] = group_members_new @dataclass(frozen=True) class Variator: """A middle-man class that ingests a mapping of normalized locations to masters plus axis definitions and uses varLib to spit out interpolated instances at specified normalized locations. fontMath objects stand in for the actual master objects from the UFO. Upon generating an instance, these objects have to be extracted into an actual UFO object. """ masters: List[FontMathObject] location_to_master: Mapping[LocationKey, FontMathObject] model: varLib.models.VariationModel @classmethod def from_masters( cls, items: List[Tuple[Location, FontMathObject]], axis_order: List[str] ): masters = [] master_locations = [] location_to_master = {} for normalized_location, master in items: master_locations.append(normalized_location) masters.append(master) location_to_master[location_to_key(normalized_location)] = master model = varLib.models.VariationModel(master_locations, axis_order) return cls(masters, location_to_master, model) def instance_at(self, normalized_location: Location) -> FontMathObject: """Return a FontMathObject for the specified location ready to be inflated. If an instance location matches a master location, this method returns the master data instead of running through varLib. This is both an optimization _and_ it enables having a Designspace with instances matching their masters without requiring them to be compatible. Glyphs.app works this way; it will only generate a font from an instance, but compatibility is only required if there is actual interpolation to be done. This enables us to store incompatible bare masters in one Designspace and having arbitrary instance data applied to them. """ normalized_location_key = location_to_key(normalized_location) if normalized_location_key in self.location_to_master: return copy.deepcopy(self.location_to_master[normalized_location_key]) return self.model.interpolateFromMasters(normalized_location, self.masters) @dataclass(frozen=True, repr=False) class InterpolatedLayer(Mapping): """Mapping of glyphs keyed by name, interpolated on demand. If the given location corresponds to one of the source layers, and the latter contains the glyph, this is used directly; otherwise, a new glyph instance is generated on-the-fly at that location, and cached for subsequent retrieval. This is useful for APIs that expect a dict of glyphs for resolving component references, e.g. FontTools pens. """ instantiator: Instantiator # axis coordinates for this layer in design space location: Location # source ufoLib2/defcon Layer (None if location isn't among the source locations) source_layer: dict[str, Glyph] | None = None _cache: dict[str, Glyph] = field(default_factory=dict) @cached_property def normalized_location(self): return self.instantiator.normalize(self.location) def __iter__(self) -> Iterable[str]: return iter(self.instantiator.glyph_names) def __len__(self) -> int: return len(self.instantiator.glyph_names) def __getitem__(self, glyph_name: str) -> Glyph: try: return self._cache.setdefault( glyph_name, self._get(glyph_name) or self._interpolate(glyph_name) ) except InstantiatorError as e: raise KeyError(glyph_name) from e def __repr__(self): return ( f"<{self.__class__.__name__} {self.location} " f"({len(self)} glyphs) at 0x{id(self):12x}>" ) def _get(self, glyph_name: str) -> Glyph | None: glyph = None if self.source_layer is not None: glyph = self.source_layer.get(glyph_name) return glyph def _interpolate(self, glyph_name: str) -> Glyph: return self.instantiator.generate_glyph_instance( glyph_name, self.normalized_location ) ufo2ft-3.3.1/Lib/ufo2ft/instructionCompiler.py000066400000000000000000000331241470175262700213020ustar00rootroot00000000000000from __future__ import annotations import array import logging from functools import partial from typing import TYPE_CHECKING, Optional from fontTools import ttLib from fontTools.misc.fixedTools import floatToFixedToFloat from fontTools.pens.hashPointPen import HashPointPen from fontTools.pens.roundingPen import RoundingPointPen from fontTools.ttLib import newTable from fontTools.ttLib.tables._g_l_y_f import ( OVERLAP_COMPOUND, ROUND_XY_TO_GRID, USE_MY_METRICS, flagOverlapSimple, ) 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, glyph: Glyph, ttglyph: TTGlyph, stored_hash: Optional[str] ) -> bool: """Check if the supplied stored glyph hash from the ufo matches the TTGlyph.""" if stored_hash is None: # The glyph hash is required logger.error( f"Glyph hash missing, glyph '{glyph.name}' will have " "no instructions in font." ) return False ttwidth = self.otf["hmtx"][glyph.name][0] hash_pen = HashPointPen(ttwidth, self.otf.getGlyphSet()) round_pen = RoundingPointPen( hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14) ) ttglyph.drawPoints(round_pen, self.otf["glyf"]) if stored_hash != hash_pen.hash: logger.error( f"The stored hash for glyph '{glyph.name}' 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; no need to inform about '.notdef' # since that glyph is often auto-generated if name != ".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) else: self._set_simple_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, 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.debug( "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." ) 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 are being set elsewhere: # - ROUND_XY_TO_GRID has already been set in TTGlyphPointPen.glyph() # called from OutlineTTFCompiler.compileGlyphs() # - USE_MY_METRICS is set automatically below if no component has it 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.debug( 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 _set_simple_flags(self, glyph: Glyph, ttglyph: TTGlyph) -> None: # Set simple glyph flags if ttglyph.numberOfContours < 1 or not ttglyph.flags: return # Set OVERLAP_SIMPLE if TRUETYPE_OVERLAP_KEY in glyph.lib: if glyph.lib[TRUETYPE_OVERLAP_KEY]: ttglyph.flags[0] |= flagOverlapSimple else: ttglyph.flags[0] &= ~flagOverlapSimple 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") ufo2ft-3.3.1/Lib/ufo2ft/maxContextCalc.py000066400000000000000000000003341470175262700201400ustar00rootroot00000000000000"""NOTE: this module was moved to fonttools, it is kept here only for backward compatibility. Please import it from the new location. """ from fontTools.otlLib.maxContextCalc import maxCtxFont __all__ = ["maxCtxFont"] ufo2ft-3.3.1/Lib/ufo2ft/outlineCompiler.py000066400000000000000000002211561470175262700204040ustar00rootroot00000000000000import logging import math from collections import Counter, namedtuple from io import BytesIO from types import SimpleNamespace from fontTools.cffLib import ( CharStrings, GlobalSubrsIndex, IndexedStrings, PrivateDict, SubrsIndex, TopDict, TopDictIndex, ) from fontTools.misc.arrayTools import unionRect from fontTools.misc.roundTools import noRound, otRound from fontTools.pens.boundsPen import ControlBoundsPen from fontTools.pens.pointPen import SegmentToPointPen from fontTools.pens.reverseContourPen import ReverseContourPen from fontTools.pens.t2CharStringPen import T2CharStringPen 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 Glyph, flagCubic from fontTools.ttLib.tables._h_e_a_d import mac_epoch_diff from fontTools.ttLib.tables.O_S_2f_2 import Panose from ufo2ft.constants import ( COLOR_LAYERS_KEY, COLOR_PALETTES_KEY, COLR_CLIP_BOXES_KEY, GLYPHS_MATH_CONSTANTS_KEY, GLYPHS_MATH_EXTENDED_SHAPE_KEY, GLYPHS_MATH_PREFIX, GLYPHS_MATH_VARIANTS_KEY, OPENTYPE_META_KEY, OPENTYPE_POST_UNDERLINE_POSITION_KEY, UNICODE_VARIATION_SEQUENCES_KEY, CFFOptimization, ) from ufo2ft.errors import InvalidFontData from ufo2ft.fontInfoData import ( dateStringForNow, dateStringToTimeValue, getAttrWithFallback, intListToNum, normalizeStringForPostscript, ) from ufo2ft.instructionCompiler import InstructionCompiler from ufo2ft.util import ( _copyGlyph, _getNewGlyphFactory, calcCodePageRanges, colrClipBoxQuantization, getMaxComponentDepth, makeOfficialGlyphOrder, makeUnicodeToGlyphNameMapping, ) logger = logging.getLogger(__name__) BoundingBox = namedtuple("BoundingBox", ["xMin", "yMin", "xMax", "yMax"]) EMPTY_BOUNDING_BOX = BoundingBox(0, 0, 0, 0) def _isNonBMP(s): for c in s: if ord(c) > 65535: return True return False def _getVerticalOrigin(font, glyph): if hasattr(glyph, "verticalOrigin") and glyph.verticalOrigin is not None: verticalOrigin = glyph.verticalOrigin else: os2 = font.get("OS/2") typo_ascender = os2.sTypoAscender if os2 is not None else 0 verticalOrigin = typo_ascender return otRound(verticalOrigin) class BaseOutlineCompiler: """Create a feature-less outline binary.""" sfntVersion = None tables = frozenset( [ "head", "hmtx", "hhea", "name", "maxp", "cmap", "OS/2", "post", "vmtx", "vhea", "COLR", "CPAL", "MATH", "meta", ] ) def __init__( self, font, glyphSet=None, glyphOrder=None, tables=None, notdefGlyph=None, colrLayerReuse=True, colrAutoClipBoxes=True, colrClipBoxQuantization=colrClipBoxQuantization, ftConfig=None, *, compilingVFDefaultSource=True, ): self.ufo = font # use the previously filtered glyphSet, if any if glyphSet is None: glyphSet = {g.name: g for g in font} # this is set to False by Interpolatable{O,T}TFCompiler when building a VF's # non-default masters. E.g. it's used by makeMissingRequiredGlyphs method below. self.compilingVFDefaultSource = compilingVFDefaultSource self.makeMissingRequiredGlyphs(font, glyphSet, self.sfntVersion, notdefGlyph) self.allGlyphs = glyphSet # store the glyph order if glyphOrder is None: glyphOrder = font.glyphOrder self.glyphOrder = self.makeOfficialGlyphOrder(glyphOrder) # make a reusable character mapping self.unicodeToGlyphNameMapping = self.makeUnicodeToGlyphNameMapping() if tables is not None: self.tables = tables self.colrLayerReuse = colrLayerReuse self.colrAutoClipBoxes = colrAutoClipBoxes self.colrClipBoxQuantization = colrClipBoxQuantization self.ftConfig = ftConfig or {} # cached values defined later on self._glyphBoundingBoxes = None self._fontBoundingBox = None self._compiledGlyphs = None self._maxComponentDepths = None def compile(self): """ Compile the OpenType binary. """ self.otf = TTFont(sfntVersion=self.sfntVersion, cfg=self.ftConfig) # only compile vertical metrics tables if vhea metrics are defined vertical_metrics = [ "openTypeVheaVertTypoAscender", "openTypeVheaVertTypoDescender", "openTypeVheaVertTypoLineGap", ] self.vertical = all( getAttrWithFallback(self.ufo.info, metric) is not None for metric in vertical_metrics ) self.colorLayers = ( COLOR_LAYERS_KEY in self.ufo.lib and COLOR_PALETTES_KEY in self.ufo.lib ) self.meta = OPENTYPE_META_KEY in self.ufo.lib # write the glyph order self.otf.setGlyphOrder(self.glyphOrder) # populate basic tables self.setupTable_head() self.setupTable_hmtx() self.setupTable_hhea() self.setupTable_name() self.setupTable_maxp() self.setupTable_cmap() self.setupTable_OS2() self.setupTable_post() if self.vertical: self.setupTable_vmtx() self.setupTable_vhea() if self.colorLayers: self.setupTable_COLR() self.setupTable_CPAL() if self.meta: self.setupTable_meta() if any(key.startswith(GLYPHS_MATH_PREFIX) for key in self.ufo.lib): self.setupTable_MATH() self.setupOtherTables() if self.colorLayers and self.colrAutoClipBoxes: self._computeCOLRClipBoxes() self.importTTX() return self.otf def compileGlyphs(self): """Compile glyphs and return dict keyed by glyph name. **This should not be called externally.** Subclasses must override this method to handle compilation of glyphs. """ raise NotImplementedError def getCompiledGlyphs(self): if self._compiledGlyphs is None: self._compiledGlyphs = self.compileGlyphs() return self._compiledGlyphs def makeGlyphsBoundingBoxes(self): """ Make bounding boxes for all the glyphs, and return a dictionary of BoundingBox(xMin, xMax, yMin, yMax) namedtuples keyed by glyph names. The bounding box of empty glyphs (without contours or components) is set to None. The bbox values are integers. **This should not be called externally.** Subclasses must override this method to handle the bounds creation for their specific glyph type. """ raise NotImplementedError @property def glyphBoundingBoxes(self): if self._glyphBoundingBoxes is None: self._glyphBoundingBoxes = self.makeGlyphsBoundingBoxes() return self._glyphBoundingBoxes def makeFontBoundingBox(self): """ Make a bounding box for the font. **This should not be called externally.** Subclasses may override this method to handle the bounds creation in a different way if desired. """ fontBox = None for glyphBox in self.glyphBoundingBoxes.values(): if glyphBox is None: continue if fontBox is None: fontBox = glyphBox else: fontBox = unionRect(fontBox, glyphBox) if fontBox is None: # unlikely fontBox = EMPTY_BOUNDING_BOX return fontBox @property def fontBoundingBox(self): if self._fontBoundingBox is None: self._fontBoundingBox = self.makeFontBoundingBox() return self._fontBoundingBox def makeUnicodeToGlyphNameMapping(self): """ Make a ``unicode : glyph name`` mapping for the font. **This should not be called externally.** Subclasses may override this method to handle the mapping creation in a different way if desired. """ return makeUnicodeToGlyphNameMapping(self.allGlyphs, self.glyphOrder) def makeMissingRequiredGlyphs(self, font, glyphSet, sfntVersion, notdefGlyph=None): """ Add .notdef to the glyph set if it is not present. **This should not be called externally.** Subclasses may override this method to handle the glyph creation in a different way if desired. """ if ".notdef" in glyphSet: return reverseContour = sfntVersion == "\000\001\000\000" if notdefGlyph is not None: notdefGlyph = _copyGlyph(notdefGlyph, reverseContour=reverseContour) else: unitsPerEm = otRound(getAttrWithFallback(font.info, "unitsPerEm")) ascender = otRound(getAttrWithFallback(font.info, "ascender")) descender = otRound(getAttrWithFallback(font.info, "descender")) defaultWidth = otRound(unitsPerEm * 0.5) notdefGlyph = StubGlyph( name=".notdef", width=defaultWidth, unitsPerEm=unitsPerEm, ascender=ascender, descender=descender, reverseContour=reverseContour, ) glyphSet[".notdef"] = notdefGlyph def glyphFactory(self): layer = self.ufo.layers.defaultLayer return _getNewGlyphFactory(layer.instantiateGlyphObject()) def makeOfficialGlyphOrder(self, glyphOrder): """ Make the final glyph order. **This should not be called externally.** Subclasses may override this method to handle the order creation in a different way if desired. """ return makeOfficialGlyphOrder(self.allGlyphs, glyphOrder) # -------------- # Table Builders # -------------- def setupTable_head(self): """ Make the head table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ if "head" not in self.tables: return self.otf["head"] = head = newTable("head") font = self.ufo head.checkSumAdjustment = 0 head.tableVersion = 1.0 head.magicNumber = 0x5F0F3CF5 # version numbers # limit minor version to 3 digits as recommended in OpenType spec: # https://www.microsoft.com/typography/otspec/recom.htm versionMajor = getAttrWithFallback(font.info, "versionMajor") versionMinor = getAttrWithFallback(font.info, "versionMinor") fullFontRevision = float("%d.%03d" % (versionMajor, versionMinor)) head.fontRevision = round(fullFontRevision, 3) if head.fontRevision != fullFontRevision: logger.warning( "Minor version in %s has too many digits and won't fit into " "the head table's fontRevision field; rounded to %s.", fullFontRevision, head.fontRevision, ) # upm head.unitsPerEm = otRound(getAttrWithFallback(font.info, "unitsPerEm")) # times head.created = ( dateStringToTimeValue(getAttrWithFallback(font.info, "openTypeHeadCreated")) - mac_epoch_diff ) head.modified = dateStringToTimeValue(dateStringForNow()) - mac_epoch_diff # bounding box xMin, yMin, xMax, yMax = self.fontBoundingBox head.xMin = otRound(xMin) head.yMin = otRound(yMin) head.xMax = otRound(xMax) head.yMax = otRound(yMax) # style mapping styleMapStyleName = getAttrWithFallback(font.info, "styleMapStyleName") macStyle = [] if styleMapStyleName == "bold": macStyle = [0] elif styleMapStyleName == "bold italic": macStyle = [0, 1] elif styleMapStyleName == "italic": macStyle = [1] head.macStyle = intListToNum(macStyle, 0, 16) # misc head.flags = intListToNum( getAttrWithFallback(font.info, "openTypeHeadFlags"), 0, 16 ) head.lowestRecPPEM = otRound( getAttrWithFallback(font.info, "openTypeHeadLowestRecPPEM") ) head.fontDirectionHint = 2 head.indexToLocFormat = 0 head.glyphDataFormat = getattr(self, "glyphDataFormat", 0) def setupTable_name(self): """ Make the name table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ if "name" not in self.tables: return font = self.ufo self.otf["name"] = name = newTable("name") name.names = [] # Build name records familyName = getAttrWithFallback(font.info, "styleMapFamilyName") styleName = getAttrWithFallback(font.info, "styleMapStyleName").title() preferredFamilyName = getAttrWithFallback( font.info, "openTypeNamePreferredFamilyName" ) preferredSubfamilyName = getAttrWithFallback( font.info, "openTypeNamePreferredSubfamilyName" ) fullName = f"{preferredFamilyName} {preferredSubfamilyName}" nameVals = { 0: getAttrWithFallback(font.info, "copyright"), 1: familyName, 2: styleName, 3: getAttrWithFallback(font.info, "openTypeNameUniqueID"), 4: fullName, 5: getAttrWithFallback(font.info, "openTypeNameVersion"), 6: getAttrWithFallback(font.info, "postscriptFontName"), 7: getAttrWithFallback(font.info, "trademark"), 8: getAttrWithFallback(font.info, "openTypeNameManufacturer"), 9: getAttrWithFallback(font.info, "openTypeNameDesigner"), 10: getAttrWithFallback(font.info, "openTypeNameDescription"), 11: getAttrWithFallback(font.info, "openTypeNameManufacturerURL"), 12: getAttrWithFallback(font.info, "openTypeNameDesignerURL"), 13: getAttrWithFallback(font.info, "openTypeNameLicense"), 14: getAttrWithFallback(font.info, "openTypeNameLicenseURL"), 16: preferredFamilyName, 17: preferredSubfamilyName, 18: getAttrWithFallback(font.info, "openTypeNameCompatibleFullName"), 19: getAttrWithFallback(font.info, "openTypeNameSampleText"), 21: getAttrWithFallback(font.info, "openTypeNameWWSFamilyName"), 22: getAttrWithFallback(font.info, "openTypeNameWWSSubfamilyName"), } # don't add typographic names if *both* are the same as the legacy ones if nameVals[1] == nameVals[16] and nameVals[2] == nameVals[17]: del nameVals[16] del nameVals[17] # postscript font name if nameVals[6]: nameVals[6] = normalizeStringForPostscript(nameVals[6]) for nameId in sorted(nameVals.keys()): nameVal = nameVals[nameId] if not nameVal: continue platformId = 3 platEncId = 10 if _isNonBMP(nameVal) else 1 langId = 0x409 # Set built name record if not set yet if name.getName(nameId, platformId, platEncId, langId): continue name.setName(nameVal, nameId, platformId, platEncId, langId) # Set name records from font.info.openTypeNameRecords for nameRecord in getAttrWithFallback(font.info, "openTypeNameRecords"): nameId = nameRecord["nameID"] platformId = nameRecord["platformID"] platEncId = nameRecord["encodingID"] langId = nameRecord["languageID"] nameVal = nameRecord["string"] name.setName(nameVal, nameId, platformId, platEncId, langId) def setupTable_maxp(self): """ Make the maxp table. **This should not be called externally.** Subclasses must override or supplement this method to handle the table creation for either CFF or TT data. """ raise NotImplementedError def setupTable_cmap(self): """ Make the cmap table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ if "cmap" not in self.tables: return from fontTools.ttLib.tables._c_m_a_p import cmap_format_4 nonBMP = {k: v for k, v in self.unicodeToGlyphNameMapping.items() if k > 65535} if nonBMP: mapping = { k: v for k, v in self.unicodeToGlyphNameMapping.items() if k <= 65535 } else: mapping = dict(self.unicodeToGlyphNameMapping) # mac cmap4_0_3 = cmap_format_4(4) cmap4_0_3.platformID = 0 cmap4_0_3.platEncID = 3 cmap4_0_3.language = 0 cmap4_0_3.cmap = mapping # windows cmap4_3_1 = cmap_format_4(4) cmap4_3_1.platformID = 3 cmap4_3_1.platEncID = 1 cmap4_3_1.language = 0 cmap4_3_1.cmap = mapping # store self.otf["cmap"] = cmap = newTable("cmap") cmap.tableVersion = 0 cmap.tables = [cmap4_0_3, cmap4_3_1] # If we have glyphs outside Unicode BMP, we must set another # subtable that can hold longer codepoints for them. if nonBMP: from fontTools.ttLib.tables._c_m_a_p import cmap_format_12 nonBMP.update(mapping) # mac cmap12_0_4 = cmap_format_12(12) cmap12_0_4.platformID = 0 cmap12_0_4.platEncID = 4 cmap12_0_4.language = 0 cmap12_0_4.cmap = nonBMP # windows cmap12_3_10 = cmap_format_12(12) cmap12_3_10.platformID = 3 cmap12_3_10.platEncID = 10 cmap12_3_10.language = 0 cmap12_3_10.cmap = nonBMP # update tables registry cmap.tables = [cmap4_0_3, cmap4_3_1, cmap12_0_4, cmap12_3_10] # unicode variation sequences uvsMapping = self.ufo.lib.get(UNICODE_VARIATION_SEQUENCES_KEY) if uvsMapping: from fontTools.ttLib.tables._c_m_a_p import cmap_format_14 cmap14_0_5 = cmap_format_14(14) cmap14_0_5.platformID = 0 cmap14_0_5.platEncID = 5 cmap14_0_5.language = 0 cmap14_0_5.cmap = {} if nonBMP: mapping = nonBMP uvsDict = dict() # public.unicodeVariationSequences uses hex strings as keys and # a dict of dicts, while cmap uses ints and a dict of tuples. for hexvs, glyphMapping in uvsMapping.items(): uvsList = [] for hexvalue, glyphName in glyphMapping.items(): value = int(hexvalue, 16) if glyphName == mapping[value]: uvsList.append((value, None)) else: uvsList.append((value, glyphName)) uvsDict[int(hexvs, 16)] = uvsList cmap14_0_5.uvsDict = uvsDict # update tables registry cmap.tables.append(cmap14_0_5) def setupTable_OS2(self): """ Make the OS/2 table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ if "OS/2" not in self.tables: return self.otf["OS/2"] = os2 = newTable("OS/2") font = self.ufo os2.version = 0x0004 # average glyph width os2.recalcAvgCharWidth(self.otf) # weight and width classes os2.usWeightClass = getAttrWithFallback(font.info, "openTypeOS2WeightClass") os2.usWidthClass = getAttrWithFallback(font.info, "openTypeOS2WidthClass") # embedding os2.fsType = intListToNum( getAttrWithFallback(font.info, "openTypeOS2Type"), 0, 16 ) # subscript, superscript, strikeout values, taken from AFDKO: # FDK/Tools/Programs/makeotf/makeotf_lib/source/hotconv/hot.c unitsPerEm = getAttrWithFallback(font.info, "unitsPerEm") italicAngle = float(getAttrWithFallback(font.info, "italicAngle")) xHeight = getAttrWithFallback(font.info, "xHeight") def adjustOffset(offset, angle): """Adjust Y offset based on italic angle, to get X offset.""" return offset * math.tan(math.radians(-angle)) if angle else 0 v = getAttrWithFallback(font.info, "openTypeOS2SubscriptXSize") if v is None: v = unitsPerEm * 0.65 os2.ySubscriptXSize = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptYSize") if v is None: v = unitsPerEm * 0.6 os2.ySubscriptYSize = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptYOffset") if v is None: v = unitsPerEm * 0.075 os2.ySubscriptYOffset = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptXOffset") if v is None: v = adjustOffset(-os2.ySubscriptYOffset, italicAngle) os2.ySubscriptXOffset = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptXSize") if v is None: v = os2.ySubscriptXSize os2.ySuperscriptXSize = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptYSize") if v is None: v = os2.ySubscriptYSize os2.ySuperscriptYSize = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptYOffset") if v is None: v = unitsPerEm * 0.35 os2.ySuperscriptYOffset = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptXOffset") if v is None: v = adjustOffset(os2.ySuperscriptYOffset, italicAngle) os2.ySuperscriptXOffset = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2StrikeoutSize") if v is None: v = getAttrWithFallback(font.info, "postscriptUnderlineThickness") os2.yStrikeoutSize = otRound(v) v = getAttrWithFallback(font.info, "openTypeOS2StrikeoutPosition") if v is None: v = xHeight * 0.6 if xHeight else unitsPerEm * 0.22 os2.yStrikeoutPosition = otRound(v) # family class ibmFontClass, ibmFontSubclass = getAttrWithFallback( font.info, "openTypeOS2FamilyClass" ) os2.sFamilyClass = (ibmFontClass << 8) + ibmFontSubclass # panose data = getAttrWithFallback(font.info, "openTypeOS2Panose") panose = Panose() panose.bFamilyType = data[0] panose.bSerifStyle = data[1] panose.bWeight = data[2] panose.bProportion = data[3] panose.bContrast = data[4] panose.bStrokeVariation = data[5] panose.bArmStyle = data[6] panose.bLetterForm = data[7] panose.bMidline = data[8] panose.bXHeight = data[9] os2.panose = panose # Unicode ranges uniRanges = getAttrWithFallback(font.info, "openTypeOS2UnicodeRanges") if uniRanges is not None: os2.ulUnicodeRange1 = intListToNum(uniRanges, 0, 32) os2.ulUnicodeRange2 = intListToNum(uniRanges, 32, 32) os2.ulUnicodeRange3 = intListToNum(uniRanges, 64, 32) os2.ulUnicodeRange4 = intListToNum(uniRanges, 96, 32) elif "cmap" in self.otf: os2.recalcUnicodeRanges(self.otf) # codepage ranges codepageRanges = getAttrWithFallback(font.info, "openTypeOS2CodePageRanges") if codepageRanges is None: unicodes = self.unicodeToGlyphNameMapping.keys() codepageRanges = calcCodePageRanges(unicodes) os2.ulCodePageRange1 = intListToNum(codepageRanges, 0, 32) os2.ulCodePageRange2 = intListToNum(codepageRanges, 32, 32) # vendor id, padded with spaces if < 4 bytes os2.achVendID = getAttrWithFallback(font.info, "openTypeOS2VendorID").ljust(4) # vertical metrics os2.sxHeight = otRound(getAttrWithFallback(font.info, "xHeight")) os2.sCapHeight = otRound(getAttrWithFallback(font.info, "capHeight")) os2.sTypoAscender = otRound( getAttrWithFallback(font.info, "openTypeOS2TypoAscender") ) os2.sTypoDescender = otRound( getAttrWithFallback(font.info, "openTypeOS2TypoDescender") ) os2.sTypoLineGap = otRound( getAttrWithFallback(font.info, "openTypeOS2TypoLineGap") ) os2.usWinAscent = otRound( getAttrWithFallback(font.info, "openTypeOS2WinAscent") ) os2.usWinDescent = otRound( getAttrWithFallback(font.info, "openTypeOS2WinDescent") ) # style mapping selection = list(getAttrWithFallback(font.info, "openTypeOS2Selection")) styleMapStyleName = getAttrWithFallback(font.info, "styleMapStyleName") if styleMapStyleName == "regular": selection.append(6) elif styleMapStyleName == "bold": selection.append(5) elif styleMapStyleName == "italic": selection.append(0) elif styleMapStyleName == "bold italic": selection += [0, 5] os2.fsSelection = intListToNum(selection, 0, 16) # characetr indexes unicodes = [i for i in self.unicodeToGlyphNameMapping.keys() if i is not None] if unicodes: minIndex = min(unicodes) maxIndex = max(unicodes) else: # the font may have *no* unicode values (it really happens!) so # there needs to be a fallback. use 0xFFFF, as AFDKO does: # FDK/Tools/Programs/makeotf/makeotf_lib/source/hotconv/map.c minIndex = 0xFFFF maxIndex = 0xFFFF if maxIndex > 0xFFFF: # the spec says that 0xFFFF should be used # as the max if the max exceeds 0xFFFF maxIndex = 0xFFFF os2.fsFirstCharIndex = minIndex os2.fsLastCharIndex = maxIndex os2.usBreakChar = 32 os2.usDefaultChar = 0 # maximum contextual lookup length os2.usMaxContex = 0 def setupTable_hmtx(self): """ Make the hmtx table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ if "hmtx" not in self.tables: return self.otf["hmtx"] = hmtx = newTable("hmtx") hmtx.metrics = {} for glyphName, glyph in self.allGlyphs.items(): width = otRound(glyph.width) if width < 0: raise ValueError("The width should not be negative: '%s'" % (glyphName)) bounds = self.glyphBoundingBoxes[glyphName] left = bounds.xMin if bounds else 0 hmtx[glyphName] = (width, left) def _setupTable_hhea_or_vhea(self, tag): """ Make the hhea table or the vhea table. This assume the hmtx or the vmtx were respectively made first. """ if tag not in self.tables: return if tag == "hhea": isHhea = True else: isHhea = False self.otf[tag] = table = newTable(tag) mtxTable = self.otf.get(tag[0] + "mtx") font = self.ufo if isHhea: table.tableVersion = 0x00010000 else: table.tableVersion = 0x00011000 # Vertical metrics in hhea, horizontal metrics in vhea # and caret info. # The hhea metrics names are formed as: # "openType" + tag.title() + "Ascender", etc. # While vhea metrics names are formed as: # "openType" + tag.title() + "VertTypo" + "Ascender", etc. # Caret info names only differ by tag.title(). commonPrefix = "openType%s" % tag.title() if isHhea: metricsPrefix = commonPrefix else: metricsPrefix = "openType%sVertTypo" % tag.title() metricsDict = { "ascent": "%sAscender" % metricsPrefix, "descent": "%sDescender" % metricsPrefix, "lineGap": "%sLineGap" % metricsPrefix, "caretSlopeRise": "%sCaretSlopeRise" % commonPrefix, "caretSlopeRun": "%sCaretSlopeRun" % commonPrefix, "caretOffset": "%sCaretOffset" % commonPrefix, } for otfName, ufoName in metricsDict.items(): setattr(table, otfName, otRound(getAttrWithFallback(font.info, ufoName))) # Horizontal metrics in hhea, vertical metrics in vhea advances = [] # width in hhea, height in vhea firstSideBearings = [] # left in hhea, top in vhea secondSideBearings = [] # right in hhea, bottom in vhea extents = [] if mtxTable is not None: for glyphName in self.glyphOrder: advance, firstSideBearing = mtxTable[glyphName] advances.append(advance) bounds = self.glyphBoundingBoxes[glyphName] if bounds is None: continue if isHhea: boundsAdvance = bounds.xMax - bounds.xMin # equation from the hhea spec for calculating xMaxExtent: # Max(lsb + (xMax - xMin)) extent = firstSideBearing + boundsAdvance else: boundsAdvance = bounds.yMax - bounds.yMin # equation from the vhea spec for calculating yMaxExtent: # Max(tsb + (yMax - yMin)). extent = firstSideBearing + boundsAdvance secondSideBearing = advance - firstSideBearing - boundsAdvance firstSideBearings.append(firstSideBearing) secondSideBearings.append(secondSideBearing) extents.append(extent) setattr( table, "advance%sMax" % ("Width" if isHhea else "Height"), max(advances) if advances else 0, ) setattr( table, "min%sSideBearing" % ("Left" if isHhea else "Top"), min(firstSideBearings) if firstSideBearings else 0, ) setattr( table, "min%sSideBearing" % ("Right" if isHhea else "Bottom"), min(secondSideBearings) if secondSideBearings else 0, ) setattr( table, "%sMaxExtent" % ("x" if isHhea else "y"), max(extents) if extents else 0, ) if isHhea: reserved = range(4) else: # vhea.reserved0 is caretOffset for legacy reasons reserved = range(1, 5) for i in reserved: setattr(table, "reserved%i" % i, 0) table.metricDataFormat = 0 # precompute the number of long{Hor,Ver}Metric records in 'hmtx' table # so we don't need to compile the latter to get this updated numLongMetrics = len(advances) if numLongMetrics > 1: lastAdvance = advances[-1] while advances[numLongMetrics - 2] == lastAdvance: numLongMetrics -= 1 if numLongMetrics <= 1: # all advances are equal numLongMetrics = 1 break setattr(table, "numberOf%sMetrics" % ("H" if isHhea else "V"), numLongMetrics) def setupTable_hhea(self): """ Make the hhea table. This assumes that the hmtx table was made first. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self._setupTable_hhea_or_vhea("hhea") def setupTable_vmtx(self): """ Make the vmtx table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ if "vmtx" not in self.tables: return self.otf["vmtx"] = vmtx = newTable("vmtx") vmtx.metrics = {} for glyphName, glyph in self.allGlyphs.items(): height = otRound(glyph.height) if height < 0: raise ValueError( "The height should not be negative: '%s'" % (glyphName) ) verticalOrigin = _getVerticalOrigin(self.otf, glyph) bounds = self.glyphBoundingBoxes[glyphName] top = bounds.yMax if bounds else 0 vmtx[glyphName] = (height, verticalOrigin - top) def setupTable_VORG(self): """ Make the VORG table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ if "VORG" not in self.tables: return self.otf["VORG"] = vorg = newTable("VORG") vorg.majorVersion = 1 vorg.minorVersion = 0 vorg.VOriginRecords = {} # Find the most frequent verticalOrigin vorg_count = Counter( _getVerticalOrigin(self.otf, glyph) for glyph in self.allGlyphs.values() ) vorg.defaultVertOriginY = vorg_count.most_common(1)[0][0] if len(vorg_count) > 1: for glyphName, glyph in self.allGlyphs.items(): vertOriginY = _getVerticalOrigin(self.otf, glyph) if vertOriginY == vorg.defaultVertOriginY: continue vorg.VOriginRecords[glyphName] = vertOriginY vorg.numVertOriginYMetrics = len(vorg.VOriginRecords) def setupTable_vhea(self): """ Make the vhea table. This assumes that the head and vmtx tables were made first. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self._setupTable_hhea_or_vhea("vhea") def setupTable_post(self): """ Make the post table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ if "post" not in self.tables: return self.otf["post"] = post = newTable("post") font = self.ufo post.formatType = 3.0 # italic angle italicAngle = float(getAttrWithFallback(font.info, "italicAngle")) post.italicAngle = italicAngle # underline if OPENTYPE_POST_UNDERLINE_POSITION_KEY in font.lib: underlinePosition = font.lib[OPENTYPE_POST_UNDERLINE_POSITION_KEY] else: underlinePosition = getAttrWithFallback( font.info, "postscriptUnderlinePosition" ) post.underlinePosition = otRound(underlinePosition) underlineThickness = getAttrWithFallback( font.info, "postscriptUnderlineThickness" ) post.underlineThickness = otRound(underlineThickness) post.isFixedPitch = int( getAttrWithFallback(font.info, "postscriptIsFixedPitch") ) # misc post.minMemType42 = 0 post.maxMemType42 = 0 post.minMemType1 = 0 post.maxMemType1 = 0 def setupTable_COLR(self): """ Compile the COLR table. **This should not be called externally.** """ if "COLR" not in self.tables: return from fontTools.colorLib.builder import buildCOLR layerInfo = self.ufo.lib[COLOR_LAYERS_KEY] glyphMap = self.otf.getReverseGlyphMap() if layerInfo: # unpack (glyphs, clipBox) tuples to a flat dict keyed by glyph name, # as colorLib buildCOLR expects clipBoxes = { glyphName: tuple(box) for glyphs, box in self.ufo.lib.get(COLR_CLIP_BOXES_KEY, ()) for glyphName in glyphs } self.otf["COLR"] = buildCOLR( layerInfo, glyphMap=glyphMap, clipBoxes=clipBoxes, allowLayerReuse=self.colrLayerReuse, ) def _computeCOLRClipBoxes(self): if ( "COLR" not in self.otf or self.otf["COLR"].version == 0 or self.otf["COLR"].table.ClipList is not None ): return q = self.colrClipBoxQuantization(self.ufo) self.otf["COLR"].table.computeClipBoxes(self.otf.getGlyphSet(), quantization=q) def setupTable_CPAL(self): """ Compile the CPAL table. **This should not be called externally.** """ if "CPAL" not in self.tables: return from fontTools.colorLib.builder import buildCPAL from fontTools.colorLib.errors import ColorLibError # colorLib wants colors as tuples, plistlib gives us lists palettes = [ [tuple(color) for color in palette] for palette in self.ufo.lib[COLOR_PALETTES_KEY] ] try: self.otf["CPAL"] = buildCPAL(palettes) except ColorLibError as e: raise InvalidFontData("Failed to build CPAL table") from e def setupTable_meta(self): """ Make the meta table. ***This should not be called externally.** Sublcasses may override or supplement this method to handle the table creation in a different way if desired. """ if "meta" not in self.tables: return font = self.ufo self.otf["meta"] = meta = newTable("meta") ufo_meta = font.lib.get(OPENTYPE_META_KEY) for key, value in ufo_meta.items(): if key in ["dlng", "slng"]: if not isinstance(value, list) or not all( isinstance(string, str) for string in value ): raise TypeError( f"public.openTypeMeta '{key}' value should " "be a list of strings" ) meta.data[key] = ",".join(value) elif key in ["appl", "bild"]: if not isinstance(value, bytes): raise TypeError( f"public.openTypeMeta '{key}' value should be bytes." ) meta.data[key] = value elif isinstance(value, bytes): meta.data[key] = value elif isinstance(value, str): meta.data[key] = value.encode("utf-8") else: raise TypeError( f"public.openTypeMeta '{key}' value should be bytes or a string." ) def _bboxWidth(self, glyph): bbox = self.glyphBoundingBoxes[glyph] if bbox is None: return 0 return bbox.xMax - bbox.xMin def _bboxHeight(self, glyph): bbox = self.glyphBoundingBoxes[glyph] if bbox is None: return 0 return bbox.yMax - bbox.yMin def setupTable_MATH(self): """ This builds MATH table based on data in the UFO font. The data is stored either in private font/glyph lib keys or as glyph anchors with specific names. The data is based on GlyphsApp MATH plugin data as written out by glyphsLib to the UFO font. The font lib keys are: - com.nagwa.MATHPlugin.constants: a dictionary of MATH constants as expected by fontTools.otlLib.builder.buildMathTable(). Example: ufo.lib["com.nagwa.MATHPlugin.constants"] = { "ScriptPercentScaleDown": 70, "ScriptScriptPercentScaleDown": 60, ... } - com.nagwa.MATHPlugin.extendedShape: a list of glyph names that are extended shapes. Example: ufo.lib["com.nagwa.MATHPlugin.extendedShapes"] = ["integral", "radical"] The glyph lib keys are: - com.nagwa.MATHPlugin.variants: a dictionary of MATH glyph variants keyed by glyph names, and each value is a dictionary with keys "hVariants", "vVariants", "hAssembly", and "vAssembly". Example: ufo["braceleft"].lib["com.nagwa.MATHPlugin.variants"] = { "vVariants": ["braceleft", "braceleft.s1", "braceleft.s2"], "vAssembly": [ # glyph name, flags, start connector length, end connector length ["braceleft.bottom", 0, 0, 200], ["braceleft.extender", 1, 200, 200], ["braceleft.middle", 0, 100, 100], ["braceleft.extender", 1, 200, 200], ["braceleft.top", 0, 200, 0], ], } The anchors are: - math.ic: italic correction anchor - math.ta: top accent attachment anchor - math.tr*: top right kerning anchors - math.tl*: top left kerning anchors - math.br*: bottom right kerning anchors - math.bl*: bottom left kerning anchors """ if "MATH" not in self.tables: return from fontTools.otlLib.builder import buildMathTable ufo = self.ufo constants = ufo.lib.get(GLYPHS_MATH_CONSTANTS_KEY) min_connector_overlap = constants.pop("MinConnectorOverlap", 0) italics_correction = {} top_accent_attachment = {} math_kerns = {} kerning_sides = { "tr": "TopRight", "tl": "TopLeft", "br": "BottomRight", "bl": "BottomLeft", } for name, glyph in self.allGlyphs.items(): kerns = {} for anchor in glyph.anchors: if anchor.name == "math.ic": # The anchor x position is absolute, but we want # a value relative to the glyph's width. italics_correction[name] = anchor.x - glyph.width if anchor.name == "math.ta": top_accent_attachment[name] = anchor.x for aName in kerning_sides.keys(): if anchor.name.startswith(f"math.{aName}"): side = kerning_sides[aName] # The anchor x positions are absolute, but we want # values relative to the glyph's width/origin. x, y = anchor.x, anchor.y if side.endswith("Right"): x -= glyph.width elif side.endswith("Left"): x = -x kerns.setdefault(side, []).append([x, y]) if kerns: math_kerns[name] = {} # Convert anchor positions to correction heights and kern # values. for side, pts in kerns.items(): pts = sorted(pts, key=lambda pt: pt[1]) # Y positions, the last one is ignored as the last kern # value is applied to all heights greater than the last one. correctionHeights = [pt[1] for pt in pts[:-1]] # X positions kernValues = [pt[0] for pt in pts] math_kerns[name][side] = (correctionHeights, kernValues) # buildMathTable takes two dictionaries of glyph variants, one for # horizontal variants and one for vertical variants, and items are # tuples of glyph name and the advance width/height of the variant. # Here we convert the UFO data to the expected format and measure the # advances. h_variants = {} v_variants = {} # It also takes two dictionaries of glyph assemblies, one for # horizontal assemblies and one for vertical assemblies, and items are # lists of tuples of assembly parts and italics correction, and the # assembly part includes the advance width/height of the part. Here we # convert the UFO data to the expected format and measure the advances. h_assemblies = {} v_assemblies = {} for name, glyph in self.allGlyphs.items(): if GLYPHS_MATH_VARIANTS_KEY in glyph.lib: variants = glyph.lib[GLYPHS_MATH_VARIANTS_KEY] if names := variants.get("hVariants"): h_variants[name] = [(n, self._bboxWidth(n)) for n in names] if names := variants.get("vVariants"): v_variants[name] = [(n, self._bboxHeight(n)) for n in names] if parts := variants.get("hAssembly"): if not all(len(part) == 4 for part in parts): raise InvalidFontData("Invalid assembly") h_assemblies[name] = ( [(*part, self._bboxWidth(part[0])) for part in parts], # If the last part has italic correction, we use it as # the assembly's. italics_correction.pop(parts[-1][0], 0), ) if parts := variants.get("vAssembly"): if not all(len(part) == 4 for part in parts): raise InvalidFontData("Invalid assembly") v_assemblies[name] = ( [(*part, self._bboxHeight(part[0])) for part in parts], # If the last part has italic correction, we use it as # the assembly's. italics_correction.pop(parts[-1][0], 0), ) # Collect the set of extended shapes, and if a shape has vertical # variants, add the variants to the set. extended_shapes = set(ufo.lib.get(GLYPHS_MATH_EXTENDED_SHAPE_KEY, [])) for name, variants in v_variants.items(): if name in extended_shapes: extended_shapes.update(v[0] for v in variants) buildMathTable( self.otf, constants=constants, italicsCorrections=italics_correction, topAccentAttachments=top_accent_attachment, extendedShapes=extended_shapes, mathKerns=math_kerns, minConnectorOverlap=min_connector_overlap, vertGlyphVariants=v_variants, horizGlyphVariants=h_variants, vertGlyphAssembly=v_assemblies, horizGlyphAssembly=h_assemblies, ) def setupOtherTables(self): """ Make the other tables. The default implementation does nothing. **This should not be called externally.** Subclasses may override this method to add other tables to the font if desired. """ pass def importTTX(self): """ Merge TTX files from data directory "com.github.fonttools.ttx" **This should not be called externally.** Subclasses may override this method to handle the bounds creation in a different way if desired. """ import os prefix = "com.github.fonttools.ttx" if not hasattr(self.ufo, "data"): return if not self.ufo.data.fileNames: return for path in self.ufo.data.fileNames: foldername, filename = os.path.split(path) if foldername == prefix and filename.endswith(".ttx"): ttx = self.ufo.data[path].decode("utf-8") fp = BytesIO(ttx.encode("utf-8")) # Preserve the original SFNT version when loading a TTX dump. sfntVersion = self.otf.sfntVersion try: self.otf.importXML(fp) finally: self.otf.sfntVersion = sfntVersion class OutlineOTFCompiler(BaseOutlineCompiler): """Compile a .otf font with CFF outlines.""" sfntVersion = "OTTO" tables = BaseOutlineCompiler.tables | {"CFF", "VORG"} def __init__( self, font, glyphSet=None, glyphOrder=None, tables=None, notdefGlyph=None, roundTolerance=None, optimizeCFF=True, colrLayerReuse=True, colrAutoClipBoxes=True, colrClipBoxQuantization=colrClipBoxQuantization, ftConfig=None, *, compilingVFDefaultSource=True, ): if roundTolerance is not None: self.roundTolerance = float(roundTolerance) else: # round all coordinates to integers by default self.roundTolerance = 0.5 super().__init__( font, glyphSet=glyphSet, glyphOrder=glyphOrder, tables=tables, notdefGlyph=notdefGlyph, colrLayerReuse=colrLayerReuse, colrAutoClipBoxes=colrAutoClipBoxes, colrClipBoxQuantization=colrClipBoxQuantization, ftConfig=ftConfig, compilingVFDefaultSource=compilingVFDefaultSource, ) if not isinstance(optimizeCFF, bool): optimizeCFF = optimizeCFF >= CFFOptimization.SPECIALIZE self.optimizeCFF = optimizeCFF self._defaultAndNominalWidths = None def getDefaultAndNominalWidths(self): """Return (defaultWidthX, nominalWidthX). If fontinfo.plist doesn't define these explicitly, compute optimal values from the glyphs' advance widths. """ if self._defaultAndNominalWidths is None: info = self.ufo.info # populate the width values if all( getattr(info, attr, None) is None for attr in ("postscriptDefaultWidthX", "postscriptNominalWidthX") ): # no custom values set in fontinfo.plist; compute optimal ones from fontTools.cffLib.width import optimizeWidths widths = [otRound(glyph.width) for glyph in self.allGlyphs.values()] defaultWidthX, nominalWidthX = optimizeWidths(widths) else: defaultWidthX = otRound( getAttrWithFallback(info, "postscriptDefaultWidthX") ) nominalWidthX = otRound( getAttrWithFallback(info, "postscriptNominalWidthX") ) self._defaultAndNominalWidths = (defaultWidthX, nominalWidthX) return self._defaultAndNominalWidths def compileGlyphs(self): """Compile and return the CFF T2CharStrings for this font.""" defaultWidth, nominalWidth = self.getDefaultAndNominalWidths() # The real PrivateDict will be created later on in setupTable_CFF. # For convenience here we use a namespace object to pass the default/nominal # widths that we need to draw the charstrings when computing their bounds. private = SimpleNamespace( defaultWidthX=defaultWidth, nominalWidthX=nominalWidth ) compiledGlyphs = {} for glyphName in self.glyphOrder: glyph = self.allGlyphs[glyphName] cs = self.getCharStringForGlyph(glyph, private) compiledGlyphs[glyphName] = cs return compiledGlyphs def makeGlyphsBoundingBoxes(self): """ Make bounding boxes for all the glyphs, and return a dictionary of BoundingBox(xMin, xMax, yMin, yMax) namedtuples keyed by glyph names. The bounding box of empty glyphs (without contours or components) is set to None. Check that the float values are within the range of the specified self.roundTolerance, and if so use the rounded value; else take the floor or ceiling to ensure that the bounding box encloses the original values. """ def toInt(value, else_callback): rounded = otRound(value) if tolerance >= 0.5 or abs(rounded - value) <= tolerance: return rounded else: return int(else_callback(value)) tolerance = self.roundTolerance glyphBoxes = {} charStrings = self.getCompiledGlyphs() for name, cs in charStrings.items(): bounds = cs.calcBounds(charStrings) if bounds is not None: rounded = [] for value in bounds[:2]: rounded.append(toInt(value, math.floor)) for value in bounds[2:]: rounded.append(toInt(value, math.ceil)) bounds = BoundingBox(*rounded) if bounds == EMPTY_BOUNDING_BOX: bounds = None glyphBoxes[name] = bounds return glyphBoxes def getCharStringForGlyph(self, glyph, private, globalSubrs=None): """ Get a Type2CharString for the *glyph* **This should not be called externally.** Subclasses may override this method to handle the charstring creation in a different way if desired. """ width = glyph.width defaultWidth = private.defaultWidthX nominalWidth = private.nominalWidthX if width == defaultWidth: # if width equals the default it can be omitted from charstring width = None else: # subtract the nominal width width -= nominalWidth if width is not None: width = otRound(width) pen = T2CharStringPen(width, self.allGlyphs, roundTolerance=self.roundTolerance) glyph.draw(pen) charString = pen.getCharString(private, globalSubrs, optimize=self.optimizeCFF) return charString def setupTable_maxp(self): """Make the maxp table.""" if "maxp" not in self.tables: return self.otf["maxp"] = maxp = newTable("maxp") maxp.tableVersion = 0x00005000 maxp.numGlyphs = len(self.glyphOrder) def setupOtherTables(self): self.setupTable_CFF() if self.vertical: self.setupTable_VORG() def setupTable_CFF(self): """Make the CFF table.""" if not {"CFF", "CFF "}.intersection(self.tables): return self.otf["CFF "] = cff = newTable("CFF ") cff = cff.cff # NOTE: Set up a back-reference to be used by some CFFFontSet methods # down the line (as of fontTools 4.21.1). cff.otFont = self.otf # set up the basics cff.major = 1 cff.minor = 0 cff.hdrSize = 4 cff.offSize = 4 cff.fontNames = [] strings = IndexedStrings() cff.strings = strings private = PrivateDict(strings=strings) private.rawDict.update(private.defaults) globalSubrs = GlobalSubrsIndex(private=private) topDict = TopDict(GlobalSubrs=globalSubrs, strings=strings) topDict.Private = private charStrings = topDict.CharStrings = CharStrings( file=None, charset=None, globalSubrs=globalSubrs, private=private, fdSelect=None, fdArray=None, ) charStrings.charStringsAreIndexed = True topDict.charset = [] charStringsIndex = charStrings.charStringsIndex = SubrsIndex( private=private, globalSubrs=globalSubrs ) cff.topDictIndex = topDictIndex = TopDictIndex() topDictIndex.append(topDict) topDictIndex.strings = strings cff.GlobalSubrs = globalSubrs # populate naming data info = self.ufo.info psName = getAttrWithFallback(info, "postscriptFontName") cff.fontNames.append(psName) topDict = cff.topDictIndex[0] topDict.version = "%d.%d" % ( getAttrWithFallback(info, "versionMajor"), getAttrWithFallback(info, "versionMinor"), ) trademark = getAttrWithFallback(info, "trademark") if trademark: trademark = normalizeStringForPostscript( trademark.replace("\u00A9", "Copyright") ) if trademark != self.ufo.info.trademark: logger.info( "The trademark was normalized for storage in the " "CFF table and consequently some characters were " "dropped: '%s'", trademark, ) if trademark is None: trademark = "" topDict.Notice = trademark copyright = getAttrWithFallback(info, "copyright") if copyright: copyright = normalizeStringForPostscript( copyright.replace("\u00A9", "Copyright") ) if copyright != self.ufo.info.copyright: logger.info( "The copyright was normalized for storage in the " "CFF table and consequently some characters were " "dropped: '%s'", copyright, ) if copyright is None: copyright = "" topDict.Copyright = copyright topDict.FullName = getAttrWithFallback(info, "postscriptFullName") topDict.FamilyName = getAttrWithFallback( info, "openTypeNamePreferredFamilyName" ) topDict.Weight = getAttrWithFallback(info, "postscriptWeightName") # populate various numbers topDict.isFixedPitch = int(getAttrWithFallback(info, "postscriptIsFixedPitch")) topDict.ItalicAngle = float(getAttrWithFallback(info, "italicAngle")) if ( OPENTYPE_POST_UNDERLINE_POSITION_KEY in self.ufo.lib and info.postscriptUnderlinePosition is None ): underlinePosition = ( self.ufo.lib[OPENTYPE_POST_UNDERLINE_POSITION_KEY] - getAttrWithFallback(info, "postscriptUnderlineThickness") / 2 ) else: underlinePosition = getAttrWithFallback(info, "postscriptUnderlinePosition") topDict.UnderlinePosition = otRound(underlinePosition) underlineThickness = getAttrWithFallback(info, "postscriptUnderlineThickness") topDict.UnderlineThickness = otRound(underlineThickness) # populate font matrix unitsPerEm = otRound(getAttrWithFallback(info, "unitsPerEm")) topDict.FontMatrix = [1.0 / unitsPerEm, 0, 0, 1.0 / unitsPerEm, 0, 0] # populate the width values defaultWidthX, nominalWidthX = self.getDefaultAndNominalWidths() if defaultWidthX: private.rawDict["defaultWidthX"] = defaultWidthX if nominalWidthX: private.rawDict["nominalWidthX"] = nominalWidthX # populate hint data blueFuzz = otRound(getAttrWithFallback(info, "postscriptBlueFuzz")) blueShift = otRound(getAttrWithFallback(info, "postscriptBlueShift")) blueScale = getAttrWithFallback(info, "postscriptBlueScale") forceBold = getAttrWithFallback(info, "postscriptForceBold") blueValues = getAttrWithFallback(info, "postscriptBlueValues") if isinstance(blueValues, list): blueValues = [otRound(i) for i in blueValues] otherBlues = getAttrWithFallback(info, "postscriptOtherBlues") if isinstance(otherBlues, list): otherBlues = [otRound(i) for i in otherBlues] familyBlues = getAttrWithFallback(info, "postscriptFamilyBlues") if isinstance(familyBlues, list): familyBlues = [otRound(i) for i in familyBlues] familyOtherBlues = getAttrWithFallback(info, "postscriptFamilyOtherBlues") if isinstance(familyOtherBlues, list): familyOtherBlues = [otRound(i) for i in familyOtherBlues] stemSnapH = getAttrWithFallback(info, "postscriptStemSnapH") if isinstance(stemSnapH, list): stemSnapH = [otRound(i) for i in stemSnapH] stemSnapV = getAttrWithFallback(info, "postscriptStemSnapV") if isinstance(stemSnapV, list): stemSnapV = [otRound(i) for i in stemSnapV] # only write the blues data if some blues are defined. if any((blueValues, otherBlues, familyBlues, familyOtherBlues)): private.rawDict["BlueFuzz"] = blueFuzz private.rawDict["BlueShift"] = blueShift private.rawDict["BlueScale"] = blueScale private.rawDict["ForceBold"] = forceBold if blueValues: private.rawDict["BlueValues"] = blueValues if otherBlues: private.rawDict["OtherBlues"] = otherBlues if familyBlues: private.rawDict["FamilyBlues"] = familyBlues if familyOtherBlues: private.rawDict["FamilyOtherBlues"] = familyOtherBlues # only write the stems if both are defined. if stemSnapH and stemSnapV: private.rawDict["StemSnapH"] = stemSnapH private.rawDict["StdHW"] = stemSnapH[0] private.rawDict["StemSnapV"] = stemSnapV private.rawDict["StdVW"] = stemSnapV[0] # populate glyphs cffGlyphs = self.getCompiledGlyphs() for glyphName in self.glyphOrder: charString = cffGlyphs[glyphName] charString.private = private charString.globalSubrs = globalSubrs # add to the font if glyphName in charStrings: # XXX a glyph already has this name. should we choke? glyphID = charStrings.charStrings[glyphName] charStringsIndex.items[glyphID] = charString else: charStringsIndex.append(charString) glyphID = len(topDict.charset) charStrings.charStrings[glyphName] = glyphID topDict.charset.append(glyphName) topDict.FontBBox = self.fontBoundingBox class OutlineTTFCompiler(BaseOutlineCompiler): """Compile a .ttf font with TrueType outlines.""" sfntVersion = "\000\001\000\000" tables = BaseOutlineCompiler.tables | { "cvt ", "fpgm", "gasp", "glyf", "loca", "prep", } def __init__( self, font, glyphSet=None, glyphOrder=None, tables=None, notdefGlyph=None, colrLayerReuse=True, colrAutoClipBoxes=True, colrClipBoxQuantization=colrClipBoxQuantization, dropImpliedOnCurves=False, autoUseMyMetrics=True, roundCoordinates=True, glyphDataFormat=0, ftConfig=None, *, compilingVFDefaultSource=True, ): super().__init__( font, glyphSet=glyphSet, glyphOrder=glyphOrder, tables=tables, notdefGlyph=notdefGlyph, colrLayerReuse=colrLayerReuse, colrAutoClipBoxes=colrAutoClipBoxes, colrClipBoxQuantization=colrClipBoxQuantization, ftConfig=ftConfig, compilingVFDefaultSource=compilingVFDefaultSource, ) self.autoUseMyMetrics = autoUseMyMetrics self.dropImpliedOnCurves = dropImpliedOnCurves self.roundCoordinates = roundCoordinates self.glyphDataFormat = glyphDataFormat def makeMissingRequiredGlyphs(self, font, glyphSet, sfntVersion, notdefGlyph=None): """ Add .notdef to the glyph set if it is not present. When compiling non-default interpolatable master TTFs used to build a VF, if any 'sparse' composite glyphs reference missing components, we add empty base glyphs so that the master TTFs' glyf table will keep the composites; varLib will ignores these empty glyphs when building variations. """ super().makeMissingRequiredGlyphs(font, glyphSet, sfntVersion, notdefGlyph) if not self.compilingVFDefaultSource: newGlyph = self.glyphFactory() for glyphName in list(glyphSet.keys()): glyph = glyphSet[glyphName] for comp in glyph.components: if comp.baseGlyph not in glyphSet: logger.info( "Added missing '%s' component base glyph, referenced from '%s'", comp.baseGlyph, glyphName, ) # use sentinel value for width/height to signal varLib this glyph # doesn't participate in {H,V}VAR glyph metrics variations glyphSet[comp.baseGlyph] = newGlyph( comp.baseGlyph, width=0xFFFF, height=0xFFFF ) def compileGlyphs(self): """Compile and return the TrueType glyphs for this font.""" allGlyphs = self.allGlyphs ttGlyphs = {} round = otRound if self.roundCoordinates else noRound glyphDataFormat = self.glyphDataFormat for name in self.glyphOrder: glyph = allGlyphs[name] pen = TTGlyphPointPen(allGlyphs) try: glyph.drawPoints(pen) except NotImplementedError: logger.error("%r has invalid curve format; skipped", name) ttGlyph = Glyph() else: ttGlyph = pen.glyph( dropImpliedOnCurves=self.dropImpliedOnCurves, round=round, ) if ( glyphDataFormat == 0 and ttGlyph.numberOfContours > 0 and any(f & flagCubic for f in ttGlyph.flags) ): raise ValueError( f"{name!r} has cubic Bezier curves, but glyphDataFormat=0; " "either convert to quadratic (convertCubics=True) or use " "allQuadratic=False so that glyphDataFormat=1." ) ttGlyphs[name] = ttGlyph return ttGlyphs def makeGlyphsBoundingBoxes(self): """Make bounding boxes for all the glyphs. Return a dictionary of BoundingBox(xMin, xMax, yMin, yMax) namedtuples keyed by glyph names. The bounding box of empty glyphs (without contours or components) is set to None. """ glyphBoxes = {} ttGlyphs = self.getCompiledGlyphs() for glyphName, glyph in ttGlyphs.items(): glyph.recalcBounds(ttGlyphs) bounds = BoundingBox(glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax) if bounds == EMPTY_BOUNDING_BOX: bounds = None 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: return self.otf["maxp"] = maxp = newTable("maxp") maxp.tableVersion = 0x00010000 maxp.numGlyphs = len(self.glyphOrder) maxp.maxZones = 1 maxp.maxTwilightPoints = 0 maxp.maxStorage = 0 maxp.maxFunctionDefs = 0 maxp.maxInstructionDefs = 0 maxp.maxStackElements = 0 maxp.maxSizeOfInstructions = 0 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.""" super().setupTable_post() if "post" not in self.otf: return post = self.otf["post"] post.formatType = 2.0 # if we set extraNames = [], it will be automatically computed upon compile as # we do below; if we do it upfront we can skip reloading in postProcessor. post.extraNames = [g for g in self.glyphOrder if g not in standardGlyphOrder] post.mapping = {} post.glyphOrder = self.glyphOrder def setupOtherTables(self): self.instructionCompiler = InstructionCompiler( self.ufo, self.otf, autoUseMyMetrics=self.autoUseMyMetrics ) self.setupTable_glyf() 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.""" if not {"glyf", "loca"}.issubset(self.tables): return self.otf["loca"] = newTable("loca") self.otf["glyf"] = glyf = newTable("glyf") glyf.glyphs = {} glyf.glyphOrder = self.glyphOrder ttGlyphs = self.getCompiledGlyphs() # 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] 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) # 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: """ This object will be used to create missing glyphs (specifically .notdef) in the provided UFO. """ def __init__( self, name, width, unitsPerEm, ascender, descender, unicodes=None, reverseContour=False, ): self.name = name self.width = width self.unitsPerEm = unitsPerEm self.ascender = ascender self.descender = descender self.unicodes = unicodes if unicodes is not None else [] self.components = [] self.anchors = [] if self.unicodes: self.unicode = self.unicodes[0] else: self.unicode = None if name == ".notdef": self.draw = self._drawDefaultNotdef self.drawPoints = self._drawDefaultNotdefPoints self.reverseContour = reverseContour self.lib = {} def __len__(self): if self.name == ".notdef": return 1 return 0 @property def height(self): return self.ascender - self.descender def draw(self, pen): pass def drawPoints(self, pen): pass def _drawDefaultNotdef(self, pen): # Draw contour in PostScript direction (counter-clockwise) by default. Reverse # for TrueType. if self.reverseContour: pen = ReverseContourPen(pen) width = otRound(self.unitsPerEm * 0.5) stroke = otRound(self.unitsPerEm * 0.05) ascender = self.ascender descender = self.descender xMin = stroke xMax = width - stroke yMax = ascender yMin = descender pen.moveTo((xMin, yMin)) pen.lineTo((xMax, yMin)) pen.lineTo((xMax, yMax)) pen.lineTo((xMin, yMax)) pen.lineTo((xMin, yMin)) pen.closePath() xMin += stroke xMax -= stroke yMax -= stroke yMin += stroke pen.moveTo((xMin, yMin)) pen.lineTo((xMin, yMax)) pen.lineTo((xMax, yMax)) pen.lineTo((xMax, yMin)) pen.lineTo((xMin, yMin)) pen.closePath() def _drawDefaultNotdefPoints(self, pen): adapterPen = SegmentToPointPen(pen, guessSmooth=False) self.draw(adapterPen) def _get_controlPointBounds(self): pen = ControlBoundsPen(None) self.draw(pen) return pen.bounds controlPointBounds = property(_get_controlPointBounds) ufo2ft-3.3.1/Lib/ufo2ft/postProcessor.py000066400000000000000000000376231470175262700201230ustar00rootroot00000000000000import enum import logging import re from io import BytesIO from fontTools.ttLib import TTFont from fontTools.ttLib.standardGlyphOrder import standardGlyphOrder from ufo2ft.constants import ( GLYPHS_DONT_USE_PRODUCTION_NAMES, KEEP_GLYPH_NAMES, USE_PRODUCTION_NAMES, CFFOptimization, ) logger = logging.getLogger(__name__) class CFFVersion(enum.IntEnum): CFF = 1 CFF2 = 2 class PostProcessor: """Does some post-processing operations on a compiled OpenType font, using info from the source UFO where necessary. """ GLYPH_NAME_INVALID_CHARS = re.compile("[^0-9a-zA-Z_.]") MAX_GLYPH_NAME_LENGTH = 63 class SubroutinizerBackend(enum.Enum): COMPREFFOR = "compreffor" CFFSUBR = "cffsubr" # can override by passing explicit subroutinizer parameter to process method DEFAULT_SUBROUTINIZER_FOR_CFF_VERSION = { 1: SubroutinizerBackend.CFFSUBR, 2: SubroutinizerBackend.CFFSUBR, } def __init__(self, otf, ufo, glyphSet=None, info=None): self.ufo = ufo self.glyphSet = glyphSet if glyphSet is not None else ufo self.info = info self.otf = otf self._postscriptNames = ufo.lib.get("public.postscriptNames") def process( self, useProductionNames=None, optimizeCFF=True, cffVersion=None, subroutinizer=None, ): """ useProductionNames (Optional[bool]): By default, when value is None, this will rename glyphs using the 'public.postscriptNames' in then UFO lib. If the mapping is not present, no glyph names are renamed. When useProductionNames is None and the UFO lib contains the plist bool key "com.github.googlei18n.ufo2ft.keepGlyphNames" set to False, then the 'post' table is set to format 3.0 and glyph names are dropped altogether from the font, saving a few KBs. Note that this only works for TTF and CFF2 flavored fonts. We currently do not support dropping glyph names from CFF 1.0 fonts. When the keepGlyphNames lib key is missing or set to True, the glyph names will be stored in 'post' table format 2.0 for TTF and CFF2 fonts, or in the CFF charset. If useProductionNames is False, no glyphs are renamed whether or not the 'public.postscriptNames' mapping is present. If the value is True, but no 'public.postscriptNames' are present, then uniXXXX names are generated from the glyphs' unicode. The 'com.github.googlei18n.ufo2ft.useProductionNames' key can be set in the UFO lib to control this parameter (plist boolean value). For legacy reasons, an alias key (with an inverted meaning) is also supported: "com.schriftgestaltung.Don't use Production Names"; when this is present if the UFO lib and is set to True, this is equivalent to 'useProductionNames' set to False. optimizeCFF (bool | CFFOptimization): If True or >= CFFOptimization.SUBROUTINIZE, subroubtinize CFF or CFF2 table (if present). cffVersion (Optiona[int]): The output CFF format, choose between 1 or 2. By default, it's the same as as the input OTF's CFF or CFF2 table, if any. Ignored for TTFs. subroutinizer (Optional[str]): The name of the library to use for compressing CFF charstrings, if optimizeCFF is True and CFF or CFF2 table is present. Choose between "cffsubr" or "compreffor". By default "cffsubr" is used for both CFF 1 and CFF 2. NOTE: compreffor currently doesn't support input fonts with CFF2 table. """ if self._get_cff_version(self.otf): if not isinstance(optimizeCFF, bool): optimizeCFF = optimizeCFF >= CFFOptimization.SUBROUTINIZE self.process_cff( optimizeCFF=optimizeCFF, cffVersion=cffVersion, subroutinizer=subroutinizer, ) self.process_glyph_names(useProductionNames) if self.info: self.apply_fontinfo() return self.otf def process_cff(self, *, optimizeCFF=True, cffVersion=None, subroutinizer=None): cffInputVersion = self._get_cff_version(self.otf) if not cffInputVersion: raise ValueError("Missing required 'CFF ' or 'CFF2' table") if cffVersion is None: cffOutputVersion = cffInputVersion else: cffOutputVersion = CFFVersion(cffVersion) if optimizeCFF: if subroutinizer is None: backend = self.DEFAULT_SUBROUTINIZER_FOR_CFF_VERSION[cffOutputVersion] else: backend = self.SubroutinizerBackend(subroutinizer) self._subroutinize(backend, self.otf, cffOutputVersion) elif cffInputVersion != cffOutputVersion: if ( cffInputVersion == CFFVersion.CFF and cffOutputVersion == CFFVersion.CFF2 ): self._convert_cff_to_cff2(self.otf) else: raise NotImplementedError( "Unsupported CFF conversion {cffInputVersion} => {cffOutputVersion}" ) def process_glyph_names(self, useProductionNames=None): if useProductionNames is None: keepGlyphNames = self.ufo.lib.get(KEEP_GLYPH_NAMES, True) useProductionNames = self.ufo.lib.get( USE_PRODUCTION_NAMES, not self.ufo.lib.get(GLYPHS_DONT_USE_PRODUCTION_NAMES) and self._postscriptNames is not None, ) else: keepGlyphNames = True if keepGlyphNames: if "CFF " not in self.otf: self.set_post_table_format(self.otf, 2.0) if useProductionNames: logger.info("Renaming glyphs to final production names") # We need to reload the font *before* renaming glyphs, since various # tables may have been build/loaded using the original glyph names. # After reloading, we can immediately set a new glyph order and update # the tables (post or CFF) that stores the new postcript names; any # other tables that get loaded subsequently will use the new glyph names. self.otf = _reloadFont(self.otf) self._rename_glyphs_from_ufo() else: if "CFF " in self.otf: logger.warning( "Dropping glyph names from CFF 1.0 is currently unsupported" ) else: # To drop glyph names from TTF or CFF2, we must reload the font *after* # setting the post format to 3.0, since other tables may still use # the old glyph names. self.set_post_table_format(self.otf, 3.0) self.otf = _reloadFont(self.otf) def _rename_glyphs_from_ufo(self): """Rename glyphs using ufo.lib.public.postscriptNames in UFO.""" rename_map = self._build_production_names() self.rename_glyphs(self.otf, rename_map) @staticmethod def rename_glyphs(otf, rename_map): newGlyphOrder = [rename_map.get(n, n) for n in otf.getGlyphOrder()] otf.setGlyphOrder(newGlyphOrder) if "post" in otf and otf["post"].formatType == 2.0: # annoyingly we need to update extraNames to match the new glyph order, # otherwise, if dumping the font to TTX directly before compiling first, # the post table will not contain the extraNames... otf["post"].extraNames = [ g for g in newGlyphOrder if g not in standardGlyphOrder ] otf["post"].mapping = {} cff_tag = "CFF " if "CFF " in otf else "CFF2" if "CFF2" in otf else None if cff_tag == "CFF " or (cff_tag == "CFF2" and otf.isLoaded(cff_tag)): cff = otf[cff_tag].cff.topDictIndex[0] char_strings = cff.CharStrings.charStrings cff.CharStrings.charStrings = { rename_map.get(n, n): v for n, v in char_strings.items() } cff.charset = [rename_map.get(n, n) for n in cff.charset] def _build_production_names(self): seen = {} rename_map = {} for name in self.otf.getGlyphOrder(): # Ignore glyphs that aren't in the source, as they are usually generated # and we lack information about them. if name not in self.glyphSet: continue prod_name = self._build_production_name(self.glyphSet[name]) # strip invalid characters not allowed in postscript glyph names if name != prod_name: valid_name = self.GLYPH_NAME_INVALID_CHARS.sub("", prod_name) if len(valid_name) > self.MAX_GLYPH_NAME_LENGTH: # if the length of the generated production name is too # long, try to fall back to the original name valid_name = self.GLYPH_NAME_INVALID_CHARS.sub("", name) else: valid_name = self.GLYPH_NAME_INVALID_CHARS.sub("", name) if len(valid_name) > self.MAX_GLYPH_NAME_LENGTH: logger.warning( "glyph name length exceeds 63 characters: '%s'", valid_name ) # add a suffix to make the production names unique rename_map[name] = self._unique_name(valid_name, seen) return rename_map @staticmethod def _unique_name(name, seen): """Append incremental '.N' suffix if glyph is a duplicate.""" if name in seen: n = seen[name] while (name + ".%d" % n) in seen: n += 1 seen[name] = n + 1 name += ".%d" % n seen[name] = 1 return name def _build_production_name(self, glyph): """Build a production name for a single glyph.""" # use PostScript names from UFO lib if available if self._postscriptNames: production_name = self._postscriptNames.get(glyph.name) return production_name if production_name else glyph.name # use name derived from unicode value unicode_val = glyph.unicode if glyph.unicode is not None: return "{}{:04X}".format( "u" if unicode_val > 0xFFFF else "uni", unicode_val ) # use production name + last (non-script) suffix if possible parts = glyph.name.rsplit(".", 1) if len(parts) == 2 and parts[0] in self.glyphSet: return "{}.{}".format( self._build_production_name(self.glyphSet[parts[0]]), parts[1], ) # use ligature name, making sure to look up components with suffixes parts = glyph.name.split(".", 1) if len(parts) == 2: liga_parts = ["{}.{}".format(n, parts[1]) for n in parts[0].split("_")] else: liga_parts = glyph.name.split("_") if len(liga_parts) > 1 and all(n in self.glyphSet for n in liga_parts): unicode_vals = [self.glyphSet[n].unicode for n in liga_parts] if all(v and v <= 0xFFFF for v in unicode_vals): return "uni" + "".join("%04X" % v for v in unicode_vals) return "_".join( self._build_production_name(self.glyphSet[n]) for n in liga_parts ) return glyph.name @staticmethod def set_post_table_format(otf, formatType): if formatType not in (2.0, 3.0): raise NotImplementedError(formatType) post = otf.get("post") if post: if post.formatType != formatType: logger.info("Setting post.formatType = %s", formatType) post.formatType = formatType # we want to update extraNames list even if formatType is the same # so we don't have to reload the font if formatType == 2.0: post.extraNames = [ g for g in otf.getGlyphOrder() if g not in standardGlyphOrder ] post.mapping = {} else: for attr in ("extraNames", "mapping"): if hasattr(post, attr): delattr(post, attr) post.glyphOrder = None @staticmethod def _get_cff_version(otf): if "CFF " in otf: return CFFVersion.CFF elif "CFF2" in otf: return CFFVersion.CFF2 else: return None @staticmethod def _convert_cff_to_cff2(otf): from fontTools.varLib.cff import convertCFFtoCFF2 logger.info("Converting CFF table to CFF2") # convertCFFtoCFF2 doesn't strip T2CharStrings' widths, so we do it ourselves # https://github.com/fonttools/fonttools/issues/1835 charstrings = otf["CFF "].cff[0].CharStrings for glyph_name in otf.getGlyphOrder(): cs = charstrings[glyph_name] cs.decompile() cs.program = _stripCharStringWidth(cs.program) convertCFFtoCFF2(otf) @classmethod def _subroutinize(cls, backend, otf, cffVersion): subroutinize = getattr(cls, f"_subroutinize_with_{backend.value}") subroutinize(otf, cffVersion) @classmethod def _subroutinize_with_compreffor(cls, otf, cffVersion): from compreffor import compress if cls._get_cff_version(otf) != CFFVersion.CFF or cffVersion != CFFVersion.CFF: raise NotImplementedError( "Only 'CFF ' 1.0 is supported by compreffor; try using cffsubr" ) logger.info("Subroutinizing CFF table with compreffor") compress(otf) @classmethod def _subroutinize_with_cffsubr(cls, otf, cffVersion): import cffsubr cffInputVersion = cls._get_cff_version(otf) assert cffInputVersion is not None, "Missing required 'CFF ' or 'CFF2' table" msg = f"Subroutinizing {cffInputVersion.name} table with cffsubr" if cffInputVersion != cffVersion: msg += f" (output format: {cffVersion.name})" logger.info(msg) return cffsubr.subroutinize(otf, cff_version=cffVersion, keep_glyph_names=False) def apply_fontinfo(self): """Apply the fontinfo data from the DesignSpace variable-font's lib to the compiled font.""" from ufo2ft.infoCompiler import InfoCompiler logger.info("Applying variable-font info from DesignSpace lib") compiler = InfoCompiler(self.otf, self.ufo, self.info) compiler.compile() # Adapted from fontTools.cff.specializer.programToCommands # https://github.com/fonttools/fonttools/blob/babca16 # /Lib/fontTools/cffLib/specializer.py#L40-L122 # When converting from CFF to CFF2 we need to drop the charstrings' widths. # This function returns a new charstring program without the initial width value. # TODO: Move to fontTools? def _stripCharStringWidth(program): seenWidthOp = False result = [] stack = [] for token in program: if not isinstance(token, str): stack.append(token) continue if (not seenWidthOp) and token in { "hstem", "hstemhm", "vstem", "vstemhm", "cntrmask", "hintmask", "hmoveto", "vmoveto", "rmoveto", "endchar", }: seenWidthOp = True parity = token in {"hmoveto", "vmoveto"} numArgs = len(stack) if numArgs and (numArgs % 2) ^ parity: stack.pop(0) # pop width result.extend(stack) result.append(token) stack = [] if stack: result.extend(stack) return result def _reloadFont(font: TTFont) -> TTFont: """Recompile a font to arrive at the final internal layout.""" stream = BytesIO() font.save(stream) stream.seek(0) # keep the same Config (constructor will make a copy) return TTFont(stream, cfg=font.cfg) ufo2ft-3.3.1/Lib/ufo2ft/preProcessor.py000066400000000000000000000517431470175262700177230ustar00rootroot00000000000000from __future__ import annotations import itertools from typing import TYPE_CHECKING from ufo2ft.constants import ( COLOR_LAYER_MAPPING_KEY, COLOR_LAYERS_KEY, COLOR_PALETTES_KEY, ) from ufo2ft.filters import isValidFilter, loadFilters from ufo2ft.filters.base import BaseFilter, BaseIFilter from ufo2ft.filters.decomposeComponents import ( DecomposeComponentsFilter, DecomposeComponentsIFilter, ) from ufo2ft.fontInfoData import getAttrWithFallback from ufo2ft.util import _GlyphSet, zip_strict if TYPE_CHECKING: from ufo2ft.instantiator import Instantiator def _load_custom_filters(ufo, filters=None): # Args: # ufo: Font # filters: Optional[List[Union[Filter, EllipsisType]]]) # Returns: List[Filter] # by default, load the filters from the lib; ellipsis is used as a placeholder # so one can optionally insert additional filters=[f1, ..., f2] either # before or after these, or override them by omitting the ellipsis. if filters is None: filters = [...] seen_ellipsis = False result = [] for f in filters: if f is ...: if seen_ellipsis: raise ValueError("ellipsis not allowed more than once") result.extend(itertools.chain(*loadFilters(ufo))) seen_ellipsis = True else: if not isValidFilter(type(f)): raise TypeError(f"Invalid filter: {f!r}") result.append(f) return result class BasePreProcessor: """Base class for objects that performs pre-processing operations on the UFO glyphs, such as decomposing composites, removing overlaps, or applying custom filters. By default the input UFO is **not** modified. The ``process`` method returns a dictionary containing the new modified glyphset, keyed by glyph name. If ``inplace`` is True, the input UFO is modified directly without the need to first copy the glyphs. Subclasses can override the ``initDefaultFilters`` method and return a list of built-in filters which are performed in a predefined order, between the user-defined pre- and post-filters. The extra kwargs passed to the constructor can be used to customize the initialization of the default filters. Custom filters can be applied before or after the default filters. These can be specified in the UFO lib.plist under the private key "com.github.googlei18n.ufo2ft.filters". Alternatively the optional ``filters`` parameter can be used. This is a list of filter instances (subclasses of BaseFilter) that overrides those defined in the UFO lib. The list can be empty, meaning no custom filters are run. If ``filters`` contain the special value ``...`` (i.e. the actual ``ellipsis`` singleton, not the str literal '...'), then all the filters from the UFO lib are loaded in its place. This allows to insert additional filters before or after those already defined in the UFO lib, as opposed to discard/replace them which is the default behavior when ``...`` is absent. """ def __init__( self, ufo, inplace=False, layerName=None, skipExportGlyphs=None, filters=None, **kwargs, ): self.ufo = ufo self.inplace = inplace self.layerName = layerName self.glyphSet = _GlyphSet.from_layer( ufo, layerName, copy=not inplace, skipExportGlyphs=skipExportGlyphs ) self.defaultFilters = self.initDefaultFilters(**kwargs) filters = _load_custom_filters(ufo, filters) self.preFilters = [f for f in filters if f.pre] self.postFilters = [f for f in filters if not f.pre] def initDefaultFilters(self, **kwargs): return [] # pragma: no cover def process(self): ufo = self.ufo glyphSet = self.glyphSet for func in self.preFilters + self.defaultFilters + self.postFilters: func(ufo, glyphSet) return glyphSet def _init_explode_color_layer_glyphs_filter(ufo, filters): # Initialize ExplodeColorLayerGlyphsFilter, which copies color glyph layers # as standalone glyphs to the default glyph set (for building COLR table), if the # UFO contains the required 'colorPalettes' key, as well as 'colorLayerMapping' lib # keys (in either the font's or glyph's lib). # Skip doing that if an explicit 'colorLayers' key is already present. if ( COLOR_PALETTES_KEY in ufo.lib and COLOR_LAYERS_KEY not in ufo.lib and ( COLOR_LAYER_MAPPING_KEY in ufo.lib or any(COLOR_LAYER_MAPPING_KEY in g.lib for g in ufo) ) ): from ufo2ft.filters.explodeColorLayerGlyphs import ExplodeColorLayerGlyphsFilter filters.append(ExplodeColorLayerGlyphsFilter()) class OTFPreProcessor(BasePreProcessor): """Preprocessor for building CFF-flavored OpenType fonts. By default, it decomposes all the components. If ``removeOverlaps`` is True, it performs a union boolean operation on all the glyphs' contours. By default, booleanOperations is used to remove overlaps. You can choose skia-pathops by setting ``overlapsBackend`` to the enum value ``RemoveOverlapsFilter.SKIA_PATHOPS``, or the string "pathops". """ def initDefaultFilters(self, removeOverlaps=False, overlapsBackend=None): filters = [] _init_explode_color_layer_glyphs_filter(self.ufo, filters) filters.append(DecomposeComponentsFilter()) if removeOverlaps: from ufo2ft.filters.removeOverlaps import RemoveOverlapsFilter if overlapsBackend is not None: filters.append(RemoveOverlapsFilter(backend=overlapsBackend)) else: filters.append(RemoveOverlapsFilter()) return filters class TTFPreProcessor(OTFPreProcessor): """Preprocessor for building TrueType-flavored OpenType fonts. By default, it decomposes all the glyphs with mixed component/contour outlines. If the ``flattenComponents`` setting is True, glyphs with nested components are flattened so that they have at most one level of components. If ``removeOverlaps`` is True, it performs a union boolean operation on all the glyphs' contours. By default, booleanOperations is used to remove overlaps. You can choose skia-pathops by setting ``overlapsBackend`` to the enum value ``RemoveOverlapsFilter.SKIA_PATHOPS``, or the string "pathops". By default, it also converts all the PostScript cubic Bezier curves to TrueType quadratic splines. If the outlines are already quadratic, you can skip this by setting ``convertCubics`` to False. The optional ``conversionError`` argument controls the tolerance of the approximation algorithm. It is measured as the maximum distance between the original and converted curve, and it's relative to the UPM of the font (default: 1/1000 or 0.001). When converting curves to quadratic, it is assumed that the contours' winding direction is set following the PostScript counter-clockwise convention. Thus, by default the direction is reversed, in order to conform to opposite clockwise convention for TrueType outlines. You can disable this by setting ``reverseDirection`` to False. If both ``inplace`` and ``rememberCurveType`` options are True, the curve type "quadratic" is saved in font' lib under a private cu2qu key; the preprocessor will not try to convert them again if the curve type is already set to "quadratic". """ def initDefaultFilters( self, removeOverlaps=False, overlapsBackend=None, flattenComponents=False, convertCubics=True, conversionError=None, allQuadratic=True, reverseDirection=True, rememberCurveType=True, ): filters = [] _init_explode_color_layer_glyphs_filter(self.ufo, filters) # len(g) is the number of contours, so we include the all glyphs # that have both components and at least one contour filters.append(DecomposeComponentsFilter(include=lambda g: len(g))) if flattenComponents: from ufo2ft.filters.flattenComponents import FlattenComponentsFilter filters.append(FlattenComponentsFilter()) if removeOverlaps: from ufo2ft.filters.removeOverlaps import RemoveOverlapsFilter if overlapsBackend is not None: filters.append(RemoveOverlapsFilter(backend=overlapsBackend)) else: filters.append(RemoveOverlapsFilter()) if convertCubics: from ufo2ft.filters.cubicToQuadratic import CubicToQuadraticFilter filters.append( CubicToQuadraticFilter( conversionError=conversionError, reverseDirection=reverseDirection, rememberCurveType=rememberCurveType and self.inplace, allQuadratic=allQuadratic, ) ) elif reverseDirection: from ufo2ft.filters.reverseContourDirection import ( ReverseContourDirectionFilter, ) filters.append(ReverseContourDirectionFilter(include=lambda g: len(g))) return filters class BaseInterpolatablePreProcessor: """Base class for interpolatable pre-processors. These apply filters to same-named glyphs from multiple source layers at once, ensuring that outlines are kept interpolation compatible. The optional `instantiator` can be used by filters to interpolate glyph instances (e.g. when decomposing composite glyphs defined at more or less source locations as some of their components' base glyphs). """ def __init__( self, ufos, inplace=False, layerNames=None, skipExportGlyphs=None, filters=None, *, instantiator: Instantiator | None = None, **kwargs, ): self.ufos = ufos self.inplace = inplace if layerNames is None: layerNames = [None] * len(ufos) assert len(ufos) == len(layerNames) self.layerNames = layerNames if instantiator is not None and len(instantiator.source_layers) != len(ufos): raise ValueError( f"Expected {len(ufos)} sources for instantiator; " f"found {len(instantiator.source_layers)}" ) self.instantiator = instantiator # For each UFO, make a mapping of name to glyph object (and ensure it # contains none of the glyphs to be skipped, or any references to it). self.glyphSets = [ _GlyphSet.from_layer(ufo, layerName, copy=not inplace) for ufo, layerName in zip_strict(ufos, layerNames) ] if skipExportGlyphs: from ufo2ft.filters.skipExportGlyphs import SkipExportGlyphsIFilter self._run(SkipExportGlyphsIFilter(skipExportGlyphs)) self.defaultFilters = self.initDefaultFilters(**kwargs) filterses = [_load_custom_filters(ufo, filters) for ufo in ufos] self.preFilters = [[f for f in filters if f.pre] for filters in filterses] self.postFilters = [[f for f in filters if not f.pre] for filters in filterses] def initDefaultFilters(self, **kwargs): filterses = [] for ufo in self.ufos: filterses.append([]) _init_explode_color_layer_glyphs_filter(ufo, filterses[-1]) return filterses def process(self): # first apply all custom pre-filters, then all default filters, and finally # all custom post-filters for filterses in (self.preFilters, self.defaultFilters, self.postFilters): for filters in itertools.zip_longest(*filterses): self._run(*filters) return self.glyphSets def _update_instantiator(self): # the instantiator's source layers must be updated after each filter is run, # since each filter can modify/remove/add glyphs. if self.instantiator is not None: self.instantiator.replace_source_layers(self.glyphSets) def _run_interpolatable(self, filter_: BaseIFilter) -> set[str]: # apply a single, interpolatable filter to all the glyphSets modified = filter_(self.ufos, self.glyphSets, self.instantiator) if modified: self._update_instantiator() return modified @staticmethod def _try_as_interpolatable_filter( filters: list[BaseFilter | None], ) -> BaseIFilter | None: # Try to combine multiple filters into a single interpolatable variant assert len(filters) > 0 filter_ = next(filter(None, filters)) filter_class = type(filter_) if not all( ( type(f) is filter_class and f.options == filter_.options and f.pre == filter_.pre ) for f in filters[1:] ): return None if isinstance(filter_, BaseIFilter): return filter_ ifilter_class = None try: ifilter_class = filter_class.getInterpolatableFilterClass() except AttributeError: pass if ifilter_class is None: return None if not isValidFilter(ifilter_class, BaseIFilter): raise ValueError(f"Invalid interpolatable filter class: {ifilter_class!r}") # in the unlikely scenario individual filters have different includes, # this effectively takes the union of those def include(g): return any(f.include(g) for f in filters) return ifilter_class( pre=filter_.pre, include=include, **filter_.options.__dict__, ) def _run(self, *filters: tuple[BaseFilter | None]) -> set[str]: # apply either multiple (one per glyphSet) or a single filter to all glyphSets if len(filters) == 1: assert filters[0] is not None if isinstance(filters[0], BaseIFilter): return self._run_interpolatable(filters[0]) filters = [filters[0]] * len(self.ufos) # attempt to convert mutltiple filters to single interpolatable variant (if any) if ifilter := self._try_as_interpolatable_filter(filters): return self._run_interpolatable(ifilter) # or else apply individual filters to the respective glyphSet, one at a time, # and hope for the best... modified = set() for filter_, ufo, glyphSet in zip_strict(filters, self.ufos, self.glyphSets): if filter_ is not None: modified |= filter_(ufo, glyphSet) if modified: self._update_instantiator() return modified class TTFInterpolatablePreProcessor(BaseInterpolatablePreProcessor): """Preprocessor for building TrueType-flavored OpenType fonts with interpolatable quadratic outlines. The constructor takes a list of UFO fonts, and the ``process`` method returns the modified glyphsets (list of dicts) in the same order. The pre-processor performs the conversion from cubic to quadratic on all the UFOs at once, then decomposes mixed contour/component glyphs. Additional pre/post custom filter are also applied to each single UFOs, respectively before or after the default filters, if they are specified in the UFO's lib.plist under the private key "com.github.googlei18n.ufo2ft.filters". NOTE: If you use any custom filters, the resulting glyphsets may no longer be interpolation compatible, depending on the particular filter used or whether they are applied to only some vs all of the UFOs. The ``conversionError``, ``reverseDirection``, ``flattenComponents`` and ``rememberCurveType`` arguments work in the same way as in the ``TTFPreProcessor``. """ def __init__( self, ufos, inplace=False, flattenComponents=False, convertCubics=True, conversionError=None, reverseDirection=True, rememberCurveType=True, layerNames=None, skipExportGlyphs=None, filters=None, allQuadratic=True, *, instantiator: Instantiator | None = None, **kwargs, ): from fontTools.cu2qu.ufo import DEFAULT_MAX_ERR super().__init__( ufos, inplace=inplace, layerNames=layerNames, skipExportGlyphs=skipExportGlyphs, filters=filters, instantiator=instantiator, **kwargs, ) self.flattenComponents = flattenComponents self.convertCubics = convertCubics self._conversionErrors = [ (conversionError or DEFAULT_MAX_ERR) * getAttrWithFallback(ufo.info, "unitsPerEm") for ufo in self.ufos ] self._reverseDirection = reverseDirection self._rememberCurveType = rememberCurveType self.allQuadratic = allQuadratic def process(self): from fontTools.cu2qu.ufo import fonts_to_quadratic # first apply all custom pre-filters for funcs in itertools.zip_longest(*self.preFilters): self._run(*funcs) # TrueType fonts cannot mix contours and components, so pick out all glyphs # that have both contours _and_ components. needs_decomposition = { gname for glyphSet in self.glyphSets for gname, glyph in glyphSet.items() if len(glyph) > 0 and glyph.components } # Variable fonts can only variate glyf components' x or y offsets, not their # 2x2 transformation matrix; decompose of these don't match across masters self.check_for_nonmatching_components(needs_decomposition) if needs_decomposition: self._run(DecomposeComponentsIFilter(include=needs_decomposition)) # then apply all default filters for funcs in itertools.zip_longest(*self.defaultFilters): self._run(*funcs) if self.convertCubics: if fonts_to_quadratic( self.glyphSets, max_err=self._conversionErrors, reverse_direction=self._reverseDirection, dump_stats=True, remember_curve_type=self._rememberCurveType and self.inplace, all_quadratic=self.allQuadratic, ): self._update_instantiator() elif self._reverseDirection: from ufo2ft.filters.reverseContourDirection import ( ReverseContourDirectionFilter, ) self._run(ReverseContourDirectionFilter(include=lambda g: len(g))) if self.flattenComponents: from ufo2ft.filters.flattenComponents import FlattenComponentsIFilter self._run(FlattenComponentsIFilter(include=lambda g: len(g.components))) # finally apply all custom post-filters for funcs in itertools.zip_longest(*self.postFilters): self._run(*funcs) return self.glyphSets def check_for_nonmatching_components(self, needs_decomposition): # Look through all the glyphsets and if we find any glyphs # where the transforms don't match across masters, we add it # to the needs_decomposition list. See #507 all_glyphs = set.union(*[set(x.keys()) for x in self.glyphSets]) for glyph in all_glyphs: if glyph in needs_decomposition: continue # We know there's an issue here layers = [ glyphset[glyph] for glyphset in self.glyphSets if glyph in glyphset ] # Skip early if there aren't any components component_counts = [len(layer.components) for layer in layers] if not any(component_counts): continue # Other bits of the system will check for incompatible construction, # we just want to stay alive. for component_index in range(0, min(component_counts)): # We only care about the two-by-twos; translations can differ transforms = [ layer.components[component_index].transformation[0:4] for layer in layers ] if any(transform != transforms[0] for transform in transforms): needs_decomposition.add(glyph) break class OTFInterpolatablePreProcessor(BaseInterpolatablePreProcessor): """Interpolatable pre-processor for CFF-flavored fonts. By default, besides any user-defined custom pre/post filters, this decomposes all composite glyphs, which aren't a thing in PostScript outlines. Unlike the non-interpolatable OTFPreProcessor, overlaps are *not* removed as that could make outlines incompatible for interpolation. """ def initDefaultFilters(self, **kwargs): filterses = super().initDefaultFilters(**kwargs) # this interpolatable filter will only run once on all the glyphSets, # (see _try_as_interpolatable_filter) decompose = DecomposeComponentsIFilter() for filters in filterses: filters.append(decompose) return filterses ufo2ft-3.3.1/Lib/ufo2ft/util.py000066400000000000000000000725651470175262700162170ustar00rootroot00000000000000from __future__ import annotations import importlib import logging import re import sys from copy import deepcopy from functools import partial from inspect import currentframe, getfullargspec from typing import Any, Mapping, NamedTuple, Set from fontTools import subset, ttLib, unicodedata from fontTools.designspaceLib import DesignSpaceDocument from fontTools.feaLib.builder import addOpenTypeFeatures from fontTools.misc.fixedTools import otRound from fontTools.misc.transform import Identity from fontTools.pens.filterPen import DecomposingFilterPointPen from fontTools.pens.reverseContourPen import ReverseContourPen from fontTools.pens.transformPen import TransformPen from ufo2ft.constants import OPENTYPE_CATEGORIES_KEY, UNICODE_SCRIPT_ALIASES from ufo2ft.errors import InvalidDesignSpaceData, InvalidFontData from ufo2ft.fontInfoData import getAttrWithFallback logger = logging.getLogger(__name__) def makeOfficialGlyphOrder(font, glyphOrder=None): """Make the final glyph order for 'font'. If glyphOrder is None, try getting the font.glyphOrder list. If not explicit glyphOrder is defined, sort glyphs alphabetically. If ".notdef" glyph is present in the font, force this to always be the first glyph (at index 0). """ if glyphOrder is None: glyphOrder = getattr(font, "glyphOrder", ()) names = set(font.keys()) order = [] if ".notdef" in names: names.remove(".notdef") order.append(".notdef") for name in glyphOrder: if name not in names: continue names.remove(name) order.append(name) order.extend(sorted(names)) return order def decomposeCompositeGlyph( glyph, glyphSet, skipMissing=False, reverseFlipped=True, include=None, decomposeNested=True, ): """Decompose composite glyph in-place resolving references from glyphSet.""" if len(glyph.components) == 0: return pen = DecomposingFilterPointPen( glyph.getPointPen(), glyphSet, reverseFlipped=reverseFlipped, include=include, decomposeNested=decomposeNested, ) for component in list(glyph.components): try: component.drawPoints(pen) except pen.MissingComponentError: if skipMissing: logger.warning( "dropping non-existent component '%s' in glyph '%s'", component.baseGlyph, glyph.name, ) else: raise glyph.removeComponent(component) class _GlyphSet(dict): @classmethod def from_layer(cls, font, layerName=None, copy=False, skipExportGlyphs=None): """Return a mapping of glyph names to glyph objects from `font`.""" if layerName is not None: layer = font.layers[layerName] else: layer = font.layers.defaultLayer if copy: self = _copyLayer(layer, obj_type=cls) self.lib = deepcopy(layer.lib) else: self = cls((g.name, g) for g in layer) self.lib = layer.lib self.name = layer.name if layerName is not None else None # If any glyphs in the skipExportGlyphs list are used as components, decompose # them in the containing glyphs... if skipExportGlyphs: from ufo2ft.filters.skipExportGlyphs import SkipExportGlyphsFilter SkipExportGlyphsFilter(skipExportGlyphs)(font, self) return self def _copyLayer(layer, obj_type=dict): try: g = next(iter(layer)) except StopIteration: # layer is empty return obj_type() newGlyph = _getNewGlyphFactory(g) glyphSet = obj_type() for glyph in layer: glyphSet[glyph.name] = _copyGlyph(glyph, glyphFactory=newGlyph) return glyphSet def _getNewGlyphFactory(glyph): # defcon.Glyph doesn't take a name argument, ufoLib2 requires one... cls = glyph.__class__ if "name" in getfullargspec(cls.__init__).args: def newGlyph(name, **kwargs): return cls(name=name, **kwargs) else: def newGlyph(name, **kwargs): # use instantiateGlyphObject() to keep any custom sub-element classes # https://github.com/googlefonts/ufo2ft/issues/363 g2 = glyph.layer.instantiateGlyphObject() g2.name = name for k, v in kwargs.items(): setattr(g2, k, v) return g2 return newGlyph def _copyGlyph(glyph, glyphFactory=None, reverseContour=False): # copy everything except unused attributes: 'guidelines', 'note', 'image' if glyphFactory is None: glyphFactory = _getNewGlyphFactory(glyph) copy = glyphFactory(glyph.name) copy.width = glyph.width copy.height = glyph.height copy.unicodes = list(glyph.unicodes) copy.anchors = [dict(a) for a in glyph.anchors] copy.lib = deepcopy(glyph.lib) pointPen = copy.getPointPen() if reverseContour: from fontTools.pens.pointPen import ReverseContourPointPen pointPen = ReverseContourPointPen(pointPen) glyph.drawPoints(pointPen) return copy def _setGlyphMargin(glyph, side, margin): # defcon.Glyph has @property setters for the margins, whereas ufoLib2.Glyph # has regular instance methods assert side in {"left", "right", "top", "bottom"} if hasattr(glyph, f"set{side.title()}Margin"): # ufoLib2 getattr(glyph, f"set{side.title()}Margin")(margin) elif hasattr(glyph, f"{side}Margin"): # defcon descriptor = getattr(type(glyph), f"{side}Margin") descriptor.__set__(glyph, margin) else: raise NotImplementedError(f"Unsupported Glyph class: {type(glyph)!r}") # DEPRECATED: use ufo2ft.util.decomposeCompositeGlyph above def deepCopyContours( glyphSet, parent, composite, transformation, specificComponents=None ): """Copy contours from component to parent, including nested components. specificComponent: an optional list of glyph name strings. If not passed or None, decompose all components of a glyph unconditionally and completely. If passed, only completely decompose components whose baseGlyph is in the list. """ for nestedComponent in composite.components: # Because this function works recursively, test at each turn if we are going to # recurse into a specificComponent. If so, set the specificComponents argument # to None so we unconditionally decompose the possibly nested component # completely. specificComponentsEffective = specificComponents if specificComponentsEffective: if nestedComponent.baseGlyph not in specificComponentsEffective: continue else: specificComponentsEffective = None try: nestedBaseGlyph = glyphSet[nestedComponent.baseGlyph] except KeyError: logger.warning( "dropping non-existent component '%s' in glyph '%s'", nestedComponent.baseGlyph, parent.name, ) else: deepCopyContours( glyphSet, parent, nestedBaseGlyph, transformation.transform(nestedComponent.transformation), specificComponents=specificComponentsEffective, ) # Check if there are any contours to copy before instantiating pens. if composite != parent and len(composite): if transformation == Identity: pen = parent.getPen() else: pen = TransformPen(parent.getPen(), transformation) # if the transformation has a negative determinant, it will # reverse the contour direction of the component xx, xy, yx, yy = transformation[:4] if xx * yy - xy * yx < 0: pen = ReverseContourPen(pen) for contour in composite: contour.draw(pen) def makeUnicodeToGlyphNameMapping(font, glyphOrder=None): """Make a unicode: glyph name mapping for this glyph set (dict or Font). Raises InvalidFontData exception if multiple glyphs are mapped to the same unicode codepoint. """ if glyphOrder is None: glyphOrder = makeOfficialGlyphOrder(font) mapping = {} for glyphName in glyphOrder: glyph = font[glyphName] unicodes = glyph.unicodes for uni in unicodes: if uni not in mapping: mapping[uni] = glyphName else: raise InvalidFontData( "cannot map '%s' to U+%04X; already mapped to '%s'" % (glyphName, uni, mapping[uni]) ) return mapping 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") def compileGDEF(featureFile, glyphOrder): """Compile and return a GDEF table from `featureFile` (feaLib FeatureFile), using the given `glyphOrder` (list of glyph names). """ from fontTools.feaLib.ast import TableBlock font = ttLib.TTFont() font.setGlyphOrder(glyphOrder) gdefDefined = False for statement in featureFile.statements: if isinstance(statement, TableBlock) and statement.name == "GDEF": gdefDefined = True if not gdefDefined: addOpenTypeFeatures(font, featureFile, tables={"GDEF", "GPOS", "GSUB"}) else: addOpenTypeFeatures(font, featureFile, tables={"GDEF"}) return font.get("GDEF") def closeGlyphsOverGSUB(gsub, glyphs): """Use the FontTools subsetter to perform a closure over the GSUB table given the initial `glyphs` (set of glyph names, str). Update the set in-place adding all the glyph names that can be reached via GSUB substitutions from this initial set. """ subsetter = subset.Subsetter() subsetter.glyphs = glyphs gsub.closure_glyphs(subsetter) def classifyGlyphs(unicodeFunc, cmap, gsub=None, extra_substitutions=None): """'unicodeFunc' is a callable that takes a Unicode codepoint and returns a string, or collection of strings, denoting some Unicode property associated with the given character (or None if a character is considered 'neutral'). 'cmap' is a dictionary mapping Unicode codepoints to glyph names. 'gsub' is an (optional) fonttools GSUB table object, used to find all the glyphs that are "reachable" via substitutions from the initial sets of glyphs defined in the cmap. 'extra_substitutions' is an optional dictionary mapping glyph names to a set of other glyphs which should be considered reachable from them (for example when using designspace rules to effect substitutions). Returns a dictionary of glyph sets associated with the given Unicode properties. """ glyphSets = {} neutralGlyphs = set() for uv, glyphName in cmap.items(): key_or_keys = unicodeFunc(uv) if key_or_keys is None: neutralGlyphs.add(glyphName) elif isinstance(key_or_keys, (list, set, tuple)): for key in key_or_keys: glyphSets.setdefault(key, set()).add(glyphName) else: glyphSets.setdefault(key_or_keys, set()).add(glyphName) if gsub is not None: if neutralGlyphs: closeGlyphsOverGSUB(gsub, neutralGlyphs) for glyphs in glyphSets.values(): s = glyphs | neutralGlyphs closeGlyphsOverGSUB(gsub, s) glyphs.update(s - neutralGlyphs) if extra_substitutions: for glyphs in glyphSets.values(): to_append = set() for glyph in glyphs: to_append |= extra_substitutions.get(glyph, set()) glyphs.update(to_append) return glyphSets def unicodeInScripts(uv, scripts): """Check UnicodeData's ScriptExtension property for unicode codepoint 'uv' and return True if it intersects with the set of 'scripts' provided, False if it does not intersect. Return None for 'Common' script ('Zyyy'). """ sx = unicodeScriptExtensions(uv) if "Zyyy" in sx: return None return not sx.isdisjoint(scripts) # we consider the 'Common' and 'Inherited' scripts as neutral for # determining a script horizontal direction DFLT_SCRIPTS = {"Zyyy", "Zinh"} def unicodeScriptDirection(uv): sc = unicodedata.script(chr(uv)) if sc in DFLT_SCRIPTS: return None return unicodedata.script_horizontal_direction(sc, "LTR") def calcCodePageRanges(unicodes): """Given a set of Unicode codepoints (integers), calculate the corresponding OS/2 CodePage range bits. This is a direct translation of FontForge implementation: https://github.com/fontforge/fontforge/blob/7b2c074/fontforge/tottf.c#L3158 """ codepageRanges = set() chars = [chr(u) for u in unicodes] hasAscii = set(range(0x20, 0x7E)).issubset(unicodes) hasLineart = "┤" in chars for char in chars: if char == "Þ" and hasAscii: codepageRanges.add(0) # Latin 1 elif char == "Ľ" and hasAscii: codepageRanges.add(1) # Latin 2: Eastern Europe if hasLineart: codepageRanges.add(58) # Latin 2 elif char == "Б": codepageRanges.add(2) # Cyrillic if "Ѕ" in chars and hasLineart: codepageRanges.add(57) # IBM Cyrillic if "╜" in chars and hasLineart: codepageRanges.add(49) # MS-DOS Russian elif char == "Ά": codepageRanges.add(3) # Greek if hasLineart and "½" in chars: codepageRanges.add(48) # IBM Greek if hasLineart and "√" in chars: codepageRanges.add(60) # Greek, former 437 G elif char == "İ" and hasAscii: codepageRanges.add(4) # Turkish if hasLineart: codepageRanges.add(56) # IBM turkish elif char == "א": codepageRanges.add(5) # Hebrew if hasLineart and "√" in chars: codepageRanges.add(53) # Hebrew elif char == "ر": codepageRanges.add(6) # Arabic if "√" in chars: codepageRanges.add(51) # Arabic if hasLineart: codepageRanges.add(61) # Arabic; ASMO 708 elif char == "ŗ" and hasAscii: codepageRanges.add(7) # Windows Baltic if hasLineart: codepageRanges.add(59) # MS-DOS Baltic elif char == "₫" and hasAscii: codepageRanges.add(8) # Vietnamese elif char == "ๅ": codepageRanges.add(16) # Thai elif char == "エ": codepageRanges.add(17) # JIS/Japan elif char == "ㄅ": codepageRanges.add(18) # Chinese: Simplified chars elif char == "ㄱ": codepageRanges.add(19) # Korean wansung elif char == "央": codepageRanges.add(20) # Chinese: Traditional chars elif char == "곴": codepageRanges.add(21) # Korean Johab elif char == "♥" and hasAscii: codepageRanges.add(30) # OEM Character Set # TODO: Symbol bit has a special meaning (check the spec), we need # to confirm if this is wanted by default. # elif chr(0xF000) <= char <= chr(0xF0FF): # codepageRanges.add(31) # Symbol Character Set elif char == "þ" and hasAscii and hasLineart: codepageRanges.add(54) # MS-DOS Icelandic elif char == "╚" and hasAscii: codepageRanges.add(62) # WE/Latin 1 codepageRanges.add(63) # US elif hasAscii and hasLineart and "√" in chars: if char == "Å": codepageRanges.add(50) # MS-DOS Nordic elif char == "é": codepageRanges.add(52) # MS-DOS Canadian French elif char == "õ": codepageRanges.add(55) # MS-DOS Portuguese if hasAscii and "‰" in chars and "∑" in chars: codepageRanges.add(29) # Macintosh Character Set (US Roman) # when no codepage ranges can be enabled, fall back to enabling bit 0 # (Latin 1) so that the font works in MS Word: # https://github.com/googlei18n/fontmake/issues/468 if not codepageRanges: codepageRanges.add(0) return codepageRanges class _LazyFontName: def __init__(self, font): self.font = font def __str__(self): return getAttrWithFallback(self.font.info, "postscriptFontName") def getDefaultMasterFont(designSpaceDoc): defaultSource = designSpaceDoc.findDefault() if not defaultSource: raise InvalidDesignSpaceData( "Can't find base (neutral) master in DesignSpace document" ) if not defaultSource.font: raise InvalidDesignSpaceData( "DesignSpace source '%s' is missing required 'font' attribute" % getattr(defaultSource, "name", "") ) return defaultSource.font def _notdefGlyphFallback(designSpaceDoc): """Return an empty glyph to be used as .notdef for sparse layer masters. Sparse layers usually do not contain a .notdef glyph, however in order to compile valid TTFs to be used as master in varLib.build, a .notdef at index 0 is required. We can't use the auto-generated .notdef glyph because it may be incompatible with the one already present in the other masters. So we make an empty glyph which will be ignored when building gvar or HVAR. If the default master does not contain a .notdef either, return None since the auto-generated .notdef can be used. """ try: baseUfo = getDefaultMasterFont(designSpaceDoc) except InvalidDesignSpaceData: notdefGlyph = None else: # unlike ufoLib2, defcon has no Font.get() method try: notdefGlyph = baseUfo[".notdef"] except KeyError: notdefGlyph = None else: notdefGlyph = _getNewGlyphFactory(notdefGlyph)(".notdef") # sentinel value for varLib that means this advance does not participate # https://github.com/fonttools/fonttools/pull/3235 notdefGlyph.width = 0xFFFF notdefGlyph.height = 0xFFFF return notdefGlyph # NOTE about the security risk involved in using eval: the function below is # meant to be used to parse string coming from the command-line, which is # inherently "trusted"; if that weren't the case, a potential attacker # could do worse things than segfaulting the Python interpreter... def _kwargsEval(s): return eval( "dict(%s)" % s, {"__builtins__": {"True": True, "False": False, "dict": dict}} ) _pluginSpecRE = re.compile( r"(?:([\w\.]+)::)?" # MODULE_NAME + '::' r"(\w+)" # CLASS_NAME [required] r"(?:\((.*)\))?" # (KWARGS) ) def _loadPluginFromString(spec, moduleName, isValidFunc): spec = spec.strip() m = _pluginSpecRE.match(spec) if not m or (m.end() - m.start()) != len(spec): raise ValueError(spec) moduleName = m.group(1) or moduleName className = m.group(2) kwargs = m.group(3) module = importlib.import_module(moduleName) klass = getattr(module, className) if not isValidFunc(klass): raise TypeError(klass) try: options = _kwargsEval(kwargs) if kwargs else {} except SyntaxError as e: raise ValueError("options have incorrect format: %r" % kwargs) from e return klass(**options) 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. To be used as the first function in top-level `ufo2ft.compile*` functions. Raise TypeError with unexpected keyword arguments (missing from 'defaults'). """ extra_kwargs = set(kwargs).difference(defaults) if extra_kwargs: # get the name of the function that called init_kwargs func_name = currentframe().f_back.f_code.co_name raise TypeError( f"{func_name}() got unexpected keyword arguments: " f"{', '.join(repr(k) for k in extra_kwargs)}" ) return {k: (kwargs[k] if k in kwargs else v) for k, v in defaults.items()} def prune_unknown_kwargs(kwargs, *callables): """Inspect callables and return a new dict skipping any unknown arguments. To be used after `init_kwargs` to narrow down arguments for underlying code. """ known_args = set() for func in callables: arg_spec = getfullargspec(func) known_args.update(arg_spec.args) # also handle optional keyword-only arguments if arg_spec.kwonlydefaults: known_args.update(arg_spec.kwonlydefaults) return {k: v for k, v in kwargs.items() if k in known_args} def ensure_all_sources_have_names(doc: DesignSpaceDocument) -> None: """Change in-place the given document to make sure that all elements have a unique name assigned. This may rename sources with a "temp_master.N" name, designspaceLib's default stand-in. """ used_names: Set[str] = set() counter = 0 for source in doc.sources: while source.name is None or source.name in used_names: source.name = f"temp_master.{counter}" counter += 1 used_names.add(source.name) def getMaxComponentDepth( glyph, glyphSet, maxComponentDepth=0, visited=None, rec_stack=None ): """Return the height of a composite glyph's tree of components. This is equal to the depth of its deepest node, where the depth means the number of edges (component references) from the node to the tree's root. For glyphs that contain no components, only contours, this is 0. Composite glyphs have max component depth of 1 or greater. Raises InvalidFontData if a cyclical component reference is detected. """ if not glyph.components: return maxComponentDepth if visited is None: visited = set() if rec_stack is None: rec_stack = [] assert glyph.name not in visited visited.add(glyph.name) rec_stack.append(glyph.name) maxComponentDepth += 1 initialMaxComponentDepth = maxComponentDepth for component in glyph.components: try: baseGlyph = glyphSet[component.baseGlyph] except KeyError: continue if component.baseGlyph not in visited: componentDepth = getMaxComponentDepth( baseGlyph, glyphSet, initialMaxComponentDepth, visited, rec_stack ) maxComponentDepth = max(maxComponentDepth, componentDepth) elif component.baseGlyph in rec_stack: raise InvalidFontData( f"cyclical component reference:" f" {' -> '.join(rec_stack)} => {component.baseGlyph}" ) rec_stack.pop() return maxComponentDepth def location_to_string(location): """Reports a designspace location (dictionary mapping axis:loc) in a user-friendly way""" return ", ".join([f"{axis}={loc:g}" for axis, loc in location.items()]) def unicodeScriptExtensions( codepoint: int, aliases: Mapping[str, str] = UNICODE_SCRIPT_ALIASES ) -> set[str]: """Returns the Unicode script extensions for a codepoint, optionally aliasing some scripts. This allows lookups to contain more than one script. The most prominent case is being able to kern Hiragana and Katakana against each other, Unicode defines "Hrkt" as an alias for both scripts. """ return {aliases.get(s, s) for s in unicodedata.script_extension(chr(codepoint))} def describe_ufo(ufo: Any) -> str: """Returns a description of a UFO suitable for logging.""" if ( hasattr(ufo, "reader") and hasattr(ufo.reader, "fs") and hasattr(ufo.reader.fs, "root_path") ): return ufo.reader.fs.root_path elif ufo.info.familyName or ufo.info.styleName: return " ".join(n for n in (ufo.info.familyName, ufo.info.styleName) if n) return repr(ufo) def colrClipBoxQuantization(ufo: Any) -> int: """Integer value to quantize COLR ClipBoxes. The higher the value, the greater the chances that that same clipboxes get reused for multiple color glyphs. This is only called when compile option `colrAutoClipBoxes` is True. By default, we quantize to 1/10th of the font's upem, rounded to nearest multiple of 10: e.g. 100 unit intervals for 1000 upem, 200 units for 2048 etc. The caller can pass their own callback function to return a different value than the default one. """ 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 class OpenTypeCategories(NamedTuple): unassigned: frozenset[str] base: frozenset[str] ligature: frozenset[str] mark: frozenset[str] component: frozenset[str] @classmethod def load(cls, font): """Return 'public.openTypeCategories' values as a tuple of sets of unassigned, bases, ligatures, marks, components.""" unassigned, bases, ligatures, marks, components = ( set(), set(), set(), set(), set(), ) openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {}) # Handle case where we are a variable feature writer if not openTypeCategories and isinstance(font, DesignSpaceDocument): designspace = font default = designspace.findDefault() if default is None: raise InvalidDesignSpaceData("No default source found in designspace") font = default.font openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {}) for glyphName, category in openTypeCategories.items(): if category == "unassigned": unassigned.add(glyphName) elif category == "base": bases.add(glyphName) elif category == "ligature": ligatures.add(glyphName) elif category == "mark": marks.add(glyphName) elif category == "component": components.add(glyphName) else: logging.getLogger("ufo2ft").warning( f"The '{OPENTYPE_CATEGORIES_KEY}' value of {glyphName} in " f"{font.info.familyName} {font.info.styleName} is '{category}' " "when it should be 'unassigned', 'base', 'ligature', 'mark' " "or 'component'." ) return cls( frozenset(unassigned), frozenset(bases), frozenset(ligatures), frozenset(marks), frozenset(components), ) def importUfoModule(): try: import ufoLib2 except ModuleNotFoundError: try: import defcon except ModuleNotFoundError: raise else: return defcon else: return ufoLib2 def openFontFactory(*, ufo_module=None): if ufo_module is None: ufo_module = importUfoModule() if hasattr(ufo_module.Font, "open"): def ctor(path=None): if path is None: return ufo_module.Font() else: return ufo_module.Font.open(path) return ctor return ufo_module.Font def openFont(*args, ufo_module=None, **kwargs): return openFontFactory(ufo_module=ufo_module)(*args, **kwargs) # zip(strict=True) was added with Python 3.10, we provide a backport below # https://docs.python.org/3/library/functions.html#zip if sys.version_info[:2] < (3, 10): def zip_strict(*iterables): # https://peps.python.org/pep-0618/#reference-implementation if not iterables: return iterators = tuple(iter(iterable) for iterable in iterables) try: while True: items = [] for iterator in iterators: items.append(next(iterator)) yield tuple(items) except StopIteration: pass if items: i = len(items) plural = " " if i == 1 else "s 1-" msg = f"zip() argument {i + 1} is shorter than argument{plural}{i}" raise ValueError(msg) sentinel = object() for i, iterator in enumerate(iterators[1:], 1): if next(iterator, sentinel) is not sentinel: plural = " " if i == 1 else "s 1-" msg = f"zip() argument {i + 1} is longer than argument{plural}{i}" raise ValueError(msg) else: zip_strict = partial(zip, strict=True) ufo2ft-3.3.1/MANIFEST.in000066400000000000000000000002441470175262700144740ustar00rootroot00000000000000include README.rst include LICENSE include tox.ini include *requirements.txt recursive-include tests *.py recursive-include tests/data *.glif *.plist *.fea *.ttx ufo2ft-3.3.1/README.rst000066400000000000000000000160431470175262700144310ustar00rootroot00000000000000|GitHub Actions status| |PyPI Version| |Codecov| |Gitter Chat| ufo2ft ====== ufo2ft ("UFO to FontTools") is a fork of `ufo2fdk `__ whose goal is to generate OpenType font binaries from UFOs without the FDK dependency. The library provides two functions, ``compileOTF`` and ``compileTTF``, which work exactly the same way: .. code:: python from defcon import Font from ufo2ft import compileOTF ufo = Font('MyFont-Regular.ufo') otf = compileOTF(ufo) otf.save('MyFont-Regular.otf') In most cases, the behavior of ufo2ft should match that of ufo2fdk, whose documentation is retained below (and hopefully is still accurate). Naming Data ~~~~~~~~~~~ As with any OpenType compiler, you have to set the font naming data to a particular standard for your naming to be set correctly. In ufo2fdk, you can get away with setting *two* naming attributes in your font.info object for simple fonts: - familyName: The name for your family. For example, "My Garamond". - styleName: The style name for this particular font. For example, "Display Light Italic" ufo2fdk will create all of the other naming data based on thse two fields. If you want to use the fully automatic naming system, all of the other name attributes should be set to ``None`` in your font. However, if you want to override the automated system at any level, you can specify particular naming attributes and ufo2fdk will honor your settings. You don't have to set *all* of the attributes, just the ones you don't want to be automated. For example, in the family "My Garamond" you have eight weights. It would be nice to style map the italics to the romans for each weight. To do this, in the individual romans and italics, you need to set the style mapping data. This is done through the ``styleMapFamilyName`` and ``styleMapStyleName`` attributes. In each of your roman and italic pairs you would do this: **My Garamond-Light.ufo** - familyName = "My Garamond" - styleName = "Light" - styleMapFamilyName = "My Garamond Display Light" - styleMapStyleName = "regular" **My Garamond-Light Italic.ufo** - familyName = "My Garamond" - styleName = "Display Light Italic" - styleMapFamilyName = "My Garamond Display Light" - styleMapStyleName = "italic" **My Garamond-Book.ufo** - familyName = "My Garamond" - styleName = "Book" - styleMapFamilyName = "My Garamond Display Book" - styleMapStyleName = "regular" **My Garamond-Book Italic.ufo** - familyName = "My Garamond" - styleName = "Display Book Italic" - styleMapFamilyName = "My Garamond Display Book" - styleMapStyleName = "italic" **etc.** Additionally, if you have defined any naming data, or any data for that matter, in table definitions within your font's features that data will be honored. Feature generation ~~~~~~~~~~~~~~~~~~ If your font's features do not contain kerning/mark/mkmk features, ufo2ft will create them based on your font's kerning/anchor data. In addition to `Adobe OpenType feature files `__, ufo2ft also supports the `MTI/Monotype format `__. For example, a GPOS table in this format would be stored within the UFO at ``data/com.github.googlei18n.ufo2ft.mtiFeatures/GPOS.mti``. Fallbacks ~~~~~~~~~ Most of the fallbacks have static values. To see what is set for these, look at ``fontInfoData.py`` in the source code. In some cases, the fallback values are dynamically generated from other data in the info object. These are handled internally with functions. Merging TTX ~~~~~~~~~~~ If the UFO data directory has a ``com.github.fonttools.ttx`` folder with TTX files ending with ``.ttx``, these will be merged in the generated font. The index TTX (generated when using using ``ttx -s``) is not required. .. |GitHub Actions status| image:: https://github.com/googlefonts/ufo2ft/workflows/Test%20+%20Deploy/badge.svg .. |PyPI Version| image:: https://img.shields.io/pypi/v/ufo2ft.svg :target: https://pypi.org/project/ufo2ft/ .. |Codecov| image:: https://codecov.io/gh/googlefonts/ufo2ft/branch/master/graph/badge.svg :target: https://codecov.io/gh/googlefonts/ufo2ft .. |Gitter Chat| image:: https://badges.gitter.im/fonttools-dev/ufo2ft.svg :alt: Join the chat at https://gitter.im/fonttools-dev/ufo2ft :target: https://gitter.im/fonttools-dev/ufo2ft?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge Color fonts ~~~~~~~~~~~ ufo2ft supports building ``COLR`` and ``CPAL`` tables. If there is ``com.github.googlei18n.ufo2ft.colorPalettes`` key in font lib, and ``com.github.googlei18n.ufo2ft.colorLayerMapping`` key in the font or in any of the glyphs lib, then ufo2ft will build ``CPAL`` table from the color palettes, and ``COLR`` table from the color layers. ``colorPalettes`` is a array of palettes, each palette is a array of colors and each color is a array of floats representing RGBA colors. For example: .. code:: xml com.github.googlei18n.ufo2ft.colorPalettes 0.26 0.0 0.23 1.0 0.86 0.73 0.28 1.0 ``colorLayerMapping`` is a array of color layers, each color layer is a array of layer name and palette color index. It is a per-glyph key, but if present in the font lib then it will be used for all glyphs that lack it. For example: .. code:: xml com.github.googlei18n.ufo2ft.colorLayerMapping color.1 1 color.2 0 With these this key present, ufo2ft will copy the color layers into individual glyphs and setup ``COLR`` table. Alternatively, if the color layers are already separate UFO glyphs, the ``com.github.googlei18n.ufo2ft.colorLayers`` font lib key can be used. It uses a table keyed by base glyph, and the value is an array of color layers, each color layer is an array of glyph name and palette color index. For example: .. code:: xml com.github.googlei18n.ufo2ft.colorLayers alef-ar alef-ar.color0 2 alefHamzaabove-ar alefHamzaabove-ar.color0 1 alefHamzaabove-ar.color1 2 Setup Notes ~~~~~~~~~~~ If you are installing ufo2ft from source, note that the strict dependency versions in `requirements.txt` are for testing, see `setup.py`'s install_requires and extras_requires for more relaxed dependency requirements. ufo2ft-3.3.1/dev-requirements.txt000066400000000000000000000000611470175262700167730ustar00rootroot00000000000000coverage pytest black isort flake8-bugbear syrupyufo2ft-3.3.1/requirements.txt000066400000000000000000000003011470175262700162140ustar00rootroot00000000000000fonttools[lxml,ufo]==4.54.1 defcon==0.10.3 compreffor==0.5.5 booleanOperations==0.9.0 cffsubr==0.3.0 skia-pathops==0.8.0.post1 fontMath==0.9.4 # alternative UFO implementation ufoLib2==0.16.0 ufo2ft-3.3.1/setup.cfg000066400000000000000000000006531470175262700145630ustar00rootroot00000000000000[wheel] universal = 1 [sdist] formats = zip [aliases] test = pytest [metadata] license_file = LICENSE [tool:pytest] minversion = 2.8 testpaths = tests python_files = *_test.py python_classes = *Test addopts = -r a filterwarnings: ignore:tostring:DeprecationWarning ignore:fromstring:DeprecationWarning ignore:.*bytes:DeprecationWarning:fs.base ignore::DeprecationWarning:fs ignore::DeprecationWarning:pkg_resourcesufo2ft-3.3.1/setup.py000066400000000000000000000040401470175262700144460ustar00rootroot00000000000000#!/usr/bin/env python import sys from setuptools import find_packages, setup needs_pytest = {"pytest", "test"}.intersection(sys.argv) pytest_runner = ["pytest_runner"] if needs_pytest else [] needs_wheel = {"bdist_wheel"}.intersection(sys.argv) wheel = ["wheel"] if needs_wheel else [] with open("README.rst", "r") as f: long_description = f.read() setup( name="ufo2ft", use_scm_version={"write_to": "Lib/ufo2ft/_version.py"}, author="Tal Leming, James Godfrey-Kittle", author_email="tal@typesupply.com", maintainer="Cosimo Lupo", maintainer_email="cosimo@anthrotype.com", description="A bridge between UFOs and FontTools.", long_description=long_description, url="https://github.com/googlefonts/ufo2ft", package_dir={"": "Lib"}, packages=find_packages("Lib"), include_package_data=True, license="MIT", setup_requires=pytest_runner + wheel + ["setuptools_scm"], tests_require=["pytest>=2.8"], install_requires=[ "fonttools[ufo]>=4.50.0", "cffsubr>=0.3.0", "booleanOperations>=0.9.0", "fontMath>=0.9.3", ], extras_require={ "pathops": ["skia-pathops>=0.8.0"], "cffsubr": [], # keep empty for backward compat "compreffor": ["compreffor>=0.5.5"], }, python_requires=">=3.8", classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: Other Environment", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", "Topic :: Software Development :: Libraries :: Python Modules", ], ) ufo2ft-3.3.1/tests/000077500000000000000000000000001470175262700141005ustar00rootroot00000000000000ufo2ft-3.3.1/tests/__init__.py000066400000000000000000000000001470175262700161770ustar00rootroot00000000000000ufo2ft-3.3.1/tests/conftest.py000066400000000000000000000126321470175262700163030ustar00rootroot00000000000000import os from pathlib import Path import py import pytest from fontTools import designspaceLib @pytest.fixture(scope="session", params=["defcon", "ufoLib2"]) def ufo_module(request): return pytest.importorskip(request.param) @pytest.fixture(scope="session") def FontClass(ufo_module): if hasattr(ufo_module.Font, "open"): def ctor(path=None): if path is None: return ufo_module.Font() else: return ufo_module.Font.open(path) return ctor return ufo_module.Font @pytest.fixture(scope="session") def InfoClass(ufo_module): return ufo_module.objects.info.Info @pytest.fixture def datadir(): return py.path.local(py.path.local(__file__).dirname).join("data") @pytest.fixture(scope="session") def data_dir(): return Path(__file__).parent / "data" def getpath(filename): dirname = os.path.dirname(__file__) return os.path.join(dirname, "data", filename) @pytest.fixture def layertestrgufo(FontClass): font = FontClass(getpath("LayerFont-Regular.ufo")) return font @pytest.fixture def layertestbdufo(FontClass): font = FontClass(getpath("LayerFont-Bold.ufo")) return font @pytest.fixture def designspace(layertestrgufo, layertestbdufo): ds = designspaceLib.DesignSpaceDocument() a1 = designspaceLib.AxisDescriptor() a1.tag = "wght" a1.name = "Weight" a1.default = a1.minimum = 350 a1.maximum = 625 ds.addAxis(a1) s1 = designspaceLib.SourceDescriptor() s1.name = "Layer Font Regular" s1.familyName = "Layer Font" s1.styleName = "Regular" s1.filename = "LayerFont-Regular.ufo" s1.location = {"Weight": 350} s1.font = layertestrgufo ds.addSource(s1) s2 = designspaceLib.SourceDescriptor() s2.name = "Layer Font Medium" s2.familyName = "Layer Font" s2.styleName = "Medium" s2.filename = "LayerFont-Regular.ufo" s2.layerName = "Medium" s2.location = {"Weight": 450} s2.font = layertestrgufo ds.addSource(s2) s3 = designspaceLib.SourceDescriptor() s3.name = "Layer Font Bold" s3.familyName = "Layer Font" s3.styleName = "Bold" s3.filename = "LayerFont-Bold.ufo" s3.location = {"Weight": 625} s3.font = layertestbdufo ds.addSource(s3) return ds @pytest.fixture def designspace_v5(FontClass): def draw_rectangle(pen, x_offset, y_offset): pen.moveTo((0 + x_offset, 0 + y_offset)) pen.lineTo((10 + x_offset, 0 + y_offset)) pen.lineTo((10 + x_offset, 10 + 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 # to offset the drawings so we get some variation. if is_sans: draw_rectangle(glyph.getPen(), 10 * number, 0) else: draw_rectangle(glyph.getPen(), -10 * number, -20) draw_rectangle(glyph.getPen(), 10 * number, 20) ds5 = designspaceLib.DesignSpaceDocument.fromfile( "tests/data/DSv5/test_v5_MutatorSans_and_Serif.designspace" ) sources = {} # Create base UFOs for index, source in enumerate(ds5.sources): 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 # Fill in sparse UFOs for index, source in enumerate(ds5.sources): if source.layerName is None: continue font = sources[source.filename] layer = font.newLayer(source.layerName) for name in ("I", "S", "I.narrow", "S.closed"): glyph = layer.newGlyph(name) draw_something(glyph, index, "Serif" not in source.filename) # Assign UFOs to their attribute for source in ds5.sources: source.font = sources[source.filename] return ds5 ufo2ft-3.3.1/tests/data/000077500000000000000000000000001470175262700150115ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/000077500000000000000000000000001470175262700213025ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/features.fea000066400000000000000000000002601470175262700235730ustar00rootroot00000000000000# Prefix: Languagesystems languagesystem DFLT dflt; languagesystem ory2 dflt; languagesystem latn dflt; # This is needed in our full builds because we use feature variations ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/fontinfo.plist000066400000000000000000000006271470175262700242060ustar00rootroot00000000000000 ascender 1069 capHeight 714 descender -293 familyName Alternates ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/000077500000000000000000000000001470175262700226105ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/contents.plist000066400000000000000000000014551470175262700255270ustar00rootroot00000000000000 ka-oriya ka-oriya.glif ka-oriya.BRACKET.varAlt01 ka-oriya.B_R_A_C_K_E_T_.varA_lt01.glif ka-oriya.below ka-oriya.below.glif lVocalicMatra-oriya lV_ocalicM_atra-oriya.glif lVocalicMatra-oriya.BRACKET.varAlt01 lV_ocalicM_atra-oriya.B_R_A_C_K_E_T_.varA_lt01.glif uuMatra-oriya uuM_atra-oriya.glif uuMatra-oriya.BRACKET.varAlt01 uuM_atra-oriya.B_R_A_C_K_E_T_.varA_lt01.glif ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/ka-oriya.B_R_A_C_K_E_T_.varA_lt01.glif000066400000000000000000000063211470175262700312240ustar00rootroot00000000000000 kaorya com.schriftgestaltung.Glyphs._originalLayerName ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/ka-oriya.below.glif000066400000000000000000000053601470175262700263020ustar00rootroot00000000000000 kasubscriptorya RMXScaler height 78 verticalShift -560 width 85 com.schriftgestaltung.Glyphs.category Mark com.schriftgestaltung.Glyphs.glyph.leftMetricsKey na-oriya.below com.schriftgestaltung.Glyphs.originalWidth 449 com.schriftgestaltung.Glyphs.subCategory Nonspacing public.markColor 0.67,0.95,0.38,1 ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/ka-oriya.glif000066400000000000000000000061271470175262700251750ustar00rootroot00000000000000 kaorya lV_ocalicM_atra-oriya.B_R_A_C_K_E_T_.varA_lt01.glif000066400000000000000000000052371470175262700335760ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs lvocalicvowelsignorya com.schriftgestaltung.Glyphs._originalLayerName com.schriftgestaltung.Glyphs.originalWidth 727.77778 public.markColor 0.98,0.36,0.67,1 ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/lV_ocalicM_atra-oriya.glif000066400000000000000000000051151470175262700276150ustar00rootroot00000000000000 lvocalicvowelsignorya com.schriftgestaltung.Glyphs.originalWidth 727.77778 public.markColor 0.98,0.36,0.67,1 ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/layerinfo.plist000066400000000000000000000013371470175262700256610ustar00rootroot00000000000000 lib com.schriftgestaltung.layerId 41544896-EF4D-4C26-89A7-324DAFD89EC7 com.schriftgestaltung.layerOrderInGlyph.ka-oriya 6 com.schriftgestaltung.layerOrderInGlyph.ka-oriya.below 4 com.schriftgestaltung.layerOrderInGlyph.lVocalicMatra-oriya 6 com.schriftgestaltung.layerOrderInGlyph.uuMatra-oriya 6 ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/uuM_atra-oriya.B_R_A_C_K_E_T_.varA_lt01.glif000066400000000000000000000035131470175262700324060ustar00rootroot00000000000000 uuvowelsignorya com.schriftgestaltung.Glyphs._originalLayerName com.schriftgestaltung.Glyphs.originalWidth 800 public.markColor 0.98,0.36,0.67,1 ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/glyphs/uuM_atra-oriya.glif000066400000000000000000000033711470175262700263550ustar00rootroot00000000000000 uuvowelsignorya com.schriftgestaltung.Glyphs.originalWidth 800 public.markColor 0.98,0.36,0.67,1 ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700252550ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/lib.plist000066400000000000000000000005521470175262700231270ustar00rootroot00000000000000 public.glyphOrder ka-oriya uuMatra-oriya lVocalicMatra-oriya ufo2ft-3.3.1/tests/data/Alternates-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700241700ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/Bug108.ttx000066400000000000000000000027761470175262700165340ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/Bug108.ufo/000077500000000000000000000000001470175262700165475ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/Bug108.ufo/features.fea000066400000000000000000000000351470175262700210400ustar00rootroot00000000000000include(Bug108_included.fea) ufo2ft-3.3.1/tests/data/Bug108.ufo/fontinfo.plist000066400000000000000000000012731470175262700214510ustar00rootroot00000000000000 familyName Bug 108 note https://github.com/googlei18n/ufo2ft/issues/108 unitsPerEm 1000 xHeight 500 ascender 750 capHeight 750 descender -250 postscriptUnderlinePosition -200 postscriptUnderlineThickness 20 ufo2ft-3.3.1/tests/data/Bug108.ufo/glyphs/000077500000000000000000000000001470175262700200555ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/Bug108.ufo/glyphs/_notdef.glif000066400000000000000000000007531470175262700223430ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/Bug108.ufo/glyphs/a.glif000066400000000000000000000004361470175262700211430ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/Bug108.ufo/glyphs/b.glif000066400000000000000000000005111470175262700211360ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/Bug108.ufo/glyphs/c.glif000066400000000000000000000006241470175262700211440ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/Bug108.ufo/glyphs/contents.plist000066400000000000000000000006371470175262700227750ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif space space.glif ufo2ft-3.3.1/tests/data/Bug108.ufo/glyphs/space.glif000066400000000000000000000001771470175262700220200ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/Bug108.ufo/groups.plist000066400000000000000000000002761470175262700211500ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/Bug108.ufo/layercontents.plist000066400000000000000000000004231470175262700225150ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/Bug108.ufo/lib.plist000066400000000000000000000011251470175262700203710ustar00rootroot00000000000000 public.glyphOrder .notdef space a b c public.postscriptNames a uni0061 b uni0062 c uni0063 space uni0020 ufo2ft-3.3.1/tests/data/Bug108.ufo/metainfo.plist000066400000000000000000000004431470175262700214270ustar00rootroot00000000000000 creator copy-paste formatVersion 3 ufo2ft-3.3.1/tests/data/Bug108_included.fea000066400000000000000000000000461470175262700203030ustar00rootroot00000000000000feature kern { pos a b -10; } kern; ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/000077500000000000000000000000001470175262700174475ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/fontinfo.plist000066400000000000000000000016051470175262700223500ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 familyName ColorTest guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/glyphs/000077500000000000000000000000001470175262700207555ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/glyphs/a.color1.glif000066400000000000000000000011731470175262700232400ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/glyphs/a.color2.glif000066400000000000000000000005161470175262700232410ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/glyphs/a.glif000066400000000000000000000005331470175262700220410ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/glyphs/contents.plist000066400000000000000000000005431470175262700236710ustar00rootroot00000000000000 a a.glif a.color1 a.color1.glif a.color2 a.color2.glif ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/glyphs/layerinfo.plist000066400000000000000000000003671470175262700240300ustar00rootroot00000000000000 color 1,0.75,0,0.7 ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/layercontents.plist000066400000000000000000000004331470175262700234160ustar00rootroot00000000000000 foreground glyphs ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/lib.plist000066400000000000000000000036761470175262700213060ustar00rootroot00000000000000 com.defcon.sortDescriptor ascending Latin-1 type characterSet com.github.googlei18n.ufo2ft.colorLayers a Format 1 Layers Format 10 Glyph a.color1 Paint Format 2 PaletteIndex 0 Format 10 Glyph a.color2 Paint Format 2 PaletteIndex 1 com.github.googlei18n.ufo2ft.colorPalettes 1 0.3 0.1 1 0 0.4 0.8 1 public.glyphOrder space a a.color1 a.color2 ufo2ft-3.3.1/tests/data/COLRv1Test.ufo/metainfo.plist000066400000000000000000000004761470175262700223350ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/000077500000000000000000000000001470175262700230455ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/fontinfo.plist000066400000000000000000000047551470175262700257570ustar00rootroot00000000000000 ascender 739.0 capHeight 694.0 copyright Copyright (c) 2009--2017 The Cantarell Authors descender -217.0 familyName Cantarell guidelines italicAngle -0.0 openTypeHeadCreated 2009/03/13 21:44:13 openTypeNameDesigner Dave Crossland, Nikolaus Waxweiler, Jacques Le Bailly, Eben Sorkin, Alexei Vanyashin openTypeNameDesignerURL http://abattis.org openTypeNameManufacturerURL http://abattis.org openTypeOS2Panose 2 0 5 3 0 0 0 0 0 0 openTypeOS2Type 3 openTypeOS2VendorID ABAT postscriptBlueScale 0.0625 postscriptBlueValues -10.0 0.0 482.0 492.0 694.0 704.0 739.0 749.0 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues -227.0 -217.0 postscriptStemSnapH 80 postscriptStemSnapV 70 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Regular unitsPerEm 1000 versionMajor 0 versionMinor 111 xHeight 482.0 ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/glyphs/000077500000000000000000000000001470175262700243535ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/glyphs/circumflexcomb.loclV_I_E_T_.glif000066400000000000000000000015211470175262700324330ustar00rootroot00000000000000 RMXScaler height 80 com.schriftgestaltung.Glyphs.originalWidth 386.0 ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/glyphs/circumflexcomb_tildecomb.glif000066400000000000000000000006011470175262700322370ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.originalWidth 432.0 ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/glyphs/contents.plist000066400000000000000000000011031470175262700272600ustar00rootroot00000000000000 circumflexcomb.loclVIET circumflexcomb.loclV_I_E_T_.glif circumflexcomb_tildecomb circumflexcomb_tildecomb.glif o o.glif ocircumflextilde ocircumflextilde.glif tildecomb.loclVIET tildecomb.loclV_I_E_T_.glif ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/glyphs/o.glif000066400000000000000000000027271470175262700254640ustar00rootroot00000000000000 o ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/glyphs/ocircumflextilde.glif000066400000000000000000000004011470175262700305530ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/glyphs/tildecomb.loclV_I_E_T_.glif000066400000000000000000000024511470175262700313760ustar00rootroot00000000000000 RMXScaler height 80 com.schriftgestaltung.Glyphs.originalWidth 450.0 ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/layercontents.plist000066400000000000000000000004371470175262700270200ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/lib.plist000066400000000000000000000016571470175262700247010ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.filters include o ocircumflextilde circumflexcomb.loclVIET tildecomb.loclVIET circumflexcomb_tildecomb name propagateAnchors pre public.glyphOrder o ocircumflextilde circumflexcomb.loclVIET tildecomb.loclVIET circumflexcomb_tildecomb ufo2ft-3.3.1/tests/data/CantarellAnchorPropagation.ufo/metainfo.plist000066400000000000000000000004761470175262700257330ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/ColorTest.ufo/000077500000000000000000000000001470175262700175175ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ColorTest.ufo/fontinfo.plist000066400000000000000000000016051470175262700224200ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 familyName ColorTest guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color1/000077500000000000000000000000001470175262700222235ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color1/a.glif000066400000000000000000000011641470175262700233100ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color1/b.glif000066400000000000000000000011641470175262700233110ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color1/c.glif000066400000000000000000000011641470175262700233120ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color1/contents.plist000066400000000000000000000005071470175262700251370ustar00rootroot00000000000000 a a.glif b b.glif c c.glif ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color1/layerinfo.plist000066400000000000000000000003671470175262700252760ustar00rootroot00000000000000 color 0,1,0.25,0.7 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color2/000077500000000000000000000000001470175262700222245ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color2/a.glif000066400000000000000000000005071470175262700233110ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color2/b.glif000066400000000000000000000002301470175262700233030ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color2/c.glif000066400000000000000000000005071470175262700233130ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color2/contents.plist000066400000000000000000000005071470175262700251400ustar00rootroot00000000000000 a a.glif b b.glif c c.glif ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs.color2/layerinfo.plist000066400000000000000000000003641470175262700252740ustar00rootroot00000000000000 color 0,1,1,0.7 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs/000077500000000000000000000000001470175262700210255ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs/a.glif000066400000000000000000000005331470175262700221110ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs/b.glif000066400000000000000000000012441470175262700221120ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.colorLayerMapping color1 1 color2 0 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs/c.glif000066400000000000000000000012441470175262700221130ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.colorLayerMapping color2 1 color1 0 ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs/contents.plist000066400000000000000000000005071470175262700237410ustar00rootroot00000000000000 a a.glif b b.glif c c.glif ufo2ft-3.3.1/tests/data/ColorTest.ufo/glyphs/layerinfo.plist000066400000000000000000000003671470175262700241000ustar00rootroot00000000000000 color 1,0.75,0,0.7 ufo2ft-3.3.1/tests/data/ColorTest.ufo/layercontents.plist000066400000000000000000000007231470175262700234700ustar00rootroot00000000000000 foreground glyphs color1 glyphs.color1 color2 glyphs.color2 ufo2ft-3.3.1/tests/data/ColorTest.ufo/lib.plist000066400000000000000000000023401470175262700213410ustar00rootroot00000000000000 com.defcon.sortDescriptor ascending Latin-1 type characterSet com.github.googlei18n.ufo2ft.colorLayerMapping color1 0 color2 1 com.github.googlei18n.ufo2ft.colorPalettes 1 0.3 0.1 1 0 0.4 0.8 1 public.glyphOrder space a b c ufo2ft-3.3.1/tests/data/ColorTest.ufo/metainfo.plist000066400000000000000000000004761470175262700224050ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/000077500000000000000000000000001470175262700201715ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/fontinfo.plist000066400000000000000000000016051470175262700230720ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 familyName ColorTest guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/glyphs/000077500000000000000000000000001470175262700214775ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/glyphs/a.color1.glif000066400000000000000000000011731470175262700237620ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/glyphs/a.color2.glif000066400000000000000000000005161470175262700237630ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/glyphs/a.glif000066400000000000000000000005331470175262700225630ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/glyphs/contents.plist000066400000000000000000000005431470175262700244130ustar00rootroot00000000000000 a a.glif a.color1 a.color1.glif a.color2 a.color2.glif ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/glyphs/layerinfo.plist000066400000000000000000000003671470175262700245520ustar00rootroot00000000000000 color 1,0.75,0,0.7 ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/layercontents.plist000066400000000000000000000004331470175262700241400ustar00rootroot00000000000000 foreground glyphs ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/lib.plist000066400000000000000000000024521470175262700220170ustar00rootroot00000000000000 com.defcon.sortDescriptor ascending Latin-1 type characterSet com.github.googlei18n.ufo2ft.colorLayers a a.color1 0 a.color2 1 com.github.googlei18n.ufo2ft.colorPalettes 1 0.3 0.1 1 0 0.4 0.8 1 public.glyphOrder space a a.color1 a.color2 ufo2ft-3.3.1/tests/data/ColorTestRaw.ufo/metainfo.plist000066400000000000000000000004761470175262700230570ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/000077500000000000000000000000001470175262700231555ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/fontinfo.plist000066400000000000000000000022241470175262700260540ustar00rootroot00000000000000 ascender 800 capHeight 700 descender -200 familyName Component Transform Test italicAngle 0 openTypeHeadCreated 2023/02/03 11:54:40 openTypeOS2Type 3 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleMapFamilyName Component Transform Test styleMapStyleName bold styleName Bold unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/glyphs/000077500000000000000000000000001470175262700244635ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/glyphs/component_different_transform.glif000066400000000000000000000010641470175262700334520ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment -1 index 0 name no_component ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/glyphs/component_no_transform.glif000066400000000000000000000002701470175262700321160ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/glyphs/component_same_transform.glif000066400000000000000000000010611470175262700324260ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment -1 index 0 name no_component ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/glyphs/contents.plist000066400000000000000000000010321470175262700273710ustar00rootroot00000000000000 component_different_transform component_different_transform.glif component_no_transform component_no_transform.glif component_same_transform component_same_transform.glif no_component no_component.glif ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/glyphs/no_component.glif000066400000000000000000000005121470175262700300220ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700271300ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/lib.plist000066400000000000000000000035061470175262700250040ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.filters name eraseOpenCorners namespace glyphsLib.filters pre 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 700 com.schriftgestaltung.customParameter.GSFontMaster.widthValue 100 com.schriftgestaltung.fontMasterOrder 1 com.schriftgestaltung.weightValue 700 com.schriftgestaltung.widthValue 100 public.glyphOrder component_different_transform component_no_transform component_same_transform no_component ufo2ft-3.3.1/tests/data/ComponentTransformTest-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700260430ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/000077500000000000000000000000001470175262700236765ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/fontinfo.plist000066400000000000000000000022321470175262700265740ustar00rootroot00000000000000 ascender 800 capHeight 700 descender -200 familyName Component Transform Test italicAngle 0 openTypeHeadCreated 2023/02/03 11:54:40 openTypeOS2Type 3 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleMapFamilyName Component Transform Test styleMapStyleName regular styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/glyphs/000077500000000000000000000000001470175262700252045ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/glyphs/component_different_transform.glif000066400000000000000000000003331470175262700341710ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/glyphs/component_no_transform.glif000066400000000000000000000002701470175262700326370ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/glyphs/component_same_transform.glif000066400000000000000000000003261470175262700331520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/glyphs/contents.plist000066400000000000000000000010321470175262700301120ustar00rootroot00000000000000 component_different_transform component_different_transform.glif component_no_transform component_no_transform.glif component_same_transform component_same_transform.glif no_component no_component.glif ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/glyphs/no_component.glif000066400000000000000000000005121470175262700305430ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700276510ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/lib.plist000066400000000000000000000035061470175262700255250ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.filters name eraseOpenCorners namespace glyphsLib.filters pre 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 400 com.schriftgestaltung.customParameter.GSFontMaster.widthValue 100 com.schriftgestaltung.fontMasterOrder 0 com.schriftgestaltung.weightValue 400 com.schriftgestaltung.widthValue 100 public.glyphOrder component_different_transform component_no_transform component_same_transform no_component ufo2ft-3.3.1/tests/data/ComponentTransformTest-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700265640ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/000077500000000000000000000000001470175262700235045ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/features.fea000066400000000000000000000007271470175262700260050ustar00rootroot00000000000000# Prefix: Languagesystems # automatic languagesystem DFLT dflt; languagesystem arab dflt; feature aalt { # automatic feature init; feature medi; feature fina; } aalt; feature ccmp { sub beh-ar by behDotless-ar dotbelow-ar; } ccmp; feature init { # automatic sub behDotless-ar by behDotless-ar.init; } init; feature medi { # automatic sub behDotless-ar by behDotless-ar.medi; } medi; feature fina { # automatic sub behDotless-ar by behDotless-ar.fina; } fina; ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/fontinfo.plist000066400000000000000000000037221470175262700264070ustar00rootroot00000000000000 ascender 800 capHeight 700 descender -200 familyName Contextual Anchors Test guidelines angle 0 name [locked] x -126 y 90 italicAngle 0 openTypeHeadCreated 2023/07/31 15:25:34 openTypeOS2Type 3 postscriptBlueValues -12 0 480 492 700 712 800 812 postscriptOtherBlues -212 -200 postscriptStemSnapH 80 88 91 postscriptStemSnapV 90 93 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 480 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/000077500000000000000000000000001470175262700250125ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/beh-ar.glif000066400000000000000000000004011470175262700270060ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/behD_otless-ar.fina.glif000066400000000000000000000013441470175262700314260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/behD_otless-ar.glif000066400000000000000000000013101470175262700305030ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/behD_otless-ar.init.alt.glif000066400000000000000000000015651470175262700322400ustar00rootroot00000000000000 public.objectLibs 051ABFBB-3A07-48C5-A5A3-82D4291DEAD1 GPOS_Context lookupflag UseMarrkFilteringSet [twodotsverticalbelow]; reh-ar * ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/behD_otless-ar.init.glif000066400000000000000000000026761470175262700314650ustar00rootroot00000000000000 public.objectLibs 50838DB6-85ED-49AE-8935-C6201FC3FCD8 GPOS_Context lookupflag UseMarrkFilteringSet [twodotsverticalbelow]; reh-ar * 575ADDBE-67A1-4781-8244-18DEC9396567 GPOS_Context reh-ar * 8AEC6EF0-8B9B-481A-AC0E-A95051FD2D12 GPOS_Context lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar * behDotess-ar.medi & ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/behD_otless-ar.medi.glif000066400000000000000000000010461470175262700314260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/behT_wodotsbelowD_otabove-ar.glif000066400000000000000000000005411470175262700334120ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/contents.plist000066400000000000000000000021251470175262700277240ustar00rootroot00000000000000 beh-ar beh-ar.glif behDotless-ar behD_otless-ar.glif behDotless-ar.fina behD_otless-ar.fina.glif behDotless-ar.init behD_otless-ar.init.glif behDotless-ar.init.alt behD_otless-ar.init.alt.glif behDotless-ar.medi behD_otless-ar.medi.glif behTwodotsbelowDotabove-ar behT_wodotsbelowD_otabove-ar.glif dotabove-ar dotabove-ar.glif dotbelow-ar dotbelow-ar.glif reh-ar reh-ar.glif twodotshorizontalbelow-ar twodotshorizontalbelow-ar.glif twodotsverticalbelow-ar twodotsverticalbelow-ar.glif ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/dotabove-ar.glif000066400000000000000000000011551470175262700300620ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment 1 index 0 name dotbelow-ar com.schriftgestaltung.Glyphs.originalWidth 300 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/dotbelow-ar.glif000066400000000000000000000014121470175262700300720ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.originalWidth 300 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/layerinfo.plist000066400000000000000000000030261470175262700300600ustar00rootroot00000000000000 lib com.schriftgestaltung.layerId 3E7589AA-8194-470F-8E2F-13C1C581BE24 com.schriftgestaltung.layerOrderInGlyph.beh-ar 1 com.schriftgestaltung.layerOrderInGlyph.behDotless-ar 1 com.schriftgestaltung.layerOrderInGlyph.behDotless-ar.fina 1 com.schriftgestaltung.layerOrderInGlyph.behDotless-ar.init 1 com.schriftgestaltung.layerOrderInGlyph.behDotless-ar.init.alt 1 com.schriftgestaltung.layerOrderInGlyph.behDotless-ar.medi 1 com.schriftgestaltung.layerOrderInGlyph.behTwodotsbelowDotabove-ar 1 com.schriftgestaltung.layerOrderInGlyph.dotabove-ar 1 com.schriftgestaltung.layerOrderInGlyph.dotbelow-ar 1 com.schriftgestaltung.layerOrderInGlyph.reh-ar 1 com.schriftgestaltung.layerOrderInGlyph.twodotshorizontalbelow-ar 1 com.schriftgestaltung.layerOrderInGlyph.twodotsverticalbelow-ar 1 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/reh-ar.glif000066400000000000000000000010421470175262700270300ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/twodotshorizontalbelow-ar.glif000066400000000000000000000024101470175262700331200ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.originalWidth 440 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/glyphs/twodotsverticalbelow-ar.glif000066400000000000000000000024261470175262700325470ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.originalWidth 300 ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700274570ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/lib.plist000066400000000000000000000117711470175262700253360ustar00rootroot00000000000000 GSOffsetHorizontal 45 GSOffsetMakeStroke 1 GSOffsetVertical 44 GSRoughenHorizontal 15 GSRoughenSegmentLength 15 GSRoughenVertical 10 com.github.googlei18n.ufo2ft.featureWriters class CursFeatureWriter class KernFeatureWriter class ContextualMarkFeatureWriter module glyphsLib.featureWriters.markFeatureWriter class GdefFeatureWriter com.github.googlei18n.ufo2ft.filters name eraseOpenCorners namespace glyphsLib.filters pre com.schriftgestaltung.customParameter.GSFont.Propagate Anchors 0 com.schriftgestaltung.customParameter.GSFont.Write DisplayStrings 0 com.schriftgestaltung.customParameter.GSFont.Write lastChange 0 com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment com.schriftgestaltung.customParameter.GSFont.useNiceNames 1 com.schriftgestaltung.customParameter.GSFontMaster.TTFStems horizontal 1 name Thin width 80 horizontal 1 name Lowercase width 88 horizontal 1 name Uppercase width 91 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 90 com.schriftgestaltung.customParameter.GSFontMaster.widthValue 100 com.schriftgestaltung.fontMasterID 3E7589AA-8194-470F-8E2F-13C1C581BE24 com.schriftgestaltung.fontMasterOrder 1 com.schriftgestaltung.weightValue 90 com.schriftgestaltung.widthValue 100 public.glyphOrder behDotless-ar behDotless-ar.fina behDotless-ar.medi behDotless-ar.init behDotless-ar.init.alt beh-ar behTwodotsbelowDotabove-ar reh-ar dotabove-ar dotbelow-ar twodotsverticalbelow-ar twodotshorizontalbelow-ar public.postscriptNames beh-ar uni0628 behDotless-ar uni066E behDotless-ar.fina uni066E.fina behDotless-ar.init uni066E.init behDotless-ar.init.alt uni066E.init.alt behDotless-ar.medi uni066E.medi behTwodotsbelowDotabove-ar uni0754 dotabove-ar dotabovear dotbelow-ar dotbelowar reh-ar uni0631 twodotshorizontalbelow-ar twodotshorizontalbelowar twodotsverticalbelow-ar twodotsverticalbelowar ufo2ft-3.3.1/tests/data/ContextualAnchorsTest-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700263720ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/000077500000000000000000000000001470175262700210665ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/fontinfo.plist000066400000000000000000000136351470175262700237750ustar00rootroot00000000000000 ascender 1069 capHeight 714 copyright Copyright 2019 Google Inc. All Rights Reserved. descender -293 familyName Noto Sans guidelines color 0,0,0,1 name m y 250 italicAngle 0 openTypeGaspRangeRecords rangeGaspBehavior 0 1 2 3 rangeMaxPPEM 65535 openTypeHeadCreated 2019/06/28 21:53:11 openTypeHeadFlags openTypeHheaAscender 1069 openTypeHheaDescender -293 openTypeHheaLineGap 0 openTypeNameDescription Designed by Monotype design team. openTypeNameDesigner Monotype Design Team openTypeNameDesignerURL http://www.monotype.com/studio openTypeNameLicense This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. openTypeNameLicenseURL http://scripts.sil.org/OFL openTypeNameManufacturer Monotype Imaging Inc. openTypeNameManufacturerURL http://www.google.com/get/noto/ openTypeNamePreferredFamilyName Noto Sans openTypeNamePreferredSubfamilyName Regular openTypeNameVersion Version 2.001 openTypeOS2CodePageRanges 0 1 2 3 4 7 8 openTypeOS2Panose 2 11 5 2 4 5 4 2 2 4 openTypeOS2Selection 8 openTypeOS2Type 2 openTypeOS2TypoAscender 1069 openTypeOS2TypoDescender -293 openTypeOS2TypoLineGap 0 openTypeOS2UnicodeRanges 0 1 2 3 4 5 6 7 9 29 30 31 32 33 34 35 36 45 62 64 67 69 91 116 openTypeOS2VendorID GOOG openTypeOS2WeightClass 400 openTypeOS2WidthClass 5 openTypeOS2WinAscent 1069 openTypeOS2WinDescent 293 postscriptBlueFuzz 1 postscriptBlueScale 0.039625 postscriptBlueShift 7 postscriptBlueValues -20 0 postscriptFontName NotoSans-Regular postscriptForceBold postscriptFullName Noto Sans Regular postscriptIsFixedPitch postscriptStemSnapH 68 79 postscriptStemSnapV 75 90 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 postscriptWeightName Regular styleMapFamilyName Noto Sans styleMapStyleName regular styleName Regular trademark Noto is a trademark of Google Inc. unitsPerEm 1000 versionMajor 2 versionMinor 2 xHeight 536 ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/glyphs/000077500000000000000000000000001470175262700223745ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/glyphs/contents.plist000066400000000000000000000005751470175262700253150ustar00rootroot00000000000000 graphemejoinercomb graphemejoinercomb.glif uniFFFC uniF_F_F_C_.glif xxx xxx.glif ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/glyphs/graphemejoinercomb.glif000066400000000000000000000245731470175262700271120ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.category Mark com.schriftgestaltung.Glyphs.lastChange 2017/12/05 22:37:40 com.schriftgestaltung.Glyphs.script com.schriftgestaltung.Glyphs.subCategory Nonspacing ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/glyphs/layerinfo.plist000066400000000000000000000011371470175262700254430ustar00rootroot00000000000000 lib com.fontlab.layer.locked com.fontlab.layer.name Regular com.fontlab.layer.opacity 1 com.fontlab.layer.service com.fontlab.layer.visible com.fontlab.layer.wireframe ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/glyphs/uniF_F_F_C_.glif000066400000000000000000000161241470175262700252570ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.category Symbol com.schriftgestaltung.Glyphs.lastChange 2017/12/05 22:37:38 com.schriftgestaltung.Glyphs.script com.schriftgestaltung.Glyphs.subCategory Other ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/glyphs/xxx.glif000066400000000000000000000006211470175262700240650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/layercontents.plist000066400000000000000000000004371470175262700250410ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/lib.plist000066400000000000000000000012211470175262700227050ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.filters include uniFFFC graphemejoinercomb name sortContours public.glyphOrder graphemejoinercomb uniFFFC xxx ufo2ft-3.3.1/tests/data/ContourOrderTest.ufo/metainfo.plist000066400000000000000000000004761470175262700237540ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/DSv5/000077500000000000000000000000001470175262700155725ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DSv5/MutatorSansVariable_Weight-CFF2.ttx000066400000000000000000000442771470175262700242640ustar00rootroot00000000000000 Weight Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Bold Condensed MutatorMathTest-SansBoldCondensed MutatorMathTest-SansLightCondensed Regular serif Sans weight Light Medium Bold width Condensed New Font Regular 0.000;NONE;NewFont-Regular New Font Regular Version 0.000 NewFont-Regular Weight Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Bold Condensed MutatorMathTest-SansBoldCondensed MutatorMathTest-SansLightCondensed Regular serif Sans weight Light Medium Bold width Condensed 50 -200 rmoveto 400 1000 -400 -1000 hlineto 50 50 rmoveto 900 300 -900 -300 vlineto 0 40 10 1 blend hmoveto 10 10 -10 hlineto 0 40 10 1 blend hmoveto 10 10 -10 hlineto 0 40 10 1 blend hmoveto 10 10 -10 hlineto 0 40 10 1 blend hmoveto 10 10 -10 hlineto 1 vsindex 0 10 1 blend hmoveto 10 10 -10 hlineto wght 0x0 300.0 300.0 700.0 256 ufo2ft-3.3.1/tests/data/DSv5/MutatorSansVariable_Weight-TTF.ttx000066400000000000000000000501441470175262700242270ustar00rootroot00000000000000 PUSHB[ ] 0 FDEF[ ] POP[ ] ENDF[ ] PUSHB[ ] 4 3 INSTCTRL[ ] PUSHB[ ] 0 0 SVTCA[0] MDRP[01100] Weight Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Bold Condensed MutatorMathTest-SansBoldCondensed MutatorMathTest-SansLightCondensed Regular serif Sans weight Light Medium Bold width Condensed New Font Regular 0.000;NONE;NewFont-Regular New Font Regular Version 0.000 NewFont-Regular Weight Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Bold Condensed MutatorMathTest-SansBoldCondensed MutatorMathTest-SansLightCondensed Regular serif Sans weight Light Medium Bold width Condensed wght 0x0 300.0 300.0 700.0 256 ufo2ft-3.3.1/tests/data/DSv5/MutatorSansVariable_Weight_Width-CFF2.ttx000066400000000000000000000740771470175262700254240ustar00rootroot00000000000000 Weight Width Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Bold Condensed MutatorMathTest-SansBoldCondensed Sans Light Extended MutatorMathTest-SansLightExtended Sans Bold Extended MutatorMathTest-SansBoldExtended Sans Medium MutatorMathTest-SansMedium MutatorMathTest-SansMedium Sans Bold MutatorMathTest-SansBold Sans Medium Extended MutatorMathTest-SansMediumExtended MutatorMathTest-SansLightCondensed Regular serif Sans weight Light Medium Bold width Condensed Normal Extended S1 S2 New Font Regular 0.000;NONE;NewFont-Regular New Font Regular Version 0.000 NewFont-Regular Weight Width Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Bold Condensed MutatorMathTest-SansBoldCondensed Sans Light Extended MutatorMathTest-SansLightExtended Sans Bold Extended MutatorMathTest-SansBoldExtended Sans Medium MutatorMathTest-SansMedium MutatorMathTest-SansMedium Sans Bold MutatorMathTest-SansBold Sans Medium Extended MutatorMathTest-SansMediumExtended MutatorMathTest-SansLightCondensed Regular serif Sans weight Light Medium Bold width Condensed Normal Extended S1 S2 50 -200 rmoveto 400 1000 -400 -1000 hlineto 50 50 rmoveto 900 300 -900 -300 vlineto 0 40 10 20 -10 0 14.30922 1 blend hmoveto 10 10 -10 hlineto 0 40 10 20 -10 0 14.30922 1 blend hmoveto 10 10 -10 hlineto 0 40 10 20 -10 0 14.30922 1 blend hmoveto 10 10 -10 hlineto 0 40 10 20 -10 0 14.30922 1 blend hmoveto 10 10 -10 hlineto 1 vsindex 0 10 20 0 1 blend hmoveto 10 10 -10 hlineto wght 0x0 300.0 300.0 700.0 256 wdth 0x0 50.0 50.0 200.0 257 ufo2ft-3.3.1/tests/data/DSv5/MutatorSansVariable_Weight_Width-TTF.ttx000066400000000000000000000772451470175262700254010ustar00rootroot00000000000000 PUSHB[ ] 0 FDEF[ ] POP[ ] ENDF[ ] PUSHB[ ] 4 3 INSTCTRL[ ] PUSHB[ ] 0 0 SVTCA[0] MDRP[01100] Weight Width Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Bold Condensed MutatorMathTest-SansBoldCondensed Sans Light Extended MutatorMathTest-SansLightExtended Sans Bold Extended MutatorMathTest-SansBoldExtended Sans Medium MutatorMathTest-SansMedium MutatorMathTest-SansMedium Sans Bold MutatorMathTest-SansBold Sans Medium Extended MutatorMathTest-SansMediumExtended MutatorMathTest-SansLightCondensed Regular serif Sans weight Light Medium Bold width Condensed Normal Extended S1 S2 New Font Regular 0.000;NONE;NewFont-Regular New Font Regular Version 0.000 NewFont-Regular Weight Width Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Bold Condensed MutatorMathTest-SansBoldCondensed Sans Light Extended MutatorMathTest-SansLightExtended Sans Bold Extended MutatorMathTest-SansBoldExtended Sans Medium MutatorMathTest-SansMedium MutatorMathTest-SansMedium Sans Bold MutatorMathTest-SansBold Sans Medium Extended MutatorMathTest-SansMediumExtended MutatorMathTest-SansLightCondensed Regular serif Sans weight Light Medium Bold width Condensed Normal Extended S1 S2 wght 0x0 300.0 300.0 700.0 256 wdth 0x0 50.0 50.0 200.0 257 ufo2ft-3.3.1/tests/data/DSv5/MutatorSansVariable_Width-CFF2.ttx000066400000000000000000000433711470175262700241060ustar00rootroot00000000000000 Width Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Light Extended MutatorMathTest-SansLightExtended MutatorMathTest-SansLightCondensed Regular serif Sans weight Light width Condensed Normal Extended New Font Regular 0.000;NONE;NewFont-Regular New Font Regular Version 0.000 NewFont-Regular Width Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Light Extended MutatorMathTest-SansLightExtended MutatorMathTest-SansLightCondensed Regular serif Sans weight Light width Condensed Normal Extended 50 -200 rmoveto 400 1000 -400 -1000 hlineto 50 50 rmoveto 900 300 -900 -300 vlineto 0 20 1 blend hmoveto 10 10 -10 hlineto 0 20 1 blend hmoveto 10 10 -10 hlineto 0 20 1 blend hmoveto 10 10 -10 hlineto 0 20 1 blend hmoveto 10 10 -10 hlineto 0 20 1 blend hmoveto 10 10 -10 hlineto wdth 0x0 50.0 50.0 200.0 256 ufo2ft-3.3.1/tests/data/DSv5/MutatorSansVariable_Width-TTF.ttx000066400000000000000000000474431470175262700240670ustar00rootroot00000000000000 PUSHB[ ] 0 FDEF[ ] POP[ ] ENDF[ ] PUSHB[ ] 4 3 INSTCTRL[ ] PUSHB[ ] 0 0 SVTCA[0] MDRP[01100] Width Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Light Extended MutatorMathTest-SansLightExtended MutatorMathTest-SansLightCondensed Regular serif Sans weight Light width Condensed Normal Extended New Font Regular 0.000;NONE;NewFont-Regular New Font Regular Version 0.000 NewFont-Regular Width Sans Light Condensed MutatorMathTest-SansLightCondensed Sans Light Extended MutatorMathTest-SansLightExtended MutatorMathTest-SansLightCondensed Regular serif Sans weight Light width Condensed Normal Extended wdth 0x0 50.0 50.0 200.0 256 ufo2ft-3.3.1/tests/data/DSv5/MutatorSerifVariable_Width-CFF2.ttx000066400000000000000000000344201470175262700242450ustar00rootroot00000000000000 Width Serif Light Condensed MutatorMathTest-SerifLightCondensed Regular serif Serif weight Light width Condensed Normal Extended New Font Regular 0.000;NONE;NewFont-Regular New Font Regular Version 0.000 NewFont-Regular Width Serif Light Condensed MutatorMathTest-SerifLightCondensed Regular serif Serif weight Light width Condensed Normal Extended 50 -200 rmoveto 400 1000 -400 -1000 hlineto 50 50 rmoveto 900 300 -900 -300 vlineto -70 -10 1 blend -20 rmoveto 10 10 -10 hlineto 140 20 1 blend 30 rmoveto 10 10 -10 hlineto -70 -10 1 blend -20 rmoveto 10 10 -10 hlineto 140 20 1 blend 30 rmoveto 10 10 -10 hlineto -70 -10 1 blend -20 rmoveto 10 10 -10 hlineto 140 20 1 blend 30 rmoveto 10 10 -10 hlineto -70 -10 1 blend -20 rmoveto 10 10 -10 hlineto 140 20 1 blend 30 rmoveto 10 10 -10 hlineto -70 -10 1 blend -20 rmoveto 10 10 -10 hlineto 140 20 1 blend 30 rmoveto 10 10 -10 hlineto wdth 0x0 50.0 50.0 200.0 256 ufo2ft-3.3.1/tests/data/DSv5/MutatorSerifVariable_Width-TTF.ttx000066400000000000000000000412741470175262700242270ustar00rootroot00000000000000 Width Serif Light Condensed MutatorMathTest-SerifLightCondensed Regular serif Serif weight Light width Condensed Normal Extended New Font Regular 0.000;NONE;NewFont-Regular New Font Regular Version 0.000 NewFont-Regular Width Serif Light Condensed MutatorMathTest-SerifLightCondensed Regular serif Serif weight Light width Condensed Normal Extended wdth 0x0 50.0 50.0 200.0 256 ufo2ft-3.3.1/tests/data/DSv5/test_v5_MutatorSans_and_Serif.designspace000066400000000000000000000163061470175262700257120ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/000077500000000000000000000000001470175262700212375ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/Designspace-MastersAsInstances.designspace000066400000000000000000000021571470175262700314500ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/DesignspaceTest.designspace000066400000000000000000000017271470175262700265420ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/000077500000000000000000000000001470175262700241215ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/fontinfo.plist000066400000000000000000000015661470175262700270300ustar00rootroot00000000000000 ascender 0 capHeight 0 descender 0 familyName MyFont guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Bold unitsPerEm 1000 xHeight 0 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/glyphs/000077500000000000000000000000001470175262700254275ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/glyphs/asas.glif000066400000000000000000000006511470175262700272230ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/glyphs/contents.plist000066400000000000000000000004401470175262700303370ustar00rootroot00000000000000 asas asas.glif l l.glif ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/glyphs/l.glif000066400000000000000000000005311470175262700265240ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700300740ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/lib.plist000066400000000000000000000004571470175262700257520ustar00rootroot00000000000000 public.glyphOrder l asas ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700270070ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/000077500000000000000000000000001470175262700243105ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/fontinfo.plist000066400000000000000000000015671470175262700272200ustar00rootroot00000000000000 ascender 0 capHeight 0 descender 0 familyName MyFont guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Light unitsPerEm 1000 xHeight 0 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/glyphs/000077500000000000000000000000001470175262700256165ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/glyphs/asas.glif000066400000000000000000000007371470175262700274170ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/glyphs/contents.plist000066400000000000000000000004401470175262700305260ustar00rootroot00000000000000 asas asas.glif l l.glif ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/glyphs/l.glif000066400000000000000000000005271470175262700267200ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/layercontents.plist000066400000000000000000000004371470175262700302630ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/lib.plist000066400000000000000000000004571470175262700261410ustar00rootroot00000000000000 public.glyphOrder l asas ufo2ft-3.3.1/tests/data/DesignspaceBrokenTest/MyFont-Light.ufo/metainfo.plist000066400000000000000000000004761470175262700271760ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/000077500000000000000000000000001470175262700210625ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont.designspace000066400000000000000000000037631470175262700245160ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/000077500000000000000000000000001470175262700241625ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/fontinfo.plist000066400000000000000000000015671470175262700270720ustar00rootroot00000000000000 ascender 0 capHeight 0 descender 0 familyName MyFont guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Black unitsPerEm 1000 xHeight 0 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/glyphs/000077500000000000000000000000001470175262700254705ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/glyphs/Q_.alt.glif000066400000000000000000000026571470175262700274630ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/glyphs/Q_.glif000066400000000000000000000031731470175262700266760ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/glyphs/Q_.ss01.alt.glif000066400000000000000000000023611470175262700302400ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/glyphs/Q_.ss01.glif000066400000000000000000000023551470175262700274640ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/glyphs/contents.plist000066400000000000000000000006341470175262700304050ustar00rootroot00000000000000 Q Q_.glif Q.alt Q_.alt.glif Q.ss01 Q_.ss01.glif Q.ss01.alt Q_.ss01.alt.glif ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/layercontents.plist000066400000000000000000000004231470175262700301300ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/lib.plist000066400000000000000000000005501470175262700260050ustar00rootroot00000000000000 public.glyphOrder Q Q.alt Q.ss01 Q.ss01.alt ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Black.ufo/metainfo.plist000066400000000000000000000004641470175262700270450ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/000077500000000000000000000000001470175262700240315ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/fontinfo.plist000066400000000000000000000015721470175262700267350ustar00rootroot00000000000000 ascender 0 capHeight 0 descender 0 familyName MyFont guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Hairline unitsPerEm 1000 xHeight 0 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/glyphs/000077500000000000000000000000001470175262700253375ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/glyphs/Q_.alt.glif000066400000000000000000000026371470175262700273300ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/glyphs/Q_.glif000066400000000000000000000031521470175262700265420ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/glyphs/Q_.ss01.alt.glif000066400000000000000000000023571470175262700301140ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/glyphs/Q_.ss01.glif000066400000000000000000000023571470175262700273350ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/glyphs/contents.plist000066400000000000000000000006341470175262700302540ustar00rootroot00000000000000 Q Q_.glif Q.alt Q_.alt.glif Q.ss01 Q_.ss01.glif Q.ss01.alt Q_.ss01.alt.glif ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/layercontents.plist000066400000000000000000000004231470175262700277770ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/lib.plist000066400000000000000000000005501470175262700256540ustar00rootroot00000000000000 public.glyphOrder Q Q.alt Q.ss01 Q.ss01.alt ufo2ft-3.3.1/tests/data/DesignspaceRuleOrder/MyFont_Hair.ufo/metainfo.plist000066400000000000000000000004641470175262700267140ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/DesignspaceTest/000077500000000000000000000000001470175262700200765ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceTest/DesignspaceTest-bare.designspace000066400000000000000000000013511470175262700263010ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/DesignspaceTest-instance-attrs.designspace000066400000000000000000000015241470175262700303310ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/DesignspaceTest-lib.designspace000066400000000000000000000024741470175262700261450ustar00rootroot00000000000000 blorb hurx public.skipExportGlyphs x y z public.skipExportGlyphs a b c ufo2ft-3.3.1/tests/data/DesignspaceTest/DesignspaceTest-opsz.designspace000066400000000000000000000011551470175262700263650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/DesignspaceTest-slnt.designspace000066400000000000000000000011461470175262700263520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/DesignspaceTest-wght-wdth.designspace000066400000000000000000000016241470175262700273100ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/DesignspaceTest.designspace000066400000000000000000000017271470175262700254010ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Bold.ufo/000077500000000000000000000000001470175262700227605ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Bold.ufo/fontinfo.plist000066400000000000000000000010441470175262700256560ustar00rootroot00000000000000 ascender 0 capHeight 0 descender 0 familyName MyFont styleName Bold unitsPerEm 1000 xHeight 0 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Bold.ufo/glyphs/000077500000000000000000000000001470175262700242665ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Bold.ufo/glyphs/contents.plist000066400000000000000000000003551470175262700272030ustar00rootroot00000000000000 l l.glif ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Bold.ufo/glyphs/l.glif000066400000000000000000000005311470175262700253630ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700267330ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700256460ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Bold.ufoz000066400000000000000000000043151470175262700231000ustar00rootroot00000000000000PKTMyFont-Bold.ufo/PKTU8MyFont-Bold.ufo/fontinfo.plist1O0w$ɒ)tCMETT %GjؖmHKAE {zCߑԆK1gqd\pWp^_ћiU9Q7eY WCHYl+ۀ{k0 qF~@Bm-3PZxc74(j npaE&Nij y{ލۺ_ڥ>k)3v/l);6 nM:'ΒdQP8OWPKTMyFont-Bold.ufo/glyphs/PKTr0%MyFont-Bold.ufo/glyphs/contents.plist=NA0NtfL(`BBD8x4P@mA=ݻm'z^M!rd];;o>c$%E!qqJI$qtI '䡵 }OoVEQCl\c.3 s4+DGddhƏtk{RNdįͶPKT$חײfMyFont-Bold.ufo/glyphs/l.glifPK0ݛxI7AY0z*4)S5Ƹ̛GdCm8f#И`)J\ҎMj-epsmd[F$PE00r i ӛpusum8ipd\+oog'4%4ʞPKT # )#MyFont-Bold.ufo/layercontents.plisteNM @i/ܩ[VTRBvh겻fVy xp;:vqd}Ý;uဍ&["o+)GB nHj!uM6E2oUJL2$k2~jޅ"ioQ% ÷FfwaǢPKTB2MHMyFont-Bold.ufo/metainfo.plistUN0DH%xA\rSASJD"E&Njx-{Kc7mvwv8OFHnu”iզ_$)K兼_V{fvОX},6+SkW9+kB3'4:Dc4z(ZX4 ascender 0 capHeight 0 descender 0 familyName MyFont styleName Light unitsPerEm 1000 xHeight 0 openTypeOS2Panose 2 11 5 4 2 2 2 2 2 4 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Light.ufo/glyphs/000077500000000000000000000000001470175262700244555ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Light.ufo/glyphs/contents.plist000066400000000000000000000003551470175262700273720ustar00rootroot00000000000000 l l.glif ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Light.ufo/glyphs/l.glif000066400000000000000000000005271470175262700255570ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Light.ufo/groups.plist000066400000000000000000000005451470175262700255470ustar00rootroot00000000000000 nonkerning_group A public.kern2.asdf A ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Light.ufo/layercontents.plist000066400000000000000000000004371470175262700271220ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Light.ufo/lib.plist000066400000000000000000000003601470175262700247710ustar00rootroot00000000000000 blorb asasa ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Light.ufo/metainfo.plist000066400000000000000000000004761470175262700260350ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Light.ufoz000066400000000000000000000057611470175262700232750ustar00rootroot00000000000000PKTMyFont-Light.ufo/PKTp/4MyFont-Light.ufo/fontinfo.plistRj0/>JQc$%E!qqJI$qtI '䡵 }OoVEQCl\c.3 s4+DGddhƏtk{RNdįͶPKT#jqTdMyFont-Light.ufo/glyphs/l.glifPK0ݛxlX)DФ'4PI,6f2oޛ7 ^wmID5@Qn Mje傗fl* Y+fZ:[$E/)WЅnH{;2ބѤ0l 7(G76~M oiqxϞPKTu4ZGsMyFont-Light.ufo/groups.plistn0ESoL%TBH P X۲ `wg|||͆m {ajj4j5Hfw 'rC9*c )ȘV Ue1 H'?{u#Ej#?A {N;V`\6O!o1SZmU|e0 +P[[~t.1<+n5E+v|7NPKT # )$MyFont-Light.ufo/layercontents.plisteNM @i/ܩ[VTRBvh겻fVy xp;:vqd}Ý;uဍ&["o+)GB nHj!uM6E2oUJL2$k2~jޅ"ioQ% ÷FfwaǢPKTwʺMyFont-Light.ufo/lib.plist=N0k/`fLŨ`BBD8x(h6"o j2;;3;tnx1jmtu%u䮝?Ep9$JCE 0 k &1< {/**L$L!6a1x3 s4R'ᲠhUZ~Pɋ_3EoPKTB2MHMyFont-Light.ufo/metainfo.plistUN0DH%xA\rSASJD"E&Njx-{Kc7mvwv8OFHnu”iզ_$)K兼_V{fvОX},6+SkW9+kB3'4:Dc4z(ZX4 ascender 0 capHeight 0 descender 0 familyName MyFont openTypeOS2Type 3 openTypeOS2WeightClass 400 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Regular unitsPerEm 1000.0 xHeight 0 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Regular.ufo/glyphs/000077500000000000000000000000001470175262700250075ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Regular.ufo/glyphs/contents.plist000066400000000000000000000003551470175262700277240ustar00rootroot00000000000000 l l.glif ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Regular.ufo/glyphs/l.glif000066400000000000000000000005531470175262700261100ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Regular.ufo/groups.plist000066400000000000000000000005451470175262700261010ustar00rootroot00000000000000 nonkerning_group A public.kern2.asdf A ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700274540ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Regular.ufo/lib.plist000066400000000000000000000006711470175262700253300ustar00rootroot00000000000000 blorb asasa designspace.location weight 100.0 public.skipExportGlyphs ufo2ft-3.3.1/tests/data/DesignspaceTest/MyFont-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700263670ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/000077500000000000000000000000001470175262700210065ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/fontinfo.plist000066400000000000000000000020661470175262700237110ustar00rootroot00000000000000 ascender 800 capHeight 700 descender -200 familyName Dotted Circle Test openTypeHeadCreated 2022/04/18 20:12:58 postscriptBlueValues -16.0 0.0 500.0 516.0 700.0 716.0 800.0 816.0 postscriptFontName DottedCircleTest-Regular postscriptOtherBlues -216.0 -200.0 styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/glyphs/000077500000000000000000000000001470175262700223145ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/glyphs/a.glif000066400000000000000000000035121470175262700234000ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2022-04-18 20:32:56 +0000 ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/glyphs/acutecomb.glif000066400000000000000000000007721470175262700251270ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2022-04-18 20:15:50 +0000 ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/glyphs/c.glif000066400000000000000000000031671470175262700234100ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2022-04-18 20:34:17 +0000 ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/glyphs/contents.plist000066400000000000000000000005761470175262700252360ustar00rootroot00000000000000 a a.glif acutecomb acutecomb.glif c c.glif dotbelowcomb dotbelowcomb.glif ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/glyphs/dotbelowcomb.glif000066400000000000000000000014261470175262700256420ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2022-04-18 20:32:37 +0000 ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/layercontents.plist000066400000000000000000000004151470175262700247550ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/lib.plist000066400000000000000000000012561470175262700226350ustar00rootroot00000000000000 com.schriftgestaltung.DisplayStrings cacacaa com.schriftgestaltung.disablesAutomaticAlignment com.schriftgestaltung.fontMasterID m01 com.schriftgestaltung.glyphOrder com.schriftgestaltung.useNiceNames public.glyphOrder a c acutecomb dotbelowcomb ufo2ft-3.3.1/tests/data/DottedCircleTest.ufo/metainfo.plist000066400000000000000000000004701470175262700236660ustar00rootroot00000000000000 creator com.schriftgestaltung.GlyphsUFOExport formatVersion 3 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/000077500000000000000000000000001470175262700221025ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/features.fea000066400000000000000000000001171470175262700243740ustar00rootroot00000000000000# Prefix: Languagesystems languagesystem DFLT dflt; languagesystem latn dflt; ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/fontinfo.plist000066400000000000000000000026401470175262700250030ustar00rootroot00000000000000 ascender 800 capHeight 700 descender -200 familyName Ignore Anchors Test italicAngle 0 openTypeHeadCreated 2023/11/22 14:08:36 openTypeOS2Type 3 postscriptBlueValues -10 0 470 480 700 710 800 810 postscriptOtherBlues -210 -200 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Thin unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 470 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/glyphs/000077500000000000000000000000001470175262700234105ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/glyphs/A_.glif000066400000000000000000000005261470175262700245750ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/glyphs/A_dieresis.glif000066400000000000000000000004401470175262700263200ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/glyphs/a.glif000066400000000000000000000007771470175262700245060ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/glyphs/adieresis.glif000066400000000000000000000005071470175262700262250ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/glyphs/contents.plist000066400000000000000000000007311470175262700263230ustar00rootroot00000000000000 A A_.glif Adieresis A_dieresis.glif a a.glif adieresis adieresis.glif dieresiscomb dieresiscomb.glif ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/glyphs/dieresiscomb.glif000066400000000000000000000014211470175262700267210ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.originalWidth 600 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/glyphs/layerinfo.plist000066400000000000000000000014361470175262700264610ustar00rootroot00000000000000 lib com.schriftgestaltung.layerId C4872ECA-A3A9-40AB-960A-1DB2202F16DE com.schriftgestaltung.layerOrderInGlyph.A 1 com.schriftgestaltung.layerOrderInGlyph.Adieresis 0 com.schriftgestaltung.layerOrderInGlyph.a 1 com.schriftgestaltung.layerOrderInGlyph.adieresis 0 com.schriftgestaltung.layerOrderInGlyph.dieresiscomb 0 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/groups.plist000066400000000000000000000010001470175262700244650ustar00rootroot00000000000000 public.kern1.A A public.kern1.a a public.kern2.A A public.kern2.a a ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/layercontents.plist000066400000000000000000000004371470175262700260550ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/lib.plist000066400000000000000000000054321470175262700237310ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.featureWriters class CursFeatureWriter class KernFeatureWriter class ContextualMarkFeatureWriter module glyphsLib.featureWriters.markFeatureWriter class GdefFeatureWriter com.github.googlei18n.ufo2ft.filters name eraseOpenCorners namespace glyphsLib.filters pre com.schriftgestaltung.customParameter.GSFont.Write DisplayStrings 0 com.schriftgestaltung.customParameter.GSFont.Write lastChange 0 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 Light com.schriftgestaltung.customParameter.GSFontMaster.weightValue 100 com.schriftgestaltung.customParameter.GSFontMaster.widthValue 100 com.schriftgestaltung.fontMasterID C4872ECA-A3A9-40AB-960A-1DB2202F16DE com.schriftgestaltung.fontMasterOrder 0 com.schriftgestaltung.weightValue 100 com.schriftgestaltung.widthValue 100 public.glyphOrder A Adieresis a adieresis dieresiscomb public.postscriptNames dieresiscomb uni0308 ufo2ft-3.3.1/tests/data/IgnoreAnchorsTest-Thin.ufo/metainfo.plist000066400000000000000000000004761470175262700247700ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/IncompatibleMasters/000077500000000000000000000000001470175262700207565ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/IncompatibleMasters/IncompatibleMasters.designspace000066400000000000000000000015461470175262700271400ustar00rootroot00000000000000 public.skipExportGlyphs b d ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/000077500000000000000000000000001470175262700240045ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/fontinfo.plist000066400000000000000000000024701470175262700267060ustar00rootroot00000000000000 ascender 800.0 capHeight 700.0 descender -200.0 familyName New Font guidelines italicAngle -0.0 openTypeHeadCreated 2019/03/06 11:24:59 openTypeOS2Type 3 postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Bold unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500.0 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/000077500000000000000000000000001470175262700253125ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/a.glif000066400000000000000000000007711470175262700264020ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:26:11 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/b.glif000066400000000000000000000044651470175262700264070ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.Export com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:43:17 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/c.glif000066400000000000000000000020031470175262700263720ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 17:23:09 com.schriftgestaltung.componentsAlignment -1 -1 0 -1 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/contents.plist000066400000000000000000000007161470175262700302300ustar00rootroot00000000000000 a a.glif b b.glif c c.glif d d.glif e e.glif f f.glif ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/d.glif000066400000000000000000000016211470175262700264000ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.Export com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:43:08 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/e.glif000066400000000000000000000014031470175262700263770ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 17:23:23 com.schriftgestaltung.componentsAlignment -1 -1 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/f.glif000066400000000000000000000004021470175262700263760ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/layerinfo.plist000066400000000000000000000014031470175262700303550ustar00rootroot00000000000000 lib com.schriftgestaltung.layerId 43102E19-C314-492C-BD24-71BD43330434 com.schriftgestaltung.layerOrderInGlyph.a 1 com.schriftgestaltung.layerOrderInGlyph.b 1 com.schriftgestaltung.layerOrderInGlyph.c 1 com.schriftgestaltung.layerOrderInGlyph.d 1 com.schriftgestaltung.layerOrderInGlyph.e 1 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/groups.plist000066400000000000000000000022001470175262700263720ustar00rootroot00000000000000 public.kern1.a a public.kern1.b b public.kern1.c c public.kern1.d d public.kern1.e e public.kern1.f f public.kern2.a a public.kern2.b b public.kern2.c c public.kern2.d d public.kern2.e e public.kern2.f f ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/kerning.plist000066400000000000000000000017041470175262700265200ustar00rootroot00000000000000 a d 10 public.kern1.a public.kern2.a 10 public.kern2.b 10 public.kern2.d 10 public.kern1.b public.kern2.d 10 public.kern1.d public.kern2.f 10 public.kern1.e public.kern2.f 10 public.kern1.f public.kern2.e 10 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700277570ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/lib.plist000066400000000000000000000045171470175262700256360ustar00rootroot00000000000000 com.schriftgestaltung.appVersion 1192 com.schriftgestaltung.customParameter.GSFont.DisplayStrings c e com.schriftgestaltung.customParameter.GSFont.Enforce Compatibility Check 1 com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment com.schriftgestaltung.customParameter.GSFont.useNiceNames 1 com.schriftgestaltung.customParameter.GSFontMaster.customValue 0.0 com.schriftgestaltung.customParameter.GSFontMaster.customValue1 0.0 com.schriftgestaltung.customParameter.GSFontMaster.customValue2 0.0 com.schriftgestaltung.customParameter.GSFontMaster.customValue3 0.0 com.schriftgestaltung.customParameter.GSFontMaster.iconName com.schriftgestaltung.customParameter.GSFontMaster.weightValue 700.0 com.schriftgestaltung.customParameter.GSFontMaster.widthValue 100.0 com.schriftgestaltung.fontMasterID 43102E19-C314-492C-BD24-71BD43330434 com.schriftgestaltung.fontMasterOrder 1 com.schriftgestaltung.glyphOrder com.schriftgestaltung.keyboardIncrement 1 com.schriftgestaltung.weight Bold com.schriftgestaltung.weightValue 700.0 com.schriftgestaltung.width Regular com.schriftgestaltung.widthValue 100.0 public.glyphOrder a c e b d f public.skipExportGlyphs b d f ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700266720ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/000077500000000000000000000000001470175262700245255ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/fontinfo.plist000066400000000000000000000024731470175262700274320ustar00rootroot00000000000000 ascender 800.0 capHeight 700.0 descender -200.0 familyName New Font guidelines italicAngle -0.0 openTypeHeadCreated 2019/03/06 11:24:59 openTypeOS2Type 3 postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500.0 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/000077500000000000000000000000001470175262700260335ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/a.glif000066400000000000000000000007751470175262700271270ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:26:11 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/b.glif000066400000000000000000000030321470175262700271150ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.Export com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:43:17 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/c.glif000066400000000000000000000017111470175262700271200ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 17:23:09 com.schriftgestaltung.componentsAlignment -1 -1 0 -1 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/contents.plist000066400000000000000000000007161470175262700307510ustar00rootroot00000000000000 a a.glif b b.glif c c.glif d d.glif e e.glif f f.glif ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/d.glif000066400000000000000000000016211470175262700271210ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.Export com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:43:08 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/e.glif000066400000000000000000000014051470175262700271220ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 17:23:23 com.schriftgestaltung.componentsAlignment -1 -1 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/f.glif000066400000000000000000000004741470175262700271300ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/layerinfo.plist000066400000000000000000000014031470175262700310760ustar00rootroot00000000000000 lib com.schriftgestaltung.layerId 17DAEA70-42F2-48A2-B948-7696177362C3 com.schriftgestaltung.layerOrderInGlyph.a 0 com.schriftgestaltung.layerOrderInGlyph.b 0 com.schriftgestaltung.layerOrderInGlyph.c 0 com.schriftgestaltung.layerOrderInGlyph.d 0 com.schriftgestaltung.layerOrderInGlyph.e 0 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/groups.plist000066400000000000000000000022001470175262700271130ustar00rootroot00000000000000 public.kern1.a a public.kern1.b b public.kern1.c c public.kern1.d d public.kern1.e e public.kern1.f f public.kern2.a a public.kern2.b b public.kern2.c c public.kern2.d d public.kern2.e e public.kern2.f f ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/kerning.plist000066400000000000000000000017041470175262700272410ustar00rootroot00000000000000 a d 10 public.kern1.a public.kern2.a 10 public.kern2.b 10 public.kern2.d 10 public.kern1.b public.kern2.d 10 public.kern1.d public.kern2.f 10 public.kern1.e public.kern2.f 10 public.kern1.f public.kern2.e 10 ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700305000ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/lib.plist000066400000000000000000000043071470175262700263540ustar00rootroot00000000000000 com.schriftgestaltung.appVersion 1192 com.schriftgestaltung.customParameter.GSFont.DisplayStrings c e com.schriftgestaltung.customParameter.GSFont.Enforce Compatibility Check 1 com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment com.schriftgestaltung.customParameter.GSFont.useNiceNames 1 com.schriftgestaltung.customParameter.GSFontMaster.customValue 0.0 com.schriftgestaltung.customParameter.GSFontMaster.customValue1 0.0 com.schriftgestaltung.customParameter.GSFontMaster.customValue2 0.0 com.schriftgestaltung.customParameter.GSFontMaster.customValue3 0.0 com.schriftgestaltung.customParameter.GSFontMaster.iconName com.schriftgestaltung.customParameter.GSFontMaster.weightValue 400.0 com.schriftgestaltung.customParameter.GSFontMaster.widthValue 100.0 com.schriftgestaltung.fontMasterID 17DAEA70-42F2-48A2-B948-7696177362C3 com.schriftgestaltung.fontMasterOrder 0 com.schriftgestaltung.glyphOrder com.schriftgestaltung.keyboardIncrement 1 com.schriftgestaltung.weight Regular com.schriftgestaltung.weightValue 400.0 com.schriftgestaltung.width Regular com.schriftgestaltung.widthValue 100.0 public.glyphOrder a c e b d f ufo2ft-3.3.1/tests/data/IncompatibleMasters/NewFont-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700274130ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/000077500000000000000000000000001470175262700224775ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/000077500000000000000000000000001470175262700251715ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/fontinfo.plist000066400000000000000000000016011470175262700300660ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 familyName Test Ufo guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Bold unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/glyphs/000077500000000000000000000000001470175262700264775ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/glyphs/contents.plist000066400000000000000000000003631470175262700314130ustar00rootroot00000000000000 test test.glif ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/glyphs/layerinfo.plist000066400000000000000000000003671470175262700315520ustar00rootroot00000000000000 color 1,0.75,0,0.7 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/glyphs/test.glif000066400000000000000000000014021470175262700303160ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/layercontents.plist000066400000000000000000000004331470175262700311400ustar00rootroot00000000000000 foreground glyphs ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/lib.plist000066400000000000000000000020061470175262700270120ustar00rootroot00000000000000 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 public.glyphOrder test ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareOne.ufo/metainfo.plist000066400000000000000000000004761470175262700300570ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/000077500000000000000000000000001470175262700252215ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/fontinfo.plist000066400000000000000000000016041470175262700301210ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 familyName Test Ufo guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/glyphs/000077500000000000000000000000001470175262700265275ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/glyphs/contents.plist000066400000000000000000000003631470175262700314430ustar00rootroot00000000000000 test test.glif ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/glyphs/layerinfo.plist000066400000000000000000000003671470175262700316020ustar00rootroot00000000000000 color 1,0.75,0,0.7 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/glyphs/test.glif000066400000000000000000000014221470175262700303500ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/layercontents.plist000066400000000000000000000004331470175262700311700ustar00rootroot00000000000000 foreground glyphs ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/lib.plist000066400000000000000000000020061470175262700270420ustar00rootroot00000000000000 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 public.glyphOrder test ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/SquareTwo.ufo/metainfo.plist000066400000000000000000000004761470175262700301070ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/InstantiatorStrictMathGlyph/StrictMathGlyph.designspace000066400000000000000000000014761470175262700300040ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/000077500000000000000000000000001470175262700203625ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/features.fea000066400000000000000000000000521470175262700226520ustar00rootroot00000000000000feature liga { sub a e s s by s; } liga; ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/fontinfo.plist000066400000000000000000000033551470175262700232670ustar00rootroot00000000000000 ascender 750 capHeight 700 copyright descender -250 familyName Layer Font guidelines italicAngle 0 note openTypeHeadCreated 2018/11/21 11:49:03 openTypeNameDesigner openTypeNameDesignerURL openTypeNameLicense openTypeNameLicenseURL openTypeNameManufacturer openTypeNameManufacturerURL postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Bold trademark unitsPerEm 1000 versionMajor 0 versionMinor 0 xHeight 500 ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/glyphs/000077500000000000000000000000001470175262700216705ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/glyphs/_notdef.glif000066400000000000000000000010461470175262700241520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/glyphs/a.glif000066400000000000000000000020271470175262700227540ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/glyphs/contents.plist000066400000000000000000000010001470175262700245710ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif dotabovecomb dotabovecomb.glif e e.glif edotabove edotabove.glif s s.glif ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/glyphs/dotabovecomb.glif000066400000000000000000000006061470175262700252010ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/glyphs/e.glif000066400000000000000000000015261470175262700227630ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/glyphs/edotabove.glif000066400000000000000000000004251470175262700245040ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/glyphs/s.glif000066400000000000000000000013421470175262700227750ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700243350ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/lib.plist000066400000000000000000000006121470175262700222040ustar00rootroot00000000000000 public.glyphOrder a e s dotabovecomb edotabove ufo2ft-3.3.1/tests/data/LayerFont-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700232500ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/000077500000000000000000000000001470175262700211035ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/features.fea000066400000000000000000000000521470175262700233730ustar00rootroot00000000000000feature liga { sub a e s s by s; } liga; ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/fontinfo.plist000066400000000000000000000031431470175262700240030ustar00rootroot00000000000000 ascender 750 capHeight 700 copyright descender -250 familyName Layer Font guidelines italicAngle 0 note openTypeHeadCreated 2018/11/21 11:49:03 openTypeNameDesigner openTypeNameDesignerURL openTypeNameLicense openTypeNameLicenseURL openTypeNameManufacturer openTypeNameManufacturerURL postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular trademark unitsPerEm 1000 versionMajor 0 versionMinor 0 xHeight 500 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs.M_edium/000077500000000000000000000000001470175262700237675ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs.M_edium/contents.plist000066400000000000000000000003551470175262700267040ustar00rootroot00000000000000 e e.glif ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs.M_edium/e.glif000066400000000000000000000015001470175262700250520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs/000077500000000000000000000000001470175262700224115ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs/_notdef.glif000066400000000000000000000010461470175262700246730ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs/a.glif000066400000000000000000000020241470175262700234720ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs/contents.plist000066400000000000000000000010001470175262700253120ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif dotabovecomb dotabovecomb.glif e e.glif edotabove edotabove.glif s s.glif ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs/dotabovecomb.glif000066400000000000000000000005661470175262700257270ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs/e.glif000066400000000000000000000014721470175262700235040ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs/edotabove.glif000066400000000000000000000004251470175262700252250ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/glyphs/s.glif000066400000000000000000000013421470175262700235160ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/layercontents.plist000066400000000000000000000005741470175262700250600ustar00rootroot00000000000000 public.default glyphs Medium glyphs.M_edium ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/lib.plist000066400000000000000000000006121470175262700227250ustar00rootroot00000000000000 public.glyphOrder a e s dotabovecomb edotabove ufo2ft-3.3.1/tests/data/LayerFont-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700237710ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MTIFeatures.ttx000066400000000000000000000023171470175262700177050ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/000077500000000000000000000000001470175262700177315ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/data/000077500000000000000000000000001470175262700206425ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/data/com.github.googlei18n.ufo2ft.mtiFeatures/000077500000000000000000000000001470175262700303475ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/data/com.github.googlei18n.ufo2ft.mtiFeatures/GSUB.mti000066400000000000000000000003421470175262700316210ustar00rootroot00000000000000FontDame GSUB table script table begin latn default 0 script table end feature table begin 0 ccmp 0 feature table end lookup 0 ligature RightToLeft no IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks no a b c lookup end ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/fontinfo.plist000066400000000000000000000013011470175262700226230ustar00rootroot00000000000000 familyName MTIFeatures note https://github.com/googlei18n/fontmake/issues/289 unitsPerEm 1000 xHeight 500 ascender 750 capHeight 750 descender -250 postscriptUnderlinePosition -200 postscriptUnderlineThickness 20 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/glyphs/000077500000000000000000000000001470175262700212375ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/glyphs/_notdef.glif000066400000000000000000000007531470175262700235250ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/glyphs/a.glif000066400000000000000000000004361470175262700223250ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/glyphs/b.glif000066400000000000000000000005111470175262700223200ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/glyphs/c.glif000066400000000000000000000006241470175262700223260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/glyphs/contents.plist000066400000000000000000000006371470175262700241570ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif space space.glif ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/glyphs/space.glif000066400000000000000000000001771470175262700232020ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/groups.plist000066400000000000000000000002761470175262700223320ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/kerning.plist000066400000000000000000000006031470175262700224420ustar00rootroot00000000000000 a a -666 b -666 b a -666 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/layercontents.plist000066400000000000000000000004231470175262700236770ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/lib.plist000066400000000000000000000011251470175262700215530ustar00rootroot00000000000000 public.glyphOrder .notdef space a b c public.postscriptNames a uni0061 b uni0062 c uni0063 space uni0020 ufo2ft-3.3.1/tests/data/MTIFeatures.ufo/metainfo.plist000066400000000000000000000004431470175262700226110ustar00rootroot00000000000000 creator copy-paste formatVersion 3 ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/000077500000000000000000000000001470175262700220455ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/fontinfo.plist000066400000000000000000000017531470175262700247520ustar00rootroot00000000000000 note https://github.com/googlefonts/ufo2ft/issues/303 ascender 750 capHeight 750 descender -250 familyName MultipleAnchorClasses guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/glyphs/000077500000000000000000000000001470175262700233535ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/glyphs/_notdef.glif000066400000000000000000000001531470175262700256330ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/glyphs/a.glif000066400000000000000000000072441470175262700244450ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/glyphs/acutecomb.glif000066400000000000000000000017111470175262700261600ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/glyphs/contents.plist000066400000000000000000000005741470175262700262730ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif acutecomb acutecomb.glif e e.glif ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/glyphs/e.glif000066400000000000000000000060751470175262700244520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/glyphs/layerinfo.plist000066400000000000000000000002741470175262700264230ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/layercontents.plist000066400000000000000000000004231470175262700260130ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/lib.plist000066400000000000000000000005321470175262700236700ustar00rootroot00000000000000 public.glyphOrder .notdef a e acutecomb ufo2ft-3.3.1/tests/data/MultipleAnchorClasses.ufo/metainfo.plist000066400000000000000000000004511470175262700247240ustar00rootroot00000000000000 creator com.fontlab.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/000077500000000000000000000000001470175262700235275ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/fontinfo.plist000066400000000000000000000017631470175262700264350ustar00rootroot00000000000000 note https://github.com/googlefonts/ufo2ft/issues/303 ascender 750 capHeight 750 descender -250 familyName MultipleAnchorClassesConflict guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/glyphs/000077500000000000000000000000001470175262700250355ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/glyphs/_notdef.glif000066400000000000000000000001531470175262700273150ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/glyphs/acutecomb.glif000066400000000000000000000016421470175262700276450ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/glyphs/ae.glif000066400000000000000000000155241470175262700262740ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/glyphs/contents.plist000066400000000000000000000005251470175262700277510ustar00rootroot00000000000000 .notdef _notdef.glif acutecomb acutecomb.glif ae ae.glif ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/glyphs/layerinfo.plist000066400000000000000000000002741470175262700301050ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/layercontents.plist000066400000000000000000000004231470175262700274750ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/lib.plist000066400000000000000000000005041470175262700253510ustar00rootroot00000000000000 public.glyphOrder .notdef acutecomb ae ufo2ft-3.3.1/tests/data/MultipleAnchorClassesConflict.ufo/metainfo.plist000066400000000000000000000004511470175262700264060ustar00rootroot00000000000000 creator com.fontlab.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSans/000077500000000000000000000000001470175262700172715ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/LICENSE000066400000000000000000000020621470175262700202760ustar00rootroot00000000000000MIT License Copyright (c) 2017 Erik van Blokland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSans-non-default-layer.designspace000066400000000000000000000020531470175262700273040ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSans-weight-only.designspace000066400000000000000000000026041470175262700262260ustar00rootroot00000000000000 com.letterror.skateboard.previewLocation weight 0.0 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSans-width-only.designspace000066400000000000000000000025621470175262700260610ustar00rootroot00000000000000 com.letterror.skateboard.previewLocation width 0.0 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSans-with-openNodes.designspace000066400000000000000000000050631470175262700266650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSans.designspace000066400000000000000000000157071470175262700237720ustar00rootroot00000000000000 com.letterror.skateboard.interestingLocation weight 775.609 width 794.522 S1 weight 855.549 width 795.978 S2 com.letterror.skateboard.previewLocation weight 700.0 width 569.078 com.superpolator.data axiscolors weight 0.5 0.5 0.5 1.0 width 0.5 0.5 0.5 1.0 instancefolder instances lineInverted lineStacked sequence lineViewFilled previewtext Aaa snippets ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/000077500000000000000000000000001470175262700247655ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/features.fea000066400000000000000000000000521470175262700272550ustar00rootroot00000000000000# this is the feature from boldcondensed. ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/fontinfo.plist000066400000000000000000000035351470175262700276720ustar00rootroot00000000000000 ascender 800 capHeight 800 copyright License same as MutatorMath. BSD 3-clause. [test-token: A] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: A] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-BoldCondensed postscriptFullName MutatorMathTest BoldCondensed postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName BoldCondensed unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs.background/000077500000000000000000000000001470175262700304115ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs.background/S_.closed.glif000066400000000000000000000037041470175262700330710ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs.background/contents.plist000066400000000000000000000003741470175262700333270ustar00rootroot00000000000000 S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs.background/layerinfo.plist000066400000000000000000000003661470175262700334630ustar00rootroot00000000000000 color 0.5,1,0,0.7 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/000077500000000000000000000000001470175262700262735ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/A_.glif000066400000000000000000000024471470175262700274640ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/A_acute.glif000066400000000000000000000005241470175262700305000ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/A_dieresis.glif000066400000000000000000000005321470175262700312050ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/B_.glif000066400000000000000000000034071470175262700274620ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/C_.glif000066400000000000000000000033711470175262700274630ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/D_.glif000066400000000000000000000021501470175262700274560ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/E_.glif000066400000000000000000000025461470175262700274700ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/F_.glif000066400000000000000000000013531470175262700274640ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/G_.glif000066400000000000000000000033561470175262700274720ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/H_.glif000066400000000000000000000013471470175262700274710ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/I_.glif000066400000000000000000000013451470175262700274700ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/I_.narrow.glif000066400000000000000000000012751470175262700310010ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/I_J_.glif000066400000000000000000000021611470175262700277360ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/J_.glif000066400000000000000000000016501470175262700274700ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/J_.narrow.glif000066400000000000000000000014711470175262700310000ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/K_.glif000066400000000000000000000015501470175262700274700ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/L_.glif000066400000000000000000000010361470175262700274700ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/M_.glif000066400000000000000000000016231470175262700274730ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/N_.glif000066400000000000000000000013451470175262700274750ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/O_.glif000066400000000000000000000025171470175262700275000ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/P_.glif000066400000000000000000000022641470175262700275000ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/Q_.glif000066400000000000000000000005731470175262700275020ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/R_.glif000066400000000000000000000034451470175262700275040ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/S_.closed.glif000066400000000000000000000043551470175262700307560ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 1000.0 width 108.00694056919657 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/S_.glif000066400000000000000000000044771470175262700275130ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/T_.glif000066400000000000000000000010421470175262700274750ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/U_.glif000066400000000000000000000016501470175262700275030ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/V_.glif000066400000000000000000000013441470175262700275040ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/W_.glif000066400000000000000000000015321470175262700275040ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/X_.glif000066400000000000000000000014401470175262700275030ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/Y_.glif000066400000000000000000000013541470175262700275100ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/Z_.glif000066400000000000000000000013501470175262700275050ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/acute.glif000066400000000000000000000005411470175262700302370ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/arrowdown.glif000066400000000000000000000007531470175262700311650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/arrowleft.glif000066400000000000000000000007521470175262700311470ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/arrowright.glif000066400000000000000000000007531470175262700313330ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/arrowup.glif000066400000000000000000000007521470175262700306410ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/colon.glif000066400000000000000000000003621470175262700302510ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/comma.glif000066400000000000000000000010111470175262700302230ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/contents.plist000066400000000000000000000051771470175262700312170ustar00rootroot00000000000000 A A_.glif Aacute A_acute.glif Adieresis A_dieresis.glif B B_.glif C C_.glif D D_.glif E E_.glif F F_.glif G G_.glif H H_.glif I I_.glif I.narrow I_.narrow.glif IJ I_J_.glif J J_.glif J.narrow J_.narrow.glif K K_.glif L L_.glif M M_.glif N N_.glif O O_.glif P P_.glif Q Q_.glif R R_.glif S S_.glif S.closed S_.closed.glif T T_.glif U U_.glif V V_.glif W W_.glif X X_.glif Y Y_.glif Z Z_.glif acute acute.glif arrowdown arrowdown.glif arrowleft arrowleft.glif arrowright arrowright.glif arrowup arrowup.glif colon colon.glif comma comma.glif dieresis dieresis.glif dot dot.glif period period.glif quotedblbase quotedblbase.glif quotedblleft quotedblleft.glif quotedblright quotedblright.glif quotesinglbase quotesinglbase.glif semicolon semicolon.glif space space.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/dieresis.glif000066400000000000000000000006001470175262700307410ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/dot.glif000066400000000000000000000005371470175262700277310ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/layerinfo.plist000066400000000000000000000005631470175262700313440ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/period.glif000066400000000000000000000005361470175262700304240ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/quotedblbase.glif000066400000000000000000000011401470175262700316040ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/quotedblleft.glif000066400000000000000000000005051470175262700316300ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/quotedblright.glif000066400000000000000000000004101470175262700320060ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/quotesinglbase.glif000066400000000000000000000010661470175262700321660ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/semicolon.glif000066400000000000000000000003651470175262700311320ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/glyphs/space.glif000066400000000000000000000002321470175262700302260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/groups.plist000066400000000000000000000005561470175262700273670ustar00rootroot00000000000000 public.kern1.@MMK_L_A A public.kern2.@MMK_R_A A ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/kerning.plist000066400000000000000000000107441470175262700275050ustar00rootroot00000000000000 A J -20 O -30 T -70 U -30 V -50 B A -20 J -50 O -20 S -10 T -10 U -20 V -30 C A -20 J -50 T -20 V -20 E J -20 T -10 V -10 F A -40 J -80 O -10 S -20 U -10 V -10 G J -20 S -10 T -40 U -10 V -30 H J -30 S -10 T -10 J J -70 L J -20 O -20 T -110 U -20 V -60 O A -30 J -60 S -10 T -30 V -30 P A -50 J -100 S -10 T -10 U -10 V -20 R H -10 J -20 O -30 S -20 T -30 U -30 V -40 S A -20 H -20 J -40 O -10 S -10 T -30 U -10 V -30 W -10 T A -65 H -10 J -130 O -20 U A -30 J -60 S -10 V -10 V J -100 O -30 S -20 U -10 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/layercontents.plist000066400000000000000000000005771470175262700307450ustar00rootroot00000000000000 foreground glyphs background glyphs.background ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/lib.plist000066400000000000000000000172401470175262700266140ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.letterror.lightMeter.prefs chunkSize 5 diameter 200 drawTail invert toolDiameter 30 toolStyle fluid com.typemytype.robofont.background.layerStrokeColor 0.0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0.0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.defcon.sortDescriptor ascending space A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n ntilde o p q r s t u v w x y z zcaron zero one two three four five six seven eight nine underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent period comma colon semicolon exclam question slash backslash bar at ampersand paragraph bullet dollar trademark fi fl .notdef a_b_c Atilde Adieresis Acircumflex Aring Ccedilla Agrave Aacute quotedblright quotedblleft type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z IJ S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon dot dieresis acute space arrowdown arrowleft arrowright arrowup ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldCondensed.ufo/metainfo.plist000066400000000000000000000004761470175262700276530ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/000077500000000000000000000000001470175262700237535ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/features.fea000066400000000000000000000000441470175262700262440ustar00rootroot00000000000000# this is the feature from BoldWide ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/fontinfo.plist000066400000000000000000000035161470175262700266570ustar00rootroot00000000000000 ascender 800 capHeight 800 copyright License same as MutatorMath. BSD 3-clause. [test-token: B] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: B] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-BoldWide postscriptFullName MutatorMathTest BoldWide postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName BoldWide unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs.background/000077500000000000000000000000001470175262700273775ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs.background/S_.closed.glif000066400000000000000000000037021470175262700320550ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs.background/S_.glif000066400000000000000000000037251470175262700306120ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs.background/contents.plist000066400000000000000000000004521470175262700323120ustar00rootroot00000000000000 S S_.glif S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs.background/layerinfo.plist000066400000000000000000000003661470175262700324510ustar00rootroot00000000000000 color 0.5,1,0,0.7 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/000077500000000000000000000000001470175262700252615ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/A_.glif000066400000000000000000000016631470175262700264510ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/A_acute.glif000066400000000000000000000005421470175262700274660ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/A_dieresis.glif000066400000000000000000000005501470175262700301730ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/B_.glif000066400000000000000000000042331470175262700264460ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/C_.glif000066400000000000000000000026121470175262700264460ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/D_.glif000066400000000000000000000021761470175262700264540ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/E_.glif000066400000000000000000000017341470175262700264540ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/F_.glif000066400000000000000000000013561470175262700264550ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/G_.glif000066400000000000000000000033231470175262700264520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/H_.glif000066400000000000000000000013541470175262700264550ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/I_.glif000066400000000000000000000013461470175262700264570ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/I_.narrow.glif000066400000000000000000000012751470175262700277670ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/I_J_.glif000066400000000000000000000022021470175262700267200ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/J_.glif000066400000000000000000000016541470175262700264620ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/J_.narrow.glif000066400000000000000000000014461470175262700277700ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/K_.glif000066400000000000000000000015531470175262700264610ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/L_.glif000066400000000000000000000010411470175262700264520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/M_.glif000066400000000000000000000016271470175262700264650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/N_.glif000066400000000000000000000013511470175262700264600ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/O_.glif000066400000000000000000000025531470175262700264660ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/P_.glif000066400000000000000000000021641470175262700264650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/Q_.glif000066400000000000000000000005741470175262700264710ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/R_.glif000066400000000000000000000027271470175262700264740ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/S_.closed.glif000066400000000000000000000045011470175262700277350ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/S_.glif000066400000000000000000000045101470175262700264650ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/T_.glif000066400000000000000000000010451470175262700264660ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/U_.glif000066400000000000000000000020151470175262700264650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/V_.glif000066400000000000000000000013461470175262700264740ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/W_.glif000066400000000000000000000015351470175262700264750ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/X_.glif000066400000000000000000000014431470175262700264740ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/Y_.glif000066400000000000000000000013561470175262700265000ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/Z_.glif000066400000000000000000000013561470175262700265010ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/acute.glif000066400000000000000000000005411470175262700272250ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/arrowdown.glif000066400000000000000000000007531470175262700301530ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/arrowleft.glif000066400000000000000000000007551470175262700301400ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/arrowright.glif000066400000000000000000000007571470175262700303250ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/arrowup.glif000066400000000000000000000007541470175262700276310ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/colon.glif000066400000000000000000000003621470175262700272370ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/comma.glif000066400000000000000000000010121470175262700272120ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/contents.plist000066400000000000000000000051771470175262700302050ustar00rootroot00000000000000 A A_.glif Aacute A_acute.glif Adieresis A_dieresis.glif B B_.glif C C_.glif D D_.glif E E_.glif F F_.glif G G_.glif H H_.glif I I_.glif I.narrow I_.narrow.glif IJ I_J_.glif J J_.glif J.narrow J_.narrow.glif K K_.glif L L_.glif M M_.glif N N_.glif O O_.glif P P_.glif Q Q_.glif R R_.glif S S_.glif S.closed S_.closed.glif T T_.glif U U_.glif V V_.glif W W_.glif X X_.glif Y Y_.glif Z Z_.glif acute acute.glif arrowdown arrowdown.glif arrowleft arrowleft.glif arrowright arrowright.glif arrowup arrowup.glif colon colon.glif comma comma.glif dieresis dieresis.glif dot dot.glif period period.glif quotedblbase quotedblbase.glif quotedblleft quotedblleft.glif quotedblright quotedblright.glif quotesinglbase quotesinglbase.glif semicolon semicolon.glif space space.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/dieresis.glif000066400000000000000000000005621470175262700277360ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/dot.glif000066400000000000000000000005371470175262700267170ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/layerinfo.plist000066400000000000000000000005631470175262700303320ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/period.glif000066400000000000000000000005361470175262700274120ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/quotedblbase.glif000066400000000000000000000004031470175262700305730ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/quotedblleft.glif000066400000000000000000000005051470175262700306160ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/quotedblright.glif000066400000000000000000000004101470175262700307740ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/quotesinglbase.glif000066400000000000000000000003151470175262700311500ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/semicolon.glif000066400000000000000000000003651470175262700301200ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/glyphs/space.glif000066400000000000000000000002321470175262700272140ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/groups.plist000066400000000000000000000005561470175262700263550ustar00rootroot00000000000000 public.kern1.@MMK_L_A A public.kern2.@MMK_R_A A ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/kerning.plist000066400000000000000000000004551470175262700264710ustar00rootroot00000000000000 T public.kern2.@MMK_R_A -150 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/layercontents.plist000066400000000000000000000005771470175262700277330ustar00rootroot00000000000000 foreground glyphs background glyphs.background ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/lib.plist000066400000000000000000000300651470175262700256020ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.letterror.lightMeter.prefs chunkSize 5 diameter 200 drawTail invert toolDiameter 30 toolStyle fluid com.typemytype.robofont.background.layerStrokeColor 0.0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0.0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright guillemotleft guillemotright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot mu dollar cent sterling currency yen Euro florin asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree fi fl .notdef a_b_c type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon dot dieresis acute space IJ arrowdown arrowleft arrowright arrowup ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansBoldWide.ufo/metainfo.plist000066400000000000000000000004761470175262700266410ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/000077500000000000000000000000001470175262700265175ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/features.fea000066400000000000000000000001041470175262700310050ustar00rootroot00000000000000# this is the feature from lightCondensed # Hi_this_is_the_feature. ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/fontinfo.plist000066400000000000000000000025521470175262700314220ustar00rootroot00000000000000 ascender 763.459275 capHeight 763.459275 copyright Copyright-token-string descender -200 familyName Intermediate guidelines italicAngle 0 openTypeOS2VendorID ADBE postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleName Narrow unitsPerEm 1000.0 versionMajor 1 versionMinor 2 xHeight 500 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.background/000077500000000000000000000000001470175262700321435ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.background/S_.glif000066400000000000000000000044611470175262700333540ustar00rootroot00000000000000 contents.plist000066400000000000000000000003441470175262700347770ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.background S S_.glif layerinfo.plist000066400000000000000000000005271470175262700351350ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.background color 0.5,1,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.interpolation/000077500000000000000000000000001470175262700327135ustar00rootroot00000000000000E_.glif000066400000000000000000000036221470175262700340250ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.interpolation contents.plist000066400000000000000000000003441470175262700355470ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.interpolation E E_.glif layerinfo.plist000066400000000000000000000005301470175262700356770ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.interpolation color 0,1,0.25,0.7 lib com.typemytype.robofont.segmentType curve glyphs.reference.interpolation/000077500000000000000000000000001470175262700345715ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufoE_.glif000066400000000000000000000016661470175262700357700ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.reference.interpolation S_.glif000066400000000000000000000061551470175262700360040ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.reference.interpolation contents.plist000066400000000000000000000004141470175262700375020ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.reference.interpolation E E_.glif S S_.glif layerinfo.plist000066400000000000000000000005251470175262700376400ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs.reference.interpolation color 0,1,1,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs/000077500000000000000000000000001470175262700300255ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs/B_.glif000066400000000000000000000034161470175262700312140ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs/E_.glif000066400000000000000000000017271470175262700312220ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs/F_.glif000066400000000000000000000016331470175262700312170ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs/G_.glif000066400000000000000000000044031470175262700312160ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs/S_.glif000066400000000000000000000051261470175262700312350ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs/contents.plist000066400000000000000000000006461470175262700327450ustar00rootroot00000000000000 B B_.glif E E_.glif F F_.glif G G_.glif S S_.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/glyphs/layerinfo.plist000066400000000000000000000005631470175262700330760ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/layercontents.plist000066400000000000000000000011471470175262700324710ustar00rootroot00000000000000 foreground glyphs background glyphs.background interpolation glyphs.interpolation reference.interpolation glyphs.reference.interpolation ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/lib.plist000066400000000000000000000403511470175262700303450ustar00rootroot00000000000000 com.letterror.lightMeter.prefs chunkSize 5 diameter 200 drawTail invert toolDiameter 30 toolStyle fluid com.typemytype.robofont.background.layerStrokeColor 0.0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0.0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1.0 0.0 0.0 0.25 @MMK_L_C 1.0 0.5 0.0 0.25 @MMK_L_E 1.0 1.0 0.0 0.25 @MMK_L_I 0.0 1.0 0.0 0.25 @MMK_L_N 0.0 1.0 1.0 0.25 @MMK_L_O 0.0 0.5 1.0 0.25 @MMK_L_S 0.0 0.0 1.0 0.25 @MMK_L_U 0.5 0.0 1.0 0.25 @MMK_L_Y 1.0 0.0 1.0 0.25 @MMK_L_Z 1.0 0.0 0.5 0.25 @MMK_L_a 1.0 0.0 0.0 0.25 @MMK_L_c 1.0 0.5 0.0 0.25 @MMK_L_e 1.0 1.0 0.0 0.25 @MMK_L_i 0.0 1.0 0.0 0.25 @MMK_L_n 0.0 1.0 1.0 0.25 @MMK_L_o 0.0 0.5 1.0 0.25 @MMK_L_s 0.0 0.0 1.0 0.25 @MMK_L_u 0.5 0.0 1.0 0.25 @MMK_L_y 1.0 0.0 1.0 0.25 @MMK_L_z 1.0 0.0 0.5 0.25 @MMK_R_A 1.0 0.0 0.0 0.25 @MMK_R_C 1.0 0.5 0.0 0.25 @MMK_R_E 1.0 1.0 0.0 0.25 @MMK_R_I 0.0 1.0 0.0 0.25 @MMK_R_N 0.0 1.0 1.0 0.25 @MMK_R_O 0.0 0.5 1.0 0.25 @MMK_R_S 0.0 0.0 1.0 0.25 @MMK_R_U 0.5 0.0 1.0 0.25 @MMK_R_Y 1.0 0.0 1.0 0.25 @MMK_R_Z 1.0 0.0 0.5 0.25 @MMK_R_a 1.0 0.0 0.0 0.25 @MMK_R_c 1.0 0.5 0.0 0.25 @MMK_R_e 1.0 1.0 0.0 0.25 @MMK_R_i 0.0 1.0 0.0 0.25 @MMK_R_n 0.0 1.0 1.0 0.25 @MMK_R_o 0.0 0.5 1.0 0.25 @MMK_R_s 0.0 0.0 1.0 0.25 @MMK_R_u 0.5 0.0 1.0 0.25 @MMK_R_y 1.0 0.0 1.0 0.25 @MMK_R_z 1.0 0.0 0.5 0.25 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe mu zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot dollar cent sterling currency yen Euro asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree florin guillemotleft guillemotright fi fl a_b_c .notdef type glyphList designspace width 0.0 weight 634.59275 public.glyphOrder B E F G S testLibItemKey a b c ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateCondensed.ufo/metainfo.plist000066400000000000000000000004761470175262700314050ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/000077500000000000000000000000001470175262700255055ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/features.fea000066400000000000000000000001041470175262700277730ustar00rootroot00000000000000# this is the feature from lightCondensed # Hi_this_is_the_feature. ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/fontinfo.plist000066400000000000000000000025501470175262700304060ustar00rootroot00000000000000 ascender 763.459275 capHeight 763.459275 copyright Copyright-token-string descender -200 familyName Intermediate guidelines italicAngle 0 openTypeOS2VendorID ADBE postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleName Wide unitsPerEm 1000.0 versionMajor 1 versionMinor 2 xHeight 500 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs.background/000077500000000000000000000000001470175262700311315ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs.background/S_.glif000066400000000000000000000045341470175262700323430ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs.background/contents.plist000066400000000000000000000003441470175262700340440ustar00rootroot00000000000000 S S_.glif layerinfo.plist000066400000000000000000000005271470175262700341230ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs.background color 0.5,1,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs/000077500000000000000000000000001470175262700270135ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs/B_.glif000066400000000000000000000034401470175262700301770ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs/E_.glif000066400000000000000000000022121470175262700301760ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs/F_.glif000066400000000000000000000016171470175262700302070ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs/G_.glif000066400000000000000000000041441470175262700302060ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs/contents.plist000066400000000000000000000005701470175262700317270ustar00rootroot00000000000000 B B_.glif E E_.glif F F_.glif G G_.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/glyphs/layerinfo.plist000066400000000000000000000005631470175262700320640ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/kerning.plist000066400000000000000000000125261470175262700302250ustar00rootroot00000000000000 @MMK_L_A V -1.4210854715202004e-14 B H -14.61629 J -31.05961624999999 O -3.654072499999998 S -9.135181250000002 T -31.059616249999998 U -12.78925375 V -31.059616250000005 public.kern2.@MMK_R_A -9.135181249999999 C J -27.405543749999993 T -14.61629 V -23.75147125 public.kern2.@MMK_R_A -14.61629 E J -29.23258 T -3.6540725000000016 F H -12.78925375 J -89.52477625 O -18.270362499999997 S -25.5785075 U -3.6540725000000016 public.kern2.@MMK_R_A -45.67590625 G J -21.924435000000003 O -5.481108749999999 S -5.481108750000001 T -38.36776125 U -7.308145 V -25.5785075 public.kern2.@MMK_R_A -14.61629 H J -14.616290000000006 S -10.962217500000001 J J -49.32997874999998 O -3.6540725 public.kern2.@MMK_R_A -25.5785075 L J -9.135181250000002 O -34.71368875 T -111.44921125000002 U -31.059616250000005 V -67.60034125000001 O J -43.848870000000005 S -16.44332625 T -32.8866525 V -29.23258 public.kern2.@MMK_R_A -10.962217499999998 P J -73.08144999999999 T -7.308145 public.kern2.@MMK_R_A -34.713688749999996 R H -16.44332625 J -56.63812374999999 O -20.097398750000004 S -25.5785075 T -29.23258 U -21.924435000000003 V -34.71368875 public.kern2.@MMK_R_A -29.23258 S H -1.827036249999999 J -42.02183375 O -14.61629 S -16.44332625 T -18.270362500000005 U -3.6540725000000016 V -7.308145000000003 public.kern2.@MMK_R_A -16.44332625 T H -5.481108750000001 J -115.10328374999995 O -32.8866525 S -9.135181249999999 public.kern2.@MMK_R_A -78.56255875 U J -51.157015 public.kern2.@MMK_R_A -27.40554375 V @MMK_R_A -1.4210854715202004e-14 H -7.308145 J -96.83292124999997 O -20.097398750000004 S -18.270362500000005 public.kern2.@MMK_R_A -76.7355225 public.kern1.@MMK_L_A J -12.78925375 O -20.097398749999996 T -69.42737749999999 U -34.713688749999996 V -65.773305 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/layercontents.plist000066400000000000000000000005771470175262700314650ustar00rootroot00000000000000 foreground glyphs background glyphs.background ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/lib.plist000066400000000000000000000375371470175262700273470ustar00rootroot00000000000000 com.typemytype.robofont.background.layerStrokeColor 0.0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0.0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1.0 0.0 0.0 0.25 @MMK_L_C 1.0 0.5 0.0 0.25 @MMK_L_E 1.0 1.0 0.0 0.25 @MMK_L_I 0.0 1.0 0.0 0.25 @MMK_L_N 0.0 1.0 1.0 0.25 @MMK_L_O 0.0 0.5 1.0 0.25 @MMK_L_S 0.0 0.0 1.0 0.25 @MMK_L_U 0.5 0.0 1.0 0.25 @MMK_L_Y 1.0 0.0 1.0 0.25 @MMK_L_Z 1.0 0.0 0.5 0.25 @MMK_L_a 1.0 0.0 0.0 0.25 @MMK_L_c 1.0 0.5 0.0 0.25 @MMK_L_e 1.0 1.0 0.0 0.25 @MMK_L_i 0.0 1.0 0.0 0.25 @MMK_L_n 0.0 1.0 1.0 0.25 @MMK_L_o 0.0 0.5 1.0 0.25 @MMK_L_s 0.0 0.0 1.0 0.25 @MMK_L_u 0.5 0.0 1.0 0.25 @MMK_L_y 1.0 0.0 1.0 0.25 @MMK_L_z 1.0 0.0 0.5 0.25 @MMK_R_A 1.0 0.0 0.0 0.25 @MMK_R_C 1.0 0.5 0.0 0.25 @MMK_R_E 1.0 1.0 0.0 0.25 @MMK_R_I 0.0 1.0 0.0 0.25 @MMK_R_N 0.0 1.0 1.0 0.25 @MMK_R_O 0.0 0.5 1.0 0.25 @MMK_R_S 0.0 0.0 1.0 0.25 @MMK_R_U 0.5 0.0 1.0 0.25 @MMK_R_Y 1.0 0.0 1.0 0.25 @MMK_R_Z 1.0 0.0 0.5 0.25 @MMK_R_a 1.0 0.0 0.0 0.25 @MMK_R_c 1.0 0.5 0.0 0.25 @MMK_R_e 1.0 1.0 0.0 0.25 @MMK_R_i 0.0 1.0 0.0 0.25 @MMK_R_n 0.0 1.0 1.0 0.25 @MMK_R_o 0.0 0.5 1.0 0.25 @MMK_R_s 0.0 0.0 1.0 0.25 @MMK_R_u 0.5 0.0 1.0 0.25 @MMK_R_y 1.0 0.0 1.0 0.25 @MMK_R_z 1.0 0.0 0.5 0.25 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe mu zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot dollar cent sterling currency yen Euro asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree florin guillemotleft guillemotright fi fl a_b_c .notdef type glyphList designspace width 1000.0 weight 634.59275 public.glyphOrder B E F G testLibItemKey a b c ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansIntermediateWide.ufo/metainfo.plist000066400000000000000000000004761470175262700303730ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/000077500000000000000000000000001470175262700251545ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/features.fea000066400000000000000000000001041470175262700274420ustar00rootroot00000000000000# this is the feature from lightCondensed # Hi_this_is_the_feature. ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/fontinfo.plist000066400000000000000000000035401470175262700300550ustar00rootroot00000000000000 ascender 700 capHeight 700 copyright License same as MutatorMath. BSD 3-clause. [test-token: C] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: C] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-LightCondensed postscriptFullName MutatorMathTest LightCondensed postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName LightCondensed unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.background/000077500000000000000000000000001470175262700306005ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.background/S_.closed.glif000066400000000000000000000076361470175262700332700ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.background/S_.glif000066400000000000000000000064361470175262700320150ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 820.0 width 1000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.background/contents.plist000066400000000000000000000004521470175262700335130ustar00rootroot00000000000000 S S_.glif S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.background/layerinfo.plist000066400000000000000000000003641470175262700336500ustar00rootroot00000000000000 color 0,1,1,0.7 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/000077500000000000000000000000001470175262700317725ustar00rootroot00000000000000S_.closed.glif000066400000000000000000000054111470175262700343700ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle com.letterror.skateboard.navigator location weight 700.0 width 569.078 contents.plist000066400000000000000000000003741470175262700346310ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle S.closed S_.closed.glif layerinfo.plist000066400000000000000000000003661470175262700347650ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle color 0.5,0,1,0.7 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/000077500000000000000000000000001470175262700314645ustar00rootroot00000000000000S_.closed.glif000066400000000000000000000067271470175262700340750ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide com.letterror.skateboard.navigator location weight 673.7998527960526 width 1000.0 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/S_.glif000066400000000000000000000067161470175262700327020ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 759.5997715404774 width 1000.0 contents.plist000066400000000000000000000004521470175262700343200ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide S S_.glif S.closed S_.closed.glif layerinfo.plist000066400000000000000000000003671470175262700344600ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide color 0,0.25,1,0.7 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/000077500000000000000000000000001470175262700320125ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/B_.glif000066400000000000000000000040771470175262700332050ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 715.728 width -138.956 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/E_.glif000066400000000000000000000026771470175262700332140ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 715.728 width -138.956 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/F_.glif000066400000000000000000000013571470175262700332070ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/G_.glif000066400000000000000000000046241470175262700332100ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 715.728 width -138.956 S_.closed.glif000066400000000000000000000006461470175262700344150ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar com.letterror.skateboard.navigator location weight 800.0 width 0.0 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/S_.glif000066400000000000000000000066521470175262700332270ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 889.1982375724028 width 0.0 contents.plist000066400000000000000000000007421470175262700346500ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar B B_.glif E E_.glif F F_.glif G G_.glif S S_.glif S.closed S_.closed.glif layerinfo.plist000066400000000000000000000003671470175262700350060ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbar color 0,1,0.25,0.7 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbars/000077500000000000000000000000001470175262700321755ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbars/E_.glif000066400000000000000000000022301470175262700333600ustar00rootroot00000000000000 contents.plist000066400000000000000000000003561470175262700350340ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbars E E_.glif layerinfo.plist000066400000000000000000000002671470175262700351700ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support.crossbars ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support/000077500000000000000000000000001470175262700301755ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support/A_.glif000066400000000000000000000014741470175262700313650ustar00rootroot00000000000000 com.letterror.skateboard.navigator location space 0.0 weight 600.0 width 500.0 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support/S_.glif000066400000000000000000000052221470175262700314020ustar00rootroot00000000000000 com.letterror.skateboard.navigator location space 25.0 weight 707.6485770089287 width 181.31051199776795 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support/W_.glif000066400000000000000000000015251470175262700314100ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support/contents.plist000066400000000000000000000005121470175262700331050ustar00rootroot00000000000000 A A_.glif S S_.glif W W_.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs.support/layerinfo.plist000066400000000000000000000005631470175262700332460ustar00rootroot00000000000000 color 0,1,0.25,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/000077500000000000000000000000001470175262700264625ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/A_.glif000066400000000000000000000016561470175262700276540ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/A_acute.glif000066400000000000000000000005401470175262700306650ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/A_dieresis.glif000066400000000000000000000005461470175262700314010ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/B_.glif000066400000000000000000000034071470175262700276510ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/C_.glif000066400000000000000000000025721470175262700276540ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/D_.glif000066400000000000000000000021601470175262700276460ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/E_.glif000066400000000000000000000017171470175262700276560ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/F_.glif000066400000000000000000000013471470175262700276560ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/G_.glif000066400000000000000000000033001470175262700276460ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/H_.glif000066400000000000000000000013451470175262700276560ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/I_.glif000066400000000000000000000013431470175262700276550ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/I_.narrow.glif000066400000000000000000000012751470175262700311700ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/I_J_.glif000066400000000000000000000021211470175262700301210ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/J_.glif000066400000000000000000000016411470175262700276570ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/J_.narrow.glif000066400000000000000000000014351470175262700311670ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/K_.glif000066400000000000000000000015461470175262700276640ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/L_.glif000066400000000000000000000010321470175262700276530ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/M_.glif000066400000000000000000000016221470175262700276610ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/N_.glif000066400000000000000000000013401470175262700276570ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/O_.glif000066400000000000000000000025341470175262700276660ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/P_.glif000066400000000000000000000021721470175262700276650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/Q_.glif000066400000000000000000000005711470175262700276670ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/R_.glif000066400000000000000000000026661470175262700276770ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/S_.closed.glif000066400000000000000000000036561470175262700311500ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/S_.glif000066400000000000000000000046251470175262700276750ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/T_.glif000066400000000000000000000010421470175262700276640ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/U_.glif000066400000000000000000000016411470175262700276720ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/V_.glif000066400000000000000000000013411470175262700276700ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/W_.glif000066400000000000000000000015251470175262700276750ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/X_.glif000066400000000000000000000014361470175262700276770ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/Y_.glif000066400000000000000000000021401470175262700276710ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/Z_.glif000066400000000000000000000013431470175262700276760ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/acute.glif000066400000000000000000000005411470175262700304260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/arrowdown.glif000066400000000000000000000011641470175262700313510ustar00rootroot00000000000000 public.markColor 0,0.95,0.95,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/arrowleft.glif000066400000000000000000000012621470175262700313330ustar00rootroot00000000000000 public.markColor 0,0.95,0.95,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/arrowright.glif000066400000000000000000000011631470175262700315160ustar00rootroot00000000000000 public.markColor 0,0.95,0.95,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/arrowup.glif000066400000000000000000000011621470175262700310240ustar00rootroot00000000000000 public.markColor 0,0.95,0.95,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/colon.glif000066400000000000000000000003461470175262700304420ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/comma.glif000066400000000000000000000010031470175262700304130ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/contents.plist000066400000000000000000000051771470175262700314060ustar00rootroot00000000000000 A A_.glif Aacute A_acute.glif Adieresis A_dieresis.glif B B_.glif C C_.glif D D_.glif E E_.glif F F_.glif G G_.glif H H_.glif I I_.glif I.narrow I_.narrow.glif IJ I_J_.glif J J_.glif J.narrow J_.narrow.glif K K_.glif L L_.glif M M_.glif N N_.glif O O_.glif P P_.glif Q Q_.glif R R_.glif S S_.glif S.closed S_.closed.glif T T_.glif U U_.glif V V_.glif W W_.glif X X_.glif Y Y_.glif Z Z_.glif acute acute.glif arrowdown arrowdown.glif arrowleft arrowleft.glif arrowright arrowright.glif arrowup arrowup.glif colon colon.glif comma comma.glif dieresis dieresis.glif dot dot.glif period period.glif quotedblbase quotedblbase.glif quotedblleft quotedblleft.glif quotedblright quotedblright.glif quotesinglbase quotesinglbase.glif semicolon semicolon.glif space space.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/dieresis.glif000066400000000000000000000005611470175262700311360ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/dot.glif000066400000000000000000000005351470175262700301160ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/layerinfo.plist000066400000000000000000000005631470175262700315330ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/period.glif000066400000000000000000000005361470175262700306130ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/quotedblbase.glif000066400000000000000000000003531470175262700320000ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/quotedblleft.glif000066400000000000000000000005051470175262700320170ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/quotedblright.glif000066400000000000000000000004101470175262700321750ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/quotesinglbase.glif000066400000000000000000000003011470175262700323440ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/semicolon.glif000066400000000000000000000003511470175262700313140ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/glyphs/space.glif000066400000000000000000000002321470175262700304150ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/groups.plist000066400000000000000000000007531470175262700275550ustar00rootroot00000000000000 public.kern1.@MMK_L_A A public.kern2.@MMK_R_A A testGroup E F H ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/kerning.plist000066400000000000000000000010051470175262700276620ustar00rootroot00000000000000 T public.kern2.@MMK_R_A -75 V public.kern2.@MMK_R_A -100 public.kern1.@MMK_L_A V -15 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/layercontents.plist000066400000000000000000000014531470175262700311260ustar00rootroot00000000000000 foreground glyphs support glyphs.support support.crossbar glyphs.support.crossbar background glyphs.background support.S.wide glyphs.support.S_.wide support.S.middle glyphs.support.S_.middle ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/lib.plist000066400000000000000000000445501470175262700270070ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.letterror.lightMeter.prefs chunkSize 5 diameter 200 drawTail invert toolDiameter 30 toolStyle fluid com.typemytype.robofont.background.layerStrokeColor 0.0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0.0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1.0 0.0 0.0 0.25 @MMK_L_C 1.0 0.5 0.0 0.25 @MMK_L_E 1.0 1.0 0.0 0.25 @MMK_L_I 0.0 1.0 0.0 0.25 @MMK_L_N 0.0 1.0 1.0 0.25 @MMK_L_O 0.0 0.5 1.0 0.25 @MMK_L_S 0.0 0.0 1.0 0.25 @MMK_L_U 0.5 0.0 1.0 0.25 @MMK_L_Y 1.0 0.0 1.0 0.25 @MMK_L_Z 1.0 0.0 0.5 0.25 @MMK_L_a 1.0 0.0 0.0 0.25 @MMK_L_c 1.0 0.5 0.0 0.25 @MMK_L_e 1.0 1.0 0.0 0.25 @MMK_L_i 0.0 1.0 0.0 0.25 @MMK_L_n 0.0 1.0 1.0 0.25 @MMK_L_o 0.0 0.5 1.0 0.25 @MMK_L_s 0.0 0.0 1.0 0.25 @MMK_L_u 0.5 0.0 1.0 0.25 @MMK_L_y 1.0 0.0 1.0 0.25 @MMK_L_z 1.0 0.0 0.5 0.25 @MMK_R_A 1.0 0.0 0.0 0.25 @MMK_R_C 1.0 0.5 0.0 0.25 @MMK_R_E 1.0 1.0 0.0 0.25 @MMK_R_I 0.0 1.0 0.0 0.25 @MMK_R_N 0.0 1.0 1.0 0.25 @MMK_R_O 0.0 0.5 1.0 0.25 @MMK_R_S 0.0 0.0 1.0 0.25 @MMK_R_U 0.5 0.0 1.0 0.25 @MMK_R_Y 1.0 0.0 1.0 0.25 @MMK_R_Z 1.0 0.0 0.5 0.25 @MMK_R_a 1.0 0.0 0.0 0.25 @MMK_R_c 1.0 0.5 0.0 0.25 @MMK_R_e 1.0 1.0 0.0 0.25 @MMK_R_i 0.0 1.0 0.0 0.25 @MMK_R_n 0.0 1.0 1.0 0.25 @MMK_R_o 0.0 0.5 1.0 0.25 @MMK_R_s 0.0 0.0 1.0 0.25 @MMK_R_u 0.5 0.0 1.0 0.25 @MMK_R_y 1.0 0.0 1.0 0.25 @MMK_R_z 1.0 0.0 0.5 0.25 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe mu zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot dollar cent sterling currency yen Euro asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree florin guillemotleft guillemotright fi fl a_b_c .notdef type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon arrowleft arrowup arrowright arrowdown dot dieresis acute space IJ testLibItemKey a b c ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightCondensed.ufo/metainfo.plist000066400000000000000000000004761470175262700300420ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/000077500000000000000000000000001470175262700241425ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/features.fea000066400000000000000000000000451470175262700264340ustar00rootroot00000000000000# this is the feature from lightWide ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/fontinfo.plist000066400000000000000000000035211470175262700270420ustar00rootroot00000000000000 ascender 700 capHeight 700 copyright License same as MutatorMath. BSD 3-clause. [test-token: D] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: D] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-LightWide postscriptFullName MutatorMathTest LightWide postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName LightWide unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.background/000077500000000000000000000000001470175262700275665ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.background/S_.closed.glif000066400000000000000000000036551470175262700322530ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.background/contents.plist000066400000000000000000000003741470175262700325040ustar00rootroot00000000000000 S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.background/layerinfo.plist000066400000000000000000000003661470175262700326400ustar00rootroot00000000000000 color 0.5,1,0,0.7 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_0.00_weight_0.00/000077500000000000000000000000001470175262700326025ustar00rootroot00000000000000contents.plist000066400000000000000000000002651470175262700354400ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_0.00_weight_0.00 layerinfo.plist000066400000000000000000000003551470175262700355730ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_0.00_weight_0.00 color 0,0.25,1,0.7 glyphs.master_width_500.00_weight_500.00/000077500000000000000000000000001470175262700330355ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufoS_.glif000066400000000000000000000046331470175262700342470ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_500.00_weight_500.00 contents.plist000066400000000000000000000003441470175262700357500ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_500.00_weight_500.00 S S_.glif layerinfo.plist000066400000000000000000000005301470175262700361000ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_500.00_weight_500.00 color 0,1,0.25,0.7 lib com.typemytype.robofont.segmentType curve glyphs.master_width_695.65_weight_166.38/000077500000000000000000000000001470175262700331125ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufocontents.plist000066400000000000000000000002651470175262700360270ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_695.65_weight_166.38 layerinfo.plist000066400000000000000000000003541470175262700361610ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_695.65_weight_166.38 color 0.5,0,1,0.7 glyphs.master_width_720.00_weight_645.00/000077500000000000000000000000001470175262700330535ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufoS_.glif000066400000000000000000000047511470175262700342660ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_720.00_weight_645.00 contents.plist000066400000000000000000000003441470175262700357660ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_720.00_weight_645.00 S S_.glif layerinfo.plist000066400000000000000000000005251470175262700361220ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs.master_width_720.00_weight_645.00 color 0,1,1,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/000077500000000000000000000000001470175262700254505ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/A_.glif000066400000000000000000000024501470175262700266330ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/A_acute.glif000066400000000000000000000005421470175262700276550ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/A_dieresis.glif000066400000000000000000000005501470175262700303620ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/B_.glif000066400000000000000000000033711470175262700266370ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/C_.glif000066400000000000000000000026101470175262700266330ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/D_.glif000066400000000000000000000021561470175262700266410ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/E_.glif000066400000000000000000000025151470175262700266410ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/F_.glif000066400000000000000000000016721470175262700266450ustar00rootroot00000000000000 com.typemytype.robofont.guideline.magnetic.tCW6w1QdWl 5 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/G_.glif000066400000000000000000000033271470175262700266450ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/H_.glif000066400000000000000000000013561470175262700266460ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/I_.glif000066400000000000000000000013471470175262700266470ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/I_.narrow.glif000066400000000000000000000012771470175262700301600ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/I_J_.glif000066400000000000000000000022061470175262700271130ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/J_.glif000066400000000000000000000020401470175262700266370ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/J_.narrow.glif000066400000000000000000000014421470175262700301530ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/K_.glif000066400000000000000000000015531470175262700266500ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/L_.glif000066400000000000000000000010361470175262700266450ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/M_.glif000066400000000000000000000016341470175262700266520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/N_.glif000066400000000000000000000013551470175262700266530ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/O_.glif000066400000000000000000000025541470175262700266560ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/P_.glif000066400000000000000000000021521470175262700266510ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/Q_.glif000066400000000000000000000005721470175262700266560ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/R_.glif000066400000000000000000000026631470175262700266620ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/S_.closed.glif000066400000000000000000000036621470175262700301330ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/S_.glif000066400000000000000000000044661470175262700266660ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/T_.glif000066400000000000000000000010451470175262700266550ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/U_.glif000066400000000000000000000016571470175262700266670ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/V_.glif000066400000000000000000000013441470175262700266610ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/W_.glif000066400000000000000000000015311470175262700266600ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/X_.glif000066400000000000000000000014411470175262700266610ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/Y_.glif000066400000000000000000000013531470175262700266640ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/Z_.glif000066400000000000000000000013601470175262700266630ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/acute.glif000066400000000000000000000005411470175262700274140ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/arrowdown.glif000066400000000000000000000010021470175262700303260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/arrowleft.glif000066400000000000000000000010061470175262700303150ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/arrowright.glif000066400000000000000000000010061470175262700305000ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/arrowup.glif000066400000000000000000000010051470175262700300060ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/colon.glif000066400000000000000000000003461470175262700274300ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/comma.glif000066400000000000000000000010111470175262700274000ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/contents.plist000066400000000000000000000051771470175262700303740ustar00rootroot00000000000000 A A_.glif Aacute A_acute.glif Adieresis A_dieresis.glif B B_.glif C C_.glif D D_.glif E E_.glif F F_.glif G G_.glif H H_.glif I I_.glif I.narrow I_.narrow.glif IJ I_J_.glif J J_.glif J.narrow J_.narrow.glif K K_.glif L L_.glif M M_.glif N N_.glif O O_.glif P P_.glif Q Q_.glif R R_.glif S S_.glif S.closed S_.closed.glif T T_.glif U U_.glif V V_.glif W W_.glif X X_.glif Y Y_.glif Z Z_.glif acute acute.glif arrowdown arrowdown.glif arrowleft arrowleft.glif arrowright arrowright.glif arrowup arrowup.glif colon colon.glif comma comma.glif dieresis dieresis.glif dot dot.glif period period.glif quotedblbase quotedblbase.glif quotedblleft quotedblleft.glif quotedblright quotedblright.glif quotesinglbase quotesinglbase.glif semicolon semicolon.glif space space.glif ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/dieresis.glif000066400000000000000000000005621470175262700301250ustar00rootroot00000000000000 public.markColor 0.6567,0.6903,1,1 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/dot.glif000066400000000000000000000005351470175262700271040ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/layerinfo.plist000066400000000000000000000005631470175262700305210ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/period.glif000066400000000000000000000005401470175262700275740ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/quotedblbase.glif000066400000000000000000000003531470175262700307660ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/quotedblleft.glif000066400000000000000000000005051470175262700310050ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/quotedblright.glif000066400000000000000000000004101470175262700311630ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/quotesinglbase.glif000066400000000000000000000003011470175262700313320ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/semicolon.glif000066400000000000000000000003511470175262700303020ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/glyphs/space.glif000066400000000000000000000002321470175262700274030ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/groups.plist000066400000000000000000000005561470175262700265440ustar00rootroot00000000000000 public.kern1.@MMK_L_A A public.kern2.@MMK_R_A A ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/kerning.plist000066400000000000000000000113641470175262700266610ustar00rootroot00000000000000 B H -40 J -85 O -10 S -25 T -85 U -35 V -85 public.kern2.@MMK_R_A -25 C J -75 T -40 V -65 public.kern2.@MMK_R_A -40 E J -80 T -10 F H -35 J -245 O -50 S -70 U -10 public.kern2.@MMK_R_A -125 G J -60 O -15 S -15 T -105 U -20 V -70 public.kern2.@MMK_R_A -40 H J -40 S -30 J J -135 O -10 public.kern2.@MMK_R_A -70 L J -25 O -95 T -305 U -85 V -185 O J -120 S -45 T -90 V -80 public.kern2.@MMK_R_A -30 P J -200 T -20 public.kern2.@MMK_R_A -95 R H -45 J -155 O -55 S -70 T -80 U -60 V -95 public.kern2.@MMK_R_A -80 S H -5 J -115 O -40 S -45 T -50 U -10 V -20 public.kern2.@MMK_R_A -45 T H -15 J -315 O -90 S -25 public.kern2.@MMK_R_A -215 U J -140 public.kern2.@MMK_R_A -75 V H -20 J -265 O -55 S -50 public.kern2.@MMK_R_A -210 public.kern1.@MMK_L_A J -35 O -55 T -190 U -95 V -180 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/layercontents.plist000066400000000000000000000005771470175262700301220ustar00rootroot00000000000000 foreground glyphs background glyphs.background ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/lib.plist000066400000000000000000000435631470175262700260000ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.typemytype.robofont.background.layerStrokeColor 1.0 0.75 0.0 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0.0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1.0 0.0 0.0 0.25 @MMK_L_C 1.0 0.5 0.0 0.25 @MMK_L_E 1.0 1.0 0.0 0.25 @MMK_L_I 0.0 1.0 0.0 0.25 @MMK_L_N 0.0 1.0 1.0 0.25 @MMK_L_O 0.0 0.5 1.0 0.25 @MMK_L_S 0.0 0.0 1.0 0.25 @MMK_L_U 0.5 0.0 1.0 0.25 @MMK_L_Y 1.0 0.0 1.0 0.25 @MMK_L_Z 1.0 0.0 0.5 0.25 @MMK_L_a 1.0 0.0 0.0 0.25 @MMK_L_c 1.0 0.5 0.0 0.25 @MMK_L_e 1.0 1.0 0.0 0.25 @MMK_L_i 0.0 1.0 0.0 0.25 @MMK_L_n 0.0 1.0 1.0 0.25 @MMK_L_o 0.0 0.5 1.0 0.25 @MMK_L_s 0.0 0.0 1.0 0.25 @MMK_L_u 0.5 0.0 1.0 0.25 @MMK_L_y 1.0 0.0 1.0 0.25 @MMK_L_z 1.0 0.0 0.5 0.25 @MMK_R_A 1.0 0.0 0.0 0.25 @MMK_R_C 1.0 0.5 0.0 0.25 @MMK_R_E 1.0 1.0 0.0 0.25 @MMK_R_I 0.0 1.0 0.0 0.25 @MMK_R_N 0.0 1.0 1.0 0.25 @MMK_R_O 0.0 0.5 1.0 0.25 @MMK_R_S 0.0 0.0 1.0 0.25 @MMK_R_U 0.5 0.0 1.0 0.25 @MMK_R_Y 1.0 0.0 1.0 0.25 @MMK_R_Z 1.0 0.0 0.5 0.25 @MMK_R_a 1.0 0.0 0.0 0.25 @MMK_R_c 1.0 0.5 0.0 0.25 @MMK_R_e 1.0 1.0 0.0 0.25 @MMK_R_i 0.0 1.0 0.0 0.25 @MMK_R_n 0.0 1.0 1.0 0.25 @MMK_R_o 0.0 0.5 1.0 0.25 @MMK_R_s 0.0 0.0 1.0 0.25 @MMK_R_u 0.5 0.0 1.0 0.25 @MMK_R_y 1.0 0.0 1.0 0.25 @MMK_R_z 1.0 0.0 0.5 0.25 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright guillemotleft guillemotright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot mu dollar cent sterling currency yen Euro florin asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree fi fl .notdef a_b_c type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon arrowleft arrowup arrowright arrowdown dot dieresis acute space IJ ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSansLightWide.ufo/metainfo.plist000066400000000000000000000004761470175262700270300ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSans_missing.designspace000066400000000000000000000204161470175262700255140ustar00rootroot00000000000000 com.superpolator.data expandRules horizontalPreviewAxis width includeLegacyRules keepWorkFiles outputFormatUFO 3 previewtext HOPA roundGeometry verticalPreviewAxis weight ufo2ft-3.3.1/tests/data/MutatorSans/MutatorSans_no_default.designspace000066400000000000000000000172321470175262700261650ustar00rootroot00000000000000 com.superpolator.data expandRules horizontalPreviewAxis width includeLegacyRules keepWorkFiles outputFormatUFO 3 previewtext HOPA roundGeometry verticalPreviewAxis weight ufo2ft-3.3.1/tests/data/MutatorSansLite/000077500000000000000000000000001470175262700201075ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/LICENSE000066400000000000000000000020621470175262700211140ustar00rootroot00000000000000MIT License Copyright (c) 2017 Erik van Blokland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorFamily_v5_discrete_axis.designspace000066400000000000000000000157661470175262700304520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/000077500000000000000000000000001470175262700256035ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/features.fea000066400000000000000000000000521470175262700300730ustar00rootroot00000000000000# this is the feature from boldcondensed. ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/fontinfo.plist000066400000000000000000000034611470175262700305060ustar00rootroot00000000000000 ascender 800 capHeight 800 copyright License same as MutatorMath. BSD 3-clause. [test-token: A] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: A] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-BoldCondensed postscriptFullName MutatorMathTest BoldCondensed postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName BoldCondensed unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs.background/000077500000000000000000000000001470175262700312275ustar00rootroot00000000000000S_.closed.glif000066400000000000000000000034711470175262700336310ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs.background contents.plist000066400000000000000000000003661470175262700340670ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs.background S.closed S_.closed.glif layerinfo.plist000066400000000000000000000003601470175262700342140ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs.background color 0.5,1,0,0.7 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/000077500000000000000000000000001470175262700271115ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/A_.glif000066400000000000000000000023051470175262700302730ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/I_.glif000066400000000000000000000012611470175262700303030ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/I_.narrow.glif000066400000000000000000000012141470175262700316100ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/I_J_.glif000066400000000000000000000020411470175262700305510ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/S_.closed.glif000066400000000000000000000040631470175262700315700ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 1000 width 108.0069405692 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/S_.glif000066400000000000000000000042251470175262700303200ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/contents.plist000066400000000000000000000007311470175262700320240ustar00rootroot00000000000000 A A_.glif I I_.glif I.narrow I_.narrow.glif IJ I_J_.glif S S_.glif S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/glyphs/layerinfo.plist000066400000000000000000000005411470175262700321560ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/layercontents.plist000066400000000000000000000005511470175262700315530ustar00rootroot00000000000000 foreground glyphs background glyphs.background ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/lib.plist000066400000000000000000000152671470175262700274410ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.letterror.lightMeter.prefs chunkSize 5 diameter 200 drawTail invert toolDiameter 30 toolStyle fluid com.typemytype.robofont.background.layerStrokeColor 0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.defcon.sortDescriptor ascending space A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n ntilde o p q r s t u v w x y z zcaron zero one two three four five six seven eight nine underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent period comma colon semicolon exclam question slash backslash bar at ampersand paragraph bullet dollar trademark fi fl .notdef a_b_c Atilde Adieresis Acircumflex Aring Ccedilla Agrave Aacute quotedblright quotedblleft type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z IJ S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon dot dieresis acute space arrowdown arrowleft arrowright arrowup ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldCondensed.ufo/metainfo.plist000066400000000000000000000004641470175262700304660ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/000077500000000000000000000000001470175262700245715ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/features.fea000066400000000000000000000000441470175262700270620ustar00rootroot00000000000000# this is the feature from BoldWide ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/fontinfo.plist000066400000000000000000000034421470175262700274730ustar00rootroot00000000000000 ascender 800 capHeight 800 copyright License same as MutatorMath. BSD 3-clause. [test-token: B] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: B] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-BoldWide postscriptFullName MutatorMathTest BoldWide postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName BoldWide unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs.background/000077500000000000000000000000001470175262700302155ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs.background/S_.closed.glif000066400000000000000000000034671470175262700327030ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs.background/S_.glif000066400000000000000000000035111470175262700314210ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs.background/contents.plist000066400000000000000000000004401470175262700331250ustar00rootroot00000000000000 S S_.glif S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs.background/layerinfo.plist000066400000000000000000000003601470175262700332610ustar00rootroot00000000000000 color 0.5,1,0,0.7 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/000077500000000000000000000000001470175262700260775ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/A_.glif000066400000000000000000000015571470175262700272710ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/I_.glif000066400000000000000000000012621470175262700272720ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/I_.narrow.glif000066400000000000000000000012141470175262700305760ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/I_J_.glif000066400000000000000000000020461470175262700275440ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/S_.closed.glif000066400000000000000000000042201470175262700305510ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/S_.glif000066400000000000000000000042361470175262700273100ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/contents.plist000066400000000000000000000007311470175262700310120ustar00rootroot00000000000000 A A_.glif I I_.glif I.narrow I_.narrow.glif IJ I_J_.glif S S_.glif S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/glyphs/layerinfo.plist000066400000000000000000000005411470175262700311440ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/layercontents.plist000066400000000000000000000005511470175262700305410ustar00rootroot00000000000000 foreground glyphs background glyphs.background ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/lib.plist000066400000000000000000000247261470175262700264270ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.letterror.lightMeter.prefs chunkSize 5 diameter 200 drawTail invert toolDiameter 30 toolStyle fluid com.typemytype.robofont.background.layerStrokeColor 0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright guillemotleft guillemotright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot mu dollar cent sterling currency yen Euro florin asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree fi fl .notdef a_b_c type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon dot dieresis acute space IJ arrowdown arrowleft arrowright arrowup ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansBoldWide.ufo/metainfo.plist000066400000000000000000000004641470175262700274540ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/000077500000000000000000000000001470175262700257725ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/features.fea000066400000000000000000000001041470175262700302600ustar00rootroot00000000000000# this is the feature from lightCondensed # Hi_this_is_the_feature. ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/fontinfo.plist000066400000000000000000000034641470175262700307000ustar00rootroot00000000000000 ascender 700 capHeight 700 copyright License same as MutatorMath. BSD 3-clause. [test-token: C] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: C] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-LightCondensed postscriptFullName MutatorMathTest LightCondensed postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName LightCondensed unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.background/000077500000000000000000000000001470175262700314165ustar00rootroot00000000000000S_.closed.glif000066400000000000000000000067231470175262700340230ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.background ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.background/S_.glif000066400000000000000000000050711470175262700326250ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 820 width 1000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 contents.plist000066400000000000000000000004401470175262700342470ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.background S S_.glif S.closed S_.closed.glif layerinfo.plist000066400000000000000000000003561470175262700344100ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.background color 0,1,1,0.7 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/000077500000000000000000000000001470175262700326105ustar00rootroot00000000000000S_.closed.glif000066400000000000000000000046741470175262700352200ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle com.letterror.skateboard.navigator location weight 700 width 569.078 contents.plist000066400000000000000000000003661470175262700354500ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle S.closed S_.closed.glif layerinfo.plist000066400000000000000000000003601470175262700355750ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle color 0.5,0,1,0.7 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/000077500000000000000000000000001470175262700323025ustar00rootroot00000000000000S_.closed.glif000066400000000000000000000057641470175262700347130ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide com.letterror.skateboard.navigator location weight 673.7998527961 width 1000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/S_.glif000066400000000000000000000057531470175262700335200ustar00rootroot00000000000000 com.letterror.skateboard.navigator location weight 759.5997715405 width 1000 contents.plist000066400000000000000000000004401470175262700351330ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide S S_.glif S.closed S_.closed.glif layerinfo.plist000066400000000000000000000003611470175262700352700ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide color 0,0.25,1,0.7 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/000077500000000000000000000000001470175262700326305ustar00rootroot00000000000000S_.glif000066400000000000000000000056771470175262700337740ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.crossbar com.letterror.skateboard.navigator location weight 889.1982375724 width 0 contents.plist000066400000000000000000000003501470175262700354610ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.crossbar S S_.glif layerinfo.plist000066400000000000000000000003611470175262700356160ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.crossbar color 0,1,0.25,0.7 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.crossbars/000077500000000000000000000000001470175262700330135ustar00rootroot00000000000000E_.glif000066400000000000000000000022301470175262700341170ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.crossbars contents.plist000066400000000000000000000003561470175262700356520ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.crossbars E E_.glif layerinfo.plist000066400000000000000000000002671470175262700360060ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support.crossbars ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support/000077500000000000000000000000001470175262700310135ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support/A_.glif000066400000000000000000000013341470175262700321760ustar00rootroot00000000000000 com.letterror.skateboard.navigator location space 0 weight 600 width 500 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support/S_.glif000066400000000000000000000046521470175262700322260ustar00rootroot00000000000000 com.letterror.skateboard.navigator location space 25 weight 707.6485770089 width 181.3105119978 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support/contents.plist000066400000000000000000000004221470175262700337230ustar00rootroot00000000000000 A A_.glif S S_.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs.support/layerinfo.plist000066400000000000000000000005411470175262700340600ustar00rootroot00000000000000 color 0,1,0.25,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/000077500000000000000000000000001470175262700273005ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/A_.glif000066400000000000000000000015521470175262700304650ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/I_.glif000066400000000000000000000012571470175262700304770ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/I_.narrow.glif000066400000000000000000000012141470175262700317770ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/I_J_.glif000066400000000000000000000020041470175262700307370ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/S_.closed.glif000066400000000000000000000034431470175262700317600ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/S_.glif000066400000000000000000000043071470175262700305100ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/contents.plist000066400000000000000000000007311470175262700322130ustar00rootroot00000000000000 A A_.glif I I_.glif I.narrow I_.narrow.glif IJ I_J_.glif S S_.glif S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/glyphs/layerinfo.plist000066400000000000000000000005411470175262700323450ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/layercontents.plist000066400000000000000000000013551470175262700317450ustar00rootroot00000000000000 foreground glyphs support glyphs.support support.crossbar glyphs.support.crossbar background glyphs.background support.S.wide glyphs.support.S_.wide support.S.middle glyphs.support.S_.middle ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/lib.plist000066400000000000000000000402541470175262700276220ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.letterror.lightMeter.prefs chunkSize 5 diameter 200 drawTail invert toolDiameter 30 toolStyle fluid com.typemytype.robofont.background.layerStrokeColor 0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1 0 0 0.25 @MMK_L_C 1 0.5 0 0.25 @MMK_L_E 1 1 0 0.25 @MMK_L_I 0 1 0 0.25 @MMK_L_N 0 1 1 0.25 @MMK_L_O 0 0.5 1 0.25 @MMK_L_S 0 0 1 0.25 @MMK_L_U 0.5 0 1 0.25 @MMK_L_Y 1 0 1 0.25 @MMK_L_Z 1 0 0.5 0.25 @MMK_L_a 1 0 0 0.25 @MMK_L_c 1 0.5 0 0.25 @MMK_L_e 1 1 0 0.25 @MMK_L_i 0 1 0 0.25 @MMK_L_n 0 1 1 0.25 @MMK_L_o 0 0.5 1 0.25 @MMK_L_s 0 0 1 0.25 @MMK_L_u 0.5 0 1 0.25 @MMK_L_y 1 0 1 0.25 @MMK_L_z 1 0 0.5 0.25 @MMK_R_A 1 0 0 0.25 @MMK_R_C 1 0.5 0 0.25 @MMK_R_E 1 1 0 0.25 @MMK_R_I 0 1 0 0.25 @MMK_R_N 0 1 1 0.25 @MMK_R_O 0 0.5 1 0.25 @MMK_R_S 0 0 1 0.25 @MMK_R_U 0.5 0 1 0.25 @MMK_R_Y 1 0 1 0.25 @MMK_R_Z 1 0 0.5 0.25 @MMK_R_a 1 0 0 0.25 @MMK_R_c 1 0.5 0 0.25 @MMK_R_e 1 1 0 0.25 @MMK_R_i 0 1 0 0.25 @MMK_R_n 0 1 1 0.25 @MMK_R_o 0 0.5 1 0.25 @MMK_R_s 0 0 1 0.25 @MMK_R_u 0.5 0 1 0.25 @MMK_R_y 1 0 1 0.25 @MMK_R_z 1 0 0.5 0.25 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe mu zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot dollar cent sterling currency yen Euro asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree florin guillemotleft guillemotright fi fl a_b_c .notdef type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon arrowleft arrowup arrowright arrowdown dot dieresis acute space IJ testLibItemKey a b c ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightCondensed.ufo/metainfo.plist000066400000000000000000000004641470175262700306550ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/000077500000000000000000000000001470175262700247605ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/features.fea000066400000000000000000000000451470175262700272520ustar00rootroot00000000000000# this is the feature from lightWide ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/fontinfo.plist000066400000000000000000000034451470175262700276650ustar00rootroot00000000000000 ascender 700 capHeight 700 copyright License same as MutatorMath. BSD 3-clause. [test-token: D] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: D] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-LightWide postscriptFullName MutatorMathTest LightWide postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName LightWide unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.background/000077500000000000000000000000001470175262700304045ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.background/S_.closed.glif000066400000000000000000000034321470175262700330620ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.background/contents.plist000066400000000000000000000003661470175262700333230ustar00rootroot00000000000000 S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.background/layerinfo.plist000066400000000000000000000003601470175262700334500ustar00rootroot00000000000000 color 0.5,1,0,0.7 glyphs.master_width_0.00_weight_0.00/000077500000000000000000000000001470175262700333415ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufocontents.plist000066400000000000000000000002651470175262700362560ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_0.00_weight_0.00 layerinfo.plist000066400000000000000000000003551470175262700364110ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_0.00_weight_0.00 color 0,0.25,1,0.7 glyphs.master_width_500.00_weight_500.00/000077500000000000000000000000001470175262700336535ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufoS_.glif000066400000000000000000000046331470175262700350650ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_500.00_weight_500.00 contents.plist000066400000000000000000000003441470175262700365660ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_500.00_weight_500.00 S S_.glif layerinfo.plist000066400000000000000000000005301470175262700367160ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_500.00_weight_500.00 color 0,1,0.25,0.7 lib com.typemytype.robofont.segmentType curve glyphs.master_width_695.65_weight_166.38/000077500000000000000000000000001470175262700337305ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufocontents.plist000066400000000000000000000002651470175262700366450ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_695.65_weight_166.38 layerinfo.plist000066400000000000000000000003541470175262700367770ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_695.65_weight_166.38 color 0.5,0,1,0.7 glyphs.master_width_720.00_weight_645.00/000077500000000000000000000000001470175262700336715ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufoS_.glif000066400000000000000000000047511470175262700351040ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_720.00_weight_645.00 contents.plist000066400000000000000000000003441470175262700366040ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_720.00_weight_645.00 S S_.glif layerinfo.plist000066400000000000000000000005251470175262700367400ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs.master_width_720.00_weight_645.00 color 0,1,1,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/000077500000000000000000000000001470175262700262665ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/A_.glif000066400000000000000000000023061470175262700274510ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/I_.glif000066400000000000000000000012631470175262700274620ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/I_.narrow.glif000066400000000000000000000012161470175262700307670ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/I_J_.glif000066400000000000000000000020511470175262700277270ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/S_.closed.glif000066400000000000000000000034471470175262700307520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/S_.glif000066400000000000000000000042141470175262700274730ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/contents.plist000066400000000000000000000007311470175262700312010ustar00rootroot00000000000000 A A_.glif I I_.glif I.narrow I_.narrow.glif IJ I_J_.glif S S_.glif S.closed S_.closed.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/glyphs/layerinfo.plist000066400000000000000000000005411470175262700313330ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/layercontents.plist000066400000000000000000000005511470175262700307300ustar00rootroot00000000000000 foreground glyphs background glyphs.background ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/lib.plist000066400000000000000000000373641470175262700266200ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.typemytype.robofont.background.layerStrokeColor 1 0.75 0 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1 0 0 0.25 @MMK_L_C 1 0.5 0 0.25 @MMK_L_E 1 1 0 0.25 @MMK_L_I 0 1 0 0.25 @MMK_L_N 0 1 1 0.25 @MMK_L_O 0 0.5 1 0.25 @MMK_L_S 0 0 1 0.25 @MMK_L_U 0.5 0 1 0.25 @MMK_L_Y 1 0 1 0.25 @MMK_L_Z 1 0 0.5 0.25 @MMK_L_a 1 0 0 0.25 @MMK_L_c 1 0.5 0 0.25 @MMK_L_e 1 1 0 0.25 @MMK_L_i 0 1 0 0.25 @MMK_L_n 0 1 1 0.25 @MMK_L_o 0 0.5 1 0.25 @MMK_L_s 0 0 1 0.25 @MMK_L_u 0.5 0 1 0.25 @MMK_L_y 1 0 1 0.25 @MMK_L_z 1 0 0.5 0.25 @MMK_R_A 1 0 0 0.25 @MMK_R_C 1 0.5 0 0.25 @MMK_R_E 1 1 0 0.25 @MMK_R_I 0 1 0 0.25 @MMK_R_N 0 1 1 0.25 @MMK_R_O 0 0.5 1 0.25 @MMK_R_S 0 0 1 0.25 @MMK_R_U 0.5 0 1 0.25 @MMK_R_Y 1 0 1 0.25 @MMK_R_Z 1 0 0.5 0.25 @MMK_R_a 1 0 0 0.25 @MMK_R_c 1 0.5 0 0.25 @MMK_R_e 1 1 0 0.25 @MMK_R_i 0 1 0 0.25 @MMK_R_n 0 1 1 0.25 @MMK_R_o 0 0.5 1 0.25 @MMK_R_s 0 0 1 0.25 @MMK_R_u 0.5 0 1 0.25 @MMK_R_y 1 0 1 0.25 @MMK_R_z 1 0 0.5 0.25 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright guillemotleft guillemotright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot mu dollar cent sterling currency yen Euro florin asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree fi fl .notdef a_b_c type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon arrowleft arrowup arrowright arrowdown dot dieresis acute space IJ ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSansLightWide.ufo/metainfo.plist000066400000000000000000000004641470175262700276430ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSans_v5_implicit_one_vf.designspace000066400000000000000000000121151470175262700304360ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSans_v5_several_vfs.designspace000066400000000000000000000131551470175262700276140ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSans_v5_several_vfs_discrete_axis.designspace000066400000000000000000000120741470175262700325210ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/000077500000000000000000000000001470175262700261365ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/features.fea000066400000000000000000000001041470175262700304240ustar00rootroot00000000000000# this is the feature from lightCondensed # Hi_this_is_the_feature. ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/fontinfo.plist000066400000000000000000000034711470175262700310420ustar00rootroot00000000000000 ascender 700 capHeight 700 copyright License same as MutatorMath. BSD 3-clause. [test-token: C] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: C] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-LightCondensed postscriptFullName MutatorMathTest LightCondensed postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName SerifLightCondensed unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/glyphs/000077500000000000000000000000001470175262700274445ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/glyphs/A_.glif000066400000000000000000000023141470175262700306260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/glyphs/contents.plist000066400000000000000000000003501470175262700323540ustar00rootroot00000000000000 A A_.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/layercontents.plist000066400000000000000000000004171470175262700321070ustar00rootroot00000000000000 foreground glyphs ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/lib.plist000066400000000000000000000402541470175262700277660ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.letterror.lightMeter.prefs chunkSize 5 diameter 200 drawTail invert toolDiameter 30 toolStyle fluid com.typemytype.robofont.background.layerStrokeColor 0 0.8 0.2 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1 0 0 0.25 @MMK_L_C 1 0.5 0 0.25 @MMK_L_E 1 1 0 0.25 @MMK_L_I 0 1 0 0.25 @MMK_L_N 0 1 1 0.25 @MMK_L_O 0 0.5 1 0.25 @MMK_L_S 0 0 1 0.25 @MMK_L_U 0.5 0 1 0.25 @MMK_L_Y 1 0 1 0.25 @MMK_L_Z 1 0 0.5 0.25 @MMK_L_a 1 0 0 0.25 @MMK_L_c 1 0.5 0 0.25 @MMK_L_e 1 1 0 0.25 @MMK_L_i 0 1 0 0.25 @MMK_L_n 0 1 1 0.25 @MMK_L_o 0 0.5 1 0.25 @MMK_L_s 0 0 1 0.25 @MMK_L_u 0.5 0 1 0.25 @MMK_L_y 1 0 1 0.25 @MMK_L_z 1 0 0.5 0.25 @MMK_R_A 1 0 0 0.25 @MMK_R_C 1 0.5 0 0.25 @MMK_R_E 1 1 0 0.25 @MMK_R_I 0 1 0 0.25 @MMK_R_N 0 1 1 0.25 @MMK_R_O 0 0.5 1 0.25 @MMK_R_S 0 0 1 0.25 @MMK_R_U 0.5 0 1 0.25 @MMK_R_Y 1 0 1 0.25 @MMK_R_Z 1 0 0.5 0.25 @MMK_R_a 1 0 0 0.25 @MMK_R_c 1 0.5 0 0.25 @MMK_R_e 1 1 0 0.25 @MMK_R_i 0 1 0 0.25 @MMK_R_n 0 1 1 0.25 @MMK_R_o 0 0.5 1 0.25 @MMK_R_s 0 0 1 0.25 @MMK_R_u 0.5 0 1 0.25 @MMK_R_y 1 0 1 0.25 @MMK_R_z 1 0 0.5 0.25 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe mu zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot dollar cent sterling currency yen Euro asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree florin guillemotleft guillemotright fi fl a_b_c .notdef type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon arrowleft arrowup arrowright arrowdown dot dieresis acute space IJ testLibItemKey a b c ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightCondensed.ufo/metainfo.plist000066400000000000000000000004641470175262700310210ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/000077500000000000000000000000001470175262700251245ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/features.fea000066400000000000000000000000451470175262700274160ustar00rootroot00000000000000# this is the feature from lightWide ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/fontinfo.plist000066400000000000000000000034521470175262700300270ustar00rootroot00000000000000 ascender 700 capHeight 700 copyright License same as MutatorMath. BSD 3-clause. [test-token: D] descender -200 familyName MutatorMathTest guidelines italicAngle 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: D] openTypeOS2VendorID LTTR postscriptBlueValues postscriptDefaultWidthX 500 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptFontName MutatorMathTest-LightWide postscriptFullName MutatorMathTest LightWide postscriptOtherBlues postscriptSlantAngle 0 postscriptStemSnapH postscriptStemSnapV postscriptWindowsCharacterSet 1 styleMapFamilyName styleMapStyleName regular styleName SerifLightWide unitsPerEm 1000 versionMajor 1 versionMinor 2 xHeight 500 year 2004 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/glyphs/000077500000000000000000000000001470175262700264325ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/glyphs/A_.glif000066400000000000000000000030521470175262700276140ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/glyphs/contents.plist000066400000000000000000000003501470175262700313420ustar00rootroot00000000000000 A A_.glif ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/layercontents.plist000066400000000000000000000004171470175262700310750ustar00rootroot00000000000000 foreground glyphs ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/lib.plist000066400000000000000000000373641470175262700267640ustar00rootroot00000000000000 com.defcon.sortDescriptor allowPseudoUnicode ascending type alphabetical allowPseudoUnicode ascending type category allowPseudoUnicode ascending type unicode allowPseudoUnicode ascending type script allowPseudoUnicode ascending type suffix allowPseudoUnicode ascending type decompositionBase com.typemytype.robofont.background.layerStrokeColor 1 0.75 0 0.7 com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5 0 0.5 0.7 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1 0 0 0.25 @MMK_L_C 1 0.5 0 0.25 @MMK_L_E 1 1 0 0.25 @MMK_L_I 0 1 0 0.25 @MMK_L_N 0 1 1 0.25 @MMK_L_O 0 0.5 1 0.25 @MMK_L_S 0 0 1 0.25 @MMK_L_U 0.5 0 1 0.25 @MMK_L_Y 1 0 1 0.25 @MMK_L_Z 1 0 0.5 0.25 @MMK_L_a 1 0 0 0.25 @MMK_L_c 1 0.5 0 0.25 @MMK_L_e 1 1 0 0.25 @MMK_L_i 0 1 0 0.25 @MMK_L_n 0 1 1 0.25 @MMK_L_o 0 0.5 1 0.25 @MMK_L_s 0 0 1 0.25 @MMK_L_u 0.5 0 1 0.25 @MMK_L_y 1 0 1 0.25 @MMK_L_z 1 0 0.5 0.25 @MMK_R_A 1 0 0 0.25 @MMK_R_C 1 0.5 0 0.25 @MMK_R_E 1 1 0 0.25 @MMK_R_I 0 1 0 0.25 @MMK_R_N 0 1 1 0.25 @MMK_R_O 0 0.5 1 0.25 @MMK_R_S 0 0 1 0.25 @MMK_R_U 0.5 0 1 0.25 @MMK_R_Y 1 0 1 0.25 @MMK_R_Z 1 0 0.5 0.25 @MMK_R_a 1 0 0 0.25 @MMK_R_c 1 0.5 0 0.25 @MMK_R_e 1 1 0 0.25 @MMK_R_i 0 1 0 0.25 @MMK_R_n 0 1 1 0.25 @MMK_R_o 0 0.5 1 0.25 @MMK_R_s 0 0 1 0.25 @MMK_R_u 0.5 0 1 0.25 @MMK_R_y 1 0 1 0.25 @MMK_R_z 1 0 0.5 0.25 com.typesupply.defcon.sortDescriptor ascending space A Agrave Aacute Acircumflex Atilde Adieresis Aring B C Ccedilla D E Egrave Eacute Ecircumflex Edieresis F G H I Igrave Iacute Icircumflex Idieresis J K L M N Ntilde O Ograve Oacute Ocircumflex Otilde Odieresis P Q R S Scaron T U Ugrave Uacute Ucircumflex Udieresis V W X Y Yacute Ydieresis Z Zcaron AE Eth Oslash Thorn Lslash OE a agrave aacute acircumflex atilde adieresis aring b c ccedilla d e egrave eacute ecircumflex edieresis f g h i igrave iacute icircumflex idieresis j k l m n ntilde o ograve oacute ocircumflex otilde odieresis p q r s scaron t u ugrave uacute ucircumflex udieresis v w x y yacute ydieresis z zcaron ordfeminine ordmasculine germandbls ae eth oslash thorn dotlessi lslash oe zero one two three four five six seven eight nine onesuperior twosuperior threesuperior onequarter onehalf threequarters underscore hyphen endash emdash parenleft parenright bracketleft bracketright braceleft braceright numbersign percent perthousand quotesingle quotedbl quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase guilsinglleft guilsinglright guillemotleft guillemotright asterisk dagger daggerdbl period comma colon semicolon ellipsis exclam exclamdown question questiondown slash backslash fraction bar brokenbar at ampersand section paragraph periodcentered bullet plus minus plusminus divide multiply equal less greater logicalnot mu dollar cent sterling currency yen Euro florin asciicircum asciitilde acute grave hungarumlaut circumflex caron breve tilde macron dieresis dotaccent ring cedilla ogonek copyright registered trademark degree fi fl .notdef a_b_c type glyphList public.glyphOrder A Aacute Adieresis B C D E F G H I J K L M N O P Q R S T U V W X Y Z S.closed I.narrow J.narrow quotesinglbase quotedblbase quotedblleft quotedblright comma period colon semicolon arrowleft arrowup arrowright arrowdown dot dieresis acute space IJ ufo2ft-3.3.1/tests/data/MutatorSansLite/MutatorSerifLightWide.ufo/metainfo.plist000066400000000000000000000004641470175262700300070ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/000077500000000000000000000000001470175262700217475ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/fontinfo.plist000066400000000000000000000121011470175262700246410ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 italicAngle -12.5 macintoshFONDFamilyID 15000 macintoshFONDName SomeFont Regular (FOND Name) note A note. openTypeHeadCreated 2000/01/01 00:00:00 openTypeHeadFlags 0 1 openTypeHeadLowestRecPPEM 10 openTypeHheaCaretOffset 0 openTypeHheaCaretSlopeRise 1 openTypeHheaCaretSlopeRun 0 openTypeNameCompatibleFullName Some Font Regular (Compatible Full Name) openTypeNameDescription Some Font by Some Designer for Some Foundry. openTypeNameDesigner Some Designer openTypeNameDesignerURL http://somedesigner.com openTypeNameLicense License info for Some Foundry. openTypeNameLicenseURL http://somefoundry.com/license openTypeNameManufacturer Some Foundry openTypeNameManufacturerURL http://somefoundry.com openTypeNamePreferredFamilyName Some Font (Preferred Family Name) openTypeNameSampleText Sample Text for Some Font. openTypeNameUniqueID OpenType name Table Unique ID openTypeNameVersion OpenType name Table Version openTypeNameWWSFamilyName Some Font (WWS Family Name) openTypeNameWWSSubfamilyName Regular (WWS Subfamily Name) openTypeOS2CodePageRanges 0 openTypeOS2FamilyClass 1 1 openTypeOS2Panose 0 1 2 3 4 5 6 7 8 9 openTypeOS2Selection 3 openTypeOS2SubscriptXOffset 0 openTypeOS2SubscriptXSize 200 openTypeOS2SubscriptYOffset -100 openTypeOS2SubscriptYSize 400 openTypeOS2SuperscriptXOffset 0 openTypeOS2SuperscriptXSize 200 openTypeOS2SuperscriptYOffset 200 openTypeOS2SuperscriptYSize 400 openTypeOS2Type openTypeOS2UnicodeRanges 0 1 openTypeOS2VendorID SOME openTypeVheaCaretOffset 0 openTypeVheaCaretSlopeRise 0 openTypeVheaCaretSlopeRun 1 openTypeVheaVertTypoAscender 750 openTypeVheaVertTypoDescender -250 openTypeVheaVertTypoLineGap 200 postscriptBlueFuzz 1 postscriptBlueScale 0.04 postscriptBlueShift 7 postscriptBlueValues 500.0 510.0 postscriptDefaultCharacter .notdef postscriptDefaultWidthX 400 postscriptFamilyBlues 500.0 510.0 postscriptFamilyOtherBlues -260.0 -250.0 postscriptForceBold postscriptIsFixedPitch postscriptNominalWidthX 400.0 postscriptOtherBlues postscriptSlantAngle -12.5 postscriptStemSnapH 0.0 0.0 postscriptStemSnapV 0.0 0.0 postscriptUniqueID 4000000 postscriptWindowsCharacterSet 1 styleName Regular trademark Trademark Some Foundry unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 year 2008 ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/000077500000000000000000000000001470175262700232555ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/_notdef.glif000066400000000000000000000007531470175262700255430ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/a.glif000066400000000000000000000005111470175262700243350ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/b.glif000066400000000000000000000010471470175262700243430ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/c.glif000066400000000000000000000007051470175262700243440ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment -1 index 0 name a ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/contents.plist000066400000000000000000000007411470175262700261710ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif d d.glif e e.glif space space.glif ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/d.glif000066400000000000000000000012571470175262700243500ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment -1 index 0 name b alignment -1 index 1 name a ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/e.glif000066400000000000000000000012241470175262700243430ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment -1 index 0 name c alignment -1 index 1 name d ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/glyphs/space.glif000066400000000000000000000002261470175262700252130ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/layercontents.plist000066400000000000000000000004151470175262700257160ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/lib.plist000066400000000000000000000063721470175262700236020ustar00rootroot00000000000000 com.schriftgestaltung.disablesAutomaticAlignment com.schriftgestaltung.font.customParameters name openTypeGaspRangeRecords value rangeGaspBehavior 1 3 rangeMaxPPEM 7 rangeGaspBehavior 0 1 2 3 rangeMaxPPEM 65535 name openTypeNameRecords value encodingID 0 languageID 0 nameID 3 platformID 1 string Unique Font Identifier encodingID 1 languageID 1033 nameID 8 platformID 3 string Some Foundry (Manufacturer Name) com.schriftgestaltung.font.userData GSDimensionPlugin.Dimensions master01 com.schriftgestaltung.fontMasterID 0A7BF222-74C5-44F5-877F-4AAEDEB31DD5 com.schriftgestaltung.glyphOrder .notdef glyph1 glyph2 space a b c d e f g h i j k l com.schriftgestaltung.master.name Bold com.schriftgestaltung.useNiceNames com.schriftgestaltung.weightValue 800 public.glyphOrder .notdef space a b c d e public.postscriptNames a uni0061 b uni0062 c uni0063 d uni0064 e uni0065 f uni0066 g uni0067 h uni0068 i uni0069 j uni006A k uni006B l uni006C space uni0020 ufo2ft-3.3.1/tests/data/NestedComponents-Bold.ufo/metainfo.plist000066400000000000000000000004701470175262700246270ustar00rootroot00000000000000 creator com.schriftgestaltung.GlyphsUFOExport formatVersion 3 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/000077500000000000000000000000001470175262700224705ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/fontinfo.plist000066400000000000000000000155421470175262700253760ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 guidelines angle 0 x 250 y 0 angle 0 x -20 y 0 angle 0 x 30 y 0 y 500 y -200 y 700 angle 135 x 0 y 0 angle 45 x 0 y 700 angle 135 x 20 y 0 italicAngle -12.5 macintoshFONDFamilyID 15000 macintoshFONDName SomeFont Regular (FOND Name) note A note. openTypeHeadCreated 2000/01/01 00:00:00 openTypeHeadFlags 0 1 openTypeHeadLowestRecPPEM 10 openTypeHheaAscender 750 openTypeHheaCaretOffset 0 openTypeHheaCaretSlopeRise 1 openTypeHheaCaretSlopeRun 0 openTypeHheaDescender -250 openTypeHheaLineGap 200 openTypeNameCompatibleFullName Some Font Regular (Compatible Full Name) openTypeNameDescription Some Font by Some Designer for Some Foundry. openTypeNameDesigner Some Designer openTypeNameDesignerURL http://somedesigner.com openTypeNameLicense License info for Some Foundry. openTypeNameLicenseURL http://somefoundry.com/license openTypeNameManufacturer Some Foundry openTypeNameManufacturerURL http://somefoundry.com openTypeNamePreferredFamilyName Some Font (Preferred Family Name) openTypeNameSampleText Sample Text for Some Font. openTypeNameUniqueID OpenType name Table Unique ID openTypeNameVersion OpenType name Table Version openTypeNameWWSFamilyName Some Font (WWS Family Name) openTypeNameWWSSubfamilyName Regular (WWS Subfamily Name) openTypeOS2CodePageRanges 0 openTypeOS2FamilyClass 1 1 openTypeOS2Panose 0 1 2 3 4 5 6 7 8 9 openTypeOS2Selection 3 openTypeOS2StrikeoutPosition 300 openTypeOS2StrikeoutSize 20 openTypeOS2SubscriptXOffset 0 openTypeOS2SubscriptXSize 200 openTypeOS2SubscriptYOffset -100 openTypeOS2SubscriptYSize 400 openTypeOS2SuperscriptXOffset 0 openTypeOS2SuperscriptXSize 200 openTypeOS2SuperscriptYOffset 200 openTypeOS2SuperscriptYSize 400 openTypeOS2Type openTypeOS2TypoAscender 750 openTypeOS2TypoDescender -250 openTypeOS2TypoLineGap 200 openTypeOS2UnicodeRanges 0 1 openTypeOS2VendorID SOME openTypeOS2WinAscent 750 openTypeOS2WinDescent 250 openTypeVheaCaretOffset 0 openTypeVheaCaretSlopeRise 0 openTypeVheaCaretSlopeRun 1 openTypeVheaVertTypoAscender 750 openTypeVheaVertTypoDescender -250 openTypeVheaVertTypoLineGap 200 postscriptBlueFuzz 1 postscriptBlueScale 0.04 postscriptBlueShift 7 postscriptBlueValues 500.0 510.0 postscriptDefaultCharacter .notdef postscriptDefaultWidthX 400 postscriptFamilyBlues 500.0 510.0 postscriptFamilyOtherBlues -260.0 -250.0 postscriptForceBold postscriptIsFixedPitch postscriptNominalWidthX 400.0 postscriptOtherBlues postscriptSlantAngle -12.5 postscriptStemSnapH 100.0 120.0 postscriptStemSnapV 80.0 90.0 postscriptUnderlinePosition -200 postscriptUnderlineThickness 20 postscriptUniqueID 4000000 postscriptWindowsCharacterSet 1 styleName Regular trademark Trademark Some Foundry unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 year 2008 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs.M_edium/000077500000000000000000000000001470175262700253545ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs.M_edium/a.glif000066400000000000000000000005261470175262700264420ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs.M_edium/contents.plist000066400000000000000000000004121470175262700302630ustar00rootroot00000000000000 a a.glif e e.glif ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs.M_edium/e.glif000066400000000000000000000003201470175262700264360ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/000077500000000000000000000000001470175262700237765ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/_notdef.glif000066400000000000000000000007711470175262700262640ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/a.glif000066400000000000000000000007341470175262700250650ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2020-12-07 11:48:13 +0000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/b.glif000066400000000000000000000012741470175262700250660ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2020-12-07 11:48:16 +0000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/c.glif000066400000000000000000000010511470175262700250600ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment -1 index 0 name a com.schriftgestaltung.Glyphs.lastChange 2020-12-07 11:48:18 +0000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/contents.plist000066400000000000000000000007411470175262700267120ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif d d.glif e e.glif space space.glif ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/d.glif000066400000000000000000000014051470175262700250640ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment -1 index 0 name b alignment -1 index 1 name a com.schriftgestaltung.Glyphs.lastChange 2020-12-07 11:48:21 +0000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/e.glif000066400000000000000000000013701470175262700250660ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.ComponentInfo alignment -1 index 0 name c alignment -1 index 1 name d com.schriftgestaltung.Glyphs.lastChange 2020-12-07 11:48:37 +0000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/glyphs/space.glif000066400000000000000000000002431470175262700257330ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/layercontents.plist000066400000000000000000000005341470175262700264410ustar00rootroot00000000000000 public.default glyphs Medium glyphs.M_edium ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/lib.plist000066400000000000000000000067171470175262700243260ustar00rootroot00000000000000 com.schriftgestaltung.disablesAutomaticAlignment com.schriftgestaltung.font.customParameters name openTypeGaspRangeRecords value rangeGaspBehavior 1 3 rangeMaxPPEM 7 rangeGaspBehavior 0 1 2 3 rangeMaxPPEM 65535 name openTypeNameRecords value encodingID 0 languageID 0 nameID 3 platformID 1 string Unique Font Identifier encodingID 1 languageID 1033 nameID 8 platformID 3 string Some Foundry (Manufacturer Name) com.schriftgestaltung.font.userData GSDimensionPlugin.Dimensions master01 com.schriftgestaltung.fontMaster.customParameters name Alignment Zones value pos -260.0 size 10.0 com.schriftgestaltung.fontMasterID master01 com.schriftgestaltung.glyphOrder .notdef glyph1 glyph2 space a b c d e f g h i j k l com.schriftgestaltung.useNiceNames com.schriftgestaltung.weightValue 400 public.glyphOrder .notdef space a b c d e public.postscriptNames a uni0061 b uni0062 c uni0063 d uni0064 e uni0065 f uni0066 g uni0067 h uni0068 i uni0069 j uni006A k uni006B l uni006C space uni0020 ufo2ft-3.3.1/tests/data/NestedComponents-Regular.ufo/metainfo.plist000066400000000000000000000004701470175262700253500ustar00rootroot00000000000000 creator com.schriftgestaltung.GlyphsUFOExport formatVersion 3 ufo2ft-3.3.1/tests/data/NestedComponents.designspace000066400000000000000000000013711470175262700225120ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/OTestFont-Bold.ufo/000077500000000000000000000000001470175262700203445ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/OTestFont-Bold.ufo/fontinfo.plist000066400000000000000000000015161470175262700232460ustar00rootroot00000000000000 ascender 800 capHeight 700 descender -200 familyName O Test Font italicAngle 0 styleMapFamilyName O Test Font styleMapStyleName bold styleName Bold unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 ufo2ft-3.3.1/tests/data/OTestFont-Bold.ufo/glyphs/000077500000000000000000000000001470175262700216525ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/OTestFont-Bold.ufo/glyphs/contents.plist000066400000000000000000000004421470175262700245640ustar00rootroot00000000000000 o o.glif space space.glif ufo2ft-3.3.1/tests/data/OTestFont-Bold.ufo/glyphs/o.glif000066400000000000000000000022051470175262700227520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/OTestFont-Bold.ufo/glyphs/space.glif000066400000000000000000000002321470175262700236050ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/OTestFont-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700243170ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/OTestFont-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700232320ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/OTestFont-Regular.ufo/000077500000000000000000000000001470175262700210655ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/OTestFont-Regular.ufo/fontinfo.plist000066400000000000000000000015241470175262700237660ustar00rootroot00000000000000 ascender 800 capHeight 700 descender -200 familyName O Test Font italicAngle 0 styleMapFamilyName O Test Font styleMapStyleName regular styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 ufo2ft-3.3.1/tests/data/OTestFont-Regular.ufo/glyphs/000077500000000000000000000000001470175262700223735ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/OTestFont-Regular.ufo/glyphs/contents.plist000066400000000000000000000004421470175262700253050ustar00rootroot00000000000000 o o.glif space space.glif ufo2ft-3.3.1/tests/data/OTestFont-Regular.ufo/glyphs/o.glif000066400000000000000000000022021470175262700234700ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/OTestFont-Regular.ufo/glyphs/space.glif000066400000000000000000000002321470175262700243260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/OTestFont-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700250400ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/OTestFont-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700237530ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/OTestFont.designspace000066400000000000000000000010321470175262700211010ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/000077500000000000000000000000001470175262700226165ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/fontinfo.plist000066400000000000000000000010731470175262700255160ustar00rootroot00000000000000 ascender 760 capHeight 714 descender -240 familyName SkipExportGlyphsTest styleName Bold unitsPerEm 1000 xHeight 553 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/glyphs/000077500000000000000000000000001470175262700241245ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/glyphs/A_.glif000066400000000000000000000022601470175262700253060ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/glyphs/A_stroke.glif000066400000000000000000000003431470175262700265360ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/glyphs/_stroke.glif000066400000000000000000000005151470175262700264360ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/glyphs/contents.plist000066400000000000000000000005411470175262700270360ustar00rootroot00000000000000 A A_.glif Astroke A_stroke.glif _stroke _stroke.glif ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700265710ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/lib.plist000066400000000000000000000007061470175262700244440ustar00rootroot00000000000000 public.glyphOrder A Astroke _stroke public.postscriptNames Astroke uni023A ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700255040ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/000077500000000000000000000000001470175262700233375ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/fontinfo.plist000066400000000000000000000010761470175262700262420ustar00rootroot00000000000000 ascender 760 capHeight 714 descender -240 familyName SkipExportGlyphsTest styleName Regular unitsPerEm 1000 xHeight 536 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs.{151}/000077500000000000000000000000001470175262700255425ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs.{151}/_stroke.glif000066400000000000000000000005161470175262700300550ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs.{151}/contents.plist000066400000000000000000000003711470175262700304550ustar00rootroot00000000000000 _stroke _stroke.glif ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs.{170}/000077500000000000000000000000001470175262700255435ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs.{170}/A_stroke.glif000066400000000000000000000003441470175262700301560ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs.{170}/contents.plist000066400000000000000000000003721470175262700304570ustar00rootroot00000000000000 Astroke A_stroke.glif ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs/000077500000000000000000000000001470175262700246455ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs/A_.glif000066400000000000000000000022571470175262700260350ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs/A_stroke.glif000066400000000000000000000003261470175262700272600ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs/_stroke.glif000066400000000000000000000005151470175262700271570ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/glyphs/contents.plist000066400000000000000000000005411470175262700275570ustar00rootroot00000000000000 A A_.glif Astroke A_stroke.glif _stroke _stroke.glif ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/layercontents.plist000066400000000000000000000007231470175262700273100ustar00rootroot00000000000000 public.default glyphs {170} glyphs.{170} {151} glyphs.{151} ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/lib.plist000066400000000000000000000007061470175262700251650ustar00rootroot00000000000000 public.glyphOrder A Astroke _stroke public.postscriptNames Astroke uni023A ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700262250ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/SkipExportGlyphsTest.designspace000066400000000000000000000024721470175262700233640ustar00rootroot00000000000000 public.skipExportGlyphs _stroke ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/000077500000000000000000000000001470175262700232525ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/fontinfo.plist000066400000000000000000000026461470175262700261610ustar00rootroot00000000000000 ascender 800 capHeight 700 descender -200 familyName Spacing Combining Test italicAngle 0 openTypeHeadCreated 2023/03/03 08:50:23 openTypeOS2Type 3 postscriptBlueValues -16 0 500 516 700 716 800 816 postscriptOtherBlues -216 -200 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/glyphs/000077500000000000000000000000001470175262700245605ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/glyphs/contents.plist000066400000000000000000000006701470175262700274750ustar00rootroot00000000000000 highspacingdot-deva highspacingdot-deva.glif ka-deva ka-deva.glif ra-deva ra-deva.glif space space.glif ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/glyphs/highspacingdot-deva.glif000066400000000000000000000017361470175262700313420ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.category Mark com.schriftgestaltung.Glyphs.lastChange 2023/03/03 09:07:06 com.schriftgestaltung.Glyphs.subCategory Spacing Combining ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/glyphs/ka-deva.glif000066400000000000000000000007011470175262700267310ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2023/03/03 09:04:18 ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/glyphs/layerinfo.plist000066400000000000000000000012561470175262700276310ustar00rootroot00000000000000 lib com.schriftgestaltung.layerId m01 com.schriftgestaltung.layerOrderInGlyph.highspacingdot-deva 0 com.schriftgestaltung.layerOrderInGlyph.ka-deva 0 com.schriftgestaltung.layerOrderInGlyph.ra-deva 0 com.schriftgestaltung.layerOrderInGlyph.space 0 ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/glyphs/ra-deva.glif000066400000000000000000000007031470175262700267420ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2023/03/03 09:04:26 ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/glyphs/space.glif000066400000000000000000000002321470175262700265130ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/kerning.plist000066400000000000000000000010761470175262700257700ustar00rootroot00000000000000 highspacingdot-deva ka-deva -200 ka-deva highspacingdot-deva -150 ra-deva -250 ra-deva ka-deva -250 ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700272250ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/lib.plist000066400000000000000000000052021470175262700250740ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.filters name eraseOpenCorners namespace glyphsLib.filters pre com.schriftgestaltung.appVersion 3177 com.schriftgestaltung.customParameter.GSFont.DisplayStrings कॱर 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 ka-deva ra-deva space highspacingdot-deva public.openTypeCategories highspacingdot-deva mark public.postscriptNames highspacingdot-deva uni0971 ka-deva uni0915 ra-deva uni0930 ufo2ft-3.3.1/tests/data/SpacingCombiningTest-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700261400ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/SwapGlyphNames/000077500000000000000000000000001470175262700177135ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/000077500000000000000000000000001470175262700206635ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/fontinfo.plist000066400000000000000000000014261470175262700235650ustar00rootroot00000000000000 ascender 750 capHeight 700 descender -250 guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/000077500000000000000000000000001470175262700221715ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/a.glif000066400000000000000000000006641470175262700232620ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/a.swap.glif000066400000000000000000000011051470175262700242220ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/aaa.glif000066400000000000000000000005761470175262700235660ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/aaa.swap.glif000066400000000000000000000006111470175262700245250ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/contents.plist000066400000000000000000000010371470175262700251040ustar00rootroot00000000000000 a a.glif a.swap a.swap.glif aaa aaa.glif aaa.swap aaa.swap.glif space space.glif x x.glif y y.glif ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/space.glif000066400000000000000000000002341470175262700241260ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/x.glif000066400000000000000000000012061470175262700233020ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/glyphs/y.glif000066400000000000000000000010121470175262700232760ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/groups.plist000066400000000000000000000007271470175262700232650ustar00rootroot00000000000000 public.kern1.a a public.kern1.aswap a.swap public.kern2.a a a.swap ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/kerning.plist000066400000000000000000000013121470175262700233720ustar00rootroot00000000000000 a y 30 a.swap y 40 public.kern1.a x 10 public.kern1.aswap x 20 y a 50 a.swap 60 ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/layercontents.plist000066400000000000000000000004371470175262700246360ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/lib.plist000066400000000000000000000006731470175262700225140ustar00rootroot00000000000000 public.glyphOrder space a a.swap aaa aaa.swap x y ufo2ft-3.3.1/tests/data/SwapGlyphNames/A.ufo/metainfo.plist000066400000000000000000000004761470175262700235510ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/000077500000000000000000000000001470175262700206645ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/fontinfo.plist000066400000000000000000000014261470175262700235660ustar00rootroot00000000000000 ascender 750 capHeight 700 descender -250 guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV unitsPerEm 1000 xHeight 500 ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/glyphs/000077500000000000000000000000001470175262700221725ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/glyphs/a.alt.glif000066400000000000000000000014201470175262700240310ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/glyphs/a.glif000066400000000000000000000016021470175262700232540ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/glyphs/adieresis.alt.glif000066400000000000000000000003151470175262700255630ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/glyphs/adieresis.glif000066400000000000000000000003351470175262700250060ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/glyphs/contents.plist000066400000000000000000000007471470175262700251140ustar00rootroot00000000000000 a a.glif a.alt a.alt.glif adieresis adieresis.glif adieresis.alt adieresis.alt.glif dieresiscomb dieresiscomb.glif ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/glyphs/dieresiscomb.glif000066400000000000000000000010331470175262700255020ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/layercontents.plist000066400000000000000000000004371470175262700246370ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/lib.plist000066400000000000000000000006321470175262700225100ustar00rootroot00000000000000 public.glyphOrder a a.alt adieresis dieresiscomb adieresis.alt ufo2ft-3.3.1/tests/data/SwapGlyphNames/B.ufo/metainfo.plist000066400000000000000000000004761470175262700235520ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/TestFont-CFF-compreffor.ttx000066400000000000000000000433071470175262700220630ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 256 hlineto -128 510 rlineto endchar 200 -55 -80 rmoveto 509 hlineto 149 -50 50 -205 -204 -50 -50 -149 vhcurveto 121 return rmoveto 509 hlineto 150 -50 50 -205 -204 -50 -50 -150 vhcurveto endchar rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto return 100 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto endchar -150 endchar -12 66 hmoveto -107 callsubr 10 100 505 rmoveto -510 210 510 vlineto endchar -26 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto endchar -26 151 197 -104 callsubr endchar -12 66 510 rmoveto 128 -435 128 435 rlineto -377 -487 -105 callsubr 10 66 510 rmoveto 256 hlineto -128 -435 rlineto -249 -52 -105 callsubr -12 66 hmoveto -107 callsubr 10 211 657 -104 callsubr -111 -152 rmoveto -510 210 510 vlineto endchar -106 callsubr 80 rmoveto -107 callsubr -106 callsubr 310 rmoveto 128 -510 128 510 rlineto endchar 200 66 hmoveto 256 hlineto -128 510 rlineto -28 -510 rmoveto -107 callsubr 200 334 hmoveto -128 510 -128 -510 rlineto 88 hmoveto -107 callsubr 0001beef ufo2ft-3.3.1/tests/data/TestFont-CFF.ttx000066400000000000000000000440111470175262700177140ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) -55 -80 rmoveto 509 hlineto 149 -98 callsubr -149 vhcurveto 121 return rmoveto 509 hlineto 150 -98 callsubr -150 vhcurveto endchar rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto return -102 callsubr endchar rmoveto -99 callsubr endchar 66 -100 callsubr return rmoveto -510 210 510 vlineto endchar hmoveto -99 callsubr return 256 hlineto -128 510 rlineto return -50 50 -205 -204 -50 -50 return 143 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto endchar -107 endchar 31 -104 callsubr 53 100 505 -101 callsubr 17 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto endchar 17 151 197 -105 callsubr endchar 31 66 510 rmoveto 128 -435 128 435 rlineto -377 -487 -106 callsubr 53 66 510 rmoveto 256 hlineto -128 -435 rlineto -249 -52 -106 callsubr 31 -104 callsubr 53 211 657 -105 callsubr -111 -152 -101 callsubr -107 callsubr 80 -103 callsubr -107 callsubr 310 rmoveto 128 -510 128 510 rlineto endchar -102 callsubr -28 -510 -103 callsubr 334 hmoveto -128 510 -128 -510 rlineto 88 -100 callsubr endchar 0001beef ufo2ft-3.3.1/tests/data/TestFont-CFF2-cffsubr.ttx000066400000000000000000000437751470175262700214460ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 66 -102 callsubr -55 -80 rmoveto 509 hlineto 149 -99 callsubr -149 vhcurveto 121 rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto rmoveto 509 hlineto 150 -99 callsubr -150 vhcurveto rmoveto -100 callsubr hmoveto -100 callsubr rmoveto -510 210 510 vlineto 256 hlineto -128 510 rlineto -50 50 -205 -204 -50 -50 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto -107 callsubr 100 505 -101 callsubr 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto 151 197 -105 callsubr 66 510 rmoveto 128 -435 128 435 rlineto -377 -487 -104 callsubr 66 510 rmoveto 256 hlineto -128 -435 rlineto -249 -52 -104 callsubr -107 callsubr 211 657 -105 callsubr -111 -152 -101 callsubr -106 callsubr 80 -103 callsubr -106 callsubr 310 rmoveto 128 -510 128 510 rlineto -107 callsubr -28 -510 -103 callsubr 334 hmoveto -128 510 -128 -510 rlineto 88 -102 callsubr 0001beef ufo2ft-3.3.1/tests/data/TestFont-CFF2-compreffor.ttx000066400000000000000000000427641470175262700221530ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 256 hlineto -128 510 rlineto 200 -55 -80 rmoveto 509 hlineto 149 -50 50 -205 -204 -50 -50 -149 vhcurveto 121 rmoveto 509 hlineto 150 -50 50 -205 -204 -50 -50 -150 vhcurveto rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto 100 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto -150 -12 66 hmoveto -107 callsubr 10 100 505 rmoveto -510 210 510 vlineto -26 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto -26 151 197 -104 callsubr -12 66 510 rmoveto 128 -435 128 435 rlineto -377 -487 -105 callsubr 10 66 510 rmoveto 256 hlineto -128 -435 rlineto -249 -52 -105 callsubr -12 66 hmoveto -107 callsubr 10 211 657 -104 callsubr -111 -152 rmoveto -510 210 510 vlineto -106 callsubr 80 rmoveto -107 callsubr -106 callsubr 310 rmoveto 128 -510 128 510 rlineto 200 66 hmoveto 256 hlineto -128 510 rlineto -28 -510 rmoveto -107 callsubr 200 334 hmoveto -128 510 -128 -510 rlineto 88 hmoveto -107 callsubr 0001beef ufo2ft-3.3.1/tests/data/TestFont-CFF2-post3.ttx000066400000000000000000000411251470175262700210470ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 66 -102 callsubr -55 -80 rmoveto 509 hlineto 149 -99 callsubr -149 vhcurveto 121 rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto rmoveto 509 hlineto 150 -99 callsubr -150 vhcurveto rmoveto -100 callsubr hmoveto -100 callsubr rmoveto -510 210 510 vlineto 256 hlineto -128 510 rlineto -50 50 -205 -204 -50 -50 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto -107 callsubr 100 505 -101 callsubr 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto 151 197 -105 callsubr 66 510 rmoveto 128 -435 128 435 rlineto -377 -487 -104 callsubr 66 510 rmoveto 256 hlineto -128 -435 rlineto -249 -52 -104 callsubr -107 callsubr 211 657 -105 callsubr -111 -152 -101 callsubr -106 callsubr 80 -103 callsubr -106 callsubr 310 rmoveto 128 -510 128 510 rlineto -107 callsubr -28 -510 -103 callsubr 334 hmoveto -128 510 -128 -510 rlineto 88 -102 callsubr 0001beef ufo2ft-3.3.1/tests/data/TestFont-NoOptimize-CFF.ttx000066400000000000000000000442221470175262700220130ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 100 450 0 rmoveto 0 750 rlineto -400 0 rlineto 0 -750 rlineto 350 50 rmoveto -300 0 rlineto 0 650 rlineto 300 0 rlineto endchar -150 endchar -12 66 0 rmoveto 256 0 rlineto -128 510 rlineto endchar 10 100 505 rmoveto 0 -510 rlineto 210 0 rlineto 0 510 rlineto endchar -26 300 -10 rmoveto 0 510 rlineto -150 0 -50 -50 0 -205 rrcurveto 0 -205 50 -50 150 0 rrcurveto endchar -26 151 197 rmoveto -34 0 -27 -27 0 -33 rrcurveto 0 -33 27 -27 34 0 rrcurveto 33 0 27 27 0 33 rrcurveto 0 33 -27 27 -33 0 rrcurveto endchar -12 66 510 rmoveto 128 -435 rlineto 128 435 rlineto -377 -487 rmoveto 509 0 rlineto 0 150 -50 50 -205 0 rrcurveto -204 0 -50 -50 0 -150 rrcurveto endchar 10 66 510 rmoveto 256 0 rlineto -128 -435 rlineto -249 -52 rmoveto 509 0 rlineto 0 150 -50 50 -205 0 rrcurveto -204 0 -50 -50 0 -150 rrcurveto endchar -12 66 0 rmoveto 256 0 rlineto -128 510 rlineto endchar 10 211 657 rmoveto -34 0 -27 -27 0 -33 rrcurveto 0 -33 27 -27 34 0 rrcurveto 33 0 27 27 0 33 rrcurveto 0 33 -27 27 -33 0 rrcurveto -111 -152 rmoveto 0 -510 rlineto 210 0 rlineto 0 510 rlineto endchar 200 -55 -80 rmoveto 509 0 rlineto 0 149 -50 50 -205 0 rrcurveto -204 0 -50 -50 0 -149 rrcurveto 121 80 rmoveto 256 0 rlineto -128 510 rlineto endchar 200 -55 -80 rmoveto 509 0 rlineto 0 149 -50 50 -205 0 rrcurveto -204 0 -50 -50 0 -149 rrcurveto 121 310 rmoveto 128 -510 rlineto 128 510 rlineto endchar 200 66 0 rmoveto 256 0 rlineto -128 510 rlineto -28 -510 rmoveto 256 0 rlineto -128 510 rlineto endchar 200 334 0 rmoveto -128 510 rlineto -128 -510 rlineto 88 0 rmoveto 256 0 rlineto -128 510 rlineto endchar 0001beef ufo2ft-3.3.1/tests/data/TestFont-NoOptimize-CFF2.ttx000066400000000000000000000441271470175262700221010ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 450 0 rmoveto 0 750 rlineto -400 0 rlineto 0 -750 rlineto 350 50 rmoveto -300 0 rlineto 0 650 rlineto 300 0 rlineto 66 0 rmoveto 256 0 rlineto -128 510 rlineto 100 505 rmoveto 0 -510 rlineto 210 0 rlineto 0 510 rlineto 300 -10 rmoveto 0 510 rlineto -150 0 -50 -50 0 -205 rrcurveto 0 -205 50 -50 150 0 rrcurveto 151 197 rmoveto -34 0 -27 -27 0 -33 rrcurveto 0 -33 27 -27 34 0 rrcurveto 33 0 27 27 0 33 rrcurveto 0 33 -27 27 -33 0 rrcurveto 66 510 rmoveto 128 -435 rlineto 128 435 rlineto -377 -487 rmoveto 509 0 rlineto 0 150 -50 50 -205 0 rrcurveto -204 0 -50 -50 0 -150 rrcurveto 66 510 rmoveto 256 0 rlineto -128 -435 rlineto -249 -52 rmoveto 509 0 rlineto 0 150 -50 50 -205 0 rrcurveto -204 0 -50 -50 0 -150 rrcurveto 66 0 rmoveto 256 0 rlineto -128 510 rlineto 211 657 rmoveto -34 0 -27 -27 0 -33 rrcurveto 0 -33 27 -27 34 0 rrcurveto 33 0 27 27 0 33 rrcurveto 0 33 -27 27 -33 0 rrcurveto -111 -152 rmoveto 0 -510 rlineto 210 0 rlineto 0 510 rlineto -55 -80 rmoveto 509 0 rlineto 0 149 -50 50 -205 0 rrcurveto -204 0 -50 -50 0 -149 rrcurveto 121 80 rmoveto 256 0 rlineto -128 510 rlineto -55 -80 rmoveto 509 0 rlineto 0 149 -50 50 -205 0 rrcurveto -204 0 -50 -50 0 -149 rrcurveto 121 310 rmoveto 128 -510 rlineto 128 510 rlineto 66 0 rmoveto 256 0 rlineto -128 510 rlineto -28 -510 rmoveto 256 0 rlineto -128 510 rlineto 334 0 rmoveto -128 510 rlineto -128 -510 rlineto 88 0 rmoveto 256 0 rlineto -128 510 rlineto 0001beef ufo2ft-3.3.1/tests/data/TestFont-NoOverlaps-CFF-pathops.ttx000066400000000000000000000436551470175262700234730ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) hlineto -128 510 return 66 510 rmoveto 85 -288 rlineto -164 -8 -42 -54 -137 vvcurveto 509 hlineto 140 -44 53 -173 6 vhcurveto 85 288 rlineto return rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto endchar 66 hmoveto 256 -107 callsubr rlineto endchar 100 505 rmoveto -510 210 510 vlineto return hlineto 123 -34 55 -127 16 vhcurveto return 143 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto endchar -107 endchar 31 -104 callsubr 53 -103 callsubr endchar 17 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto endchar 17 151 197 -105 callsubr 31 -106 callsubr endchar 53 -106 callsubr -85 -288 rmoveto -43 -147 -43 147 rlineto 1 15 16 0 17 hhcurveto 13 13 0 -1 12 hvcurveto endchar 31 -104 callsubr 53 -103 callsubr -99 152 -105 callsubr -55 -80 rmoveto 509 -102 callsubr -99 396 -100 -397 rlineto -117 -18 -32 -56 -119 vvcurveto endchar -55 -80 rmoveto 199 hlineto 50 -200 50 200 rlineto 210 -102 callsubr 29 116 rlineto -256 hlineto 29 -117 rlineto -118 -18 -32 -55 -120 vvcurveto endchar 66 hmoveto 356 -107 callsubr -50 -199 -50 199 rlineto endchar 334 hmoveto 88 -107 callsubr -44 -175 -44 175 -128 -510 rlineto endchar 0001beef ufo2ft-3.3.1/tests/data/TestFont-NoOverlaps-CFF.ttx000066400000000000000000000435531470175262700220140ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) -55 23 rmoveto 509 hlineto 140 -44 53 -173 6 vhcurveto 85 288 rlineto -256 hlineto 85 -288 rlineto -164 -8 -42 -54 -137 vvcurveto return rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto return 66 hmoveto 256 hlineto -128 510 rlineto endchar rmoveto -510 210 510 vlineto endchar hlineto 123 -34 55 -127 16 vhcurveto return 143 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto endchar -107 endchar 31 -105 callsubr 53 100 505 -104 callsubr 17 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto endchar 17 151 197 -106 callsubr endchar 31 -107 callsubr endchar 53 -107 callsubr 254 200 rmoveto 13 13 0 -1 12 hvcurveto -43 -147 -43 147 rlineto 1 15 16 0 17 hhcurveto endchar 31 -105 callsubr 53 211 657 -106 callsubr -111 -152 -104 callsubr -55 -80 rmoveto 509 -103 callsubr -99 396 -100 -397 rlineto -117 -18 -32 -56 -119 vvcurveto endchar -55 -80 rmoveto 199 hlineto 50 -200 50 200 rlineto 210 -103 callsubr 29 116 rlineto -256 hlineto 29 -117 rlineto -118 -18 -32 -55 -120 vvcurveto endchar 66 hmoveto 356 hlineto -128 510 -50 -199 -50 199 rlineto endchar 294 510 rmoveto -44 -175 -44 175 -128 -510 rlineto 344 hlineto endchar 0001beef ufo2ft-3.3.1/tests/data/TestFont-NoOverlaps-TTF-pathops.ttx000066400000000000000000000506111470175262700235200ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 0001beef ufo2ft-3.3.1/tests/data/TestFont-NoOverlaps-TTF.ttx000066400000000000000000000506111470175262700220440ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 0001beef ufo2ft-3.3.1/tests/data/TestFont-Specialized-CFF.ttx000066400000000000000000000431541470175262700221550ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 100 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto endchar -150 endchar -12 66 hmoveto 256 hlineto -128 510 rlineto endchar 10 100 505 rmoveto -510 210 510 vlineto endchar -26 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto endchar -26 151 197 rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto endchar -12 66 510 rmoveto 128 -435 128 435 rlineto -377 -487 rmoveto 509 hlineto 150 -50 50 -205 -204 -50 -50 -150 vhcurveto endchar 10 66 510 rmoveto 256 hlineto -128 -435 rlineto -249 -52 rmoveto 509 hlineto 150 -50 50 -205 -204 -50 -50 -150 vhcurveto endchar -12 66 hmoveto 256 hlineto -128 510 rlineto endchar 10 211 657 rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto -111 -152 rmoveto -510 210 510 vlineto endchar 200 -55 -80 rmoveto 509 hlineto 149 -50 50 -205 -204 -50 -50 -149 vhcurveto 121 80 rmoveto 256 hlineto -128 510 rlineto endchar 200 -55 -80 rmoveto 509 hlineto 149 -50 50 -205 -204 -50 -50 -149 vhcurveto 121 310 rmoveto 128 -510 128 510 rlineto endchar 200 66 hmoveto 256 hlineto -128 510 rlineto -28 -510 rmoveto 256 hlineto -128 510 rlineto endchar 200 334 hmoveto -128 510 -128 -510 rlineto 88 hmoveto 256 hlineto -128 510 rlineto endchar 0001beef ufo2ft-3.3.1/tests/data/TestFont-Specialized-CFF2.ttx000066400000000000000000000430611470175262700222340ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 450 hmoveto 750 -400 -750 vlineto 350 50 rmoveto -300 650 300 hlineto 66 hmoveto 256 hlineto -128 510 rlineto 100 505 rmoveto -510 210 510 vlineto 300 -10 rmoveto 510 vlineto -150 -50 -50 -205 -205 50 -50 150 hvcurveto 151 197 rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto 66 510 rmoveto 128 -435 128 435 rlineto -377 -487 rmoveto 509 hlineto 150 -50 50 -205 -204 -50 -50 -150 vhcurveto 66 510 rmoveto 256 hlineto -128 -435 rlineto -249 -52 rmoveto 509 hlineto 150 -50 50 -205 -204 -50 -50 -150 vhcurveto 66 hmoveto 256 hlineto -128 510 rlineto 211 657 rmoveto -34 -27 -27 -33 -33 27 -27 34 33 27 27 33 33 -27 27 -33 hvcurveto -111 -152 rmoveto -510 210 510 vlineto -55 -80 rmoveto 509 hlineto 149 -50 50 -205 -204 -50 -50 -149 vhcurveto 121 80 rmoveto 256 hlineto -128 510 rlineto -55 -80 rmoveto 509 hlineto 149 -50 50 -205 -204 -50 -50 -149 vhcurveto 121 310 rmoveto 128 -510 128 510 rlineto 66 hmoveto 256 hlineto -128 510 rlineto -28 -510 rmoveto 256 hlineto -128 510 rlineto 334 hmoveto -128 510 -128 -510 rlineto 88 hmoveto 256 hlineto -128 510 rlineto 0001beef ufo2ft-3.3.1/tests/data/TestFont-TTF-post3.ttx000066400000000000000000000453521470175262700210320ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 0001beef ufo2ft-3.3.1/tests/data/TestFont-not-allQuadratic.ttx000066400000000000000000000125671470175262700225350ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ttx000066400000000000000000000502741470175262700173300ustar00rootroot00000000000000 Unique Font Identifier Copyright © Some Foundry. Some Font Regular (Style Map Family Name) Regular OpenType name Table Unique ID Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) OpenType name Table Version SomeFont-Regular Postscript Font Name Trademark Some Foundry Some Foundry (Manufacturer Name) Some Designer Some Font by Some Designer for Some Foundry. http://somefoundry.com http://somedesigner.com License info for Some Foundry. http://somefoundry.com/license Some Font (Preferred Family Name) Regular (Preferred Subfamily Name) Some Font Regular (Compatible Full Name) Sample Text for Some Font. Some Font (WWS Family Name) Regular (WWS Subfamily Name) 0001beef ufo2ft-3.3.1/tests/data/TestFont.ufo/000077500000000000000000000000001470175262700173475ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestFont.ufo/data/000077500000000000000000000000001470175262700202605ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestFont.ufo/data/com.github.fonttools.ttx/000077500000000000000000000000001470175262700251635ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestFont.ufo/data/com.github.fonttools.ttx/CUST.ttx000066400000000000000000000002711470175262700265020ustar00rootroot00000000000000 0001beef ufo2ft-3.3.1/tests/data/TestFont.ufo/features.fea000066400000000000000000000000001470175262700216300ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestFont.ufo/fontinfo.plist000066400000000000000000000215341470175262700222530ustar00rootroot00000000000000 ascender 750 capHeight 750 copyright Copyright © Some Foundry. descender -250 familyName Some Font (Family Name) guidelines x 250 x -20 x 30 y 500 y -200 y 700 angle 135 x 0 y 0 angle 45 x 0 y 700 angle 135 x 20 y 0 italicAngle -12.5 macintoshFONDFamilyID 15000 macintoshFONDName SomeFont Regular (FOND Name) note A note. openTypeGaspRangeRecords rangeGaspBehavior 1 3 rangeMaxPPEM 7 rangeGaspBehavior 0 1 2 3 rangeMaxPPEM 65535 openTypeHeadCreated 2000/01/01 00:00:00 openTypeHeadFlags 0 1 openTypeHeadLowestRecPPEM 10 openTypeHheaAscender 750 openTypeHheaCaretOffset 0 openTypeHheaCaretSlopeRise 1 openTypeHheaCaretSlopeRun 0 openTypeHheaDescender -250 openTypeHheaLineGap 200 openTypeNameCompatibleFullName Some Font Regular (Compatible Full Name) openTypeNameDescription Some Font by Some Designer for Some Foundry. openTypeNameDesigner Some Designer openTypeNameDesignerURL http://somedesigner.com openTypeNameLicense License info for Some Foundry. openTypeNameLicenseURL http://somefoundry.com/license openTypeNameManufacturer Some Foundry openTypeNameManufacturerURL http://somefoundry.com openTypeNamePreferredFamilyName Some Font (Preferred Family Name) openTypeNamePreferredSubfamilyName Regular (Preferred Subfamily Name) openTypeNameRecords encodingID 0 languageID 0 nameID 3 platformID 1 string Unique Font Identifier encodingID 1 languageID 1033 nameID 8 platformID 3 string Some Foundry (Manufacturer Name) openTypeNameSampleText Sample Text for Some Font. openTypeNameUniqueID OpenType name Table Unique ID openTypeNameVersion OpenType name Table Version openTypeNameWWSFamilyName Some Font (WWS Family Name) openTypeNameWWSSubfamilyName Regular (WWS Subfamily Name) openTypeOS2CodePageRanges 0 1 openTypeOS2FamilyClass 1 1 openTypeOS2Panose 0 1 2 3 4 5 6 7 8 9 openTypeOS2Selection 3 openTypeOS2StrikeoutPosition 300 openTypeOS2StrikeoutSize 20 openTypeOS2SubscriptXOffset 0 openTypeOS2SubscriptXSize 200 openTypeOS2SubscriptYOffset -100 openTypeOS2SubscriptYSize 400 openTypeOS2SuperscriptXOffset 0 openTypeOS2SuperscriptXSize 200 openTypeOS2SuperscriptYOffset 200 openTypeOS2SuperscriptYSize 400 openTypeOS2Type openTypeOS2TypoAscender 750 openTypeOS2TypoDescender -250 openTypeOS2TypoLineGap 200 openTypeOS2UnicodeRanges 0 1 openTypeOS2VendorID SOME openTypeOS2WeightClass 500 openTypeOS2WidthClass 5 openTypeOS2WinAscent 750 openTypeOS2WinDescent 250 openTypeVheaCaretOffset 0 openTypeVheaCaretSlopeRise 0 openTypeVheaCaretSlopeRun 1 openTypeVheaVertTypoAscender 750 openTypeVheaVertTypoDescender -250 openTypeVheaVertTypoLineGap 200 postscriptBlueFuzz 1 postscriptBlueScale 0.039625 postscriptBlueShift 7 postscriptBlueValues 500 510 postscriptDefaultCharacter .notdef postscriptDefaultWidthX 400 postscriptFamilyBlues 500 510 postscriptFamilyOtherBlues -250 -260 postscriptFontName SomeFont-Regular (Postscript Font Name) postscriptForceBold postscriptFullName Some Font-Regular (Postscript Full Name) postscriptIsFixedPitch postscriptNominalWidthX 400 postscriptOtherBlues -250 -260 postscriptSlantAngle -12.5 postscriptStemSnapH 100 120 postscriptStemSnapV 80 90 postscriptUnderlinePosition -200 postscriptUnderlineThickness 20 postscriptUniqueID 4000000 postscriptWeightName Medium postscriptWindowsCharacterSet 1 styleMapFamilyName Some Font Regular (Style Map Family Name) styleMapStyleName regular styleName Regular (Style Name) trademark Trademark Some Foundry unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 500 year 2008 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/000077500000000000000000000000001470175262700206555ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/_notdef.glif000066400000000000000000000007711470175262700231430ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/a.glif000066400000000000000000000004531470175262700217420ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/b.glif000066400000000000000000000005261470175262700217440ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/c.glif000066400000000000000000000006411470175262700217430ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/contents.plist000066400000000000000000000014201470175262700235640ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif d d.glif e e.glif f f.glif g g.glif h h.glif i i.glif j j.glif k k.glif l l.glif space space.glif ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/d.glif000066400000000000000000000011621470175262700217430ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/e.glif000066400000000000000000000010561470175262700217460ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/f.glif000066400000000000000000000010561470175262700217470ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/g.glif000066400000000000000000000002671470175262700217530ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/h.glif000066400000000000000000000003531470175262700217500ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/i.glif000066400000000000000000000006671470175262700217610ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/j.glif000066400000000000000000000007221470175262700217520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/k.glif000066400000000000000000000003361470175262700217540ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/l.glif000066400000000000000000000003701470175262700217530ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/glyphs/space.glif000066400000000000000000000002141470175262700226100ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestFont.ufo/kerning.plist000066400000000000000000000006511470175262700220630ustar00rootroot00000000000000 a a 5 b -10 space 1 b a -7 ufo2ft-3.3.1/tests/data/TestFont.ufo/layercontents.plist000066400000000000000000000004231470175262700233150ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/TestFont.ufo/lib.plist000066400000000000000000000023351470175262700211750ustar00rootroot00000000000000 public.glyphOrder .notdef glyph1 glyph2 space a b c d e f g h i j k l public.postscriptNames a uni0061 b uni0062 c uni0063 d uni0064 e uni0065 f uni0066 g uni0067 h uni0068 i uni0069 j uni006A k uni006B l uni006C space uni0020 ufo2ft-3.3.1/tests/data/TestFont.ufo/metainfo.plist000066400000000000000000000004531470175262700222300ustar00rootroot00000000000000 creator org.robofab.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/000077500000000000000000000000001470175262700215605ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/features.fea000066400000000000000000000001171470175262700240520ustar00rootroot00000000000000# Prefix: Languagesystems languagesystem DFLT dflt; languagesystem math dflt; ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/fontinfo.plist000066400000000000000000000023601470175262700244600ustar00rootroot00000000000000 ascender 800 capHeight 656 descender -200 familyName Test Math Font italicAngle 0 openTypeHeadCreated 2024/02/12 18:20:25 openTypeOS2Type 3 postscriptBlueValues -18 0 postscriptOtherBlues -201 -200 postscriptUnderlinePosition -150 postscriptUnderlineThickness 50 styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 449 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/000077500000000000000000000000001470175262700230665ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/contents.plist000066400000000000000000000024601470175262700260020ustar00rootroot00000000000000 parenleft parenleft.glif parenleft.bot parenleft.bot.glif parenleft.ext parenleft.ext.glif parenleft.size1 parenleft.size1.glif parenleft.size2 parenleft.size2.glif parenleft.size3 parenleft.size3.glif parenleft.size4 parenleft.size4.glif parenleft.top parenleft.top.glif parenright parenright.glif parenright.bot parenright.bot.glif parenright.ext parenright.ext.glif parenright.size1 parenright.size1.glif parenright.size2 parenright.size2.glif parenright.size3 parenright.size3.glif parenright.size4 parenright.size4.glif parenright.top parenright.top.glif ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.bot.glif000066400000000000000000000011061470175262700264720ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.ext.glif000066400000000000000000000005151470175262700265110ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.glif000066400000000000000000000032601470175262700257120ustar00rootroot00000000000000 com.nagwa.MATHPlugin.variants vAssembly parenleft.bot 0 0 314 parenleft.ext 1 630 630 parenleft.top 0 314 0 vVariants parenleft parenleft.size1 parenleft.size2 parenleft.size3 parenleft.size4 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size1.glif000066400000000000000000000013021470175262700267370ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size2.glif000066400000000000000000000013071470175262700267450ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size3.glif000066400000000000000000000013151470175262700267450ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size4.glif000066400000000000000000000013161470175262700267470ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.top.glif000066400000000000000000000003131470175262700265070ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.bot.glif000066400000000000000000000003131470175262700266540ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.ext.glif000066400000000000000000000002771470175262700267010ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.glif000066400000000000000000000023031470175262700260720ustar00rootroot00000000000000 com.nagwa.MATHPlugin.variants vAssembly parenright.bot 0 0 314 parenright.ext 1 630 630 parenright.top 0 314 0 vVariants parenright parenright.size1 parenright.size2 parenright.size3 parenright.size4 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size1.glif000066400000000000000000000003171470175262700271270ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size2.glif000066400000000000000000000003171470175262700271300ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size3.glif000066400000000000000000000003171470175262700271310ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size4.glif000066400000000000000000000003171470175262700271320ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.top.glif000066400000000000000000000003131470175262700266720ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700255330ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/lib.plist000066400000000000000000000116701470175262700234100ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.useProductionNames com.nagwa.MATHPlugin.constants AccentBaseHeight 449 AxisHeight 250 DelimitedSubFormulaMinHeight 1010 DisplayOperatorMinHeight 2260 FlattenedAccentBaseHeight 656 FractionDenomDisplayStyleGapMin 137 FractionDenominatorDisplayStyleShiftDown 685 FractionDenominatorGapMin 45 FractionDenominatorShiftDown 345 FractionNumDisplayStyleGapMin 137 FractionNumeratorDisplayStyleShiftUp 677 FractionNumeratorGapMin 45 FractionNumeratorShiftUp 394 FractionRuleThickness 45 LowerLimitBaselineDropMin 600 LowerLimitGapMin 166 MinConnectorOverlap 20 OverbarExtraAscender 45 OverbarRuleThickness 45 OverbarVerticalGap 137 RadicalDegreeBottomRaisePercent 60 RadicalDisplayStyleVerticalGap 158 RadicalExtraAscender 70 RadicalKernAfterDegree -400 RadicalKernBeforeDegree 277 RadicalRuleThickness 45 RadicalVerticalGap 57 ScriptPercentScaleDown 70 ScriptScriptPercentScaleDown 50 SkewedFractionHorizontalGap 400 SkewedFractionVerticalGap 60 SpaceAfterScript 50 StackBottomDisplayStyleShiftDown 685 StackBottomShiftDown 345 StackDisplayStyleGapMin 321 StackGapMin 137 StackTopDisplayStyleShiftUp 677 StackTopShiftUp 444 StretchStackBottomShiftDown 600 StretchStackGapAboveMin 111 StretchStackGapBelowMin 166 StretchStackTopShiftUp 199 SubSuperscriptGapMin 183 SubscriptBaselineDropMin 50 SubscriptShiftDown 149 SubscriptTopMax 359 SuperscriptBaselineDropMax 385 SuperscriptBottomMaxWithSubscript 359 SuperscriptBottomMin 112 SuperscriptShiftUp 362 SuperscriptShiftUpCramped 289 UnderbarExtraDescender 45 UnderbarRuleThickness 45 UnderbarVerticalGap 137 UpperLimitBaselineRiseMin 199 UpperLimitGapMin 111 com.nagwa.MATHPlugin.extendedShape parenleft parenright public.glyphOrder parenleft parenleft.bot parenleft.ext parenleft.size1 parenleft.size2 parenleft.size3 parenleft.size4 parenleft.top parenright parenright.bot parenright.ext parenright.size1 parenright.size2 parenright.size3 parenright.size4 parenright.top ufo2ft-3.3.1/tests/data/TestMathFont-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700244460ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/000077500000000000000000000000001470175262700206765ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/fontinfo.plist000066400000000000000000000017741470175262700236060ustar00rootroot00000000000000 ascender 600 capHeight 700 descender -400 familyName TestVarFont 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 ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/glyphs/000077500000000000000000000000001470175262700222045ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/glyphs/alef-ar.fina.glif000066400000000000000000000006741470175262700253010ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/glyphs/contents.plist000066400000000000000000000004701470175262700251170ustar00rootroot00000000000000 alef-ar.fina alef-ar.fina.glif space space.glif ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/glyphs/space.glif000066400000000000000000000002321470175262700241370ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700246510ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/lib.plist000066400000000000000000000006651470175262700225300ustar00rootroot00000000000000 public.glyphOrder space alef-ar.fina public.postscriptNames alef-ar.fina uniFE8E ufo2ft-3.3.1/tests/data/TestVarFont-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700235640ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/TestVarFont-MyFontVF1.ttx000066400000000000000000000031711470175262700215220ustar00rootroot00000000000000 Weight TestVarFont Regular 1.000;NONE;TestVarFont-Regular TestVarFont Regular Version 1.000 TestVarFont-Regular Weight ufo2ft-3.3.1/tests/data/TestVarFont-MyFontVF2.ttx000066400000000000000000000034261470175262700215260ustar00rootroot00000000000000 Weight My Font Narrow VF Regular 2.000;NONE;MyFontNarrVF-Regular My Font Narrow VF Regular Version 2.000 MyFontNarrVF-Regular Weight My Font Narrow VF is a registered trademark... ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/000077500000000000000000000000001470175262700214175ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/fontinfo.plist000066400000000000000000000017731470175262700243260ustar00rootroot00000000000000 ascender 600 capHeight 0 descender -400 familyName TestVarFont italicAngle 0 openTypeHeadCreated 2022/07/26 14:49:29 openTypeOS2Type 3 postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 0 ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/glyphs/000077500000000000000000000000001470175262700227255ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/glyphs/alef-ar.fina.glif000066400000000000000000000006741470175262700260220ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/glyphs/contents.plist000066400000000000000000000004701470175262700256400ustar00rootroot00000000000000 alef-ar.fina alef-ar.fina.glif space space.glif ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/glyphs/space.glif000066400000000000000000000002321470175262700246600ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700253720ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/lib.plist000066400000000000000000000006651470175262700232510ustar00rootroot00000000000000 public.glyphOrder space alef-ar.fina public.postscriptNames alef-ar.fina uniFE8E ufo2ft-3.3.1/tests/data/TestVarFont-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700243050ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/TestVarFont.designspace000066400000000000000000000030701470175262700214370ustar00rootroot00000000000000 public.fontInfo familyName My Font Narrow VF styleName Regular postscriptFontName MyFontNarrVF-Regular trademark My Font Narrow VF is a registered trademark... versionMajor 2 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/000077500000000000000000000000001470175262700205235ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/fontinfo.plist000066400000000000000000000017731470175262700234320ustar00rootroot00000000000000 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 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/glyphs/000077500000000000000000000000001470175262700220315ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/glyphs/alef-ar.fina.glif000066400000000000000000000016601470175262700251220ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2023/11/20 16:51:14 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/glyphs/contents.plist000066400000000000000000000010451470175262700247430ustar00rootroot00000000000000 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 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/glyphs/dotabove-ar.glif000066400000000000000000000017121470175262700251000ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2023/11/20 16:50:50 com.schriftgestaltung.Glyphs.originalWidth 300 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/glyphs/layerinfo.plist000066400000000000000000000013201470175262700250720ustar00rootroot00000000000000 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 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif000066400000000000000000000055631470175262700310560ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs._originalLayerName 26 Jul 22, 15:59 com.schriftgestaltung.Glyphs.lastChange 2022/07/27 08:01:34 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.glif000066400000000000000000000061431470175262700250160ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2022/07/27 08:01:34 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/glyphs/space.glif000066400000000000000000000004461470175262700237730ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2022/07/26 15:00:45 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/kerning.plist000066400000000000000000000004551470175262700232410ustar00rootroot00000000000000 alef-ar.fina alef-ar.fina 35 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/layercontents.plist000066400000000000000000000004371470175262700244760ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/lib.plist000066400000000000000000000055321470175262700223530ustar00rootroot00000000000000 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 ufo2ft-3.3.1/tests/data/TestVarfea-Bold.ufo/metainfo.plist000066400000000000000000000004761470175262700234110ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/000077500000000000000000000000001470175262700212445ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/fontinfo.plist000066400000000000000000000030221470175262700241400ustar00rootroot00000000000000 ascender 600 capHeight 0 descender -400 familyName TestVarfea guidelines italicAngle 0 openTypeHeadCreated 2022/07/26 14:49:29 openTypeOS2Type 3 postscriptBlueValues -16 0 600 616 postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues -416 -400 postscriptStemSnapH postscriptStemSnapV postscriptUnderlinePosition -100 postscriptUnderlineThickness 50 styleName Regular unitsPerEm 1000 versionMajor 1 versionMinor 0 xHeight 0 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/000077500000000000000000000000001470175262700225525ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/a.glif000066400000000000000000000004601470175262700236350ustar00rootroot00000000000000 public.markColor 0.5955,0.7484,1,1 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/alef-ar.fina.glif000066400000000000000000000016571470175262700256510ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2023/11/20 16:51:14 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/contents.plist000066400000000000000000000012151470175262700254630ustar00rootroot00000000000000 a a.glif alef-ar.fina alef-ar.fina.glif dotabove-ar dotabove-ar.glif gravecmb gravecmb.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 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/dotabove-ar.glif000066400000000000000000000017101470175262700256170ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2023/11/20 16:50:50 com.schriftgestaltung.Glyphs.originalWidth 300 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/gravecmb.glif000066400000000000000000000004701470175262700252040ustar00rootroot00000000000000 public.markColor 0.5955,0.7484,1,1 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/layerinfo.plist000066400000000000000000000013471470175262700256240ustar00rootroot00000000000000 color 0.5,0,0.5,0.7 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 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif000066400000000000000000000055611470175262700315750ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs._originalLayerName 26 Jul 22, 15:58 com.schriftgestaltung.Glyphs.lastChange 2022/07/27 08:01:34 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.glif000066400000000000000000000061461470175262700255420ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2022/07/27 08:01:34 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/glyphs/space.glif000066400000000000000000000004461470175262700245140ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2022/07/26 15:00:45 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/groups.plist000066400000000000000000000005401470175262700236370ustar00rootroot00000000000000 public.kern1.a a public.kern2.a a ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/kerning.plist000066400000000000000000000006351470175262700237620ustar00rootroot00000000000000 alef-ar.fina alef-ar.fina 15 public.kern1.a public.kern2.a 0 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/layercontents.plist000066400000000000000000000004371470175262700252170ustar00rootroot00000000000000 public.default glyphs ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/lib.plist000066400000000000000000000053601470175262700230730ustar00rootroot00000000000000 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 peh-ar.init.BRACKET.varAlt01 a gravecmb public.postscriptNames alef-ar.fina uniFE8E dotabove-ar dotabovear peh-ar.init uniFB58 peh-ar.init.BRACKET.varAlt01 uniFB58.BRACKET.varAlt01 ufo2ft-3.3.1/tests/data/TestVarfea-Regular.ufo/metainfo.plist000066400000000000000000000004761470175262700241320ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/data/TestVarfea.designspace000066400000000000000000000017141470175262700212670ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestVarfea.glyphs000066400000000000000000000162021470175262700203060ustar00rootroot00000000000000{ .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; } ufo2ft-3.3.1/tests/data/TestVariableFont-CFF2-cffsubr.ttx000066400000000000000000000421221470175262700230750ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 2 blend rmoveto -16 -94 78 -2 9 88 50 -250 rmoveto 400 1000 -400 hlineto 50 -950 rmoveto 900 300 -900 vlineto 468 -1 rmoveto -21 435 -233 70 -205 -76 27 -91 -56 1 blend 172 60 155 -40 -59 2 2 blend 3 -360 56 1 blend rlineto 12 266 59 -2 2 blend rmoveto -352 -23 3 -218 139 -34 221 83 -6 63 -222 -60 -75 52 15 40 13 37 -21 5 blend 2 46 294 35 -78 -30 2 blend rlineto -21 597 -8 28 -106 callsubr -19 -48 44 7 -4 29 6 blend rlineto -107 callsubr -107 callsubr 167 395 -5 -84 43 118 -106 callsubr -7 -19 -17 -48 16 44 3 7 -1 -4 10 29 6 blend rlineto 559 459 rmoveto -235 71 -286 -187 389 -188 -145 -79 -229 98 -28 -91 279 -96 278 187 -369 192 113 76 -22 55 -58 -61 19 49 34 9 9 -56 -2 -41 46 12 29 24 -57 -31 18 blend 213 -66 rlineto wght 0x0 350.0 350.0 625.0 256 ufo2ft-3.3.1/tests/data/TestVariableFont-CFF2-post3.ttx000066400000000000000000000406771470175262700225300ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight 50 -250 rmoveto 400 1000 -400 hlineto 50 -950 rmoveto 900 300 -900 vlineto 468 -1 rmoveto -21 435 -233 70 -205 -76 27 -91 -56 1 blend 172 60 155 -40 -59 2 2 blend 3 -360 56 1 blend rlineto 12 266 59 -2 2 blend rmoveto -352 -23 3 -218 139 -34 221 83 -6 63 -222 -60 -75 52 15 40 13 37 -21 5 blend 2 46 294 35 -78 -30 2 blend rlineto 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 167 395 -5 -84 43 118 2 blend rmoveto -16 -94 78 -2 9 88 -7 -19 -17 -48 16 44 3 7 -1 -4 10 29 6 blend rlineto 559 459 rmoveto -235 71 -286 -187 389 -188 -145 -79 -229 98 -28 -91 279 -96 278 187 -369 192 113 76 -22 55 -58 -61 19 49 34 9 9 -56 -2 -41 46 12 29 24 -57 -31 18 blend 213 -66 rlineto -21 597 -8 28 2 blend rmoveto -16 -94 78 -2 9 88 -19 -48 44 7 -4 29 6 blend rlineto wght 0x0 350.0 350.0 625.0 256 ufo2ft-3.3.1/tests/data/TestVariableFont-CFF2-sparse-notdefGlyph.ttx000066400000000000000000000130201470175262700252140ustar00rootroot00000000000000 50 -250 rmoveto 400 1000 -400 hlineto 50 -950 rmoveto 900 300 -900 vlineto 100 500 1 blend 200 rmoveto 278 -112 222 -138 -138 -112 -222 -278 277 -112 223 -138 -138 -112 -223 -277 8 blend vhcurveto 468 -1 rmoveto -21 435 -233 70 -205 -76 27 -91 -56 1 blend 172 60 155 -40 -59 2 2 blend 3 -360 56 1 blend rlineto 12 266 59 -2 2 blend rmoveto -352 -23 3 -218 139 -34 221 83 -6 63 -222 -60 -75 52 15 40 13 37 -21 5 blend 2 46 294 35 -78 -30 2 blend rlineto -21 597 -8 28 2 blend rmoveto -16 -94 78 -2 9 88 -19 -48 44 7 -4 29 6 blend rlineto 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 167 395 -5 -84 43 118 2 blend rmoveto -16 -94 78 -2 9 88 -7 -19 -17 -48 16 44 3 7 -1 -4 10 29 6 blend rlineto 559 459 rmoveto -235 71 -286 -187 389 -188 -145 -79 -229 98 -28 -91 279 -96 278 187 -369 192 113 76 -22 55 -58 -61 19 49 34 9 9 -56 -2 -41 46 12 29 24 -57 -31 18 blend 213 -66 rlineto ufo2ft-3.3.1/tests/data/TestVariableFont-CFF2-useProductionNames.ttx000066400000000000000000000424271470175262700253020ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight 50 -250 rmoveto 400 1000 -400 hlineto 50 -950 rmoveto 900 300 -900 vlineto 468 -1 rmoveto -21 435 -233 70 -205 -76 27 -91 -56 1 blend 172 60 155 -40 -59 2 2 blend 3 -360 56 1 blend rlineto 12 266 59 -2 2 blend rmoveto -352 -23 3 -218 139 -34 221 83 -6 63 -222 -60 -75 52 15 40 13 37 -21 5 blend 2 46 294 35 -78 -30 2 blend rlineto 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 559 459 rmoveto -235 71 -286 -187 389 -188 -145 -79 -229 98 -28 -91 279 -96 278 187 -369 192 113 76 -22 55 -58 -61 19 49 34 9 9 -56 -2 -41 46 12 29 24 -57 -31 18 blend 213 -66 rlineto 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 167 395 -5 -84 43 118 2 blend rmoveto -16 -94 78 -2 9 88 -7 -19 -17 -48 16 44 3 7 -1 -4 10 29 6 blend rlineto -21 597 -8 28 2 blend rmoveto -16 -94 78 -2 9 88 -19 -48 44 7 -4 29 6 blend rlineto wght 0x0 350.0 350.0 625.0 256 ufo2ft-3.3.1/tests/data/TestVariableFont-CFF2.ttx000066400000000000000000000421371470175262700214530ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight 50 -250 rmoveto 400 1000 -400 hlineto 50 -950 rmoveto 900 300 -900 vlineto 468 -1 rmoveto -21 435 -233 70 -205 -76 27 -91 -56 1 blend 172 60 155 -40 -59 2 2 blend 3 -360 56 1 blend rlineto 12 266 59 -2 2 blend rmoveto -352 -23 3 -218 139 -34 221 83 -6 63 -222 -60 -75 52 15 40 13 37 -21 5 blend 2 46 294 35 -78 -30 2 blend rlineto -21 597 -8 28 2 blend rmoveto -16 -94 78 -2 9 88 -19 -48 44 7 -4 29 6 blend rlineto 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 1 vsindex 127 228 -1 70 -25 1 2 blend rmoveto 449 -2 1 -45 -2 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -280 -54 -82 188 170 153 163 -124 -355 6 27 0 0 -27 0 36 0 -29 0 -34 0 31 0 -1 0 2 0 -45 -2 13 28 100 37 0 13 0 -2 55 -40 -54 -32 -86 -30 -57 -85 -60 34 57 84 146 -5 0 21 blend rlineto 167 395 -5 -84 43 118 2 blend rmoveto -16 -94 78 -2 9 88 -7 -19 -17 -48 16 44 3 7 -1 -4 10 29 6 blend rlineto 559 459 rmoveto -235 71 -286 -187 389 -188 -145 -79 -229 98 -28 -91 279 -96 278 187 -369 192 113 76 -22 55 -58 -61 19 49 34 9 9 -56 -2 -41 46 12 29 24 -57 -31 18 blend 213 -66 rlineto wght 0x0 350.0 350.0 625.0 256 ufo2ft-3.3.1/tests/data/TestVariableFont-TTF-not-allQuadratic.ttx000066400000000000000000000071361470175262700246320ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/TestVariableFont-TTF-post3.ttx000066400000000000000000000476451470175262700225070ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight wght 0x0 350.0 350.0 625.0 256 ufo2ft-3.3.1/tests/data/TestVariableFont-TTF-useProductionNames.ttx000066400000000000000000000514221470175262700252520ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight wght 0x0 350.0 350.0 625.0 256 ufo2ft-3.3.1/tests/data/TestVariableFont-TTF.ttx000066400000000000000000000511161470175262700214250ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight wght 0x0 350.0 350.0 625.0 256 ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/000077500000000000000000000000001470175262700201725ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/fontinfo.plist000066400000000000000000000023521470175262700230730ustar00rootroot00000000000000 ascender 2146.0 capHeight 1456.0 copyright Copyright 2011 Google Inc. All Rights Reserved. descender -555.0 familyName Roboto italicAngle 0 openTypeNameDesigner Christian Robertson openTypeNameDesignerURL Google.com openTypeNameLicense Licensed under the Apache License, Version 2.0 openTypeNameLicenseURL http://www.apache.org/licenses/LICENSE-2.0 openTypeNameManufacturer Google openTypeNameManufacturerURL Google.com styleName Regular trademark Roboto is a trademark of Google. unitsPerEm 2048.0 versionMajor 0 versionMinor 0 xHeight 1082.0 ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/glyphs/000077500000000000000000000000001470175262700215005ustar00rootroot00000000000000ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/glyphs/I_.glif000066400000000000000000000005351470175262700226750ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/glyphs/I_acute.glif000066400000000000000000000003561470175262700237200ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/glyphs/acute.glif000066400000000000000000000005471470175262700234520ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/glyphs/contents.plist000066400000000000000000000005761470175262700244220ustar00rootroot00000000000000 I I_.glif Iacute I_acute.glif acute acute.glif romanthree romanthree.glif ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/glyphs/romanthree.glif000066400000000000000000000004131470175262700245050ustar00rootroot00000000000000 ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/layercontents.plist000066400000000000000000000004111470175262700241350ustar00rootroot00000000000000 foreground glyphs ufo2ft-3.3.1/tests/data/UseMyMetrics.ufo/metainfo.plist000066400000000000000000000004451470175262700230540ustar00rootroot00000000000000 creator org.robofab.ufoLib formatVersion 3 ufo2ft-3.3.1/tests/featureCompiler_test.py000066400000000000000000000247541470175262700206530ustar00rootroot00000000000000import logging import re from textwrap import dedent import py import pytest from fontTools import ttLib from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound from ufo2ft.featureCompiler import FeatureCompiler, logger, parseLayoutFeatures from ufo2ft.featureWriters import ( FEATURE_WRITERS_KEY, BaseFeatureWriter, KernFeatureWriter, ast, ) class ParseLayoutFeaturesTest: def test_include(self, FontClass, tmpdir): tmpdir.join("test.fea").write_text( dedent( """\ # hello world """ ), encoding="utf-8", ) ufo = FontClass() ufo.features.text = dedent( """\ include(test.fea) """ ) ufo.save(str(tmpdir.join("Test.ufo"))) fea = parseLayoutFeatures(ufo) assert "# hello world" in str(fea) def test_include_no_ufo_path(self, FontClass, tmpdir, monkeypatch): ufo = FontClass() ufo.features.text = dedent( """\ include(test.fea) """ ) with monkeypatch.context() as context: context.chdir(str(tmpdir)) ufo.save("Test.ufo") with pytest.raises(IncludedFeaNotFound): parseLayoutFeatures(ufo) def test_include_not_found(self, FontClass, tmpdir, caplog): caplog.set_level(logging.ERROR) tmpdir.join("test.fea").write_text( dedent( """\ # hello world """ ), encoding="utf-8", ) ufo = FontClass() ufo.features.text = dedent( """\ include(../test.fea) """ ) ufo.save(str(tmpdir.join("Test.ufo"))) with caplog.at_level(logging.WARNING, logger=logger.name): with pytest.raises(IncludedFeaNotFound): parseLayoutFeatures(ufo) assert len(caplog.records) == 1 assert "change the file name in the include" in caplog.text def test_include_dir(self, FontClass, tmp_path, caplog): features_dir = tmp_path / "features" features_dir.mkdir() (features_dir / "test.fea").write_text( dedent( """\ # hello world """ ), encoding="utf-8", ) ufo = FontClass() ufo.features.text = dedent( """\ include(test.fea) """ ) ufo.save(tmp_path / "Test.ufo") fea = parseLayoutFeatures(ufo, features_dir) assert "# hello world" in str(fea) def test_include_dir_cwd(self, FontClass, tmp_path, monkeypatch): (tmp_path / "test.fea").write_text("# hello world", encoding="utf-8") ufo = FontClass() ufo.features.text = "include(test.fea)" with monkeypatch.context() as context: context.chdir(tmp_path) ufo.save("Test.ufo") fea = parseLayoutFeatures(ufo) assert "# hello world" in str(fea) class DummyFeatureWriter: tableTag = "GPOS" def write(self, font, feaFile, compiler=None): pass class FeatureCompilerTest: def test_ttFont(self, FontClass): ufo = FontClass() ufo.newGlyph("f") ufo.newGlyph("f_f") ufo.features.text = dedent( """\ feature liga { sub f f by f_f; } liga; """ ) ttFont = ttLib.TTFont() ttFont.setGlyphOrder(["f", "f_f"]) compiler = FeatureCompiler(ufo, ttFont) compiler.compile() assert "GSUB" in ttFont gsub = ttFont["GSUB"].table assert gsub.FeatureList.FeatureCount == 1 assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "liga" def test_ttFont_None(self, FontClass): ufo = FontClass() ufo.newGlyph("f") ufo.newGlyph("f_f") ufo.features.text = dedent( """\ feature liga { sub f f by f_f; } liga; """ ) compiler = FeatureCompiler(ufo) ttFont = compiler.compile() assert "GSUB" in ttFont gsub = ttFont["GSUB"].table assert gsub.FeatureList.FeatureCount == 1 assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "liga" def test_deprecated_methods(self, FontClass): compiler = FeatureCompiler(FontClass()) with pytest.warns(UserWarning, match="method is deprecated"): compiler.setupFile_features() compiler.features = "" with pytest.warns(UserWarning, match="method is deprecated"): compiler.setupFile_featureTables() class UserCompiler(FeatureCompiler): def setupFile_features(self): self.features = "# hello world" def setupFile_featureTables(self): self.ttFont = ttLib.TTFont() compiler = UserCompiler(FontClass()) with pytest.warns(UserWarning, match="method is deprecated"): compiler.compile() def test_deprecated_mtiFeatures_argument(self, FontClass): with pytest.warns(UserWarning, match="argument is ignored"): FeatureCompiler(FontClass(), mtiFeatures="whatever") def test_featureWriters_empty(self, FontClass): kernWriter = KernFeatureWriter(ignoreMarks=False) ufo = FontClass() ufo.newGlyph("a") ufo.newGlyph("v") ufo.kerning.update({("a", "v"): -40}) compiler = FeatureCompiler(ufo, featureWriters=[kernWriter]) ttFont1 = compiler.compile() assert "GPOS" in ttFont1 compiler = FeatureCompiler(ufo, featureWriters=[]) ttFont2 = compiler.compile() assert "GPOS" not in ttFont2 def test_loadFeatureWriters_from_UFO_lib(self, FontClass): ufo = FontClass() ufo.newGlyph("a") ufo.newGlyph("v") ufo.kerning.update({("a", "v"): -40}) ufo.lib[FEATURE_WRITERS_KEY] = [{"class": "KernFeatureWriter"}] compiler = FeatureCompiler(ufo) ttFont = compiler.compile() assert len(compiler.featureWriters) == 1 assert isinstance(compiler.featureWriters[0], KernFeatureWriter) assert "GPOS" in ttFont def test_loadFeatureWriters_from_both_UFO_lib_and_argument(self, FontClass): ufo = FontClass() ufo.lib[FEATURE_WRITERS_KEY] = [{"class": "KernFeatureWriter"}] compiler = FeatureCompiler(ufo, featureWriters=[..., DummyFeatureWriter]) assert len(compiler.featureWriters) == 2 assert isinstance(compiler.featureWriters[0], KernFeatureWriter) assert isinstance(compiler.featureWriters[1], DummyFeatureWriter) def test_loadFeatureWriters_from_both_defaults_and_argument(self, FontClass): ufo = FontClass() compiler = FeatureCompiler(ufo, featureWriters=[DummyFeatureWriter, ...]) assert len(compiler.featureWriters) == 1 + len( FeatureCompiler.defaultFeatureWriters ) assert isinstance(compiler.featureWriters[0], DummyFeatureWriter) def test_GSUB_writers_run_first(self, FontClass): class FooFeatureWriter(BaseFeatureWriter): tableTag = "GSUB" def write(self, font, feaFile, compiler=None): foo = ast.FeatureBlock("FOO ") foo.statements.append( ast.SingleSubstStatement( [ast.GlyphName("a")], [ast.GlyphName("v")], prefix="", suffix="", forceChain=None, ) ) feaFile.statements.append(foo) featureWriters = [KernFeatureWriter, FooFeatureWriter] ufo = FontClass() ufo.newGlyph("a") ufo.newGlyph("v") ufo.kerning.update({("a", "v"): -40}) compiler = FeatureCompiler(ufo, featureWriters=featureWriters) assert len(compiler.featureWriters) == 2 assert compiler.featureWriters[0].tableTag == "GSUB" assert compiler.featureWriters[1].tableTag == "GPOS" ttFont = compiler.compile() assert "GSUB" in ttFont gsub = ttFont["GSUB"].table assert gsub.FeatureList.FeatureCount == 1 assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "FOO " def test_buildTables_FeatureLibError(self, FontClass, caplog): caplog.set_level(logging.CRITICAL) ufo = FontClass() ufo.newGlyph("f") ufo.newGlyph("f.alt01") ufo.newGlyph("f_f") features = dedent( """\ feature BUGS { # invalid lookup MIXED_TYPE { sub f by f.alt01; sub f f by f_f; } MIXED_TYPE; } BUGS; """ ) ufo.features.text = features compiler = FeatureCompiler(ufo) tmpfile = None try: with caplog.at_level(logging.ERROR, logger=logger.name): with pytest.raises(FeatureLibError): compiler.compile() assert len(caplog.records) == 1 assert "Compilation failed! Inspect temporary file" in caplog.text tmpfile = py.path.local(re.findall(".*: '(.*)'$", caplog.text)[0]) assert tmpfile.exists() assert tmpfile.read_text("utf-8") == features finally: if tmpfile is not None: tmpfile.remove(ignore_errors=True) def test_setupFeatures_custom_feaIncludeDir(self, FontClass, tmp_path): (tmp_path / "family.fea").write_text( """\ feature liga { sub f f by f_f; } liga; """ ) ufo = FontClass() ufo.newGlyph("a") ufo.newGlyph("v") ufo.newGlyph("f") ufo.newGlyph("f_f") ufo.kerning.update({("a", "v"): -40}) ufo.features.text = dedent( """\ include(family.fea); """ ) compiler = FeatureCompiler(ufo, feaIncludeDir=str(tmp_path)) compiler.setupFeatures() assert compiler.features == dedent( """\ feature liga { sub f f by f_f; } liga; lookup kern_Default { lookupflag IgnoreMarks; pos a v -40; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) ufo2ft-3.3.1/tests/featureWriters/000077500000000000000000000000001470175262700171135ustar00rootroot00000000000000ufo2ft-3.3.1/tests/featureWriters/__init__.py000066400000000000000000000015151470175262700212260ustar00rootroot00000000000000from ufo2ft.featureCompiler import parseLayoutFeatures from ufo2ft.featureWriters import ast class FeatureWriterTest: # subclasses must override this FeatureWriter = None @classmethod def writeFeatures(cls, ufo, compiler=None, **kwargs): """Return a new FeatureFile object containing only the newly generated statements, or None if no new feature was generated. """ writer = cls.FeatureWriter(**kwargs) feaFile = parseLayoutFeatures(ufo) old_statements = [st.asFea() for st in feaFile.statements] if writer.write(ufo, feaFile, compiler=compiler): new = ast.FeatureFile() for statement in feaFile.statements: if statement.asFea() not in old_statements: new.statements.append(statement) return new ufo2ft-3.3.1/tests/featureWriters/__snapshots__/000077500000000000000000000000001470175262700217315ustar00rootroot00000000000000ufo2ft-3.3.1/tests/featureWriters/__snapshots__/kernFeatureWriter2_test.ambr000066400000000000000000000543151470175262700273750ustar00rootroot00000000000000# serializer version: 1 # name: test_ambiguous_direction_pair ''' lookup kern_ltr { lookupflag IgnoreMarks; pos bar bar 1; } kern_ltr; lookup kern_rtl { lookupflag IgnoreMarks; pos bar bar 1; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_ltr; script arab; language dflt; lookup kern_rtl; script hebr; language dflt; lookup kern_rtl; script latn; language dflt; lookup kern_ltr; } kern; ''' # --- # name: test_arabic_numerals ''' lookup kern_rtl { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_rtl; } kern; ''' # --- # name: test_arabic_numerals.1 ''' lookup kern_rtl { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_rtl; script arab; language dflt; lookup kern_rtl; } kern; ''' # --- # name: test_arabic_numerals.2 ''' lookup kern_rtl { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_rtl; script arab; language dflt; lookup kern_rtl; script thaa; language dflt; lookup kern_rtl; } kern; ''' # --- # name: test_arabic_numerals.3 ''' lookup kern_rtl { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_rtl; script thaa; language dflt; lookup kern_rtl; } kern; ''' # --- # name: test_defining_classdefs ''' @kern1.dflt.ssatelugu.alt = [ss-telugu.alt]; @kern1.ltr.shatelugu.below = [sha-telugu.below]; @kern1.ltr.ssatelugu.alt = [ssa-telugu.alt]; @kern2.ltr.katelugu.below = [ka-telugu.below]; @kern2.ltr.rVocalicMatratelugu = [rVocalicMatra-telugu]; lookup kern_dflt { lookupflag IgnoreMarks; enum pos @kern1.dflt.ssatelugu.alt sha-telugu.below 150; } kern_dflt; lookup kern_ltr { lookupflag IgnoreMarks; enum pos @kern1.ltr.ssatelugu.alt sha-telugu.below 150; pos @kern1.ltr.shatelugu.below @kern2.ltr.katelugu.below 20; pos @kern1.dflt.ssatelugu.alt @kern2.ltr.katelugu.below 60; pos @kern1.ltr.ssatelugu.alt @kern2.ltr.katelugu.below 60; } kern_ltr; lookup kern_ltr_marks { pos @kern1.dflt.ssatelugu.alt @kern2.ltr.rVocalicMatratelugu 180; pos @kern1.ltr.ssatelugu.alt @kern2.ltr.rVocalicMatratelugu 180; } kern_ltr_marks; feature kern { script DFLT; language dflt; lookup kern_dflt; lookup kern_ltr; lookup kern_ltr_marks; } kern; feature dist { script tel2; language dflt; lookup kern_dflt; lookup kern_ltr; lookup kern_ltr_marks; script telu; language dflt; lookup kern_dflt; lookup kern_ltr; lookup kern_ltr_marks; } dist; ''' # --- # name: test_dflt_language ''' lookup kern_dflt { lookupflag IgnoreMarks; pos comma comma 2; } kern_dflt; lookup kern_ltr { lookupflag IgnoreMarks; pos a a 1; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_dflt; lookup kern_ltr; language ZND; script latn; language dflt; lookup kern_dflt; lookup kern_ltr; language ANG; } kern; ''' # --- # name: test_dist_LTR ''' @kern1.ltr.KND_aaMatra_R = [aaMatra_kannada]; @kern2.ltr.KND_ailength_L = [aaMatra_kannada]; lookup kern_ltr { lookupflag IgnoreMarks; pos @kern1.ltr.KND_aaMatra_R @kern2.ltr.KND_ailength_L 34; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_ltr; script latn; language dflt; lookup kern_ltr; } kern; feature dist { script knd2; language dflt; lookup kern_ltr; script knda; language dflt; lookup kern_ltr; } dist; ''' # --- # name: test_dist_LTR_and_RTL ''' @kern1.ltr.KND_aaMatra_R = [aaMatra_kannada]; @kern2.ltr.KND_ailength_L = [aaMatra_kannada]; lookup kern_ltr { lookupflag IgnoreMarks; pos @kern1.ltr.KND_aaMatra_R @kern2.ltr.KND_ailength_L 34; } kern_ltr; lookup kern_rtl { lookupflag IgnoreMarks; pos u10A1E u10A06 <117 0 117 0>; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_ltr; } kern; feature dist { script khar; language dflt; lookup kern_rtl; script knd2; language dflt; lookup kern_ltr; script knda; language dflt; lookup kern_ltr; } dist; ''' # --- # name: test_dist_RTL ''' lookup kern_rtl { lookupflag IgnoreMarks; pos u10A1E u10A06 <117 0 117 0>; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_rtl; script arab; language dflt; lookup kern_rtl; } kern; feature dist { script khar; language dflt; lookup kern_rtl; } dist; ''' # --- # name: test_hyphenated_duplicates ''' @kern1.dflt.hyphen = [comma]; @kern1.dflt.hyphen_1 = [period]; lookup kern_dflt { lookupflag IgnoreMarks; enum pos @kern1.dflt.hyphen comma 1; enum pos @kern1.dflt.hyphen_1 period 2; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_ignoreMarks ''' lookup kern_dflt { lookupflag IgnoreMarks; pos four six -55; pos one six -30; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_ignoreMarks.1 ''' lookup kern_dflt { pos four six -55; pos one six -30; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_insert_comment_after ''' feature kern { pos one four' -50 six; # # } kern; lookup kern_dflt { lookupflag IgnoreMarks; pos seven six 25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_insert_comment_after.1 ''' lookup kern_dflt { lookupflag IgnoreMarks; pos seven six 25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_insert_comment_before ''' lookup kern_dflt { lookupflag IgnoreMarks; pos seven six 25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; feature kern { # # pos one four' -50 six; } kern; ''' # --- # name: test_insert_comment_before.1 ''' lookup kern_dflt { lookupflag IgnoreMarks; pos seven six 25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_insert_comment_before_extended ''' lookup kern_dflt { lookupflag IgnoreMarks; pos seven six 25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; feature kern { # # pos one four' -50 six; } kern; ''' # --- # name: test_insert_comment_middle ''' feature kern { pos one four' -50 six; # } kern; lookup kern_dflt { lookupflag IgnoreMarks; pos seven six 25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; feature kern { # pos one six' -50 six; } kern; ''' # --- # name: test_insert_comment_middle.1 ''' lookup kern_dflt { lookupflag IgnoreMarks; pos seven six 25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_kern_LTR_and_RTL ''' @kern1.ltr.A = [A Aacute]; @kern1.rtl.reh = [reh-ar reh-ar.fina zain-ar]; @kern2.rtl.alef = [alef-ar alef-ar.isol]; lookup kern_dflt { pos seven four -25; } kern_dflt; lookup kern_ltr { enum pos @kern1.ltr.A V -40; } kern_ltr; lookup kern_rtl { pos four-ar seven-ar -30; pos reh-ar.fina lam-ar.init <-80 0 -80 0>; pos @kern1.rtl.reh @kern2.rtl.alef <-100 0 -100 0>; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_dflt; lookup kern_ltr; script arab; language dflt; lookup kern_dflt; lookup kern_rtl; language URD; script latn; language dflt; lookup kern_dflt; lookup kern_ltr; language TRK; } kern; ''' # --- # name: test_kern_LTR_and_RTL_with_marks ''' @kern1.ltr.A = [A Aacute]; @kern1.rtl.reh = [reh-ar reh-ar.fina zain-ar]; @kern2.rtl.alef = [alef-ar alef-ar.isol]; lookup kern_dflt { lookupflag IgnoreMarks; pos seven four -25; } kern_dflt; lookup kern_ltr { lookupflag IgnoreMarks; enum pos @kern1.ltr.A V -40; } kern_ltr; lookup kern_ltr_marks { pos V acutecomb 70; } kern_ltr_marks; lookup kern_rtl { lookupflag IgnoreMarks; pos four-ar seven-ar -30; pos reh-ar.fina lam-ar.init <-80 0 -80 0>; pos @kern1.rtl.reh @kern2.rtl.alef <-100 0 -100 0>; } kern_rtl; lookup kern_rtl_marks { pos reh-ar fatha-ar <80 0 80 0>; } kern_rtl_marks; feature kern { script DFLT; language dflt; lookup kern_dflt; lookup kern_ltr; lookup kern_ltr_marks; script arab; language dflt; lookup kern_dflt; lookup kern_rtl; lookup kern_rtl_marks; language URD; script latn; language dflt; lookup kern_dflt; lookup kern_ltr; lookup kern_ltr_marks; language TRK; } kern; ''' # --- # name: test_kern_RTL_and_DFLT_numbers ''' lookup kern_dflt { lookupflag IgnoreMarks; pos seven four -25; } kern_dflt; lookup kern_rtl { lookupflag IgnoreMarks; pos yod-hb bet-hb <-100 0 -100 0>; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_dflt; lookup kern_rtl; script hebr; language dflt; lookup kern_dflt; lookup kern_rtl; } kern; ''' # --- # name: test_kern_RTL_with_marks ''' @kern1.rtl.reh = [reh-ar reh-ar.fina zain-ar]; @kern2.rtl.alef = [alef-ar alef-ar.isol]; lookup kern_rtl { lookupflag IgnoreMarks; pos reh-ar.fina lam-ar.init <-80 0 -80 0>; pos @kern1.rtl.reh @kern2.rtl.alef <-100 0 -100 0>; } kern_rtl; lookup kern_rtl_marks { pos reh-ar fatha-ar <80 0 80 0>; } kern_rtl_marks; feature kern { script DFLT; language dflt; lookup kern_rtl; lookup kern_rtl_marks; script arab; language dflt; lookup kern_rtl; lookup kern_rtl_marks; language ARA; } kern; ''' # --- # name: test_kern_hira_kana_hrkt ''' lookup kern_dflt { lookupflag IgnoreMarks; pos period period 5; } kern_dflt; lookup kern_ltr { lookupflag IgnoreMarks; pos a-hira a-hira 1; pos a-hira a-kana 2; pos a-hira period 6; pos a-kana a-hira 3; pos a-kana a-kana 4; pos a-kana period 8; pos period a-hira 7; pos period a-kana 9; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_dflt; lookup kern_ltr; script kana; language dflt; lookup kern_dflt; lookup kern_ltr; } kern; ''' # --- # name: test_kern_independent_of_languagesystem[same] ''' lookup kern_ltr { lookupflag IgnoreMarks; pos A V -40; } kern_ltr; lookup kern_rtl { lookupflag IgnoreMarks; pos reh-ar alef-ar <-100 0 -100 0>; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_ltr; script arab; language dflt; lookup kern_rtl; script latn; language dflt; lookup kern_ltr; } kern; ''' # --- # name: test_kern_mixed_bidis ''' lookup kern_dflt { lookupflag IgnoreMarks; pos comma comma -1; } kern_dflt; lookup kern_ltr { lookupflag IgnoreMarks; pos a a 1; pos a comma 2; pos comma a 3; } kern_ltr; lookup kern_rtl { lookupflag IgnoreMarks; pos alef-ar alef-ar <4 0 4 0>; pos alef-ar comma-ar <5 0 5 0>; pos comma-ar alef-ar <6 0 6 0>; pos comma-ar one-adlam <12 0 12 0>; pos one-adlam comma-ar <11 0 11 0>; pos one-adlam one-adlam <10 0 10 0>; pos one-ar one-ar 9; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_dflt; lookup kern_ltr; script arab; language dflt; lookup kern_dflt; lookup kern_rtl; script latn; language dflt; lookup kern_dflt; lookup kern_ltr; } kern; feature dist { script adlm; language dflt; lookup kern_dflt; lookup kern_rtl; } dist; ''' # --- # name: test_kern_split_and_drop ''' @kern1.ltr.bar = [a-cy]; @kern1.ltr.bar_1 = [period]; @kern1.ltr.foo = [a a-orya alpha]; @kern2.ltr.bar = [a-cy]; @kern2.ltr.bar_1 = [period]; @kern2.ltr.foo = [a a-orya alpha]; lookup kern_ltr { lookupflag IgnoreMarks; pos @kern1.ltr.foo @kern2.ltr.bar 20; pos @kern1.ltr.foo @kern2.ltr.bar_1 20; pos @kern1.ltr.bar @kern2.ltr.foo 20; pos @kern1.ltr.bar_1 @kern2.ltr.foo 20; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_ltr; script cyrl; language dflt; lookup kern_ltr; script grek; language dflt; lookup kern_ltr; script latn; language dflt; lookup kern_ltr; } kern; feature dist { script ory2; language dflt; lookup kern_ltr; script orya; language dflt; lookup kern_ltr; } dist; ''' # --- # name: test_kern_split_and_drop_mixed ''' @kern1.ltr.foo = [V W]; @kern2.ltr.foo = [W]; lookup kern_ltr { lookupflag IgnoreMarks; pos @kern1.ltr.foo @kern2.ltr.foo -20; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_ltr; script latn; language dflt; lookup kern_ltr; } kern; ''' # --- # name: test_kern_split_multi_glyph_class[same] ''' @kern1.dflt.foo = [period]; @kern1.ltr.foo = [a]; @kern2.dflt.foo = [period]; @kern2.ltr.foo = [b]; lookup kern_dflt { lookupflag IgnoreMarks; pos period period 9; enum pos period @kern2.dflt.foo 13; enum pos @kern1.dflt.foo period 11; pos @kern1.dflt.foo @kern2.dflt.foo 14; } kern_dflt; lookup kern_ltr { lookupflag IgnoreMarks; pos a a 1; pos a b 2; pos a period 3; pos b a 4; pos b b 5; pos b period 6; pos period a 7; pos period b 8; enum pos a @kern2.ltr.foo 12; enum pos a @kern2.dflt.foo 12; enum pos period @kern2.ltr.foo 13; enum pos @kern1.ltr.foo b 10; enum pos @kern1.ltr.foo period 11; enum pos @kern1.dflt.foo b 10; pos @kern1.ltr.foo @kern2.ltr.foo 14; pos @kern1.ltr.foo @kern2.dflt.foo 14; pos @kern1.dflt.foo @kern2.ltr.foo 14; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_dflt; lookup kern_ltr; script latn; language dflt; lookup kern_dflt; lookup kern_ltr; } kern; ''' # --- # name: test_kern_uniqueness ''' @kern1.ltr.questiondown = [questiondown]; @kern2.ltr.y = [y]; lookup kern_ltr { lookupflag IgnoreMarks; pos questiondown y 35; enum pos questiondown @kern2.ltr.y -35; enum pos @kern1.ltr.questiondown y 35; pos @kern1.ltr.questiondown @kern2.ltr.y 15; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_ltr; script latn; language dflt; lookup kern_ltr; } kern; ''' # --- # name: test_kern_zyyy_zinh ''' lookup kern_dflt { lookupflag IgnoreMarks; pos uni0300 uni0300 0; pos uni0310 uni0310 1; pos uni0320 uni0320 2; pos uni0330 uni0330 3; pos uni0640 uni0640 4; pos uni0650 uni0650 5; pos uni0670 uni0670 6; pos uni10100 uni10100 36; pos uni10110 uni10110 37; pos uni10120 uni10120 38; pos uni10130 uni10130 39; pos uni102E0 uni102E0 40; pos uni102F0 uni102F0 41; pos uni1BCA0 uni1BCA0 42; pos uni1CD0 uni1CD0 7; pos uni1CE0 uni1CE0 8; pos uni1CF0 uni1CF0 9; pos uni1D360 uni1D360 43; pos uni1D370 uni1D370 44; pos uni1DC0 uni1DC0 10; pos uni1F250 uni1F250 45; pos uni20F0 uni20F0 11; pos uni2E30 uni2E30 12; pos uni2FF0 uni2FF0 13; pos uni3010 uni3010 14; pos uni3030 uni3030 15; pos uni30A0 uni30A0 16; pos uni3190 uni3190 17; pos uni31C0 uni31C0 18; pos uni31D0 uni31D0 19; pos uni31E0 uni31E0 20; pos uni3220 uni3220 21; pos uni3230 uni3230 22; pos uni3240 uni3240 23; pos uni3280 uni3280 24; pos uni3290 uni3290 25; pos uni32A0 uni32A0 26; pos uni32B0 uni32B0 27; pos uni32C0 uni32C0 28; pos uni3360 uni3360 29; pos uni3370 uni3370 30; pos uni33E0 uni33E0 31; pos uni33F0 uni33F0 32; pos uniA700 uniA700 33; pos uniA830 uniA830 34; pos uniFF70 uniFF70 35; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; script grek; language dflt; lookup kern_dflt; script hani; language dflt; lookup kern_dflt; script kana; language dflt; lookup kern_dflt; } kern; feature dist { script dev2; language dflt; lookup kern_dflt; script deva; language dflt; lookup kern_dflt; script dupl; language dflt; lookup kern_dflt; } dist; ''' # --- # name: test_mark_base_kerning ''' @kern1.ltr.etamil = [va-tamil]; @kern1.ltr.etamil_1 = [aulengthmark-tamil]; @kern2.ltr.etamil = [va-tamil]; @kern2.ltr.etamil_1 = [aulengthmark-tamil]; lookup kern_ltr { lookupflag IgnoreMarks; pos aa-tamil va-tamil -20; pos va-tamil aa-tamil -20; enum pos aa-tamil @kern2.ltr.etamil -35; enum pos @kern1.ltr.etamil aa-tamil -35; pos @kern1.ltr.etamil @kern2.ltr.etamil -100; } kern_ltr; lookup kern_ltr_marks { pos aulengthmark-tamil aulengthmark-tamil -200; enum pos aa-tamil @kern2.ltr.etamil_1 -35; enum pos @kern1.ltr.etamil_1 aa-tamil -35; pos @kern1.ltr.etamil_1 @kern2.ltr.etamil_1 -100; pos @kern1.ltr.etamil_1 @kern2.ltr.etamil -100; pos @kern1.ltr.etamil @kern2.ltr.etamil_1 -100; } kern_ltr_marks; feature kern { script DFLT; language dflt; lookup kern_ltr; lookup kern_ltr_marks; } kern; feature dist { script tml2; language dflt; lookup kern_ltr; lookup kern_ltr_marks; script taml; language dflt; lookup kern_ltr; lookup kern_ltr_marks; } dist; ''' # --- # name: test_mark_to_base_kern ''' lookup kern_ltr { lookupflag IgnoreMarks; pos B C -30; } kern_ltr; lookup kern_ltr_marks { pos A acutecomb -55; } kern_ltr_marks; feature kern { script DFLT; language dflt; lookup kern_ltr; lookup kern_ltr_marks; script latn; language dflt; lookup kern_ltr; lookup kern_ltr_marks; } kern; ''' # --- # name: test_mark_to_base_kern.1 ''' lookup kern_ltr { pos A acutecomb -55; pos B C -30; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_ltr; script latn; language dflt; lookup kern_ltr; } kern; ''' # --- # name: test_mark_to_base_only ''' lookup kern_dflt_marks { pos A acutecomb -55; } kern_dflt_marks; feature kern { script DFLT; language dflt; lookup kern_dflt_marks; } kern; ''' # --- # name: test_mode.1 ''' feature kern { pos one four' -50 six; } kern; lookup kern_dflt { lookupflag IgnoreMarks; pos seven six 25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_mode[existing] ''' feature kern { pos one four' -50 six; } kern; ''' # --- # name: test_quantize ''' lookup kern_dflt { lookupflag IgnoreMarks; pos four six -55; pos one six -25; } kern_dflt; feature kern { script DFLT; language dflt; lookup kern_dflt; } kern; ''' # --- # name: test_skip_spacing_marks ''' lookup kern_ltr { @MFS_kern_ltr = [highspacingdot-deva]; lookupflag UseMarkFilteringSet @MFS_kern_ltr; pos ka-deva ra-deva -250; pos ra-deva ka-deva -250; } kern_ltr; lookup kern_ltr_marks { pos highspacingdot-deva ka-deva -200; pos ka-deva highspacingdot-deva -150; } kern_ltr_marks; feature kern { script DFLT; language dflt; lookup kern_ltr; lookup kern_ltr_marks; } kern; feature dist { script dev2; language dflt; lookup kern_ltr; lookup kern_ltr_marks; script deva; language dflt; lookup kern_ltr; lookup kern_ltr_marks; } dist; ''' # --- # name: test_skip_zero_class_kerns ''' @kern1.ltr.baz = [E F]; @kern1.ltr.foo = [A B]; @kern2.ltr.bar = [C D]; lookup kern_ltr { lookupflag IgnoreMarks; pos G H -5; enum pos A @kern2.ltr.bar 5; enum pos @kern1.ltr.foo D 15; pos @kern1.ltr.foo @kern2.ltr.bar 10; pos @kern1.ltr.baz @kern2.ltr.bar -10; } kern_ltr; feature kern { script DFLT; language dflt; lookup kern_ltr; script latn; language dflt; lookup kern_ltr; } kern; ''' # --- ufo2ft-3.3.1/tests/featureWriters/ast_test.py000066400000000000000000000015451470175262700213200ustar00rootroot00000000000000from io import StringIO from fontTools.feaLib.parser import Parser from ufo2ft.featureWriters import ast def test_iterClassDefinitions(): text = """ @TopLevelClass = [a b c]; lookup test_lookup { @LookupLevelClass = [d e f]; } test_lookup; feature test { @FeatureLevelClass = [g h i]; lookup test_nested_lookup { @NestedLookupLevelClass = [j k l]; } test_nested_lookup; } test; """ glyph_names = "a b c d e f g h i j k l".split() feature_file = StringIO(text) p = Parser(feature_file, glyph_names) doc = p.parse() class_defs = ast.iterClassDefinitions(doc) result = [cd.name for cd in class_defs] expected = [ "TopLevelClass", "LookupLevelClass", "FeatureLevelClass", "NestedLookupLevelClass", ] assert result == expected ufo2ft-3.3.1/tests/featureWriters/cursFeatureWriter_test.py000066400000000000000000000376751470175262700242330ustar00rootroot00000000000000from textwrap import dedent import pytest from ufo2ft.featureWriters.cursFeatureWriter import CursFeatureWriter from . import FeatureWriterTest @pytest.fixture def testufo(FontClass): ufo = FontClass() ufo.newGlyph("a").appendAnchor({"name": "exit", "x": 100, "y": 200}) glyph = ufo.newGlyph("b") glyph.appendAnchor({"name": "entry", "x": 0, "y": 200}) glyph.appendAnchor({"name": "exit", "x": 111, "y": 200}) ufo.newGlyph("c").appendAnchor({"name": "entry", "x": 100, "y": 200}) return ufo class CursFeatureWriterTest(FeatureWriterTest): FeatureWriter = CursFeatureWriter def test_curs_feature(self, testufo): generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs { lookupflag RightToLeft IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; } curs; } curs; """ ) def test_curs_feature_LTR(self, testufo): testufo["a"].unicode = ord("a") testufo["b"].unicode = ord("b") testufo["c"].unicode = ord("c") generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs_ltr { lookupflag IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; } curs_ltr; } curs; """ ) def test_curs_feature_mixed(self, testufo): testufo["a"].unicode = ord("a") testufo["b"].unicode = ord("b") testufo["c"].unicode = ord("c") glyph = testufo.newGlyph("a.swsh") glyph.appendAnchor({"name": "entry", "x": 100, "y": 200}) glyph = testufo.newGlyph("alef") glyph.unicode = 0x0627 glyph = testufo.newGlyph("alef.fina") glyph.appendAnchor({"name": "entry", "x": 300, "y": 10}) glyph = testufo.newGlyph("meem") glyph.unicode = 0x0645 glyph = testufo.newGlyph("meem.init") glyph.appendAnchor({"name": "exit", "x": 0, "y": 10}) glyph = testufo.newGlyph("meem.medi") glyph.appendAnchor({"name": "entry", "x": 500, "y": 10}) glyph.appendAnchor({"name": "exit", "x": 0, "y": 10}) glyph = testufo.newGlyph("meem.fina") glyph.appendAnchor({"name": "entry", "x": 500, "y": 10}) testufo.features.text = dedent( """\ feature swsh { sub a by a.swsh; } swsh; feature init { sub meem by meem.init; } init; feature medi { sub meem by meem.medi; } medi; feature fina { sub alef by alef.fina; sub meem by meem.fina; } fina; """ ) testufo.lib["public.glyphOrder"] = [ "a", "b", "c", "a.swsh", "alef", "alef.fina", "meem", "meem.init", "meem.medi", "meem.fina", ] generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs_ltr { lookupflag IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; pos cursive a.swsh ; } curs_ltr; lookup curs_rtl { lookupflag RightToLeft IgnoreMarks; pos cursive alef.fina ; pos cursive meem.init ; pos cursive meem.medi ; pos cursive meem.fina ; } curs_rtl; } curs; """ ) def test_curs_feature_multiple_anchors(self, testufo): glyph = testufo.newGlyph("d") glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) glyph = testufo.newGlyph("e") glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) glyph = testufo.newGlyph("f") glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) glyph.appendAnchor({"name": "exit.2", "x": 0, "y": 400}) glyph = testufo.newGlyph("g") glyph.appendAnchor({"name": "entry.2", "x": 100, "y": 200}) generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs { lookupflag RightToLeft IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; } curs; lookup curs_1 { lookupflag RightToLeft IgnoreMarks; pos cursive d ; pos cursive e ; pos cursive f ; } curs_1; lookup curs_2 { lookupflag RightToLeft IgnoreMarks; pos cursive f ; pos cursive g ; } curs_2; } curs; """ ) def test_curs_feature_multiple_anchors_LTR(self, testufo): testufo["a"].unicode = ord("a") testufo["b"].unicode = ord("b") testufo["c"].unicode = ord("c") glyph = testufo.newGlyph("d") glyph.unicode = ord("d") glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) glyph = testufo.newGlyph("e") glyph.unicode = ord("e") glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) glyph = testufo.newGlyph("f") glyph.unicode = ord("f") glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) glyph.appendAnchor({"name": "exit.2", "x": 0, "y": 400}) glyph = testufo.newGlyph("g") glyph.unicode = ord("g") glyph.appendAnchor({"name": "entry.2", "x": 100, "y": 200}) generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs_ltr { lookupflag IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; } curs_ltr; lookup curs_1_ltr { lookupflag IgnoreMarks; pos cursive d ; pos cursive e ; pos cursive f ; } curs_1_ltr; lookup curs_2_ltr { lookupflag IgnoreMarks; pos cursive f ; pos cursive g ; } curs_2_ltr; } curs; """ ) def test_curs_feature_multiple_anchors_mixed(self, testufo): testufo["a"].unicode = ord("a") testufo["b"].unicode = ord("b") testufo["c"].unicode = ord("c") glyph = testufo.newGlyph("d") glyph.unicode = ord("d") glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) glyph = testufo.newGlyph("e") glyph.unicode = ord("e") glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) glyph = testufo.newGlyph("f") glyph.unicode = ord("f") glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) glyph.appendAnchor({"name": "exit.2", "x": 0, "y": 400}) glyph = testufo.newGlyph("g") glyph.unicode = ord("g") glyph.appendAnchor({"name": "entry.2", "x": 100, "y": 200}) glyph = testufo.newGlyph("alef-ar") glyph.appendAnchor({"name": "entry", "x": 100, "y": 200}) glyph.appendAnchor({"name": "exit", "x": 0, "y": 200}) glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) glyph = testufo.newGlyph("beh-ar") glyph.unicode = 0x0628 glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 200}) glyph.appendAnchor({"name": "exit.2", "x": 0, "y": 100}) glyph = testufo.newGlyph("hah-ar") glyph.unicode = 0x0647 glyph.appendAnchor({"name": "entry", "x": 100, "y": 100}) glyph.appendAnchor({"name": "entry.2", "x": 100, "y": 200}) generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs_ltr { lookupflag IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; } curs_ltr; lookup curs_rtl { lookupflag RightToLeft IgnoreMarks; pos cursive alef-ar ; pos cursive hah-ar ; } curs_rtl; lookup curs_1_ltr { lookupflag IgnoreMarks; pos cursive d ; pos cursive e ; pos cursive f ; } curs_1_ltr; lookup curs_1_rtl { lookupflag RightToLeft IgnoreMarks; pos cursive alef-ar ; pos cursive beh-ar ; } curs_1_rtl; lookup curs_2_ltr { lookupflag IgnoreMarks; pos cursive f ; pos cursive g ; } curs_2_ltr; lookup curs_2_rtl { lookupflag RightToLeft IgnoreMarks; pos cursive beh-ar ; pos cursive hah-ar ; } curs_2_rtl; } curs; """ ) def test_curs_feature_forced_RTL(self, testufo): for c in ("a", "b", "c"): g = testufo[c] g.unicode = ord(c) anchors = list(g.anchors) g.anchors = [] for a in anchors: g.appendAnchor({"name": a.name + ".RTL", "x": a.x, "y": a.y}) generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs_RTL { lookupflag RightToLeft IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; } curs_RTL; } curs; """ ) def test_curs_feature_forced_LTR(self, testufo): for n, u in (("a", 0x0627), ("b", 0x0628), ("c", 0x062C)): g = testufo[n] g.unicode = u anchors = list(g.anchors) g.anchors = [] for a in anchors: g.appendAnchor({"name": a.name + ".LTR", "x": a.x, "y": a.y}) generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs_LTR { lookupflag IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; } curs_LTR; } curs; """ ) def test_curs_feature_mixed_forced_direction(self, testufo): testufo["a"].unicode = ord("a") testufo["b"].unicode = ord("b") testufo["c"].unicode = ord("c") glyph = testufo.newGlyph("d") glyph.unicode = ord("d") glyph.appendAnchor({"name": "exit.RTL", "x": 110, "y": 210}) glyph = testufo.newGlyph("e") glyph.unicode = ord("e") glyph.appendAnchor({"name": "entry.RTL", "x": 10, "y": 210}) glyph.appendAnchor({"name": "exit.RTL", "x": 121, "y": 210}) glyph = testufo.newGlyph("f") glyph.unicode = ord("f") glyph.appendAnchor({"name": "entry.RTL", "x": 110, "y": 210}) glyph = testufo.newGlyph("alef") glyph.unicode = 0x0627 glyph.appendAnchor({"name": "entry", "x": 100, "y": 200}) glyph = testufo.newGlyph("beh") glyph.unicode = 0x0628 glyph.appendAnchor({"name": "entry", "x": 0, "y": 200}) glyph.appendAnchor({"name": "exit", "x": 111, "y": 200}) glyph = testufo.newGlyph("jeem") glyph.unicode = 0x062C glyph.appendAnchor({"name": "entry", "x": 100, "y": 200}) glyph = testufo.newGlyph("heh") glyph.unicode = 0x0647 glyph.appendAnchor({"name": "entry.LTR", "x": 110, "y": 210}) glyph = testufo.newGlyph("waw") glyph.unicode = 0x0648 glyph.appendAnchor({"name": "exit.LTR", "x": 10, "y": 210}) glyph.appendAnchor({"name": "exit.LTR", "x": 121, "y": 210}) glyph = testufo.newGlyph("zain") glyph.unicode = 0x0632 glyph.appendAnchor({"name": "entry.LTR", "x": 110, "y": 210}) generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ feature curs { lookup curs_ltr { lookupflag IgnoreMarks; pos cursive a ; pos cursive b ; pos cursive c ; } curs_ltr; lookup curs_rtl { lookupflag RightToLeft IgnoreMarks; pos cursive alef ; pos cursive beh ; pos cursive jeem ; } curs_rtl; lookup curs_LTR { lookupflag IgnoreMarks; pos cursive heh ; pos cursive waw ; pos cursive zain ; } curs_LTR; lookup curs_RTL { lookupflag RightToLeft IgnoreMarks; pos cursive d ; pos cursive e ; pos cursive f ; } curs_RTL; } curs; """ ) ufo2ft-3.3.1/tests/featureWriters/featureWriters_test.py000066400000000000000000000045661470175262700235520ustar00rootroot00000000000000from ufo2ft.featureWriters import ( FEATURE_WRITERS_KEY, BaseFeatureWriter, loadFeatureWriterFromString, loadFeatureWriters, ) try: from plistlib import FMT_XML, loads def readPlistFromString(s): return loads(s, fmt=FMT_XML) except ImportError: from plistlib import readPlistFromString import pytest from ..testSupport import _TempModule TEST_LIB_PLIST = readPlistFromString( b""" com.github.googlei18n.ufo2ft.featureWriters class KernFeatureWriter options mode skip """ ) class FooBarWriter(BaseFeatureWriter): tableTag = "GSUB" def __init__(self, **kwargs): pass def write(self, font, feaFile, compiler=None): return False @pytest.fixture(scope="module", autouse=True) def customWriterModule(): """Make a temporary 'myFeatureWriters' module containing a 'FooBarWriter' class for testing the wruter loading machinery. """ with _TempModule("myFeatureWriters") as temp_module: temp_module.module.__dict__["FooBarWriter"] = FooBarWriter yield VALID_SPEC_LISTS = [ [{"class": "KernFeatureWriter"}], [ {"class": "KernFeatureWriter", "options": {"ignoreMarks": False}}, {"class": "MarkFeatureWriter", "options": {"features": ["mark"]}}, ], [{"class": "FooBarWriter", "module": "myFeatureWriters", "options": {"a": 1}}], TEST_LIB_PLIST[FEATURE_WRITERS_KEY], ] @pytest.mark.parametrize("specList", VALID_SPEC_LISTS) def test_loadFeatureWriters_valid(specList, FontClass): ufo = FontClass() ufo.lib[FEATURE_WRITERS_KEY] = specList for writer in loadFeatureWriters(ufo, ignoreErrors=False): assert writer.tableTag in {"GSUB", "GPOS"} assert callable(writer.write) VALID_SPEC_STRINGS = [ "KernFeatureWriter", "KernFeatureWriter(ignoreMarks=False)", "MarkFeatureWriter(features=['mark'])", "myFeatureWriters::FooBarWriter(a=1)", ] @pytest.mark.parametrize("spec", VALID_SPEC_STRINGS) def test_loadFeatureWriterFromString_valid(spec, FontClass): writer = loadFeatureWriterFromString(spec) assert writer.tableTag in {"GSUB", "GPOS"} assert callable(writer.write) ufo2ft-3.3.1/tests/featureWriters/gdefFeatureWriter_test.py000066400000000000000000000173611470175262700241520ustar00rootroot00000000000000import logging from textwrap import dedent import pytest from ufo2ft.featureCompiler import parseLayoutFeatures from ufo2ft.featureWriters import GdefFeatureWriter from . import FeatureWriterTest @pytest.fixture def testufo(FontClass): ufo = FontClass() ufo.newGlyph("a") ufo.newGlyph("f") ufo.newGlyph("f.component") ufo.newGlyph("i") liga = ufo.newGlyph("f_f_i") liga.appendAnchor({"name": "caret_2", "x": 400, "y": 0}) liga.appendAnchor({"name": "caret_1", "x": 200, "y": 0}) liga = ufo.newGlyph("f_i") liga.appendAnchor({"name": "caret_", "x": 200, "y": 0}) ufo.newGlyph("acutecomb") ufo.newGlyph("tildecomb") ufo.lib["public.glyphOrder"] = [ "a", "f", "f.component", "i", "f_f_i", "f_i", "acutecomb", "tildecomb", ] return ufo class GdefFeatureWriterTest(FeatureWriterTest): FeatureWriter = GdefFeatureWriter @classmethod def writeGDEF(cls, ufo, **kwargs): writer = cls.FeatureWriter(**kwargs) feaFile = parseLayoutFeatures(ufo) if writer.write(ufo, feaFile): return feaFile def test_no_GDEF_no_openTypeCategories_in_font(self, testufo): newFea = self.writeGDEF(testufo) assert str(newFea) == dedent( """\ table GDEF { LigatureCaretByPos f_f_i 200 400; LigatureCaretByPos f_i 200; } GDEF; """ ) def test_GDEF_in_font(self, testufo): testufo.features.text = dedent( """\ table GDEF { GlyphClassDef [a], [], [acutecomb], []; LigatureCaretByPos f_i 300; } GDEF; """ ) assert self.writeGDEF(testufo) is None def test_openTypeCategories_in_font(self, testufo): testufo.lib["public.openTypeCategories"] = { "a": "base", "f.component": "component", "f_i": "ligature", "acutecomb": "mark", } newFea = self.writeGDEF(testufo) assert str(newFea) == dedent( """\ table GDEF { GlyphClassDef [a], [f_i], [acutecomb], [f.component]; LigatureCaretByPos f_f_i 200 400; LigatureCaretByPos f_i 200; } GDEF; """ ) def test_GDEF_and_openTypeCategories_in_font(self, testufo): testufo.lib["public.openTypeCategories"] = { "a": "base", "f.component": "component", "f_i": "ligature", "acutecomb": "mark", } testufo.features.text = dedent( """\ table GDEF { GlyphClassDef [i], [], [tildecomb], []; LigatureCaretByPos f_i 100; } GDEF; """ ) assert self.writeGDEF(testufo) is None def test_GDEF_LigatureCarets_and_openTypeCategories_in_font(self, testufo): testufo.lib["public.openTypeCategories"] = { "a": "base", "f.component": "component", "f_i": "ligature", "acutecomb": "mark", } testufo.features.text = dedent( """\ table GDEF { LigatureCaretByPos f_i 100; } GDEF; """ ) newFea = self.writeGDEF(testufo) assert str(newFea) == dedent( """\ table GDEF { LigatureCaretByPos f_i 100; GlyphClassDef [a], [f_i], [acutecomb], [f.component]; } GDEF; """ ) def test_GDEF_GlyphClassDef_and_carets_in_font(self, testufo): testufo.lib["public.openTypeCategories"] = { "a": "base", "f.component": "component", "f_i": "ligature", "acutecomb": "mark", } testufo.features.text = dedent( """\ table GDEF { GlyphClassDef [], [], [acutecomb tildecomb], []; } GDEF; """ ) newFea = self.writeGDEF(testufo) assert str(newFea) == dedent( """\ table GDEF { GlyphClassDef [], [], [acutecomb tildecomb], []; LigatureCaretByPos f_f_i 200 400; LigatureCaretByPos f_i 200; } GDEF; """ ) def test_mark_and_openTypeCategories_in_font(self, testufo): testufo.lib["public.openTypeCategories"] = { "a": "base", "f.component": "component", "f_f_i": "base", "f_i": "ligature", "acutecomb": "mark", "tildecomb": "component", } testufo.features.text = old = dedent( """\ feature mark { markClass tildecomb @TOP_MARKS; pos base a mark @TOP_MARKS; pos base f mark @TOP_MARKS; pos ligature f_f_i mark @TOP_MARKS ligComponent mark @TOP_MARKS ligComponent mark @TOP_MARKS; } mark; """ ) newFea = self.writeGDEF(testufo) assert str(newFea) == old + "\n" + dedent( """\ table GDEF { GlyphClassDef [a f_f_i], [f_i], [acutecomb], [f.component tildecomb]; LigatureCaretByPos f_f_i 200 400; LigatureCaretByPos f_i 200; } GDEF; """ ) def test_vertical_carets(self, testufo): vliga = testufo.newGlyph("vi_li_ga") vliga.appendAnchor({"name": "vcaret_1", "x": 0, "y": 100}) vliga.appendAnchor({"name": "vcaret_2", "x": 0, "y": 200}) vliga = testufo.newGlyph("vli_ga") vliga.appendAnchor({"name": "vcaret_", "x": 0, "y": 100}) newFea = self.writeGDEF(testufo) assert str(newFea) == dedent( """\ table GDEF { LigatureCaretByPos f_f_i 200 400; LigatureCaretByPos f_i 200; LigatureCaretByPos vi_li_ga 100 200; LigatureCaretByPos vli_ga 100; } GDEF; """ ) def test_floaty_carets(self, testufo): # Some Glyphs sources happen to contain fractional caret positions. # In the Adobe feature file syntax (and binary OpenType GDEF tables), # caret positions must be integers. liga = testufo.newGlyph("li_ga") liga.appendAnchor({"name": "vcaret_1", "x": 0, "y": 200.1111}) liga.appendAnchor({"name": "caret_1", "x": 499.9876, "y": 0}) newFea = self.writeGDEF(testufo) assert str(newFea) == dedent( """\ table GDEF { LigatureCaretByPos f_f_i 200 400; LigatureCaretByPos f_i 200; LigatureCaretByPos li_ga 200 500; } GDEF; """ ) def test_getOpenTypeCategories_invalid(self, testufo, caplog): caplog.set_level(logging.WARNING) testufo.lib["public.openTypeCategories"] = { "a": "base", "f.component": "component", "f_f_i": "base", "f_i": "ligature", "acutecomb": "mark", "tildecomb": "components", } logger = "ufo2ft.featureWriters.gdefFeatureWriter.GdefFeatureWriter" with caplog.at_level(logging.WARNING, logger=logger): self.writeGDEF(testufo) assert len(caplog.records) == 1 assert "The 'public.openTypeCategories' value of tildecomb in" in caplog.text assert "is 'components' when it should be" in caplog.text ufo2ft-3.3.1/tests/featureWriters/kernFeatureWriter2_test.py000066400000000000000000001064421470175262700242650ustar00rootroot00000000000000import logging import fontTools.feaLib.ast as fea_ast import pytest from fontTools import unicodedata from syrupy.extensions.amber import AmberSnapshotExtension from syrupy.location import PyTestLocation from syrupy.types import SnapshotIndex from ufo2ft.constants import UNICODE_SCRIPT_ALIASES from ufo2ft.featureCompiler import FeatureCompiler, parseLayoutFeatures from ufo2ft.featureWriters.kernFeatureWriter2 import KernFeatureWriter from ufo2ft.util import DFLT_SCRIPTS, unicodeScriptExtensions from . import FeatureWriterTest class KernFeatureWriterTest(FeatureWriterTest): FeatureWriter = KernFeatureWriter class SameUfoLibResultsExtension(AmberSnapshotExtension): """Make tests use the same snapshots when parameterized. Instead of having the snapshots of "test_something[defcon]" and "test_something[ufoLib2]" be duplicates, use the same snapshots for both, because the UFO library shouldn't make a difference. """ @classmethod def get_snapshot_name( cls, *, test_location: "PyTestLocation", index: "SnapshotIndex" ) -> str: index_suffix = "" if isinstance(index, (str,)): index_suffix = f"[{index}]" elif index: index_suffix = f".{index}" return f"{test_location.methodname}{index_suffix}" @pytest.fixture def snapshot(snapshot): return snapshot.use_extension(SameUfoLibResultsExtension) def makeUFO(cls, glyphMap, groups=None, kerning=None, features=None): ufo = cls() for name, uni in glyphMap.items(): glyph = ufo.newGlyph(name) if isinstance(uni, (list, tuple)): glyph.unicodes = uni elif uni is not None: glyph.unicode = uni if groups is not None: ufo.groups.update(groups) if kerning is not None: ufo.kerning.update(kerning) if features is not None: ufo.features.text = features return ufo def getClassDefs(feaFile): return [ s for s in feaFile.statements if isinstance(s, fea_ast.GlyphClassDefinition) ] def getGlyphs(classDef): return [str(g) for g in classDef.glyphs.glyphSet()] def getLookups(feaFile): return [s for s in feaFile.statements if isinstance(s, fea_ast.LookupBlock)] def getPairPosRules(lookup): return [s for s in lookup.statements if isinstance(s, fea_ast.PairPosStatement)] def test_cleanup_missing_glyphs(FontClass): groups = { "public.kern1.A": ["A", "Aacute", "Abreve", "Acircumflex"], "public.kern2.B": ["B", "D", "E", "F"], "public.kern1.C": ["foobar"], } kerning = { ("public.kern1.A", "public.kern2.B"): 10, ("public.kern1.A", "baz"): -25, ("baz", "public.kern2.B"): -20, ("public.kern1.C", "public.kern2.B"): 20, } ufo = FontClass() exclude = {"Abreve", "D", "foobar"} for glyphs in groups.values(): for glyph in glyphs: if glyph in exclude: continue ufo.newGlyph(glyph) ufo.groups.update(groups) ufo.kerning.update(kerning) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) writer.write(ufo, feaFile) classDefs = getClassDefs(feaFile) assert len(classDefs) == 2 assert classDefs[0].name == "kern1.dflt.A" assert classDefs[1].name == "kern2.dflt.B" assert getGlyphs(classDefs[0]) == ["A", "Aacute", "Acircumflex"] assert getGlyphs(classDefs[1]) == ["B", "E", "F"] lookups = getLookups(feaFile) assert len(lookups) == 1 kern_lookup = lookups[0] # We have no codepoints defined for these, so they're considered common assert kern_lookup.name == "kern_dflt" rules = getPairPosRules(kern_lookup) assert len(rules) == 1 assert str(rules[0]) == "pos @kern1.dflt.A @kern2.dflt.B 10;" def test_ignoreMarks(snapshot, FontClass): font = FontClass() for name in ("one", "four", "six"): font.newGlyph(name) font.kerning.update({("four", "six"): -55.0, ("one", "six"): -30.0}) # default is ignoreMarks=True writer = KernFeatureWriter() feaFile = fea_ast.FeatureFile() assert writer.write(font, feaFile) assert feaFile.asFea() == snapshot writer = KernFeatureWriter(ignoreMarks=False) feaFile = fea_ast.FeatureFile() assert writer.write(font, feaFile) assert feaFile.asFea() == snapshot def test_mark_to_base_kern(snapshot, FontClass): font = FontClass() for name in ("A", "B", "C"): font.newGlyph(name).unicode = ord(name) font.newGlyph("acutecomb").unicode = 0x0301 font.kerning.update({("A", "acutecomb"): -55.0, ("B", "C"): -30.0}) font.features.text = """\ @Bases = [A B C]; @Marks = [acutecomb]; table GDEF { GlyphClassDef @Bases, [], @Marks, ; } GDEF; """ # default is ignoreMarks=True feaFile = KernFeatureWriterTest.writeFeatures(font) assert feaFile.asFea() == snapshot feaFile = KernFeatureWriterTest.writeFeatures(font, ignoreMarks=False) assert feaFile.asFea() == snapshot def test_mark_to_base_only(snapshot, FontClass): font = FontClass() for name in ("A", "B", "C"): font.newGlyph(name) font.newGlyph("acutecomb").unicode = 0x0301 font.kerning.update({("A", "acutecomb"): -55.0}) font.features.text = """\ @Bases = [A B C]; @Marks = [acutecomb]; table GDEF { GlyphClassDef @Bases, [], @Marks, ; } GDEF; """ # default is ignoreMarks=True feaFile = KernFeatureWriterTest.writeFeatures(font) assert feaFile.asFea() == snapshot def test_mode(snapshot, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = """\ feature kern { pos one four' -50 six; } kern; """ ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() # default mode="skip" feaFile = parseLayoutFeatures(ufo) assert not writer.write(ufo, feaFile) assert str(feaFile) == snapshot(name="existing") # pass optional "append" mode writer = KernFeatureWriter(mode="append") feaFile = parseLayoutFeatures(ufo) assert writer.write(ufo, feaFile) assert feaFile.asFea() == snapshot # pass "skip" mode explicitly writer = KernFeatureWriter(mode="skip") feaFile = parseLayoutFeatures(ufo) assert not writer.write(ufo, feaFile) assert feaFile.asFea() == snapshot(name="existing") def test_insert_comment_before(snapshot, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = """\ feature kern { # # Automatic Code # pos one four' -50 six; } kern; """ ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) assert writer.write(ufo, feaFile) assert feaFile.asFea() == snapshot # test append mode ignores insert marker generated = KernFeatureWriterTest.writeFeatures(ufo, mode="append") assert generated.asFea() == snapshot def test_comment_wrong_case_or_missing(snapshot, FontClass, caplog): ufo = FontClass() for name in ("a", "b"): ufo.newGlyph(name) ufo.kerning.update({("a", "b"): 25.0}) ufo.features.text = ( """ feature kern { # Automatic code } kern; """ ).strip() with caplog.at_level(logging.WARNING): compiler = FeatureCompiler(ufo, featureWriters=[KernFeatureWriter]) font = compiler.compile() # We mis-cased the insertion marker above, so it's ignored and we end up # with an empty `kern` block overriding the other kerning in the font # source and therefore no `GPOS` table. assert "miscased" in caplog.text assert "Dropping the former" in caplog.text assert "GPOS" not in font # Append mode ignores insertion markers and so should not log warnings # and have kerning in the final font. caplog.clear() with caplog.at_level(logging.WARNING): compiler = FeatureCompiler( ufo, featureWriters=[KernFeatureWriter(mode="append")] ) font = compiler.compile() assert not caplog.text assert "GPOS" in font def test_insert_comment_before_extended(snapshot, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = """\ feature kern { # # Automatic Code End # pos one four' -50 six; } kern; """ ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) assert writer.write(ufo, feaFile) assert feaFile.asFea() == snapshot def test_insert_comment_after(snapshot, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = """\ feature kern { pos one four' -50 six; # # Automatic Code # } kern; """ ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) assert writer.write(ufo, feaFile) assert feaFile.asFea() == snapshot # test append mode ignores insert marker generated = KernFeatureWriterTest.writeFeatures(ufo, mode="append") assert generated.asFea() == snapshot def test_insert_comment_middle(snapshot, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = """\ feature kern { pos one four' -50 six; # # Automatic Code # pos one six' -50 six; } kern; """ ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) writer.write(ufo, feaFile) assert str(feaFile) == snapshot # test append mode ignores insert marker generated = KernFeatureWriterTest.writeFeatures(ufo, mode="append") assert generated.asFea() == snapshot def test_arabic_numerals(snapshot, FontClass): """Test that arabic numerals (with bidi type AN) are kerned LTR. See: * https://github.com/googlei18n/ufo2ft/issues/198 * https://github.com/googlei18n/ufo2ft/pull/200 Additionally, some Arabic numerals are used in more than one script. One approach is to look at other glyphs with distinct script associations and consider the font to be supporting those. """ ufo = FontClass() for name, code in [("four-ar", 0x664), ("seven-ar", 0x667)]: glyph = ufo.newGlyph(name) glyph.unicode = code ufo.kerning.update({("four-ar", "seven-ar"): -30}) generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot ufo.newGlyph("alef-ar").unicode = 0x627 generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot ufo.features.text = """ languagesystem DFLT dflt; languagesystem Thaa dflt; """ generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot del ufo["alef-ar"] generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot def test_skip_zero_class_kerns(snapshot, FontClass): glyphs = { "A": ord("A"), "B": ord("B"), "C": ord("C"), "D": ord("D"), "E": ord("E"), "F": ord("F"), "G": ord("G"), "H": ord("H"), } groups = { "public.kern1.foo": ["A", "B"], "public.kern2.bar": ["C", "D"], "public.kern1.baz": ["E", "F"], "public.kern2.nul": ["G", "H"], } kerning = { ("public.kern1.foo", "public.kern2.bar"): 10, ("public.kern1.baz", "public.kern2.bar"): -10, ("public.kern1.foo", "D"): 15, ("A", "public.kern2.bar"): 5, ("G", "H"): -5, # class-class zero-value pairs are skipped ("public.kern1.foo", "public.kern2.nul"): 0, } ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot def test_kern_uniqueness(snapshot, FontClass): glyphs = { ".notdef": None, "questiondown": 0xBF, "y": 0x79, } groups = { "public.kern1.questiondown": ["questiondown"], "public.kern2.y": ["y"], } kerning = { ("public.kern1.questiondown", "public.kern2.y"): 15, ("public.kern1.questiondown", "y"): 35, ("questiondown", "public.kern2.y"): -35, ("questiondown", "y"): 35, } ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) # The final kerning value for questiondown, y is 35 and all variants # must be present. Ensures the uniqueness filter doesn't filter things # out. assert newFeatures.asFea() == snapshot def test_kern_LTR_and_RTL(snapshot, FontClass): glyphs = { ".notdef": None, "four": 0x34, "seven": 0x37, "A": 0x41, "V": 0x56, "Aacute": 0xC1, "alef-ar": 0x627, "reh-ar": 0x631, "zain-ar": 0x632, "lam-ar": 0x644, "four-ar": 0x664, "seven-ar": 0x667, # # we also add glyphs without unicode codepoint, but linked to # # an encoded 'character' glyph by some GSUB rule "alef-ar.isol": None, "lam-ar.init": None, "reh-ar.fina": None, } groups = { "public.kern1.A": ["A", "Aacute"], "public.kern1.reh": ["reh-ar", "zain-ar", "reh-ar.fina"], "public.kern2.alef": ["alef-ar", "alef-ar.isol"], } kerning = { ("public.kern1.A", "V"): -40, ("seven", "four"): -25, ("reh-ar.fina", "lam-ar.init"): -80, ("public.kern1.reh", "public.kern2.alef"): -100, ("four-ar", "seven-ar"): -30, } features = """\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem latn TRK; languagesystem arab dflt; languagesystem arab URD; feature init { script arab; sub lam-ar by lam-ar.init; language URD; } init; feature fina { script arab; sub reh-ar by reh-ar.fina; language URD; } fina; feature isol { script arab; sub alef-ar by alef-ar.isol; } isol; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = KernFeatureWriterTest.writeFeatures(ufo, ignoreMarks=False) assert newFeatures.asFea() == snapshot def test_kern_LTR_and_RTL_with_marks(snapshot, FontClass): glyphs = { ".notdef": None, "four": 0x34, "seven": 0x37, "A": 0x41, "V": 0x56, "Aacute": 0xC1, "acutecomb": 0x301, "alef-ar": 0x627, "reh-ar": 0x631, "zain-ar": 0x632, "lam-ar": 0x644, "four-ar": 0x664, "seven-ar": 0x667, "fatha-ar": 0x64E, # we also add glyphs without unicode codepoint, but linked to # an encoded 'character' glyph by some GSUB rule "alef-ar.isol": None, "lam-ar.init": None, "reh-ar.fina": None, } groups = { "public.kern1.A": ["A", "Aacute"], "public.kern1.reh": ["reh-ar", "zain-ar", "reh-ar.fina"], "public.kern2.alef": ["alef-ar", "alef-ar.isol"], } kerning = { ("public.kern1.A", "V"): -40, ("seven", "four"): -25, ("reh-ar.fina", "lam-ar.init"): -80, ("public.kern1.reh", "public.kern2.alef"): -100, ("four-ar", "seven-ar"): -30, ("V", "acutecomb"): 70, ("reh-ar", "fatha-ar"): 80, } features = """\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem latn TRK; languagesystem arab dflt; languagesystem arab URD; feature init { script arab; sub lam-ar by lam-ar.init; language URD; } init; feature fina { script arab; sub reh-ar by reh-ar.fina; language URD; } fina; feature isol { script arab; sub alef-ar by alef-ar.isol; } isol; @Bases = [A V Aacute alef-ar reh-ar zain-ar lam-ar alef-ar.isol lam-ar.init reh-ar.fina]; @Marks = [acutecomb fatha-ar]; table GDEF { GlyphClassDef @Bases, [], @Marks, ; } GDEF; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot def test_kern_RTL_with_marks(snapshot, FontClass): glyphs = { ".notdef": None, "alef-ar": 0x627, "reh-ar": 0x631, "zain-ar": 0x632, "lam-ar": 0x644, "four-ar": 0x664, "seven-ar": 0x667, "fatha-ar": 0x64E, # we also add glyphs without unicode codepoint, but linked to # an encoded 'character' glyph by some GSUB rule "alef-ar.isol": None, "lam-ar.init": None, "reh-ar.fina": None, } groups = { "public.kern1.reh": ["reh-ar", "zain-ar", "reh-ar.fina"], "public.kern2.alef": ["alef-ar", "alef-ar.isol"], } kerning = { ("reh-ar.fina", "lam-ar.init"): -80, ("public.kern1.reh", "public.kern2.alef"): -100, ("reh-ar", "fatha-ar"): 80, } features = """\ languagesystem arab dflt; languagesystem arab ARA; feature init { script arab; sub lam-ar by lam-ar.init; } init; feature fina { script arab; sub reh-ar by reh-ar.fina; } fina; feature isol { script arab; sub alef-ar by alef-ar.isol; } isol; @Bases = [alef-ar reh-ar zain-ar lam-ar alef-ar.isol lam-ar.init reh-ar.fina]; @Marks = [fatha-ar]; table GDEF { GlyphClassDef @Bases, [], @Marks, ; } GDEF; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot def test_kern_independent_of_languagesystem(snapshot, FontClass): glyphs = {"A": 0x41, "V": 0x56, "reh-ar": 0x631, "alef-ar": 0x627} kerning = {("A", "V"): -40, ("reh-ar", "alef-ar"): -100} # No languagesystems declared. ufo = makeUFO(FontClass, glyphs, kerning=kerning) generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot(name="same") features = "languagesystem arab dflt;" ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot(name="same") def test_dist_LTR(snapshot, FontClass): glyphs = {"aaMatra_kannada": 0x0CBE, "ailength_kannada": 0xCD6} groups = { "public.kern1.KND_aaMatra_R": ["aaMatra_kannada"], "public.kern2.KND_ailength_L": ["aaMatra_kannada"], } kerning = {("public.kern1.KND_aaMatra_R", "public.kern2.KND_ailength_L"): 34} features = """\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem knda dflt; languagesystem knd2 dflt; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot def test_dist_RTL(snapshot, FontClass): glyphs = {"u10A06": 0x10A06, "u10A1E": 0x10A1E} kerning = {("u10A1E", "u10A06"): 117} features = """\ languagesystem DFLT dflt; languagesystem arab dflt; languagesystem khar dflt; """ ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot def test_dist_LTR_and_RTL(snapshot, FontClass): glyphs = { "aaMatra_kannada": 0x0CBE, "ailength_kannada": 0xCD6, "u10A06": 0x10A06, "u10A1E": 0x10A1E, } groups = { "public.kern1.KND_aaMatra_R": ["aaMatra_kannada"], "public.kern2.KND_ailength_L": ["aaMatra_kannada"], } kerning = { ("public.kern1.KND_aaMatra_R", "public.kern2.KND_ailength_L"): 34, ("u10A1E", "u10A06"): 117, } features = """\ languagesystem DFLT dflt; languagesystem knda dflt; languagesystem knd2 dflt; languagesystem khar dflt; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot def test_ambiguous_direction_pair(snapshot, FontClass, caplog): """Test that glyphs with ambiguous BiDi classes get split correctly.""" glyphs = { "A": 0x0041, "one": 0x0031, "yod-hb": 0x05D9, "reh-ar": 0x0631, "one-ar": 0x0661, "bar": [0x0073, 0x0627], } kerning = { ("bar", "bar"): 1, ("bar", "A"): 2, ("reh-ar", "A"): 3, ("reh-ar", "one-ar"): 4, ("yod-hb", "one"): 5, } features = """\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem arab dflt; """ ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) with caplog.at_level(logging.INFO): generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot assert caplog.messages == [ "Skipping part of a kerning pair with mixed direction (LeftToRight, RightToLeft)", # noqa: B950 "Skipping part of a kerning pair with mixed direction (RightToLeft, LeftToRight)", # noqa: B950 "Skipping part of a kerning pair with conflicting BiDi classes", # noqa: B950 "Skipping part of a kerning pair with mixed direction (RightToLeft, LeftToRight)", # noqa: B950 "Skipping part of a kerning pair with mixed direction (RightToLeft, LeftToRight)", # noqa: B950 "Skipping part of a kerning pair with conflicting BiDi classes", # noqa: B950 "Skipping part of a kerning pair with conflicting BiDi classes", # noqa: B950 ] def test_kern_RTL_and_DFLT_numbers(snapshot, FontClass): glyphs = {"four": 0x34, "seven": 0x37, "bet-hb": 0x5D1, "yod-hb": 0x5D9} kerning = {("seven", "four"): -25, ("yod-hb", "bet-hb"): -100} features = """\ languagesystem DFLT dflt; languagesystem hebr dflt; """ ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) generated = KernFeatureWriterTest.writeFeatures(ufo) assert generated.asFea() == snapshot def test_quantize(snapshot, FontClass): font = FontClass() for name in ("one", "four", "six"): font.newGlyph(name) font.kerning.update({("four", "six"): -57.0, ("one", "six"): -24.0}) writer = KernFeatureWriter(quantization=5) feaFile = fea_ast.FeatureFile() writer.write(font, feaFile) assert feaFile.asFea() == snapshot def test_skip_spacing_marks(snapshot, data_dir, FontClass): fontPath = data_dir / "SpacingCombiningTest-Regular.ufo" testufo = FontClass(fontPath) generated = KernFeatureWriterTest.writeFeatures(testufo) assert generated.asFea() == snapshot def test_kern_split_multi_glyph_class(snapshot, FontClass): """Test that kern pair types are correctly split across directions.""" glyphs = { "a": ord("a"), "b": ord("b"), "period": ord("."), } groups = { "public.kern1.foo": ["a", "period"], "public.kern2.foo": ["b", "period"], } kerning = { # Glyph-to-glyph ("a", "a"): 1, ("a", "b"): 2, ("a", "period"): 3, ("b", "a"): 4, ("b", "b"): 5, ("b", "period"): 6, ("period", "a"): 7, ("period", "b"): 8, ("period", "period"): 9, # Class-to-glyph ("public.kern1.foo", "b"): 10, ("public.kern1.foo", "period"): 11, # Glyph-to-class ("a", "public.kern2.foo"): 12, ("period", "public.kern2.foo"): 13, # Class-to-class ("public.kern1.foo", "public.kern2.foo"): 14, } ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot(name="same") # Making a common glyph implicitly have an explicit script assigned (GSUB # closure) will still keep it in the common section. features = """ feature ss01 { sub a by period; # Make period be both Latn and Zyyy. } ss01; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot(name="same") def test_kern_split_and_drop(snapshot, FontClass, caplog): """Test that mixed directions pairs are pruned and only the compatible parts are kept.""" glyphs = { "a": ord("a"), "alpha": ord("α"), "a-orya": 0x0B05, "a-cy": 0x0430, "alef-ar": 0x627, "period": ord("."), } groups = { "public.kern1.foo": ["a", "alpha", "a-orya"], "public.kern2.foo": ["a", "alpha", "a-orya"], "public.kern1.bar": ["a-cy", "alef-ar", "period"], "public.kern2.bar": ["a-cy", "alef-ar", "period"], } kerning = { ("public.kern1.foo", "public.kern2.bar"): 20, ("public.kern1.bar", "public.kern2.foo"): 20, } ufo = makeUFO(FontClass, glyphs, groups, kerning) with caplog.at_level(logging.INFO): newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot assert caplog.messages == [ "Skipping part of a kerning pair <('a', 'a-orya', 'alpha') ('alef-ar',) 20> with mixed direction (LeftToRight, RightToLeft)", # noqa: B950 "Skipping part of a kerning pair <('alef-ar',) ('a', 'a-orya', 'alpha') 20> with mixed direction (RightToLeft, LeftToRight)", # noqa: B950 ] def test_kern_split_and_drop_mixed(snapshot, caplog, FontClass): """Test that mixed directions pairs are dropped. And that scripts with no remaining lookups don't crash. """ glyphs = {"V": ord("V"), "W": ord("W"), "gba-nko": 0x07DC} groups = {"public.kern1.foo": ["V", "W"], "public.kern2.foo": ["gba-nko", "W"]} kerning = {("public.kern1.foo", "public.kern2.foo"): -20} ufo = makeUFO(FontClass, glyphs, groups, kerning) with caplog.at_level(logging.INFO): newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot assert ( "<('V', 'W') ('gba-nko',) -20> with mixed direction (LeftToRight, RightToLeft)" in caplog.text ) def test_kern_mixed_bidis(snapshot, caplog, FontClass): """Test that BiDi types for pairs are respected.""" # TODO: Add Adlam numbers (rtl) glyphs = { "a": ord("a"), "comma": ord(","), "alef-ar": 0x0627, "comma-ar": 0x060C, "one-ar": 0x0661, "one-adlam": 0x1E951, } kerning = { # Undetermined: LTR ("comma", "comma"): -1, # LTR ("a", "a"): 1, ("a", "comma"): 2, ("comma", "a"): 3, # RTL ("alef-ar", "alef-ar"): 4, ("alef-ar", "comma-ar"): 5, ("comma-ar", "alef-ar"): 6, ("one-adlam", "one-adlam"): 10, ("one-adlam", "comma-ar"): 11, ("comma-ar", "one-adlam"): 12, # Mixed: should be dropped ("alef-ar", "one-ar"): 7, ("one-ar", "alef-ar"): 8, ("one-ar", "one-adlam"): 13, # LTR despite being an RTL script ("one-ar", "one-ar"): 9, } ufo = makeUFO(FontClass, glyphs, None, kerning) with caplog.at_level(logging.INFO): newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot assert " with conflicting BiDi classes" in caplog.text assert " with conflicting BiDi classes" in caplog.text assert " with conflicting BiDi classes" in caplog.text def unicodeScript(codepoint: int) -> str: """Returns the Unicode script for a codepoint, combining some scripts into the same bucket. This allows lookups to contain more than one script. The most prominent case is being able to kern Hiragana and Katakana against each other, Unicode defines "Hrkt" as an alias for both scripts. Note: Keep in sync with unicodeScriptExtensions! """ script = unicodedata.script(chr(codepoint)) return UNICODE_SCRIPT_ALIASES.get(script, script) def test_kern_zyyy_zinh(snapshot, FontClass): """Test that a sampling of glyphs with a common or inherited script, but a disjoint set of explicit script extensions end up in the correct lookups.""" glyphs = {} for i in range(0, 0x110000, 0x10): script = unicodeScript(i) script_extension = unicodeScriptExtensions(i) if script not in script_extension: assert script in DFLT_SCRIPTS name = f"uni{i:04X}" glyphs[name] = i kerning = {(glyph, glyph): i for i, glyph in enumerate(glyphs)} ufo = makeUFO(FontClass, glyphs, None, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot def test_kern_hira_kana_hrkt(snapshot, FontClass): """Test that Hiragana and Katakana lands in the same lookup and can be kerned against each other and common glyphs are kerned just once.""" glyphs = {"a-hira": 0x3042, "a-kana": 0x30A2, "period": ord(".")} kerning = { ("a-hira", "a-hira"): 1, ("a-hira", "a-kana"): 2, ("a-kana", "a-hira"): 3, ("a-kana", "a-kana"): 4, ("period", "period"): 5, ("a-hira", "period"): 6, ("period", "a-hira"): 7, ("a-kana", "period"): 8, ("period", "a-kana"): 9, } ufo = makeUFO(FontClass, glyphs, None, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot # TODO: Keep? Then modify comments. def test_defining_classdefs(snapshot, FontClass): """Check that we aren't redefining class definitions with different content.""" glyphs = { "halant-telugu": 0xC4D, # Telu "ka-telugu.below": None, # Telu by substitution "ka-telugu": 0xC15, # Telu "rVocalicMatra-telugu": 0xC43, # Telu "sha-telugu.below": None, # Default "ss-telugu.alt": None, # Default "ssa-telugu.alt": None, # Telu by substitution "ssa-telugu": 0xC37, # Telu } groups = { "public.kern1.sha-telugu.below": ["sha-telugu.below"], # The following group is a mix of Telu and Default through its gylphs. The # kerning for bases below will create a Telu and Default split group. # Important for the NOTE below. "public.kern1.ssa-telugu.alt": ["ssa-telugu.alt", "ss-telugu.alt"], "public.kern2.ka-telugu.below": ["ka-telugu.below"], "public.kern2.rVocalicMatra-telugu": ["rVocalicMatra-telugu"], } kerning = { # The follwoing three pairs are base-to-base pairs: ("public.kern1.sha-telugu.below", "public.kern2.ka-telugu.below"): 20, ("public.kern1.ssa-telugu.alt", "public.kern2.ka-telugu.below"): 60, ("public.kern1.ssa-telugu.alt", "sha-telugu.below"): 150, # NOTE: This last pair kerns bases against marks, triggering an extra # pass to make a mark lookup that will create new classDefs. This extra # pass will work on just this one pair, and kern splitting won't split # off a Default group from `public.kern1.ssa-telugu.alt`, you get just a # Telu pair. Unless the writer keeps track of which classDefs it already # generated, this will overwrite the previous `@kern1.Telu.ssatelugu.alt # = [ssa-telugu.alt]` with `@kern1.Telu.ssatelugu.alt = # [ss-telugu.alt]`, losing kerning. ("public.kern1.ssa-telugu.alt", "public.kern2.rVocalicMatra-telugu"): 180, } features = """ feature blwf { script tel2; sub halant-telugu ka-telugu by ka-telugu.below; } blwf; feature psts { script tel2; sub ssa-telugu' [rVocalicMatra-telugu sha-telugu.below ka-telugu.below] by ssa-telugu.alt; } psts; """ # noqa: B950 ufo = makeUFO(FontClass, glyphs, groups, kerning, features) ufo.lib["public.openTypeCategories"] = { "halant-telugu": "mark", "ka-telugu": "base", "rVocalicMatra-telugu": "mark", "ss-telugu.alt": "base", "ssa-telugu.alt": "base", "ssa-telugu": "base", } newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot def test_mark_base_kerning(snapshot, FontClass): """Check that kerning of bases against marks is correctly split into base-only and mixed-mark-and-base lookups, to preserve the semantics of kerning exceptions (pairs modifying the effect of other pairs).""" glyphs = { "aa-tamil": 0x0B86, "va-tamil": 0x0BB5, "aulengthmark-tamil": 0x0BD7, } groups = { # Each group is a mix of mark and base glyph. "public.kern1.e-tamil": ["aulengthmark-tamil", "va-tamil"], "public.kern2.e-tamil": ["aulengthmark-tamil", "va-tamil"], } kerning = { ("aa-tamil", "va-tamil"): -20, ("aa-tamil", "public.kern2.e-tamil"): -35, ("va-tamil", "aa-tamil"): -20, ("public.kern1.e-tamil", "aa-tamil"): -35, ("aulengthmark-tamil", "aulengthmark-tamil"): -200, ("public.kern1.e-tamil", "public.kern2.e-tamil"): -100, } ufo = makeUFO(FontClass, glyphs, groups, kerning) ufo.lib["public.openTypeCategories"] = { "aulengthmark-tamil": "mark", } newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot def test_hyphenated_duplicates(snapshot, FontClass): """Check that kerning group names are kept separate even if their sanitized names are the same.""" glyphs = {"comma": ord(","), "period": ord(".")} groups = { "public.kern1.hy-phen": ["comma"], "public.kern1.hyp-hen": ["period"], } kerning = { ("public.kern1.hy-phen", "comma"): 1, ("public.kern1.hyp-hen", "period"): 2, } ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot def test_dflt_language(snapshot, FontClass): """Check that languages defined for the special DFLT script are registered as well.""" glyphs = {"a": ord("a"), "comma": ord(",")} groups = {} kerning = {("a", "a"): 1, ("comma", "comma"): 2} features = """ languagesystem DFLT dflt; languagesystem DFLT ZND; languagesystem latn dflt; languagesystem latn ANG; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert newFeatures.asFea() == snapshot ufo2ft-3.3.1/tests/featureWriters/kernFeatureWriter_test.py000066400000000000000000002037051470175262700242030ustar00rootroot00000000000000import logging import os from textwrap import dedent import pytest from fontTools import unicodedata from ufo2ft.constants import UNICODE_SCRIPT_ALIASES from ufo2ft.featureCompiler import FeatureCompiler, parseLayoutFeatures from ufo2ft.featureWriters import KernFeatureWriter, ast from ufo2ft.util import DFLT_SCRIPTS, unicodeScriptExtensions from . import FeatureWriterTest def makeUFO(cls, glyphMap, groups=None, kerning=None, features=None): ufo = cls() for name, uni in glyphMap.items(): glyph = ufo.newGlyph(name) if uni is not None: glyph.unicode = uni if groups is not None: ufo.groups.update(groups) if kerning is not None: ufo.kerning.update(kerning) if features is not None: ufo.features.text = features return ufo def getClassDefs(feaFile): return [s for s in feaFile.statements if isinstance(s, ast.GlyphClassDefinition)] def getGlyphs(classDef): return [str(g) for g in classDef.glyphs.glyphSet()] def getLookups(feaFile): return [s for s in feaFile.statements if isinstance(s, ast.LookupBlock)] def getPairPosRules(lookup): return [s for s in lookup.statements if isinstance(s, ast.PairPosStatement)] class KernFeatureWriterTest(FeatureWriterTest): FeatureWriter = KernFeatureWriter def test_cleanup_missing_glyphs(self, FontClass): groups = { "public.kern1.A": ["A", "Aacute", "Abreve", "Acircumflex"], "public.kern2.B": ["B", "D", "E", "F"], "public.kern1.C": ["foobar"], } kerning = { ("public.kern1.A", "public.kern2.B"): 10, ("public.kern1.A", "baz"): -25, ("baz", "public.kern2.B"): -20, ("public.kern1.C", "public.kern2.B"): 20, } ufo = FontClass() exclude = {"Abreve", "D", "foobar"} for glyphs in groups.values(): for glyph in glyphs: if glyph in exclude: continue ufo.newGlyph(glyph) ufo.groups.update(groups) ufo.kerning.update(kerning) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) writer.write(ufo, feaFile) classDefs = getClassDefs(feaFile) assert len(classDefs) == 2 assert classDefs[0].name == "kern1.Default.A" assert classDefs[1].name == "kern2.Default.B" assert getGlyphs(classDefs[0]) == ["A", "Aacute", "Acircumflex"] assert getGlyphs(classDefs[1]) == ["B", "E", "F"] lookups = getLookups(feaFile) assert len(lookups) == 1 kern_lookup = lookups[0] # We have no codepoints defined for these, so they're considered common assert kern_lookup.name == "kern_Default" rules = getPairPosRules(kern_lookup) assert len(rules) == 1 assert str(rules[0]) == "pos @kern1.Default.A @kern2.Default.B 10;" def test_ignoreMarks(self, FontClass): font = FontClass() for name in ("one", "four", "six"): font.newGlyph(name) font.kerning.update({("four", "six"): -55.0, ("one", "six"): -30.0}) # default is ignoreMarks=True writer = KernFeatureWriter() feaFile = ast.FeatureFile() assert writer.write(font, feaFile) assert str(feaFile) == dedent( """\ lookup kern_Default { lookupflag IgnoreMarks; pos four six -55; pos one six -30; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) writer = KernFeatureWriter(ignoreMarks=False) feaFile = ast.FeatureFile() assert writer.write(font, feaFile) assert str(feaFile) == dedent( """\ lookup kern_Default { pos four six -55; pos one six -30; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) def test_mark_to_base_kern(self, FontClass): font = FontClass() for name in ("A", "B", "C"): font.newGlyph(name).unicode = ord(name) font.newGlyph("acutecomb").unicode = 0x0301 font.kerning.update({("A", "acutecomb"): -55.0, ("B", "C"): -30.0}) font.features.text = dedent( """\ @Bases = [A B C]; @Marks = [acutecomb]; table GDEF { GlyphClassDef @Bases, [], @Marks, ; } GDEF; """ ) # default is ignoreMarks=True feaFile = self.writeFeatures(font) assert dedent(str(feaFile)) == dedent( """\ lookup kern_Latn { lookupflag IgnoreMarks; pos B C -30; } kern_Latn; lookup kern_Latn_marks { pos A acutecomb -55; } kern_Latn_marks; feature kern { script DFLT; language dflt; lookup kern_Latn; lookup kern_Latn_marks; script latn; language dflt; lookup kern_Latn; lookup kern_Latn_marks; } kern; """ ) feaFile = self.writeFeatures(font, ignoreMarks=False) assert dedent(str(feaFile)) == dedent( """\ lookup kern_Latn { pos A acutecomb -55; pos B C -30; } kern_Latn; feature kern { script DFLT; language dflt; lookup kern_Latn; script latn; language dflt; lookup kern_Latn; } kern; """ ) def test_mark_to_base_only(self, FontClass): font = FontClass() for name in ("A", "B", "C"): font.newGlyph(name) font.newGlyph("acutecomb").unicode = 0x0301 font.kerning.update({("A", "acutecomb"): -55.0}) font.features.text = dedent( """\ @Bases = [A B C]; @Marks = [acutecomb]; table GDEF { GlyphClassDef @Bases, [], @Marks, ; } GDEF; """ ) # default is ignoreMarks=True feaFile = self.writeFeatures(font) assert str(feaFile) == dedent( """\ lookup kern_Default_marks { pos A acutecomb -55; } kern_Default_marks; feature kern { script DFLT; language dflt; lookup kern_Default_marks; } kern; """ ) def test_mode(self, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = dedent( """\ feature kern { pos one four' -50 six; } kern; """ ) ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() # default mode="skip" feaFile = parseLayoutFeatures(ufo) assert not writer.write(ufo, feaFile) assert str(feaFile) == existing # pass optional "append" mode writer = KernFeatureWriter(mode="append") feaFile = parseLayoutFeatures(ufo) assert writer.write(ufo, feaFile) expected = existing + dedent( """ lookup kern_Default { lookupflag IgnoreMarks; pos seven six 25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) assert str(feaFile) == expected # pass "skip" mode explicitly writer = KernFeatureWriter(mode="skip") feaFile = parseLayoutFeatures(ufo) assert not writer.write(ufo, feaFile) assert str(feaFile) == existing def test_insert_comment_before(self, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = dedent( """\ feature kern { # # Automatic Code # pos one four' -50 six; } kern; """ ) ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) assert writer.write(ufo, feaFile) expected = dedent( """\ lookup kern_Default { lookupflag IgnoreMarks; pos seven six 25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; feature kern { # # pos one four' -50 six; } kern; """ ) assert str(feaFile).strip() == expected.strip() # test append mode ignores insert marker generated = self.writeFeatures(ufo, mode="append") assert str(generated) == dedent( """\ lookup kern_Default { lookupflag IgnoreMarks; pos seven six 25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) def test_comment_wrong_case_or_missing(self, FontClass, caplog): ufo = FontClass() for name in ("a", "b"): ufo.newGlyph(name) ufo.kerning.update({("a", "b"): 25.0}) ufo.features.text = dedent( """ feature kern { # Automatic code } kern; """ ).strip() with caplog.at_level(logging.WARNING): compiler = FeatureCompiler(ufo, featureWriters=[KernFeatureWriter]) font = compiler.compile() # We mis-cased the insertion marker above, so it's ignored and we end up # with an empty `kern` block overriding the other kerning in the font # source and therefore no `GPOS` table. assert "miscased" in caplog.text assert "Dropping the former" in caplog.text assert "GPOS" not in font # Append mode ignores insertion markers and so should not log warnings # and have kerning in the final font. caplog.clear() with caplog.at_level(logging.WARNING): compiler = FeatureCompiler( ufo, featureWriters=[KernFeatureWriter(mode="append")] ) font = compiler.compile() assert not caplog.text assert "GPOS" in font def test_insert_comment_before_extended(self, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = dedent( """\ feature kern { # # Automatic Code End # pos one four' -50 six; } kern; """ ) ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) assert writer.write(ufo, feaFile) expected = dedent( """\ lookup kern_Default { lookupflag IgnoreMarks; pos seven six 25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; feature kern { # # pos one four' -50 six; } kern; """ ) assert str(feaFile).strip() == expected.strip() def test_insert_comment_after(self, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = dedent( """\ feature kern { pos one four' -50 six; # # Automatic Code # } kern; """ ) ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) assert writer.write(ufo, feaFile) expected = dedent( """\ feature kern { pos one four' -50 six; # # } kern; lookup kern_Default { lookupflag IgnoreMarks; pos seven six 25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) assert str(feaFile) == expected # test append mode ignores insert marker generated = self.writeFeatures(ufo, mode="append") assert str(generated) == dedent( """\ lookup kern_Default { lookupflag IgnoreMarks; pos seven six 25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) def test_insert_comment_middle(self, FontClass): ufo = FontClass() for name in ("one", "four", "six", "seven"): ufo.newGlyph(name) existing = dedent( """\ feature kern { pos one four' -50 six; # # Automatic Code # pos one six' -50 six; } kern; """ ) ufo.features.text = existing ufo.kerning.update({("seven", "six"): 25.0}) writer = KernFeatureWriter() feaFile = parseLayoutFeatures(ufo) writer.write(ufo, feaFile) assert str(feaFile) == dedent( """\ feature kern { pos one four' -50 six; # } kern; lookup kern_Default { lookupflag IgnoreMarks; pos seven six 25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; feature kern { # pos one six' -50 six; } kern; """ ) # test append mode ignores insert marker generated = self.writeFeatures(ufo, mode="append") assert str(generated) == dedent( """\ lookup kern_Default { lookupflag IgnoreMarks; pos seven six 25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) def test_arabic_numerals(self, FontClass): """Test that arabic numerals (with bidi type AN) are kerned LTR. See: * https://github.com/googlei18n/ufo2ft/issues/198 * https://github.com/googlei18n/ufo2ft/pull/200 Additionally, some Arabic numerals are used in more than one script. One approach is to look at other glyphs with distinct script associations and consider the font to be supporting those. """ ufo = FontClass() for name, code in [("four-ar", 0x664), ("seven-ar", 0x667)]: glyph = ufo.newGlyph(name) glyph.unicode = code ufo.kerning.update({("four-ar", "seven-ar"): -30}) generated = self.writeFeatures(ufo) assert ( dedent(str(generated)) == dedent( """ lookup kern_Default { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ).lstrip("\n") ) ufo.newGlyph("alef-ar").unicode = 0x627 generated = self.writeFeatures(ufo) assert dedent(str(generated)) == dedent( """\ lookup kern_Arab { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_Arab; feature kern { script DFLT; language dflt; lookup kern_Arab; script arab; language dflt; lookup kern_Arab; } kern; """ ) ufo.features.text = """ languagesystem DFLT dflt; languagesystem Thaa dflt; """ generated = self.writeFeatures(ufo) assert dedent(str(generated)) == dedent( """\ lookup kern_Arab_Thaa { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_Arab_Thaa; feature kern { script DFLT; language dflt; lookup kern_Arab_Thaa; script arab; language dflt; lookup kern_Arab_Thaa; script thaa; language dflt; lookup kern_Arab_Thaa; } kern; """ ) del ufo["alef-ar"] generated = self.writeFeatures(ufo) assert dedent(str(generated)) == dedent( """\ lookup kern_Thaa { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_Thaa; feature kern { script DFLT; language dflt; lookup kern_Thaa; script thaa; language dflt; lookup kern_Thaa; } kern; """ ) def test_skip_zero_class_kerns(self, FontClass): glyphs = { "A": ord("A"), "B": ord("B"), "C": ord("C"), "D": ord("D"), "E": ord("E"), "F": ord("F"), "G": ord("G"), "H": ord("H"), } groups = { "public.kern1.foo": ["A", "B"], "public.kern2.bar": ["C", "D"], "public.kern1.baz": ["E", "F"], "public.kern2.nul": ["G", "H"], } kerning = { ("public.kern1.foo", "public.kern2.bar"): 10, ("public.kern1.baz", "public.kern2.bar"): -10, ("public.kern1.foo", "D"): 15, ("A", "public.kern2.bar"): 5, ("G", "H"): -5, # class-class zero-value pairs are skipped ("public.kern1.foo", "public.kern2.nul"): 0, } expectation = dedent( """\ @kern1.Latn.baz = [E F]; @kern1.Latn.foo = [A B]; @kern2.Latn.bar = [C D]; lookup kern_Latn { lookupflag IgnoreMarks; pos G H -5; enum pos A @kern2.Latn.bar 5; enum pos @kern1.Latn.foo D 15; pos @kern1.Latn.foo @kern2.Latn.bar 10; pos @kern1.Latn.baz @kern2.Latn.bar -10; } kern_Latn; feature kern { script DFLT; language dflt; lookup kern_Latn; script latn; language dflt; lookup kern_Latn; } kern; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)).lstrip("\n") == expectation def test_kern_uniqueness(self, FontClass): glyphs = { ".notdef": None, "questiondown": 0xBF, "y": 0x79, } groups = { "public.kern1.questiondown": ["questiondown"], "public.kern2.y": ["y"], } kerning = { ("public.kern1.questiondown", "public.kern2.y"): 15, ("public.kern1.questiondown", "y"): 35, ("questiondown", "public.kern2.y"): -35, ("questiondown", "y"): 35, } ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = self.writeFeatures(ufo) # The final kerning value for questiondown, y is 35 and all variants # must be present. Ensures the uniqueness filter doesn't filter things # out. assert dedent(str(newFeatures)) == dedent( """\ @kern1.Latn.questiondown = [questiondown]; @kern2.Latn.y = [y]; lookup kern_Latn { lookupflag IgnoreMarks; pos questiondown y 35; enum pos questiondown @kern2.Latn.y -35; enum pos @kern1.Latn.questiondown y 35; pos @kern1.Latn.questiondown @kern2.Latn.y 15; } kern_Latn; feature kern { script DFLT; language dflt; lookup kern_Latn; script latn; language dflt; lookup kern_Latn; } kern; """ ) def test_kern_LTR_and_RTL(self, FontClass): glyphs = { ".notdef": None, "four": 0x34, "seven": 0x37, "A": 0x41, "V": 0x56, "Aacute": 0xC1, "alef-ar": 0x627, "reh-ar": 0x631, "zain-ar": 0x632, "lam-ar": 0x644, "four-ar": 0x664, "seven-ar": 0x667, # # we also add glyphs without unicode codepoint, but linked to # # an encoded 'character' glyph by some GSUB rule "alef-ar.isol": None, "lam-ar.init": None, "reh-ar.fina": None, } groups = { "public.kern1.A": ["A", "Aacute"], "public.kern1.reh": ["reh-ar", "zain-ar", "reh-ar.fina"], "public.kern2.alef": ["alef-ar", "alef-ar.isol"], } kerning = { ("public.kern1.A", "V"): -40, ("seven", "four"): -25, ("reh-ar.fina", "lam-ar.init"): -80, ("public.kern1.reh", "public.kern2.alef"): -100, ("four-ar", "seven-ar"): -30, } features = dedent( """\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem latn TRK; languagesystem arab dflt; languagesystem arab URD; feature init { script arab; sub lam-ar by lam-ar.init; language URD; } init; feature fina { script arab; sub reh-ar by reh-ar.fina; language URD; } fina; feature isol { script arab; sub alef-ar by alef-ar.isol; } isol; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = self.writeFeatures(ufo, ignoreMarks=False) assert dedent(str(newFeatures)) == dedent( """\ @kern1.Arab.reh = [reh-ar reh-ar.fina zain-ar]; @kern1.Latn.A = [A Aacute]; @kern2.Arab.alef = [alef-ar alef-ar.isol]; lookup kern_Arab { pos four-ar seven-ar -30; pos reh-ar.fina lam-ar.init <-80 0 -80 0>; pos @kern1.Arab.reh @kern2.Arab.alef <-100 0 -100 0>; } kern_Arab; lookup kern_Latn { enum pos @kern1.Latn.A V -40; } kern_Latn; lookup kern_Default { pos seven four -25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; lookup kern_Latn; script arab; language dflt; lookup kern_Default; lookup kern_Arab; language URD; script latn; language dflt; lookup kern_Default; lookup kern_Latn; language TRK; } kern; """ ) def test_kern_LTR_and_RTL_with_marks(self, FontClass): glyphs = { ".notdef": None, "four": 0x34, "seven": 0x37, "A": 0x41, "V": 0x56, "Aacute": 0xC1, "acutecomb": 0x301, "alef-ar": 0x627, "reh-ar": 0x631, "zain-ar": 0x632, "lam-ar": 0x644, "four-ar": 0x664, "seven-ar": 0x667, "fatha-ar": 0x64E, # we also add glyphs without unicode codepoint, but linked to # an encoded 'character' glyph by some GSUB rule "alef-ar.isol": None, "lam-ar.init": None, "reh-ar.fina": None, } groups = { "public.kern1.A": ["A", "Aacute"], "public.kern1.reh": ["reh-ar", "zain-ar", "reh-ar.fina"], "public.kern2.alef": ["alef-ar", "alef-ar.isol"], } kerning = { ("public.kern1.A", "V"): -40, ("seven", "four"): -25, ("reh-ar.fina", "lam-ar.init"): -80, ("public.kern1.reh", "public.kern2.alef"): -100, ("four-ar", "seven-ar"): -30, ("V", "acutecomb"): 70, ("reh-ar", "fatha-ar"): 80, } features = dedent( """\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem latn TRK; languagesystem arab dflt; languagesystem arab URD; feature init { script arab; sub lam-ar by lam-ar.init; language URD; } init; feature fina { script arab; sub reh-ar by reh-ar.fina; language URD; } fina; feature isol { script arab; sub alef-ar by alef-ar.isol; } isol; @Bases = [A V Aacute alef-ar reh-ar zain-ar lam-ar alef-ar.isol lam-ar.init reh-ar.fina]; @Marks = [acutecomb fatha-ar]; table GDEF { GlyphClassDef @Bases, [], @Marks, ; } GDEF; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = self.writeFeatures(ufo) assert dedent(str(newFeatures)).lstrip("\n") == dedent( """\ @kern1.Arab.reh = [reh-ar reh-ar.fina zain-ar]; @kern1.Latn.A = [A Aacute]; @kern2.Arab.alef = [alef-ar alef-ar.isol]; lookup kern_Arab { lookupflag IgnoreMarks; pos four-ar seven-ar -30; pos reh-ar.fina lam-ar.init <-80 0 -80 0>; pos @kern1.Arab.reh @kern2.Arab.alef <-100 0 -100 0>; } kern_Arab; lookup kern_Arab_marks { pos reh-ar fatha-ar <80 0 80 0>; } kern_Arab_marks; lookup kern_Latn { lookupflag IgnoreMarks; enum pos @kern1.Latn.A V -40; } kern_Latn; lookup kern_Latn_marks { pos V acutecomb 70; } kern_Latn_marks; lookup kern_Default { lookupflag IgnoreMarks; pos seven four -25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; lookup kern_Latn; lookup kern_Latn_marks; script arab; language dflt; lookup kern_Default; lookup kern_Arab; lookup kern_Arab_marks; language URD; script latn; language dflt; lookup kern_Default; lookup kern_Latn; lookup kern_Latn_marks; language TRK; } kern; """ ) def test_kern_RTL_with_marks(self, FontClass): glyphs = { ".notdef": None, "alef-ar": 0x627, "reh-ar": 0x631, "zain-ar": 0x632, "lam-ar": 0x644, "four-ar": 0x664, "seven-ar": 0x667, "fatha-ar": 0x64E, # # we also add glyphs without unicode codepoint, but linked to # # an encoded 'character' glyph by some GSUB rule "alef-ar.isol": None, "lam-ar.init": None, "reh-ar.fina": None, } groups = { "public.kern1.reh": ["reh-ar", "zain-ar", "reh-ar.fina"], "public.kern2.alef": ["alef-ar", "alef-ar.isol"], } kerning = { ("reh-ar.fina", "lam-ar.init"): -80, ("public.kern1.reh", "public.kern2.alef"): -100, ("reh-ar", "fatha-ar"): 80, } features = dedent( """\ languagesystem arab dflt; languagesystem arab ARA; feature init { script arab; sub lam-ar by lam-ar.init; } init; feature fina { script arab; sub reh-ar by reh-ar.fina; } fina; feature isol { script arab; sub alef-ar by alef-ar.isol; } isol; @Bases = [alef-ar reh-ar zain-ar lam-ar alef-ar.isol lam-ar.init reh-ar.fina]; @Marks = [fatha-ar]; table GDEF { GlyphClassDef @Bases, [], @Marks, ; } GDEF; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = self.writeFeatures(ufo) assert dedent(str(newFeatures)).lstrip("\n") == dedent( """\ @kern1.Arab.reh = [reh-ar reh-ar.fina zain-ar]; @kern2.Arab.alef = [alef-ar alef-ar.isol]; lookup kern_Arab { lookupflag IgnoreMarks; pos reh-ar.fina lam-ar.init <-80 0 -80 0>; pos @kern1.Arab.reh @kern2.Arab.alef <-100 0 -100 0>; } kern_Arab; lookup kern_Arab_marks { pos reh-ar fatha-ar <80 0 80 0>; } kern_Arab_marks; feature kern { script DFLT; language dflt; lookup kern_Arab; lookup kern_Arab_marks; script arab; language dflt; lookup kern_Arab; lookup kern_Arab_marks; language ARA; } kern; """ ) def test_kern_independent_of_languagesystem(self, FontClass): glyphs = {"A": 0x41, "V": 0x56, "reh-ar": 0x631, "alef-ar": 0x627} kerning = {("A", "V"): -40, ("reh-ar", "alef-ar"): -100} # No languagesystems decalred. ufo = makeUFO(FontClass, glyphs, kerning=kerning) generated = self.writeFeatures(ufo) expectation = dedent( """\ lookup kern_Arab { lookupflag IgnoreMarks; pos reh-ar alef-ar <-100 0 -100 0>; } kern_Arab; lookup kern_Latn { lookupflag IgnoreMarks; pos A V -40; } kern_Latn; feature kern { script DFLT; language dflt; lookup kern_Latn; script arab; language dflt; lookup kern_Arab; script latn; language dflt; lookup kern_Latn; } kern; """ ) assert dedent(str(generated)) == expectation features = dedent("languagesystem arab dflt;") ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) generated = self.writeFeatures(ufo) assert dedent(str(generated)).lstrip("\n") == expectation def test_dist_LTR(self, FontClass): glyphs = {"aaMatra_kannada": 0x0CBE, "ailength_kannada": 0xCD6} groups = { "public.kern1.KND_aaMatra_R": ["aaMatra_kannada"], "public.kern2.KND_ailength_L": ["aaMatra_kannada"], } kerning = {("public.kern1.KND_aaMatra_R", "public.kern2.KND_ailength_L"): 34} features = dedent( """\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem knda dflt; languagesystem knd2 dflt; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning, features) generated = self.writeFeatures(ufo) assert dedent(str(generated)) == dedent( """\ @kern1.Knda.KND_aaMatra_R = [aaMatra_kannada]; @kern2.Knda.KND_ailength_L = [aaMatra_kannada]; lookup kern_Knda { lookupflag IgnoreMarks; pos @kern1.Knda.KND_aaMatra_R @kern2.Knda.KND_ailength_L 34; } kern_Knda; feature dist { script knd2; language dflt; lookup kern_Knda; script knda; language dflt; lookup kern_Knda; } dist; """ ) def test_dist_RTL(self, FontClass): glyphs = {"u10A06": 0x10A06, "u10A1E": 0x10A1E} kerning = {("u10A1E", "u10A06"): 117} features = dedent( """\ languagesystem DFLT dflt; languagesystem arab dflt; languagesystem khar dflt; """ ) ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) generated = self.writeFeatures(ufo) assert str(generated) == dedent( """\ lookup kern_Khar { lookupflag IgnoreMarks; pos u10A1E u10A06 <117 0 117 0>; } kern_Khar; feature dist { script khar; language dflt; lookup kern_Khar; } dist; """ ) def test_dist_LTR_and_RTL(self, FontClass): glyphs = { "aaMatra_kannada": 0x0CBE, "ailength_kannada": 0xCD6, "u10A06": 0x10A06, "u10A1E": 0x10A1E, } groups = { "public.kern1.KND_aaMatra_R": ["aaMatra_kannada"], "public.kern2.KND_ailength_L": ["aaMatra_kannada"], } kerning = { ("public.kern1.KND_aaMatra_R", "public.kern2.KND_ailength_L"): 34, ("u10A1E", "u10A06"): 117, } features = dedent( """\ languagesystem DFLT dflt; languagesystem knda dflt; languagesystem knd2 dflt; languagesystem khar dflt; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning, features) generated = self.writeFeatures(ufo) assert dedent(str(generated)).lstrip("\n") == dedent( """\ @kern1.Knda.KND_aaMatra_R = [aaMatra_kannada]; @kern2.Knda.KND_ailength_L = [aaMatra_kannada]; lookup kern_Khar { lookupflag IgnoreMarks; pos u10A1E u10A06 <117 0 117 0>; } kern_Khar; lookup kern_Knda { lookupflag IgnoreMarks; pos @kern1.Knda.KND_aaMatra_R @kern2.Knda.KND_ailength_L 34; } kern_Knda; feature dist { script khar; language dflt; lookup kern_Khar; script knd2; language dflt; lookup kern_Knda; script knda; language dflt; lookup kern_Knda; } dist; """ ) def test_ambiguous_direction_pair(self, FontClass, caplog): ufo = FontClass() ufo.newGlyph("A").unicode = 0x41 ufo.newGlyph("one").unicode = 0x31 ufo.newGlyph("yod-hb").unicode = 0x5D9 ufo.newGlyph("reh-ar").unicode = 0x631 ufo.newGlyph("one-ar").unicode = 0x661 ufo.newGlyph("bar").unicodes = [0x73, 0x627] ufo.kerning.update( { ("bar", "bar"): 1, ("bar", "A"): 2, ("reh-ar", "A"): 3, ("reh-ar", "one-ar"): 4, ("yod-hb", "one"): 5, } ) ufo.features.text = dedent( """\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem arab dflt; """ ) with caplog.at_level(logging.INFO): generated = self.writeFeatures(ufo) assert not generated assert ( len([r for r in caplog.records if "with ambiguous direction" in r.message]) == 5 ) def test_kern_RTL_and_DFLT_numbers(self, FontClass): glyphs = {"four": 0x34, "seven": 0x37, "bet-hb": 0x5D1, "yod-hb": 0x5D9} kerning = {("seven", "four"): -25, ("yod-hb", "bet-hb"): -100} features = dedent( """\ languagesystem DFLT dflt; languagesystem hebr dflt; """ ) ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) generated = self.writeFeatures(ufo) assert dedent(str(generated)) == dedent( """\ lookup kern_Hebr { lookupflag IgnoreMarks; pos yod-hb bet-hb <-100 0 -100 0>; } kern_Hebr; lookup kern_Default { lookupflag IgnoreMarks; pos seven four -25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; lookup kern_Hebr; script hebr; language dflt; lookup kern_Default; lookup kern_Hebr; } kern; """ ) def test_quantize(self, FontClass): font = FontClass() for name in ("one", "four", "six"): font.newGlyph(name) font.kerning.update({("four", "six"): -57.0, ("one", "six"): -24.0}) writer = KernFeatureWriter(quantization=5) feaFile = ast.FeatureFile() assert writer.write(font, feaFile) assert str(feaFile) == dedent( """\ lookup kern_Default { lookupflag IgnoreMarks; pos four six -55; pos one six -25; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) def test_skip_spacing_marks(self, FontClass): dirname = os.path.dirname(os.path.dirname(__file__)) fontPath = os.path.join(dirname, "data", "SpacingCombiningTest-Regular.ufo") testufo = FontClass(fontPath) generated = self.writeFeatures(testufo) assert dedent(str(generated)) == dedent( """\ lookup kern_Deva { @MFS_kern_Deva = [highspacingdot-deva]; lookupflag UseMarkFilteringSet @MFS_kern_Deva; pos ka-deva ra-deva -250; pos ra-deva ka-deva -250; } kern_Deva; lookup kern_Deva_marks { pos highspacingdot-deva ka-deva -200; pos ka-deva highspacingdot-deva -150; } kern_Deva_marks; feature dist { script dev2; language dflt; lookup kern_Deva; lookup kern_Deva_marks; script deva; language dflt; lookup kern_Deva; lookup kern_Deva_marks; } dist; """ ) def test_kern_split_multi_glyph_class(FontClass): glyphs = { "a": ord("a"), "b": ord("b"), "period": ord("."), } groups = { "public.kern1.foo": ["a", "period"], "public.kern2.foo": ["b", "period"], } kerning = { ("a", "a"): 1, ("a", "b"): 2, ("a", "period"): 3, ("b", "a"): 4, ("b", "b"): 5, ("b", "period"): 6, ("period", "a"): 7, ("period", "b"): 8, ("period", "period"): 9, # Class-to-glyph ("public.kern1.foo", "b"): 10, ("public.kern1.foo", "period"): 11, # Glyph-to-class ("a", "public.kern2.foo"): 12, ("period", "public.kern2.foo"): 13, # Class-to-class ("public.kern1.foo", "public.kern2.foo"): 14, } expectation = dedent( """\ @kern1.Default.foo = [period]; @kern1.Latn.foo = [a]; @kern2.Default.foo = [period]; @kern2.Latn.foo = [b]; lookup kern_Latn { lookupflag IgnoreMarks; pos a a 1; pos a b 2; pos a period 3; pos b a 4; pos b b 5; pos b period 6; pos period a 7; pos period b 8; enum pos a @kern2.Latn.foo 12; enum pos a @kern2.Default.foo 12; enum pos period @kern2.Latn.foo 13; enum pos @kern1.Latn.foo b 10; enum pos @kern1.Latn.foo period 11; enum pos @kern1.Default.foo b 10; pos @kern1.Latn.foo @kern2.Latn.foo 14; pos @kern1.Latn.foo @kern2.Default.foo 14; pos @kern1.Default.foo @kern2.Latn.foo 14; } kern_Latn; lookup kern_Default { lookupflag IgnoreMarks; pos period period 9; enum pos period @kern2.Default.foo 13; enum pos @kern1.Default.foo period 11; pos @kern1.Default.foo @kern2.Default.foo 14; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; lookup kern_Latn; script latn; language dflt; lookup kern_Default; lookup kern_Latn; } kern; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)).lstrip("\n") == expectation # Making a common glyph implicitly have an explicit script assigned (GSUB # closure) will still keep it in the common section. features = dedent( """ feature ss01 { sub a by period; # Make period be both Latn and Zyyy. } ss01; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)).lstrip("\n") == expectation def test_kern_split_and_drop(FontClass, caplog): glyphs = { "a": ord("a"), "alpha": ord("α"), "a-orya": 0x0B05, "a-cy": 0x0430, "alef-ar": 0x627, "period": ord("."), } groups = { "public.kern1.foo": ["a", "alpha", "a-orya"], "public.kern2.foo": ["a", "alpha", "a-orya"], "public.kern1.bar": ["a-cy", "alef-ar", "period"], "public.kern2.bar": ["a-cy", "alef-ar", "period"], } kerning = { ("public.kern1.foo", "public.kern2.bar"): 20, ("public.kern1.bar", "public.kern2.foo"): 20, } ufo = makeUFO(FontClass, glyphs, groups, kerning) with caplog.at_level(logging.INFO): newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ @kern1.Cyrl_Grek_Latn_Orya.bar = [a-cy]; @kern1.Cyrl_Grek_Latn_Orya.bar_1 = [period]; @kern1.Cyrl_Grek_Latn_Orya.foo = [a a-orya alpha]; @kern2.Cyrl_Grek_Latn_Orya.bar = [a-cy]; @kern2.Cyrl_Grek_Latn_Orya.bar_1 = [period]; @kern2.Cyrl_Grek_Latn_Orya.foo = [a a-orya alpha]; lookup kern_Cyrl_Grek_Latn_Orya { lookupflag IgnoreMarks; pos @kern1.Cyrl_Grek_Latn_Orya.foo @kern2.Cyrl_Grek_Latn_Orya.bar 20; pos @kern1.Cyrl_Grek_Latn_Orya.foo @kern2.Cyrl_Grek_Latn_Orya.bar_1 20; pos @kern1.Cyrl_Grek_Latn_Orya.bar @kern2.Cyrl_Grek_Latn_Orya.foo 20; pos @kern1.Cyrl_Grek_Latn_Orya.bar_1 @kern2.Cyrl_Grek_Latn_Orya.foo 20; } kern_Cyrl_Grek_Latn_Orya; feature kern { script DFLT; language dflt; lookup kern_Cyrl_Grek_Latn_Orya; script cyrl; language dflt; lookup kern_Cyrl_Grek_Latn_Orya; script grek; language dflt; lookup kern_Cyrl_Grek_Latn_Orya; script latn; language dflt; lookup kern_Cyrl_Grek_Latn_Orya; } kern; feature dist { script ory2; language dflt; lookup kern_Cyrl_Grek_Latn_Orya; script orya; language dflt; lookup kern_Cyrl_Grek_Latn_Orya; } dist; """ ) assert caplog.messages == [ "Skipping kerning pair <('a', 'a-orya', 'alpha') ('a-cy', 'alef-ar', 'period') 20> with mixed direction (LTR, RTL)", "Skipping kerning pair <('a-cy', 'alef-ar', 'period') ('a', 'a-orya', 'alpha') 20> with mixed direction (RTL, LTR)", "Merging kerning lookups from the following scripts: Cyrl, Grek, Latn, Orya", ] def test_kern_split_and_drop_mixed(caplog, FontClass): """Test that mixed script pairs don't go anywhere.""" glyphs = {"V": ord("V"), "W": ord("W"), "gba-nko": 0x07DC} groups = {"public.kern1.foo": ["V", "W"], "public.kern2.foo": ["gba-nko", "W"]} kerning = {("public.kern1.foo", "public.kern2.foo"): -20} ufo = makeUFO(FontClass, glyphs, groups, kerning) with caplog.at_level(logging.INFO): newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ @kern1.Latn.foo = [V W]; @kern2.Latn.foo = [W]; lookup kern_Latn { lookupflag IgnoreMarks; pos @kern1.Latn.foo @kern2.Latn.foo -20; } kern_Latn; feature kern { script DFLT; language dflt; lookup kern_Latn; script latn; language dflt; lookup kern_Latn; } kern; """ ) assert ( "Skipping kerning pair <('V', 'W') ('W', 'gba-nko') -20> with mixed direction (LTR, RTL)" in caplog.text ) def test_kern_split_and_mix_common(FontClass): """Test that that everyone gets common-script glyphs, but they get it per-script.""" glyphs = {"V": ord("V"), "W": ord("W"), "gba-nko": 0x07DC, "period": ord(".")} groups = {"public.kern1.foo": ["V", "gba-nko", "W"]} kerning = {("public.kern1.foo", "period"): -20} ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ @kern1.Latn.foo = [V W]; @kern1.Nkoo.foo = [gba-nko]; lookup kern_Latn { lookupflag IgnoreMarks; enum pos @kern1.Latn.foo period -20; } kern_Latn; lookup kern_Nkoo { lookupflag IgnoreMarks; enum pos @kern1.Nkoo.foo period <-20 0 -20 0>; } kern_Nkoo; feature kern { script DFLT; language dflt; lookup kern_Latn; script latn; language dflt; lookup kern_Latn; script nko; language dflt; lookup kern_Nkoo; } kern; """ ) def test_kern_keep_common(FontClass): """Test that if both sides are common, the output is common.""" glyphs = {"period": ord(".")} kerning = {("period", "period"): -20} ufo = makeUFO(FontClass, glyphs, None, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ lookup kern_Default { lookupflag IgnoreMarks; pos period period -20; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) def test_kern_multi_script(FontClass): """Test that glyphs with more than one script get associated with all of the relevant scripts in the pair.""" glyphs = {"gba-nko": 0x07DC, "comma-ar": 0x060C, "lam-ar": 0x0644} groups = { "public.kern1.foo": ["lam-ar", "gba-nko"], "public.kern2.foo": ["comma-ar"], } kerning = {("public.kern1.foo", "public.kern2.foo"): -20} ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ @kern1.Arab_Nkoo.foo = [gba-nko lam-ar]; @kern2.Arab_Nkoo.foo = [comma-ar]; lookup kern_Arab_Nkoo { lookupflag IgnoreMarks; pos @kern1.Arab_Nkoo.foo @kern2.Arab_Nkoo.foo <-20 0 -20 0>; } kern_Arab_Nkoo; feature kern { script DFLT; language dflt; lookup kern_Arab_Nkoo; script arab; language dflt; lookup kern_Arab_Nkoo; script nko; language dflt; lookup kern_Arab_Nkoo; } kern; """ ) def test_kern_mixed_bidis(caplog, FontClass): """Test that BiDi types for pairs are respected.""" glyphs = { "a": ord("a"), "comma": ord(","), "alef-ar": 0x0627, "comma-ar": 0x060C, "one-ar": 0x0661, } kerning = { # Undetermined: LTR ("comma", "comma"): -1, # LTR ("a", "a"): 1, ("a", "comma"): 2, ("comma", "a"): 3, # RTL ("alef-ar", "alef-ar"): 4, ("alef-ar", "comma-ar"): 5, ("comma-ar", "alef-ar"): 6, # Mixed: should be dropped ("alef-ar", "one-ar"): 7, ("one-ar", "alef-ar"): 8, # LTR despite being an RTL script ("one-ar", "one-ar"): 9, } ufo = makeUFO(FontClass, glyphs, None, kerning) with caplog.at_level(logging.INFO): newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ lookup kern_Arab { lookupflag IgnoreMarks; pos alef-ar alef-ar <4 0 4 0>; pos alef-ar comma-ar <5 0 5 0>; pos comma-ar alef-ar <6 0 6 0>; pos one-ar one-ar 9; } kern_Arab; lookup kern_Latn { lookupflag IgnoreMarks; pos a a 1; pos a comma 2; pos comma a 3; } kern_Latn; lookup kern_Default { lookupflag IgnoreMarks; pos comma comma -1; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; lookup kern_Latn; script arab; language dflt; lookup kern_Default; lookup kern_Arab; script latn; language dflt; lookup kern_Default; lookup kern_Latn; } kern; """ ) assert " with ambiguous direction" in caplog.text assert " with ambiguous direction" in caplog.text def unicodeScript(codepoint: int) -> str: """Returns the Unicode script for a codepoint, combining some scripts into the same bucket. This allows lookups to contain more than one script. The most prominent case is being able to kern Hiragana and Katakana against each other, Unicode defines "Hrkt" as an alias for both scripts. Note: Keep in sync with unicodeScriptExtensions! """ script = unicodedata.script(chr(codepoint)) return UNICODE_SCRIPT_ALIASES.get(script, script) def test_kern_zyyy_zinh(FontClass): """Test that a sampling of glyphs with a common or inherited script, but a disjoint set of explicit script extensions end up in the correct lookups.""" glyphs = {} for i in range(0, 0x110000, 0x10): script = unicodeScript(i) script_extension = unicodeScriptExtensions(i) if script not in script_extension: assert script in DFLT_SCRIPTS name = f"uni{i:04X}" glyphs[name] = i kerning = {(glyph, glyph): i for i, glyph in enumerate(glyphs)} ufo = makeUFO(FontClass, glyphs, None, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ lookup kern_Deva { lookupflag IgnoreMarks; pos uni1CD0 uni1CD0 7; pos uni1CE0 uni1CE0 8; pos uni1CF0 uni1CF0 9; pos uni20F0 uni20F0 11; pos uniA830 uniA830 34; } kern_Deva; lookup kern_Dupl { lookupflag IgnoreMarks; pos uni1BCA0 uni1BCA0 42; } kern_Dupl; lookup kern_Grek { lookupflag IgnoreMarks; pos uni0300 uni0300 0; pos uni1DC0 uni1DC0 10; } kern_Grek; lookup kern_Hani_Hrkt { lookupflag IgnoreMarks; pos uni1D360 uni1D360 43; pos uni1D370 uni1D370 44; pos uni1F250 uni1F250 45; pos uni2FF0 uni2FF0 13; pos uni3010 uni3010 14; pos uni3030 uni3030 15; pos uni30A0 uni30A0 16; pos uni3190 uni3190 17; pos uni31C0 uni31C0 18; pos uni31D0 uni31D0 19; pos uni31E0 uni31E0 20; pos uni3220 uni3220 21; pos uni3230 uni3230 22; pos uni3240 uni3240 23; pos uni3280 uni3280 24; pos uni3290 uni3290 25; pos uni32A0 uni32A0 26; pos uni32B0 uni32B0 27; pos uni32C0 uni32C0 28; pos uni3360 uni3360 29; pos uni3370 uni3370 30; pos uni33E0 uni33E0 31; pos uni33F0 uni33F0 32; pos uniA700 uniA700 33; pos uniFF70 uniFF70 35; } kern_Hani_Hrkt; lookup kern_Default { lookupflag IgnoreMarks; pos uni0310 uni0310 1; pos uni0320 uni0320 2; pos uni0330 uni0330 3; pos uni0640 uni0640 4; pos uni0650 uni0650 5; pos uni0670 uni0670 6; pos uni10100 uni10100 36; pos uni10110 uni10110 37; pos uni10120 uni10120 38; pos uni10130 uni10130 39; pos uni102E0 uni102E0 40; pos uni102F0 uni102F0 41; pos uni2E30 uni2E30 12; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; lookup kern_Grek; lookup kern_Hani_Hrkt; script grek; language dflt; lookup kern_Default; lookup kern_Grek; script hani; language dflt; lookup kern_Default; lookup kern_Hani_Hrkt; script kana; language dflt; lookup kern_Default; lookup kern_Hani_Hrkt; } kern; feature dist { script dev2; language dflt; lookup kern_Default; lookup kern_Deva; script deva; language dflt; lookup kern_Default; lookup kern_Deva; script dupl; language dflt; lookup kern_Default; lookup kern_Dupl; } dist; """ ) def test_kern_hira_kana_hrkt(FontClass): """Test that Hiragana and Katakana lands in the same lookup and can be kerned against each other and common glyphs are kerned just once.""" glyphs = {"a-hira": 0x3042, "a-kana": 0x30A2, "period": ord(".")} kerning = { ("a-hira", "a-hira"): 1, ("a-hira", "a-kana"): 2, ("a-kana", "a-hira"): 3, ("a-kana", "a-kana"): 4, ("period", "period"): 5, ("a-hira", "period"): 6, ("period", "a-hira"): 7, ("a-kana", "period"): 8, ("period", "a-kana"): 9, } ufo = makeUFO(FontClass, glyphs, None, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ lookup kern_Hrkt { lookupflag IgnoreMarks; pos a-hira a-hira 1; pos a-hira a-kana 2; pos a-hira period 6; pos a-kana a-hira 3; pos a-kana a-kana 4; pos a-kana period 8; pos period a-hira 7; pos period a-kana 9; } kern_Hrkt; lookup kern_Default { lookupflag IgnoreMarks; pos period period 5; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; lookup kern_Hrkt; script kana; language dflt; lookup kern_Default; lookup kern_Hrkt; } kern; """ ) # flake8: noqa: B950 def test_defining_classdefs(FontClass): """Check that we aren't redefining class definitions with different content.""" glyphs = { "halant-telugu": 0xC4D, # Telu "ka-telugu.below": None, # Telu by substitution "ka-telugu": 0xC15, # Telu "rVocalicMatra-telugu": 0xC43, # Telu "sha-telugu.below": None, # Default "ss-telugu.alt": None, # Default "ssa-telugu.alt": None, # Telu by substitution "ssa-telugu": 0xC37, # Telu } groups = { "public.kern1.sha-telugu.below": ["sha-telugu.below"], # The following group is a mix of Telu and Default through its gylphs. The # kerning for bases below will create a Telu and Default split group. # Important for the NOTE below. "public.kern1.ssa-telugu.alt": ["ssa-telugu.alt", "ss-telugu.alt"], "public.kern2.ka-telugu.below": ["ka-telugu.below"], "public.kern2.rVocalicMatra-telugu": ["rVocalicMatra-telugu"], } kerning = { # The follwoing three pairs are base-to-base pairs: ("public.kern1.sha-telugu.below", "public.kern2.ka-telugu.below"): 20, ("public.kern1.ssa-telugu.alt", "public.kern2.ka-telugu.below"): 60, ("public.kern1.ssa-telugu.alt", "sha-telugu.below"): 150, # NOTE: This last pair kerns bases against marks, triggering an extra # pass to make a mark lookup that will create new classDefs. This extra # pass will work on just this one pair, and kern splitting won't split # off a Default group from `public.kern1.ssa-telugu.alt`, you get just a # Telu pair. Unless the writer keeps track of which classDefs it already # generated, this will overwrite the previous `@kern1.Telu.ssatelugu.alt # = [ssa-telugu.alt]` with `@kern1.Telu.ssatelugu.alt = # [ss-telugu.alt]`, losing kerning. ("public.kern1.ssa-telugu.alt", "public.kern2.rVocalicMatra-telugu"): 180, } features = """ feature blwf { script tel2; sub halant-telugu ka-telugu by ka-telugu.below; } blwf; feature psts { script tel2; sub ssa-telugu' [rVocalicMatra-telugu sha-telugu.below ka-telugu.below] by ssa-telugu.alt; } psts; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) ufo.lib["public.openTypeCategories"] = { "halant-telugu": "mark", "ka-telugu": "base", "rVocalicMatra-telugu": "mark", "ss-telugu.alt": "base", "ssa-telugu.alt": "base", "ssa-telugu": "base", } newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ @kern1.Default.ssatelugu.alt = [ss-telugu.alt]; @kern1.Telu.shatelugu.below = [sha-telugu.below]; @kern1.Telu.ssatelugu.alt = [ssa-telugu.alt]; @kern2.Telu.katelugu.below = [ka-telugu.below]; @kern2.Telu.rVocalicMatratelugu = [rVocalicMatra-telugu]; lookup kern_Telu { lookupflag IgnoreMarks; enum pos @kern1.Telu.ssatelugu.alt sha-telugu.below 150; pos @kern1.Telu.shatelugu.below @kern2.Telu.katelugu.below 20; pos @kern1.Default.ssatelugu.alt @kern2.Telu.katelugu.below 60; pos @kern1.Telu.ssatelugu.alt @kern2.Telu.katelugu.below 60; } kern_Telu; lookup kern_Telu_marks { pos @kern1.Default.ssatelugu.alt @kern2.Telu.rVocalicMatratelugu 180; pos @kern1.Telu.ssatelugu.alt @kern2.Telu.rVocalicMatratelugu 180; } kern_Telu_marks; lookup kern_Default { lookupflag IgnoreMarks; enum pos @kern1.Default.ssatelugu.alt sha-telugu.below 150; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; feature dist { script tel2; language dflt; lookup kern_Default; lookup kern_Telu; lookup kern_Telu_marks; script telu; language dflt; lookup kern_Default; lookup kern_Telu; lookup kern_Telu_marks; } dist; """ ) def test_mark_base_kerning(FontClass): """Check that kerning of bases against marks is correctly split into base-only and mixed-mark-and-base lookups, to preserve the semantics of kerning exceptions (pairs modifying the effect of other pairs).""" glyphs = { "aa-tamil": 0x0B86, "va-tamil": 0x0BB5, "aulengthmark-tamil": 0x0BD7, } groups = { # Each group is a mix of mark and base glyph. "public.kern1.e-tamil": ["aulengthmark-tamil", "va-tamil"], "public.kern2.e-tamil": ["aulengthmark-tamil", "va-tamil"], } kerning = { ("aa-tamil", "va-tamil"): -20, ("aa-tamil", "public.kern2.e-tamil"): -35, ("va-tamil", "aa-tamil"): -20, ("public.kern1.e-tamil", "aa-tamil"): -35, ("aulengthmark-tamil", "aulengthmark-tamil"): -200, ("public.kern1.e-tamil", "public.kern2.e-tamil"): -100, } ufo = makeUFO(FontClass, glyphs, groups, kerning) ufo.lib["public.openTypeCategories"] = { "aulengthmark-tamil": "mark", } newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ @kern1.Taml.etamil = [va-tamil]; @kern1.Taml.etamil_1 = [aulengthmark-tamil]; @kern2.Taml.etamil = [va-tamil]; @kern2.Taml.etamil_1 = [aulengthmark-tamil]; lookup kern_Taml { lookupflag IgnoreMarks; pos aa-tamil va-tamil -20; pos va-tamil aa-tamil -20; enum pos aa-tamil @kern2.Taml.etamil -35; enum pos @kern1.Taml.etamil aa-tamil -35; pos @kern1.Taml.etamil @kern2.Taml.etamil -100; } kern_Taml; lookup kern_Taml_marks { pos aulengthmark-tamil aulengthmark-tamil -200; enum pos aa-tamil @kern2.Taml.etamil_1 -35; enum pos @kern1.Taml.etamil_1 aa-tamil -35; pos @kern1.Taml.etamil_1 @kern2.Taml.etamil_1 -100; pos @kern1.Taml.etamil_1 @kern2.Taml.etamil -100; pos @kern1.Taml.etamil @kern2.Taml.etamil_1 -100; } kern_Taml_marks; feature dist { script tml2; language dflt; lookup kern_Taml; lookup kern_Taml_marks; script taml; language dflt; lookup kern_Taml; lookup kern_Taml_marks; } dist; """ ) def test_hyphenated_duplicates(FontClass): """Check that kerning group names are kept separate even if their sanitized names are the same.""" glyphs = {"comma": ord(","), "period": ord(".")} groups = { "public.kern1.hy-phen": ["comma"], "public.kern1.hyp-hen": ["period"], } kerning = { ("public.kern1.hy-phen", "comma"): 1, ("public.kern1.hyp-hen", "period"): 2, } ufo = makeUFO(FontClass, glyphs, groups, kerning) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ @kern1.Default.hyphen = [comma]; @kern1.Default.hyphen_1 = [period]; lookup kern_Default { lookupflag IgnoreMarks; enum pos @kern1.Default.hyphen comma 1; enum pos @kern1.Default.hyphen_1 period 2; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; } kern; """ ) def test_dflt_language(FontClass): """Check that languages defined for the special DFLT script are registered as well.""" glyphs = {"a": ord("a"), "comma": ord(",")} groups = {} kerning = {("a", "a"): 1, ("comma", "comma"): 2} features = """ languagesystem DFLT dflt; languagesystem DFLT ZND; languagesystem latn dflt; languagesystem latn ANG; """ ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = KernFeatureWriterTest.writeFeatures(ufo) assert dedent(str(newFeatures)) == dedent( """\ lookup kern_Latn { lookupflag IgnoreMarks; pos a a 1; } kern_Latn; lookup kern_Default { lookupflag IgnoreMarks; pos comma comma 2; } kern_Default; feature kern { script DFLT; language dflt; lookup kern_Default; lookup kern_Latn; language ZND; script latn; language dflt; lookup kern_Default; lookup kern_Latn; language ANG; } kern; """ ) if __name__ == "__main__": import sys sys.exit(pytest.main(sys.argv)) ufo2ft-3.3.1/tests/featureWriters/markFeatureWriter_test.py000066400000000000000000002173041470175262700241760ustar00rootroot00000000000000import logging import os import re from textwrap import dedent import pytest from ufo2ft.constants import OBJECT_LIBS_KEY from ufo2ft.featureCompiler import FeatureCompiler, parseLayoutFeatures from ufo2ft.featureWriters import ast from ufo2ft.featureWriters.markFeatureWriter import ( MarkFeatureWriter, NamedAnchor, parseAnchorName, ) from . import FeatureWriterTest @pytest.fixture def testufo(FontClass): ufo = FontClass() ufo.newGlyph("a").appendAnchor({"name": "top", "x": 100, "y": 200}) liga = ufo.newGlyph("f_i") liga.appendAnchor({"name": "top_1", "x": 100, "y": 500}) liga.appendAnchor({"name": "top_2", "x": 600, "y": 500}) ufo.newGlyph("acutecomb").appendAnchor({"name": "_top", "x": 100, "y": 200}) accent = ufo.newGlyph("tildecomb") accent.appendAnchor({"name": "_top", "x": 100, "y": 200}) accent.appendAnchor({"name": "top", "x": 100, "y": 300}) return ufo @pytest.mark.parametrize( "input_expected", [ ("top", (False, "top", None, False, False)), ("top_", (False, "top_", None, False, False)), ("top1", (False, "top1", None, False, False)), ("_bottom", (True, "bottom", None, False, False)), ("bottom_2", (False, "bottom", 2, False, False)), ("top_right_1", (False, "top_right", 1, False, False)), ], ) def test_parseAnchorName(input_expected): anchorName, (isMark, key, number, isContextual, isIgnorable) = input_expected assert parseAnchorName(anchorName) == ( isMark, key, number, isContextual, isIgnorable, ) def test_parseAnchorName_invalid(): with pytest.raises(ValueError, match="mark anchor cannot be numbered"): parseAnchorName("_top_2") with pytest.raises(ValueError, match="mark anchor key is nil"): parseAnchorName("_") def test_NamedAnchor_invalid(): with pytest.raises(ValueError, match="indexes must start from 1"): NamedAnchor("top_0", 1, 2) def test_NamedAnchor_repr(): expected = "NamedAnchor(name='top', x=1.0, y=2.0)" assert repr(NamedAnchor("top", 1.0, 2.0)) == expected class MarkFeatureWriterTest(FeatureWriterTest): FeatureWriter = MarkFeatureWriter def test__makeMarkClassDefinitions_empty(self, FontClass): ufo = FontClass() ufo.newGlyph("a").appendAnchor({"name": "top", "x": 250, "y": 500}) ufo.newGlyph("c").appendAnchor({"name": "bottom", "x": 250, "y": -100}) ufo.newGlyph("grave").appendAnchor({"name": "_top", "x": 100, "y": 200}) ufo.newGlyph("cedilla").appendAnchor({"name": "_bottom", "x": 100, "y": 0}) writer = MarkFeatureWriter() feaFile = ast.FeatureFile() writer.setContext(ufo, feaFile) markClassDefs = writer._makeMarkClassDefinitions() assert len(feaFile.markClasses) == 2 assert [str(mcd) for mcd in markClassDefs] == [ "markClass cedilla @MC_bottom;", "markClass grave @MC_top;", ] def test__makeMarkClassDefinitions_non_empty(self, FontClass): ufo = FontClass() ufo.newGlyph("a").appendAnchor({"name": "top", "x": 250, "y": 500}) ufo.newGlyph("c").appendAnchor({"name": "bottom", "x": 250, "y": -100}) ufo.newGlyph("grave").appendAnchor({"name": "_top", "x": 100, "y": 200}) ufo.newGlyph("cedilla").appendAnchor({"name": "_bottom", "x": 100, "y": 0}) ufo.features.text = dedent( """\ markClass cedilla @MC_bottom; markClass grave @MC_top; """ ) writer = MarkFeatureWriter() feaFile = parseLayoutFeatures(ufo) writer.setContext(ufo, feaFile) markClassDefs = writer._makeMarkClassDefinitions() assert len(markClassDefs) == 1 assert len(feaFile.markClasses) == 3 assert "MC_bottom" in feaFile.markClasses assert "MC_top" in feaFile.markClasses assert [str(mcd) for mcd in markClassDefs] == [ "markClass cedilla @MC_bottom_1;" ] def test_skip_empty_feature(self, FontClass): ufo = FontClass() assert not self.writeFeatures(ufo) ufo.newGlyph("a").appendAnchor({"name": "top", "x": 100, "y": 200}) ufo.newGlyph("acutecomb").appendAnchor({"name": "_top", "x": 100, "y": 200}) fea = str(self.writeFeatures(ufo)) assert "feature mark" in fea assert "feature mkmk" not in fea def test_skip_unnamed_anchors(self, FontClass, caplog): caplog.set_level(logging.ERROR) ufo = FontClass() ufo.newGlyph("a").appendAnchor({"x": 100, "y": 200}) writer = MarkFeatureWriter() feaFile = ast.FeatureFile() logger = "ufo2ft.featureWriters.markFeatureWriter.MarkFeatureWriter" with caplog.at_level(logging.WARNING, logger=logger): writer.setContext(ufo, feaFile) assert len(caplog.records) == 1 assert "unnamed anchor discarded in glyph 'a'" in caplog.text def test_warn_duplicate_anchor_names(self, FontClass, caplog): caplog.set_level(logging.ERROR) ufo = FontClass() ufo.newGlyph("a").anchors = [ {"name": "top", "x": 100, "y": 200}, {"name": "top", "x": 200, "y": 300}, ] writer = MarkFeatureWriter() feaFile = ast.FeatureFile() logger = "ufo2ft.featureWriters.markFeatureWriter.MarkFeatureWriter" with caplog.at_level(logging.WARNING, logger=logger): writer.setContext(ufo, feaFile) assert len(caplog.records) == 1 assert "duplicate anchor 'top' in glyph 'a'" in caplog.text def test_warn_liga_anchor_in_mark_glyph(self, testufo, caplog): caplog.set_level(logging.ERROR) testufo.newGlyph("ogonekcomb").anchors = [ {"name": "_top", "x": 200, "y": -40}, {"name": "top_1", "x": 200, "y": 450}, # should not be there! ] logger = "ufo2ft.featureWriters.markFeatureWriter.MarkFeatureWriter" with caplog.at_level(logging.WARNING, logger=logger): _ = self.writeFeatures(testufo) assert len(caplog.records) == 1 assert "invalid ligature anchor 'top_1' in mark glyph" in caplog.text def test_ligature_NULL_anchor(self, testufo): testufo.newGlyph("f_f_foo").anchors = [ {"name": "top_1", "x": 250, "y": 600}, {"name": "top_2", "x": 500, "y": 600}, {"name": "_3", "x": 0, "y": 0}, # this becomes ] generated = self.writeFeatures(testufo) assert re.search(r"ligComponent\s+", str(generated)) def test_skip_existing_feature(self, testufo): testufo.features.text = dedent( """\ markClass acutecomb @MC_top; feature mark { lookup mark1 { pos base a mark @MC_top; } mark1; } mark; """ ) generated = self.writeFeatures(testufo) # only mkmk is generated, mark was already present assert str(generated) == dedent( """\ markClass tildecomb @MC_top; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_append_feature(self, testufo): testufo.features.text = dedent( """\ markClass acutecomb @MC_top; feature mark { lookup mark1 { pos base a mark @MC_top; } mark1; } mark; """ ) generated = self.writeFeatures(testufo, mode="append") assert str(generated) == dedent( """\ markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_insert_comment_before(self, testufo): writer = MarkFeatureWriter() testufo.features.text = dedent( """\ markClass acutecomb @MC_top; feature mark { # # Automatic Code # lookup mark1 { pos base a mark @MC_top; } mark1; } mark; """ ) feaFile = parseLayoutFeatures(testufo) assert writer.write(testufo, feaFile) assert str(feaFile) == dedent( """\ markClass tildecomb @MC_top; markClass acutecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mark { # # lookup mark1 { pos base a mark @MC_top; } mark1; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) # test append mode ignores insert marker generated = self.writeFeatures(testufo, mode="append") assert str(generated) == dedent( """\ markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_insert_comment_after(self, testufo): writer = MarkFeatureWriter() testufo.features.text = dedent( """\ markClass acutecomb @MC_top; feature mark { lookup mark1 { pos base a mark @MC_top; } mark1; # # Automatic Code # } mark; """ ) feaFile = parseLayoutFeatures(testufo) assert writer.write(testufo, feaFile) assert str(feaFile) == dedent( """\ markClass tildecomb @MC_top; markClass acutecomb @MC_top; feature mark { lookup mark1 { pos base a mark @MC_top; } mark1; # # } mark; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) # test append mode ignores insert marker generated = self.writeFeatures(testufo, mode="append") assert str(generated) == dedent( """\ markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_insert_comment_middle(self, testufo): writer = MarkFeatureWriter() testufo.features.text = dedent( """\ markClass acutecomb @MC_top; feature mark { lookup mark1 { pos base a mark @MC_top; } mark1; # # Automatic Code # lookup mark2 { pos base a mark @MC_top; } mark2; } mark; """ ) feaFile = parseLayoutFeatures(testufo) writer.write(testufo, feaFile) assert str(feaFile) == dedent( """\ markClass tildecomb @MC_top; markClass acutecomb @MC_top; feature mark { lookup mark1 { pos base a mark @MC_top; } mark1; # } mark; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mark { # lookup mark2 { pos base a mark @MC_top; } mark2; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) # test append mode ignores insert marker generated = self.writeFeatures(testufo, mode="append") assert str(generated) == dedent( """\ markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_insert_comment_outside_block(self, testufo): writer = MarkFeatureWriter() testufo.features.text = dedent( """\ # # Automatic Code # """ ) feaFile = parseLayoutFeatures(testufo) assert writer.write(testufo, feaFile) testufo.features.text = dedent( """\ # # Automatic Code # markClass acutecomb @MC_top; feature mark { lookup mark1 { pos base a mark @MC_top; } mark1; } mark; """ ) feaFile = parseLayoutFeatures(testufo) assert writer.write(testufo, feaFile) # test append mode writer = MarkFeatureWriter(mode="append") assert writer.write(testufo, feaFile) def test_defs_and_lookups_first(self, testufo): testufo.newGlyph("circumflexcomb") writer = MarkFeatureWriter() testufo.features.text = dedent( """\ feature mkmk { # Automatic Code # Move acutecomb down and right if preceded by circumflexcomb lookup move_acutecomb { lookupflag UseMarkFilteringSet [acutecomb circumflexcomb]; pos circumflexcomb acutecomb' <0 20 0 20>; } move_acutecomb; } mkmk; """ ) feaFile = parseLayoutFeatures(testufo) assert writer.write(testufo, feaFile) assert str(feaFile) == dedent( """\ markClass acutecomb @MC_top; markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; feature mkmk { # Move acutecomb down and right if preceded by circumflexcomb lookup move_acutecomb { lookupflag UseMarkFilteringSet [acutecomb circumflexcomb]; pos circumflexcomb acutecomb' <0 20 0 20>; } move_acutecomb; } mkmk; """ ) def test_mark_mkmk_features(self, testufo): writer = MarkFeatureWriter() # by default both mark + mkmk are built feaFile = ast.FeatureFile() assert writer.write(testufo, feaFile) assert str(feaFile) == dedent( """\ markClass acutecomb @MC_top; markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_write_only_one(self, testufo): writer = MarkFeatureWriter(features=["mkmk"]) # only builds "mkmk" feaFile = ast.FeatureFile() assert writer.write(testufo, feaFile) fea = str(feaFile) assert "feature mark" not in fea assert "feature mkmk" in fea writer = MarkFeatureWriter(features=["mark"]) # only builds "mark" feaFile = ast.FeatureFile() assert writer.write(testufo, feaFile) fea = str(feaFile) assert "feature mark" in fea assert "feature mkmk" not in fea def test_predefined_anchor_lists(self, FontClass): """Roboto uses some weird anchor naming scheme, see: https://github.com/google/roboto/blob/ 5700de83856781fa0c097a349e46dbaae5792cb0/ scripts/lib/fontbuild/markFeature.py#L41-L47 """ class RobotoMarkFeatureWriter(MarkFeatureWriter): class NamedAnchor(NamedAnchor): markPrefix = "_mark" ignoreRE = "(^mkmk|_acc$)" ufo = FontClass() a = ufo.newGlyph("a") a.anchors = [ {"name": "top", "x": 250, "y": 600}, {"name": "bottom", "x": 250, "y": -100}, ] f_i = ufo.newGlyph("f_i") f_i.anchors = [ {"name": "top_1", "x": 200, "y": 700}, {"name": "top_2", "x": 500, "y": 700}, ] gravecomb = ufo.newGlyph("gravecomb") gravecomb.anchors = [ {"name": "_marktop", "x": 160, "y": 780}, {"name": "mkmktop", "x": 150, "y": 800}, {"name": "mkmkbottom_acc", "x": 150, "y": 600}, ] ufo.newGlyph("cedillacomb").appendAnchor( {"name": "_markbottom", "x": 200, "y": 0} ) ufo.newGlyph("ogonekcomb").appendAnchor({"name": "_bottom", "x": 180, "y": -10}) writer = RobotoMarkFeatureWriter(groupMarkClasses=True) feaFile = ast.FeatureFile() writer.write(ufo, feaFile) assert str(feaFile) == dedent( """\ markClass cedillacomb @MC_markbottom; markClass gravecomb @MC_marktop; feature mark { lookup mark2base { pos base a mark @MC_markbottom mark @MC_marktop; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_marktop ligComponent mark @MC_marktop; } mark2liga; } mark; feature mkmk { lookup mark2mark_bottom { @MFS_mark2mark_bottom = [cedillacomb gravecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_bottom; pos mark gravecomb mark @MC_markbottom; } mark2mark_bottom; lookup mark2mark_top { @MFS_mark2mark_top = [gravecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark gravecomb mark @MC_marktop; } mark2mark_top; } mkmk; """ # noqa: B950 ) @pytest.mark.parametrize( "groupMarkClasses, expected", [ ( True, dedent( """\ markClass nukta-kannada @MC_bottom; markClass candrabindu-kannada @MC_top; markClass halant-kannada @MC_topright; feature abvm { lookup abvm_mark2base { pos base ka-kannada.base mark @MC_top mark @MC_topright; } abvm_mark2base; } abvm; feature blwm { lookup blwm_mark2base { pos base ka-kannada mark @MC_bottom; pos base ka-kannada.base mark @MC_bottom; } blwm_mark2base; } blwm; feature mark { lookup mark2base { pos base dottedCircle mark @MC_bottom mark @MC_top mark @MC_topright; } mark2base; } mark; """ # noqa: B950 ), ), ( False, dedent( """\ markClass nukta-kannada @MC_bottom; markClass candrabindu-kannada @MC_top; markClass halant-kannada @MC_topright; feature abvm { lookup abvm_mark2base { pos base ka-kannada.base mark @MC_top; } abvm_mark2base; lookup abvm_mark2base_1 { pos base ka-kannada.base mark @MC_topright; } abvm_mark2base_1; } abvm; feature blwm { lookup blwm_mark2base { pos base ka-kannada mark @MC_bottom; pos base ka-kannada.base mark @MC_bottom; } blwm_mark2base; } blwm; feature mark { lookup mark2base { pos base dottedCircle mark @MC_bottom; } mark2base; lookup mark2base_1 { pos base dottedCircle mark @MC_top; } mark2base_1; lookup mark2base_2 { pos base dottedCircle mark @MC_topright; } mark2base_2; } mark; """ # noqa: B950 ), ), ], ) def test_abvm_blwm_features(self, FontClass, groupMarkClasses, expected): ufo = FontClass() ufo.info.unitsPerEm = 1000 dottedCircle = ufo.newGlyph("dottedCircle") dottedCircle.unicode = 0x25CC dottedCircle.anchors = [ {"name": "top", "x": 297, "y": 552}, {"name": "topright", "x": 491, "y": 458}, {"name": "bottom", "x": 297, "y": 0}, ] nukta = ufo.newGlyph("nukta-kannada") nukta.unicode = 0x0CBC nukta.appendAnchor({"name": "_bottom", "x": 0, "y": 0}) nukta = ufo.newGlyph("candrabindu-kannada") nukta.unicode = 0x0C81 nukta.appendAnchor({"name": "_top", "x": 0, "y": 547}) halant = ufo.newGlyph("halant-kannada") halant.unicode = 0x0CCD halant.appendAnchor({"name": "_topright", "x": -456, "y": 460}) ka = ufo.newGlyph("ka-kannada") ka.unicode = 0x0C95 ka.appendAnchor({"name": "bottom", "x": 290, "y": 0}) ka_base = ufo.newGlyph("ka-kannada.base") ka_base.appendAnchor({"name": "top", "x": 291, "y": 547}) ka_base.appendAnchor({"name": "topright", "x": 391, "y": 460}) ka_base.appendAnchor({"name": "bottom", "x": 290, "y": 0}) ufo.features.text = dedent( """\ languagesystem DFLT dflt; languagesystem knda dflt; languagesystem knd2 dflt; feature psts { sub ka-kannada' halant-kannada by ka-kannada.base; } psts; """ ) generated = self.writeFeatures(ufo, groupMarkClasses=groupMarkClasses) assert str(generated) == expected def test_shared_script_char(self, FontClass): ufo = FontClass() ufo.info.unitsPerEm = 1000 dottedCircle = ufo.newGlyph("kashida-ar") dottedCircle.unicode = 0x0640 dottedCircle.anchors = [ {"name": "top", "x": 100, "y": 100}, {"name": "bottom", "x": 100, "y": -100}, ] nukta = ufo.newGlyph("fatha-ar") nukta.unicode = 0x064E nukta.appendAnchor({"name": "_top", "x": 0, "y": 0}) nukta = ufo.newGlyph("kasra-ar") nukta.unicode = 0x0650 nukta.appendAnchor({"name": "_bottom", "x": 0, "y": 547}) ufo.features.text = dedent( """\ languagesystem DFLT dflt; languagesystem arab dflt; """ ) generated = self.writeFeatures(ufo, groupMarkClasses=True) assert str(generated) == dedent( """\ markClass kasra-ar @MC_bottom; markClass fatha-ar @MC_top; feature mark { lookup mark2base { pos base kashida-ar mark @MC_bottom mark @MC_top; } mark2base; } mark; """ # noqa: B950 ) expected = dedent( """\ markClass kasra-ar @MC_bottom; markClass fatha-ar @MC_top; feature abvm { lookup abvm_mark2base { pos base kashida-ar mark @MC_top; } abvm_mark2base; } abvm; feature blwm { lookup blwm_mark2base { pos base kashida-ar mark @MC_bottom; } blwm_mark2base; } blwm; feature mark { lookup mark2base { pos base kashida-ar mark @MC_bottom mark @MC_top; } mark2base; } mark; """ # noqa: B950 ) ufo.features.text = "" generated = self.writeFeatures(ufo, groupMarkClasses=True) assert str(generated) == expected ufo.features.text = dedent( """\ languagesystem DFLT dflt; languagesystem arab dflt; languagesystem adlm dflt; """ ) generated = self.writeFeatures(ufo, groupMarkClasses=True) assert str(generated) == expected @pytest.mark.parametrize( "groupMarkClasses, expected", [ ( True, dedent( """\ markClass barcomb @MC_bar; markClass cedillacomb @MC_bottom; markClass bazcomb @MC_bottom; markClass acutecomb @MC_top; markClass tildecomb @MC_top; markClass foocomb @MC_top; feature abvm { lookup abvm_mark2liga { pos ligature foo_bar_baz mark @MC_top ligComponent ligComponent mark @MC_bar mark @MC_top; pos ligature bar_foo ligComponent mark @MC_top; } abvm_mark2liga; lookup abvm_mark2mark_bar { @MFS_abvm_mark2mark_bar = [barcomb]; lookupflag UseMarkFilteringSet @MFS_abvm_mark2mark_bar; pos mark barcomb mark @MC_bar; } abvm_mark2mark_bar; lookup abvm_mark2mark_top { @MFS_abvm_mark2mark_top = [foocomb]; lookupflag UseMarkFilteringSet @MFS_abvm_mark2mark_top; pos mark foocomb mark @MC_top; } abvm_mark2mark_top; } abvm; feature blwm { lookup blwm_mark2liga { pos ligature foo_bar_baz mark @MC_bottom ligComponent ligComponent ; } blwm_mark2liga; lookup blwm_mark2mark_bottom { @MFS_blwm_mark2mark_bottom = [bazcomb]; lookupflag UseMarkFilteringSet @MFS_blwm_mark2mark_bottom; pos mark bazcomb mark @MC_bottom; } blwm_mark2mark_bottom; } blwm; feature mark { lookup mark2base { pos base a mark @MC_top; pos base c mark @MC_bottom; pos base dottedCircle mark @MC_bar mark @MC_bottom mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_bottom { @MFS_mark2mark_bottom = [cedillacomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_bottom; pos mark cedillacomb mark @MC_bottom; } mark2mark_bottom; lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ # noqa: B950 ), ), ( False, dedent( """\ markClass barcomb @MC_bar; markClass cedillacomb @MC_bottom; markClass bazcomb @MC_bottom; markClass acutecomb @MC_top; markClass tildecomb @MC_top; markClass foocomb @MC_top; feature abvm { lookup abvm_mark2liga { pos ligature foo_bar_baz ligComponent ligComponent mark @MC_bar; } abvm_mark2liga; lookup abvm_mark2liga_1 { pos ligature foo_bar_baz mark @MC_top ligComponent ligComponent mark @MC_top; pos ligature bar_foo ligComponent mark @MC_top; } abvm_mark2liga_1; lookup abvm_mark2mark_bar { @MFS_abvm_mark2mark_bar = [barcomb]; lookupflag UseMarkFilteringSet @MFS_abvm_mark2mark_bar; pos mark barcomb mark @MC_bar; } abvm_mark2mark_bar; lookup abvm_mark2mark_top { @MFS_abvm_mark2mark_top = [foocomb]; lookupflag UseMarkFilteringSet @MFS_abvm_mark2mark_top; pos mark foocomb mark @MC_top; } abvm_mark2mark_top; } abvm; feature blwm { lookup blwm_mark2liga { pos ligature foo_bar_baz mark @MC_bottom ligComponent ligComponent ; } blwm_mark2liga; lookup blwm_mark2mark_bottom { @MFS_blwm_mark2mark_bottom = [bazcomb]; lookupflag UseMarkFilteringSet @MFS_blwm_mark2mark_bottom; pos mark bazcomb mark @MC_bottom; } blwm_mark2mark_bottom; } blwm; feature mark { lookup mark2base { pos base dottedCircle mark @MC_bar; } mark2base; lookup mark2base_1 { pos base c mark @MC_bottom; pos base dottedCircle mark @MC_bottom; } mark2base_1; lookup mark2base_2 { pos base a mark @MC_top; pos base dottedCircle mark @MC_top; } mark2base_2; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_bottom { @MFS_mark2mark_bottom = [cedillacomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_bottom; pos mark cedillacomb mark @MC_bottom; } mark2mark_bottom; lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ # noqa: B950 ), ), ], ) def test_all_features(self, testufo, groupMarkClasses, expected): ufo = testufo ufo.info.unitsPerEm = 1000 ufo.newGlyph("cedillacomb").anchors = [ {"name": "_bottom", "x": 10, "y": -5}, {"name": "bottom", "x": 20, "y": -309}, ] ufo.newGlyph("c").appendAnchor({"name": "bottom", "x": 240, "y": 0}) dottedCircle = ufo.newGlyph("dottedCircle") dottedCircle.unicode = 0x25CC dottedCircle.anchors = [ {"name": "top", "x": 297, "y": 552}, {"name": "bottom", "x": 297, "y": 0}, {"name": "bar", "x": 491, "y": 458}, ] # too lazy, couldn't come up with a real-word example :/ foocomb = ufo.newGlyph("foocomb") foocomb.unicode = 0x0B85 foocomb.anchors = [ {"name": "_top", "x": 100, "y": 40}, {"name": "top", "x": 100, "y": 190}, ] barcomb = ufo.newGlyph("barcomb") barcomb.unicode = 0x0B86 barcomb.anchors = [ {"name": "_bar", "x": 100, "y": 40}, {"name": "bar", "x": 100, "y": 440.1}, ] bazcomb = ufo.newGlyph("bazcomb") bazcomb.unicode = 0x0B87 bazcomb.anchors = [ {"name": "_bottom", "x": 90, "y": 320}, {"name": "bottom", "x": 100, "y": -34}, ] foo_bar_baz = ufo.newGlyph("foo_bar_baz") foo_bar_baz.unicode = 0x0B88 foo_bar_baz.anchors = [ {"name": "top_1", "x": 100, "y": 500}, {"name": "bottom_1", "x": 100, "y": 10}, {"name": "_2", "x": 600, "y": 500}, {"name": "top_3", "x": 1000, "y": 500}, {"name": "bar_3", "x": 1100, "y": 499}, # below half UPEM ] bar_foo = ufo.newGlyph("bar_foo") bar_foo.unicode = 0x0B89 # sequence doesn't start from 1, the first is implied NULL anchor bar_foo.anchors = [{"name": "top_2", "x": 600, "y": 501}] testufo.glyphOrder = [ "a", "f_i", "acutecomb", "tildecomb", "cedillacomb", "c", "dottedCircle", "foocomb", "barcomb", "bazcomb", "foo_bar_baz", "bar_foo", ] ufo.features.text = dedent( """\ languagesystem DFLT dflt; languagesystem taml dflt; """ ) generated = self.writeFeatures(testufo, groupMarkClasses=groupMarkClasses) assert str(generated) == expected def test_mark_mkmk_features_with_GDEF(self, testufo): D = testufo.newGlyph("D") D.anchors = [ {"name": "top", "x": 300, "y": 700}, {"name": "center", "x": 320, "y": 360}, ] # these glyphs have compatible anchors but since they not listed in # the GDEF groups, they won't be included in the mark/mkmk feature testufo.newGlyph("Alpha").appendAnchor({"name": "topleft", "x": -10, "y": 400}) testufo.newGlyph("psili").appendAnchor({"name": "_topleft", "x": 0, "y": 50}) dotaccentcomb = testufo.newGlyph("dotaccentcomb") # this mark glyph has more than one mark anchor, and both will be # generated. Since the two mark anchors cannot cohabit in the same # mark lookup, two lookups will be generated. dotaccentcomb.anchors = [ {"name": "_center", "x": 0, "y": 0}, {"name": "_top", "x": 0, "y": 0}, {"name": "top", "x": 0, "y": 300}, ] testufo.features.text = dedent( """\ @Bases = [a D]; @Marks = [acutecomb tildecomb dotaccentcomb]; table GDEF { GlyphClassDef @Bases, [f_i], @Marks, ; } GDEF; """ ) testufo.glyphOrder = [ "Alpha", "D", "a", "acutecomb", "dotaccentcomb", "f_i", "psili", "tildecomb", ] generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ markClass dotaccentcomb @MC_center; markClass acutecomb @MC_top; markClass dotaccentcomb @MC_top; markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base D mark @MC_center; } mark2base; lookup mark2base_1 { pos base D mark @MC_top; pos base a mark @MC_top; } mark2base_1; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb dotaccentcomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark dotaccentcomb mark @MC_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_mark_mkmk_features_with_GDEF_and_openTypeCategories(self, testufo): # this glyph has compatible anchors and has an openTypeCategories "base" # value D = testufo.newGlyph("D") D.anchors = [ {"name": "top", "x": 300, "y": 700}, {"name": "center", "x": 320, "y": 360}, ] # these glyphs have compatible anchors but since they not listed in # the GDEF groups, they won't be included in the mark/mkmk feature testufo.newGlyph("Alpha").appendAnchor({"name": "topleft", "x": -10, "y": 400}) testufo.newGlyph("psili").appendAnchor({"name": "_topleft", "x": 0, "y": 50}) dotaccentcomb = testufo.newGlyph("dotaccentcomb") # this mark glyph has more than one mark anchor, and both will be # generated. Since the two mark anchors cannot cohabit in the same # mark lookup, two lookups will be generated. dotaccentcomb.anchors = [ {"name": "_center", "x": 0, "y": 0}, {"name": "_top", "x": 0, "y": 0}, {"name": "top", "x": 0, "y": 300}, ] # will be ignored because in GDEF table below testufo.lib["public.openTypeCategories"] = { "D": "base", "dotaccentcomb": "mark", "tildecomb": "base", } testufo.features.text = dedent( """\ @Bases = [a]; @Marks = [acutecomb tildecomb]; table GDEF { GlyphClassDef @Bases, [f_i], @Marks, ; } GDEF; """ ) testufo.glyphOrder = [ "Alpha", "D", "a", "acutecomb", "dotaccentcomb", "f_i", "psili", "tildecomb", ] generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ markClass acutecomb @MC_top; markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_multiple_anchor_classes_base(self, FontClass): dirname = os.path.dirname(os.path.dirname(__file__)) fontPath = os.path.join(dirname, "data", "MultipleAnchorClasses.ufo") testufo = FontClass(fontPath) generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ markClass acutecomb @MC_topA; markClass acutecomb @MC_topE; feature mark { lookup mark2base { pos base a mark @MC_topA; } mark2base; lookup mark2base_1 { pos base e mark @MC_topE; } mark2base_1; } mark; """ ) @pytest.mark.parametrize( "groupMarkClasses, expected", [ ( True, # 'MC_top' should be last thanks to the anchorSortKey. Arguably # this is wrong and might not match current Glyphs.app's behavior # but we decided to keep for backward compatibility with existing # projects. dedent( """\ markClass acutecomb @MC_top; markClass acutecomb @MC_topOther; feature mark { lookup mark2liga { pos ligature f_f mark @MC_topOther ligComponent mark @MC_topOther; pos ligature f_l ligComponent mark @MC_topOther; } mark2liga; lookup mark2liga_1 { pos ligature f_i mark @MC_top ligComponent mark @MC_top; pos ligature f_l mark @MC_top ligComponent ; } mark2liga_1; } mark; """ ), ), ( False, # with groupMarkClasses=False, lookups are simply sorted by mark # class name alphabetically so 'MC_topOther' is last and wins dedent( """\ markClass acutecomb @MC_top; markClass acutecomb @MC_topOther; feature mark { lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; pos ligature f_l mark @MC_top ligComponent ; } mark2liga; lookup mark2liga_1 { pos ligature f_f mark @MC_topOther ligComponent mark @MC_topOther; pos ligature f_l ligComponent mark @MC_topOther; } mark2liga_1; } mark; """ ), ), ], ) def test_multiple_anchor_classes_liga(self, FontClass, groupMarkClasses, expected): ufo = FontClass() liga = ufo.newGlyph("f_i") liga.appendAnchor({"name": "top_1", "x": 100, "y": 500}) liga.appendAnchor({"name": "top_2", "x": 600, "y": 500}) ligaOther = ufo.newGlyph("f_f") ligaOther.appendAnchor({"name": "topOther_1", "x": 101, "y": 501}) ligaOther.appendAnchor({"name": "topOther_2", "x": 601, "y": 501}) ligaMix = ufo.newGlyph("f_l") ligaMix.appendAnchor({"name": "top_1", "x": 102, "y": 502}) ligaMix.appendAnchor({"name": "topOther_2", "x": 602, "y": 502}) acutecomb = ufo.newGlyph("acutecomb") acutecomb.appendAnchor({"name": "_top", "x": 100, "y": 200}) acutecomb.appendAnchor({"name": "_topOther", "x": 150, "y": 250}) generated = self.writeFeatures(ufo, groupMarkClasses=groupMarkClasses) assert str(generated) == expected @pytest.mark.parametrize( "groupMarkClasses, warning, expected", [ ( True, # 'MC_top' should be last thanks to the anchorSortKey "MC_topOther, MC_top", dedent( """\ markClass acutecomb @MC_top; markClass acutecomb @MC_topOther; feature mark { lookup mark2base { pos base a mark @MC_topOther; } mark2base; lookup mark2base_1 { pos base a mark @MC_top; } mark2base_1; } mark; """ ), ), ( False, # 'MC_topOther' (sorted alphabetically) should be last "MC_top, MC_topOther", dedent( """\ markClass acutecomb @MC_top; markClass acutecomb @MC_topOther; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2base_1 { pos base a mark @MC_topOther; } mark2base_1; } mark; """ ), ), ], ) def test_multiple_anchor_classes_conflict_warning( self, FontClass, caplog, groupMarkClasses, warning, expected ): """Check that when there is an ambiguity in the form of one base glyph and one mark glyph being able to be linked through two different anchor pairs, the mark feature writer emits a warning about the situation but still outputs a valid feature declaraction. The last lookup in that feature declaration will "win" and determine the outcome of mark positioning. See this comment for more information: https://github.com/googlefonts/ufo2ft/pull/416#issuecomment-721693266 """ caplog.set_level(logging.INFO) ufo = FontClass() liga = ufo.newGlyph("a") liga.appendAnchor({"name": "top", "x": 100, "y": 500}) liga.appendAnchor({"name": "topOther", "x": 150, "y": 550}) acutecomb = ufo.newGlyph("acutecomb") acutecomb.appendAnchor({"name": "_top", "x": 100, "y": 200}) acutecomb.appendAnchor({"name": "_topOther", "x": 150, "y": 250}) generated = self.writeFeatures(ufo, groupMarkClasses=groupMarkClasses) assert ( "The base glyph a and mark glyph acutecomb are ambiguously " f"connected by several anchor classes: {warning}. " "The last one will prevail." in caplog.text ) assert str(generated) == expected def test_skipExportGlyphs(self, testufo): testufo.lib["public.skipExportGlyphs"] = ["f_i", "tildecomb"] testufo.glyphOrder = ["a", "f_i", "acutecomb", "tildcomb"] generated = self.writeFeatures(testufo) assert str(generated) == dedent( """\ markClass acutecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; } mark; """ ) def test_quantize(self, testufo): testufo.newGlyph("ogonekcomb").anchors = [ {"name": "_top", "x": 236, "y": 188}, ] testufo.lib["public.skipExportGlyphs"] = ["f_i", "tildecomb"] generated = self.writeFeatures(testufo, quantization=50) assert str(generated) == dedent( """\ markClass acutecomb @MC_top; markClass ogonekcomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; } mark; """ ) def test_extra_substitutions(self, FontClass): dirname = os.path.dirname(os.path.dirname(__file__)) fontPath = os.path.join(dirname, "data", "Alternates-Regular.ufo") testufo = FontClass(fontPath) generated = self.writeFeatures( testufo, compiler=FeatureCompiler( testufo, extraSubstitutions={ "uuMatra-oriya": {"uuMatra-oriya.BRACKET.varAlt01"}, "ka-oriya": {"ka-oriya.BRACKET.varAlt01"}, "lVocalicMatra-oriya": {"lVocalicMatra-oriya.BRACKET.varAlt01"}, }, ), ) assert ( "@MFS_blwm_mark2mark_bottom = [uuMatra-oriya lVocalicMatra-oriya " "ka-oriya.below lVocalicMatra-oriya.BRACKET.varAlt01 " "uuMatra-oriya.BRACKET.varAlt01]" in str(generated) ) def test_contextual_anchors(self, FontClass): dirname = os.path.dirname(os.path.dirname(__file__)) fontPath = os.path.join(dirname, "data", "ContextualAnchorsTest-Regular.ufo") testufo = FontClass(fontPath) writer = MarkFeatureWriter() feaFile = ast.FeatureFile() assert str(feaFile) == "" assert writer.write(testufo, feaFile) assert len(feaFile.markClasses) == 2 assert "MC_bottom" in feaFile.markClasses feature = feaFile.statements[-1] assert feature.name == "mark" # note there are two mark2base lookups because ufo2ft v3 generates one lookup # per mark class (previously 'top' and 'bottom' would go into one lookup) assert str(feature) == dedent( """\ feature mark { lookup mark2base; lookup mark2base_1; lookup ContextualMarkDispatch_0; lookup ContextualMarkDispatch_1; lookup ContextualMarkDispatch_2; } mark; """ ) lookup = feature.statements[-3].lookup assert str(lookup) == ( "lookup ContextualMarkDispatch_0 {\n" " lookupflag UseMarrkFilteringSet [twodotshorizontalbelow];\n" " # reh-ar * behDotess-ar.medi &\n" " pos reh-ar [behDotless-ar.init] behDotess-ar.medi" " @MC_bottom'" " lookup ContextualMark_0;\n" "} ContextualMarkDispatch_0;\n" ) lookup = feature.statements[-2].lookup assert str(lookup) == ( "lookup ContextualMarkDispatch_1 {\n" " lookupflag UseMarrkFilteringSet [twodotsverticalbelow];\n" " # reh-ar *\n" " pos reh-ar [behDotless-ar.init behDotless-ar.init.alt]" " @MC_bottom'" " lookup ContextualMark_1;\n" "} ContextualMarkDispatch_1;\n" ) lookup = feature.statements[-1].lookup assert str(lookup) == ( "lookup ContextualMarkDispatch_2 {\n" " # reh-ar *\n" " pos reh-ar [behDotless-ar.init]" " @MC_bottom'" " lookup ContextualMark_2;\n" "} ContextualMarkDispatch_2;\n" ) def test_contextual_anchors_no_mark_feature(self, testufo): a = testufo["a"] a.appendAnchor({"name": "*top", "x": 200, "y": 200, "identifier": "*top"}) a.lib[OBJECT_LIBS_KEY] = { "*top": { "GPOS_Context": "f *", }, } writer = MarkFeatureWriter(features=["mkmk"]) feaFile = ast.FeatureFile() assert str(feaFile) == "" assert writer.write(testufo, feaFile) assert str(feaFile) == dedent( """\ markClass acutecomb @MC_top; markClass tildecomb @MC_top; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) writer = MarkFeatureWriter() feaFile = ast.FeatureFile() assert str(feaFile) == "" assert writer.write(testufo, feaFile) assert str(feaFile) == dedent( """\ markClass acutecomb @MC_top; markClass tildecomb @MC_top; lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; lookup ContextualMark_0 { pos base a mark @MC_top; } ContextualMark_0; lookup ContextualMarkDispatch_0 { # f * pos f [a] @MC_top' lookup ContextualMark_0; } ContextualMarkDispatch_0; feature mark { lookup mark2base; lookup mark2liga; lookup ContextualMarkDispatch_0; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_contextual_liga_anchors(self, testufo): a = testufo["a"] a.appendAnchor({"name": "*top", "x": 200, "y": 200, "identifier": "*top"}) a.lib[OBJECT_LIBS_KEY] = { "*top": { "GPOS_Context": "f *", }, } fi = testufo["f_i"] fi.appendAnchor( {"name": "*top_1.tilde", "x": 300, "y": 500, "identifier": "*top_1.tilde"} ) fi.appendAnchor( {"name": "*top_1.acute", "x": 200, "y": 300, "identifier": "*top_1.acute"} ) fi.lib[OBJECT_LIBS_KEY] = { "*top_1.tilde": { "GPOS_Context": "* tildecomb", }, "*top_1.acute": { "GPOS_Context": "* acutecomb", }, } fl = testufo.newGlyph("f_l") fl.appendAnchor({"name": "top_1", "x": 200, "y": 400}) fl.appendAnchor({"name": "top_2", "x": 500, "y": 400}) fl.appendAnchor({"name": "*top_2", "x": 100, "y": 400, "identifier": "*top_2"}) fl.lib[OBJECT_LIBS_KEY] = { "*top_2": { "GPOS_Context": "* tildecomb", }, } writer = MarkFeatureWriter() feaFile = ast.FeatureFile() assert str(feaFile) == "" assert writer.write(testufo, feaFile) assert str(feaFile) == dedent( """\ markClass acutecomb @MC_top; markClass tildecomb @MC_top; lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; pos ligature f_l mark @MC_top ligComponent mark @MC_top; } mark2liga; lookup ContextualMark_0 { pos base a mark @MC_top; } ContextualMark_0; lookup ContextualMark_1 { pos ligature f_i mark @MC_top ligComponent ; pos ligature f_l ligComponent mark @MC_top; } ContextualMark_1; lookup ContextualMark_2 { pos ligature f_i mark @MC_top ligComponent ; } ContextualMark_2; lookup ContextualMarkDispatch_0 { # f * pos f [a] @MC_top' lookup ContextualMark_0; # * tildecomb pos [f_i f_l] @MC_top' lookup ContextualMark_1 tildecomb; # * acutecomb pos [f_i] @MC_top' lookup ContextualMark_2 acutecomb; } ContextualMarkDispatch_0; feature mark { lookup mark2base; lookup mark2liga; lookup ContextualMarkDispatch_0; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_contextual_anchor_no_context(self, testufo, caplog): a = testufo["a"] a.appendAnchor({"name": "*top", "x": 200, "y": 200, "identifier": "*top"}) a.lib[OBJECT_LIBS_KEY] = {"*top": {"foo": "bar"}} writer = MarkFeatureWriter() feaFile = ast.FeatureFile() assert str(feaFile) == "" logger = "ufo2ft.featureWriters.markFeatureWriter.MarkFeatureWriter" with caplog.at_level(logging.WARNING, logger=logger): assert writer.write(testufo, feaFile) assert len(caplog.records) == 1 assert ( "contextual anchor '*top' in glyph 'a' has no context data; skipped" in caplog.text ) assert str(feaFile) == dedent( """\ markClass acutecomb @MC_top; markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base a mark @MC_top; } mark2base; lookup mark2liga { pos ligature f_i mark @MC_top ligComponent mark @MC_top; } mark2liga; } mark; feature mkmk { lookup mark2mark_top { @MFS_mark2mark_top = [acutecomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) def test_ignorable_anchors(self, FontClass): dirname = os.path.dirname(os.path.dirname(__file__)) fontPath = os.path.join(dirname, "data", "IgnoreAnchorsTest-Thin.ufo") testufo = FontClass(fontPath) writer = MarkFeatureWriter() feaFile = ast.FeatureFile() assert str(feaFile) == "" assert writer.write(testufo, feaFile) assert len(feaFile.markClasses) == 1 assert "MC_top" in feaFile.markClasses feature = feaFile.statements[-2] assert feature.name == "mark" assert len(feature.statements) == 1 lookup = feature.statements[0] assert len(lookup.statements) == 4 for statement in lookup.statements: assert isinstance(statement, ast.MarkBasePosStatement) assert len(statement.marks) == 1 assert statement.marks[0][1].name == "MC_top" if __name__ == "__main__": import sys sys.exit(pytest.main(sys.argv)) ufo2ft-3.3.1/tests/featureWriters/variableFeatureWriter_test.py000066400000000000000000000116561470175262700250330ustar00rootroot00000000000000import 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; markClass gravecmb @MC_top; feature curs { lookup curs_rtl { lookupflag RightToLeft IgnoreMarks; pos cursive alef-ar.fina ; pos cursive peh-ar.init ; pos cursive peh-ar.init.BRACKET.varAlt01 ; } curs_rtl; } curs; 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; pos base a mark @MC_top; } mark2base; } mark; table GDEF { LigatureCaretByPos peh-ar.init 100; } GDEF; """ # noqa: B950 ) def test_variable_features_old_kern_writer(FontClass): tmp = io.StringIO() designspace = designspaceLib.DesignSpaceDocument.fromfile( "tests/data/TestVarfea.designspace" ) designspace.loadSourceFonts(FontClass) default_source = designspace.findDefault() assert default_source is not None default_ufo = default_source.font assert default_ufo is not None default_ufo.lib["com.github.googlei18n.ufo2ft.featureWriters"] = [ { "module": "ufo2ft.featureWriters.kernFeatureWriter2", "class": "KernFeatureWriter", }, { "module": "ufo2ft.featureWriters.markFeatureWriter", "class": "MarkFeatureWriter", }, { "module": "ufo2ft.featureWriters.gdefFeatureWriter", "class": "GdefFeatureWriter", }, { "module": "ufo2ft.featureWriters.cursFeatureWriter", "class": "CursFeatureWriter", }, ] for index, source in enumerate(designspace.sources): font = source.font font.groups["public.kern1.alef"] = ["alef-ar.fina"] font.groups["public.kern2.alef"] = ["alef-ar.fina"] font.kerning[("public.kern1.alef", "public.kern2.alef")] = index _ = compileVariableTTF(designspace, debugFeatureFile=tmp) assert dedent("\n" + tmp.getvalue()) == dedent( """ markClass dotabove-ar @MC_top; markClass gravecmb @MC_top; @kern1.rtl.alef = [alef-ar.fina]; @kern2.rtl.alef = [alef-ar.fina]; lookup kern_rtl { lookupflag IgnoreMarks; pos alef-ar.fina alef-ar.fina <(wght=100:15 wght=1000:35) 0 (wght=100:15 wght=1000:35) 0>; pos @kern1.rtl.alef @kern2.rtl.alef <(wght=100:0 wght=1000:1) 0 (wght=100:0 wght=1000:1) 0>; } kern_rtl; feature kern { script DFLT; language dflt; lookup kern_rtl; script arab; language dflt; lookup kern_rtl; } kern; feature mark { lookup mark2base { pos base alef-ar.fina mark @MC_top; pos base a mark @MC_top; } mark2base; } mark; table GDEF { LigatureCaretByPos peh-ar.init 100; } GDEF; feature curs { lookup curs_rtl { lookupflag RightToLeft IgnoreMarks; pos cursive alef-ar.fina ; pos cursive peh-ar.init ; pos cursive peh-ar.init.BRACKET.varAlt01 ; } curs_rtl; } curs; """ # noqa: B950 ) ufo2ft-3.3.1/tests/filters/000077500000000000000000000000001470175262700155505ustar00rootroot00000000000000ufo2ft-3.3.1/tests/filters/__init__.py000066400000000000000000000000001470175262700176470ustar00rootroot00000000000000ufo2ft-3.3.1/tests/filters/decomposeComponents_test.py000066400000000000000000000320241470175262700232060ustar00rootroot00000000000000import logging import pytest from fontTools.pens.basePen import MissingComponentError from ufo2ft.filters.decomposeComponents import ( DecomposeComponentsFilter, DecomposeComponentsIFilter, ) from ufo2ft.instantiator import Instantiator from ufo2ft.util import _GlyphSet def test_missing_component_error(FontClass, caplog): ufo = FontClass() a = ufo.newGlyph("a") a.width = 100 pen = a.getPen() pen.moveTo((0, 0)) pen.lineTo((300, 0)) pen.lineTo((300, 300)) pen.lineTo((0, 300)) pen.closePath() aacute = ufo.newGlyph("aacute") aacute.width = 100 pen = aacute.getPen() pen.addComponent("a", (1, 0, 0, 1, 0, 0)) pen.addComponent("acute", (1, 0, 0, 1, 350, 0)) # missing assert len(ufo["aacute"]) == 0 assert len(ufo["aacute"].components) == 2 filter_ = DecomposeComponentsFilter() with pytest.raises(MissingComponentError, match="'acute'"): filter_(ufo) def test_nested_components(FontClass): ufo = FontClass() a = ufo.newGlyph("six.lf") a.width = 100 pen = a.getPen() pen.moveTo((0, 0)) pen.lineTo((300, 0)) pen.lineTo((300, 300)) pen.lineTo((0, 300)) pen.closePath() b = ufo.newGlyph("nine.lf") b.width = 100 pen = b.getPen() pen.addComponent("six.lf", (-1, 0, 0, -1, 0, 0)) c = ufo.newGlyph("nine") c.width = 100 pen = c.getPen() pen.addComponent("nine.lf", (1, 0, 0, 1, 0, 0)) filter_ = DecomposeComponentsFilter() assert filter_(ufo) assert len(ufo["six.lf"]) == 1 assert not ufo["six.lf"].components assert len(ufo["nine.lf"]) == 1 assert not ufo["nine.lf"].components assert len(ufo["nine"]) == 1 assert not ufo["nine"].components @pytest.fixture def ufos_and_glyphSets(FontClass): """Return two parallel lists of UFOs and glyphSets for testing. This fixture creates two UFOs, a Regular and a Bold, each containing 5 glyphs: "agrave" composite glyph composed from "a" and "gravecomb" components, both in turn simple contour glyphs, and "igrave" composed of "dotlessi" and "gravecomb" components. The Regular UFO also contains a 'sparse' Medium layer with only two glyphs: a different "agrave" composite glyph, but no "a" nor "gravecomb" components; and a different shape for the "dotlessi" glyph, but no "igrave" nor "gravecomb". The decomposing (interpolatable) filter should interpolate the missing components or composites on-the-fly using the instantiator, when available. """ regular_ufo = FontClass() a = regular_ufo.newGlyph("a") a.width = 500 a.unicodes = [ord("a")] pen = a.getPointPen() pen.beginPath() pen.addPoint((100, 0), "line") pen.addPoint((400, 0), "line") pen.addPoint((400, 500), "line") pen.addPoint((100, 500), "line") pen.endPath() i = regular_ufo.newGlyph("dotlessi") i.width = 300 i.unicodes = [0x0131] pen = i.getPointPen() pen.beginPath() pen.addPoint((100, 0), "line") pen.addPoint((200, 0), "line") pen.addPoint((200, 500), "line") pen.addPoint((100, 500), "line") pen.endPath() gravecomb = regular_ufo.newGlyph("gravecomb") gravecomb.unicodes = [0x0300] pen = gravecomb.getPointPen() pen.beginPath() pen.addPoint((30, 550), "line") pen.addPoint((0, 750), "line") pen.addPoint((-50, 750), "line") pen.addPoint((0, 550), "line") pen.endPath() agrave = regular_ufo.newGlyph("agrave") agrave.width = a.width agrave.unicodes = [0x00E0] pen = agrave.getPointPen() pen.addComponent("a", (1, 0, 0, 1, 0, 0)) pen.addComponent("gravecomb", (1, 0, 0, 1, 250, 0)) igrave = regular_ufo.newGlyph("igrave") igrave.width = i.width igrave.unicodes = [0x00EC] pen = igrave.getPointPen() pen.addComponent("dotlessi", (1, 0, 0, 1, 0, 0)) pen.addComponent("gravecomb", (1, 0, 0, 1, 150, 0)) # The Medium layer has "agrave" but does not have "a" and "gravecomb" medium_layer = regular_ufo.newLayer("Medium") agrave = medium_layer.newGlyph("agrave") agrave.width = 550 pen = agrave.getPointPen() pen.addComponent("a", (1, 0, 0, 1, 0, 0)) pen.addComponent("gravecomb", (1, 0, 0, 1, 275, 0)) # The Medium layer also has a different "dotlessi" glyph, which the # other layers don't have. i = medium_layer.newGlyph("dotlessi") i.width = 350 pen = i.getPointPen() pen.beginPath() pen.addPoint((100, 0), "line") pen.addPoint((250, 0), "line") pen.addPoint((175, 500), "line") pen.addPoint((175, 500), "line") pen.endPath() bold_ufo = FontClass() a = bold_ufo.newGlyph("a") a.width = 600 pen = a.getPointPen() pen.beginPath() pen.addPoint((150, 0), "line") pen.addPoint((450, 0), "line") pen.addPoint((450, 500), "line") pen.addPoint((150, 500), "line") pen.endPath() i = bold_ufo.newGlyph("dotlessi") i.width = 400 pen = i.getPointPen() pen.beginPath() pen.addPoint((100, 0), "line") pen.addPoint((300, 0), "line") pen.addPoint((300, 500), "line") pen.addPoint((100, 500), "line") pen.endPath() gravecomb = bold_ufo.newGlyph("gravecomb") pen = gravecomb.getPointPen() pen.beginPath() pen.addPoint((40, 550), "line") pen.addPoint((0, 750), "line") pen.addPoint((-70, 750), "line") pen.addPoint((0, 550), "line") pen.endPath() agrave = bold_ufo.newGlyph("agrave") agrave.width = a.width pen = agrave.getPointPen() pen.addComponent("a", (1, 0, 0, 1, 0, 0)) pen.addComponent("gravecomb", (1, 0, 0, 1, 300, 0)) igrave = bold_ufo.newGlyph("igrave") igrave.width = i.width pen = igrave.getPointPen() pen.addComponent("dotlessi", (1, 0, 0, 1, 0, 0)) pen.addComponent("gravecomb", (1, 0, 0, 1, 200, 0)) ufos = [regular_ufo, regular_ufo, bold_ufo] glyphSets = [ _GlyphSet.from_layer(regular_ufo), _GlyphSet.from_layer(regular_ufo, layerName="Medium"), _GlyphSet.from_layer(bold_ufo), ] return ufos, glyphSets class DecomposeComponentsIFilterTest: def test_composite_with_intermediate_master(self, ufos_and_glyphSets): ufos, glyphSets = ufos_and_glyphSets regular_glyphs, medium_glyphs, bold_glyphs = glyphSets assert "agrave" in medium_glyphs assert {"a", "gravecomb"}.isdisjoint(medium_glyphs) instantiator = Instantiator( {"Weight": (100, 100, 200)}, [ ({"Weight": 100}, regular_glyphs), ({"Weight": 150}, medium_glyphs), ({"Weight": 200}, bold_glyphs), ], ) filter_ = DecomposeComponentsIFilter(include={"agrave"}) modified = filter_(ufos, glyphSets, instantiator=instantiator) assert modified == {"agrave"} agrave = regular_glyphs["agrave"] assert len(agrave.components) == 0 assert [[(p.x, p.y) for p in c] for c in agrave] == [ [(100, 0), (400, 0), (400, 500), (100, 500)], [(280, 550), (250, 750), (200, 750), (250, 550)], ] # 'agrave' was fully decomposed also in the medium layer, despite the # latter not containing sources for the "a" and "gravecomb" component glyphs. # These were interpolated on-the-fly while decomposing the composite glyph. agrave = medium_glyphs["agrave"] assert len(agrave.components) == 0 assert [[(p.x, p.y) for p in c] for c in agrave] == [ [(125, 0), (425, 0), (425, 500), (125, 500)], [(310, 550), (275, 750), (215, 750), (275, 550)], ] agrave = bold_glyphs["agrave"] assert len(agrave.components) == 0 assert [[(p.x, p.y) for p in c] for c in agrave] == [ [(150, 0), (450, 0), (450, 500), (150, 500)], [(340, 550), (300, 750), (230, 750), (300, 550)], ] def test_component_with_intermediate_master(self, ufos_and_glyphSets): ufos, glyphSets = ufos_and_glyphSets regular_glyphs, medium_glyphs, bold_glyphs = glyphSets assert {"dotlessi", "gravecomb", "igrave"}.issubset(regular_glyphs) assert {"dotlessi", "gravecomb", "igrave"}.issubset(bold_glyphs) assert "dotlessi" in medium_glyphs assert {"igrave", "gravecomb"}.isdisjoint(medium_glyphs) instantiator = Instantiator( {"Weight": (100, 100, 200)}, [ ({"Weight": 100}, regular_glyphs), ({"Weight": 150}, medium_glyphs), ({"Weight": 200}, bold_glyphs), ], ) filter_ = DecomposeComponentsIFilter(include={"igrave"}) modified = filter_(ufos, glyphSets, instantiator=instantiator) assert modified == {"igrave"} igrave = regular_glyphs["igrave"] assert len(igrave.components) == 0 assert [[(p.x, p.y) for p in c] for c in igrave] == [ [(100, 0), (200, 0), (200, 500), (100, 500)], [(180, 550), (150, 750), (100, 750), (150, 550)], ] # 'igrave' was also decomposed in the Medium layer, despite it was not # originally present; it was added by the filter and interpolated on-the-fly, # because Medium contained a different 'dotlessi' used as a component. igrave = medium_glyphs["igrave"] assert len(igrave.components) == 0 assert [[(p.x, p.y) for p in c] for c in igrave] == [ [(100, 0), (250, 0), (175, 500), (175, 500)], [(210, 550), (175, 750), (115, 750), (175, 550)], ] assert {"dotlessi", "igrave"}.issubset(medium_glyphs) assert "gravecomb" not in medium_glyphs igrave = bold_glyphs["igrave"] assert len(igrave.components) == 0 assert [[(p.x, p.y) for p in c] for c in igrave] == [ [(100, 0), (300, 0), (300, 500), (100, 500)], [(240, 550), (200, 750), (130, 750), (200, 550)], ] def test_without_instantiator(self, ufos_and_glyphSets): # without an instantiator (i.e. when the filter is run from the legacy # `compileInterpolatableTTFs` without a designspace as input but only a buch # of UFOs), the filter will raise a MissingComponentError while # trying to decompose 'agrave', because it can't interpolate the missing # components 'a' and 'gravecomb' ufos, glyphSets = ufos_and_glyphSets medium_glyphs = glyphSets[1] assert {"agrave", "dotlessi"}.issubset(medium_glyphs) assert {"a", "gravecomb", "igrave"}.isdisjoint(medium_glyphs) with pytest.raises(MissingComponentError, match="'a'"): DecomposeComponentsIFilter(include={"agrave"})(ufos, glyphSets) # the filter will not fail to decompose 'igrave' in Regular or Bold, however the # Medium master will not contain decomposed outlines for 'igrave', and # in the VF produced from these masters the 'igrave' will appear different # at runtime from 'dotlessi' when the Medium instance is selected. modified = DecomposeComponentsIFilter(include={"igrave"})(ufos, glyphSets) assert modified == {"igrave"} assert "igrave" not in medium_glyphs def test_locations_from_component_glyphs_get_cached( self, caplog, ufos_and_glyphSets ): ufos, glyphSets = ufos_and_glyphSets regular_glyphs, medium_glyphs, bold_glyphs = glyphSets instantiator = Instantiator( {"Weight": (100, 100, 200)}, [ ({"Weight": 100}, regular_glyphs), ({"Weight": 150}, medium_glyphs), ({"Weight": 200}, bold_glyphs), ], ) philter = DecomposeComponentsIFilter() philter.set_context(ufos, glyphSets, instantiator) igrave_locations = philter.glyphSourceLocations("igrave") # igrave is defined only at Weight 100 and 200 assert igrave_locations == { frozenset({("Weight", 100)}), frozenset({("Weight", 200)}), } # locationsFromComponentGlyphs logs DEBUG messages while traversing # recursively each component glyph with caplog.at_level(logging.DEBUG, logger="ufo2ft.filters"): component_locations = philter.locationsFromComponentGlyphs("igrave") assert "igrave" in caplog.text assert "dotlessi" in caplog.text assert "gravecomb" in caplog.text # one of igrave's components (dotlessi) is also defined at Weight 150 expected_component_locations = igrave_locations | {frozenset({("Weight", 150)})} assert component_locations == expected_component_locations # locationsFromComponentGlyphs uses a cache to avoid traversing again component # glyphs that were visited before; its result isn't expected to change within # the current filter call. caplog.clear() with caplog.at_level(logging.DEBUG, logger="ufo2ft.filters"): component_locations = philter.locationsFromComponentGlyphs("igrave") assert "igrave" in caplog.text assert "dotlessi" not in caplog.text assert "gravecomb" not in caplog.text assert component_locations == expected_component_locations ufo2ft-3.3.1/tests/filters/decomposeTransformedComponents_test.py000066400000000000000000000166761470175262700254320ustar00rootroot00000000000000from ufo2ft.filters.decomposeTransformedComponents import ( DecomposeTransformedComponentsFilter, ) from ufo2ft.preProcessor import TTFInterpolatablePreProcessor class DecomposeTransformedComponentsFilterTest: def test_transformed_components(self, FontClass): ufo = FontClass() a = ufo.newGlyph("six.lf") a.width = 300 pen = a.getPen() pen.moveTo((0, 0)) pen.lineTo((300, 0)) pen.lineTo((150, 300)) pen.closePath() # six has one component c = ufo.newGlyph("six") c.width = 300 pen = c.getPen() pen.addComponent("six.lf", (1, 0, 0, 1, 0, 0)) # nine.lf has one transformed component of a component b = ufo.newGlyph("nine.lf") b.width = 300 pen = b.getPen() pen.addComponent("six.lf", (-1, 0, 0, -1, 0, 0)) # nine has one transformed component c = ufo.newGlyph("nine") c.width = 300 pen = c.getPen() pen.addComponent("six", (-1, 0, 0, -1, 0, 0)) # nine.of has one component of a transformed component d = ufo.newGlyph("nine.of") d.width = 300 pen = d.getPen() pen.addComponent("nine", (1, 0, 0, 1, 0, -80)) filter_ = DecomposeTransformedComponentsFilter() assert filter_(ufo) # six.lf has one outline and no component assert len(ufo["six.lf"]) == 1 assert not ufo["six.lf"].components # six has no outline and one component assert len(ufo["six"]) == 0 assert len(ufo["six"].components) == 1 # nine.lf has one outline and no component, it was decomposed assert len(ufo["nine.lf"]) == 1 assert not ufo["nine.lf"].components # nine has one outline and no component, it was decomposed assert len(ufo["nine"]) == 1 assert not ufo["nine"].components # nine.of has no outline and one component, it was not decomposed assert len(ufo["nine.of"]) == 0 assert len(ufo["nine.of"].components) == 1 def test_decompose_compatibly(self, FontClass): ufo1 = FontClass() c = ufo1.newGlyph("comp") c.width = 300 pen = c.getPen() pen.moveTo((0, 0)) pen.lineTo((300, 0)) pen.lineTo((150, 300)) pen.closePath() b = ufo1.newGlyph("base") b.width = 300 pen = b.getPen() pen.addComponent("comp", (0.5, 0, 0, 0.5, 0, 0)) ufo2 = FontClass() c = ufo2.newGlyph("comp") c.width = 600 pen = c.getPen() pen.moveTo((0, 0)) pen.lineTo((600, 0)) pen.lineTo((300, 600)) pen.closePath() b = ufo2.newGlyph("base") b.width = 600 pen = b.getPen() pen.addComponent("comp", (1, 0, 0, 1, 0, 0)) # Because ufo1.base needs decomposing, so should ufo2.base glyphsets = TTFInterpolatablePreProcessor( [ufo1, ufo2], filters=[DecomposeTransformedComponentsFilter(pre=True)] ).process() assert len(glyphsets[0]["base"]) == 1 assert len(glyphsets[1]["base"]) == 1 def test_decompose_compatibly_nested_transformed_components(self, FontClass): # This replicates three glyphs from the 'changa.zip' test fonts at # https://github.com/googlefonts/ufo2ft/issues/621 # In both fonts, the "exclam" glyph is made of one simple contour and one # component ("period"); "exclamdown" in turn is made of one "exclam" component # that is flipped vertically and horizontally; "period" is a single contour. # But only in the Bold.ufo, the "exclam" contains a scaled down "period"; in # the Regular.ufo, the "period" component only has an offset. # This would previously trigger a situation whereby after "exclamdown" was # decomposed, its points were no longer interpolation compatible across masters # because the order in which the contours were decomposed was different. # This is because filters used to modify glyphs in-place in alphabetical order, # so 'exclam' comes before 'exclamdown', and in the Bold.ufo, 'exclam' has # a 2x3 transform so is decomposed (with the period appended at the end), but # then 'exclamdown' needs decomposing as well (for it's flipped) and the # already decomposed 'exclam' contours are drawn onto it in the same order; # whereas in Regular.ufo, the 'exclam' does not contain transformed components # so it's kept as composite (for the time being, it will be decomposed later on # because it's mixed), but when it's the turn of 'exclamdown', the period's # contour gets appended to it before the rest of the rest of the 'exclam' # (deepCopyContours follows a post-order depth-first traversal so the children # get decomposed before the parent) -- leading to cu2qu crashing... Pfew! regular_ufo = FontClass() period = regular_ufo.newGlyph("period") period.width = 230 pen = period.getPen() pen.moveTo((50, 62)) pen.curveTo((50, 13), (61, -6), (115, -6)) pen.curveTo((168, -6), (180, 13), (180, 62)) pen.curveTo((180, 110), (168, 131), (115, 131)) pen.curveTo((61, 131), (50, 110), (50, 62)) pen.closePath() exclam = regular_ufo.newGlyph("exclam") exclam.width = 250 pen = exclam.getPen() pen.moveTo((93, 196)) pen.lineTo((156, 196)) pen.lineTo((186, 627)) pen.curveTo((186, 637), (181, 645), (161, 645)) pen.lineTo((87, 645)) pen.curveTo((67, 645), (63, 637), (63, 627)) pen.closePath() pen.addComponent("period", (1, 0, 0, 1, 10, 0)) exclamdown = regular_ufo.newGlyph("exclamdown") exclamdown.width = 250 pen = exclamdown.getPen() pen.addComponent("exclam", (-1, 0, 0, -1, 250, 509)) bold_ufo = FontClass() period = bold_ufo.newGlyph("period") period.width = 277 pen = period.getPen() pen.moveTo((30, 99)) pen.curveTo((30, 23), (50, -6), (139, -6)) pen.curveTo((227, -6), (247, 23), (247, 99)) pen.curveTo((247, 175), (227, 206), (139, 206)) pen.curveTo((50, 206), (30, 175), (30, 99)) pen.closePath() exclam = bold_ufo.newGlyph("exclam") exclam.width = 297 pen = exclam.getPen() pen.moveTo((84, 230)) pen.lineTo((214, 230)) pen.lineTo((254, 618)) pen.curveTo((254, 633), (247, 645), (217, 645)) pen.lineTo((81, 645)) pen.curveTo((51, 645), (44, 633), (44, 618)) pen.closePath() pen.addComponent("period", (0.87, 0, 0, 0.87, 28, -1)) exclamdown = bold_ufo.newGlyph("exclamdown") exclamdown.width = 297 pen = exclamdown.getPen() pen.addComponent("exclam", (-1, 0, 0, -1, 298, 509)) # We test that, even with DecomposeTransformedComponentsFilter(pre=True) and # the above nested/transformed/mixed component setup, we don't crash cu2qu # with errors about masters with inconsistent contour order after decomposition # of "exclamdown". glyphsets = TTFInterpolatablePreProcessor( [regular_ufo, bold_ufo], filters=[DecomposeTransformedComponentsFilter(pre=True)], ).process() assert len(glyphsets[0]["exclam"]) == 2 assert len(glyphsets[0]["exclamdown"]) == 2 assert len(glyphsets[1]["exclam"]) == 2 assert len(glyphsets[1]["exclamdown"]) == 2 ufo2ft-3.3.1/tests/filters/dottedCircle_test.py000066400000000000000000000041511470175262700215670ustar00rootroot00000000000000import pytest from ufo2ft.filters import loadFilters from ufo2ft.filters.dottedCircle import DottedCircleFilter from ufo2ft.util import _GlyphSet def test_dotted_circle_filter(FontClass, datadir): ufo_path = datadir.join("DottedCircleTest.ufo") font = FontClass(ufo_path) assert "uni25CC" not in font philter = DottedCircleFilter() glyphset = _GlyphSet.from_layer(font) modified = philter(font, glyphset) assert "uni25CC" in modified dotted_circle = glyphset["uni25CC"] # check the Glyph's module is the same as the Font's (both ufoLib2 or defcon, # not mixed): https://github.com/googlefonts/ufo2ft/issues/644 font_ufo_module = type(font).__module__.split(".")[0] glyph_ufo_module = type(dotted_circle).__module__.split(".")[0] assert glyph_ufo_module == font_ufo_module anchors = list(sorted(dotted_circle.anchors, key=lambda x: x.name)) assert anchors[0].x == 464 assert anchors[0].y == -17 assert anchors[0].name == "bottom" assert anchors[1].x == 563 assert anchors[1].y == 546 assert anchors[1].name == "top" assert len(dotted_circle) == 12 assert int(dotted_circle.width) == 688 assert dotted_circle.unicodes == [0x25CC] def test_empty_font(FontClass): """Check that the filter works on an empty font, i.e. uses fallbacks where appropriate.""" font = FontClass() font.lib["com.github.googlei18n.ufo2ft.filters"] = [ {"name": "dottedCircle", "pre": True} ] pre_filters, _ = loadFilters(font) (philter,) = pre_filters glyphset = _GlyphSet.from_layer(font) modified = philter(font, glyphset) assert "uni25CC" in modified @pytest.mark.filterwarnings("ignore:Please update") def test_empty_font_deprecated(FontClass): """Check that the module redirection works.""" font = FontClass() font.lib["com.github.googlei18n.ufo2ft.filters"] = [ {"name": "DottedCircleFilter", "pre": True} ] pre_filters, _ = loadFilters(font) (philter,) = pre_filters glyphset = _GlyphSet.from_layer(font) modified = philter(font, glyphset) assert "uni25CC" in modified ufo2ft-3.3.1/tests/filters/explodeColorLayerGlyphs_test.py000066400000000000000000000021431470175262700240040ustar00rootroot00000000000000"""Comprises tests for the ExplodeColorLayerGlyphsFilter filter.""" from pathlib import Path import pytest from ufo2ft.filters.explodeColorLayerGlyphs import ExplodeColorLayerGlyphsFilter from ufo2ft.util import _GlyphSet @pytest.fixture def data_dir(): return Path(__file__).parent.parent / "data" def test_strip_color_codepoints(FontClass, data_dir): """Test that the filter strips codepoints from glyphs when copying them from color layers into default layer alternates. See: https://github.com/googlefonts/ufo2ft/pull/739#issuecomment-1516075892""" # Load a test UFO with color layers, and give a codepoint to one of the # glyphs in those layers. ufo = FontClass(data_dir / "ColorTest.ufo") color_glyph = ufo.layers["color1"]["a"] color_glyph.unicode = 0x3020 # Apply the filter to the UFO. filter = ExplodeColorLayerGlyphsFilter() glyphset = _GlyphSet.from_layer(ufo) _ = filter(ufo, glyphset) # Test that the newly-copied alternate has had its codepoint removed. new_default_alt = glyphset["a.color1"] assert new_default_alt.unicode is None ufo2ft-3.3.1/tests/filters/filters_test.py000066400000000000000000000156041470175262700206370ustar00rootroot00000000000000from types import SimpleNamespace import pytest from fontTools.misc.loggingTools import CapturingLogHandler from ufo2ft.filters import ( FILTERS_KEY, BaseFilter, getFilterClass, loadFilterFromString, loadFilters, logger, ) from ..testSupport import _TempModule class FooBarFilter(BaseFilter): """A filter that does nothing.""" _args = ("a", "b") _kwargs = {"c": 0} def filter(self, glyph): return False @pytest.fixture(scope="module", autouse=True) def fooBar(): """Make a temporary 'ufo2ft.filters.fooBar' module containing a 'FooBarFilter' class for testing the filter loading machinery. """ with _TempModule("ufo2ft.filters.fooBar") as temp_module: temp_module.module.__dict__["FooBarFilter"] = FooBarFilter yield def test_getFilterClass(): assert getFilterClass("Foo Bar") == FooBarFilter assert getFilterClass("FooBar") == FooBarFilter assert getFilterClass("fooBar") == FooBarFilter with pytest.raises(ImportError): getFilterClass("Baz") with pytest.raises(ImportError): getFilterClass("FooBarFilter") with _TempModule("myfilters"), _TempModule("myfilters.fooBar") as temp_module: with pytest.raises(AttributeError): # this fails because `myfilters.fooBar` module does not # have a `FooBarFilter` class getFilterClass("Foo Bar", pkg="myfilters") temp_module.module.__dict__["FooBarFilter"] = FooBarFilter # this will attempt to import the `FooBarFilter` class from the # `myfilters.fooBar` module assert getFilterClass("Foo Bar", pkg="myfilters") == FooBarFilter class MockFont(SimpleNamespace): pass class MockGlyph(SimpleNamespace): pass def test_loadFilters_empty(): ufo = MockFont(lib={}) assert FILTERS_KEY not in ufo.lib assert loadFilters(ufo) == ([], []) @pytest.fixture def ufo(): ufo = MockFont(lib={}) ufo.lib[FILTERS_KEY] = [{"name": "Foo Bar", "args": ["foo", "bar"]}] return ufo def test_loadFilters_pre(ufo): ufo.lib[FILTERS_KEY][0]["pre"] = True pre, post = loadFilters(ufo) assert len(pre) == 1 assert not post assert isinstance(pre[0], FooBarFilter) def test_loadFilters_custom_namespace(ufo): ufo.lib[FILTERS_KEY][0]["name"] = "Self Destruct" ufo.lib[FILTERS_KEY][0]["namespace"] = "my_dangerous_filters" class SelfDestructFilter(FooBarFilter): def filter(self, glyph): # Don't try this at home!!! LOL :) # shutil.rmtree(os.path.expanduser("~")) return True with _TempModule("my_dangerous_filters"), _TempModule( "my_dangerous_filters.selfDestruct" ) as temp: temp.module.__dict__["SelfDestructFilter"] = SelfDestructFilter _, [filter_obj] = loadFilters(ufo) assert isinstance(filter_obj, SelfDestructFilter) def test_loadFilters_args_missing(ufo): del ufo.lib[FILTERS_KEY][0]["args"] with pytest.raises(TypeError) as exc_info: loadFilters(ufo) assert exc_info.match("missing") def test_loadFilters_args_unsupported(ufo): ufo.lib[FILTERS_KEY][0]["args"].append("baz") with pytest.raises(TypeError) as exc_info: loadFilters(ufo) assert exc_info.match("unsupported") def test_loadFilters_args_as_keywords(ufo): del ufo.lib[FILTERS_KEY][0]["args"] ufo.lib[FILTERS_KEY][0]["kwargs"] = {"a": "foo", "b": "bar"} _, [filter_obj] = loadFilters(ufo) assert filter_obj.options.a == "foo" assert filter_obj.options.b == "bar" def test_loadFilters_args_as_duplicated_keywords(ufo): ufo.lib[FILTERS_KEY][0]["args"] = ["foo"] ufo.lib[FILTERS_KEY][0]["kwargs"] = {"a": "foo", "b": "bar"} with pytest.raises(TypeError) as exc_info: loadFilters(ufo) assert exc_info.match("duplicated") def test_loadFilters_include_all(ufo): _, [filter_obj] = loadFilters(ufo) assert filter_obj.include(MockGlyph(name="hello")) assert filter_obj.include(MockGlyph(name="world")) def test_loadFilters_include_list(ufo): ufo.lib[FILTERS_KEY][0]["include"] = ["a", "b"] _, [filter_obj] = loadFilters(ufo) assert filter_obj.include(MockGlyph(name="a")) assert filter_obj.include(MockGlyph(name="b")) assert not filter_obj.include(MockGlyph(name="c")) def test_loadFilters_exclude_list(ufo): ufo.lib[FILTERS_KEY][0]["exclude"] = ["a", "b"] _, [filter_obj] = loadFilters(ufo) assert not filter_obj.include(MockGlyph(name="a")) assert not filter_obj.include(MockGlyph(name="b")) assert filter_obj.include(MockGlyph(name="c")) def test_loadFilters_both_include_exclude(ufo): ufo.lib[FILTERS_KEY][0]["include"] = ["a", "b"] ufo.lib[FILTERS_KEY][0]["exclude"] = ["c", "d"] with pytest.raises(ValueError) as exc_info: loadFilters(ufo) assert exc_info.match("arguments are mutually exclusive") def test_loadFilters_failed(ufo): ufo.lib[FILTERS_KEY].append(dict(name="Non Existent")) with CapturingLogHandler(logger, level="ERROR") as captor: loadFilters(ufo) captor.assertRegex("Failed to load filter") def test_loadFilters_kwargs_unsupported(ufo): ufo.lib[FILTERS_KEY][0]["kwargs"] = {} ufo.lib[FILTERS_KEY][0]["kwargs"]["c"] = 1 ufo.lib[FILTERS_KEY][0]["kwargs"]["d"] = 2 # unknown with pytest.raises(TypeError) as exc_info: loadFilters(ufo) assert exc_info.match("got an unsupported keyword") VALID_SPEC_STRINGS = [ "RemoveOverlapsFilter", "PropagateAnchorsFilter(include=['a', 'b', 'c'])", "ufo2ft.filters.fooBar::FooBarFilter(a='a', b='b', c=1)", ] @pytest.mark.parametrize("spec", VALID_SPEC_STRINGS) def test_loadFilterFromString(spec, ufo): philter = loadFilterFromString(spec) assert callable(philter) def test_loadFilterFromString_args_missing(ufo): with pytest.raises(TypeError) as info: loadFilterFromString( "ufo2ft.filters.fooBar::FooBarFilter(a='a', c=1)", ) assert info.match("missing 1 required positional argument: 'b'") with pytest.raises(TypeError) as info: loadFilterFromString( "ufo2ft.filters.fooBar::FooBarFilter(c=1)", ) assert info.match("missing 2 required positional arguments: 'a', 'b'") def test_BaseFilter_repr(): class NoArgFilter(BaseFilter): pass assert repr(NoArgFilter()) == "NoArgFilter()" assert repr(FooBarFilter("a", "b", c=1)) == ("FooBarFilter('a', 'b', c=1)") assert ( repr(FooBarFilter("c", "d", include=["x", "y"])) == "FooBarFilter('c', 'd', c=0, include=['x', 'y'])" ) assert ( repr(FooBarFilter("e", "f", c=2.0, exclude=("z",))) == "FooBarFilter('e', 'f', c=2.0, exclude=('z',))" ) def f(g): return False assert repr( FooBarFilter("g", "h", include=f) ) == "FooBarFilter('g', 'h', c=0, include={})".format(repr(f)) if __name__ == "__main__": import sys sys.exit(pytest.main(sys.argv)) ufo2ft-3.3.1/tests/filters/flattenComponents_test.py000066400000000000000000000204121470175262700226630ustar00rootroot00000000000000import pytest from fontTools.misc.loggingTools import CapturingLogHandler from ufo2ft.filters.flattenComponents import FlattenComponentsFilter, logger @pytest.fixture( params=[ { "glyphs": [ {"name": "space", "width": 500}, { "name": "contourGlyph", "width": 350, "outline": [ ("moveTo", ((0, 0),)), ("lineTo", ((300, 0),)), ("lineTo", ((300, 300),)), ("lineTo", ((0, 300),)), ("closePath", ()), ], }, { "name": "componentGlyph", "width": 350, "outline": [("addComponent", ("contourGlyph", (1, 0, 0, 1, 0, 0)))], }, { "name": "nestedComponentGlyph", "width": 350, "outline": [ ("addComponent", ("componentGlyph", (1, 0, 0, 1, 0, 0))) ], }, { "name": "componentAndNestedComponentsGlyph", "width": 700, "outline": [ ("addComponent", ("contourGlyph", (1, 0, 0, 1, 0, 0))), ("addComponent", ("componentGlyph", (1, 0, 0, 1, 350, 0))), ( "addComponent", ("nestedComponentGlyph", (1, 0, 0, 1, 700, 0)), ), ], }, { "name": "contourAndComponentGlyph", "width": 600, "outline": [ ("moveTo", ((400, 0),)), ("lineTo", ((400, 100),)), ("lineTo", ((500, 100),)), ("lineTo", ((500, 0),)), ("closePath", ()), ("addComponent", ("contourGlyph", (1, 0, 0, 1, 0, 0))), ], }, { "name": "nestedContourAndComponentGlyph", "width": 600, "outline": [ ( "addComponent", ("contourAndComponentGlyph", (1, 0, 0, 1, 50, 0)), ), ], }, { "name": "nestedNestedContourAndComponentGlyph", "width": 600, "outline": [ ( "addComponent", ("nestedContourAndComponentGlyph", (1, 0, 0, 1, 45, 0)), ), ], }, { "name": "scaledComponentGlyph", "width": 600, "outline": [ ( "addComponent", ("contourGlyph", (0.5, 0, 0, 0.5, 50, 50)), ), ], }, { "name": "nestedScaledComponentGlyph", "width": 600, "outline": [ ( "addComponent", ("scaledComponentGlyph", (1, 0, 0, 1, 40, 40)), ), ], }, { "name": "scaledNestedComponentGlyph", "width": 600, "outline": [ ( "addComponent", ("scaledComponentGlyph", (1.2, 0, 0, 1.2, 40, 40)), ), ], }, ] } ] ) def font(request, FontClass): font = FontClass() for param in request.param["glyphs"]: glyph = font.newGlyph(param["name"]) glyph.width = param.get("width", 0) pen = glyph.getPen() for operator, operands in param.get("outline", []): getattr(pen, operator)(*operands) return font class FlattenComponentsFilterTest: def test_empty_glyph(self, font): philter = FlattenComponentsFilter(include={"space"}) assert not philter(font) def test_contour_glyph(self, font): philter = FlattenComponentsFilter(include={"contourGlyph"}) assert not philter(font) def test_component_glyph(self, font): philter = FlattenComponentsFilter(include={"componentGlyph"}) assert not philter(font) def test_nested_components_glyph(self, font): philter = FlattenComponentsFilter(include={"nestedComponentGlyph"}) modified = philter(font) assert modified == {"nestedComponentGlyph"} assert [ (c.baseGlyph, c.transformation) for c in font["nestedComponentGlyph"].components ] == [("contourGlyph", (1, 0, 0, 1, 0, 0))] def test_nested_contour_and_component_glyph(self, font): philter = FlattenComponentsFilter( include={ "nestedContourAndComponentGlyph", "nestedNestedContourAndComponentGlyph", } ) modified = philter(font) assert modified == {"nestedNestedContourAndComponentGlyph"} assert [ (c.baseGlyph, c.transformation) for c in font["nestedNestedContourAndComponentGlyph"].components ] == [("contourAndComponentGlyph", (1, 0, 0, 1, 95, 0))] def test_scaled_component_glyph(self, font): philter = FlattenComponentsFilter( include={ "scaledComponentGlyph", "nestedScaledComponentGlyph", "scaledNestedComponentGlyph", } ) modified = philter(font) assert modified == { "nestedScaledComponentGlyph", "scaledNestedComponentGlyph", } assert [ (c.baseGlyph, c.transformation) for c in font["nestedScaledComponentGlyph"].components ] == [("contourGlyph", (0.5, 0, 0, 0.5, 90, 90))] assert [ (c.baseGlyph, c.transformation) for c in font["scaledNestedComponentGlyph"].components ] == [("contourGlyph", (0.6, 0, 0, 0.6, 100, 100))] def test_whole_font(self, font): philter = FlattenComponentsFilter() modified = philter(font) assert modified == { "nestedComponentGlyph", "componentAndNestedComponentsGlyph", "nestedNestedContourAndComponentGlyph", "nestedScaledComponentGlyph", "scaledNestedComponentGlyph", } assert [ (c.baseGlyph, c.transformation) for c in font["nestedComponentGlyph"].components ] == [("contourGlyph", (1, 0, 0, 1, 0, 0))] assert [ (c.baseGlyph, c.transformation) for c in font["componentAndNestedComponentsGlyph"].components ] == [ ("contourGlyph", (1, 0, 0, 1, 0, 0)), ("contourGlyph", (1, 0, 0, 1, 350, 0)), ("contourGlyph", (1, 0, 0, 1, 700, 0)), ] assert [ (c.baseGlyph, c.transformation) for c in font["nestedContourAndComponentGlyph"].components ] == [ ("contourAndComponentGlyph", (1, 0, 0, 1, 50, 0)), ] assert [ (c.baseGlyph, c.transformation) for c in font["nestedNestedContourAndComponentGlyph"].components ] == [("contourAndComponentGlyph", (1, 0, 0, 1, 95, 0))] assert [ (c.baseGlyph, c.transformation) for c in font["nestedScaledComponentGlyph"].components ] == [("contourGlyph", (0.5, 0, 0, 0.5, 90, 90))] assert [ (c.baseGlyph, c.transformation) for c in font["scaledNestedComponentGlyph"].components ] == [("contourGlyph", (0.6, 0, 0, 0.6, 100, 100))] def test_logger(self, font): with CapturingLogHandler(logger, level="INFO") as captor: philter = FlattenComponentsFilter() _ = philter(font) captor.assertRegex("Flattened composite glyphs: 5") ufo2ft-3.3.1/tests/filters/propagateAnchors_test.py000066400000000000000000000266311470175262700224710ustar00rootroot00000000000000import pytest from fontTools.designspaceLib import DesignSpaceDocument from fontTools.misc.loggingTools import CapturingLogHandler import ufo2ft.filters from ufo2ft.filters.propagateAnchors import ( PropagateAnchorsFilter, PropagateAnchorsIFilter, logger, ) from ufo2ft.instantiator import Instantiator from ufo2ft.util import _GlyphSet @pytest.fixture( params=[ { "glyphs": [ {"name": "space", "width": 500}, { "name": "a", "width": 350, "outline": [ ("moveTo", ((0, 0),)), ("lineTo", ((300, 0),)), ("lineTo", ((300, 300),)), ("lineTo", ((0, 300),)), ("closePath", ()), ], "anchors": [(175, 300, "top"), (175, 0, "bottom")], }, { "name": "dieresiscomb", "width": 0, "outline": [ ("moveTo", ((-120, 320),)), ("lineTo", ((-60, 320),)), ("lineTo", ((-60, 360),)), ("lineTo", ((-120, 360),)), ("closePath", ()), ("moveTo", ((120, 320),)), ("lineTo", ((60, 320),)), ("lineTo", ((60, 360),)), ("lineTo", ((120, 360),)), ("closePath", ()), ], "anchors": [(0, 300, "_top"), (0, 480, "top")], }, { "name": "macroncomb", "width": 0, "outline": [ ("moveTo", ((-120, 330),)), ("lineTo", ((120, 330),)), ("lineTo", ((120, 350),)), ("lineTo", ((-120, 350),)), ("closePath", ()), ], "anchors": [(0, 300, "_top"), (0, 480, "top")], }, { "name": "a-cyr", "width": 350, "outline": [("addComponent", ("a", (1, 0, 0, 1, 0, 0)))], }, { "name": "amacron", "width": 350, "outline": [ ("addComponent", ("a", (1, 0, 0, 1, 0, 0))), ("addComponent", ("macroncomb", (1, 0, 0, 1, 175, 0))), ], "anchors": [(176, 481, "top")], }, { "name": "adieresis", "width": 350, "outline": [ ("addComponent", ("a", (1, 0, 0, 1, 0, 0))), ("addComponent", ("dieresiscomb", (1, 0, 0, 1, 175, 0))), ], }, { "name": "amacrondieresis", "width": 350, "outline": [ ("addComponent", ("amacron", (1, 0, 0, 1, 0, 0))), ("addComponent", ("dieresiscomb", (1, 0, 0, 1, 175, 180))), ], }, { "name": "adieresismacron", "width": 350, "outline": [ ("addComponent", ("a", (1, 0, 0, 1, 0, 0))), ("addComponent", ("dieresiscomb", (1, 0, 0, 1, 175, 0))), ("addComponent", ("macroncomb", (1, 0, 0, 1, 175, 180))), ], }, { "name": "a_a", "width": 700, "outline": [ ("addComponent", ("a", (1, 0, 0, 1, 0, 0))), ("addComponent", ("a", (1, 0, 0, 1, 350, 0))), ], }, { "name": "emacron", "width": 350, "outline": [ ("addComponent", ("e", (1, 0, 0, 1, 0, 0))), ("addComponent", ("macroncomb", (1, 0, 0, 1, 175, 0))), ], }, { "name": "r", "width": 350, "outline": [ ("moveTo", ((0, 0),)), ("lineTo", ((0, 300),)), ("lineTo", ((175, 300),)), ("closePath", ()), ], "anchors": [(175, 300, "top"), (175, 0, "bottom")], }, { "name": "rcombbelow", "width": 0, "outline": [ ("addComponent", ("r", (0.5, 0, 0, 0.5, -100, -100))), ], "anchors": [(0, 0, "_bottom")], }, ] } ] ) def font(request, FontClass): font = FontClass() for param in request.param["glyphs"]: glyph = font.newGlyph(param["name"]) glyph.width = param.get("width", 0) pen = glyph.getPen() for operator, operands in param.get("outline", []): getattr(pen, operator)(*operands) for x, y, name in param.get("anchors", []): glyph.appendAnchor(dict(x=x, y=y, name=name)) # classify as 'mark' all glyphs with zero width and 'comb' in their name font.lib["public.openTypeCategories"] = { g["name"]: "mark" for g in request.param["glyphs"] if g.get("width", 0) == 0 and "comb" in g["name"] } return font EXPECTED = { # single component glyph "a-cyr": ([("bottom", 175, 0), ("top", 175, 300)], {"a-cyr"}), # two component glyph "adieresis": ([("bottom", 175, 0), ("top", 175, 480)], {"adieresis"}), # one anchor, two component glyph "amacron": ([("top", 176, 481), ("bottom", 175, 0)], {"amacron"}), # three component glyph "adieresismacron": ([("bottom", 175, 0), ("top", 175, 660)], {"adieresismacron"}), # nested component glyph "amacrondieresis": ( [("bottom", 175, 0), ("top", 175, 660)], # 'amacron' is used as component by 'amacrondieresis' hence it is modified # as well... {"amacrondieresis", "amacron"}, ), # ligature glyph "a_a": ( [ ("bottom_1", 175, 0), ("bottom_2", 525, 0), ("top_1", 175, 300), ("top_2", 525, 300), ], {"a_a"}, ), # the composite glyph is a mark with anchors, hence propagation is not performed, # i.e. 'top' and 'bottom' are *not* copied to 'rcombbelow': # https://github.com/googlefonts/ufo2ft/issues/802 "rcombbelow": ([("_bottom", 0, 0)], set()), } class PropagateAnchorsFilterTest: def test_empty_glyph(self, font): philter = PropagateAnchorsFilter(include={"space"}) assert not philter(font) def test_contour_glyph(self, font): philter = PropagateAnchorsFilter(include={"a"}) assert not philter(font) @pytest.mark.parametrize("name", list(EXPECTED)) def test_include_one_glyph_at_a_time(self, font, name): philter = PropagateAnchorsFilter(include={name}) modified = philter(font) expected_anchors, expected_modified = EXPECTED[name] assert modified == expected_modified assert [(a.name, a.x, a.y) for a in font[name].anchors] == expected_anchors def test_whole_font(self, font): philter = PropagateAnchorsFilter() modified = philter(font) assert modified == {k for k in EXPECTED if k in EXPECTED[k][1]} for name, (expected_anchors, _) in EXPECTED.items(): assert [(a.name, a.x, a.y) for a in font[name].anchors] == expected_anchors def test_fail_during_anchor_propagation(self, font): name = "emacron" with CapturingLogHandler(logger, level="WARNING") as captor: philter = PropagateAnchorsFilter(include={name}) philter(font) captor.assertRegex( "Anchors not propagated for inexistent component e " "in glyph emacron" ) def test_logger(self, font): with CapturingLogHandler(logger, level="INFO") as captor: philter = PropagateAnchorsFilter() philter(font) captor.assertRegex("Glyphs with propagated anchors: 6") def test_CantarellAnchorPropagation(FontClass, datadir): ufo_path = datadir.join("CantarellAnchorPropagation.ufo") ufo = FontClass(ufo_path) pre_filters, _ = ufo2ft.filters.loadFilters(ufo) philter = pre_filters[0] philter(ufo) anchors_combined = { (a.name, a.x, a.y) for a in ufo["circumflexcomb_tildecomb"].anchors } assert ("top", 214.0, 730.0) in anchors_combined assert ("_top", 213.0, 482.0) in anchors_combined anchors_o = {(a.name, a.x, a.y) for a in ufo["ocircumflextilde"].anchors} assert ("top", 284.0, 730.0) in anchors_o def test_CantarellAnchorPropagation_reduced_filter(FontClass, datadir): ufo_path = datadir.join("CantarellAnchorPropagation.ufo") ufo = FontClass(ufo_path) ufo.lib["com.github.googlei18n.ufo2ft.filters"][0]["include"] = ["ocircumflextilde"] pre_filters, _ = ufo2ft.filters.loadFilters(ufo) philter = pre_filters[0] philter(ufo) anchors_combined = { (a.name, a.x, a.y) for a in ufo["circumflexcomb_tildecomb"].anchors } assert ("top", 214.0, 730.0) in anchors_combined assert ("_top", 213.0, 482.0) in anchors_combined anchors_o = {(a.name, a.x, a.y) for a in ufo["ocircumflextilde"].anchors} assert ("top", 284.0, 730.0) in anchors_o class PropagateAnchorsIFilterTest: def test_propagate_from_interpolated_components(self, FontClass, data_dir): ds_path = data_dir / "SkipExportGlyphsTest.designspace" ds = DesignSpaceDocument.fromfile(ds_path) ds.loadSourceFonts(FontClass) ufos = [s.font for s in ds.sources] glyphSets = [_GlyphSet.from_layer(s.font, s.layerName) for s in ds.sources] assert len(ufos) == len(glyphSets) == 4 # the composite glyph 'Astroke' has no anchors, but 'A' has some for glyphSet in glyphSets: if "Astroke" in glyphSet: assert not glyphSet["Astroke"].anchors if "A" in glyphSet: assert glyphSet["A"].anchors # in glyphSets[2] the 'Astroke' component base glyphs are missing so their # propagated anchors are supposed to be interpolated on the fly assert "Astroke" in glyphSets[2] assert {c.baseGlyph for c in glyphSets[2]["Astroke"].components}.isdisjoint( glyphSets[2].keys() ) assert not glyphSets[2]["Astroke"].anchors instantiator = Instantiator.from_designspace( ds, do_kerning=False, do_info=False ) philter = PropagateAnchorsIFilter() modified = philter(ufos, glyphSets, instantiator) assert modified == {"Astroke"} assert [dict(a) for a in glyphSets[2]["Astroke"].anchors] == [ {"name": "bottom", "x": 458, "y": 0}, {"name": "center", "x": 457, "y": 358}, {"name": "top", "x": 457, "y": 714}, {"name": "topright", "x": 716, "y": 714}, ] assert {c.baseGlyph for c in glyphSets[2]["Astroke"].components}.isdisjoint( glyphSets[2].keys() ) ufo2ft-3.3.1/tests/filters/sortContours_test.py000066400000000000000000000333111470175262700217060ustar00rootroot00000000000000import logging import pytest import ufo2ft import ufo2ft.filters.sortContours @pytest.fixture def font(request, datadir, FontClass): font = FontClass(datadir.join("ContourOrderTest.ufo")) return font def test_sort_contour_order(font, FontClass): test_ufo = FontClass() font_compiled = ufo2ft.compileTTF(font, inplace=True) font_glyf = font_compiled["glyf"] glyph_uniFFFC = font_glyf["uniFFFC"] glyph_test1 = test_ufo.newGlyph("test1") glyph_uniFFFC.draw(glyph_test1.getPen(), font_glyf) assert [ [(p.x, p.y, p.segmentType, p.smooth) for p in c] for c in glyph_test1 ] == EXPECTED_glyph_uniFFFC glyph_graphemejoinercomb = font_glyf["graphemejoinercomb"] glyph_test2 = test_ufo.newGlyph("test2") glyph_graphemejoinercomb.draw(glyph_test2.getPen(), font_glyf) assert [ [(p.x, p.y, p.segmentType, p.smooth) for p in c] for c in glyph_test2 ] == EXPECTED_glyph_graphemejoinercomb def test_no_sort_contour_order(font, FontClass): test_ufo = FontClass() del font.lib["com.github.googlei18n.ufo2ft.filters"] font_compiled = ufo2ft.compileTTF(font, inplace=True) font_glyf = font_compiled["glyf"] glyph_uniFFFC = font_glyf["uniFFFC"] glyph_test1 = test_ufo.newGlyph("test1") glyph_uniFFFC.draw(glyph_test1.getPen(), font_glyf) assert [ [(p.x, p.y, p.segmentType, p.smooth) for p in c] for c in glyph_test1 ] != EXPECTED_glyph_uniFFFC glyph_graphemejoinercomb = font_glyf["graphemejoinercomb"] glyph_test2 = test_ufo.newGlyph("test2") glyph_graphemejoinercomb.draw(glyph_test2.getPen(), font_glyf) assert [ [(p.x, p.y, p.segmentType, p.smooth) for p in c] for c in glyph_test2 ] != EXPECTED_glyph_graphemejoinercomb def test_warn_pre_filter(font, caplog): font.lib["com.github.googlei18n.ufo2ft.filters"][0]["pre"] = True font.lib["com.github.googlei18n.ufo2ft.filters"][0]["include"].append("xxx") with caplog.at_level( logging.WARNING, logger=ufo2ft.filters.sortContours.logger.name ): _ = ufo2ft.compileTTF(font, inplace=True) assert len(caplog.records) == 1 assert "contains components which will not be sorted" in caplog.text def test_no_warn_post_filter(font, caplog): font.lib["com.github.googlei18n.ufo2ft.filters"][0]["include"].append("xxx") with caplog.at_level( logging.WARNING, logger=ufo2ft.filters.sortContours.logger.name ): _ = ufo2ft.compileTTF(font, inplace=True) assert len(caplog.records) == 0 EXPECTED_glyph_uniFFFC = [ [ (41, -187, "line", False), (41, -39, "line", False), (95, -39, "line", False), (95, -134, "line", False), (189, -134, "line", False), (189, -187, "line", False), ], [ (95, 19, "line", False), (41, 19, "line", False), (41, 151, "line", False), (95, 151, "line", False), ], [ (95, 210, "line", False), (41, 210, "line", False), (41, 343, "line", False), (95, 343, "line", False), ], [ (95, 402, "line", False), (41, 402, "line", False), (41, 534, "line", False), (95, 534, "line", False), ], [ (41, 593, "line", False), (41, 741, "line", False), (189, 741, "line", False), (189, 687, "line", False), (95, 687, "line", False), (95, 593, "line", False), ], [ (422, 307, "qcurve", True), (422, 241, None, False), (360, 160, None, False), (294, 160, "qcurve", True), (228, 160, None, False), (166, 241, None, False), (166, 307, "qcurve", True), (166, 374, None, False), (228, 454, None, False), (294, 454, "qcurve", True), (360, 454, None, False), (422, 374, None, False), ], [ (228, 307, "qcurve", True), (228, 262, None, False), (260, 211, None, False), (294, 211, "qcurve", True), (329, 211, None, False), (360, 262, None, False), (360, 307, "qcurve", True), (360, 352, None, False), (329, 403, None, False), (294, 403, "qcurve", True), (260, 403, None, False), (228, 352, None, False), ], [ (248, -187, "line", False), (248, -134, "line", False), (380, -134, "line", False), (380, -187, "line", False), ], [ (248, 687, "line", False), (248, 741, "line", False), (380, 741, "line", False), (380, 687, "line", False), ], [ (439, -187, "line", False), (439, -134, "line", False), (572, -134, "line", False), (572, -187, "line", False), ], [ (439, 687, "line", False), (439, 741, "line", False), (572, 741, "line", False), (572, 687, "line", False), ], [ (463, 450, "line", False), (547, 450, "line", True), (600, 450, None, False), (655, 418, None, False), (655, 377, "qcurve", True), (655, 353, None, False), (632, 321, None, False), (611, 317, "qcurve", False), (611, 313, "line", False), (633, 309, None, False), (663, 281, None, False), (663, 247, "qcurve", True), (663, 208, None, False), (610, 164, None, False), (564, 164, "qcurve", True), (463, 164, "line", False), ], [ (523, 289, "line", False), (523, 214, "line", False), (559, 214, "line", True), (583, 214, None, False), (601, 235, None, False), (601, 253, "qcurve", True), (601, 269, None, False), (583, 289, None, False), (557, 289, "qcurve", True), ], [ (523, 337, "line", False), (555, 337, "line", True), (578, 337, None, False), (595, 353, None, False), (595, 369, "qcurve", True), (595, 400, None, False), (552, 400, "qcurve", True), (523, 400, "line", False), ], [ (630, -187, "line", False), (630, -134, "line", False), (763, -134, "line", False), (763, -187, "line", False), ], [ (630, 687, "line", False), (630, 741, "line", False), (763, 741, "line", False), (763, 687, "line", False), ], [ (728, 161, "qcurve", True), (704, 161, None, False), (689, 166, "qcurve", False), (689, 216, "line", False), (697, 215, None, False), (712, 212, None, False), (722, 212, "qcurve", True), (740, 212, None, False), (764, 229, None, False), (764, 254, "qcurve", True), (764, 450, "line", False), (825, 450, "line", False), (825, 256, "line", True), (825, 207, None, False), (771, 161, None, False), ], [ (821, -187, "line", False), (821, -134, "line", False), (916, -134, "line", False), (916, -39, "line", False), (969, -39, "line", False), (969, -187, "line", False), ], [ (821, 687, "line", False), (821, 741, "line", False), (969, 741, "line", False), (969, 593, "line", False), (916, 593, "line", False), (916, 687, "line", False), ], [ (969, 19, "line", False), (916, 19, "line", False), (916, 151, "line", False), (969, 151, "line", False), ], [ (969, 210, "line", False), (916, 210, "line", False), (916, 343, "line", False), (969, 343, "line", False), ], [ (969, 402, "line", False), (916, 402, "line", False), (916, 534, "line", False), (969, 534, "line", False), ], ] EXPECTED_glyph_graphemejoinercomb = [ [ (-357, 0, "line", False), (-357, 157, "line", False), (-303, 157, "line", False), (-303, 54, "line", False), (-201, 54, "line", False), (-201, 0, "line", False), ], [ (-357, 279, "line", False), (-357, 436, "line", False), (-303, 436, "line", False), (-303, 279, "line", False), ], [ (-357, 558, "line", False), (-357, 714, "line", False), (-201, 714, "line", False), (-201, 660, "line", False), (-303, 660, "line", False), (-303, 558, "line", False), ], [ (-218, 330, "qcurve", True), (-245, 330, None, False), (-245, 357, "qcurve", True), (-245, 384, None, False), (-218, 384, "qcurve", True), (-191, 384, None, False), (-191, 357, "qcurve", True), (-191, 330, None, False), ], [ (-200, 244, "qcurve", True), (-227, 244, None, False), (-227, 271, "qcurve", True), (-227, 298, None, False), (-200, 298, "qcurve", True), (-173, 298, None, False), (-173, 271, "qcurve", True), (-173, 244, None, False), ], [ (-200, 416, "qcurve", True), (-227, 416, None, False), (-227, 443, "qcurve", True), (-227, 470, None, False), (-200, 470, "qcurve", True), (-173, 470, None, False), (-173, 443, "qcurve", True), (-173, 416, None, False), ], [ (-157, 174, "qcurve", True), (-184, 174, None, False), (-184, 201, "qcurve", True), (-184, 228, None, False), (-157, 228, "qcurve", True), (-130, 228, None, False), (-130, 201, "qcurve", True), (-130, 174, None, False), ], [ (-157, 486, "qcurve", True), (-184, 486, None, False), (-184, 513, "qcurve", True), (-184, 540, None, False), (-157, 540, "qcurve", True), (-130, 540, None, False), (-130, 513, "qcurve", True), (-130, 486, None, False), ], [ (-86, 128, "qcurve", True), (-113, 128, None, False), (-113, 155, "qcurve", True), (-113, 182, None, False), (-86, 182, "qcurve", True), (-59, 182, None, False), (-59, 155, "qcurve", True), (-59, 128, None, False), ], [ (-86, 532, "qcurve", True), (-113, 532, None, False), (-113, 559, "qcurve", True), (-113, 586, None, False), (-86, 586, "qcurve", True), (-59, 586, None, False), (-59, 559, "qcurve", True), (-59, 532, None, False), ], [ (-79, 0, "line", False), (-79, 54, "line", False), (79, 54, "line", False), (79, 0, "line", False), ], [ (-79, 660, "line", False), (-79, 714, "line", False), (79, 714, "line", False), (79, 660, "line", False), ], [ (0, 112, "qcurve", True), (-27, 112, None, False), (-27, 139, "qcurve", True), (-27, 166, None, False), (0, 166, "qcurve", True), (27, 166, None, False), (27, 139, "qcurve", True), (27, 112, None, False), ], [ (0, 548, "qcurve", True), (-27, 548, None, False), (-27, 575, "qcurve", True), (-27, 602, None, False), (0, 602, "qcurve", True), (27, 602, None, False), (27, 575, "qcurve", True), (27, 548, None, False), ], [ (86, 128, "qcurve", True), (59, 128, None, False), (59, 155, "qcurve", True), (59, 182, None, False), (86, 182, "qcurve", True), (113, 182, None, False), (113, 155, "qcurve", True), (113, 128, None, False), ], [ (86, 532, "qcurve", True), (59, 532, None, False), (59, 559, "qcurve", True), (59, 586, None, False), (86, 586, "qcurve", True), (113, 586, None, False), (113, 559, "qcurve", True), (113, 532, None, False), ], [ (157, 174, "qcurve", True), (130, 174, None, False), (130, 201, "qcurve", True), (130, 228, None, False), (157, 228, "qcurve", True), (184, 228, None, False), (184, 201, "qcurve", True), (184, 174, None, False), ], [ (157, 486, "qcurve", True), (130, 486, None, False), (130, 513, "qcurve", True), (130, 540, None, False), (157, 540, "qcurve", True), (184, 540, None, False), (184, 513, "qcurve", True), (184, 486, None, False), ], [ (204, 244, "qcurve", True), (177, 244, None, False), (177, 271, "qcurve", True), (177, 298, None, False), (204, 298, "qcurve", True), (231, 298, None, False), (231, 271, "qcurve", True), (231, 244, None, False), ], [ (204, 416, "qcurve", True), (177, 416, None, False), (177, 443, "qcurve", True), (177, 470, None, False), (204, 470, "qcurve", True), (231, 470, None, False), (231, 443, "qcurve", True), (231, 416, None, False), ], [ (223, 330, "qcurve", True), (196, 330, None, False), (196, 357, "qcurve", True), (196, 384, None, False), (223, 384, "qcurve", True), (250, 384, None, False), (250, 357, "qcurve", True), (250, 330, None, False), ], [ (201, 0, "line", False), (201, 54, "line", False), (304, 54, "line", False), (304, 157, "line", False), (357, 157, "line", False), (357, 0, "line", False), ], [ (304, 558, "line", False), (304, 660, "line", False), (201, 660, "line", False), (201, 714, "line", False), (357, 714, "line", False), (357, 558, "line", False), ], [ (304, 279, "line", False), (304, 436, "line", False), (357, 436, "line", False), (357, 279, "line", False), ], ] ufo2ft-3.3.1/tests/filters/transformations_test.py000066400000000000000000000162141470175262700224160ustar00rootroot00000000000000from math import isclose import pytest from ufo2ft.filters.transformations import TransformationsFilter @pytest.fixture( params=[ { "capHeight": 700, "xHeight": 500, "glyphs": [ {"name": "space", "width": 500}, { "name": "a", "width": 350, "outline": [ ("moveTo", ((0, 0),)), ("lineTo", ((300, 0),)), ("lineTo", ((300, 300),)), ("lineTo", ((0, 300),)), ("closePath", ()), ], "anchors": [(100, 200, "top"), (100, -200, "bottom")], }, { "name": "b", "width": 450, "outline": [ ("addComponent", ("a", (1, 0, 0, 1, 0, 0))), ("addComponent", ("c", (1, 0, 0, 1, 0, 0))), ("addComponent", ("a", (1, 0, 0, 1, 10, -10))), ], }, { "name": "c", "outline": [ ("moveTo", ((0, 0),)), ("lineTo", ((300, 0),)), ("lineTo", ((150, 300),)), ("closePath", ()), ], }, { "name": "d", "outline": [("addComponent", ("b", (1, 0, 0, -1, 0, 0)))], }, ], } ] ) def font(request, FontClass): font = FontClass() font.info.capHeight = request.param["capHeight"] font.info.xHeight = request.param["xHeight"] for param in request.param["glyphs"]: glyph = font.newGlyph(param["name"]) glyph.width = param.get("width", 0) pen = glyph.getPen() for operator, operands in param.get("outline", []): getattr(pen, operator)(*operands) for x, y, name in param.get("anchors", []): glyph.appendAnchor(dict(x=x, y=y, name=name)) return font @pytest.fixture( params=TransformationsFilter.Origin, ids=[e.name for e in TransformationsFilter.Origin], ) def origin(request): return request.param class TransformationsFilterTest: def test_invalid_origin_value(self): with pytest.raises(ValueError) as excinfo: TransformationsFilter(Origin=5) excinfo.match(r"is not a valid (TransformationsFilter\.)?Origin") def test_empty_glyph(self, font): filter_ = TransformationsFilter(OffsetY=51, include={"space"}) assert not filter_(font) def test_Identity(self, font): filter_ = TransformationsFilter() assert not filter_(font) def test_OffsetX(self, font): filter_ = TransformationsFilter(OffsetX=-10) assert filter_(font) a = font["a"] assert (a[0][0].x, a[0][0].y) == (-10, 0) assert (a.anchors[1].x, a.anchors[1].y) == (90, -200) # base glyph was already transformed, component didn't change assert font["b"].components[0].transformation[-2:] == (0, 0) def test_OffsetY(self, font): filter_ = TransformationsFilter(OffsetY=51) assert filter_(font) a = font["a"] assert (a[0][0].x, a[0][0].y) == (0, 51) assert (a.anchors[1].x, a.anchors[1].y) == (100, -149) assert font["b"].components[0].transformation[-2:] == (0, 0) def test_OffsetXY(self, font): filter_ = TransformationsFilter(OffsetX=-10, OffsetY=51) assert filter_(font) a = font["a"] assert (a[0][0].x, a[0][0].y) == (-10, 51) assert (a.anchors[1].x, a.anchors[1].y) == (90, -149) assert font["b"].components[0].transformation[-2:] == (0, 0) def test_ScaleX(self, font, origin): # different Origin heights should not affect horizontal scale filter_ = TransformationsFilter(ScaleX=50, Origin=origin) assert filter_(font) a = font["a"] assert (a[0][0].x, a[0][0].y) == (0, 0) assert (a[0][2].x, a[0][2].y) == (150, 300) assert a.width == 350 * 0.50 def test_ScaleY(self, font, origin): percent = 50 filter_ = TransformationsFilter(ScaleY=percent, Origin=origin) assert filter_(font) factor = percent / 100 origin_height = filter_.get_origin_height(font, origin) bottom = origin_height * factor top = bottom + 300 * factor a = font["a"] # only y coords change assert (a[0][0].x, a[0][0].y) == (0, bottom) assert (a[0][2].x, a[0][2].y) == (300, top) def test_ScaleXY(self, font, origin): percent = 50 filter_ = TransformationsFilter(ScaleX=percent, ScaleY=percent, Origin=origin) assert filter_(font) factor = percent / 100 origin_height = filter_.get_origin_height(font, origin) bottom = origin_height * factor top = bottom + 300 * factor a = font["a"] # both x and y change assert (a[0][0].x, a[0][0].y) == (0, bottom) assert (a[0][2].x, a[0][2].y) == (150, top) assert a.width == 350 * factor def test_Slant(self, font, origin): filter_ = TransformationsFilter(Slant=45, Origin=origin) assert filter_(font) origin_height = filter_.get_origin_height(font, origin) a = font["a"] assert isclose(a[0][0].x, -origin_height) assert a[0][0].y == 0 def test_composite_glyphs(self, font): filter_ = TransformationsFilter( OffsetX=-10, OffsetY=51, ScaleX=50, ScaleY=50, exclude={"c"} ) assert filter_(font) b = font["b"] # component 'a' #1 was not transformed, because the base glyph was already # transformed, and the component's own transformation is identity assert b.components[0].transformation == (1, 0, 0, 1, 0, 0) # component 'c' was transformed, because base glyph was not included assert b.components[1].transformation == (0.5, 0, 0, 0.5, -10, 51) # component 'a' #2 was partly transformed: the base glyph was transformed, but # the component's original transformation was not identity; thus # it was modified to compensate for the transformation already applied to # the base glyph (scale stays same, offsets are scaled) assert b.components[2].transformation == (1, 0, 0, 1, 5, -5) d = font["d"] # component 'b' was transformed as well as its base glyph, because # its original transform had a scale, so it was necessary to # compensate for the transformation applied on the base glyph assert d.components[0].transformation == (1, 0, 0, -1, 0, 102) def test_ScaleOffset_width(self, font, origin): percent = 50 filter_ = TransformationsFilter( OffsetX=-100, ScaleX=percent, ScaleY=percent, Origin=origin ) assert filter_(font) factor = percent / 100 a = font["a"] # The offset value here should not change the fact that the glyph # bounding box is scaled by 50%. assert a.width == 350 * factor ufo2ft-3.3.1/tests/fontInfoData_test.py000066400000000000000000000213351470175262700200710ustar00rootroot00000000000000import os import random import time import pytest from ufo2ft.fontInfoData import ( dateStringToTimeValue, getAttrWithFallback, normalizeStringForPostscript, ) @pytest.fixture def info(InfoClass): self = InfoClass() self.familyName = "Family Name" self.styleName = "Style Name" self.unitsPerEm = 1000 self.descender = -250 self.xHeight = 450 self.capHeight = 600 self.ascender = 650 self.italicAngle = 0 return self class GetAttrWithFallbackTest: @pytest.mark.parametrize( "infoDict,expected", [ # no styleMapFamilyName, no styleMapStyleName ( {}, { "familyName": "Family Name", "styleName": "Style Name", "styleMapFamilyName": "Family Name Style Name", "styleMapStyleName": "regular", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Style Name", }, ), # no styleMapStyleName ( {"styleMapFamilyName": "Style Map Family Name"}, { "styleMapFamilyName": "Style Map Family Name", "styleMapStyleName": "regular", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Style Name", }, ), # no styleMapFamilyName, no styleMapStyleName but styleName="Regular" ( {"styleName": "Regular"}, { "familyName": "Family Name", "styleName": "Regular", "styleMapFamilyName": "Family Name", "styleMapStyleName": "regular", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Regular", }, ), # no styleMapFamilyName but styleName="Regular" ( {"styleName": "Regular", "styleMapStyleName": "regular"}, { "styleMapFamilyName": "Family Name", "styleMapStyleName": "regular", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Regular", }, ), # no styleMapStyleName but styleName="Regular" ( {"styleName": "Regular", "styleMapFamilyName": "Style Map Family Name"}, { "styleMapFamilyName": "Style Map Family Name", "styleMapStyleName": "regular", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Regular", }, ), # no styleMapFamilyName, no styleMapStyleName but styleName="Bold" ( {"styleName": "Bold"}, { "familyName": "Family Name", "styleName": "Bold", "styleMapFamilyName": "Family Name", "styleMapStyleName": "bold", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Bold", }, ), ], ) def test_family_and_style_names(self, info, infoDict, expected): for key, value in infoDict.items(): setattr(info, key, value) for key, value in expected.items(): assert getAttrWithFallback(info, key) == value def test_redundant_metadata(self, info): assert getAttrWithFallback(info, "openTypeNameVersion") == "Version 0.000" info.versionMinor = 1 info.versionMajor = 1 assert getAttrWithFallback(info, "openTypeNameVersion") == "Version 1.001" assert ( getAttrWithFallback(info, "openTypeNameUniqueID") == "1.001;NONE;FamilyName-StyleName" ) assert getAttrWithFallback(info, "postscriptSlantAngle") == 0 def test_unecessary_metadata(self, info): assert getAttrWithFallback(info, "postscriptWeightName") is None info.postscriptWeightName = "Normal" assert getAttrWithFallback(info, "postscriptWeightName") == "Normal" def test_vertical_metrics(self, info): assert getAttrWithFallback(info, "openTypeHheaAscender") == 950 assert getAttrWithFallback(info, "openTypeHheaDescender") == -250 assert getAttrWithFallback(info, "openTypeOS2TypoAscender") == 650 assert getAttrWithFallback(info, "openTypeOS2TypoDescender") == -250 assert getAttrWithFallback(info, "openTypeOS2WinAscent") == 950 assert getAttrWithFallback(info, "openTypeOS2WinDescent") == 250 def test_caret_slope(self, info): assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRise") == 1000 assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRun") == 0 info.italicAngle = -12 assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRise") == 1000 assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRun") == 213 info.italicAngle = 12 assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRise") == 1000 assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRun") == -213 info.openTypeHheaCaretSlopeRise = 2048 assert info.openTypeHheaCaretSlopeRun is None assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRise") == 2048 assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRun") == -435 info.openTypeHheaCaretSlopeRise = None info.openTypeHheaCaretSlopeRun = 200 assert info.openTypeHheaCaretSlopeRise is None assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRise") == -941 assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRun") == 200 def test_head_created(self, info): os.environ["SOURCE_DATE_EPOCH"] = "1514485183" try: assert ( getAttrWithFallback(info, "openTypeHeadCreated") == "2017/12/28 18:19:43" ) finally: del os.environ["SOURCE_DATE_EPOCH"] assert getAttrWithFallback(info, "openTypeHeadCreated") != "2017/12/28 18:19:43" def test_empty_info(self, InfoClass): info = InfoClass() assert getAttrWithFallback(info, "familyName") == "New Font" assert getAttrWithFallback(info, "styleName") == "Regular" assert getAttrWithFallback(info, "unitsPerEm") == 1000 assert getAttrWithFallback(info, "ascender") == 800 assert getAttrWithFallback(info, "capHeight") == 700 assert getAttrWithFallback(info, "xHeight") == 500 assert getAttrWithFallback(info, "descender") == -200 def test_empty_info_2048(self, InfoClass): info = InfoClass() info.unitsPerEm = 2048 assert getAttrWithFallback(info, "unitsPerEm") == 2048 assert getAttrWithFallback(info, "ascender") == 1638 assert getAttrWithFallback(info, "capHeight") == 1434 assert getAttrWithFallback(info, "xHeight") == 1024 assert getAttrWithFallback(info, "descender") == -410 def test_underline_position(self, info): assert getAttrWithFallback(info, "postscriptUnderlinePosition") == -75 info.postscriptUnderlinePosition = -485 assert getAttrWithFallback(info, "postscriptUnderlinePosition") == -485 class PostscriptBlueScaleFallbackTest: def test_without_blue_zones(self, info): postscriptBlueScale = getAttrWithFallback(info, "postscriptBlueScale") assert postscriptBlueScale == 0.039625 def test_with_blue_zones(self, info): info.postscriptBlueValues = [ -13, 0, 470, 483, 534, 547, 556, 569, 654, 667, 677, 690, 738, 758, ] info.postscriptOtherBlues = [-255, -245] postscriptBlueScale = getAttrWithFallback(info, "postscriptBlueScale") assert postscriptBlueScale == 0.0375 class NormalizeStringForPostscriptTest: def test_no_change(self): assert ( normalizeStringForPostscript("Sample copyright notice.") == "Sample copyright notice." ) class DateStringToTimeValueTest: def test_roundtrip_random_timestamp(self): timestamp = random.randint(0, 10**9) ds = time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(timestamp)) assert dateStringToTimeValue(ds) == timestamp if __name__ == "__main__": import sys sys.exit(pytest.main(sys.argv)) ufo2ft-3.3.1/tests/infoCompiler_test.py000066400000000000000000000046071470175262700201460ustar00rootroot00000000000000import pytest from fontTools.ttLib import TTFont from ufo2ft.infoCompiler import InfoCompiler from .outlineCompiler_test import getpath @pytest.fixture def testttf(): font = TTFont() font.importXML(getpath("TestFont.ttx")) return font @pytest.fixture def testufo(FontClass): font = FontClass(getpath("TestFont.ufo")) return font class InfoCompilerTest: def test_head(self, testttf, testufo): info = {"versionMajor": 5, "versionMinor": 6} compiler = InfoCompiler(testttf, testufo, info) ttf = compiler.compile() assert ttf["head"].fontRevision == 5.006 def test_hhea(self, testttf, testufo): info = {"openTypeHheaAscender": 100, "openTypeHheaDescender": -200} compiler = InfoCompiler(testttf, testufo, info) ttf = compiler.compile() assert ttf["hhea"].ascent == 100 assert ttf["hhea"].descent == -200 def test_vhea(self, testttf, testufo): info = { "openTypeVheaVertTypoAscender": 100, "openTypeVheaVertTypoDescender": -200, } compiler = InfoCompiler(testttf, testufo, info) ttf = compiler.compile() assert ttf["vhea"].ascent == 100 assert ttf["vhea"].descent == -200 def test_name(self, testttf, testufo): info = {"postscriptFontName": "TestFontOverride-Italic"} compiler = InfoCompiler(testttf, testufo, info) ttf = compiler.compile() assert ttf["name"].getDebugName(6) == "TestFontOverride-Italic" def test_OS2(self, testttf, testufo): info = {"openTypeOS2TypoAscender": 100, "openTypeOS2TypoDescender": -200} compiler = InfoCompiler(testttf, testufo, info) ttf = compiler.compile() assert ttf["OS/2"].sTypoAscender == 100 assert ttf["OS/2"].sTypoDescender == -200 def test_post(self, testttf, testufo): info = {"italicAngle": 30.6} compiler = InfoCompiler(testttf, testufo, info) ttf = compiler.compile() assert ttf["post"].italicAngle == 30.6 def test_gasp(self, testttf, testufo): info = { "openTypeGaspRangeRecords": [ { "rangeMaxPPEM": 8, "rangeGaspBehavior": [0, 2], } ] } compiler = InfoCompiler(testttf, testufo, info) ttf = compiler.compile() assert ttf["gasp"].gaspRange == {8: 5} ufo2ft-3.3.1/tests/instantiator_test.py000066400000000000000000000654121470175262700202400ustar00rootroot00000000000000import logging import fontTools.designspaceLib as designspaceLib import pytest from fontTools.pens.recordingPen import RecordingPen import ufo2ft.instantiator from ufo2ft.util import openFont, openFontFactory def test_interpolation_weight_width_class(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "MutatorSans" / "MutatorSans.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) for instance in designspace.instances: instance.font = generator.generate_instance(instance) # LightCondensed font = designspace.instances[0].font assert font.info.openTypeOS2WeightClass == 1 assert font.info.openTypeOS2WidthClass == 1 # BoldCondensed font = designspace.instances[1].font assert font.info.openTypeOS2WeightClass == 1000 assert font.info.openTypeOS2WidthClass == 1 # LightWide font = designspace.instances[2].font assert font.info.openTypeOS2WeightClass == 1 assert font.info.openTypeOS2WidthClass == 9 # BoldWide font = designspace.instances[3].font assert font.info.openTypeOS2WeightClass == 1000 assert font.info.openTypeOS2WidthClass == 9 # Medium_Narrow_I font = designspace.instances[4].font assert font.info.openTypeOS2WeightClass == 500 assert font.info.openTypeOS2WidthClass == 9 # Medium_Wide_I font = designspace.instances[5].font assert font.info.openTypeOS2WeightClass == 500 assert font.info.openTypeOS2WidthClass == 9 # Two font = designspace.instances[6].font assert font.info.openTypeOS2WeightClass == 1000 assert font.info.openTypeOS2WidthClass == 9 # One font = designspace.instances[7].font assert font.info.openTypeOS2WeightClass == 500 assert font.info.openTypeOS2WidthClass == 9 def test_default_groups_only(ufo_module, data_dir, caplog): """Test that only the default source's groups end up in instances.""" d = designspaceLib.DesignSpaceDocument() d.addAxisDescriptor( name="Weight", tag="wght", minimum=300, default=300, maximum=900 ) d.addSourceDescriptor(location={"Weight": 300}, font=ufo_module.Font()) d.addSourceDescriptor(location={"Weight": 900}, font=ufo_module.Font()) d.addInstanceDescriptor(styleName="2", location={"Weight": 400}) d.findDefault() d.sources[0].font.groups["public.kern1.GRK_alpha_alt_LC_1ST"] = [ "alpha.alt", "alphatonos.alt", ] d.sources[1].font.groups["public.kern1.GRK_alpha_LC_1ST"] = [ "alpha.alt", "alphatonos.alt", ] generator = ufo2ft.instantiator.Instantiator.from_designspace(d) assert "contains different groups than the default source" in caplog.text instance = generator.generate_instance(d.instances[0]) assert instance.groups == { "public.kern1.GRK_alpha_alt_LC_1ST": ["alpha.alt", "alphatonos.alt"] } def test_default_groups_only2(ufo_module, data_dir, caplog): """Test that the group difference warning is not triggered if non-default source groups are empty.""" d = designspaceLib.DesignSpaceDocument() d.addAxisDescriptor( name="Weight", tag="wght", minimum=300, default=300, maximum=900 ) d.addSourceDescriptor(location={"Weight": 300}, font=ufo_module.Font()) d.addSourceDescriptor(location={"Weight": 900}, font=ufo_module.Font()) d.addInstanceDescriptor(styleName="2", location={"Weight": 400}) d.findDefault() d.sources[0].font.groups["public.kern1.GRK_alpha_alt_LC_1ST"] = [ "alpha.alt", "alphatonos.alt", ] generator = ufo2ft.instantiator.Instantiator.from_designspace(d) assert "contains different groups than the default source" not in caplog.text instance = generator.generate_instance(d.instances[0]) assert instance.groups == { "public.kern1.GRK_alpha_alt_LC_1ST": ["alpha.alt", "alphatonos.alt"] } def test_interpolation_no_rounding(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "MutatorSans" / "MutatorSans.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) designspace.instances[4].location = {"weight": 123.456, "width": 789.123} generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=False ) instance_font = generator.generate_instance(designspace.instances[4]) assert isinstance(instance_font.info.ascender, float) assert isinstance(instance_font.kerning[("A", "J")], float) assert isinstance(instance_font["A"].contours[0][0].x, float) def test_interpolation_rounding(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "MutatorSans" / "MutatorSans.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) designspace.instances[4].location = {"weight": 123.456, "width": 789.123} generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[4]) assert isinstance(instance_font.info.ascender, int) assert isinstance(instance_font.kerning[("A", "J")], int) assert isinstance(instance_font["A"].contours[0][0].x, int) def test_weight_class_from_wght_axis(): assert ufo2ft.instantiator.weight_class_from_wght_value(-500) == 1 assert ufo2ft.instantiator.weight_class_from_wght_value(1.1) == 1 assert ufo2ft.instantiator.weight_class_from_wght_value(1) == 1 assert ufo2ft.instantiator.weight_class_from_wght_value(500.6) == 501 assert ufo2ft.instantiator.weight_class_from_wght_value(1000) == 1000 assert ufo2ft.instantiator.weight_class_from_wght_value(1000.0) == 1000 assert ufo2ft.instantiator.weight_class_from_wght_value(1000.1) == 1000 assert ufo2ft.instantiator.weight_class_from_wght_value(2000.1) == 1000 def test_width_class_from_wdth_axis(): assert ufo2ft.instantiator.width_class_from_wdth_value(-500) == 1 assert ufo2ft.instantiator.width_class_from_wdth_value(50) == 1 assert ufo2ft.instantiator.width_class_from_wdth_value(62.5) == 2 assert ufo2ft.instantiator.width_class_from_wdth_value(75) == 3 assert ufo2ft.instantiator.width_class_from_wdth_value(87.5) == 4 assert ufo2ft.instantiator.width_class_from_wdth_value(100) == 5 assert ufo2ft.instantiator.width_class_from_wdth_value(112) == 6 assert ufo2ft.instantiator.width_class_from_wdth_value(112.5) == 6 assert ufo2ft.instantiator.width_class_from_wdth_value(125) == 7 assert ufo2ft.instantiator.width_class_from_wdth_value(130) == 7 assert ufo2ft.instantiator.width_class_from_wdth_value(150) == 8 assert ufo2ft.instantiator.width_class_from_wdth_value(190) == 9 assert ufo2ft.instantiator.width_class_from_wdth_value(200) == 9 assert ufo2ft.instantiator.width_class_from_wdth_value(1000) == 9 def test_swap_glyph_names(ufo_module, data_dir): ufo = openFont(data_dir / "SwapGlyphNames" / "A.ufo", ufo_module=ufo_module) ufo2ft.instantiator.swap_glyph_names(ufo, "a", "a.swap") # Test swapped outlines. assert ufo["a"].unicode == 0x61 assert len(ufo["a"]) == 1 assert len(ufo["a"][0]) == 8 assert ufo["a"].width == 666 assert ufo["a.swap"].unicode is None assert len(ufo["a.swap"]) == 1 assert len(ufo["a.swap"][0]) == 4 assert ufo["a.swap"].width == 600 # Test swapped components. assert sorted(c.baseGlyph for c in ufo["aaa"].components) == [ "a.swap", "a.swap", "x", ] assert sorted(c.baseGlyph for c in ufo["aaa.swap"].components) == ["a", "a", "y"] # Test swapped anchors. assert [dict(a) for a in ufo["a"].anchors] == [ dict(x=153, y=0, name="bottom"), dict(x=153, y=316, name="top"), ] assert [dict(a) for a in ufo["a.swap"].anchors] == [ dict(x=351, y=0, name="bottom"), dict(x=351, y=613, name="top"), ] # Test swapped glyph kerning. assert ufo.kerning == { ("public.kern1.a", "x"): 10, ("public.kern1.aswap", "x"): 20, ("a", "y"): 40, ("a.swap", "y"): 30, ("y", "a"): 60, ("y", "a.swap"): 50, } # Test swapped group membership. assert ufo.groups == { "public.kern1.a": ["a.swap"], "public.kern1.aswap": ["a"], "public.kern2.a": ["a.swap", "a"], } # Swap a second time. ufo2ft.instantiator.swap_glyph_names(ufo, "aaa", "aaa.swap") # Test swapped glyphs. assert sorted(c.baseGlyph for c in ufo["aaa"].components) == ["a", "a", "y"] assert sorted(c.baseGlyph for c in ufo["aaa.swap"].components) == [ "a.swap", "a.swap", "x", ] # Test for no leftover temporary glyphs. assert {g.name for g in ufo} == { "space", "a", "a.swap", "aaa", "aaa.swap", "x", "y", } with pytest.raises(ufo2ft.instantiator.InstantiatorError, match="Cannot swap"): ufo2ft.instantiator.swap_glyph_names(ufo, "aaa", "aaa.swapa") def test_swap_glyph_names_spec(ufo_module, data_dir): """Test that the rule example in the designspaceLib spec works. `adieresis` should look the same as before the rule application. [1]: fonttools/Doc/source/designspaceLib#ufo-instances """ ufo = openFont(data_dir / "SwapGlyphNames" / "B.ufo", ufo_module=ufo_module) ufo2ft.instantiator.swap_glyph_names(ufo, "a", "a.alt") assert sorted(c.baseGlyph for c in ufo["adieresis"].components) == [ "a.alt", "dieresiscomb", ] assert sorted(c.baseGlyph for c in ufo["adieresis.alt"].components) == [ "a", "dieresiscomb", ] def test_rules_are_applied_deterministically(ufo_module, data_dir): """Test that a combination of designspace rules that end up mapping serveral input glyphs to the same destination glyph result in a correct and deterministic series of glyph swaps. The example is a font with 2 Q designs that depend on a style axis style < 0.5: Q style >= 0.5: Q.ss01 and each Q also has an alternative shape in bolder weights (like Skia) weight < 780: Q weight >= 780: Q.alt weight < 730: Q.ss01 weight >= 730: Q.ss01.alt Then we generate an instance at style = 1, weight = 900. From the rules, the default CMAP entry for Q should have the outlines of Q.ss01.alt from the black UFO. """ doc = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceRuleOrder" / "MyFont.designspace" ) doc.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace(doc) instance = generator.generate_instance(doc.instances[0]) pen = RecordingPen() instance["Q"].draw(pen) instance_recording = pen.value black_ufo = openFont( data_dir / "DesignspaceRuleOrder" / "MyFont_Black.ufo", ufo_module=ufo_module ) pen = RecordingPen() black_ufo["Q.ss01.alt"].draw(pen) black_ufo_recording = pen.value assert instance_recording == black_ufo_recording def test_raise_no_default_master(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "MutatorSans" / "MutatorSans_no_default.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) with pytest.raises(ufo2ft.instantiator.InstantiatorError, match="no default"): ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) def test_raise_failed_glyph_interpolation(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceBrokenTest" / "DesignspaceTest.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace(designspace) with pytest.raises( ufo2ft.instantiator.InstantiatorError, match="Failed to generate instance" ): for instance in designspace.instances: instance.font = generator.generate_instance(instance) def test_ignore_failed_glyph_interpolation(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceBrokenTest" / "DesignspaceTest.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace(designspace) generator.skip_export_glyphs.append("asas") for instance in designspace.instances: instance.font = generator.generate_instance(instance) assert ( not instance.font["asas"].contours and not instance.font["asas"].components ) def test_raise_anisotropic_location(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "MutatorSans" / "MutatorSans-width-only.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) designspace.instances[0].location["width"] = (100, 900) with pytest.raises( ufo2ft.instantiator.InstantiatorError, match="anisotropic instance locations" ): ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) def test_copy_nonkerning_group(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace(designspace) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.groups == { "nonkerning_group": ["A"], "public.kern2.asdf": ["A"], } def test_interpolation(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font["l"].width == 220 def test_interpolation_only_default(ufo_module, data_dir, caplog): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "MutatorSans" / "MutatorSans.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) for name in designspace.default.font.glyphOrder: if name != "A": del designspace.default.font[name] with caplog.at_level(logging.WARNING): generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) assert "contains glyphs that are missing from the" in caplog.text instance_font = generator.generate_instance(designspace.instances[0]) assert {g.name for g in instance_font} == {"A"} def test_interpolation_masters_as_instances(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceBrokenTest" / "Designspace-MastersAsInstances.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.info.styleName == "Light ASDF" assert instance_font["l"].width == 160 instance_font = generator.generate_instance(designspace.instances[1]) assert instance_font.info.styleName == "Bold ASDF" assert instance_font["l"].width == 280 def test_non_default_layer(ufo_module, data_dir, caplog): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "MutatorSans" / "MutatorSans-non-default-layer.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert {g.name for g in instance_font} == {"A", "S", "W"} def test_instance_attributes(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest-instance-attrs.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.info.familyName == "aaa" assert instance_font.info.styleName == "sss" assert instance_font.info.postscriptFontName == "ppp" assert instance_font.info.styleMapFamilyName == "yyy" assert instance_font.info.styleMapStyleName == "xxx" def test_instance_no_attributes(ufo_module, data_dir, caplog): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest-bare.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) with caplog.at_level(logging.WARNING): instance_font = generator.generate_instance(designspace.instances[0]) assert "missing the stylename attribute" in caplog.text assert instance_font.info.familyName == "MyFont" assert instance_font.info.styleName == "Light" assert instance_font.info.postscriptFontName is None assert instance_font.info.styleMapFamilyName is None assert instance_font.info.styleMapStyleName is None def test_axis_mapping(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest-wght-wdth.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.info.openTypeOS2WeightClass == 400 assert instance_font.info.openTypeOS2WidthClass == 5 assert instance_font.info.italicAngle is None assert instance_font.lib["designspace.location"] == [ ("weight", 100.0), ("width", 100.0), ] def test_axis_mapping_manual_os2_classes(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest-wght-wdth.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) designspace.sources[0].font.info.openTypeOS2WeightClass = 800 designspace.sources[0].font.info.openTypeOS2WidthClass = 7 designspace.sources[1].font.info.openTypeOS2WeightClass = 900 designspace.sources[1].font.info.openTypeOS2WidthClass = 9 generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.info.openTypeOS2WeightClass == 850 assert instance_font.info.openTypeOS2WidthClass == 8 assert instance_font.info.italicAngle is None assert instance_font.lib["designspace.location"] == [ ("weight", 100.0), ("width", 100.0), ] def test_axis_mapping_no_os2_width_class_inference(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest-bare.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) designspace.sources[0].font.info.openTypeOS2WeightClass = 800 designspace.sources[1].font.info.openTypeOS2WeightClass = 900 generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.info.openTypeOS2WeightClass == 850 assert instance_font.info.openTypeOS2WidthClass is None assert instance_font.info.italicAngle is None assert instance_font.lib["designspace.location"] == [("weight", 100.0)] def test_axis_mapping_no_os2_class_inference(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest-opsz.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.info.openTypeOS2WeightClass is None assert instance_font.info.openTypeOS2WidthClass is None assert instance_font.info.italicAngle is None assert instance_font.lib["designspace.location"] == [("optical", 15.0)] def test_axis_mapping_italicAngle_inference(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest-slnt.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.info.openTypeOS2WeightClass is None assert instance_font.info.openTypeOS2WidthClass is None assert instance_font.info.italicAngle == 40.123 assert instance_font.lib["designspace.location"] == [("slant", 40.123)] def test_lib_into_instance(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest-lib.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) assert designspace.default.font.lib["blorb"] == "asasa" assert "public.skipExportGlyphs" not in designspace.sources[0].font.lib instance_font = generator.generate_instance(designspace.instances[0]) assert instance_font.lib["blorb"] == "asasa" assert instance_font.lib["public.skipExportGlyphs"] == ["a", "b", "c"] instance_font2 = generator.generate_instance(designspace.instances[1]) assert instance_font2.lib["blorb"] == "asasa" assert instance_font2.lib["public.skipExportGlyphs"] == ["a", "b", "c"] def test_data_independence(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "DesignspaceTest" / "DesignspaceTest.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) instance_font1 = generator.generate_instance(designspace.instances[0]) designspace.instances[0].lib["aaaaaaaa"] = 1 instance_font2 = generator.generate_instance(designspace.instances[0]) instance_font1["l"].unicodes.append(2) assert instance_font1["l"].unicodes == [0x6C, 2] assert instance_font2["l"].unicodes == [0x6C] instance_font1["l"].lib["asdf"] = 1 assert instance_font1["l"].lib == {"asdf": 1} assert not instance_font2["l"].lib generator.copy_lib["sdjkhsjdhjdf"] = 1 instance_font1.lib["asdf"] = 1 assert instance_font1.lib == { "asdf": 1, "blorb": "asasa", "designspace.location": [("weight", 100.0)], "public.skipExportGlyphs": [], } assert instance_font2.lib == { "blorb": "asasa", "designspace.location": [("weight", 100.0)], "public.skipExportGlyphs": [], } assert generator.copy_info.openTypeOS2Panose == [2, 11, 5, 4, 2, 2, 2, 2, 2, 4] generator.copy_info.openTypeOS2Panose.append(1000) assert instance_font1.info.openTypeOS2Panose is None assert instance_font2.info.openTypeOS2Panose is None # copy_feature_text not tested because it is a(n immutable) string assert not generator.skip_export_glyphs generator.skip_export_glyphs.extend(["a", "b"]) assert not instance_font1.lib["public.skipExportGlyphs"] assert not instance_font2.lib["public.skipExportGlyphs"] instance_font1.lib["public.skipExportGlyphs"].append("z") assert not instance_font2.lib["public.skipExportGlyphs"] def test_skipped_fontinfo_attributes(): """Test that we consider all available font info attributes for copying.""" import fontMath.mathInfo import fontTools.ufoLib SKIPPED_ATTRS = { "guidelines", "macintoshFONDFamilyID", "macintoshFONDName", "openTypeNameCompatibleFullName", "openTypeNamePreferredFamilyName", "openTypeNamePreferredSubfamilyName", "openTypeNameUniqueID", "openTypeNameWWSFamilyName", "openTypeNameWWSSubfamilyName", "openTypeOS2Panose", "postscriptFontName", "postscriptFullName", "postscriptUniqueID", "styleMapFamilyName", "styleMapStyleName", "styleName", "woffMetadataUniqueID", "year", } assert ( fontTools.ufoLib.fontInfoAttributesVersion3 - set(fontMath.mathInfo._infoAttrs.keys()) - {"postscriptWeightName"} # Handled in fontMath specially. - ufo2ft.instantiator.UFO_INFO_ATTRIBUTES_TO_COPY_TO_INSTANCES == SKIPPED_ATTRS ) def test_designspace_v5_discrete_axis_raises_error(data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "MutatorSansLite" / "MutatorFamily_v5_discrete_axis.designspace" ) # The error message should advise to use `splitInterpolable()` with pytest.raises( ufo2ft.instantiator.InstantiatorError, match="splitInterpolable" ): ufo2ft.instantiator.Instantiator.from_designspace(designspace) def test_strict_math_glyph(ufo_module, data_dir): designspace = designspaceLib.DesignSpaceDocument.fromfile( data_dir / "InstantiatorStrictMathGlyph" / "StrictMathGlyph.designspace" ) designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module)) generator = ufo2ft.instantiator.Instantiator.from_designspace( designspace, round_geometry=True ) fonts = [ generator.generate_instance(instance) for instance in designspace.instances ] assert len(fonts) == 1 glyph = fonts[0]["test"] assert len(glyph.contours) == 1 assert len(glyph.contours[0].points) == 16 ufo2ft-3.3.1/tests/instructionCompiler_test.py000066400000000000000000000703571470175262700216010ustar00rootroot00000000000000import logging from functools import partial import pytest from fontTools.cu2qu.ufo import font_to_quadratic from fontTools.misc.fixedTools import floatToFixedToFloat from fontTools.pens.hashPointPen import HashPointPen from fontTools.pens.roundingPen import RoundingPointPen from fontTools.ttLib.tables._g_l_y_f import ( OVERLAP_COMPOUND, ROUND_XY_TO_GRID, USE_MY_METRICS, flagOverlapSimple, ) 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) round_pen = RoundingPointPen( hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14) ) glyph.drawPoints(round_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, 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, 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, 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, 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, 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 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 # ... except for '.notdef' which is frequently generated with caplog.at_level(logging.INFO, logger="ufo2ft.instructionCompiler"): ic.compileGlyphInstructions(None, ".notdef") assert ( "Skipping compilation of instructions for glyph '.notdef'" not 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 @pytest.mark.parametrize("autoUseMyMetrics", [True, False]) def test_set_composite_flags_auto_use_my_metrics_warn_if_components_mismatch( self, quadufo, quadfont, autoUseMyMetrics, caplog ): ic = InstructionCompiler(quadufo, quadfont, autoUseMyMetrics=autoUseMyMetrics) name = "h" glyph = quadufo[name] ttglyph = quadfont["glyf"][name] assert len(glyph.components) == len(ttglyph.components) glyph.clearComponents() # to produce an artificial len(components) mismatch assert len(glyph.components) != len(ttglyph.components) with caplog.at_level(logging.DEBUG, logger="ufo2ft.instructionCompiler"): ic._set_composite_flags(glyph=glyph, ttglyph=ttglyph) assert "Number of components differ" in caplog.text @pytest.mark.parametrize("overlap", [None, False, True]) def test_set_simple_flags(self, quadufo, quadfont, overlap): ic = InstructionCompiler(quadufo, quadfont) name = "a" glyph = quadufo[name] if overlap is not None: glyph.lib = {"public.truetype.overlap": overlap} ttglyph = quadfont["glyf"][name] ic._set_simple_flags(glyph=glyph, ttglyph=ttglyph) if overlap: assert ttglyph.flags[0] & flagOverlapSimple else: assert not ttglyph.flags[0] & flagOverlapSimple # 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" ufo2ft-3.3.1/tests/integration_test.py000066400000000000000000000564661470175262700200550ustar00rootroot00000000000000import difflib import io import logging import os import re import sys from pathlib import Path from textwrap import dedent import pytest from fontTools.designspaceLib import DesignSpaceDocument from fontTools.otlLib.optimize.gpos import COMPRESSION_LEVEL as GPOS_COMPRESSION_LEVEL from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.transformPen import TransformPen from fontTools.ttLib.tables._g_l_y_f import ( OVERLAP_COMPOUND, flagCubic, flagOverlapSimple, ) from ufo2ft import ( compileInterpolatableTTFs, compileOTF, compileTTF, compileVariableCFF2, compileVariableCFF2s, compileVariableTTF, compileVariableTTFs, ) from ufo2ft.constants import KEEP_GLYPH_NAMES, TRUETYPE_OVERLAP_KEY from ufo2ft.errors import InvalidFontData from ufo2ft.filters import TransformationsFilter def getpath(filename): dirname = os.path.dirname(__file__) return os.path.join(dirname, "data", filename) @pytest.fixture def testufo(FontClass): return FontClass(getpath("TestFont.ufo")) def readLines(f): f.seek(0) lines = [] for line in f.readlines(): # Elide ttLibVersion because it frequently changes. # Use os-native line separators so we can run difflib. if line.startswith("" + os.linesep) else: lines.append(line.rstrip() + os.linesep) return lines def expectTTX(font, expectedTTX, tables=None): with open(getpath(expectedTTX), encoding="utf-8") as f: expected = readLines(f) font.recalcTimestamp = False font["head"].created, font["head"].modified = 3570196637, 3601822698 font["head"].checkSumAdjustment = 0x12345678 f = io.StringIO() font.saveXML(f, tables=tables) actual = readLines(f) if actual != expected: for line in difflib.unified_diff( expected, actual, fromfile=expectedTTX, tofile="" ): sys.stderr.write(line) pytest.fail("TTX output is different from expected") @pytest.fixture(params=[None, True, False]) def useProductionNames(request): return request.param class IntegrationTest: _layoutTables = ["GDEF", "GSUB", "GPOS", "BASE"] # We have specific unit tests for CFF vs TrueType output, but we run # an integration test here to make sure things work end-to-end. # No need to test both formats for every single test case. def test_TestFont_TTF(self, testufo): ttf = compileTTF(testufo) expectTTX(ttf, "TestFont.ttx") def test_TestFont_CFF(self, testufo): otf = compileOTF(testufo) expectTTX(otf, "TestFont-CFF.ttx") def test_included_features(self, FontClass): """Checks how the compiler handles include statements in features.fea. The compiler should detect which features are defined by the features.fea inside the compiled UFO, or by feature files that are included from there. https://github.com/googlei18n/ufo2ft/issues/108 Relative paths should be resolved taking the UFO path as reference, not the embedded features.fea file. https://github.com/unified-font-object/ufo-spec/issues/55 """ ufo = FontClass(getpath("Bug108.ufo")) ttf = compileTTF(ufo) expectTTX(ttf, "Bug108.ttx", tables=self._layoutTables) def test_included_features_with_custom_include_dir(self, FontClass, tmp_path): ufo = FontClass(getpath("Bug108.ufo")) features_dir = tmp_path / "features" features_dir.mkdir() (features_dir / "foobarbaz.fea").write_text( Path(getpath("Bug108_included.fea")).read_text() ) ufo.features.text = "include(features/foobarbaz.fea);" ttf = compileTTF(ufo, feaIncludeDir=tmp_path) expectTTX(ttf, "Bug108.ttx", tables=self._layoutTables) def test_mti_features(self, FontClass): """Checks handling of UFOs with embdedded MTI/Monotype feature files https://github.com/googlei18n/fontmake/issues/289 """ ufo = FontClass(getpath("MTIFeatures.ufo")) ttf = compileTTF(ufo) expectTTX(ttf, "MTIFeatures.ttx", tables=self._layoutTables) def test_removeOverlaps_CFF(self, testufo): otf = compileOTF(testufo, removeOverlaps=True) expectTTX(otf, "TestFont-NoOverlaps-CFF.ttx") def test_removeOverlaps_CFF_pathops(self, testufo): otf = compileOTF(testufo, removeOverlaps=True, overlapsBackend="pathops") expectTTX(otf, "TestFont-NoOverlaps-CFF-pathops.ttx") def test_removeOverlaps(self, testufo): ttf = compileTTF(testufo, removeOverlaps=True) expectTTX(ttf, "TestFont-NoOverlaps-TTF.ttx") def test_removeOverlaps_pathops(self, testufo): ttf = compileTTF(testufo, removeOverlaps=True, overlapsBackend="pathops") expectTTX(ttf, "TestFont-NoOverlaps-TTF-pathops.ttx") def test_nestedComponents(self, FontClass): ufo = FontClass(getpath("NestedComponents-Regular.ufo")) ttf = compileTTF(ufo) assert ttf["maxp"].maxComponentDepth != 1 ttf = compileTTF(ufo, flattenComponents=True) assert ttf["maxp"].maxComponentDepth == 1 def test_nestedComponents_interpolatable(self, FontClass): ufos = [ FontClass(getpath("NestedComponents-Regular.ufo")), FontClass(getpath("NestedComponents-Bold.ufo")), ] ttfs = compileInterpolatableTTFs(ufos) for ttf in ttfs: assert ttf["maxp"].maxComponentDepth != 1 ttfs = compileInterpolatableTTFs(ufos, flattenComponents=True) for ttf in ttfs: assert ttf["maxp"].maxComponentDepth == 1 def test_nestedComponents_variable(self, FontClass): designspace = DesignSpaceDocument.fromfile( getpath("NestedComponents.designspace") ) designspace.loadSourceFonts(FontClass) vf = compileVariableTTF(designspace, flattenComponents=True) assert vf["maxp"].maxComponentDepth == 1 def test_interpolatableTTFs_lazy(self, FontClass): # two same UFOs **must** be interpolatable ufos = [FontClass(getpath("TestFont.ufo")) for _ in range(2)] ttfs = list(compileInterpolatableTTFs(ufos)) expectTTX(ttfs[0], "TestFont.ttx") expectTTX(ttfs[1], "TestFont.ttx") @pytest.mark.parametrize( "cff_version, expected_ttx", [(1, "TestFont-NoOptimize-CFF.ttx"), (2, "TestFont-NoOptimize-CFF2.ttx")], ids=["cff1", "cff2"], ) def test_optimizeCFF_none(self, testufo, cff_version, expected_ttx): otf = compileOTF(testufo, optimizeCFF=0, cffVersion=cff_version) expectTTX(otf, expected_ttx) @pytest.mark.parametrize( "cff_version, expected_ttx", [(1, "TestFont-Specialized-CFF.ttx"), (2, "TestFont-Specialized-CFF2.ttx")], ids=["cff1", "cff2"], ) def test_optimizeCFF_specialize(self, testufo, cff_version, expected_ttx): otf = compileOTF(testufo, optimizeCFF=1, cffVersion=cff_version) expectTTX(otf, expected_ttx) @pytest.mark.parametrize( "subroutinizer, cff_version, expected_ttx", [ (None, 1, "TestFont-CFF.ttx"), ("compreffor", 1, "TestFont-CFF-compreffor.ttx"), ("cffsubr", 1, "TestFont-CFF.ttx"), (None, 2, "TestFont-CFF2-cffsubr.ttx"), # ("compreffor", 2, "TestFont-CFF2-compreffor.ttx"), ("cffsubr", 2, "TestFont-CFF2-cffsubr.ttx"), ], ids=[ "default-cff1", "compreffor-cff1", "cffsubr-cff1", "default-cff2", # "compreffor-cff2", "cffsubr-cff2", ], ) def test_optimizeCFF_subroutinize( self, testufo, cff_version, subroutinizer, expected_ttx ): otf = compileOTF( testufo, optimizeCFF=2, cffVersion=cff_version, subroutinizer=subroutinizer ) expectTTX(otf, expected_ttx) def test_compileVariableTTF(self, designspace, useProductionNames): varfont = compileVariableTTF(designspace, useProductionNames=useProductionNames) expectTTX( varfont, "TestVariableFont-TTF{}.ttx".format( "-useProductionNames" if useProductionNames else "" ), ) def test_compileVariableCFF2(self, designspace, useProductionNames): varfont = compileVariableCFF2( designspace, useProductionNames=useProductionNames ) expectTTX( varfont, "TestVariableFont-CFF2{}.ttx".format( "-useProductionNames" if useProductionNames else "" ), ) def test_compileVariableCFF2_subroutinized(self, designspace): varfont = compileVariableCFF2(designspace, optimizeCFF=2) expectTTX(varfont, "TestVariableFont-CFF2-cffsubr.ttx") def test_debugFeatureFile(self, designspace): tmp = io.StringIO() _ = compileVariableTTF(designspace, debugFeatureFile=tmp) 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", [ ("TTF", {}, "TestFont-TTF-post3.ttx"), ("OTF", {"cffVersion": 2}, "TestFont-CFF2-post3.ttx"), ], ) def test_drop_glyph_names(self, testufo, output_format, options, expected_ttx): testufo.lib[KEEP_GLYPH_NAMES] = False compile_func = globals()[f"compile{output_format}"] ttf = compile_func(testufo, **options) expectTTX(ttf, expected_ttx) @pytest.mark.parametrize( "output_format, options, expected_ttx", [ ("VariableTTF", {}, "TestVariableFont-TTF-post3.ttx"), ("VariableCFF2", {}, "TestVariableFont-CFF2-post3.ttx"), ], ) def test_drop_glyph_names_variable( self, designspace, output_format, options, expected_ttx ): # set keepGlyphNames in the default UFO.lib where postProcessor finds it designspace.findDefault().font.lib[KEEP_GLYPH_NAMES] = False compile_func = globals()[f"compile{output_format}"] ttf = compile_func(designspace, **options) expectTTX(ttf, expected_ttx) @pytest.mark.parametrize( "compileFunc", [ compileOTF, compileTTF, ], ) def test_compile_filters(self, compileFunc, FontClass): ufo = FontClass(getpath("LayerFont-Regular.ufo")) filters = [TransformationsFilter(OffsetY=10)] ttf = compileFunc(ufo, filters=filters) pen1 = BoundsPen(ufo) glyph = ufo["a"] glyph.draw(pen1) glyphSet = ttf.getGlyphSet() tt_glyph = glyphSet["a"] pen2 = BoundsPen(glyphSet) tt_glyph.draw(pen2) assert pen1.bounds[0] == pen2.bounds[0] assert pen1.bounds[1] + 10 == pen2.bounds[1] assert pen1.bounds[2] == pen2.bounds[2] assert pen1.bounds[3] + 10 == pen2.bounds[3] @pytest.mark.parametrize( "compileFunc", [ compileVariableTTF, compileVariableCFF2, ], ) def test_compileVariable_filters(self, designspace, compileFunc): filters = [TransformationsFilter(OffsetY=10)] varfont = compileFunc(designspace, filters=filters) ufo = designspace.sources[0].font pen1 = BoundsPen(ufo) glyph = ufo["a"] glyph.draw(pen1) glyphSet = varfont.getGlyphSet() tt_glyph = glyphSet["a"] pen2 = BoundsPen(glyphSet) tt_glyph.draw(pen2) assert pen1.bounds[0] == pen2.bounds[0] assert pen1.bounds[1] + 10 == pen2.bounds[1] assert pen1.bounds[2] == pen2.bounds[2] assert pen1.bounds[3] + 10 == pen2.bounds[3] def test_compileInterpolatableTTFs(self, FontClass): ufos = [ FontClass(getpath("NestedComponents-Regular.ufo")), FontClass(getpath("NestedComponents-Bold.ufo")), ] filters = [TransformationsFilter(OffsetY=10)] ttfs = compileInterpolatableTTFs(ufos, filters=filters) for i, ttf in enumerate(ttfs): glyph = ufos[i]["a"] pen1 = BoundsPen(ufos[i]) glyph.draw(pen1) glyphSet = ttf.getGlyphSet() tt_glyph = glyphSet["uni0061"] pen2 = BoundsPen(glyphSet) tt_glyph.draw(pen2) assert pen1.bounds[0] == pen2.bounds[0] assert pen1.bounds[1] + 10 == pen2.bounds[1] assert pen1.bounds[2] == pen2.bounds[2] assert pen1.bounds[3] + 10 == pen2.bounds[3] def test_compileVariableTTFs(self, designspace_v5): fonts = compileVariableTTFs(designspace_v5) # NOTE: Test dumps were generated like this: # for k, font in fonts.items(): # font.recalcTimestamp = False # font["head"].created, font["head"].modified = 3570196637, 3601822698 # font["head"].checkSumAdjustment = 0x12345678 # font.saveXML(f"tests/data/DSv5/{k}-TTF.ttx") assert set(fonts.keys()) == { "MutatorSansVariable_Weight_Width", "MutatorSansVariable_Weight", "MutatorSansVariable_Width", "MutatorSerifVariable_Width", } # The STAT table is set to [SRIF=0, wght=[300, 700], wdth=[50, 200]] + S1 + S2 expectTTX( fonts["MutatorSansVariable_Weight_Width"], "DSv5/MutatorSansVariable_Weight_Width-TTF.ttx", ) # The STAT table is set to [SRIF=0, wght=[300, 700], wdth=50] expectTTX( fonts["MutatorSansVariable_Weight"], "DSv5/MutatorSansVariable_Weight-TTF.ttx", ) # The STAT table is set to [SRIF=0, wght=300, wdth=[50, 200]] expectTTX( fonts["MutatorSansVariable_Width"], "DSv5/MutatorSansVariable_Width-TTF.ttx", ) # The STAT table is set to [SRIF=1, wght=300, wdth=[50, 200]] expectTTX( fonts["MutatorSerifVariable_Width"], "DSv5/MutatorSerifVariable_Width-TTF.ttx", ) def test_compileVariableCFF2s(self, designspace_v5): fonts = compileVariableCFF2s(designspace_v5) # NOTE: Test dumps were generated like this: # for k, font in fonts.items(): # font.recalcTimestamp = False # font["head"].created, font["head"].modified = 3570196637, 3601822698 # font["head"].checkSumAdjustment = 0x12345678 # font.saveXML(f"tests/data/DSv5/{k}-CFF2.ttx") assert set(fonts.keys()) == { "MutatorSansVariable_Weight_Width", "MutatorSansVariable_Weight", "MutatorSansVariable_Width", "MutatorSerifVariable_Width", } # The STAT table is set to [SRIF=0, wght=[300, 700], wdth=[50, 200]] + S1 + S2 expectTTX( fonts["MutatorSansVariable_Weight_Width"], "DSv5/MutatorSansVariable_Weight_Width-CFF2.ttx", ) # The STAT table is set to [SRIF=0, wght=[300, 700], wdth=50] expectTTX( fonts["MutatorSansVariable_Weight"], "DSv5/MutatorSansVariable_Weight-CFF2.ttx", ) # The STAT table is set to [SRIF=0, wght=300, wdth=[50, 200]] expectTTX( fonts["MutatorSansVariable_Width"], "DSv5/MutatorSansVariable_Width-CFF2.ttx", ) # The STAT table is set to [SRIF=1, wght=300, wdth=[50, 200]] expectTTX( fonts["MutatorSerifVariable_Width"], "DSv5/MutatorSerifVariable_Width-CFF2.ttx", ) @pytest.mark.parametrize( "compileFunc", [ compileOTF, compileTTF, ], ) def test_compile_overloaded_codepoints(self, FontClass, compileFunc): """Confirm that ufo2ft produces an error when compiling a UFO with multiple glyphs using the same codepoint. Currently only covers individual UFOs.""" # Create a UFO in-memory with two glyphs using the same codepoint. ufo = FontClass() glyph_a = ufo.newGlyph("A") glyph_b = ufo.newGlyph("B") glyph_a.unicode = glyph_b.unicode = 0x0041 # Confirm that ufo2ft raises an appropriate exception with an # appropriate description when compiling. with pytest.raises( InvalidFontData, match=re.escape("cannot map 'B' to U+0041; already mapped to 'A'"), ): _ = compileFunc(ufo) def test_compileTTF_glyf1_not_allQuadratic(self, testufo): ttf = compileTTF(testufo, allQuadratic=False) expectTTX(ttf, "TestFont-not-allQuadratic.ttx", tables=["glyf"]) assert ttf["head"].glyphDataFormat == 1 @staticmethod def drawCurvedContour(glyph, transform=None): pen = glyph.getPen() if transform is not None: pen = TransformPen(pen, transform) pen.moveTo((500, 0)) pen.curveTo((500, 277.614), (388.072, 500), (250, 500)) pen.curveTo((111.928, 500), (0, 277.614), (0, 0)) pen.closePath() def test_compileVariableTTF_glyf1_not_allQuadratic(self, designspace): base_master = designspace.findDefault() assert base_master is not None # add a glyph with some curveTo to exercise the cu2qu codepath glyph = base_master.font.newGlyph("curved") glyph.width = 1000 self.drawCurvedContour(glyph) vf = compileVariableTTF(designspace, allQuadratic=False) expectTTX(vf, "TestVariableFont-TTF-not-allQuadratic.ttx", tables=["glyf"]) assert vf["head"].glyphDataFormat == 1 def test_compileTTF_overlap_simple_flag(self, testufo): """Test that the OVERLAP_{SIMPLE,COMPOUND} are set on glyphs that have it""" testufo["a"].lib = {TRUETYPE_OVERLAP_KEY: True} testufo["h"].lib = {TRUETYPE_OVERLAP_KEY: True} ttf = compileTTF(testufo, useProductionNames=False) # OVERLAP_SIMPLE is set on 'a' but not on 'b' assert ttf["glyf"]["a"].flags[0] & flagOverlapSimple assert not ttf["glyf"]["b"].flags[0] & flagOverlapSimple # OVERLAP_COMPOUND is set on 'h' but not on 'g' assert not ttf["glyf"]["g"].components[0].flags & OVERLAP_COMPOUND assert ttf["glyf"]["h"].components[0].flags & OVERLAP_COMPOUND def test_compileVariableTTF_notdefGlyph_with_curves(self, designspace): # The test DS contains two full masters (Regular and Bold) and one intermediate # 'sparse' (Medium) master, which does not contain a .notdef glyph and as such # is supposed to inherit one from the default master. If the notdef contains # any curves, an error occured because these are weren't been converted to # quadratic: https://github.com/googlefonts/ufo2ft/issues/501 # First we draw an additional contour containing cubic curves in the Regular # and Bold's .notdef glyphs for src_idx, transform in ((0, (1, 0, 0, 1, 0, 0)), (2, (2, 0, 0, 2, 0, 0))): notdef = designspace.sources[src_idx].font[".notdef"] self.drawCurvedContour(notdef, transform) assert ".notdef" not in designspace.sources[1].font.layers["Medium"] # this must NOT fail! vf = compileVariableTTF(designspace, convertCubics=True, allQuadratic=True) # and because allQuadratic=True, we expect .notdef contains no cubic curves assert not any(f & flagCubic for f in vf["glyf"][".notdef"].flags) # ensure .notdef has variations and was NOT dropped as incompatible, # varLib only warns: https://github.com/fonttools/fonttools/issues/2572 assert ".notdef" in vf["gvar"].variations def test_compileVariableCFF2_sparse_notdefGlyph(self, designspace): # test that sparse layer without .notdef does not participate in computation # of CFF2 and HVAR deltas for the .notdef glypht for src_idx, transform in ((0, (1, 0, 0, 1, 0, 0)), (2, (2, 0, 0, 2, 0, 0))): notdef = designspace.sources[src_idx].font[".notdef"] self.drawCurvedContour(notdef, transform) designspace.sources[2].font[".notdef"].width *= 2 assert ".notdef" not in designspace.sources[1].font.layers["Medium"] vf = compileVariableCFF2(designspace) expectTTX( vf, "TestVariableFont-CFF2-sparse-notdefGlyph.ttx", tables=["CFF2", "hmtx", "HVAR"], ) @pytest.mark.parametrize("compileMethod", [compileTTF, compileOTF]) @pytest.mark.parametrize("compression_level", [0, 9]) def test_compile_static_font_with_gpos_compression( self, caplog, compileMethod, testufo, compression_level ): with caplog.at_level(logging.INFO, logger="fontTools"): compileMethod(testufo, ftConfig={GPOS_COMPRESSION_LEVEL: compression_level}) disabled = compression_level == 0 logged = "Compacting GPOS..." in caplog.text assert logged ^ disabled @pytest.mark.parametrize("compileMethod", [compileVariableTTF, compileVariableCFF2]) @pytest.mark.parametrize("variableFeatures", [True, False]) @pytest.mark.parametrize("compression_level", [0, 9]) def test_compile_variable_font_with_gpos_compression( self, caplog, compileMethod, FontClass, variableFeatures, compression_level ): designspace = DesignSpaceDocument.fromfile(getpath("TestVarfea.designspace")) designspace.loadSourceFonts(FontClass) with caplog.at_level(logging.INFO, logger="fontTools"): compileMethod( designspace, ftConfig={GPOS_COMPRESSION_LEVEL: compression_level}, variableFeatures=variableFeatures, ) disabled = compression_level == 0 logged = "Compacting GPOS..." in caplog.text assert logged ^ disabled @pytest.mark.parametrize( "compileMethod", [compileVariableTTFs, compileVariableCFF2s] ) def test_apply_varfont_info(self, FontClass, compileMethod): designspace = DesignSpaceDocument.fromfile(getpath("TestVarFont.designspace")) designspace.loadSourceFonts(FontClass) fonts = compileMethod(designspace) assert len(fonts) == 2 expectTTX(fonts["MyFontVF1"], "TestVarFont-MyFontVF1.ttx", ["head", "name"]) expectTTX(fonts["MyFontVF2"], "TestVarFont-MyFontVF2.ttx", ["head", "name"]) def test_compile_variable_ttf_drop_implied_oncurves(self, FontClass, caplog): # https://github.com/googlefonts/ufo2ft/pull/817 designspace = DesignSpaceDocument.fromfile(getpath("OTestFont.designspace")) designspace.loadSourceFonts(FontClass) # dropImpliedOnCurves is False by default vf1 = compileVariableTTF(designspace) with caplog.at_level(logging.INFO, logger="fontTools.varLib"): vf2 = compileVariableTTF(designspace, dropImpliedOnCurves=True) assert "Failed to drop implied oncurves" not in caplog.text assert "Dropped 4 on-curve points" in caplog.text o1 = vf1["glyf"]["o"].coordinates o2 = vf2["glyf"]["o"].coordinates assert len(o1) == len(o2) + 4 if __name__ == "__main__": sys.exit(pytest.main(sys.argv)) ufo2ft-3.3.1/tests/outlineCompiler_test.py000066400000000000000000001421721470175262700206720ustar00rootroot00000000000000import logging import os from io import BytesIO import pytest from fontTools.colorLib.unbuilder import unbuildColrV1 from fontTools.cu2qu.ufo import font_to_quadratic from fontTools.misc.arrayTools import quantizeRect from fontTools.ttLib import TTFont from fontTools.ttLib.tables._g_l_y_f import USE_MY_METRICS from ufo2ft import ( compileInterpolatableOTFsFromDS, compileInterpolatableTTFs, compileInterpolatableTTFsFromDS, compileOTF, compileTTF, ) from ufo2ft.constants import ( GLYPHS_DONT_USE_PRODUCTION_NAMES, GLYPHS_MATH_CONSTANTS_KEY, GLYPHS_MATH_EXTENDED_SHAPE_KEY, GLYPHS_MATH_VARIANTS_KEY, OPENTYPE_POST_UNDERLINE_POSITION_KEY, SPARSE_OTF_MASTER_TABLES, SPARSE_TTF_MASTER_TABLES, USE_PRODUCTION_NAMES, ) from ufo2ft.errors import InvalidFontData from ufo2ft.fontInfoData import intListToNum from ufo2ft.outlineCompiler import OutlineOTFCompiler, OutlineTTFCompiler def getpath(filename): dirname = os.path.dirname(__file__) return os.path.join(dirname, "data", filename) @pytest.fixture def testufo(FontClass): font = FontClass(getpath("TestFont.ufo")) del font.lib["public.postscriptNames"] return font @pytest.fixture def quadufo(FontClass): font = FontClass(getpath("TestFont.ufo")) font_to_quadratic(font) 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")) @pytest.fixture def emptyufo(FontClass): font = FontClass() font.info.unitsPerEm = 1000 font.info.familyName = "Test Font" font.info.styleName = "Regular" font.info.ascender = 750 font.info.descender = -250 font.info.xHeight = 500 font.info.capHeight = 750 return font class OutlineTTFCompilerTest: def test_compile_with_gasp(self, quadufo): compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert "gasp" in compiler.otf assert compiler.otf["gasp"].gaspRange == {7: 10, 65535: 15} def test_compile_without_gasp(self, quadufo): quadufo.info.openTypeGaspRangeRecords = None compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert "gasp" not in compiler.otf def test_compile_empty_gasp(self, quadufo): # ignore empty gasp quadufo.info.openTypeGaspRangeRecords = [] compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert "gasp" not in compiler.otf def test_makeGlyphsBoundingBoxes(self, quadufo): compiler = OutlineTTFCompiler(quadufo) assert compiler.glyphBoundingBoxes[".notdef"] == (50, 0, 450, 750) # no outline data assert compiler.glyphBoundingBoxes["space"] is None # 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() # the first component in the 'Iacute' composite glyph ('acute') # does _not_ have the USE_MY_METRICS flag assert not (ttf["glyf"]["Iacute"].components[0].flags & USE_MY_METRICS) # the second component in the 'Iacute' composite glyph ('I') # has the USE_MY_METRICS flag set assert ttf["glyf"]["Iacute"].components[1].flags & USE_MY_METRICS # none of the 'I' components of the 'romanthree' glyph has # the USE_MY_METRICS flag set, because the composite glyph has a # different width for component in ttf["glyf"]["romanthree"].components: assert not (component.flags & USE_MY_METRICS) def test_autoUseMyMetrics_False(self, use_my_metrics_ufo): compiler = OutlineTTFCompiler(use_my_metrics_ufo, autoUseMyMetrics=False) ttf = compiler.compile() assert not (ttf["glyf"]["Iacute"].components[1].flags & USE_MY_METRICS) def test_autoUseMyMetrics_None(self, use_my_metrics_ufo): compiler = OutlineTTFCompiler(use_my_metrics_ufo) # setting 'autoUseMyMetrics' attribute to None disables the feature compiler.autoUseMyMetrics = None ttf = compiler.compile() assert not (ttf["glyf"]["Iacute"].components[1].flags & USE_MY_METRICS) def test_importTTX(self, testufo): compiler = OutlineTTFCompiler(testufo) otf = compiler.otf = TTFont() compiler.importTTX() assert "CUST" in otf assert otf["CUST"].data == b"\x00\x01\xbe\xef" assert otf.sfntVersion == "\x00\x01\x00\x00" def test_no_contour_glyphs(self, testufo): for glyph in testufo: glyph.clearContours() compiler = OutlineTTFCompiler(testufo) compiler.compile() assert compiler.otf["hhea"].advanceWidthMax == 600 assert compiler.otf["hhea"].minLeftSideBearing == 0 assert compiler.otf["hhea"].minRightSideBearing == 0 assert compiler.otf["hhea"].xMaxExtent == 0 def test_os2_no_widths(self, quadufo): for glyph in quadufo: glyph.width = 0 compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert compiler.otf["OS/2"].xAvgCharWidth == 0 def test_missing_component(self, emptyufo): ufo = emptyufo a = ufo.newGlyph("a") pen = a.getPen() pen.moveTo((0, 0)) pen.lineTo((100, 0)) pen.lineTo((100, 100)) pen.lineTo((0, 100)) pen.closePath() # a mixed contour/component glyph, which is decomposed by the # TTGlyphPen; one of the components does not exist thus should # be dropped b = ufo.newGlyph("b") pen = b.getPen() pen.moveTo((0, 200)) pen.lineTo((100, 200)) pen.lineTo((50, 300)) pen.closePath() pen.addComponent("a", (1, 0, 0, 1, 0, 0)) pen.addComponent("c", (1, 0, 0, 1, 0, 0)) # missing d = ufo.newGlyph("d") pen = d.getPen() pen.addComponent("c", (1, 0, 0, 1, 0, 0)) # missing e = ufo.newGlyph("e") pen = e.getPen() pen.addComponent("a", (1, 0, 0, 1, 0, 0)) pen.addComponent("c", (1, 0, 0, 1, 0, 0)) # missing compiler = OutlineTTFCompiler(ufo) ttFont = compiler.compile() glyf = ttFont["glyf"] assert glyf["a"].numberOfContours == 1 assert glyf["b"].numberOfContours == 2 assert glyf["d"].numberOfContours == 0 assert glyf["e"].numberOfContours == -1 # composite glyph assert len(glyf["e"].components) == 1 def test_contour_starts_with_offcurve_point(self, emptyufo): ufo = emptyufo a = ufo.newGlyph("a") pen = a.getPointPen() pen.beginPath() pen.addPoint((0, 0), None) pen.addPoint((0, 10), None) pen.addPoint((10, 10), None) pen.addPoint((10, 0), None) pen.addPoint((5, 0), "qcurve") pen.endPath() compiler = OutlineTTFCompiler(ufo) ttFont = compiler.compile() glyf = ttFont["glyf"] assert glyf["a"].numberOfContours == 1 coords, endPts, flags = glyf["a"].getCoordinates(glyf) assert list(coords) == [(0, 0), (0, 10), (10, 10), (10, 0), (5, 0)] assert endPts == [4] assert list(flags) == [0, 0, 0, 0, 1] def test_setupTable_meta(self, quadufo): quadufo.lib["public.openTypeMeta"] = { "appl": b"BEEF", "bild": b"AAAA", "dlng": ["en-Latn", "nl-Latn"], "slng": ["Latn"], "PRIB": b"Some private bytes", "PRIA": "Some private ascii string", "PRIU": "Some private unicode string…", } compiler = OutlineTTFCompiler(quadufo) ttFont = compiler.compile() meta = ttFont["meta"] assert meta.data["appl"] == b"BEEF" assert meta.data["bild"] == b"AAAA" assert meta.data["dlng"] == "en-Latn,nl-Latn" assert meta.data["slng"] == "Latn" assert meta.data["PRIB"] == b"Some private bytes" assert meta.data["PRIA"] == b"Some private ascii string" assert meta.data["PRIU"] == "Some private unicode string…".encode("utf-8") def test_setupTable_name(self, quadufo): compiler = OutlineTTFCompiler(quadufo) compiler.compile() actual = compiler.otf["name"].getName(1, 3, 1, 1033).string assert actual == "Some Font Regular (Style Map Family Name)" quadufo.info.openTypeNameRecords.append( { "nameID": 1, "platformID": 3, "encodingID": 1, "languageID": 1033, "string": "Custom Name for Windows", } ) compiler = OutlineTTFCompiler(quadufo) compiler.compile() actual = compiler.otf["name"].getName(1, 3, 1, 1033).string assert actual == "Custom Name for Windows" def test_post_underline_without_public_key(self, quadufo): compiler = OutlineTTFCompiler(quadufo) compiler.compile() actual = compiler.otf["post"].underlinePosition assert actual == -200 def test_post_underline_with_public_key(self, quadufo): quadufo.lib[OPENTYPE_POST_UNDERLINE_POSITION_KEY] = -485 compiler = OutlineTTFCompiler(quadufo) compiler.compile() actual = compiler.otf["post"].underlinePosition assert actual == -485 class OutlineOTFCompilerTest: def test_setupTable_CFF_all_blues_defined(self, testufo): testufo.info.postscriptBlueFuzz = 2 testufo.info.postscriptBlueShift = 8 testufo.info.postscriptBlueScale = 0.049736 testufo.info.postscriptForceBold = False testufo.info.postscriptBlueValues = [-12, 0, 486, 498, 712, 724] testufo.info.postscriptOtherBlues = [-217, -205] testufo.info.postscriptFamilyBlues = [-12, 0, 486, 498, 712, 724] testufo.info.postscriptFamilyOtherBlues = [-217, -205] compiler = OutlineOTFCompiler(testufo) compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() cff = compiler.otf["CFF "].cff private = cff[list(cff.keys())[0]].Private assert private.BlueFuzz == 2 assert private.BlueShift == 8 assert private.BlueScale == 0.049736 assert private.ForceBold == 0 assert private.BlueValues == [-12, 0, 486, 498, 712, 724] assert private.OtherBlues == [-217, -205] assert private.FamilyBlues == [-12, 0, 486, 498, 712, 724] assert private.FamilyOtherBlues == [-217, -205] def test_setupTable_CFF_no_blues_defined(self, testufo): # no blue values defined testufo.info.postscriptBlueValues = [] testufo.info.postscriptOtherBlues = [] testufo.info.postscriptFamilyBlues = [] testufo.info.postscriptFamilyOtherBlues = [] # the following attributes have no effect testufo.info.postscriptBlueFuzz = 2 testufo.info.postscriptBlueShift = 8 testufo.info.postscriptBlueScale = 0.049736 testufo.info.postscriptForceBold = False compiler = OutlineOTFCompiler(testufo) compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() cff = compiler.otf["CFF "].cff private = cff[list(cff.keys())[0]].Private # expect default values as defined in fontTools' cffLib.py assert private.BlueFuzz == 1 assert private.BlueShift == 7 assert private.BlueScale == 0.039625 assert private.ForceBold == 0 # CFF PrivateDict has no blues attributes assert not hasattr(private, "BlueValues") assert not hasattr(private, "OtherBlues") assert not hasattr(private, "FamilyBlues") assert not hasattr(private, "FamilyOtherBlues") def test_setupTable_CFF_some_blues_defined(self, testufo): testufo.info.postscriptBlueFuzz = 2 testufo.info.postscriptForceBold = True testufo.info.postscriptBlueValues = [] testufo.info.postscriptOtherBlues = [-217, -205] testufo.info.postscriptFamilyBlues = [] testufo.info.postscriptFamilyOtherBlues = [] compiler = OutlineOTFCompiler(testufo) compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() cff = compiler.otf["CFF "].cff private = cff[list(cff.keys())[0]].Private assert private.BlueFuzz == 2 assert private.BlueShift == 7 # default assert private.BlueScale == 0.039625 # default assert private.ForceBold is True assert not hasattr(private, "BlueValues") assert private.OtherBlues == [-217, -205] assert not hasattr(private, "FamilyBlues") assert not hasattr(private, "FamilyOtherBlues") @staticmethod def get_charstring_program(ttFont, glyphName): cff = ttFont["CFF "].cff charstrings = cff[list(cff.keys())[0]].CharStrings c, _ = charstrings.getItemAndSelector(glyphName) c.decompile() return c.program def assertProgramEqual(self, expected, actual): assert len(expected) == len(actual) for exp_token, act_token in zip(expected, actual): if isinstance(exp_token, str): assert exp_token == act_token else: assert not isinstance(act_token, str) assert exp_token == pytest.approx(act_token) def test_setupTable_CFF_round_all(self, testufo): # by default all floats are rounded to integer compiler = OutlineOTFCompiler(testufo) otf = compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() # glyph 'd' in TestFont.ufo contains float coordinates program = self.get_charstring_program(otf, "d") self.assertProgramEqual( program, [ -26, 151, 197, "rmoveto", -34, -27, -27, -33, -33, 27, -27, 34, 33, 27, 27, 33, 33, -27, 27, -33, "hvcurveto", "endchar", ], ) def test_setupTable_CFF_round_none(self, testufo): # roundTolerance=0 means 'don't round, keep all floats' compiler = OutlineOTFCompiler(testufo, roundTolerance=0) otf = compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() program = self.get_charstring_program(otf, "d") self.assertProgramEqual( program, [ -26, 150.66, 197.32, "rmoveto", -33.66, -26.67, -26.99, -33.33, -33.33, 26.67, -26.66, 33.66, 33.33, 26.66, 26.66, 33.33, 33.33, -26.66, 26.99, -33.33, "hvcurveto", "endchar", ], ) def test_setupTable_CFF_round_some(self, testufo): # only floats 'close enough' are rounded to integer compiler = OutlineOTFCompiler(testufo, roundTolerance=0.34) otf = compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() program = self.get_charstring_program(otf, "d") self.assertProgramEqual( program, [ -26, 150.66, 197, "rmoveto", -33.66, -27, -27, -33, -33, 27, -27, 33.66, 33.34, 26.65, 27, 33, 33, -26.65, 27, -33.34, "hvcurveto", "endchar", ], ) def test_setupTable_CFF_optimize(self, testufo): compiler = OutlineOTFCompiler(testufo, optimizeCFF=True) otf = compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() program = self.get_charstring_program(otf, "a") self.assertProgramEqual( program, [-12, 66, "hmoveto", 256, "hlineto", -128, 510, "rlineto", "endchar"], ) def test_setupTable_CFF_no_optimize(self, testufo): compiler = OutlineOTFCompiler(testufo, optimizeCFF=False) otf = compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() program = self.get_charstring_program(otf, "a") self.assertProgramEqual( program, [-12, 66, 0, "rmoveto", 256, 0, "rlineto", -128, 510, "rlineto", "endchar"], ) def test_makeGlyphsBoundingBoxes(self, testufo): compiler = OutlineOTFCompiler(testufo) # with default roundTolerance, all coordinates and hence the bounding # box values are rounded with otRound() assert compiler.glyphBoundingBoxes["d"] == (90, 77, 211, 197) def test_makeGlyphsBoundingBoxes_floats(self, testufo): # specifying a custom roundTolerance affects which coordinates are # rounded; in this case, the top-most Y coordinate stays a float # (197.32), hence the bbox.yMax (198) is rounded using math.ceiling() compiler = OutlineOTFCompiler(testufo, roundTolerance=0.1) assert compiler.glyphBoundingBoxes["d"] == (90, 77, 211, 198) def test_importTTX(self, testufo): compiler = OutlineOTFCompiler(testufo) otf = compiler.otf = TTFont(sfntVersion="OTTO") compiler.importTTX() assert "CUST" in otf assert otf["CUST"].data == b"\x00\x01\xbe\xef" assert otf.sfntVersion == "OTTO" def test_no_contour_glyphs(self, testufo): for glyph in testufo: glyph.clearContours() compiler = OutlineOTFCompiler(testufo) compiler.compile() assert compiler.otf["hhea"].advanceWidthMax == 600 assert compiler.otf["hhea"].minLeftSideBearing == 0 assert compiler.otf["hhea"].minRightSideBearing == 0 assert compiler.otf["hhea"].xMaxExtent == 0 def test_optimized_default_and_nominal_widths(self, FontClass): ufo = FontClass() ufo.info.unitsPerEm = 1000 for glyphName, width in ( (".notdef", 500), ("space", 250), ("a", 388), ("b", 410), ("c", 374), ("d", 374), ("e", 388), ("f", 410), ("g", 388), ("h", 410), ("i", 600), ("j", 600), ("k", 600), ("l", 600), ): glyph = ufo.newGlyph(glyphName) glyph.width = width compiler = OutlineOTFCompiler(ufo) compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_hmtx() compiler.setupTable_CFF() cff = compiler.otf["CFF "].cff topDict = cff[list(cff.keys())[0]] private = topDict.Private assert private.defaultWidthX == 600 assert private.nominalWidthX == 303 charStrings = topDict.CharStrings # the following have width == defaultWidthX, so it's omitted for g in ("i", "j", "k", "l"): assert charStrings.getItemAndSelector(g)[0].program == ["endchar"] # 'space' has width 250, so the width encoded in its charstring is: # 250 - nominalWidthX assert charStrings.getItemAndSelector("space")[0].program == [-53, "endchar"] def test_optimized_default_but_no_nominal_widths(self, FontClass): ufo = FontClass() ufo.info.familyName = "Test" ufo.info.styleName = "R" ufo.info.ascender = 1 ufo.info.descender = 1 ufo.info.capHeight = 1 ufo.info.xHeight = 1 ufo.info.unitsPerEm = 1000 ufo.info.postscriptDefaultWidthX = 500 for glyphName, width in ( (".notdef", 500), ("space", 500), ("a", 500), ): glyph = ufo.newGlyph(glyphName) glyph.width = width font = compileOTF(ufo) cff = font["CFF "].cff private = cff[list(cff.keys())[0]].Private assert private.defaultWidthX == 500 assert private.nominalWidthX == 0 def test_underline_without_public_key(self, testufo): # Test with no lib key compiler = OutlineOTFCompiler(testufo) compiler.compile() post = compiler.otf["post"].underlinePosition cff = compiler.otf["CFF "].cff cff_underline = cff[list(cff.keys())[0]].UnderlinePosition assert post == -200 assert cff_underline == -200 def test_underline_with_public_key(self, testufo): # Test with a lib key and postscriptUnderlinePosition testufo.lib[OPENTYPE_POST_UNDERLINE_POSITION_KEY] = -485 testufo.info.postscriptUnderlinePosition = -42 compiler = OutlineOTFCompiler(testufo) compiler.compile() post = compiler.otf["post"].underlinePosition cff = compiler.otf["CFF "].cff cff_underline = cff[list(cff.keys())[0]].UnderlinePosition assert post == -485 assert cff_underline == -42 def test_underline_with_public_key_and_no_psPosition(self, testufo): # Test with a lib key and no postscriptUnderlinePosition testufo.lib[OPENTYPE_POST_UNDERLINE_POSITION_KEY] = -485 testufo.info.postscriptUnderlinePosition = None testufo.info.postscriptUnderlineThickness = 100 compiler = OutlineOTFCompiler(testufo) compiler.compile() post = compiler.otf["post"].underlinePosition cff = compiler.otf["CFF "].cff cff_underline = cff[list(cff.keys())[0]].UnderlinePosition assert post == -485 assert cff_underline == -535 def test_underline_with_no_public_key_and_no_psPosition(self, testufo): compiler = OutlineOTFCompiler(testufo) compiler.compile() post = compiler.otf["post"].underlinePosition cff = compiler.otf["CFF "].cff cff_underline = cff[list(cff.keys())[0]].UnderlinePosition # Note: This is actually incorrect according to the post/cff # spec, but it is how UFO3 has things defined, and is expected # current behavior. assert post == -200 assert cff_underline == -200 def test_underline_ps_rounding(self, testufo): # Test rounding testufo.lib[OPENTYPE_POST_UNDERLINE_POSITION_KEY] = -485 testufo.info.postscriptUnderlinePosition = None testufo.info.postscriptUnderlineThickness = 43 compiler = OutlineOTFCompiler(testufo) compiler.compile() post = compiler.otf["post"].underlinePosition cff = compiler.otf["CFF "].cff cff_underline = cff[list(cff.keys())[0]].UnderlinePosition assert post == -485 assert cff_underline == -506 class GlyphOrderTest: def test_compile_original_glyph_order(self, quadufo): DEFAULT_ORDER = [ ".notdef", "space", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", ] compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert compiler.otf.getGlyphOrder() == DEFAULT_ORDER def test_compile_tweaked_glyph_order(self, quadufo): NEW_ORDER = [ ".notdef", "space", "b", "a", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", ] quadufo.lib["public.glyphOrder"] = NEW_ORDER compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert compiler.otf.getGlyphOrder() == NEW_ORDER def test_compile_strange_glyph_order(self, quadufo): """Move space and .notdef to end of glyph ids ufo2ft always puts .notdef first. """ NEW_ORDER = ["b", "a", "c", "d", "space", ".notdef"] EXPECTED_ORDER = [ ".notdef", "b", "a", "c", "d", "space", "e", "f", "g", "h", "i", "j", "k", "l", ] quadufo.lib["public.glyphOrder"] = NEW_ORDER compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER class NamesTest: @pytest.mark.parametrize( "prod_names_key, prod_names_value", [(USE_PRODUCTION_NAMES, False), (GLYPHS_DONT_USE_PRODUCTION_NAMES, True)], ids=["useProductionNames", "Don't use Production Names"], ) def test_compile_without_production_names( self, testufo, prod_names_key, prod_names_value ): expected = [ ".notdef", "space", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", ] result = compileTTF(testufo, useProductionNames=False) assert result.getGlyphOrder() == expected testufo.lib[prod_names_key] = prod_names_value result = compileTTF(testufo) assert result.getGlyphOrder() == expected def test_compile_with_production_names(self, testufo): original = [ ".notdef", "space", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", ] modified = [ ".notdef", "uni0020", "uni0061", "uni0062", "uni0063", "uni0064", "uni0065", "uni0066", "uni0067", "uni0068", "uni0069", "uni006A", "uni006B", "uni006C", ] result = compileTTF(testufo) assert result.getGlyphOrder() == original result = compileTTF(testufo, useProductionNames=True) assert result.getGlyphOrder() == modified testufo.lib[USE_PRODUCTION_NAMES] = True result = compileTTF(testufo) assert result.getGlyphOrder() == modified def test_postprocess_production_names_no_notdef(self, testufo): import ufo2ft del testufo[".notdef"] assert ".notdef" not in testufo result = compileTTF(testufo, useProductionNames=False) assert ".notdef" in result.getGlyphOrder() pp = ufo2ft.postProcessor.PostProcessor(result, testufo, glyphSet=None) try: f = pp.process(useProductionNames=True) except Exception as e: pytest.xfail("Unexpected exception: " + str(e)) assert ".notdef" in f.getGlyphOrder() CUSTOM_POSTSCRIPT_NAMES = { ".notdef": ".notdef", "space": "foo", "a": "bar", "b": "baz", "c": "meh", "d": "doh", "e": "bim", "f": "bum", "g": "bam", "h": "bib", "i": "bob", "j": "bub", "k": "kkk", "l": "lll", } @pytest.mark.parametrize("use_production_names", [None, True]) def test_compile_with_custom_postscript_names(self, testufo, use_production_names): testufo.lib["public.postscriptNames"] = self.CUSTOM_POSTSCRIPT_NAMES result = compileTTF(testufo, useProductionNames=use_production_names) assert sorted(result.getGlyphOrder()) == sorted( self.CUSTOM_POSTSCRIPT_NAMES.values() ) @pytest.mark.parametrize("use_production_names", [None, True]) def test_compile_with_custom_postscript_names_notdef_preserved( self, testufo, use_production_names ): custom_names = dict(self.CUSTOM_POSTSCRIPT_NAMES) del custom_names[".notdef"] testufo.lib["public.postscriptNames"] = custom_names result = compileTTF(testufo, useProductionNames=use_production_names) assert result.getGlyphOrder() == [ ".notdef", "foo", "bar", "baz", "meh", "doh", "bim", "bum", "bam", "bib", "bob", "bub", "kkk", "lll", ] def test_warn_name_exceeds_max_length(self, testufo, caplog): long_name = 64 * "a" testufo.newGlyph(long_name) with caplog.at_level(logging.WARNING, logger="ufo2ft.postProcessor"): result = compileTTF(testufo, useProductionNames=True) assert "length exceeds 63 characters" in caplog.text assert long_name in result.getGlyphOrder() def test_duplicate_glyph_names(self, testufo): order = ["ab", "ab.1", "a-b", "a/b", "ba"] testufo.lib["public.glyphOrder"] = order testufo.lib["public.postscriptNames"] = {"ba": "ab"} for name in order: if name not in testufo: testufo.newGlyph(name) result = compileTTF(testufo, useProductionNames=True).getGlyphOrder() assert result[1] == "ab" assert result[2] == "ab.1" assert result[3] == "ab.2" assert result[4] == "ab.3" assert result[5] == "ab.4" def test_too_long_production_name(self, testufo): name = "_".join(("a",) * 16) testufo.newGlyph(name) result = compileTTF(testufo, useProductionNames=True).getGlyphOrder() # the production name uniXXXX would exceed the max length so the # original name is used assert name in result class ColrCpalTest: def test_colr_cpal(self, FontClass): testufo = FontClass(getpath("ColorTest.ufo")) assert "com.github.googlei18n.ufo2ft.colorLayerMapping" in testufo.lib assert "com.github.googlei18n.ufo2ft.colorPalettes" in testufo.lib result = compileTTF(testufo) assert "COLR" in result assert "CPAL" in result layers = { gn: [(layer.name, layer.colorID) for layer in layers] for gn, layers in result["COLR"].ColorLayers.items() } assert layers == { "a": [("a.color1", 0), ("a.color2", 1)], "b": [("b.color1", 1), ("b.color2", 0)], "c": [("c.color2", 1), ("c.color1", 0)], } def test_colr_cpal_raw(self, FontClass): testufo = FontClass(getpath("ColorTestRaw.ufo")) assert "com.github.googlei18n.ufo2ft.colorLayers" in testufo.lib assert "com.github.googlei18n.ufo2ft.colorPalettes" in testufo.lib result = compileTTF(testufo) palettes = [ [(c.red, c.green, c.blue, c.alpha) for c in p] for p in result["CPAL"].palettes ] assert palettes == [[(255, 76, 26, 255), (0, 102, 204, 255)]] layers = { gn: [(layer.name, layer.colorID) for layer in layers] for gn, layers in result["COLR"].ColorLayers.items() } assert layers == {"a": [("a.color1", 0), ("a.color2", 1)]} def test_colr_cpal_otf(self, FontClass): testufo = FontClass(getpath("ColorTest.ufo")) assert "com.github.googlei18n.ufo2ft.colorLayerMapping" in testufo.lib assert "com.github.googlei18n.ufo2ft.colorPalettes" in testufo.lib result = compileOTF(testufo) assert "COLR" in result assert "CPAL" in result layers = { gn: [(layer.name, layer.colorID) for layer in layers] for gn, layers in result["COLR"].ColorLayers.items() } assert layers == { "a": [("a.color1", 0), ("a.color2", 1)], "b": [("b.color1", 1), ("b.color2", 0)], "c": [("c.color2", 1), ("c.color1", 0)], } def test_colr_cpal_interpolatable_ttf(self, FontClass): testufo = FontClass(getpath("ColorTest.ufo")) assert "com.github.googlei18n.ufo2ft.colorLayerMapping" in testufo.lib assert "com.github.googlei18n.ufo2ft.colorPalettes" in testufo.lib result = list(compileInterpolatableTTFs([testufo]))[0] assert "COLR" in result assert "CPAL" in result layers = { gn: [(layer.name, layer.colorID) for layer in layers] for gn, layers in result["COLR"].ColorLayers.items() } assert layers == { "a": [("a.color1", 0), ("a.color2", 1)], "b": [("b.color1", 1), ("b.color2", 0)], "c": [("c.color2", 1), ("c.color1", 0)], } @pytest.mark.parametrize("compileFunc", [compileTTF, compileOTF]) @pytest.mark.parametrize("manualClipBoxes", [True, False]) @pytest.mark.parametrize( "autoClipBoxes, quantization", [ (True, 1), (True, 32), (True, 100), (False, None), ], ) def test_colrv1_computeClipBoxes( self, FontClass, compileFunc, manualClipBoxes, autoClipBoxes, quantization, ): testufo = FontClass(getpath("COLRv1Test.ufo")) assert "com.github.googlei18n.ufo2ft.colorLayers" in testufo.lib assert "com.github.googlei18n.ufo2ft.colorPalettes" in testufo.lib assert "com.github.googlei18n.ufo2ft.colrClipBoxes" not in testufo.lib if manualClipBoxes: testufo.lib["com.github.googlei18n.ufo2ft.colrClipBoxes"] = [ ("a", (0, 0, 1000, 1000)) ] result = compileFunc( testufo, colrAutoClipBoxes=autoClipBoxes, colrClipBoxQuantization=lambda _ufo: quantization, ) palettes = [ [(c.red, c.green, c.blue, c.alpha) for c in p] for p in result["CPAL"].palettes ] assert palettes == [[(255, 76, 26, 255), (0, 102, 204, 255)]] colr = result["COLR"].table layers = unbuildColrV1(colr.LayerList, colr.BaseGlyphList) assert layers == { "a": { "Format": 1, "Layers": [ { "Format": 10, "Paint": {"Format": 2, "PaletteIndex": 0, "Alpha": 1.0}, "Glyph": "a.color1", }, { "Format": 10, "Paint": {"Format": 2, "PaletteIndex": 1, "Alpha": 1.0}, "Glyph": "a.color2", }, ], } } if manualClipBoxes or autoClipBoxes: assert colr.ClipList is not None clipBoxes = {g: clip.as_tuple() for g, clip in colr.ClipList.clips.items()} if manualClipBoxes: # the one that was set manually always prevails assert clipBoxes == {"a": (0, 0, 1000, 1000)} elif autoClipBoxes: # the clipBox that was computed automatically assert clipBoxes == { "a": quantizeRect((111, 82, 485, 626), quantization) } else: # no clipboxes, neither manual nor automatic assert colr.ClipList is None @pytest.mark.parametrize("compileFunc", [compileTTF, compileOTF]) def test_strip_color_codepoints(self, FontClass, compileFunc): """Test that glyphs in the color layer do not become accessible by codepoint in the final font, given that they are copied into the default layer as alternates. See: https://github.com/googlefonts/ufo2ft/pull/739#issuecomment-1516075892""" # Load a test UFO with color layers, and give a codepoint to one of the # glyphs in those layers. ufo = FontClass(getpath("ColorTest.ufo")) color_glyph = ufo.layers["color1"]["a"] color_glyph.unicode = 0x3020 # Build the UFO into a TTF or OTF. built = compileFunc(ufo) # Confirm that it has no entry for the codepoint above. cmap = built.getBestCmap() assert 0x3020 not in cmap class CmapTest: def test_cmap_BMP(self, testufo): compiler = OutlineOTFCompiler(testufo) otf = compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_cmap() assert "cmap" in otf cmap = otf["cmap"] assert len(cmap.tables) == 2 cmap4_0_3 = cmap.tables[0] cmap4_3_1 = cmap.tables[1] assert (cmap4_0_3.platformID, cmap4_0_3.platEncID) == (0, 3) assert (cmap4_3_1.platformID, cmap4_3_1.platEncID) == (3, 1) assert cmap4_0_3.language == cmap4_3_1.language assert cmap4_0_3.language == 0 mapping = {c: chr(c) for c in range(0x61, 0x6D)} mapping[0x20] = "space" assert cmap4_0_3.cmap == cmap4_3_1.cmap assert cmap4_0_3.cmap == mapping def test_cmap_nonBMP_with_UVS(self, testufo): u1F170 = testufo.newGlyph("u1F170") u1F170.unicode = 0x1F170 testufo.newGlyph("u1F170.text") testufo.lib["public.unicodeVariationSequences"] = { "FE0E": { "1F170": "u1F170.text", }, "FE0F": { "1F170": "u1F170", }, } compiler = OutlineOTFCompiler(testufo) otf = compiler.compile() assert "cmap" in otf cmap = otf["cmap"] cmap.compile(otf) assert len(cmap.tables) == 5 cmap4_0_3 = cmap.tables[0] cmap12_0_4 = cmap.tables[1] cmap14_0_5 = cmap.tables[2] cmap4_3_1 = cmap.tables[3] cmap12_3_10 = cmap.tables[4] assert (cmap4_0_3.platformID, cmap4_0_3.platEncID) == (0, 3) assert (cmap4_3_1.platformID, cmap4_3_1.platEncID) == (3, 1) assert cmap4_0_3.language == cmap4_3_1.language assert cmap4_0_3.language == 0 mapping = {c: chr(c) for c in range(0x61, 0x6D)} mapping[0x20] = "space" assert cmap4_0_3.cmap == cmap4_3_1.cmap assert cmap4_0_3.cmap == mapping assert (cmap12_0_4.platformID, cmap12_0_4.platEncID) == (0, 4) assert (cmap12_3_10.platformID, cmap12_3_10.platEncID) == (3, 10) assert cmap12_0_4.language == cmap12_3_10.language assert cmap12_0_4.language == 0 mapping[0x1F170] = "u1F170" assert cmap12_0_4.cmap == cmap12_3_10.cmap assert cmap12_0_4.cmap == mapping assert (cmap14_0_5.platformID, cmap14_0_5.platEncID) == (0, 5) assert cmap14_0_5.language == 0 assert cmap14_0_5.uvsDict == { 0xFE0E: [(0x1F170, "u1F170.text")], 0xFE0F: [(0x1F170, None)], } ASCII = [chr(c) for c in range(0x20, 0x7E)] @pytest.mark.parametrize( "unicodes, expected", [ [ASCII + ["Þ"], {0}], # Latin 1 [ASCII + ["Ľ"], {1}], # Latin 2: Eastern Europe [ASCII + ["Ľ", "┤"], {1, 58}], # Latin 2 [["Б"], {2}], # Cyrillic [["Б", "Ѕ", "┤"], {2, 57}], # IBM Cyrillic [["Б", "╜", "┤"], {2, 49}], # MS-DOS Russian [["Ά"], {3}], # Greek [["Ά", "½", "┤"], {3, 48}], # IBM Greek [["Ά", "√", "┤"], {3, 60}], # Greek, former 437 G [ASCII + ["İ"], {4}], # Turkish [ASCII + ["İ", "┤"], {4, 56}], # IBM turkish [["א"], {5}], # Hebrew [["א", "√", "┤"], {5, 53}], # Hebrew [["ر"], {6}], # Arabic [["ر", "√"], {6, 51}], # Arabic [["ر", "√", "┤"], {6, 51, 61}], # Arabic; ASMO 708 [ASCII + ["ŗ"], {7}], # Windows Baltic [ASCII + ["ŗ", "┤"], {7, 59}], # MS-DOS Baltic [ASCII + ["₫"], {8}], # Vietnamese [["ๅ"], {16}], # Thai [["エ"], {17}], # JIS/Japan [["ㄅ"], {18}], # Chinese: Simplified chars [["ㄱ"], {19}], # Korean wansung [["央"], {20}], # Chinese: Traditional chars [["곴"], {21}], # Korean Johab [ASCII + ["♥"], {30}], # OEM Character Set [ASCII + ["þ", "┤"], {54}], # MS-DOS Icelandic [ASCII + ["╚"], {62, 63}], # WE/Latin 1 [ASCII + ["┤", "√", "Å"], {50}], # MS-DOS Nordic [ASCII + ["┤", "√", "é"], {52}], # MS-DOS Canadian French [ASCII + ["┤", "√", "õ"], {55}], # MS-DOS Portuguese [ASCII + ["‰", "∑"], {29}], # Macintosh Character Set (US Roman) [[" ", "0", "1", "2", "අ"], {0}], # always fallback to Latin 1 ], ) def test_calcCodePageRanges(emptyufo, unicodes, expected): font = emptyufo for i, c in enumerate(unicodes): font.newGlyph("glyph%d" % i).unicode = ord(c) compiler = OutlineOTFCompiler(font) compiler.compile() assert compiler.otf["OS/2"].ulCodePageRange1 == intListToNum( expected, start=0, length=32 ) assert compiler.otf["OS/2"].ulCodePageRange2 == intListToNum( expected, start=32, length=32 ) def test_custom_layer_compilation(layertestrgufo): ufo = layertestrgufo font_otf = compileOTF(ufo, layerName="Medium") assert font_otf.getGlyphOrder() == [".notdef", "e"] font_ttf = compileTTF(ufo, layerName="Medium") assert font_ttf.getGlyphOrder() == [".notdef", "e"] def test_custom_layer_compilation_interpolatable(layertestrgufo, layertestbdufo): ufo1 = layertestrgufo ufo2 = layertestbdufo master_ttfs = list( compileInterpolatableTTFs([ufo1, ufo1, ufo2], layerNames=[None, "Medium", None]) ) assert master_ttfs[0].getGlyphOrder() == [ ".notdef", "a", "e", "s", "dotabovecomb", "edotabove", ] assert master_ttfs[1].getGlyphOrder() == [".notdef", "e"] assert master_ttfs[2].getGlyphOrder() == [ ".notdef", "a", "e", "s", "dotabovecomb", "edotabove", ] sparse_tables = [tag for tag in master_ttfs[1].keys() if tag != "GlyphOrder"] assert SPARSE_TTF_MASTER_TABLES.issuperset(sparse_tables) @pytest.mark.parametrize("inplace", [False, True], ids=["not inplace", "inplace"]) def test_custom_layer_compilation_interpolatable_from_ds(designspace, inplace): result = compileInterpolatableTTFsFromDS(designspace, inplace=inplace) assert (designspace is result) == inplace master_ttfs = [s.font for s in result.sources] assert master_ttfs[0].getGlyphOrder() == [ ".notdef", "a", "e", "s", "dotabovecomb", "edotabove", ] assert master_ttfs[1].getGlyphOrder() == [".notdef", "e"] assert master_ttfs[2].getGlyphOrder() == [ ".notdef", "a", "e", "s", "dotabovecomb", "edotabove", ] sparse_tables = [tag for tag in master_ttfs[1].keys() if tag != "GlyphOrder"] assert SPARSE_TTF_MASTER_TABLES.issuperset(sparse_tables) # sentinel value used by varLib to ignore the post table for this sparse # master when building the MVAR table assert master_ttfs[1]["post"].underlinePosition == -0x8000 assert master_ttfs[1]["post"].underlineThickness == -0x8000 @pytest.mark.parametrize("inplace", [False, True], ids=["not inplace", "inplace"]) def test_custom_layer_compilation_interpolatable_otf_from_ds(designspace, inplace): result = compileInterpolatableOTFsFromDS(designspace, inplace=inplace) assert (designspace is result) == inplace master_otfs = [s.font for s in result.sources] assert master_otfs[0].getGlyphOrder() == [ ".notdef", "a", "e", "s", "dotabovecomb", "edotabove", ] # 'edotabove' composite glyph needed to be decomposed because these are CFF fonts; # and because one of its components 'e' has an additional intermediate master, the # latter 'bubbled up' to the parent glyph when this got decomposed; hence why # we see 'edotabove' in master_otfs[1] below, but we do not in the previous test # with interpolatalbe TTFs where 'edotabove' stays a composite glyph. assert master_otfs[1].getGlyphOrder() == [".notdef", "e", "edotabove"] assert master_otfs[2].getGlyphOrder() == [ ".notdef", "a", "e", "s", "dotabovecomb", "edotabove", ] sparse_tables = [tag for tag in master_otfs[1].keys() if tag != "GlyphOrder"] assert SPARSE_OTF_MASTER_TABLES.issuperset(sparse_tables) def test_compilation_from_ds_missing_source_font(designspace): designspace.sources[0].font = None with pytest.raises(AttributeError, match="missing required 'font'"): compileInterpolatableTTFsFromDS(designspace) def test_compile_empty_ufo(FontClass): ufo = FontClass() font = compileTTF(ufo) assert font["name"].getName(1, 3, 1).toUnicode() == "New Font" assert font["name"].getName(2, 3, 1).toUnicode() == "Regular" assert font["name"].getName(4, 3, 1).toUnicode() == "New Font Regular" assert font["head"].unitsPerEm == 1000 assert font["OS/2"].sTypoAscender == 800 assert font["OS/2"].sCapHeight == 700 assert font["OS/2"].sxHeight == 500 assert font["OS/2"].sTypoDescender == -200 def test_pass_on_conversion_error(FontClass): ufo = FontClass() ufo.info.unitsPerEm = 2000 # Draw quarter circle glyph = ufo.newGlyph("test") pen = glyph.getPointPen() pen.beginPath() pen.addPoint((0, 43), segmentType="line") pen.addPoint((25, 43)) pen.addPoint((43, 25)) pen.addPoint((43, 0), segmentType="curve") pen.addPoint((0, 0), segmentType="line") pen.endPath() font1 = compileTTF(ufo) # Default error: 0.001 font2 = compileTTF(ufo, cubicConversionError=0.0005) # One off-curve: font1_coords = list(font1["glyf"]["test"].coordinates) assert font1_coords == [(0, 43), (0, 0), (43, 0), (43, 43)] # Two off-curves: font2_coords = list(font2["glyf"]["test"].coordinates) assert font2_coords == [(0, 43), (0, 0), (43, 0), (43, 19), (19, 43)] @pytest.mark.parametrize("CompilerClass", [OutlineOTFCompiler, OutlineTTFCompiler]) @pytest.mark.parametrize( "vendorID, expected", [ ("A", "A "), ("AA", "AA "), ("AAA", "AAA "), ("AAAA", "AAAA"), ], ) def test_achVendId_space_padded_if_less_than_4_chars( FontClass, CompilerClass, vendorID, expected ): ufo = FontClass() ufo.info.openTypeOS2VendorID = vendorID font = CompilerClass(ufo).compile() tmp = BytesIO() font.save(tmp) font = TTFont(tmp) assert font["OS/2"].achVendID == expected @pytest.mark.parametrize("compile", [compileTTF, compileOTF]) def test_MATH_table(FontClass, compile): ufo = FontClass(getpath("TestMathFont-Regular.ufo")) result = compile(ufo) assert "MATH" in result math = result["MATH"].table for key, value in ufo.lib[GLYPHS_MATH_CONSTANTS_KEY].items(): attr = getattr(math.MathConstants, key) if isinstance(attr, int): assert attr == value else: assert attr.Value == value extendedShapes = set(ufo.lib[GLYPHS_MATH_EXTENDED_SHAPE_KEY]) for glyph in ufo.lib[GLYPHS_MATH_EXTENDED_SHAPE_KEY]: if variants := ufo[glyph].lib.get(GLYPHS_MATH_VARIANTS_KEY): extendedShapes.update(variants.get("vVariants", [])) assert set(math.MathGlyphInfo.ExtendedShapeCoverage.glyphs) == extendedShapes assert set(math.MathVariants.VertGlyphCoverage.glyphs) == { "parenright", "parenleft", } assert math.MathVariants.VertGlyphConstruction assert len(math.MathVariants.VertGlyphConstruction) == 2 assert ( math.MathVariants.VertGlyphConstruction[0].GlyphAssembly.ItalicsCorrection.Value == 0 ) assert ( len(math.MathVariants.VertGlyphConstruction[0].GlyphAssembly.PartRecords) == 3 ) assert not math.MathVariants.HorizGlyphCoverage assert not math.MathVariants.HorizGlyphConstruction @pytest.mark.parametrize("compile", [compileTTF, compileOTF]) @pytest.mark.parametrize( "attribute", [ "vAssembly", "hAssembly", "vVariants", "hVariants", ], ) def test_MATH_table_ignore_empty(FontClass, compile, attribute): # Should not raise becaise of empty assembly/variants ufo = FontClass(getpath("TestMathFont-Regular.ufo")) ufo["parenright"].lib[GLYPHS_MATH_VARIANTS_KEY][attribute] = [] compile(ufo) @pytest.mark.parametrize("compile", [compileTTF, compileOTF]) @pytest.mark.parametrize("attribute", ["vAssembly", "hAssembly"]) def test_MATH_table_invalid(FontClass, compile, attribute): ufo = FontClass(getpath("TestMathFont-Regular.ufo")) ufo["parenright"].lib[GLYPHS_MATH_VARIANTS_KEY][attribute] = [ ["parenright.top", 0, 0], ["parenright.ext", 1, 100, 100], ["parenright.bot", 0, 100, 0], ] with pytest.raises(InvalidFontData, match="Invalid assembly"): compile(ufo) if __name__ == "__main__": import sys sys.exit(pytest.main(sys.argv)) ufo2ft-3.3.1/tests/preProcessor_test.py000066400000000000000000000535151470175262700202100ustar00rootroot00000000000000import logging import os import pytest from fontTools import designspaceLib from fontTools.cu2qu.ufo import CURVE_TYPE_LIB_KEY import ufo2ft from ufo2ft.constants import ( COLOR_LAYER_MAPPING_KEY, COLOR_LAYERS_KEY, COLOR_PALETTES_KEY, ) from ufo2ft.filters import FILTERS_KEY, loadFilterFromString from ufo2ft.filters.explodeColorLayerGlyphs import ExplodeColorLayerGlyphsFilter from ufo2ft.preProcessor import ( TTFInterpolatablePreProcessor, TTFPreProcessor, _init_explode_color_layer_glyphs_filter, ) def getpath(filename): dirname = os.path.dirname(__file__) return os.path.join(dirname, "data", filename) def glyph_has_qcurve(ufo, glyph_name): return any( s.segmentType == "qcurve" for contour in ufo[glyph_name] for s in contour ) class TTFPreProcessorTest: def test_no_inplace(self, FontClass): ufo = FontClass(getpath("TestFont.ufo")) glyphSet = TTFPreProcessor(ufo, inplace=False).process() assert not glyph_has_qcurve(ufo, "c") assert glyph_has_qcurve(glyphSet, "c") assert CURVE_TYPE_LIB_KEY not in ufo.layers.defaultLayer.lib def test_inplace_remember_curve_type(self, FontClass, caplog): caplog.set_level(logging.ERROR) ufo = FontClass(getpath("TestFont.ufo")) assert CURVE_TYPE_LIB_KEY not in ufo.lib assert CURVE_TYPE_LIB_KEY not in ufo.layers.defaultLayer.lib assert not glyph_has_qcurve(ufo, "c") TTFPreProcessor(ufo, inplace=True, rememberCurveType=True).process() assert CURVE_TYPE_LIB_KEY not in ufo.lib assert ufo.layers.defaultLayer.lib[CURVE_TYPE_LIB_KEY] == "quadratic" assert glyph_has_qcurve(ufo, "c") logger = "ufo2ft.filters.cubicToQuadratic" with caplog.at_level(logging.INFO, logger=logger): TTFPreProcessor(ufo, inplace=True, rememberCurveType=True).process() assert len(caplog.records) == 1 assert "Curves already converted to quadratic" in caplog.text assert glyph_has_qcurve(ufo, "c") def test_inplace_no_remember_curve_type(self, FontClass): ufo = FontClass(getpath("TestFont.ufo")) assert CURVE_TYPE_LIB_KEY not in ufo.lib assert CURVE_TYPE_LIB_KEY not in ufo.layers.defaultLayer.lib for _ in range(2): TTFPreProcessor(ufo, inplace=True, rememberCurveType=False).process() assert CURVE_TYPE_LIB_KEY not in ufo.lib assert CURVE_TYPE_LIB_KEY not in ufo.layers.defaultLayer.lib assert glyph_has_qcurve(ufo, "c") def test_custom_filters(self, FontClass): ufo1 = FontClass(getpath("TestFont.ufo")) ufo1.lib[FILTERS_KEY] = [ {"name": "transformations", "kwargs": {"OffsetX": -40}, "pre": True} ] ufo2 = FontClass(getpath("TestFont.ufo")) ufo2.lib[FILTERS_KEY] = [{"name": "transformations", "kwargs": {"OffsetY": 10}}] glyphSets0 = TTFPreProcessor(ufo1).process() glyphSets1 = TTFPreProcessor(ufo2).process() assert (glyphSets0["a"][0][0].x - glyphSets1["a"][0][0].x) == -40 assert (glyphSets1["a"][0][0].y - glyphSets0["a"][0][0].y) == 10 def test_custom_filters_as_argument(self, FontClass): from ufo2ft.filters import RemoveOverlapsFilter, TransformationsFilter ufo1 = FontClass(getpath("TestFont.ufo")) ufo2 = FontClass(getpath("TestFont.ufo")) filter1 = RemoveOverlapsFilter(backend="pathops") filter2 = TransformationsFilter(include=["d"], pre=True, OffsetY=-200) filter3 = TransformationsFilter(OffsetX=10) glyphSets0 = TTFPreProcessor( ufo1, filters=[filter1, filter2, filter3] ).process() glyphSets1 = TTFPreProcessor( ufo2, filters=[filter1, filter2, filter3] ).process() # Both UFOs have the same filters applied assert (glyphSets0["a"][0][0].x - glyphSets1["a"][0][0].x) == 0 # "a" has initially its starting point at (66, 0) assert (glyphSets0["a"][0][0].x, glyphSets0["a"][0][0].y) == (76, 0) assert (glyphSets1["a"][0][0].x, glyphSets1["a"][0][0].y) == (76, 0) # A component was shifted to overlap with another in a pre-filter # filter2, before overlaps were removed in a post-filter filter1 assert len(glyphSets0["d"].components) == 0 def test_custom_filters_in_both_lib_and_argument_with_ellipsis(self, FontClass): from ufo2ft.filters import TransformationsFilter ufo = FontClass(getpath("TestFont.ufo")) ufo.lib[FILTERS_KEY] = [ {"name": "transformations", "kwargs": {"OffsetX": 10}, "pre": True} ] glyphSet = TTFPreProcessor( ufo, filters=[..., TransformationsFilter(OffsetY=-10)] ).process() a = glyphSet["a"] assert (a[0][0].x, a[0][0].y) == (ufo["a"][0][0].x + 10, ufo["a"][0][0].y - 10) def test_no_convertCubics_reverseDirection(self, FontClass): ufo = FontClass(getpath("TestFont.ufo")) glyphSet = TTFPreProcessor( ufo, convertCubics=False, reverseDirection=True ).process() contours = [contour for contour in glyphSet["c"]] points = [point for point in contours[0]] assert points[0].segmentType == "line" assert points[1].segmentType is None assert points[2].segmentType is None assert points[3].segmentType == "curve" class TTFInterpolatablePreProcessorTest: def test_no_inplace(self, FontClass): ufo1 = FontClass(getpath("TestFont.ufo")) ufo2 = FontClass(getpath("TestFont.ufo")) ufos = [ufo1, ufo2] assert CURVE_TYPE_LIB_KEY not in ufo1.lib assert CURVE_TYPE_LIB_KEY not in ufo1.layers.defaultLayer.lib assert not glyph_has_qcurve(ufo1, "c") glyphSets = TTFInterpolatablePreProcessor(ufos, inplace=False).process() for i in range(2): assert glyph_has_qcurve(glyphSets[i], "c") assert CURVE_TYPE_LIB_KEY not in ufos[i].lib assert CURVE_TYPE_LIB_KEY not in ufos[i].layers.defaultLayer.lib def test_inplace_remember_curve_type(self, FontClass): ufo1 = FontClass(getpath("TestFont.ufo")) ufo2 = FontClass(getpath("TestFont.ufo")) ufos = [ufo1, ufo2] assert CURVE_TYPE_LIB_KEY not in ufo1.lib assert CURVE_TYPE_LIB_KEY not in ufo1.layers.defaultLayer.lib assert not glyph_has_qcurve(ufo1, "c") TTFInterpolatablePreProcessor( ufos, inplace=True, rememberCurveType=True ).process() assert ufo1.layers.defaultLayer.lib[CURVE_TYPE_LIB_KEY] == "quadratic" assert glyph_has_qcurve(ufo1, "c") assert ufo2.layers.defaultLayer.lib[CURVE_TYPE_LIB_KEY] == "quadratic" assert glyph_has_qcurve(ufo2, "c") def test_inplace_no_remember_curve_type(self, FontClass): ufo1 = FontClass(getpath("TestFont.ufo")) ufo2 = FontClass(getpath("TestFont.ufo")) ufos = [ufo1, ufo2] for _ in range(2): TTFInterpolatablePreProcessor( ufos, inplace=True, rememberCurveType=False ).process() assert CURVE_TYPE_LIB_KEY not in ufo1.layers.defaultLayer.lib assert CURVE_TYPE_LIB_KEY not in ufo2.layers.defaultLayer.lib assert glyph_has_qcurve(ufo1, "c") assert glyph_has_qcurve(ufo2, "c") def test_custom_filters(self, FontClass): ufo1 = FontClass(getpath("TestFont.ufo")) ufo1.lib[FILTERS_KEY] = [ {"name": "transformations", "kwargs": {"OffsetX": -40}, "pre": True} ] ufo2 = FontClass(getpath("TestFont.ufo")) ufo2.lib[FILTERS_KEY] = [{"name": "transformations", "kwargs": {"OffsetY": 10}}] ufos = [ufo1, ufo2] glyphSets = TTFInterpolatablePreProcessor(ufos).process() assert (glyphSets[0]["a"][0][0].x - glyphSets[1]["a"][0][0].x) == -40 assert (glyphSets[1]["a"][0][0].y - glyphSets[0]["a"][0][0].y) == 10 def test_custom_filters_as_argument(self, FontClass): ufo1 = FontClass(getpath("TestFont.ufo")) ufo2 = FontClass(getpath("TestFont.ufo")) filter1 = loadFilterFromString("RemoveOverlapsFilter(backend='pathops')") filter2 = loadFilterFromString( "TransformationsFilter(OffsetY=-200, include=['d'], pre=True)" ) filter3 = loadFilterFromString("TransformationsFilter(OffsetX=10)") ufos = [ufo1, ufo2] glyphSets = TTFInterpolatablePreProcessor( ufos, filters=[filter1, filter2, filter3], ).process() # Both UFOs have the same filters applied assert (glyphSets[0]["a"][0][0].x - glyphSets[1]["a"][0][0].x) == 0 # "a" has initially its starting point at (66, 0) assert (glyphSets[0]["a"][0][0].x, glyphSets[0]["a"][0][0].y) == (76, 0) assert (glyphSets[1]["a"][0][0].x, glyphSets[1]["a"][0][0].y) == (76, 0) # A component was shifted to overlap with another in a pre-filter # filter2, before overlaps were removed in a post-filter filter1 assert len(glyphSets[0]["d"].components) == 0 def test_custom_filters_in_both_lib_and_argument_with_ellipsis(self, FontClass): from ufo2ft.filters import TransformationsFilter ufo1 = FontClass(getpath("TestFont.ufo")) ufo1.lib[FILTERS_KEY] = [ {"name": "transformations", "kwargs": {"OffsetX": 10}, "pre": True} ] ufo2 = FontClass(getpath("TestFont.ufo")) ufo2.lib[FILTERS_KEY] = [ {"name": "transformations", "kwargs": {"OffsetX": 20}, "pre": True} ] glyphSets = TTFInterpolatablePreProcessor( [ufo1, ufo2], filters=[..., TransformationsFilter(OffsetY=-10)] ).process() a1 = glyphSets[0]["a"] assert (a1[0][0].x, a1[0][0].y) == ( ufo1["a"][0][0].x + 10, ufo1["a"][0][0].y - 10, ) a2 = glyphSets[1]["a"] assert (a2[0][0].x, a2[0][0].y) == ( ufo2["a"][0][0].x + 20, ufo2["a"][0][0].y - 10, ) def test_no_convertCubics_reverseDirection(self, FontClass): ufo1 = FontClass(getpath("TestFont.ufo")) ufo2 = FontClass(getpath("TestFont.ufo")) glyphSets = TTFInterpolatablePreProcessor( [ufo1, ufo2], convertCubics=False, reverseDirection=True ).process() for glyphSet in glyphSets: contours = [contour for contour in glyphSet["c"]] points = [point for point in contours[0]] assert points[0].segmentType == "line" assert points[1].segmentType is None assert points[2].segmentType is None assert points[3].segmentType == "curve" class SkipExportGlyphsTest: def test_skip_export_glyphs_filter(self, FontClass): from ufo2ft.util import _GlyphSet ufo = FontClass(getpath("IncompatibleMasters/NewFont-Regular.ufo")) skipExportGlyphs = ["b", "d"] glyphSet = _GlyphSet.from_layer(ufo, skipExportGlyphs=skipExportGlyphs) assert set(glyphSet.keys()) == {"a", "c", "e", "f"} assert len(glyphSet["a"]) == 1 assert not glyphSet["a"].components assert len(glyphSet["c"]) == 5 # 4 "d" components decomposed plus 1 outline assert list(c.baseGlyph for c in glyphSet["c"].components) == ["a"] assert len(glyphSet["e"]) == 1 assert list(c.baseGlyph for c in glyphSet["e"].components) == ["c", "c"] assert not glyphSet["f"] assert list(c.baseGlyph for c in glyphSet["f"].components) == ["a", "a"] def test_skip_export_glyphs_filter_nested(self, FontClass): from ufo2ft.util import _GlyphSet ufo = FontClass() glyph_N = ufo.newGlyph("N") glyph_N.width = 100 pen = glyph_N.getPen() pen.moveTo((0, 0)) pen.lineTo((300, 0)) pen.lineTo((300, 400)) pen.lineTo((0, 400)) pen.closePath() glyph_o = ufo.newGlyph("o") glyph_o.width = 100 pen = glyph_o.getPen() pen.moveTo((0, 0)) pen.lineTo((300, 0)) pen.lineTo((300, 300)) pen.lineTo((0, 300)) pen.closePath() glyph_onumero = ufo.newGlyph("_o.numero") glyph_onumero.width = 100 pen = glyph_onumero.getPen() pen.addComponent("o", (-1, 0, 0, -1, 0, 100)) pen.moveTo((0, 0)) pen.lineTo((300, 0)) pen.lineTo((300, 50)) pen.lineTo((0, 50)) pen.closePath() glyph_numero = ufo.newGlyph("numero") glyph_numero.width = 200 pen = glyph_numero.getPen() pen.addComponent("N", (1, 0, 0, 1, 0, 0)) pen.addComponent("_o.numero", (1, 0, 0, 1, 400, 0)) skipExportGlyphs = ["_o.numero"] glyphSet = _GlyphSet.from_layer(ufo, skipExportGlyphs=skipExportGlyphs) # "numero" now contains two components "N" and "o", and one contour from the # decomposed "_o.numero" assert {c.baseGlyph for c in glyphSet["numero"].components} == {"N", "o"} assert len(glyphSet["numero"]) == 1 assert set(glyphSet.keys()) == {"N", "numero", "o"} # "_o.numero" is gone def test_skip_export_glyphs_designspace(self, FontClass): # Designspace has a public.skipExportGlyphs lib key excluding "b" and "d". designspace = designspaceLib.DesignSpaceDocument.fromfile( getpath("IncompatibleMasters/IncompatibleMasters.designspace") ) for source in designspace.sources: source.font = FontClass( getpath(os.path.join("IncompatibleMasters", source.filename)) ) ufo2ft.compileInterpolatableTTFsFromDS(designspace, inplace=True) for source in designspace.sources: assert source.font.getGlyphOrder() == [".notdef", "a", "c", "e", "f"] gpos_table = source.font["GPOS"].table assert gpos_table.LookupList.Lookup[0].SubTable[0].Coverage.glyphs == [ "a", "e", "f", ] glyphs = source.font["glyf"].glyphs for g in glyphs.values(): g.expand(source.font["glyf"]) assert glyphs["a"].numberOfContours == 1 assert not hasattr(glyphs["a"], "components") assert glyphs["c"].numberOfContours == 6 assert not hasattr(glyphs["c"], "components") assert glyphs["e"].numberOfContours == 13 assert not hasattr(glyphs["e"], "components") assert glyphs["f"].isComposite() def test_skip_export_glyphs_designspace_variable(self, FontClass): # The designspace has a public.skipExportGlyphs lib key excluding "_stroke"; # there are four sources, a Regular, Medium, Semibold and Bold; there is a # composite glyph "Astroke" that is composed of "A" and "_stroke". designspace = designspaceLib.DesignSpaceDocument.fromfile( getpath("SkipExportGlyphsTest.designspace") ) designspace.loadSourceFonts(FontClass) vf = ufo2ft.compileVariableTTF(designspace, useProductionNames=False) # We expect that "_stroke" glyph is not exported and "Astroke" is decomposed to # simple contour glyph. glyf = vf["glyf"] assert "_stroke" not in glyf assert "Astroke" in glyf Astroke = glyf["Astroke"] assert not Astroke.isComposite() assert Astroke.numberOfContours == 3 # 'Astroke' composite glyph should have 3 delta sets in gvar: two (corresponding # to the Semibold and Bold masters) were already in the sources, and a third one # was added by the preprocessor for the Medium master, because "_stroke" was # present in the Medium and marked as non-export, to be decomposed. gvar = vf["gvar"] assert len(gvar.variations["Astroke"]) == 3 # Now we add a new composite glyph "_stroke.alt" to the full Regular and Bold # sources and replace reference to the "_stroke" component in the "Astroke" # with the new "_stroke.alt". So "Astroke" has now a component which in turn # references another component (i.e. nested components). We also set the # public.skipExportGlyphs to exclude "_stroke.alt" from the export, but not # "_stroke" anymore, which should now be exported. designspace.lib["public.skipExportGlyphs"] = ["_stroke.alt"] num_Astroke_sources = 0 for source in designspace.sources: if source.layerName is None: layer = source.font.layers.defaultLayer stroke_alt = layer.newGlyph("_stroke.alt") stroke_alt.getPen().addComponent("_stroke", (1, 0, 0, 1, 0, -100)) else: layer = source.font.layers[source.layerName] if "Astroke" in layer: Astroke = layer["Astroke"] for component in Astroke.components: if component.baseGlyph == "_stroke": component.baseGlyph = "_stroke.alt" num_Astroke_sources += 1 vf = ufo2ft.compileVariableTTF(designspace, useProductionNames=False) # we expect that "_stroke.alt" glyph is not exported and the reference to it # in "Astroke" is replaced with "_stroke" with the offset adjusted. "Astroke" # itself should NOT be decomposed to simple glyph. glyf = vf["glyf"] assert "_stroke.alt" not in glyf assert "_stroke" in glyf assert "Astroke" in glyf Astroke = glyf["Astroke"] assert Astroke.isComposite() assert [c.glyphName for c in Astroke.components] == ["A", "_stroke"] stroke_comp = Astroke.components[1] assert (stroke_comp.x, stroke_comp.y) == (0, -100) # 'Astroke' composite glyph should have 2 delta sets in gvar: i.e. one for each # of the non-default masters it was originally present in. No additional # master should be added by the preprocessor in this case. gvar = vf["gvar"] assert len(gvar.variations["Astroke"]) == num_Astroke_sources - 1 def test_skip_export_glyphs_multi_ufo(self, FontClass): # Bold has a public.skipExportGlyphs lib key excluding "b", "d" and "f". ufo1 = FontClass(getpath("IncompatibleMasters/NewFont-Regular.ufo")) ufo2 = FontClass(getpath("IncompatibleMasters/NewFont-Bold.ufo")) fonts = ufo2ft.compileInterpolatableTTFs([ufo1, ufo2], inplace=True) for font in fonts: assert set(font.getGlyphOrder()) == {".notdef", "a", "c", "e"} gpos_table = font["GPOS"].table assert gpos_table.LookupList.Lookup[0].SubTable[0].Coverage.glyphs == ["a"] glyphs = font["glyf"].glyphs for g in glyphs.values(): g.expand(font["glyf"]) assert glyphs["a"].numberOfContours == 1 assert not hasattr(glyphs["a"], "components") assert glyphs["c"].numberOfContours == 6 assert not hasattr(glyphs["c"], "components") assert glyphs["e"].numberOfContours == 13 assert not hasattr(glyphs["e"], "components") def test_skip_export_glyphs_single_ufo(self, FontClass): # UFO has a public.skipExportGlyphs lib key excluding "b", "d" and "f". ufo = FontClass(getpath("IncompatibleMasters/NewFont-Bold.ufo")) font = ufo2ft.compileTTF(ufo, inplace=True) assert set(font.getGlyphOrder()) == {".notdef", "a", "c", "e"} gpos_table = font["GPOS"].table assert gpos_table.LookupList.Lookup[0].SubTable[0].Coverage.glyphs == ["a"] glyphs = font["glyf"].glyphs for g in glyphs.values(): g.expand(font["glyf"]) assert glyphs["a"].numberOfContours == 1 assert not hasattr(glyphs["a"], "components") assert glyphs["c"].numberOfContours == 6 assert not hasattr(glyphs["c"], "components") assert glyphs["e"].numberOfContours == 13 assert not hasattr(glyphs["e"], "components") def test_decompose_transformed_different_transforms(self, FontClass): ufo1 = FontClass(getpath("ComponentTransformTest-Regular.ufo")) ufo2 = FontClass(getpath("ComponentTransformTest-Bold.ufo")) fonts = ufo2ft.compileInterpolatableTTFs([ufo1, ufo2], inplace=True) for font in fonts: glyphs = font["glyf"].glyphs for g in glyphs.values(): g.expand(font["glyf"]) assert not hasattr(glyphs["no_component"], "components") assert hasattr(glyphs["component_no_transform"], "components") assert hasattr(glyphs["component_same_transform"], "components") assert not hasattr(glyphs["component_different_transform"], "components") @pytest.fixture def color_ufo(FontClass): ufo = FontClass() ufo.lib[COLOR_PALETTES_KEY] = [[(1, 0.3, 0.1, 1), (0, 0.4, 0.8, 1)]] return ufo class InitExplodeColorLayerGlyphsFilterTest: def test_no_color_palettes(self, FontClass): ufo = FontClass() filters = [] _init_explode_color_layer_glyphs_filter(ufo, filters) assert not filters def test_no_color_layer_mapping(self, color_ufo): filters = [] _init_explode_color_layer_glyphs_filter(color_ufo, filters) assert not filters def test_explicit_color_layers(self, color_ufo): color_ufo.lib[COLOR_LAYERS_KEY] = {"a": [("a.z_0", 1), ("a.z_1", 0)]} filters = [] _init_explode_color_layer_glyphs_filter(color_ufo, filters) assert not filters def test_font_color_layer_mapping(self, color_ufo): color_ufo.lib[COLOR_LAYER_MAPPING_KEY] = [("z_0", 1), ("z_1", 0)] filters = [] _init_explode_color_layer_glyphs_filter(color_ufo, filters) assert isinstance(filters[0], ExplodeColorLayerGlyphsFilter) def test_glyph_color_layer_mapping(self, color_ufo): color_ufo.newGlyph("a").lib[COLOR_LAYER_MAPPING_KEY] = [("z_0", 0), ("z_1", 1)] filters = [] _init_explode_color_layer_glyphs_filter(color_ufo, filters) assert isinstance(filters[0], ExplodeColorLayerGlyphsFilter) ufo2ft-3.3.1/tests/testSupport.py000066400000000000000000000013671470175262700170350ustar00rootroot00000000000000import sys import types class _TempModule: """Temporarily replace a module in sys.modules with an empty namespace""" def __init__(self, mod_name): mod_name = str(mod_name) self.mod_name = mod_name self.module = types.ModuleType(mod_name) self._saved_module = [] def __enter__(self): mod_name = self.mod_name try: self._saved_module.append(sys.modules[mod_name]) except KeyError: pass sys.modules[mod_name] = self.module return self def __exit__(self, *args): if self._saved_module: sys.modules[self.mod_name] = self._saved_module[0] else: del sys.modules[self.mod_name] self._saved_module = [] ufo2ft-3.3.1/tests/util_test.py000066400000000000000000000100731470175262700164670ustar00rootroot00000000000000"""Tests for utility functions that ufo2ft provides.""" import re import pytest from ufo2ft import util from ufo2ft.errors import InvalidFontData from ufo2ft.util import zip_strict def test_overloaded_mapping_raises_error(FontClass): """Test that util.makeUnicodeToGlyphNameMapping() raises an error when multiple glyphs are mapped to the same codepoint.""" # Make an empty font in memory with glyphs 'A' and 'B'. test_ufo = FontClass() glyph_a = test_ufo.newGlyph("A") glyph_b = test_ufo.newGlyph("B") # Test that the util function DOES NOT raise an error when the glyphs are # mapped to distinct codepoints, and that the function returns the correct # mapping. glyph_a.unicodes = [0x0041] glyph_b.unicodes = [0x0042] assert util.makeUnicodeToGlyphNameMapping(test_ufo) == {0x0041: "A", 0x0042: "B"} # Test that the util function DOES raise an error when multiple glyphs are # mapped to the same codepoint, and that this error is generally # descriptive. glyph_a.unicodes = [0x0041] glyph_b.unicodes = [0x0041] with pytest.raises( InvalidFontData, match=re.escape("cannot map 'B' to U+0041; already mapped to 'A'"), ): util.makeUnicodeToGlyphNameMapping(test_ufo) def test_getMaxComponentDepth_cyclical_reference(): # ufoLib2 lets you create cyclical component references (defcon would fail with # RecursionError while creating them so we don't test it below). # Here we test that we properly detect them and provide a descriptive error message. # https://github.com/googlefonts/fontmake/issues/1066 test_ufo = pytest.importorskip("ufoLib2").Font() glyph_a = test_ufo.newGlyph("A") glyph_b = test_ufo.newGlyph("B") glyph_c = test_ufo.newGlyph("C") glyph_a.getPen().addComponent("C", (1, 0, 0, 1, 0, 0)) glyph_b.getPen().addComponent("A", (1, 0, 0, 1, 0, 0)) glyph_c.getPen().addComponent("B", (1, 0, 0, 1, 0, 0)) with pytest.raises( InvalidFontData, match="cyclical component reference: A -> C -> B => A" ): util.getMaxComponentDepth(glyph_a, test_ufo) with pytest.raises( InvalidFontData, match="cyclical component reference: B -> A -> C => B" ): util.getMaxComponentDepth(glyph_b, test_ufo) with pytest.raises( InvalidFontData, match="cyclical component reference: C -> B -> A => C" ): util.getMaxComponentDepth(glyph_c, test_ufo) glyph_d = test_ufo.newGlyph("D") glyph_e = test_ufo.newGlyph("E") glyph_f = test_ufo.newGlyph("F") glyph_g = test_ufo.newGlyph("G") glyph_h = test_ufo.newGlyph("H") # adding same component multiple times should not cause infinite recursion glyph_d.getPen().addComponent("E", (1, 0, 0, 1, 0, 0)) glyph_d.getPen().addComponent("E", (1, 0, 0, 1, 0, 0)) # G is reachable from both E and F, but there is no cycle. glyph_e.getPen().addComponent("F", (1, 0, 0, 1, 0, 0)) glyph_f.getPen().addComponent("G", (1, 0, 0, 1, 0, 0)) glyph_e.getPen().addComponent("G", (1, 0, 0, 1, 0, 0)) glyph_g.getPen().addComponent("H", (1, 0, 0, 1, 0, 0)) assert util.getMaxComponentDepth(glyph_d, test_ufo) == 4 assert util.getMaxComponentDepth(glyph_e, test_ufo) == 3 assert util.getMaxComponentDepth(glyph_f, test_ufo) == 2 assert util.getMaxComponentDepth(glyph_g, test_ufo) == 1 assert util.getMaxComponentDepth(glyph_h, test_ufo) == 0 def test_zip_strict(): assert list(zip_strict([0, 1], [2, 3])) == [(0, 2), (1, 3)] with pytest.raises( ValueError, match=r"zip\(\) argument 2 is shorter than argument 1" ): list(zip_strict([0, 1, 2], [3, 4])) with pytest.raises( ValueError, match=r"zip\(\) argument 3 is shorter than arguments 1-2" ): list(zip_strict([0, 1, 2], [3, 4], [5])) with pytest.raises( ValueError, match=r"zip\(\) argument 2 is longer than argument 1" ): list(zip_strict([0], [1, 2])) with pytest.raises( ValueError, match=r"zip\(\) argument 3 is longer than arguments 1-2" ): list(zip_strict([0, 1], [2, 3], [1, 2, 3])) ufo2ft-3.3.1/tox.ini000066400000000000000000000024301470175262700142500ustar00rootroot00000000000000[tox] envlist = lint, py3{8,9,10,11}-cov, htmlcov skip_missing_interpreters = true [testenv] deps = -r requirements.txt -r dev-requirements.txt ; download the latest pip, setuptools and wheel when creating the venv download = true commands = # run the test suite against the package installed inside tox env. # We use parallel mode and then combine later so that coverage.py will take # paths like .tox/py37/lib/python3.7/site-packages/fontTools and collapse # them into Lib/fontTools. cov: coverage run --parallel-mode -m pytest {posargs} !cov: pytest {posargs} [testenv:lint] skip_install = true deps = -r dev-requirements.txt commands = black --check --diff . isort --gitignore --check-only --diff . flake8 [testenv:htmlcov] deps = coverage skip_install = true commands = coverage combine coverage xml coverage report coverage html [testenv:codecov] passenv = * deps = coverage codecov skip_install = true ignore_outcome = true commands = coverage combine codecov --env TOXENV [flake8] select = C, E, F, W, B, B9 ignore = E203, E266, E501, W503, B905, B907 max-line-length = 88 exclude = .git, __pycache__, build, dist, .eggs, .tox, venv, venv*, .venv, .venv* [isort] profile = black known_first_party = ufo2ft