pax_global_header00006660000000000000000000000064136255150250014516gustar00rootroot0000000000000052 comment=beb6a6c98f2ca39f8022ee26ae663e714a8ce314 ufo2ft-2.12.2/000077500000000000000000000000001362551502500130075ustar00rootroot00000000000000ufo2ft-2.12.2/.codecov.yml000066400000000000000000000001211362551502500152240ustar00rootroot00000000000000comment: false coverage: status: project: false patch: false ufo2ft-2.12.2/.coveragerc000066400000000000000000000016011362551502500151260ustar00rootroot00000000000000[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-2.12.2/.gitattributes000066400000000000000000000004411362551502500157010ustar00rootroot00000000000000# 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-2.12.2/.gitignore000066400000000000000000000006351362551502500150030ustar00rootroot00000000000000# 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 *~ # autogenerated by setuptools-scm Lib/ufo2ft/_version.py ufo2ft-2.12.2/.pyup.yml000066400000000000000000000002501362551502500146020ustar00rootroot00000000000000# controls the frequency of updates (undocumented beta feature) schedule: every week # do not pin dependencies unless they have explicit version specifiers pin: False ufo2ft-2.12.2/.travis.yml000066400000000000000000000024621362551502500151240ustar00rootroot00000000000000language: python env: global: - TWINE_USERNAME="anthrotype" - secure: XiJ7iEFtyPHc5nA3c64ou/ddDI+6+aeXpeExISQVxBwwXzAqc9sF7VFsWeInGWA2lQpuSau6tqn76HESX7OFjFNmhaF6gZ8psU/TszUMGa+IJAr8rE6cJRP121c6CB9/nrgil89IVIaIAiBhbm/4FzaTANpN6BMo/YtlCBYdvq4EWccHjKf5WeTtzOJTJJCz2C/r4VVTSj+iGWyuHS/lYYXkWBfJBlXead0rD5ZwbYW5KDevBJGr4TDu3eqa2P1BQ3xmlzhheHk8TXi1XheN6Kp2158oc+GfPATcQsjaqSvuLDw5E3A6uhIeBxXfJ5Jm4YejeqUv/LqO27Iz7NGkfMZxjsuObtlLhXv09piUjxjFVXqgmqKx16G+/bS43DjZIOglMfcemuIF3jogPYTw27yFVeTQaUQuSN89rJsdUsNLuOa6VesFzl8w6+/7KE+bx7G9OHIhN/wudPTza703pfwpqvOJ9zfm7HA//zfwNY+uyfKcai98eKPSn/AnakZ9Sgfa9UK6y0+xMOyP9DeW2HfFCuuBHahT/wS6nSCPLJs+C2WSnk+LsevEXwsTOmhi7XRwG8ZJOlHoMV3EjWfwh4GFGAIarvdAuzz/bEBmLlD/RFWYijECnw168cb0/jqmeOuidnaDFm/aK6GzThNdXR6zGYRxn4917j05a1jJ2s4= matrix: include: - python: 3.6 env: TOXENV=py36-cov - python: 3.7 dist: xenial env: TOXENV=py37-cov branches: only: - master - /^v\d+\.\d+.*$/ install: pip install tox script: tox after_success: - tox -e codecov - | if [ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_REPO_SLUG" == "googlefonts/ufo2ft" ] && [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then pip install --upgrade twine pip setuptools wheel python setup.py sdist pip wheel --no-deps --wheel-dir dist . twine upload dist/*.whl dist/*.zip fi ufo2ft-2.12.2/LICENSE000066400000000000000000000020631362551502500140150ustar00rootroot00000000000000The 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-2.12.2/Lib/000077500000000000000000000000001362551502500135155ustar00rootroot00000000000000ufo2ft-2.12.2/Lib/ufo2ft/000077500000000000000000000000001362551502500147225ustar00rootroot00000000000000ufo2ft-2.12.2/Lib/ufo2ft/__init__.py000066400000000000000000000547461362551502500170530ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import from enum import IntEnum from fontTools.misc.py23 import * from fontTools import varLib from ufo2ft.preProcessor import ( OTFPreProcessor, TTFPreProcessor, TTFInterpolatablePreProcessor, ) from ufo2ft.featureCompiler import ( FeatureCompiler, MtiFeatureCompiler, MTI_FEATURES_PREFIX, ) from ufo2ft.outlineCompiler import OutlineOTFCompiler, OutlineTTFCompiler from ufo2ft.postProcessor import PostProcessor from ufo2ft.constants import SPARSE_TTF_MASTER_TABLES, SPARSE_OTF_MASTER_TABLES from ufo2ft.util import getDefaultMasterFont import logging try: from ._version import version as __version__ except ImportError: __version__ = "0.0.0+unknown" logger = logging.getLogger(__name__) class CFFOptimization(IntEnum): NONE = 0 SPECIALIZE = 1 SUBROUTINIZE = 2 def compileOTF( ufo, preProcessorClass=OTFPreProcessor, outlineCompilerClass=OutlineOTFCompiler, featureCompilerClass=None, featureWriters=None, glyphOrder=None, useProductionNames=None, optimizeCFF=CFFOptimization.SUBROUTINIZE, roundTolerance=None, removeOverlaps=False, overlapsBackend=None, inplace=False, layerName=None, skipExportGlyphs=None, debugFeatureFile=None, _tables=None, ): """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. *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. """ logger.info("Pre-processing glyphs") if skipExportGlyphs is None: skipExportGlyphs = ufo.lib.get("public.skipExportGlyphs", []) preProcessor = preProcessorClass( ufo, inplace=inplace, removeOverlaps=removeOverlaps, overlapsBackend=overlapsBackend, layerName=layerName, skipExportGlyphs=skipExportGlyphs, ) glyphSet = preProcessor.process() logger.info("Building OpenType tables") optimizeCFF = CFFOptimization(optimizeCFF) outlineCompiler = outlineCompilerClass( ufo, glyphSet=glyphSet, glyphOrder=glyphOrder, roundTolerance=roundTolerance, optimizeCFF=optimizeCFF >= CFFOptimization.SPECIALIZE, tables=_tables, ) otf = outlineCompiler.compile() # Only the default layer is likely to have all glyphs used in feature code. if layerName is None: compileFeatures( ufo, otf, glyphSet=glyphSet, featureWriters=featureWriters, featureCompilerClass=featureCompilerClass, debugFeatureFile=debugFeatureFile, ) postProcessor = PostProcessor(otf, ufo, glyphSet=glyphSet) otf = postProcessor.process( useProductionNames, optimizeCFF=optimizeCFF >= CFFOptimization.SUBROUTINIZE ) return otf def compileTTF( ufo, preProcessorClass=TTFPreProcessor, outlineCompilerClass=OutlineTTFCompiler, featureCompilerClass=None, featureWriters=None, glyphOrder=None, useProductionNames=None, convertCubics=True, cubicConversionError=None, reverseDirection=True, rememberCurveType=True, removeOverlaps=False, overlapsBackend=None, inplace=False, layerName=None, skipExportGlyphs=None, debugFeatureFile=None, ): """Create FontTools TrueType font from a UFO. *removeOverlaps* performs a union operation on all the glyphs' contours. *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. """ logger.info("Pre-processing glyphs") if skipExportGlyphs is None: skipExportGlyphs = ufo.lib.get("public.skipExportGlyphs", []) preProcessor = preProcessorClass( ufo, inplace=inplace, removeOverlaps=removeOverlaps, overlapsBackend=overlapsBackend, convertCubics=convertCubics, conversionError=cubicConversionError, reverseDirection=reverseDirection, rememberCurveType=rememberCurveType, layerName=layerName, skipExportGlyphs=skipExportGlyphs, ) glyphSet = preProcessor.process() logger.info("Building OpenType tables") outlineCompiler = outlineCompilerClass( ufo, glyphSet=glyphSet, glyphOrder=glyphOrder ) otf = outlineCompiler.compile() # Only the default layer is likely to have all glyphs used in feature code. if layerName is None: compileFeatures( ufo, otf, glyphSet=glyphSet, featureWriters=featureWriters, featureCompilerClass=featureCompilerClass, debugFeatureFile=debugFeatureFile, ) postProcessor = PostProcessor(otf, ufo, glyphSet=glyphSet) otf = postProcessor.process(useProductionNames) return otf def compileInterpolatableTTFs( ufos, preProcessorClass=TTFInterpolatablePreProcessor, outlineCompilerClass=OutlineTTFCompiler, featureCompilerClass=None, featureWriters=None, glyphOrder=None, useProductionNames=None, cubicConversionError=None, reverseDirection=True, inplace=False, layerNames=None, skipExportGlyphs=None, debugFeatureFile=None, ): """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. """ from ufo2ft.util import _LazyFontName if layerNames is None: layerNames = [None] * len(ufos) assert len(ufos) == len(layerNames) if skipExportGlyphs is None: skipExportGlyphs = set() for ufo in ufos: skipExportGlyphs.update(ufo.lib.get("public.skipExportGlyphs", [])) logger.info("Pre-processing glyphs") preProcessor = preProcessorClass( ufos, inplace=inplace, conversionError=cubicConversionError, reverseDirection=reverseDirection, layerNames=layerNames, skipExportGlyphs=skipExportGlyphs, ) glyphSets = preProcessor.process() for ufo, glyphSet, layerName in zip(ufos, glyphSets, layerNames): fontName = _LazyFontName(ufo) if layerName is not None: logger.info("Building OpenType tables for %s-%s", fontName, layerName) else: logger.info("Building OpenType tables for %s", fontName) outlineCompiler = outlineCompilerClass( ufo, glyphSet=glyphSet, glyphOrder=glyphOrder, tables=SPARSE_TTF_MASTER_TABLES if layerName else None, ) ttf = outlineCompiler.compile() # Only the default layer is likely to have all glyphs used in feature # code. if layerName is None: if debugFeatureFile: debugFeatureFile.write("\n### %s ###\n" % fontName) compileFeatures( ufo, ttf, glyphSet=glyphSet, featureWriters=featureWriters, featureCompilerClass=featureCompilerClass, debugFeatureFile=debugFeatureFile, ) postProcessor = PostProcessor(ttf, ufo, glyphSet=glyphSet) ttf = postProcessor.process(useProductionNames) if layerName is not None: # 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 yield ttf def compileInterpolatableTTFsFromDS( designSpaceDoc, preProcessorClass=TTFInterpolatablePreProcessor, outlineCompilerClass=OutlineTTFCompiler, featureCompilerClass=None, featureWriters=None, glyphOrder=None, useProductionNames=None, cubicConversionError=None, reverseDirection=True, inplace=False, debugFeatureFile=None, ): """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. """ ufos, 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 layerNames.append(source.layerName) skipExportGlyphs = designSpaceDoc.lib.get("public.skipExportGlyphs", []) ttfs = compileInterpolatableTTFs( ufos, preProcessorClass=preProcessorClass, outlineCompilerClass=outlineCompilerClass, featureCompilerClass=featureCompilerClass, featureWriters=featureWriters, glyphOrder=glyphOrder, useProductionNames=useProductionNames, cubicConversionError=cubicConversionError, reverseDirection=reverseDirection, inplace=inplace, layerNames=layerNames, skipExportGlyphs=skipExportGlyphs, debugFeatureFile=debugFeatureFile, ) if inplace: result = designSpaceDoc else: # TODO try a more efficient copy method that doesn't involve (de)serializing result = designSpaceDoc.__class__.fromstring(designSpaceDoc.tostring()) for source, ttf in zip(result.sources, ttfs): source.font = ttf return result def compileInterpolatableOTFsFromDS( designSpaceDoc, preProcessorClass=OTFPreProcessor, outlineCompilerClass=OutlineOTFCompiler, featureCompilerClass=None, featureWriters=None, glyphOrder=None, useProductionNames=None, roundTolerance=None, inplace=False, debugFeatureFile=None, ): """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. """ for source in designSpaceDoc.sources: if source.font is None: raise AttributeError( "designspace source '%s' is missing required 'font' attribute" % getattr(source, "name", "") ) skipExportGlyphs = designSpaceDoc.lib.get("public.skipExportGlyphs", []) otfs = [] for source in designSpaceDoc.sources: otfs.append( compileOTF( ufo=source.font, layerName=source.layerName, preProcessorClass=preProcessorClass, outlineCompilerClass=outlineCompilerClass, featureCompilerClass=featureCompilerClass, featureWriters=featureWriters, glyphOrder=glyphOrder, useProductionNames=useProductionNames, optimizeCFF=CFFOptimization.NONE, roundTolerance=roundTolerance, removeOverlaps=False, overlapsBackend=None, inplace=inplace, skipExportGlyphs=skipExportGlyphs, debugFeatureFile=debugFeatureFile, _tables=SPARSE_OTF_MASTER_TABLES if source.layerName else None, ) ) if inplace: result = designSpaceDoc else: # TODO try a more efficient copy method that doesn't involve (de)serializing result = designSpaceDoc.__class__.fromstring(designSpaceDoc.tostring()) for source, otf in zip(result.sources, otfs): source.font = otf return result def compileFeatures( ufo, ttFont=None, glyphSet=None, featureWriters=None, featureCompilerClass=None, debugFeatureFile=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 featureCompilerClass is None: if any( fn.startswith(MTI_FEATURES_PREFIX) and fn.endswith(".mti") for fn in ufo.data.fileNames ): featureCompilerClass = MtiFeatureCompiler else: featureCompilerClass = FeatureCompiler featureCompiler = featureCompilerClass( ufo, ttFont, glyphSet=glyphSet, featureWriters=featureWriters ) otFont = featureCompiler.compile() if debugFeatureFile: if hasattr(featureCompiler, "writeFeatures"): featureCompiler.writeFeatures(debugFeatureFile) return otFont def compileVariableTTF( designSpaceDoc, preProcessorClass=TTFInterpolatablePreProcessor, outlineCompilerClass=OutlineTTFCompiler, featureCompilerClass=None, featureWriters=None, glyphOrder=None, useProductionNames=None, cubicConversionError=None, reverseDirection=True, excludeVariationTables=(), optimizeGvar=True, inplace=False, debugFeatureFile=None, ): """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. """ baseUfo = getDefaultMasterFont(designSpaceDoc) ttfDesignSpace = compileInterpolatableTTFsFromDS( designSpaceDoc, preProcessorClass=preProcessorClass, outlineCompilerClass=outlineCompilerClass, featureCompilerClass=featureCompilerClass, featureWriters=featureWriters, glyphOrder=glyphOrder, useProductionNames=False, # will rename glyphs after varfont is built cubicConversionError=cubicConversionError, reverseDirection=reverseDirection, inplace=inplace, debugFeatureFile=debugFeatureFile, ) logger.info("Building variable TTF font") varfont = varLib.build( ttfDesignSpace, exclude=excludeVariationTables, optimize=optimizeGvar )[0] postProcessor = PostProcessor(varfont, baseUfo) varfont = postProcessor.process(useProductionNames) return varfont def compileVariableCFF2( designSpaceDoc, preProcessorClass=OTFPreProcessor, outlineCompilerClass=OutlineOTFCompiler, featureCompilerClass=None, featureWriters=None, glyphOrder=None, useProductionNames=None, roundTolerance=None, excludeVariationTables=(), inplace=False, debugFeatureFile=None, ): """Create FontTools CFF2 variable font from the DesignSpaceDocument UFO sources with interpolatable outlines, using fontTools.varLib.build. *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. """ baseUfo = getDefaultMasterFont(designSpaceDoc) otfDesignSpace = compileInterpolatableOTFsFromDS( designSpaceDoc, preProcessorClass=preProcessorClass, outlineCompilerClass=outlineCompilerClass, featureCompilerClass=featureCompilerClass, featureWriters=featureWriters, glyphOrder=glyphOrder, useProductionNames=False, # will rename glyphs after varfont is built roundTolerance=roundTolerance, inplace=inplace, debugFeatureFile=debugFeatureFile, ) logger.info("Building variable CFF2 font") varfont = varLib.build(otfDesignSpace, exclude=excludeVariationTables)[0] postProcessor = PostProcessor(varfont, baseUfo) varfont = postProcessor.process(useProductionNames) return varfont ufo2ft-2.12.2/Lib/ufo2ft/constants.py000066400000000000000000000014301362551502500173060ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals SPARSE_TTF_MASTER_TABLES = frozenset( ["glyf", "head", "hmtx", "loca", "maxp", "post", "vmtx"] ) 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" COLOR_LAYERS_KEY = UFO2FT_PREFIX + "colorLayers" COLOR_PALETTES_KEY = UFO2FT_PREFIX + "colorPalettes" COLOR_LAYER_MAPPING_KEY = UFO2FT_PREFIX + "colorLayerMapping" ufo2ft-2.12.2/Lib/ufo2ft/errors.py000066400000000000000000000004631362551502500166130ustar00rootroot00000000000000class Error(Exception): """Base exception class for all ufo2ft errors.""" pass class InvalidFontData(Error): """Raised when input font contains invalid data.""" pass class InvalidDesignSpaceData(Error): """Raised when input DesignSpace document contains invalid data.""" pass ufo2ft-2.12.2/Lib/ufo2ft/featureCompiler.py000066400000000000000000000237311362551502500204300ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import logging import os from inspect import isclass from tempfile import NamedTemporaryFile from collections import OrderedDict from fontTools.misc.py23 import tobytes, tounicode, UnicodeIO from fontTools.feaLib.parser import Parser from fontTools.feaLib.builder import addOpenTypeFeaturesFromString from fontTools.feaLib.error import IncludedFeaNotFound, FeatureLibError from fontTools import mtiLib from ufo2ft.constants import MTI_FEATURES_PREFIX from ufo2ft.featureWriters import ( KernFeatureWriter, MarkFeatureWriter, loadFeatureWriters, ast, ) logger = logging.getLogger(__name__) def parseLayoutFeatures(font): """ Parse OpenType layout features in the UFO and return a feaLib.ast.FeatureFile instance. """ featxt = tounicode(font.features.text or "", "utf-8") if not featxt: return ast.FeatureFile() buf = UnicodeIO(featxt) # the path is used by the lexer to resolve 'include' statements # and print filename in error messages. For the UFO spec, this # should be the path of the UFO, not the inner features.fea: # https://github.com/unified-font-object/ufo-spec/issues/55 ufoPath = font.path if ufoPath is not None: buf.name = ufoPath glyphNames = set(font.keys()) try: parser = Parser(buf, glyphNames) 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(object): """Base class for generating OpenType features and compiling OpenType layout tables from these. """ def __init__(self, ufo, ttFont=None, glyphSet=None, **kwargs): """ 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. """ 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: assert set(glyphOrder) == set(glyphSet.keys()) else: glyphSet = ufo self.glyphSet = OrderedDict((gn, glyphSet[gn]) for gn in glyphOrder) 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( "%r method is deprecated; use %r instead" % (arg, repl), 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 = [KernFeatureWriter, MarkFeatureWriter] def __init__(self, ufo, ttFont=None, glyphSet=None, featureWriters=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: [KernFeatureWriter, MarkFeatureWriter]. This generates "kern" (or "dist" for Indic scripts), "mark" and "mkmk" features. If the featureWriters list is empty, no automatic feature is generated and only pre-existing features are compiled. """ BaseFeatureCompiler.__init__(self, ufo, ttFont, glyphSet) 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 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: currently, KernFeatureWriter and MarkFeatureWriter. 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. """ if featureWriters is None: featureWriters = loadFeatureWriters(self.ufo) if featureWriters is None: featureWriters = self.defaultFeatureWriters 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. """ if self.featureWriters: featureFile = parseLayoutFeatures(self.ufo) for writer in self.featureWriters: writer.write(self.ufo, 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 = tounicode(self.ufo.features.text or "", "utf-8") 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 try: addOpenTypeFeaturesFromString(self.ttFont, self.features, filename=path) except FeatureLibError: if path is None: # if compilation fails, create temporary file for inspection data = tobytes(self.features, encoding="utf-8") with NamedTemporaryFile(delete=False) as tmp: tmp.write(data) logger.error("Compilation failed! Inspect temporary file: %r", tmp.name) raise 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 = tounicode(ufo.data[fn], encoding="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 ufo2ft-2.12.2/Lib/ufo2ft/featureWriters/000077500000000000000000000000001362551502500177355ustar00rootroot00000000000000ufo2ft-2.12.2/Lib/ufo2ft/featureWriters/__init__.py000066400000000000000000000133521362551502500220520ustar00rootroot00000000000000from __future__ import print_function, absolute_import, division, unicode_literals from .baseFeatureWriter import BaseFeatureWriter from .kernFeatureWriter import KernFeatureWriter from .markFeatureWriter import MarkFeatureWriter import importlib import re from inspect import isclass try: from inspect import getfullargspec as getargspec # PY3 except ImportError: from inspect import getargspec # PY2 import logging from ufo2ft.constants import FEATURE_WRITERS_KEY __all__ = [ "BaseFeatureWriter", "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 getargspec(klass.write).args != getargspec(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 # 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}} ) _featureWriterSpecRE = re.compile( r"(?:([\w\.]+)::)?" # MODULE_NAME + '::' r"(\w+)" # CLASS_NAME [required] r"(?:\((.*)\))?" # (KWARGS) ) 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") """ spec = spec.strip() m = _featureWriterSpecRE.match(spec) if not m or (m.end() - m.start()) != len(spec): raise ValueError(spec) moduleName = m.group(1) or "ufo2ft.featureWriters" className = m.group(2) kwargs = m.group(3) module = importlib.import_module(moduleName) klass = getattr(module, className) if not isValidFeatureWriter(klass): raise TypeError(klass) try: options = _kwargsEval(kwargs) if kwargs else {} except SyntaxError: raise ValueError("options have incorrect format: %r" % kwargs) return klass(**options) ufo2ft-2.12.2/Lib/ufo2ft/featureWriters/ast.py000066400000000000000000000161161362551502500211030ustar00rootroot00000000000000"""Helpers to build or extract data from feaLib AST objects.""" from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.feaLib import ast from fontTools import unicodedata import collections 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 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): """Return dictionary keyed by Unicode script code containing lists of (OT_SCRIPT_TAG, [OT_LANGUAGE_TAG, ...]) tuples (excluding "DFLT"). """ languagesByScript = collections.OrderedDict() for ls in [ st for st in feaFile.statements if isinstance(st, ast.LanguageSystemStatement) ]: if ls.script == "DFLT": 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 iterClassDefinitions(feaFile, featureTag=None): if featureTag is None: # start from top-level class definitions for s in feaFile.statements: if isinstance(s, ast.GlyphClassDefinition): yield s # then iterate over per-feature class definitions for fea in iterFeatureBlocks(feaFile, tag=featureTag): for s in fea.statements: if isinstance(s, ast.GlyphClassDefinition): yield s LOOKUP_FLAGS = { "RightToLeft": 1, "IgnoreBaseGlyphs": 2, "IgnoreLigatures": 4, "IgnoreMarks": 8, } def makeLookupFlag(name=None, markAttachment=None, markFilteringSet=None): value = 0 if name is None else LOOKUP_FLAGS[name] 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 st in feaLib.statements: if isinstance(st, ast.TableBlock) and st.name == "GDEF": for st in st.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-2.12.2/Lib/ufo2ft/featureWriters/baseFeatureWriter.py000066400000000000000000000151431362551502500237360ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import SimpleNamespace from collections import OrderedDict import logging from ufo2ft.featureWriters import ast class BaseFeatureWriter(object): """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. """ tableTag = None features = frozenset() mode = "skip" 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. Returns the context namespace instance. """ todo = set(self.features) if self.mode == "skip": existing = ast.findFeatureTags(feaFile) todo.difference_update(existing) self.context = SimpleNamespace( font=font, feaFile=feaFile, compiler=compiler, todo=todo ) 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. 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 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 makeOfficialGlyphOrder glyphSet = self.context.font glyphOrder = makeOfficialGlyphOrder(self.context.font) 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 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() else: # the 'real' glyph order doesn't matter because the table is not # compiled to binary, only the glyph names are used glyphOrder = sorted(self.context.font.keys()) gsub = compileGSUB(self.context.feaFile, glyphOrder) if compiler and not hasattr(compiler, "_gsub"): compiler._gsub = gsub return gsub ufo2ft-2.12.2/Lib/ufo2ft/featureWriters/kernFeatureWriter.py000066400000000000000000000524711362551502500237700ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import unichr, basestring, SimpleNamespace from fontTools.misc.fixedTools import otRound from fontTools import unicodedata from ufo2ft.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import classifyGlyphs 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-dist.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 = { # Indic shaper's scripts # Unicode-1.1 additions "Beng", # Bengali "Deva", # Devanagari "Gujr", # Gujarati "Guru", # Gurmukhi "Knda", # Kannada "Mlym", # Malayalam "Orya", # Oriya "Taml", # Tamil "Telu", # Telugu # Unicode-3.0 additions "Sinh", # Sinhala # Khmer shaper "Khmr", # Khmer # Myanmar shaper "Mymr", # Myanmar # USE shaper's scripts # Unicode-3.2 additions "Buhd", # Buhid "Hano", # Hanunoo "Tglg", # Tagalog "Tagb", # Tagbanwa # Unicode-4.0 additions "Limb", # Limbu "Tale", # Tai Le # Unicode-4.1 additions "Bugi", # Buginese "Khar", # Kharoshthi "Sylo", # Syloti Nagri "Tfng", # Tifinagh # Unicode-5.0 additions "Bali", # Balinese # Unicode-5.1 additions "Cham", # Cham "Kali", # Kayah Li "Lepc", # Lepcha "Rjng", # Rejang "Saur", # Saurashtra "Sund", # Sundanese # Unicode-5.2 additions "Egyp", # Egyptian Hieroglyphs "Java", # Javanese "Kthi", # Kaithi "Mtei", # Meetei Mayek "Lana", # Tai Tham "Tavt", # Tai Viet # Unicode-6.0 additions "Batk", # Batak "Brah", # Brahmi # Unicode-6.1 additions "Cakm", # Chakma "Shrd", # Sharada "Takr", # Takri # Unicode-7.0 additions "Dupl", # Duployan "Gran", # Grantha "Khoj", # Khojki "Sind", # Khudawadi "Mahj", # Mahajani "Modi", # Modi "Hmng", # Pahawh Hmong "Sidd", # Siddham "Tirh", # Tirhuta # Unicode-8.0 additions "Ahom", # Ahom "Mult", # Multani # Unicode-9.0 additions "Bhks", # Bhaiksuki "Marc", # Marchen "Newa", # Newa # Unicode-10.0 additions "Gonm", # Masaram Gondi "Soyo", # Soyombo "Zanb", # Zanabazar Square # Unicode-11.0 additions "Dogr", # Dogra "Gong", # Gunjala Gondi "Maka", # Makasar # Unicode-12.0 additions "Nand", # Nandinagari } # we consider the 'Common' and 'Inherited' scripts as neutral for # determining a kerning pair's horizontal direction DFLT_SCRIPTS = {"Zyyy", "Zinh"} def unicodeScriptDirection(uv): sc = unicodedata.script(unichr(uv)) if sc in DFLT_SCRIPTS: return None return unicodedata.script_horizontal_direction(sc) RTL_BIDI_TYPES = {"R", "AL"} LTR_BIDI_TYPES = {"L", "AN", "EN"} def unicodeBidiType(uv): """Return "R" for characters with RTL direction, or "L" for LTR (whether 'strong' or 'weak'), or None for neutral direction. """ char = unichr(uv) bidiType = unicodedata.bidirectional(char) if bidiType in RTL_BIDI_TYPES: return "R" elif bidiType in LTR_BIDI_TYPES: return "L" else: return None class KerningPair(object): __slots__ = ("side1", "side2", "value", "directions", "bidiTypes") def __init__(self, side1, side2, value, directions=None, bidiTypes=None): if isinstance(side1, basestring): self.side1 = ast.GlyphName(side1) elif isinstance(side1, ast.GlyphClassDefinition): self.side1 = ast.GlyphClassName(side1) else: raise AssertionError(side1) if isinstance(side2, basestring): self.side2 = ast.GlyphName(side2) elif isinstance(side2, ast.GlyphClassDefinition): self.side2 = ast.GlyphClassName(side2) else: raise AssertionError(side2) self.value = value self.directions = directions or set() self.bidiTypes = bidiTypes or set() @property def firstIsClass(self): return isinstance(self.side1, ast.GlyphClassName) @property def secondIsClass(self): return isinstance(self.side2, ast.GlyphClassName) @property def glyphs(self): if self.firstIsClass: classDef1 = self.side1.glyphclass glyphs1 = set(g.asFea() for g in classDef1.glyphSet()) else: glyphs1 = {self.side1.asFea()} if self.secondIsClass: classDef2 = self.side2.glyphclass glyphs2 = set(g.asFea() for g in classDef2.glyphSet()) else: glyphs2 = {self.side2.asFea()} return glyphs1 | glyphs2 def __repr__(self): return "<%s %s %s %s%s%s>" % ( self.__class__.__name__, self.side1, self.side2, self.value, " %r" % self.directions if self.directions else "", " %r" % self.bidiTypes if self.bidiTypes else "", ) class KernFeatureWriter(BaseFeatureWriter): """Generates a kerning feature based on groups and rules contained in an UFO's kerning data. There are currently two possible writing modes: 2) "skip" (default) will not write anything if the features are already present; 1) "append" will add additional lookups to an existing feature, if present, or it will add a new one at the end of all features. """ tableTag = "GPOS" features = frozenset(["kern", "dist"]) options = dict(ignoreMarks=True) def setContext(self, font, feaFile, compiler=None): ctx = super(KernFeatureWriter, self).setContext( font, feaFile, compiler=compiler ) ctx.gdefClasses = ast.getGDEFGlyphClasses(feaFile) ctx.kerning = self.getKerningData(font, feaFile, self.getOrderedGlyphSet()) feaScripts = ast.getScriptLanguageSystems(feaFile) ctx.scriptGroups = self._groupScriptsByTagAndDirection(feaScripts) return ctx def shouldContinue(self): if not self.context.kerning.pairs: self.log.debug("No kerning data; skipped") return False if "dist" in self.context.todo and "dist" not in self.context.scriptGroups: self.log.debug( "No dist-enabled scripts defined in languagesystem " "statements; dist feature will not be generated" ) self.context.todo.remove("dist") return super(KernFeatureWriter, self).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 statements = self.context.feaFile.statements # first add the glyph class definitions side1Classes = self.context.kerning.side1Classes side2Classes = self.context.kerning.side2Classes for classes in (side1Classes, side2Classes): statements.extend([c for _, c in sorted(classes.items())]) # add empty line to separate classes from following statements if statements: statements.append(ast.Comment("")) # finally add the lookup and feature blocks for _, lookupGroup in sorted(lookups.items()): statements.extend(lookupGroup) if "kern" in features: statements.append(features["kern"]) if "dist" in features: statements.append(features["dist"]) return True @classmethod def getKerningData(cls, font, feaFile=None, glyphSet=None): side1Classes, side2Classes = cls.getKerningClasses(font, feaFile, glyphSet) pairs = cls.getKerningPairs(font, side1Classes, side2Classes, glyphSet) return SimpleNamespace( side1Classes=side1Classes, side2Classes=side2Classes, pairs=pairs ) @staticmethod def getKerningGroups(font, glyphSet=None): if glyphSet: allGlyphs = set(glyphSet.keys()) else: allGlyphs = set(font.keys()) side1Groups = {} side2Groups = {} for name, members in font.groups.items(): # prune non-existent or skipped glyphs members = [g for g in members if g in allGlyphs] if not members: # skip empty groups continue # skip groups without UFO3 public.kern{1,2} prefix if name.startswith(SIDE1_PREFIX): side1Groups[name] = members elif name.startswith(SIDE2_PREFIX): side2Groups[name] = members return side1Groups, side2Groups @classmethod def getKerningClasses(cls, font, feaFile=None, glyphSet=None): side1Groups, side2Groups = cls.getKerningGroups(font, glyphSet) side1Classes = ast.makeGlyphClassDefinitions( side1Groups, feaFile, stripPrefix="public." ) side2Classes = ast.makeGlyphClassDefinitions( side2Groups, feaFile, stripPrefix="public." ) return side1Classes, side2Classes @staticmethod def getKerningPairs(font, side1Classes, side2Classes, glyphSet=None): if glyphSet: allGlyphs = set(glyphSet.keys()) else: allGlyphs = set(font.keys()) kerning = font.kerning pairsByFlags = {} for (side1, side2) in kerning: # filter out pairs that reference missing groups or glyphs if side1 not in side1Classes and side1 not in allGlyphs: continue if side2 not in side2Classes and side2 not in allGlyphs: continue flags = (side1 in side1Classes, side2 in side2Classes) pairsByFlags.setdefault(flags, set()).add((side1, side2)) result = [] for flags, pairs in sorted(pairsByFlags.items()): for side1, side2 in sorted(pairs): value = kerning[side1, side2] if all(flags) and value == 0: # ignore zero-valued class kern pairs continue firstIsClass, secondIsClass = flags if firstIsClass: side1 = side1Classes[side1] if secondIsClass: side2 = side2Classes[side2] result.append(KerningPair(side1, side2, value)) return result def _intersectPairs(self, attribute, glyphSets): allKeys = set() for pair in self.context.kerning.pairs: for key, glyphs in glyphSets.items(): if not pair.glyphs.isdisjoint(glyphs): getattr(pair, attribute).add(key) allKeys.add(key) return allKeys @staticmethod def _groupScriptsByTagAndDirection(feaScripts): # Read scripts/languages defined in feaFile's 'languagesystem' # statements and group them by the feature tag (kern or dist) # they are associated with, and the global script's horizontal # direction (DFLT is excluded) scriptGroups = {} for scriptCode, scriptLangSys in feaScripts.items(): direction = unicodedata.script_horizontal_direction(scriptCode) if scriptCode in DIST_ENABLED_SCRIPTS: tag = "dist" else: tag = "kern" scriptGroups.setdefault(tag, {}).setdefault(direction, []).extend( scriptLangSys ) return scriptGroups @staticmethod def _makePairPosRule(pair, rtl=False): enumerated = pair.firstIsClass ^ pair.secondIsClass value = otRound(pair.value) if rtl and "L" in pair.bidiTypes: # numbers are always shaped LTR even in RTL scripts rtl = False valuerecord = ast.ValueRecord( xPlacement=value if rtl else None, yPlacement=0 if rtl else None, xAdvance=value, yAdvance=0 if rtl else None, ) return ast.PairPosStatement( glyphs1=pair.side1, valuerecord1=valuerecord, glyphs2=pair.side2, valuerecord2=None, enumerated=enumerated, ) def _makeKerningLookup( self, name, pairs, exclude=None, rtl=False, ignoreMarks=True ): assert pairs rules = [] for pair in pairs: if exclude is not None and exclude(pair): self.log.debug("pair excluded from '%s' lookup: %r", name, pair) continue rules.append(self._makePairPosRule(pair, rtl=rtl)) if rules: lookup = ast.LookupBlock(name) if ignoreMarks and self.options.ignoreMarks: lookup.statements.append(ast.makeLookupFlag("IgnoreMarks")) lookup.statements.extend(rules) return lookup def _makeKerningLookups(self): cmap = self.makeUnicodeToGlyphNameMapping() if any(unicodeScriptDirection(uv) == "RTL" for uv in cmap): # If there are any characters from globally RTL scripts in the # cmap, we compile a temporary GSUB table to resolve substitutions # and group glyphs by script horizontal direction and bidirectional # type. We then mark each kerning pair with these properties when # any of the glyphs involved in a pair intersects these groups. gsub = self.compileGSUB() dirGlyphs = classifyGlyphs(unicodeScriptDirection, cmap, gsub) directions = self._intersectPairs("directions", dirGlyphs) shouldSplit = "RTL" in directions if shouldSplit: bidiGlyphs = classifyGlyphs(unicodeBidiType, cmap, gsub) self._intersectPairs("bidiTypes", bidiGlyphs) else: shouldSplit = False marks = self.context.gdefClasses.mark lookups = {} if shouldSplit: # make one DFLT lookup with script-agnostic characters, and two # LTR/RTL lookups excluding pairs from the opposite group. # We drop kerning pairs with ambiguous direction: i.e. those containing # glyphs from scripts with different overall horizontal direction, or # glyphs with incompatible bidirectional type (e.g. arabic letters vs # arabic numerals). pairs = [] for pair in self.context.kerning.pairs: if ("RTL" in pair.directions and "LTR" in pair.directions) or ( "R" in pair.bidiTypes and "L" in pair.bidiTypes ): self.log.warning( "skipped kern pair with ambiguous direction: %r", pair ) continue pairs.append(pair) if not pairs: return lookups if self.options.ignoreMarks: # If there are pairs with a mix of mark/base then the IgnoreMarks # flag is unnecessary and should not be set basePairs, markPairs = self._splitBaseAndMarkPairs(pairs, marks) self._makeSplitDirectionKernLookups(lookups, basePairs) if markPairs: self._makeSplitDirectionKernLookups( lookups, markPairs, ignoreMarks=False, suffix="_marks" ) else: self._makeSplitDirectionKernLookups(lookups, pairs) else: # only make a single (implicitly LTR) lookup including all base/base pairs # and a single lookup including all base/mark pairs (if any) pairs = self.context.kerning.pairs if self.options.ignoreMarks: basePairs, markPairs = self._splitBaseAndMarkPairs(pairs, marks) lookups["LTR"] = [self._makeKerningLookup("kern_ltr", basePairs)] if markPairs: lookups["LTR"].append( self._makeKerningLookup( "kern_ltr_marks", markPairs, ignoreMarks=False ) ) else: lookups["LTR"] = [self._makeKerningLookup("kern_ltr", pairs)] return lookups def _splitBaseAndMarkPairs(self, pairs, marks): basePairs, markPairs = [], [] if marks: for pair in pairs: if any(glyph in marks for glyph in pair.glyphs): markPairs.append(pair) else: basePairs.append(pair) else: basePairs[:] = pairs return basePairs, markPairs def _makeSplitDirectionKernLookups( self, lookups, pairs, ignoreMarks=True, suffix="" ): dfltKern = self._makeKerningLookup( "kern_dflt" + suffix, pairs, exclude=(lambda pair: {"LTR", "RTL"}.intersection(pair.directions)), rtl=False, ignoreMarks=ignoreMarks, ) if dfltKern: lookups.setdefault("DFLT", []).append(dfltKern) ltrKern = self._makeKerningLookup( "kern_ltr" + suffix, pairs, exclude=(lambda pair: not pair.directions or "RTL" in pair.directions), rtl=False, ignoreMarks=ignoreMarks, ) if ltrKern: lookups.setdefault("LTR", []).append(ltrKern) rtlKern = self._makeKerningLookup( "kern_rtl" + suffix, pairs, exclude=(lambda pair: not pair.directions or "LTR" in pair.directions), rtl=True, ignoreMarks=ignoreMarks, ) if rtlKern: lookups.setdefault("RTL", []).append(rtlKern) def _makeFeatureBlocks(self, lookups): features = {} if "kern" in self.context.todo: kern = ast.FeatureBlock("kern") self._registerKernLookups(kern, lookups) if kern.statements: features["kern"] = kern if "dist" in self.context.todo: dist = ast.FeatureBlock("dist") self._registerDistLookups(dist, lookups) if dist.statements: features["dist"] = dist return features def _registerKernLookups(self, feature, lookups): if "DFLT" in lookups: ast.addLookupReferences(feature, lookups["DFLT"]) scriptGroups = self.context.scriptGroups if "dist" in self.context.todo: distScripts = scriptGroups["dist"] else: distScripts = {} kernScripts = scriptGroups.get("kern", {}) ltrScripts = kernScripts.get("LTR", []) rtlScripts = kernScripts.get("RTL", []) ltrLookups = lookups.get("LTR") rtlLookups = lookups.get("RTL") if ltrLookups and rtlLookups: if ltrScripts and rtlScripts: for script, langs in ltrScripts: ast.addLookupReferences(feature, ltrLookups, script, langs) for script, langs in rtlScripts: ast.addLookupReferences(feature, rtlLookups, script, langs) elif ltrScripts: ast.addLookupReferences(feature, rtlLookups, script="DFLT") for script, langs in ltrScripts: ast.addLookupReferences(feature, ltrLookups, script, langs) elif rtlScripts: ast.addLookupReferences(feature, ltrLookups, script="DFLT") for script, langs in rtlScripts: ast.addLookupReferences(feature, rtlLookups, script, langs) else: if not (distScripts.get("LTR") and distScripts.get("RTL")): raise ValueError( "cannot use DFLT script for both LTR and RTL kern " "lookups; add 'languagesystems' to features for at " "least one LTR or RTL script using the kern feature" ) elif ltrLookups: if not (rtlScripts or distScripts): ast.addLookupReferences(feature, ltrLookups) else: ast.addLookupReferences(feature, ltrLookups, script="DFLT") for script, langs in ltrScripts: ast.addLookupReferences(feature, ltrLookups, script, langs) elif rtlLookups: if not (ltrScripts or distScripts): ast.addLookupReferences(feature, rtlLookups) else: ast.addLookupReferences(feature, rtlLookups, script="DFLT") for script, langs in rtlScripts: ast.addLookupReferences(feature, rtlLookups, script, langs) def _registerDistLookups(self, feature, lookups): scripts = self.context.scriptGroups["dist"] ltrLookups = lookups.get("LTR") if ltrLookups: for script, langs in scripts.get("LTR", []): ast.addLookupReferences(feature, ltrLookups, script, langs) rtlLookups = lookups.get("RTL") if rtlLookups: for script, langs in scripts.get("RTL", []): ast.addLookupReferences(feature, rtlLookups, script, langs) ufo2ft-2.12.2/Lib/ufo2ft/featureWriters/markFeatureWriter.py000066400000000000000000000654121362551502500237620ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import re from collections import OrderedDict from functools import partial import itertools from fontTools.misc.py23 import tostr, tounicode from fontTools.misc.fixedTools import otRound from ufo2ft.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import unicodeInScripts, classifyGlyphs from ufo2ft.fontInfoData import getAttrWithFallback class AbstractMarkPos(object): """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=otRound(anchor.x), y=otRound(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 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=otRound(anchor.x), y=otRound(anchor.y)), anchor.markClass) for anchor in sorted(component, key=lambda a: a.name) ] for component in self.marks ] 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 if ignoreRE is not None: anchorName = re.sub(ignoreRE, "", 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 return isMark, key, number class NamedAnchor(object): """A position with a name, and an associated markClass.""" __slots__ = ("name", "x", "y", "isMark", "key", "number", "markClass") # 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): self.name = tounicode(name) self.x = x self.y = y isMark, key, number = 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 @property def markAnchorName(self): return self.markPrefix + self.key def __repr__(self): items = ("%s=%r" % (tostr(k), getattr(self, k)) for k in ("name", "x", "y")) return tostr("%s(%s)") % (type(self).__name__, ", ".join(items)) 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 "Indic" script codes defined below, 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 glyphs containing anchors from predefined lists of "above" and "below" anchor names (see below). If Indic 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. """ 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" # The anchor names and list of scripts for which 'abvm' and 'blwm' # features are generated is the same as the one Glyphs.app uses, see: # https://github.com/googlei18n/ufo2ft/issues/179 abvmAnchorNames = {"top", "topleft", "topright", "candra", "bindu", "candrabindu"} blwmAnchorNames = {"bottom", "bottomleft", "bottomright", "nukta"} indicScripts = { "Beng", # Bengali "Cham", # Cham "Deva", # Devanagari "Gujr", # Gujarati "Guru", # Gurmukhi "Knda", # Kannada "Mlym", # Malayalam "Orya", # Oriya "Taml", # Tamil "Telu", # Telugu } # 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(MarkFeatureWriter, self).setContext( font, feaFile, compiler=compiler ) ctx.gdefClasses = ast.getGDEFGlyphClasses(feaFile) ctx.anchorLists = self._getAnchorLists() ctx.anchorPairs = self._getAnchorPairs() def shouldContinue(self): if not self.context.anchorPairs: self.log.debug("No mark-attaching anchors found; skipped") return False return super(MarkFeatureWriter, self).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 ) a = self.NamedAnchor(name=anchorName, x=anchor.x, y=anchor.y) 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): def sort_key(a): return self.anchorSortKey.get(a.name, 0) 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 # only use the first mark anchor, using a predefined sorting, to # determine which markClass a mark glyph belongs. This is to avoid # overlapping mark classes within the same lookup anchor = sorted(markAnchors, key=sort_key)[0] 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=otRound(x), y=otRound(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 _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 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.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 # 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 @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 = "%smark2mark_%s" % (prefix, 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): baseLkp = self._makeMarkLookup( "mark2base", self.context.markToBaseAttachments, include ) ligaLkp = self._makeMarkLookup( "mark2liga", self.context.markToLigaAttachments, include ) if baseLkp is None and ligaLkp is None: return feature = ast.FeatureBlock("mark") if baseLkp: feature.statements.append(baseLkp) if ligaLkp: feature.statements.append(ligaLkp) return feature 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 _getVerticalThreshold(self): # anchors with unknown names whose Y coordinate is greater or equal to # the line that cuts the UPEM square in half will be treated as "above # base" marks, those that fall below the threshold as "below base". return getAttrWithFallback(self.context.font.info, "unitsPerEm") // 2 def _isAboveMark(self, anchor): if anchor.name in self.abvmAnchorNames: return True if anchor.name in self.blwmAnchorNames: return False if anchor.y >= self.context.threshold: return True return False 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) baseLkp = self._makeMarkLookup( "%s_mark2base" % tag, self.context.markToBaseAttachments, include=include, marksFilter=marksFilter, ) ligaLkp = self._makeMarkLookup( "%s_mark2liga" % tag, self.context.markToLigaAttachments, include=include, marksFilter=marksFilter, ) 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([baseLkp, ligaLkp, mkmkLookups]): return feature = ast.FeatureBlock(tag) if baseLkp: feature.statements.append(baseLkp) if ligaLkp: feature.statements.append(ligaLkp) feature.statements.extend(mkmkLookups) return feature def _makeFeatures(self): ctx = self.context ctx.markToBaseAttachments = self._makeMarkToBaseAttachments() ctx.markToLigaAttachments = self._makeMarkToLigaAttachments() ctx.markToMarkAttachments = self._makeMarkToMarkAttachments() indicGlyphs = self._getIndicGlyphs() def isIndic(glyphName): return glyphName in indicGlyphs def isNotIndic(glyphName): return glyphName not in indicGlyphs features = {} todo = ctx.todo if "mark" in todo: mark = self._makeMarkFeature(include=isNotIndic) if mark is not None: features["mark"] = mark if "mkmk" in todo: mkmk = self._makeMkmkFeature(include=isNotIndic) if mkmk is not None: features["mkmk"] = mkmk if "abvm" in todo or "blwm" in todo: if indicGlyphs: self.context.threshold = self._getVerticalThreshold() for tag in ("abvm", "blwm"): if tag not in todo: continue feature = self._makeAbvmOrBlwmFeature(tag, include=isIndic) if feature is not None: features[tag] = feature return features def _getIndicGlyphs(self): cmap = self.makeUnicodeToGlyphNameMapping() unicodeIsIndic = partial(unicodeInScripts, scripts=self.indicScripts) if any(unicodeIsIndic for uv in cmap): # If there are any characters from Indic scripts in the cmap, we # compile a temporary GSUB table to resolve substitutions and get # the set of all the "Indic" glyphs, including alternate glyphs. gsub = self.compileGSUB() glyphGroups = classifyGlyphs(unicodeIsIndic, cmap, gsub) # the 'glyphGroups' dict is keyed by the return value of the # classifying include, so here 'True' means all the Indic glyphs return glyphGroups.get(True, set()) else: return set() def _write(self): self._pruneUnusedAnchors() newClassDefs = self._makeMarkClassDefinitions() self._setBaseAnchorMarkClasses() features = self._makeFeatures() if not features: return False feaFile = self.context.feaFile feaFile.statements.extend(newClassDefs) # add empty line to separate classes from following statements feaFile.statements.append(ast.Comment("")) for _, feature in sorted(features.items()): feaFile.statements.append(feature) return True ufo2ft-2.12.2/Lib/ufo2ft/filters/000077500000000000000000000000001362551502500163725ustar00rootroot00000000000000ufo2ft-2.12.2/Lib/ufo2ft/filters/__init__.py000066400000000000000000000210301362551502500204770ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import importlib import logging from fontTools.misc.py23 import SimpleNamespace from fontTools.misc.loggingTools import Timer from ufo2ft.util import _LazyFontName, _GlyphSet from ufo2ft.constants import FILTERS_KEY as UFO2FT_FILTERS_KEY # keep previous name 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:] + "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(UFO2FT_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( include=filterDict.get("include"), exclude=filterDict.get("exclude"), *filterDict.get("args", []), **filterDict.get("kwargs", {}) ) if filterDict.get("pre"): preFilters.append(filterObj) else: postFilters.append(filterObj) return preFilters, postFilters class BaseFilter(object): # 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 = {} def __init__(self, *args, **kwargs): self.options = options = SimpleNamespace() # process positional arguments num_required = len(self._args) num_args = len(args) if num_args < num_required: missing = [repr(a) for a in self._args[num_args:]] num_missing = len(missing) raise TypeError( "missing {0} required positional argument{1}: {2}".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 {0} unsupported positional argument{1}: {2}".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 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 {0}unsupported keyword argument{1}: {2}".format( "an " if num_left == 1 else "", "s" if len(kwargs) > 1 else "", ", ".join("'{}'".format(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( "{0}={1!r}".format(k, getattr(self.options, k)) for k in sorted(self._kwargs) ) ) if hasattr(self, "_include_repr"): items.append("include={}".format(self._include_repr())) elif hasattr(self, "_exclude_repr"): items.append("exclude={}".format(self._exclude_repr())) return "{0}({1})".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() 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 with Timer() as t: # we sort the glyph names to make loop deterministic for glyphName in sorted(glyphSet.keys()): if glyphName in modified: continue glyph = glyphSet[glyphName] if include(glyph) and filter_(glyph): modified.add(glyphName) num = len(modified) if num > 0: logger.debug( "Took %.3fs to run %s on %d glyph%s", t, self.name, len(modified), "" if num == 1 else "s", ) return modified ufo2ft-2.12.2/Lib/ufo2ft/filters/cubicToQuadratic.py000066400000000000000000000046651362551502500222050ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from ufo2ft.fontInfoData import getAttrWithFallback from ufo2ft.filters import BaseFilter from cu2qu.ufo import DEFAULT_MAX_ERR, CURVE_TYPE_LIB_KEY from cu2qu.pens import Cu2QuPointPen import logging logger = logging.getLogger(__name__) class CubicToQuadraticFilter(BaseFilter): _kwargs = { "conversionError": None, "reverseDirection": True, "rememberCurveType": False, } def set_context(self, font, glyphSet): ctx = super(CubicToQuadraticFilter, self).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(CubicToQuadraticFilter, self).__call__(font, glyphSet) if modified: stats = self.context.stats logger.info( "New spline lengths: %s" % (", ".join("%s: %d" % (l, stats[l]) for l 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, ) contours = list(glyph) glyph.clearContours() for contour in contours: contour.drawPoints(pen) return True ufo2ft-2.12.2/Lib/ufo2ft/filters/decomposeComponents.py000066400000000000000000000007161362551502500227740ustar00rootroot00000000000000from __future__ import absolute_import, division, print_function, unicode_literals from fontTools.misc.transform import Transform import ufo2ft.util from ufo2ft.filters import BaseFilter class DecomposeComponentsFilter(BaseFilter): def filter(self, glyph): if not glyph.components: return False ufo2ft.util.deepCopyContours(self.context.glyphSet, glyph, glyph, Transform()) glyph.clearComponents() return True ufo2ft-2.12.2/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py000066400000000000000000000054011362551502500235670ustar00rootroot00000000000000from ufo2ft.filters import BaseFilter from ufo2ft.util import _GlyphSet from ufo2ft.constants import COLOR_LAYERS_KEY, COLOR_LAYER_MAPPING_KEY 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 font.lib[COLOR_LAYERS_KEY] = {} 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 glyphSet[layerGlyphName] = layerGlyph self.context.colorLayerGlyphNames.add(layerGlyphName) return layerGlyphName def filter(self, glyph): 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 layers = [] for layerName, colorID in colorLayerMapping: layerGlyphSet = self._getLayer(font, layerName) if glyph.name in layerGlyphSet: layerGlyphName = self._copyGlyph( layerGlyphSet, glyphSet, glyph.name, layerName) layers.append((layerGlyphName, colorID)) if layers: colorLayers[glyph.name] = layers ufo2ft-2.12.2/Lib/ufo2ft/filters/flattenComponents.py000066400000000000000000000034541362551502500224550ustar00rootroot00000000000000from fontTools.misc.transform import Transform from ufo2ft.filters import BaseFilter import logging logger = logging.getLogger(__name__) class FlattenComponentsFilter(BaseFilter): def __call__(self, font, glyphSet=None): if super(FlattenComponentsFilter, self).__call__(font, glyphSet): modified = self.context.modified if modified: logger.info("Flattened composite glyphs: %i" % len(modified)) return modified def filter(self, glyph): flattened = False if not glyph.components: return flattened pen = glyph.getPen() for comp in list(glyph.components): flattened_tuples = _flattenComponent(self.context.glyphSet, comp) if flattened_tuples[0] != (comp.baseGlyph, comp.transformation): flattened = True glyph.removeComponent(comp) for flattened_tuple in flattened_tuples: pen.addComponent(*flattened_tuple) if flattened: self.context.modified.add(glyph.name) return flattened def _flattenComponent(glyphSet, component): """Returns a list of tuples (baseGlyph, transform) of nested component.""" glyph = glyphSet[component.baseGlyph] if not glyph.components: transformation = Transform(*component.transformation) return [(component.baseGlyph, transformation)] all_flattened_components = [] for nested in glyph.components: flattened_components = _flattenComponent(glyphSet, nested) for i, (_, tr) in enumerate(flattened_components): tr = tr.transform(component.transformation) flattened_components[i] = (flattened_components[i][0], tr) all_flattened_components.extend(flattened_components) return all_flattened_components ufo2ft-2.12.2/Lib/ufo2ft/filters/propagateAnchors.py000066400000000000000000000152061362551502500222500ustar00rootroot00000000000000# 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 sys import fontTools.pens.boundsPen from fontTools.misc.transform import Transform from ufo2ft.filters import BaseFilter import logging logger = logging.getLogger(__name__) class PropagateAnchorsFilter(BaseFilter): def set_context(self, font, glyphSet): ctx = super(PropagateAnchorsFilter, self).set_context(font, glyphSet) ctx.processed = set() return ctx def __call__(self, font, glyphSet=None): if super(PropagateAnchorsFilter, self).__call__(font, glyphSet): modified = self.context.modified 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) return len(glyph.anchors) > before def _propagate_glyph_anchors(glyphSet, composite, processed): """ 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: 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) 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)) ) 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)) 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( "Don't know to to compute the bounds of component '{}' ".format(component) ) ufo2ft-2.12.2/Lib/ufo2ft/filters/removeOverlaps.py000066400000000000000000000032051362551502500217550ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from ufo2ft.filters import BaseFilter from enum import Enum import logging 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 union, BooleanOperationsError 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 union, PathOpsError 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-2.12.2/Lib/ufo2ft/filters/sortContours.py000066400000000000000000000031321362551502500214670ustar00rootroot00000000000000from __future__ import absolute_import, division, print_function, unicode_literals import 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-2.12.2/Lib/ufo2ft/filters/transformations.py000066400000000000000000000116161362551502500222020ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import math from collections import namedtuple import logging from ufo2ft.fontInfoData import getAttrWithFallback from ufo2ft.filters import BaseFilter from fontTools.misc.py23 import round from fontTools.misc.fixedTools import otRound from fontTools.misc.transform import Transform, Identity from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.transformPen import TransformPen as _TransformPen from enum import IntEnum log = logging.getLogger(__name__) class TransformPen(_TransformPen): def __init__(self, outPen, transformation, modified=None): super(TransformPen, self).__init__(outPen, transformation) self.modified = modified if modified is not None else set() self._inverted = self._transformation.inverse() def addComponent(self, baseGlyph, transformation): if baseGlyph in self.modified: if transformation[:4] == (1, 0, 0, 1): # if the component's transform only has a simple offset, then # we don't need to transform the component again self._outPen.addComponent(baseGlyph, transformation) return # 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(TransformPen, self).addComponent(baseGlyph, transformation) 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(TransformationsFilter, self).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 = RecordingPen() glyph.draw(rec) glyph.clearContours() glyph.clearComponents() outpen = glyph.getPen() filterpen = TransformPen(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)) return True ufo2ft-2.12.2/Lib/ufo2ft/fontInfoData.py000066400000000000000000000425641362551502500176630ustar00rootroot00000000000000""" 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. """ from __future__ import print_function, division, absolute_import, unicode_literals import logging import math from datetime import datetime import calendar import time import unicodedata import os from fontTools.misc.py23 import tobytes, tostr, tounicode, unichr from fontTools.misc.fixedTools import otRound from fontTools.misc.textTools import binary2num from fontTools import ufoLib 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.utcfromtimestamp(int(os.environ["SOURCE_DATE_EPOCH"])) 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 1. If italicAngle is non-zero, compute the slope rise from the complementary openTypeHheaCaretSlopeRun, if the latter is defined. Else, default to an arbitrary fixed reference point (1000). """ 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))) else: return 1000 # just an arbitrary non-zero reference point return 1 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 "%s;%s;%s" % (version, vendor, fontName) def openTypeNamePreferredFamilyNameFallback(info): """ Fallback to *familyName*. """ return getAttrWithFallback(info, "familyName") def openTypeNamePreferredSubfamilyNameFallback(info): """ Fallback to *styleName*. """ return getAttrWithFallback(info, "styleName") def openTypeNameCompatibleFullNameFallback(info): """ Fallback to *styleMapFamilyName styleMapStyleName*. If *styleMapStyleName* is *regular* this will not add the style name. """ familyName = getAttrWithFallback(info, "styleMapFamilyName") styleMapStyleName = getAttrWithFallback(info, "styleMapStyleName") if styleMapStyleName != "regular": familyName += " " + styleMapStyleName.title() return familyName 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 = set([unichr(i) for i in range(33, 127)]) def normalizeStringForPostscript(s, allowSpaces=True): s = tounicode(s) 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 = tounicode(tobytes(c, errors="replace")) normalized.append(tostr(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 = "%s-%s" % ( getAttrWithFallback(info, "openTypeNamePreferredFamilyName"), getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName"), ) return normalizeNameForPostscript(name) def postscriptFullNameFallback(info): """ Fallback to *openTypeNamePreferredFamilyName openTypeNamePreferredSubfamilyName*. """ return "%s %s" % ( 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, 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, openTypeNameCompatibleFullName=openTypeNameCompatibleFullNameFallback, 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 = set( [ "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-2.12.2/Lib/ufo2ft/maxContextCalc.py000066400000000000000000000004561362551502500202160ustar00rootroot00000000000000"""NOTE: this module was moved to fonttools, it is kept here only for backward compatibility. Please import it from the new location. """ from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.otlLib.maxContextCalc import maxCtxFont __all__ = ["maxCtxFont"] ufo2ft-2.12.2/Lib/ufo2ft/outlineCompiler.py000066400000000000000000001520401362551502500204500ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import byteord, tounicode, round, unichr, BytesIO import logging import math from collections import Counter, namedtuple from types import SimpleNamespace from fontTools.ttLib import TTFont, newTable from fontTools.cffLib import ( TopDictIndex, TopDict, CharStrings, SubrsIndex, GlobalSubrsIndex, PrivateDict, IndexedStrings, ) from fontTools.pens.boundsPen import ControlBoundsPen from fontTools.pens.t2CharStringPen import T2CharStringPen from fontTools.pens.ttGlyphPen import TTGlyphPen from fontTools.ttLib.tables.O_S_2f_2 import Panose from fontTools.ttLib.tables._h_e_a_d import mac_epoch_diff from fontTools.ttLib.tables._g_l_y_f import Glyph, USE_MY_METRICS from fontTools.misc.arrayTools import unionRect from fontTools.misc.fixedTools import otRound from ufo2ft.errors import InvalidFontData from ufo2ft.fontInfoData import ( getAttrWithFallback, dateStringToTimeValue, dateStringForNow, intListToNum, normalizeStringForPostscript, ) from ufo2ft.util import ( makeOfficialGlyphOrder, makeUnicodeToGlyphNameMapping, calcCodePageRanges, ) from ufo2ft.constants import COLOR_LAYERS_KEY, COLOR_PALETTES_KEY 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(object): """Create a feature-less outline binary.""" sfntVersion = None tables = frozenset( ["head", "hmtx", "hhea", "name", "maxp", "cmap", "OS/2", "post", "vmtx", "vhea", "COLR", "CPAL"] ) def __init__(self, font, glyphSet=None, glyphOrder=None, tables=None): self.ufo = font # use the previously filtered glyphSet, if any if glyphSet is None: glyphSet = {g.name: g for g in font} self.makeMissingRequiredGlyphs(font, glyphSet) 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 # cached values defined later on self._glyphBoundingBoxes = None self._fontBoundingBox = None self._compiledGlyphs = None def compile(self): """ Compile the OpenType binary. """ self.otf = TTFont(sfntVersion=self.sfntVersion) # 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 # 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() self.setupOtherTables() 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 glyphName, glyphBox in self.glyphBoundingBoxes.items(): 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) @staticmethod def makeMissingRequiredGlyphs(font, glyphSet): """ 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 unitsPerEm = otRound(getAttrWithFallback(font.info, "unitsPerEm")) ascender = otRound(getAttrWithFallback(font.info, "ascender")) descender = otRound(getAttrWithFallback(font.info, "descender")) defaultWidth = otRound(unitsPerEm * 0.5) glyphSet[".notdef"] = StubGlyph( name=".notdef", width=defaultWidth, unitsPerEm=unitsPerEm, ascender=ascender, descender=descender, ) 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_gasp(self): if "gasp" not in self.tables: return self.otf["gasp"] = gasp = newTable("gasp") gasp_ranges = dict() for record in self.ufo.info.openTypeGaspRangeRecords: rangeMaxPPEM = record["rangeMaxPPEM"] behavior_bits = record["rangeGaspBehavior"] rangeGaspBehavior = intListToNum(behavior_bits, 0, 4) gasp_ranges[rangeMaxPPEM] = rangeGaspBehavior gasp.gaspRange = gasp_ranges def setupTable_head(self): """ Make the head table. **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 = 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 = [] # 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"] # on Python 2, plistLib (used by ufoLib) returns unicode strings # only when plist data contain non-ascii characters, and returns # ascii-encoded bytes when it can. On the other hand, fontTools's # name table `setName` method wants unicode strings, so we must # decode them first nameVal = tounicode(nameRecord["string"], encoding="ascii") name.setName(nameVal, nameId, platformId, platEncId, langId) # 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 = "%s %s" % (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, } # don't add typographic names if they are the same as the legacy ones if nameVals[1] == nameVals[16]: del nameVals[16] if nameVals[2] == nameVals[17]: 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 nameVal = tounicode(nameVal, encoding="ascii") 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) 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 = dict( (k, v) for k, v in self.unicodeToGlyphNameMapping.items() if k > 65535 ) if nonBMP: mapping = dict( (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] 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.xAvgCharWidth = 0 hmtx = self.otf.get("hmtx") if hmtx is not None: widths = [width for width, _ in hmtx.metrics.values() if width > 0] if widths: os2.xAvgCharWidth = otRound(sum(widths) / len(widths)) # 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 = 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) else: 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 os2.achVendID = tounicode( getAttrWithFallback(font.info, "openTypeOS2VendorID"), encoding="ascii", errors="ignore", ) # 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.allGlyphs: 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 # glyph count setattr( table, "numberOf%sMetrics" % ("H" if isHhea else "V"), len(self.allGlyphs) ) 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(): vorg.VOriginRecords[glyphName] = _getVerticalOrigin(self.otf, glyph) 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 = getAttrWithFallback(font.info, "italicAngle") post.italicAngle = italicAngle # underline underlinePosition = getAttrWithFallback( font.info, "postscriptUnderlinePosition" ) post.underlinePosition = otRound(underlinePosition) underlineThickness = getAttrWithFallback( font.info, "postscriptUnderlineThickness" ) post.underlineThickness = otRound(underlineThickness) post.isFixedPitch = 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] self.otf["COLR"] = buildCOLR(layerInfo) 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 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 import re prefix = "com.github.fonttools.ttx" sfntVersionRE = re.compile( '(^$)', flags=re.MULTILINE ) 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") # strip 'sfntVersion' attribute from ttFont element, # if present, to avoid overwriting the current value ttx = sfntVersionRE.sub(r"\1\3", ttx) fp = BytesIO(ttx.encode("utf-8")) self.otf.importXML(fp) 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, roundTolerance=None, optimizeCFF=True, ): if roundTolerance is not None: self.roundTolerance = float(roundTolerance) else: # round all coordinates to integers by default self.roundTolerance = 0.5 super(OutlineOTFCompiler, self).__init__( font, glyphSet=glyphSet, glyphOrder=glyphOrder, tables=tables ) 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 # 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 = getAttrWithFallback(info, "postscriptIsFixedPitch") topDict.ItalicAngle = getAttrWithFallback(info, "italicAngle") 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 | {"loca", "gasp", "glyf"} def compileGlyphs(self): """Compile and return the TrueType glyphs for this font.""" allGlyphs = self.allGlyphs ttGlyphs = {} for name in self.glyphOrder: glyph = allGlyphs[name] pen = TTGlyphPen(allGlyphs) try: glyph.draw(pen) except NotImplementedError: logger.error("%r has invalid curve format; skipped", name) ttGlyph = Glyph() else: ttGlyph = pen.glyph() 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 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() ) def setupTable_post(self): """Make a format 2 post table with the compiler's glyph order.""" super(OutlineTTFCompiler, self).setupTable_post() if "post" not in self.otf: return post = self.otf["post"] post.formatType = 2.0 post.extraNames = [] post.mapping = {} post.glyphOrder = self.glyphOrder def setupOtherTables(self): self.setupTable_glyf() if self.ufo.info.openTypeGaspRangeRecords: self.setupTable_gasp() 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 hmtx = self.otf.get("hmtx") ttGlyphs = self.getCompiledGlyphs() for name in self.glyphOrder: ttGlyph = ttGlyphs[name] if ttGlyph.isComposite() and hmtx is not None and self.autoUseMyMetrics: self.autoUseMyMetrics(ttGlyph, name, hmtx) glyf[name] = ttGlyph @staticmethod def autoUseMyMetrics(ttGlyph, glyphName, hmtx): """ Set the "USE_MY_METRICS" flag on the first component having the same advance width as the composite glyph, no transform and no horizontal shift (but allow it to shift vertically). This forces the composite glyph to use the possibly hinted horizontal metrics of the sub-glyph, instead of those from the "hmtx" table. """ width = hmtx[glyphName][0] for component in ttGlyph.components: try: baseName, transform = component.getComponentInfo() except AttributeError: # component uses '{first,second}Pt' instead of 'x' and 'y' continue try: baseMetrics = hmtx[baseName] except KeyError: continue # ignore missing components else: if baseMetrics[0] == width and transform[:-1] == (1, 0, 0, 1, 0): component.flags |= USE_MY_METRICS break class StubGlyph(object): """ This object will be used to create missing glyphs (specifically .notdef) in the provided UFO. """ def __init__(self, name, width, unitsPerEm, ascender, descender, unicodes=[]): self.name = name self.width = width self.unitsPerEm = unitsPerEm self.ascender = ascender self.descender = descender self.unicodes = unicodes self.components = [] self.anchors = [] if unicodes: self.unicode = unicodes[0] else: self.unicode = None if name == ".notdef": self.draw = self._drawDefaultNotdef 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 _drawDefaultNotdef(self, 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 _get_controlPointBounds(self): pen = ControlBoundsPen(None) self.draw(pen) return pen.bounds controlPointBounds = property(_get_controlPointBounds) ufo2ft-2.12.2/Lib/ufo2ft/postProcessor.py000066400000000000000000000154131362551502500201650ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import BytesIO from fontTools.ttLib import TTFont from ufo2ft.constants import USE_PRODUCTION_NAMES, GLYPHS_DONT_USE_PRODUCTION_NAMES import logging import re logger = logging.getLogger(__name__) class PostProcessor(object): """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 def __init__(self, otf, ufo, glyphSet=None): self.ufo = ufo self.glyphSet = glyphSet if glyphSet is not None else ufo stream = BytesIO() otf.save(stream) stream.seek(0) self.otf = TTFont(stream) self._postscriptNames = ufo.lib.get("public.postscriptNames") def process(self, useProductionNames=None, optimizeCFF=True): """ useProductionNames: 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. If the value 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: Run compreffor to subroubtinize CFF table, if present. """ if useProductionNames is None: useProductionNames = self.ufo.lib.get( USE_PRODUCTION_NAMES, not self.ufo.lib.get(GLYPHS_DONT_USE_PRODUCTION_NAMES) and self._postscriptNames is not None, ) if useProductionNames: logger.info("Renaming glyphs to final production names") self._rename_glyphs_from_ufo() if optimizeCFF and "CFF " in self.otf: from compreffor import compress logger.info("Subroutinizing CFF table") compress(self.otf) return self.otf def _rename_glyphs_from_ufo(self): """Rename glyphs using ufo.lib.public.postscriptNames in UFO.""" rename_map = self._build_production_names() otf = self.otf otf.setGlyphOrder([rename_map.get(n, n) for n in otf.getGlyphOrder()]) # we need to compile format 2 'post' table so that the 'extraNames' # attribute is updated with the list of the names outside the # standard Macintosh glyph order; otherwise, if one dumps the font # to TTX directly before compiling first, the post table will not # contain the extraNames. if "post" in otf and otf["post"].formatType == 2.0: otf["post"].extraNames = [] otf["post"].compile(self.otf) if "CFF " in otf: cff = otf["CFF "].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 "%s%04X" % ("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 "%s.%s" % ( 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 = ["%s.%s" % (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 ufo2ft-2.12.2/Lib/ufo2ft/preProcessor.py000066400000000000000000000213761362551502500177730ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import basestring from ufo2ft.fontInfoData import getAttrWithFallback from ufo2ft.filters import loadFilters from ufo2ft.filters.decomposeComponents import DecomposeComponentsFilter from ufo2ft.util import _GlyphSet class BasePreProcessor(object): """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 are specified in the UFO lib.plist under the private key "com.github.googlei18n.ufo2ft.filters". """ def __init__( self, ufo, inplace=False, layerName=None, skipExportGlyphs=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) self.preFilters, self.postFilters = loadFilters(ufo) 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 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 = [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 ``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, convertCubics=True, conversionError=None, reverseDirection=True, rememberCurveType=True, ): # len(g) is the number of contours, so we include the all glyphs # that have both components and at least one contour filters = [DecomposeComponentsFilter(include=lambda g: len(g))] 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, ) ) return filters class TTFInterpolatablePreProcessor(object): """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`` and ``rememberCurveType`` arguments work in the same way as in the ``TTFPreProcessor``. """ def __init__( self, ufos, inplace=False, conversionError=None, reverseDirection=True, rememberCurveType=True, layerNames=None, skipExportGlyphs=None, ): from cu2qu.ufo import DEFAULT_MAX_ERR self.ufos = ufos self.inplace = inplace if layerNames is None: layerNames = [None] * len(ufos) assert len(ufos) == len(layerNames) self.layerNames = layerNames self.glyphSets = [ _GlyphSet.from_layer( ufo, layerName, copy=not inplace, skipExportGlyphs=skipExportGlyphs ) for ufo, layerName in zip(ufos, layerNames) ] self._conversionErrors = [ (conversionError or DEFAULT_MAX_ERR) * getAttrWithFallback(ufo.info, "unitsPerEm") for ufo in ufos ] self._reverseDirection = reverseDirection self._rememberCurveType = rememberCurveType self.preFilters, self.postFilters = [], [] for ufo in ufos: pre, post = loadFilters(ufo) self.preFilters.append(pre) self.postFilters.append(post) def process(self): from cu2qu.ufo import fonts_to_quadratic # first apply all custom pre-filters for funcs, ufo, glyphSet in zip(self.preFilters, self.ufos, self.glyphSets): for func in funcs: func(ufo, glyphSet) fonts_to_quadratic( self.glyphSets, max_err=self._conversionErrors, reverse_direction=self._reverseDirection, dump_stats=True, remember_curve_type=self._rememberCurveType and self.inplace, ) decompose = DecomposeComponentsFilter(include=lambda g: len(g)) for ufo, glyphSet in zip(self.ufos, self.glyphSets): decompose(ufo, glyphSet) # finally apply all custom post-filters for funcs, ufo, glyphSet in zip(self.postFilters, self.ufos, self.glyphSets): for func in funcs: func(ufo, glyphSet) return self.glyphSets ufo2ft-2.12.2/Lib/ufo2ft/util.py000066400000000000000000000342311362551502500162540ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals try: from inspect import getfullargspec as getargspec # PY3 except ImportError: from inspect import getargspec # PY2 from copy import deepcopy from fontTools.misc.py23 import unichr from fontTools import ttLib from fontTools import subset from fontTools import unicodedata from fontTools.feaLib.builder import addOpenTypeFeatures from fontTools.misc.transform import Identity, Transform from fontTools.pens.reverseContourPen import ReverseContourPen from fontTools.pens.transformPen import TransformPen import logging 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 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 # If any glyphs in the skipExportGlyphs list are used as components, decompose # them in the containing glyphs... if skipExportGlyphs: for glyph in self.values(): if any(c.baseGlyph in skipExportGlyphs for c in glyph.components): deepCopyContours(self, glyph, glyph, Transform(), skipExportGlyphs) if hasattr(glyph, "removeComponent"): # defcon for c in [ component for component in glyph.components if component.baseGlyph in skipExportGlyphs ]: glyph.removeComponent(c) else: # ufoLib2 glyph.components[:] = [ c for c in glyph.components if c.baseGlyph not in skipExportGlyphs ] # ... and then remove them from the glyph set, if even present. for glyph_name in skipExportGlyphs: if glyph_name in self: del self[glyph_name] self.name = layer.name if layerName is not None else None return self def _copyLayer(layer, obj_type=dict): # defcon.Glyph doesn't take a name argument, ufoLib2 requires one... try: g = next(iter(layer)) except StopIteration: # layer is empty return obj_type() cls = g.__class__ if "name" in getargspec(cls.__init__).args: def newGlyph(name): return cls(name=name) else: def newGlyph(name): # use instantiateGlyphObject() to keep any custom sub-element classes # https://github.com/googlefonts/ufo2ft/issues/363 g2 = g.layer.instantiateGlyphObject() g2.name = name return g2 # copy everything except unused attributes: 'guidelines', 'note', 'image' glyphSet = obj_type() for glyph in layer: copy = newGlyph(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() glyph.drawPoints(pointPen) glyphSet[glyph.name] = copy return glyphSet 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: from ufo2ft.errors import InvalidFontData InvalidFontData( "cannot map '%s' to U+%04X; already mapped to '%s'" % (glyphName, uni, mapping[uni]) ) return mapping def compileGSUB(featureFile, glyphOrder): """ Compile and return a GSUB table from `featureFile` (feaLib FeatureFile), using the given `glyphOrder` (list of glyph names). """ font = ttLib.TTFont() font.setGlyphOrder(glyphOrder) addOpenTypeFeatures(font, featureFile, tables={"GSUB"}) return font.get("GSUB") 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): """ 'unicodeFunc' is a callable that takes a Unicode codepoint and returns a string 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. Returns a dictionary of glyph sets associated with the given Unicode properties. """ glyphSets = {} neutralGlyphs = set() for uv, glyphName in cmap.items(): key = unicodeFunc(uv) if key is None: neutralGlyphs.add(glyphName) else: glyphSets.setdefault(key, 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) 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 = unicodedata.script_extension(unichr(uv)) if "Zyyy" in sx: return None return not sx.isdisjoint(scripts) 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 = [unichr(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 unichr(0xF000) <= char <= unichr(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(object): def __init__(self, font): self.font = font def __str__(self): from ufo2ft.fontInfoData import getAttrWithFallback return getAttrWithFallback(self.font.info, "postscriptFontName") def getDefaultMasterFont(designSpaceDoc): defaultSource = designSpaceDoc.findDefault() if not defaultSource: from ufo2ft.errors import InvalidDesignSpaceData raise InvalidDesignSpaceData( "Can't find base (neutral) master in DesignSpace document" ) if not defaultSource.font: from ufo2ft.errors import InvalidDesignSpaceData raise InvalidDesignSpaceData( "DesignSpace source '%s' is missing required 'font' attribute" % getattr(defaultSource, "name", "") ) return defaultSource.font ufo2ft-2.12.2/MANIFEST.in000066400000000000000000000002441362551502500145450ustar00rootroot00000000000000include README.rst include LICENSE include tox.ini include *requirements.txt recursive-include tests *.py recursive-include tests/data *.glif *.plist *.fea *.ttx ufo2ft-2.12.2/README.rst000066400000000000000000000112051362551502500144750ustar00rootroot00000000000000|Travis CI Status| |Appveyor CI 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. .. |Travis CI Status| image:: https://travis-ci.org/googlefonts/ufo2ft.svg :target: https://travis-ci.org/googlefonts/ufo2ft .. |Appveyor CI status| image:: https://ci.appveyor.com/api/projects/status/jaw9bi221plmjlny/branch/master?svg=true :target: https://ci.appveyor.com/project/fonttools/ufo2ft/branch/master .. |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 ufo2ft-2.12.2/appveyor.yml000066400000000000000000000036761362551502500154130ustar00rootroot00000000000000environment: matrix: - JOB: "3.6 32-bit" PYTHON_HOME: "C:\\Python36" TOXENV: "py36-cov" TOXPYTHON: "C:\\Python36\\python.exe" - JOB: "3.6 64-bit" PYTHON_HOME: "C:\\Python36-x64" TOXENV: "py36-cov" TOXPYTHON: "C:\\Python36-x64\\python.exe" - JOB: "3.7 32-bit" PYTHON_HOME: "C:\\Python37" TOXENV: "py37-cov" TOXPYTHON: "C:\\Python37\\python.exe" - JOB: "3.7 64-bit" PYTHON_HOME: "C:\\Python37-x64" TOXENV: "py37-cov" TOXPYTHON: "C:\\Python37-x64\\python.exe" # Do not build feature branches with open Pull Requests after the initial # opening of a PR. skip_branch_with_pr: true install: # If there is a newer build queued for the same PR, cancel this one. # The AppVeyor 'rollout builds' option is supposed to serve the same # purpose but it is problematic because it tends to cancel builds pushed # directly to master instead of just PR builds (or the converse). # credits: JuliaLang developers. - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` throw "There are newer queued builds for this pull request, failing early." } # Prepend Python to the PATH of this build - "SET PATH=%PYTHON_HOME%;%PYTHON_HOME%\\Scripts;%PATH%" # check that we have the expected version and architecture for Python - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # upgrade pip to avoid out-of-date warnings - "python -m pip install --disable-pip-version-check --user --upgrade pip" - "python -m pip --version" # install the dependencies to run the tests - "python -m pip install -r dev-requirements.txt" build: false test_script: - "tox" ufo2ft-2.12.2/dev-requirements.txt000066400000000000000000000000461362551502500170470ustar00rootroot00000000000000pytest>=2.8 virtualenv>=15.0 tox>=2.3 ufo2ft-2.12.2/requirements.txt000066400000000000000000000002301362551502500162660ustar00rootroot00000000000000fonttools[lxml,ufo]==4.4.0 defcon==0.6.0 cu2qu==1.6.6 compreffor==0.4.6.post1 booleanOperations==0.9.0 # alternative UFO implementation ufoLib2==0.5.1 ufo2ft-2.12.2/setup.cfg000066400000000000000000000005431362551502500146320ustar00rootroot00000000000000[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 ufo2ft-2.12.2/setup.py000066400000000000000000000037531362551502500145310ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import print_function, division, absolute_import import sys from setuptools import setup, find_packages 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.4.0", "cu2qu>=1.6.6", "compreffor>=0.4.6", "booleanOperations>=0.9.0", ], extras_require={"pathops": ["skia-pathops>=0.2.0"]}, python_requires=">=3.6", 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-2.12.2/tests/000077500000000000000000000000001362551502500141515ustar00rootroot00000000000000ufo2ft-2.12.2/tests/__init__.py000066400000000000000000000000001362551502500162500ustar00rootroot00000000000000ufo2ft-2.12.2/tests/conftest.py000066400000000000000000000042171362551502500163540ustar00rootroot00000000000000import os 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") 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 ufo2ft-2.12.2/tests/data/000077500000000000000000000000001362551502500150625ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/Bug108.ttx000066400000000000000000000030101362551502500165630ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/Bug108.ufo/000077500000000000000000000000001362551502500166205ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/Bug108.ufo/features.fea000066400000000000000000000000351362551502500211110ustar00rootroot00000000000000include(Bug108_included.fea) ufo2ft-2.12.2/tests/data/Bug108.ufo/fontinfo.plist000066400000000000000000000012731362551502500215220ustar00rootroot00000000000000 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-2.12.2/tests/data/Bug108.ufo/glyphs/000077500000000000000000000000001362551502500201265ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/Bug108.ufo/glyphs/_notdef.glif000066400000000000000000000007531362551502500224140ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/Bug108.ufo/glyphs/a.glif000066400000000000000000000004361362551502500212140ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/Bug108.ufo/glyphs/b.glif000066400000000000000000000005111362551502500212070ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/Bug108.ufo/glyphs/c.glif000066400000000000000000000006241362551502500212150ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/Bug108.ufo/glyphs/contents.plist000066400000000000000000000006371362551502500230460ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif space space.glif ufo2ft-2.12.2/tests/data/Bug108.ufo/glyphs/space.glif000066400000000000000000000001771362551502500220710ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/Bug108.ufo/groups.plist000066400000000000000000000002761362551502500212210ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/Bug108.ufo/layercontents.plist000066400000000000000000000004231362551502500225660ustar00rootroot00000000000000 public.default glyphs ufo2ft-2.12.2/tests/data/Bug108.ufo/lib.plist000066400000000000000000000011251362551502500204420ustar00rootroot00000000000000 public.glyphOrder .notdef space a b c public.postscriptNames a uni0061 b uni0062 c uni0063 space uni0020 ufo2ft-2.12.2/tests/data/Bug108.ufo/metainfo.plist000066400000000000000000000003601362551502500214760ustar00rootroot00000000000000 formatVersion 3 ufo2ft-2.12.2/tests/data/Bug108_included.fea000066400000000000000000000000461362551502500203540ustar00rootroot00000000000000feature kern { pos a b -10; } kern; ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/000077500000000000000000000000001362551502500231165ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/fontinfo.plist000066400000000000000000000047551362551502500260300ustar00rootroot00000000000000 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-2.12.2/tests/data/CantarellAnchorPropagation.ufo/glyphs/000077500000000000000000000000001362551502500244245ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/glyphs/circumflexcomb.loclV_I_E_T_.glif000066400000000000000000000015211362551502500325040ustar00rootroot00000000000000 RMXScaler height 80 com.schriftgestaltung.Glyphs.originalWidth 386.0 ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/glyphs/circumflexcomb_tildecomb.glif000066400000000000000000000006011362551502500323100ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.originalWidth 432.0 ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/glyphs/contents.plist000066400000000000000000000011031362551502500273310ustar00rootroot00000000000000 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-2.12.2/tests/data/CantarellAnchorPropagation.ufo/glyphs/o.glif000066400000000000000000000027271362551502500255350ustar00rootroot00000000000000 o ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/glyphs/ocircumflextilde.glif000066400000000000000000000004011362551502500306240ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/glyphs/tildecomb.loclV_I_E_T_.glif000066400000000000000000000024511362551502500314470ustar00rootroot00000000000000 RMXScaler height 80 com.schriftgestaltung.Glyphs.originalWidth 450.0 ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/layercontents.plist000066400000000000000000000004371362551502500270710ustar00rootroot00000000000000 public.default glyphs ufo2ft-2.12.2/tests/data/CantarellAnchorPropagation.ufo/lib.plist000066400000000000000000000016571362551502500247520ustar00rootroot00000000000000 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-2.12.2/tests/data/CantarellAnchorPropagation.ufo/metainfo.plist000066400000000000000000000004761362551502500260040ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/ColorTest.ufo/000077500000000000000000000000001362551502500175705ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/ColorTest.ufo/fontinfo.plist000066400000000000000000000016051362551502500224710ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 familyName ColorTest guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular unitsPerEm 1000 xHeight 500 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color1/000077500000000000000000000000001362551502500222745ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color1/a.glif000066400000000000000000000012141362551502500233550ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color1/b.glif000066400000000000000000000011641362551502500233620ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color1/c.glif000066400000000000000000000011641362551502500233630ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color1/contents.plist000066400000000000000000000005071362551502500252100ustar00rootroot00000000000000 a a.glif b b.glif c c.glif ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color1/layerinfo.plist000066400000000000000000000003671362551502500253470ustar00rootroot00000000000000 color 0,1,0.25,0.7 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color2/000077500000000000000000000000001362551502500222755ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color2/a.glif000066400000000000000000000005371362551502500233650ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color2/b.glif000066400000000000000000000002301362551502500233540ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color2/c.glif000066400000000000000000000005071362551502500233640ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color2/contents.plist000066400000000000000000000005071362551502500252110ustar00rootroot00000000000000 a a.glif b b.glif c c.glif ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs.color2/layerinfo.plist000066400000000000000000000003641362551502500253450ustar00rootroot00000000000000 color 0,1,1,0.7 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs/000077500000000000000000000000001362551502500210765ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs/a.glif000066400000000000000000000005331362551502500221620ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs/b.glif000066400000000000000000000012441362551502500221630ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.colorLayerMapping color1 1 color2 0 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs/c.glif000066400000000000000000000012441362551502500221640ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.colorLayerMapping color2 1 color1 0 ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs/contents.plist000066400000000000000000000005071362551502500240120ustar00rootroot00000000000000 a a.glif b b.glif c c.glif ufo2ft-2.12.2/tests/data/ColorTest.ufo/glyphs/layerinfo.plist000066400000000000000000000003671362551502500241510ustar00rootroot00000000000000 color 1,0.75,0,0.7 ufo2ft-2.12.2/tests/data/ColorTest.ufo/layercontents.plist000066400000000000000000000007231362551502500235410ustar00rootroot00000000000000 foreground glyphs color1 glyphs.color1 color2 glyphs.color2 ufo2ft-2.12.2/tests/data/ColorTest.ufo/lib.plist000066400000000000000000000026731362551502500214230ustar00rootroot00000000000000 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 com.github.googlei18n.ufo2ft.filters name Explode Color Layer Glyphs pre public.glyphOrder space a b c ufo2ft-2.12.2/tests/data/ColorTest.ufo/metainfo.plist000066400000000000000000000004761362551502500224560ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/000077500000000000000000000000001362551502500202425ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/fontinfo.plist000066400000000000000000000016051362551502500231430ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 familyName ColorTest guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV styleName Regular unitsPerEm 1000 xHeight 500 ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/glyphs/000077500000000000000000000000001362551502500215505ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/glyphs/a.color1.glif000066400000000000000000000011731362551502500240330ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/glyphs/a.color2.glif000066400000000000000000000005161362551502500240340ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/glyphs/a.glif000066400000000000000000000005331362551502500226340ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/glyphs/contents.plist000066400000000000000000000005431362551502500244640ustar00rootroot00000000000000 a a.glif a.color1 a.color1.glif a.color2 a.color2.glif ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/glyphs/layerinfo.plist000066400000000000000000000003671362551502500246230ustar00rootroot00000000000000 color 1,0.75,0,0.7 ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/layercontents.plist000066400000000000000000000004331362551502500242110ustar00rootroot00000000000000 foreground glyphs ufo2ft-2.12.2/tests/data/ColorTestRaw.ufo/lib.plist000066400000000000000000000024521362551502500220700ustar00rootroot00000000000000 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-2.12.2/tests/data/ColorTestRaw.ufo/metainfo.plist000066400000000000000000000004761362551502500231300ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/ContourOrderTest.ufo/000077500000000000000000000000001362551502500211375ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/ContourOrderTest.ufo/fontinfo.plist000066400000000000000000000136351362551502500240460ustar00rootroot00000000000000 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-2.12.2/tests/data/ContourOrderTest.ufo/glyphs/000077500000000000000000000000001362551502500224455ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/ContourOrderTest.ufo/glyphs/contents.plist000066400000000000000000000005751362551502500253660ustar00rootroot00000000000000 graphemejoinercomb graphemejoinercomb.glif uniFFFC uniF_F_F_C_.glif xxx xxx.glif ufo2ft-2.12.2/tests/data/ContourOrderTest.ufo/glyphs/graphemejoinercomb.glif000066400000000000000000000245731362551502500271630ustar00rootroot00000000000000 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-2.12.2/tests/data/ContourOrderTest.ufo/glyphs/layerinfo.plist000066400000000000000000000011371362551502500255140ustar00rootroot00000000000000 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-2.12.2/tests/data/ContourOrderTest.ufo/glyphs/uniF_F_F_C_.glif000066400000000000000000000161241362551502500253300ustar00rootroot00000000000000 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-2.12.2/tests/data/ContourOrderTest.ufo/glyphs/xxx.glif000066400000000000000000000006211362551502500241360ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/ContourOrderTest.ufo/layercontents.plist000066400000000000000000000004371362551502500251120ustar00rootroot00000000000000 public.default glyphs ufo2ft-2.12.2/tests/data/ContourOrderTest.ufo/lib.plist000066400000000000000000000012211362551502500227560ustar00rootroot00000000000000 com.github.googlei18n.ufo2ft.filters include uniFFFC graphemejoinercomb name sortContours public.glyphOrder graphemejoinercomb uniFFFC xxx ufo2ft-2.12.2/tests/data/ContourOrderTest.ufo/metainfo.plist000066400000000000000000000004761362551502500240250ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/IncompatibleMasters/000077500000000000000000000000001362551502500210275ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/IncompatibleMasters/IncompatibleMasters.designspace000066400000000000000000000015461362551502500272110ustar00rootroot00000000000000 public.skipExportGlyphs b d ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/000077500000000000000000000000001362551502500240555ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/fontinfo.plist000066400000000000000000000024701362551502500267570ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/000077500000000000000000000000001362551502500253635ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/a.glif000066400000000000000000000007711362551502500264530ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:26:11 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/b.glif000066400000000000000000000044651362551502500264600ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.Export com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:43:17 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/c.glif000066400000000000000000000020031362551502500264430ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 17:23:09 com.schriftgestaltung.componentsAlignment -1 -1 0 -1 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/contents.plist000066400000000000000000000007161362551502500303010ustar00rootroot00000000000000 a a.glif b b.glif c c.glif d d.glif e e.glif f f.glif ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/d.glif000066400000000000000000000016211362551502500264510ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.Export com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:43:08 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/e.glif000066400000000000000000000014031362551502500264500ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 17:23:23 com.schriftgestaltung.componentsAlignment -1 -1 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/f.glif000066400000000000000000000004021362551502500264470ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/glyphs/layerinfo.plist000066400000000000000000000014031362551502500304260ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/groups.plist000066400000000000000000000022001362551502500264430ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/kerning.plist000066400000000000000000000017041362551502500265710ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/layercontents.plist000066400000000000000000000004371362551502500300300ustar00rootroot00000000000000 public.default glyphs ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/lib.plist000066400000000000000000000045171362551502500257070ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Bold.ufo/metainfo.plist000066400000000000000000000004761362551502500267430ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/000077500000000000000000000000001362551502500245765ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/fontinfo.plist000066400000000000000000000024731362551502500275030ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/000077500000000000000000000000001362551502500261045ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/a.glif000066400000000000000000000007751362551502500272000ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:26:11 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/b.glif000066400000000000000000000030321362551502500271660ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.Export com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:43:17 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/c.glif000066400000000000000000000017111362551502500271710ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 17:23:09 com.schriftgestaltung.componentsAlignment -1 -1 0 -1 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/contents.plist000066400000000000000000000007161362551502500310220ustar00rootroot00000000000000 a a.glif b b.glif c c.glif d d.glif e e.glif f f.glif ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/d.glif000066400000000000000000000016211362551502500271720ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.Export com.schriftgestaltung.Glyphs.lastChange 2019/03/06 11:43:08 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/e.glif000066400000000000000000000014051362551502500271730ustar00rootroot00000000000000 com.schriftgestaltung.Glyphs.lastChange 2019/03/06 17:23:23 com.schriftgestaltung.componentsAlignment -1 -1 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/f.glif000066400000000000000000000004741362551502500272010ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/glyphs/layerinfo.plist000066400000000000000000000014031362551502500311470ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/groups.plist000066400000000000000000000022001362551502500271640ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/kerning.plist000066400000000000000000000017041362551502500273120ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/layercontents.plist000066400000000000000000000004371362551502500305510ustar00rootroot00000000000000 public.default glyphs ufo2ft-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/lib.plist000066400000000000000000000043071362551502500264250ustar00rootroot00000000000000 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-2.12.2/tests/data/IncompatibleMasters/NewFont-Regular.ufo/metainfo.plist000066400000000000000000000004761362551502500274640ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/000077500000000000000000000000001362551502500204335ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/features.fea000066400000000000000000000000521362551502500227230ustar00rootroot00000000000000feature liga { sub a e s s by s; } liga; ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/fontinfo.plist000066400000000000000000000033551362551502500233400ustar00rootroot00000000000000 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-2.12.2/tests/data/LayerFont-Bold.ufo/glyphs/000077500000000000000000000000001362551502500217415ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/glyphs/a.glif000066400000000000000000000020271362551502500230250ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/glyphs/contents.plist000066400000000000000000000007071362551502500246570ustar00rootroot00000000000000 a a.glif dotabovecomb dotabovecomb.glif e e.glif edotabove edotabove.glif s s.glif ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/glyphs/dotabovecomb.glif000066400000000000000000000006061362551502500252520ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/glyphs/e.glif000066400000000000000000000015261362551502500230340ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/glyphs/edotabove.glif000066400000000000000000000004251362551502500245550ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/glyphs/s.glif000066400000000000000000000013421362551502500230460ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/layercontents.plist000066400000000000000000000004371362551502500244060ustar00rootroot00000000000000 public.default glyphs ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/lib.plist000066400000000000000000000006121362551502500222550ustar00rootroot00000000000000 public.glyphOrder a e s dotabovecomb edotabove ufo2ft-2.12.2/tests/data/LayerFont-Bold.ufo/metainfo.plist000066400000000000000000000004761362551502500233210ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/000077500000000000000000000000001362551502500211545ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/features.fea000066400000000000000000000000521362551502500234440ustar00rootroot00000000000000feature liga { sub a e s s by s; } liga; ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/fontinfo.plist000066400000000000000000000031431362551502500240540ustar00rootroot00000000000000 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-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs.M_edium/000077500000000000000000000000001362551502500240405ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs.M_edium/contents.plist000066400000000000000000000003551362551502500267550ustar00rootroot00000000000000 e e.glif ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs.M_edium/e.glif000066400000000000000000000015001362551502500251230ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs/000077500000000000000000000000001362551502500224625ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs/a.glif000066400000000000000000000020241362551502500235430ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs/contents.plist000066400000000000000000000007071362551502500254000ustar00rootroot00000000000000 a a.glif dotabovecomb dotabovecomb.glif e e.glif edotabove edotabove.glif s s.glif ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs/dotabovecomb.glif000066400000000000000000000005661362551502500260000ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs/e.glif000066400000000000000000000014721362551502500235550ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs/edotabove.glif000066400000000000000000000004251362551502500252760ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/glyphs/s.glif000066400000000000000000000013421362551502500235670ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/layercontents.plist000066400000000000000000000005741362551502500251310ustar00rootroot00000000000000 public.default glyphs Medium glyphs.M_edium ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/lib.plist000066400000000000000000000006121362551502500227760ustar00rootroot00000000000000 public.glyphOrder a e s dotabovecomb edotabove ufo2ft-2.12.2/tests/data/LayerFont-Regular.ufo/metainfo.plist000066400000000000000000000004761362551502500240420ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/MTIFeatures.ttx000066400000000000000000000023311362551502500177520ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/000077500000000000000000000000001362551502500200025ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/data/000077500000000000000000000000001362551502500207135ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/data/com.github.googlei18n.ufo2ft.mtiFeatures/000077500000000000000000000000001362551502500304205ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/data/com.github.googlei18n.ufo2ft.mtiFeatures/GSUB.mti000066400000000000000000000003421362551502500316720ustar00rootroot00000000000000FontDame 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-2.12.2/tests/data/MTIFeatures.ufo/fontinfo.plist000066400000000000000000000013011362551502500226740ustar00rootroot00000000000000 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-2.12.2/tests/data/MTIFeatures.ufo/glyphs/000077500000000000000000000000001362551502500213105ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/glyphs/_notdef.glif000066400000000000000000000007531362551502500235760ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/glyphs/a.glif000066400000000000000000000004361362551502500223760ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/glyphs/b.glif000066400000000000000000000005111362551502500223710ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/glyphs/c.glif000066400000000000000000000006241362551502500223770ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/glyphs/contents.plist000066400000000000000000000006371362551502500242300ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif space space.glif ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/glyphs/space.glif000066400000000000000000000001771362551502500232530ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/groups.plist000066400000000000000000000002761362551502500224030ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/kerning.plist000066400000000000000000000006031362551502500225130ustar00rootroot00000000000000 a a -666 b -666 b a -666 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/layercontents.plist000066400000000000000000000004231362551502500237500ustar00rootroot00000000000000 public.default glyphs ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/lib.plist000066400000000000000000000011251362551502500216240ustar00rootroot00000000000000 public.glyphOrder .notdef space a b c public.postscriptNames a uni0061 b uni0062 c uni0063 space uni0020 ufo2ft-2.12.2/tests/data/MTIFeatures.ufo/metainfo.plist000066400000000000000000000003601362551502500226600ustar00rootroot00000000000000 formatVersion 3 ufo2ft-2.12.2/tests/data/TestFont-CFF.ttx000066400000000000000000000415251362551502500177740ustar00rootroot00000000000000 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) 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-2.12.2/tests/data/TestFont-NoOptimize-CFF.ttx000066400000000000000000000424641362551502500220720ustar00rootroot00000000000000 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) 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-2.12.2/tests/data/TestFont-NoOverlaps-CFF-pathops.ttx000066400000000000000000000416771362551502500235460ustar00rootroot00000000000000 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) 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 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 -106 callsubr -12 -107 callsubr endchar 10 -107 callsubr -85 -288 rmoveto -43 -147 -43 147 rlineto 1 15 16 0 17 hhcurveto 13 13 0 -1 12 hvcurveto endchar -12 66 hmoveto 256 hlineto -128 510 rlineto endchar 10 100 505 rmoveto -510 210 510 vlineto -99 152 -106 callsubr 200 -55 -80 rmoveto 509 hlineto 123 -34 55 -127 16 vhcurveto -99 396 -100 -397 rlineto -117 -18 -32 -56 -119 vvcurveto endchar 200 -55 -80 rmoveto 199 hlineto 50 -200 50 200 rlineto 210 hlineto 123 -34 55 -127 16 vhcurveto 29 116 -256 0 29 -117 rlineto -118 -18 -32 -55 -120 vvcurveto endchar 200 66 hmoveto 356 hlineto -128 510 -50 -199 -50 199 rlineto endchar 200 334 hmoveto 88 hlineto -128 510 -44 -175 -44 175 -128 -510 rlineto endchar 0001beef ufo2ft-2.12.2/tests/data/TestFont-NoOverlaps-CFF.ttx000066400000000000000000000416761362551502500220710ustar00rootroot00000000000000 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) -55 23 rmoveto 509 hlineto 140 -44 53 -173 6 vhcurveto 85 288 -256 0 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 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 -106 callsubr endchar -12 -107 callsubr endchar 10 -107 callsubr 254 200 rmoveto 13 13 0 -1 12 hvcurveto -43 -147 -43 147 rlineto 1 15 16 0 17 hhcurveto endchar -12 66 hmoveto 256 hlineto -128 510 rlineto endchar 10 211 657 -106 callsubr -111 -152 rmoveto -510 210 510 vlineto endchar 200 -55 -80 rmoveto 509 hlineto 123 -34 55 -127 16 vhcurveto -99 396 -100 -397 rlineto -117 -18 -32 -56 -119 vvcurveto endchar 200 -55 -80 rmoveto 199 hlineto 50 -200 50 200 rlineto 210 hlineto 123 -34 55 -127 16 vhcurveto 29 116 -256 0 29 -117 rlineto -118 -18 -32 -55 -120 vvcurveto endchar 200 66 hmoveto 356 hlineto -128 510 -50 -199 -50 199 rlineto endchar 200 294 510 rmoveto -44 -175 -44 175 -128 -510 rlineto 344 hlineto endchar 0001beef ufo2ft-2.12.2/tests/data/TestFont-NoOverlaps-TTF-pathops.ttx000066400000000000000000000470531362551502500235770ustar00rootroot00000000000000 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) 0001beef ufo2ft-2.12.2/tests/data/TestFont-NoOverlaps-TTF.ttx000066400000000000000000000470271362551502500221240ustar00rootroot00000000000000 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) 0001beef ufo2ft-2.12.2/tests/data/TestFont-Specialized-CFF.ttx000066400000000000000000000414161362551502500222250ustar00rootroot00000000000000 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) 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-2.12.2/tests/data/TestFont.ttx000066400000000000000000000465121362551502500174010ustar00rootroot00000000000000 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) 0001beef ufo2ft-2.12.2/tests/data/TestFont.ufo/000077500000000000000000000000001362551502500174205ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/TestFont.ufo/data/000077500000000000000000000000001362551502500203315ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/TestFont.ufo/data/com.github.fonttools.ttx/000077500000000000000000000000001362551502500252345ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/TestFont.ufo/data/com.github.fonttools.ttx/CUST.ttx000066400000000000000000000002711362551502500265530ustar00rootroot00000000000000 0001beef ufo2ft-2.12.2/tests/data/TestFont.ufo/features.fea000066400000000000000000000000001362551502500217010ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/TestFont.ufo/fontinfo.plist000066400000000000000000000215341362551502500223240ustar00rootroot00000000000000 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-2.12.2/tests/data/TestFont.ufo/glyphs/000077500000000000000000000000001362551502500207265ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/_notdef.glif000066400000000000000000000007711362551502500232140ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/a.glif000066400000000000000000000004531362551502500220130ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/b.glif000066400000000000000000000005261362551502500220150ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/c.glif000066400000000000000000000006411362551502500220140ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/contents.plist000066400000000000000000000014201362551502500236350ustar00rootroot00000000000000 .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-2.12.2/tests/data/TestFont.ufo/glyphs/d.glif000066400000000000000000000011621362551502500220140ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/e.glif000066400000000000000000000010561362551502500220170ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/f.glif000066400000000000000000000010561362551502500220200ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/g.glif000066400000000000000000000002671362551502500220240ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/h.glif000066400000000000000000000003531362551502500220210ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/i.glif000066400000000000000000000006671362551502500220320ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/j.glif000066400000000000000000000007221362551502500220230ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/k.glif000066400000000000000000000003361362551502500220250ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/l.glif000066400000000000000000000003701362551502500220240ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/glyphs/space.glif000066400000000000000000000002141362551502500226610ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/TestFont.ufo/kerning.plist000066400000000000000000000006511362551502500221340ustar00rootroot00000000000000 a a 5 b -10 space 1 b a -7 ufo2ft-2.12.2/tests/data/TestFont.ufo/layercontents.plist000066400000000000000000000004231362551502500233660ustar00rootroot00000000000000 public.default glyphs ufo2ft-2.12.2/tests/data/TestFont.ufo/lib.plist000066400000000000000000000023351362551502500212460ustar00rootroot00000000000000 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-2.12.2/tests/data/TestFont.ufo/metainfo.plist000066400000000000000000000004531362551502500223010ustar00rootroot00000000000000 creator org.robofab.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/data/TestVariableFont-CFF2-useProductionNames.ttx000066400000000000000000000430151362551502500253450ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight 50 -250 rmoveto 400 1000 -400 -1000 hlineto 50 50 rmoveto 900 300 -900 -300 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 127 228 70 1 2 blend rmoveto 449 -2 -45 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -27 36 -29 -34 31 -1 2 -45 13 100 10 blend -280 -54 -82 188 170 153 163 -124 -355 55 -54 -86 -57 -60 57 146 7 blend 6 rlineto 167 395 -84 118 2 blend rmoveto -16 -94 78 -2 9 88 -19 -48 44 7 -4 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-2.12.2/tests/data/TestVariableFont-CFF2.ttx000066400000000000000000000425251362551502500215250ustar00rootroot00000000000000 Weight Layer Font Regular 0.000;NONE;LayerFont-Regular Layer Font Regular Version 0.000 LayerFont-Regular Weight 50 -250 rmoveto 400 1000 -400 -1000 hlineto 50 50 rmoveto 900 300 -900 -300 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 127 228 70 1 2 blend rmoveto 449 -2 -45 -2 2 blend -5 79 -255 208 -276 -252 148 -279 338 63 -17 84 -27 36 -29 -34 31 -1 2 -45 13 100 10 blend -280 -54 -82 188 170 153 163 -124 -355 55 -54 -86 -57 -60 57 146 7 blend 6 rlineto 167 395 -84 118 2 blend rmoveto -16 -94 78 -2 9 88 -19 -48 44 7 -4 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-2.12.2/tests/data/TestVariableFont-TTF-useProductionNames.ttx000066400000000000000000000522731362551502500253300ustar00rootroot00000000000000 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-2.12.2/tests/data/TestVariableFont-TTF.ttx000066400000000000000000000517671362551502500215120ustar00rootroot00000000000000 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-2.12.2/tests/data/UseMyMetrics.ufo/000077500000000000000000000000001362551502500202435ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/UseMyMetrics.ufo/fontinfo.plist000066400000000000000000000023521362551502500231440ustar00rootroot00000000000000 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-2.12.2/tests/data/UseMyMetrics.ufo/glyphs/000077500000000000000000000000001362551502500215515ustar00rootroot00000000000000ufo2ft-2.12.2/tests/data/UseMyMetrics.ufo/glyphs/I_.glif000066400000000000000000000005351362551502500227460ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/UseMyMetrics.ufo/glyphs/I_acute.glif000066400000000000000000000003561362551502500237710ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/UseMyMetrics.ufo/glyphs/acute.glif000066400000000000000000000005471362551502500235230ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/UseMyMetrics.ufo/glyphs/contents.plist000066400000000000000000000005761362551502500244730ustar00rootroot00000000000000 I I_.glif Iacute I_acute.glif acute acute.glif romanthree romanthree.glif ufo2ft-2.12.2/tests/data/UseMyMetrics.ufo/glyphs/romanthree.glif000066400000000000000000000004131362551502500245560ustar00rootroot00000000000000 ufo2ft-2.12.2/tests/data/UseMyMetrics.ufo/layercontents.plist000066400000000000000000000004111362551502500242060ustar00rootroot00000000000000 foreground glyphs ufo2ft-2.12.2/tests/data/UseMyMetrics.ufo/metainfo.plist000066400000000000000000000004451362551502500231250ustar00rootroot00000000000000 creator org.robofab.ufoLib formatVersion 3 ufo2ft-2.12.2/tests/featureCompiler_test.py000066400000000000000000000166131362551502500207170ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from textwrap import dedent import logging import re from fontTools import ttLib from fontTools.feaLib.error import IncludedFeaNotFound, FeatureLibError from ufo2ft.featureWriters import ( BaseFeatureWriter, KernFeatureWriter, FEATURE_WRITERS_KEY, ast, ) from ufo2ft.featureCompiler import FeatureCompiler, parseLayoutFeatures, logger import py import pytest from .testSupport import pushd class ParseLayoutFeaturesTest(object): 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): ufo = FontClass() ufo.features.text = dedent( """\ include(test.fea) """ ) with pushd(str(tmpdir)): 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 class FeatureCompilerTest(object): 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_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( "a", "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) ufo2ft-2.12.2/tests/featureWriters/000077500000000000000000000000001362551502500171645ustar00rootroot00000000000000ufo2ft-2.12.2/tests/featureWriters/__init__.py000066400000000000000000000013711362551502500212770ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from ufo2ft.featureCompiler import parseLayoutFeatures from ufo2ft.featureWriters import ast class FeatureWriterTest(object): # subclasses must override this FeatureWriter = None @classmethod def writeFeatures(cls, ufo, **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) n = len(feaFile.statements) if writer.write(ufo, feaFile): new = ast.FeatureFile() new.statements = feaFile.statements[n:] return new ufo2ft-2.12.2/tests/featureWriters/featureWriters_test.py000066400000000000000000000047511362551502500236170ustar00rootroot00000000000000from __future__ import print_function, absolute_import, division, unicode_literals from ufo2ft.featureWriters import ( BaseFeatureWriter, FEATURE_WRITERS_KEY, loadFeatureWriters, loadFeatureWriterFromString, ) try: from plistlib import loads, FMT_XML 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( """ com.github.googlei18n.ufo2ft.featureWriters class KernFeatureWriter options mode skip """.encode( "utf-8" ) ) 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-2.12.2/tests/featureWriters/kernFeatureWriter_test.py000066400000000000000000000716601362551502500242570ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from textwrap import dedent import logging from ufo2ft.featureCompiler import parseLayoutFeatures from ufo2ft.featureWriters import KernFeatureWriter, ast import pytest 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.A" assert classDefs[1].name == "kern2.B" assert getGlyphs(classDefs[0]) == ["A", "Aacute", "Acircumflex"] assert getGlyphs(classDefs[1]) == ["B", "E", "F"] lookups = getLookups(feaFile) assert len(lookups) == 1 kern_ltr = lookups[0] assert kern_ltr.name == "kern_ltr" rules = getPairPosRules(kern_ltr) assert len(rules) == 1 assert str(rules[0]) == "pos @kern1.A @kern2.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_ltr { lookupflag IgnoreMarks; pos four six -55; pos one six -30; } kern_ltr; feature kern { lookup kern_ltr; } kern; """ ) writer = KernFeatureWriter(ignoreMarks=False) feaFile = ast.FeatureFile() assert writer.write(font, feaFile) assert str(feaFile) == dedent( """\ lookup kern_ltr { pos four six -55; pos one six -30; } kern_ltr; feature kern { lookup kern_ltr; } kern; """ ) def test_mark_to_base_kern(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, ("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 str(feaFile) == dedent( """ lookup kern_ltr { lookupflag IgnoreMarks; pos B C -30; } kern_ltr; lookup kern_ltr_marks { pos A acutecomb -55; } kern_ltr_marks; feature kern { lookup kern_ltr; lookup kern_ltr_marks; } kern; """ ) feaFile = self.writeFeatures(font, ignoreMarks=False) assert str(feaFile) == dedent( """ lookup kern_ltr { pos A acutecomb -55; pos B C -30; } kern_ltr; feature kern { lookup kern_ltr; } 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_ltr { lookupflag IgnoreMarks; pos seven six 25; } kern_ltr; feature kern { lookup kern_ltr; } 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_arabic_numerals(self, FontClass): """ Test that arabic numerals (with bidi type AN) are kerned LTR. https://github.com/googlei18n/ufo2ft/issues/198 https://github.com/googlei18n/ufo2ft/pull/200 """ 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}) ufo.features.text = dedent( """ languagesystem DFLT dflt; languagesystem arab dflt; """ ) generated = self.writeFeatures(ufo) assert str(generated) == dedent( """ lookup kern_rtl { lookupflag IgnoreMarks; pos four-ar seven-ar -30; } kern_rtl; feature kern { lookup kern_rtl; } kern; """ ) def test__groupScriptsByTagAndDirection(self, FontClass): font = FontClass() font.features.text = dedent( """ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem latn TRK; languagesystem arab dflt; languagesystem arab URD; languagesystem deva dflt; languagesystem dev2 dflt; """ ) feaFile = parseLayoutFeatures(font) scripts = ast.getScriptLanguageSystems(feaFile) scriptGroups = KernFeatureWriter._groupScriptsByTagAndDirection(scripts) assert "kern" in scriptGroups assert list(scriptGroups["kern"]["LTR"]) == [("latn", ["dflt", "TRK "])] assert list(scriptGroups["kern"]["RTL"]) == [("arab", ["dflt", "URD "])] assert "dist" in scriptGroups assert list(scriptGroups["dist"]["LTR"]) == [ ("deva", ["dflt"]), ("dev2", ["dflt"]), ] def test_getKerningClasses(self, FontClass): font = FontClass() for i in range(65, 65 + 6): # A..F font.newGlyph(chr(i)) font.groups.update({"public.kern1.A": ["A", "B"], "public.kern2.C": ["C", "D"]}) # simulate a name clash between pre-existing class definitions in # feature file, and those generated by the feature writer font.features.text = "@kern1.A = [E F];" writer = KernFeatureWriter() feaFile = parseLayoutFeatures(font) side1Classes, side2Classes = KernFeatureWriter.getKerningClasses(font, feaFile) assert "public.kern1.A" in side1Classes # the new class gets a unique name assert side1Classes["public.kern1.A"].name == "kern1.A_1" assert getGlyphs(side1Classes["public.kern1.A"]) == ["A", "B"] assert "public.kern2.C" in side2Classes assert side2Classes["public.kern2.C"].name == "kern2.C" assert getGlyphs(side2Classes["public.kern2.C"]) == ["C", "D"] def test_correct_invalid_class_names(self, FontClass): font = FontClass() for i in range(65, 65 + 12): # A..L font.newGlyph(chr(i)) font.groups.update( { "public.kern1.foo$": ["A", "B", "C"], "public.kern1.foo@": ["D", "E", "F"], "@public.kern2.bar": ["G", "H", "I"], "public.kern2.bar&": ["J", "K", "L"], } ) font.kerning.update( { ("public.kern1.foo$", "@public.kern2.bar"): 10, ("public.kern1.foo@", "public.kern2.bar&"): -10, } ) side1Classes, side2Classes = KernFeatureWriter.getKerningClasses(font) assert side1Classes["public.kern1.foo$"].name == "kern1.foo" assert side1Classes["public.kern1.foo@"].name == "kern1.foo_1" # no valid 'public.kern{1,2}.' prefix, skipped assert "@public.kern2.bar" not in side2Classes assert side2Classes["public.kern2.bar&"].name == "kern2.bar" def test_getKerningPairs(self, FontClass): font = FontClass() for i in range(65, 65 + 8): # A..H font.newGlyph(chr(i)) font.groups.update( { "public.kern1.foo": ["A", "B"], "public.kern2.bar": ["C", "D"], "public.kern1.baz": ["E", "F"], "public.kern2.nul": ["G", "H"], } ) font.kerning.update( { ("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, } ) s1c, s2c = KernFeatureWriter.getKerningClasses(font) pairs = KernFeatureWriter.getKerningPairs(font, s1c, s2c) assert len(pairs) == 5 assert "G H -5" in repr(pairs[0]) assert (pairs[0].firstIsClass, pairs[0].secondIsClass) == (False, False) assert pairs[0].glyphs == {"G", "H"} assert "A @kern2.bar 5" in repr(pairs[1]) assert (pairs[1].firstIsClass, pairs[1].secondIsClass) == (False, True) assert pairs[1].glyphs == {"A", "C", "D"} assert "@kern1.foo D 15" in repr(pairs[2]) assert (pairs[2].firstIsClass, pairs[2].secondIsClass) == (True, False) assert pairs[2].glyphs == {"A", "B", "D"} assert "@kern1.baz @kern2.bar -10" in repr(pairs[3]) assert (pairs[3].firstIsClass, pairs[3].secondIsClass) == (True, True) assert pairs[3].glyphs == {"C", "D", "E", "F"} assert "@kern1.foo @kern2.bar 10" in repr(pairs[4]) assert (pairs[4].firstIsClass, pairs[4].secondIsClass) == (True, True) assert pairs[4].glyphs == {"A", "B", "C", "D"} 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; """ ) ufo = makeUFO(FontClass, glyphs, groups, kerning, features) newFeatures = self.writeFeatures(ufo, ignoreMarks=False) assert str(newFeatures) == dedent( """\ @kern1.A = [A Aacute]; @kern1.reh = [reh-ar zain-ar reh-ar.fina]; @kern2.alef = [alef-ar alef-ar.isol]; lookup kern_dflt { pos seven four -25; } kern_dflt; lookup kern_ltr { enum pos @kern1.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.reh @kern2.alef <-100 0 -100 0>; } kern_rtl; feature kern { lookup kern_dflt; script latn; language dflt; lookup kern_ltr; language TRK; script arab; language dflt; lookup kern_rtl; language URD; } 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; @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 str(newFeatures) == dedent( """\ @kern1.A = [A Aacute]; @kern1.reh = [reh-ar zain-ar reh-ar.fina]; @kern2.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.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.reh @kern2.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 { lookup kern_dflt; script latn; language dflt; lookup kern_ltr; lookup kern_ltr_marks; language TRK; script arab; language dflt; lookup kern_rtl; lookup kern_rtl_marks; language URD; } 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; @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 str(newFeatures) == dedent( """\ @kern1.reh = [reh-ar zain-ar reh-ar.fina]; @kern2.alef = [alef-ar alef-ar.isol]; lookup kern_rtl { lookupflag IgnoreMarks; pos reh-ar.fina lam-ar.init <-80 0 -80 0>; pos @kern1.reh @kern2.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 { lookup kern_rtl; lookup kern_rtl_marks; } kern; """ ) def test_kern_LTR_and_RTL_one_uses_DFLT(self, FontClass): glyphs = {"A": 0x41, "V": 0x56, "reh-ar": 0x631, "alef-ar": 0x627} kerning = {("A", "V"): -40, ("reh-ar", "alef-ar"): -100} features = "languagesystem latn dflt;" ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) generated = self.writeFeatures(ufo) assert str(generated) == dedent( """ 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_rtl; script latn; language dflt; lookup kern_ltr; } kern; """ ) features = dedent("languagesystem arab dflt;") ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features) generated = self.writeFeatures(ufo) assert str(generated) == dedent( """ 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; } kern; """ ) def test_kern_LTR_and_RTL_cannot_use_DFLT(self, FontClass): glyphs = {"A": 0x41, "V": 0x56, "reh-ar": 0x631, "alef-ar": 0x627} kerning = {("A", "V"): -40, ("reh-ar", "alef-ar"): -100} ufo = makeUFO(FontClass, glyphs, kerning=kerning) with pytest.raises(ValueError, match="cannot use DFLT script"): self.writeFeatures(ufo) 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 str(generated) == dedent( """\ @kern1.KND_aaMatra_R = [aaMatra_kannada]; @kern2.KND_ailength_L = [aaMatra_kannada]; lookup kern_ltr { lookupflag IgnoreMarks; pos @kern1.KND_aaMatra_R @kern2.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 knda; language dflt; lookup kern_ltr; script knd2; language dflt; lookup kern_ltr; } 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_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; """ ) 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 str(generated) == dedent( """\ @kern1.KND_aaMatra_R = [aaMatra_kannada]; @kern2.KND_ailength_L = [aaMatra_kannada]; lookup kern_ltr { lookupflag IgnoreMarks; pos @kern1.KND_aaMatra_R @kern2.KND_ailength_L 34; } kern_ltr; lookup kern_rtl { lookupflag IgnoreMarks; pos u10A1E u10A06 <117 0 117 0>; } kern_rtl; feature dist { script knda; language dflt; lookup kern_ltr; script knd2; language dflt; lookup kern_ltr; script khar; language dflt; lookup kern_rtl; } dist; """ ) def test_skip_ambiguous_direction_pair(self, FontClass, caplog): caplog.set_level(logging.ERROR) 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; """ ) logger = "ufo2ft.featureWriters.kernFeatureWriter.KernFeatureWriter" with caplog.at_level(logging.WARNING, logger=logger): generated = self.writeFeatures(ufo) assert not generated assert len(caplog.records) == 5 assert "skipped kern pair with ambiguous direction" in caplog.text 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 str(generated) == dedent( """ 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 { lookup kern_dflt; lookup kern_rtl; } kern; """ ) if __name__ == "__main__": import sys sys.exit(pytest.main(sys.argv)) ufo2ft-2.12.2/tests/featureWriters/markFeatureWriter_test.py000066400000000000000000000570701362551502500242510ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from textwrap import dedent import logging from ufo2ft.featureWriters import ast from ufo2ft.featureWriters.markFeatureWriter import ( MarkFeatureWriter, NamedAnchor, parseAnchorName, ) from ufo2ft.featureCompiler import parseLayoutFeatures import pytest 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)), ("top_", (False, "top_", None)), ("top1", (False, "top1", None)), ("_bottom", (True, "bottom", None)), ("bottom_2", (False, "bottom", 2)), ("top_right_1", (False, "top_right", 1)), ], ) def test_parseAnchorName(input_expected): anchorName, (isMark, key, number) = input_expected assert parseAnchorName(anchorName) == (isMark, key, number) 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(): import sys if sys.version_info >= (3,): expected = "NamedAnchor(name='top', x=1.0, y=2.0)" else: expected = "NamedAnchor(name=u'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): fea = 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 "ligComponent " in 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_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() 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; """ ) def test_abvm_blwm_features(self, FontClass): 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) assert str(generated) == 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; """ ) def test_all_features(self, testufo): ufo = testufo ufo.info.unitsPerEm = 1000 cedilla = ufo.newGlyph("cedillacomb").anchors = [ {"name": "_bottom", "x": 10, "y": -5}, {"name": "bottom", "x": 20, "y": -309}, ] c = 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", ] generated = self.writeFeatures(testufo) assert str(generated) == 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_top; pos ligature bar_foo ligComponent mark @MC_top; } abvm_mark2liga; 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 mark @MC_bar; } blwm_mark2liga; lookup blwm_mark2mark_bar { @MFS_blwm_mark2mark_bar = [barcomb]; lookupflag UseMarkFilteringSet @MFS_blwm_mark2mark_bar; pos mark barcomb mark @MC_bar; } blwm_mark2mark_bar; 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; """ ) 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 lised 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, but only one will be # used to define its markClass. Following Glyphs.app, the anchors # '_bottom' and '_top' get priority over others. 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 acutecomb @MC_top; markClass dotaccentcomb @MC_top; markClass tildecomb @MC_top; feature mark { lookup mark2base { pos base D mark @MC_top; 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 dotaccentcomb tildecomb]; lookupflag UseMarkFilteringSet @MFS_mark2mark_top; pos mark dotaccentcomb mark @MC_top; pos mark tildecomb mark @MC_top; } mark2mark_top; } mkmk; """ ) if __name__ == "__main__": import sys sys.exit(pytest.main(sys.argv)) ufo2ft-2.12.2/tests/filters/000077500000000000000000000000001362551502500156215ustar00rootroot00000000000000ufo2ft-2.12.2/tests/filters/__init__.py000066400000000000000000000000001362551502500177200ustar00rootroot00000000000000ufo2ft-2.12.2/tests/filters/decomposeComponents_test.py000066400000000000000000000034711362551502500232630ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from ufo2ft.filters.decomposeComponents import DecomposeComponentsFilter from ufo2ft.util import logger import logging def test_missing_component_is_dropped(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 with caplog.at_level(logging.WARNING, logger=logger.name): filter_ = DecomposeComponentsFilter() assert filter_(ufo) assert len(ufo["aacute"]) == 1 assert len(ufo["aacute"].components) == 0 assert len(caplog.records) == 1 assert "dropping non-existent component" in caplog.text 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 ufo2ft-2.12.2/tests/filters/filters_test.py000066400000000000000000000131341362551502500207040ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import from ufo2ft.filters import ( getFilterClass, BaseFilter, loadFilters, UFO2FT_FILTERS_KEY, logger, ) from fontTools.misc.py23 import SimpleNamespace from fontTools.misc.loggingTools import CapturingLogHandler import pytest 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 _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 UFO2FT_FILTERS_KEY not in ufo.lib assert loadFilters(ufo) == ([], []) @pytest.fixture def ufo(): ufo = MockFont(lib={}) ufo.lib[UFO2FT_FILTERS_KEY] = [{"name": "Foo Bar", "args": ["foo", "bar"]}] return ufo def test_loadFilters_pre(ufo): ufo.lib[UFO2FT_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[UFO2FT_FILTERS_KEY][0]["name"] = "Self Destruct" ufo.lib[UFO2FT_FILTERS_KEY][0]["namespace"] = "my_dangerous_filters" class SelfDestructFilter(FooBarFilter): def filter(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[UFO2FT_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[UFO2FT_FILTERS_KEY][0]["args"].append("baz") with pytest.raises(TypeError) as exc_info: loadFilters(ufo) assert exc_info.match("unsupported") 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[UFO2FT_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[UFO2FT_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[UFO2FT_FILTERS_KEY][0]["include"] = ["a", "b"] ufo.lib[UFO2FT_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[UFO2FT_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[UFO2FT_FILTERS_KEY][0]["kwargs"] = {} ufo.lib[UFO2FT_FILTERS_KEY][0]["kwargs"]["c"] = 1 ufo.lib[UFO2FT_FILTERS_KEY][0]["kwargs"]["d"] = 2 # unknown with pytest.raises(TypeError) as exc_info: loadFilters(ufo) assert exc_info.match("got an unsupported keyword") 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',))" ) f = lambda g: False assert repr( FooBarFilter("g", "h", include=f) ) == "FooBarFilter('g', 'h', c=0, include={})".format(repr(f)) if __name__ == "__main__": sys.exit(pytest.main(sys.argv)) ufo2ft-2.12.2/tests/filters/flattenComponents_test.py000066400000000000000000000064061362551502500227430ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import from ufo2ft.filters.flattenComponents import FlattenComponentsFilter, logger from fontTools.misc.loggingTools import CapturingLogHandler import pytest @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", ()), ], }, { "name": "b", "width": 350, "outline": [("addComponent", ("a", (1, 0, 0, 1, 0, 0)))], }, { "name": "c", "width": 350, "outline": [("addComponent", ("b", (1, 0, 0, 1, 0, 0)))], }, { "name": "d", "width": 700, "outline": [ ("addComponent", ("a", (1, 0, 0, 1, 0, 0))), ("addComponent", ("b", (1, 0, 0, 1, 350, 0))), ("addComponent", ("c", (1, 0, 0, 1, 700, 0))), ], }, ] } ] ) 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(object): def test_empty_glyph(self, font): philter = FlattenComponentsFilter(include={"space"}) assert not philter(font) def test_contour_glyph(self, font): philter = FlattenComponentsFilter(include={"a"}) assert not philter(font) def test_component_glyph(self, font): philter = FlattenComponentsFilter(include={"b"}) assert not philter(font) def test_nested_components_glyph(self, font): philter = FlattenComponentsFilter(include={"c"}) modified = philter(font) assert modified == set(["c"]) assert [(c.baseGlyph, c.transformation) for c in font["c"].components] == [ ("a", (1, 0, 0, 1, 0, 0)) ] def test_whole_font(self, font): philter = FlattenComponentsFilter() modified = philter(font) assert modified == set(["c", "d"]) assert [(c.baseGlyph, c.transformation) for c in font["c"].components] == [ ("a", (1, 0, 0, 1, 0, 0)) ] assert [(c.baseGlyph, c.transformation) for c in font["d"].components] == [ ("a", (1, 0, 0, 1, 0, 0)), ("a", (1, 0, 0, 1, 350, 0)), ("a", (1, 0, 0, 1, 700, 0)), ] def test_logger(self, font): with CapturingLogHandler(logger, level="INFO") as captor: philter = FlattenComponentsFilter() modified = philter(font) captor.assertRegex("Flattened composite glyphs: 2") ufo2ft-2.12.2/tests/filters/propagateAnchors_test.py000066400000000000000000000220221362551502500225300ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import import ufo2ft.filters from ufo2ft.filters.propagateAnchors import PropagateAnchorsFilter, logger from fontTools.misc.loggingTools import CapturingLogHandler import pytest @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))), ], }, ] } ] ) 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)) return font class PropagateAnchorsFilterTest(object): 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) def test_single_component_glyph(self, font): philter = PropagateAnchorsFilter(include={"a-cyr"}) assert philter(font) == {"a-cyr"} assert [(a.name, a.x, a.y) for a in font["a-cyr"].anchors] == [ ("bottom", 175, 0), ("top", 175, 300), ] def test_two_component_glyph(self, font): name = "adieresis" philter = PropagateAnchorsFilter(include={name}) assert philter(font) == {name} assert [(a.name, a.x, a.y) for a in font[name].anchors] == [ ("bottom", 175, 0), ("top", 175, 480), ] def test_one_anchor_two_component_glyph(self, font): name = "amacron" philter = PropagateAnchorsFilter(include={name}) assert philter(font) == {name} assert [(a.name, a.x, a.y) for a in font[name].anchors] == [ ("top", 176, 481), ("bottom", 175, 0), ] def test_three_component_glyph(self, font): name = "adieresismacron" philter = PropagateAnchorsFilter(include={name}) assert philter(font) == {name} assert [(a.name, a.x, a.y) for a in font[name].anchors] == [ ("bottom", 175, 0), ("top", 175, 660), ] def test_nested_component_glyph(self, font): name = "amacrondieresis" philter = PropagateAnchorsFilter(include={name}) assert philter(font) == {name} assert [(a.name, a.x, a.y) for a in font[name].anchors] == [ ("bottom", 175, 0), ("top", 175, 660), ] def test_ligature_glyph(self, font): name = "a_a" philter = PropagateAnchorsFilter(include={name}) assert philter(font) == {name} assert [(a.name, a.x, a.y) for a in font[name].anchors] == [ ("bottom_1", 175, 0), ("bottom_2", 525, 0), ("top_1", 175, 300), ("top_2", 525, 300), ] def test_whole_font(self, font): philter = PropagateAnchorsFilter() modified = philter(font) assert modified == set( [ "a-cyr", "amacron", "adieresis", "adieresismacron", "amacrondieresis", "a_a", ] ) 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 ufo2ft-2.12.2/tests/filters/sortContours_test.py000066400000000000000000000334431362551502500217650ustar00rootroot00000000000000from __future__ import absolute_import, division, print_function import 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 ): font_compiled = 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 ): font_compiled = 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-2.12.2/tests/filters/transformations_test.py000066400000000000000000000144641362551502500224740ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import from ufo2ft.filters.transformations import TransformationsFilter, log from fontTools.misc.loggingTools import CapturingLogHandler from fontTools.misc.py23 import isclose import pytest @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))), ], }, { "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(object): def test_invalid_origin_value(self): with pytest.raises(ValueError) as excinfo: TransformationsFilter(Origin=5) excinfo.match("is not a valid 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) 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) 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' was not transformed, because it doesn't have a scale # or skew and the base glyph was already included 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) 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) ufo2ft-2.12.2/tests/fontInfoData_test.py000066400000000000000000000220521362551502500201370ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import os import random import time from ufo2ft.fontInfoData import ( getAttrWithFallback, normalizeStringForPostscript, dateStringToTimeValue, ) import pytest @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(object): @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", "openTypeNameCompatibleFullName": "Family Name Style Name", }, ), # no styleMapStyleName ( {"styleMapFamilyName": "Style Map Family Name"}, { "styleMapFamilyName": "Style Map Family Name", "styleMapStyleName": "regular", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Style Name", "openTypeNameCompatibleFullName": "Style Map Family 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", "openTypeNameCompatibleFullName": "Family Name", }, ), # no styleMapFamilyName but styleName="Regular" ( {"styleName": "Regular", "styleMapStyleName": "regular"}, { "styleMapFamilyName": "Family Name", "styleMapStyleName": "regular", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Regular", "openTypeNameCompatibleFullName": "Family Name", }, ), # no styleMapStyleName but styleName="Regular" ( {"styleName": "Regular", "styleMapFamilyName": "Style Map Family Name"}, { "styleMapFamilyName": "Style Map Family Name", "styleMapStyleName": "regular", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Regular", "openTypeNameCompatibleFullName": "Style Map Family Name", }, ), # no styleMapFamilyName, no styleMapStyleName but styleName="Bold" ( {"styleName": "Bold"}, { "familyName": "Family Name", "styleName": "Bold", "styleMapFamilyName": "Family Name", "styleMapStyleName": "bold", "openTypeNamePreferredFamilyName": "Family Name", "openTypeNamePreferredSubfamilyName": "Bold", "openTypeNameCompatibleFullName": "Family Name 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") == 1 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 getattr(info, "openTypeHheaCaretSlopeRun") is None assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRise") == 2048 assert getAttrWithFallback(info, "openTypeHheaCaretSlopeRun") == -435 info.openTypeHheaCaretSlopeRise = None info.openTypeHheaCaretSlopeRun = 200 assert getattr(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 class PostscriptBlueScaleFallbackTest(object): 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(object): def test_no_change(self): assert ( normalizeStringForPostscript("Sample copyright notice.") == "Sample copyright notice." ) class DateStringToTimeValueTest(object): 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-2.12.2/tests/integration_test.py000066400000000000000000000131631362551502500201110ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import io from fontTools.misc.py23 import * from ufo2ft import ( compileOTF, compileTTF, compileInterpolatableTTFs, compileVariableTTF, compileVariableCFF2, ) import warnings import difflib import os import sys import pytest 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), "r", encoding="utf-8") as f: expected = readLines(f) font.recalcTimestamp = False font["head"].created, font["head"].modified = 3570196637, 3601822698 font["head"].checkSumAdjustment = 0x12345678 f = UnicodeIO() 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(object): _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_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_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") def test_optimizeCFF_none(self, testufo): otf = compileOTF(testufo, optimizeCFF=0) expectTTX(otf, "TestFont-NoOptimize-CFF.ttx") def test_optimizeCFF_specialize(self, testufo): otf = compileOTF(testufo, optimizeCFF=1) expectTTX(otf, "TestFont-Specialized-CFF.ttx") def test_optimizeCFF_subroutinize(self, testufo): otf = compileOTF(testufo, optimizeCFF=2) expectTTX(otf, "TestFont-CFF.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_debugFeatureFile(self, designspace): tmp = io.StringIO() varfont = compileVariableTTF(designspace, debugFeatureFile=tmp) assert "### LayerFont-Regular ###" in tmp.getvalue() assert "### LayerFont-Bold ###" in tmp.getvalue() if __name__ == "__main__": sys.exit(pytest.main(sys.argv)) ufo2ft-2.12.2/tests/outlineCompiler_test.py000066400000000000000000000771311362551502500207450ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import print_function, absolute_import, division, unicode_literals from cu2qu.ufo import font_to_quadratic from fontTools.ttLib import TTFont from fontTools.misc.py23 import basestring, unichr, byteord from fontTools import designspaceLib from ufo2ft.outlineCompiler import OutlineTTFCompiler, OutlineOTFCompiler from ufo2ft.fontInfoData import intListToNum from fontTools.ttLib.tables._g_l_y_f import USE_MY_METRICS from ufo2ft.constants import ( USE_PRODUCTION_NAMES, GLYPHS_DONT_USE_PRODUCTION_NAMES, SPARSE_TTF_MASTER_TABLES, SPARSE_OTF_MASTER_TABLES, ) from ufo2ft import ( compileTTF, compileOTF, compileInterpolatableTTFs, compileInterpolatableTTFsFromDS, compileInterpolatableOTFsFromDS, ) import os import logging import pytest 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 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(object): def test_setupTable_gasp(self, testufo): compiler = OutlineTTFCompiler(testufo) compiler.otf = TTFont() compiler.setupTable_gasp() assert "gasp" in compiler.otf assert compiler.otf["gasp"].gaspRange == {7: 10, 65535: 15} def test_compile_with_gasp(self, testufo): compiler = OutlineTTFCompiler(testufo) compiler.compile() assert "gasp" in compiler.otf assert compiler.otf["gasp"].gaspRange == {7: 10, 65535: 15} def test_compile_without_gasp(self, testufo): testufo.info.openTypeGaspRangeRecords = None compiler = OutlineTTFCompiler(testufo) compiler.compile() assert "gasp" not in compiler.otf def test_compile_empty_gasp(self, testufo): # ignore empty gasp testufo.info.openTypeGaspRangeRecords = [] compiler = OutlineTTFCompiler(testufo) 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_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_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, testufo): for glyph in testufo: glyph.width = 0 compiler = OutlineTTFCompiler(testufo) 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 class OutlineOTFCompilerTest(object): 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, basestring): assert exp_token == act_token else: assert not isinstance(act_token, basestring) 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 class GlyphOrderTest(object): def test_compile_original_glyph_order(self, testufo): DEFAULT_ORDER = [ ".notdef", "space", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", ] compiler = OutlineTTFCompiler(testufo) compiler.compile() assert compiler.otf.getGlyphOrder() == DEFAULT_ORDER def test_compile_tweaked_glyph_order(self, testufo): NEW_ORDER = [ ".notdef", "space", "b", "a", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", ] testufo.lib["public.glyphOrder"] = NEW_ORDER compiler = OutlineTTFCompiler(testufo) compiler.compile() assert compiler.otf.getGlyphOrder() == NEW_ORDER def test_compile_strange_glyph_order(self, testufo): """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", ] testufo.lib["public.glyphOrder"] = NEW_ORDER compiler = OutlineTTFCompiler(testufo) compiler.compile() assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER class NamesTest(object): @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)]} ASCII = [unichr(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 = byteord(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", ] assert master_otfs[1].getGlyphOrder() == [".notdef", "e"] 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 if __name__ == "__main__": import sys sys.exit(pytest.main(sys.argv)) ufo2ft-2.12.2/tests/preProcessor_test.py000066400000000000000000000246321362551502500202570ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import os import logging import ufo2ft from ufo2ft.preProcessor import TTFPreProcessor, TTFInterpolatablePreProcessor from ufo2ft.filters import UFO2FT_FILTERS_KEY from cu2qu.ufo import CURVE_TYPE_LIB_KEY from fontTools import designspaceLib 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(object): 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") class TTFInterpolatablePreProcessorTest(object): 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[UFO2FT_FILTERS_KEY] = [ {"name": "transformations", "kwargs": {"OffsetX": -40}, "pre": True} ] ufo2 = FontClass(getpath("TestFont.ufo")) ufo2.lib[UFO2FT_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 class SkipExportGlyphsTest(object): 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()) == set(["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) assert len(glyphSet["numero"].components) == 1 # The "N" component assert len(glyphSet["numero"]) == 2 # The two contours of "o" and "_o.numero" 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_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") ufo2ft-2.12.2/tests/testSupport.py000066400000000000000000000020651362551502500171020ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import import sys import os import types import contextlib from fontTools.misc.py23 import tostr class _TempModule(object): """Temporarily replace a module in sys.modules with an empty namespace""" def __init__(self, mod_name): mod_name = tostr(mod_name, encoding="ascii") 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 = [] @contextlib.contextmanager def pushd(target): saved = os.getcwd() os.chdir(target) try: yield saved finally: os.chdir(saved) ufo2ft-2.12.2/tox.ini000066400000000000000000000015151362551502500143240ustar00rootroot00000000000000[tox] envlist = py{27,37}-cov, htmlcov [testenv] deps = cov: coverage pytest -rrequirements.txt extras = pathops ; 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/py36/lib/python3.6/site-packages/fontTools and collapse # them into Lib/fontTools. cov: coverage run --parallel-mode -m pytest {posargs} nocov: pytest {posargs} [testenv:htmlcov] deps = coverage skip_install = true commands = coverage combine coverage html [testenv:codecov] passenv = * deps = coverage codecov skip_install = true ignore_outcome = true commands = coverage combine codecov --env TOXENV