pax_global_header00006660000000000000000000000064132157431630014517gustar00rootroot0000000000000052 comment=c17b47e2bdeac208e9b48c2938293ec411622dfc ufo2ft-1.1.0/000077500000000000000000000000001321574316300127235ustar00rootroot00000000000000ufo2ft-1.1.0/.codecov.yml000066400000000000000000000001211321574316300151400ustar00rootroot00000000000000comment: false coverage: status: project: false patch: false ufo2ft-1.1.0/.coveragerc000066400000000000000000000016011321574316300150420ustar00rootroot00000000000000[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-1.1.0/.gitignore000066400000000000000000000005241321574316300147140ustar00rootroot00000000000000# 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/ # OSX Finder .DS_Store # pyenv python configuration file .python-version # autosaved emacs files *~ ufo2ft-1.1.0/.pyup.yml000066400000000000000000000002501321574316300145160ustar00rootroot00000000000000# controls the frequency of updates (undocumented beta feature) schedule: every week # do not pin dependencies unless they have explicit version specifiers pin: False ufo2ft-1.1.0/.travis.yml000066400000000000000000000041071321574316300150360ustar00rootroot00000000000000sudo: false language: python matrix: include: - python: 2.7 env: TOXENV=py27-cov - python: 3.6 env: TOXENV=py36-cov branches: only: - master - /^v\d+\.\d+.*$/ install: pip install tox script: tox after_success: - tox -e codecov deploy: # deploy to PyPI on tags - provider: pypi server: https://upload.pypi.org/legacy/ on: repo: googlei18n/ufo2ft tags: true all_branches: true python: 3.6 user: anthrotype password: secure: TKU+9wrb+oMWQycxAGBFkMvftFaJnVh/X22o0JWrAMoo2IiakyU+/cHCmlSAJBuQlAYyKywa3gI9ajf73vlDhCZMRiXiyfYQykxV+G/CfVqyAxXkF5cOTvg0O6xxS4OMrsuMCuRtG6m8l4FryJ9NETQiksDgDcggKXzs7COWeUEfnd15nFEerKzefn2NLJtOIpQqVPs068hrGHZHrDkt1k03ffxgWuBqRjiR1u8WmksZGxedO1v9weUUsQauJ/GpvCVUcepE2b1tvTON2tpucm6Txuf574GBFHL25fFKeppI9CvdjLZFpG0Yx41WJCE1GNL79oimg2ss5SWxojr7t7LLlIRVZBv2CRx4aS2+ACXxU8IYqL0+VMRZNPJsB39aAUUZyuhR2S+LtqzitVeJLLn83aAlcY80s87Z8h0x3XE4YSZ8IwWk2cpUcw1P+UbOP/xo+ZA7+i2EhRRu3hELC6pBImrgpjqrWrlOzG1fzR2q21i+vAgcTodadNFmmQkpD0DuGaxuZ8EMPJbyZBmyumANOfbiCYkGbKVt5Ng4uah8sh7TBj0S/+nNLalb9X0DxBmW+GFOSk8LdNnICEXHaJ8gpWRpbC3yAA4xFAAbYgPHbae4DgWHeMNJhPudHDUdF3IAcOZ0o68oZaO2rloBlWeYcox7La6kSPFjvRIMY2Y= distributions: sdist bdist_wheel # also create a Github Release with the current tag - provider: releases api_key: secure: zXtgsom/sEEoj/pg0qq0qGFI93QkNEHe4gjX1AOID7oDORz1AP6nBst5zxoMJy/4DH4tNiWTQZdk+OVDmkysn5kJearDZ1uM4g02nhSYxXZ/a50p6ewciaYE1KwBt8IxiqhvvRdY2Fab1eW1aTlRjpCYoFvsovck7sWJ96dhfUMGIRzk9cbo3/JJ052kLz34lgM3PLlToN57WHu3y4TO0tXTjTdo4V8uB0l9O8j9P0ez1Q82ZPcxFicr4HUIqZn4gBu7cRXZr3PXu6Rx54eg/QFD8hVMoH9JOafmRfKmsayE6g0qDbFUMOnoMFg0on8lh2OR5EZlAznrccFccwAGNykY+CxP/dC/Io2XktrA/HIPrWy2rGojSNAYQe3LS5oW/SEbZSTGSG45VdaX7oBzgp/4eKXN/V+n3W68JGjKKN2foIFU3vAL0dm8fA+A2N/fBSR3bCyuwZ/u+Bt+ads0M6dmlBln+s8y1PPdhY4RRaU4I+7m8KOBfMUPzYeAq5DoWWDe8omtqmPHs0DMjr4gtBXOD0qeNE40ogc/LeXJ3l8e1MN8zq0OYfngJJop/HMWfyg7kaVurWCigbsJMNkMpAam8hq7BsLWncRJMQax5NHXAcAqWOVemANOW+H3gcKFPtjzOPblxZNoDK/Y6R2wiY5pfDXl1aMct2BF0mXZPNA= on: repo: googlei18n/ufo2ft tags: true all_branches: true python: 3.6 ufo2ft-1.1.0/LICENSE000066400000000000000000000020631321574316300137310ustar00rootroot00000000000000The 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-1.1.0/Lib/000077500000000000000000000000001321574316300134315ustar00rootroot00000000000000ufo2ft-1.1.0/Lib/ufo2ft/000077500000000000000000000000001321574316300146365ustar00rootroot00000000000000ufo2ft-1.1.0/Lib/ufo2ft/__init__.py000066400000000000000000000173021321574316300167520ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import import os from fontTools.misc.py23 import * from ufo2ft.preProcessor import ( OTFPreProcessor, TTFPreProcessor, TTFInterpolatablePreProcessor) from ufo2ft.featureCompiler import FeatureCompiler from ufo2ft.outlineCompiler import OutlineOTFCompiler, OutlineTTFCompiler from ufo2ft.postProcessor import PostProcessor __version__ = "1.1.0" def compileOTF(ufo, preProcessorClass=OTFPreProcessor, outlineCompilerClass=OutlineOTFCompiler, featureCompilerClass=FeatureCompiler, kernWriterClass=None, # deprecated markWriterClass=None, # deprecated featureWriters=None, glyphOrder=None, useProductionNames=None, optimizeCFF=True, roundTolerance=None, removeOverlaps=False, inplace=False): """Create FontTools CFF font from a UFO. *removeOverlaps* performs a union operation on all the glyphs' contours. *optimizeCFF* sets whether the CFF table should be subroutinized. *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. """ preProcessor = preProcessorClass( ufo, inplace=inplace, removeOverlaps=removeOverlaps) glyphSet = preProcessor.process() outlineCompiler = outlineCompilerClass( ufo, glyphSet=glyphSet, glyphOrder=glyphOrder, roundTolerance=roundTolerance) otf = outlineCompiler.compile() featureWriters = _replaceDeprecatedFeatureWriters( featureWriters, kernWriterClass, markWriterClass) featureCompiler = featureCompilerClass( ufo, otf, featureWriters=featureWriters, mtiFeatures=_getMtiFeatures(ufo)) featureCompiler.compile() postProcessor = PostProcessor(otf, ufo) otf = postProcessor.process(useProductionNames, optimizeCFF) return otf def compileTTF(ufo, preProcessorClass=TTFPreProcessor, outlineCompilerClass=OutlineTTFCompiler, featureCompilerClass=FeatureCompiler, kernWriterClass=None, # deprecated markWriterClass=None, # deprecated featureWriters=None, glyphOrder=None, useProductionNames=None, convertCubics=True, cubicConversionError=None, reverseDirection=True, removeOverlaps=False, inplace=False): """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. """ preProcessor = preProcessorClass( ufo, inplace=inplace, removeOverlaps=removeOverlaps, convertCubics=convertCubics, conversionError=cubicConversionError, reverseDirection=reverseDirection) glyphSet = preProcessor.process() outlineCompiler = outlineCompilerClass( ufo, glyphSet=glyphSet, glyphOrder=glyphOrder) otf = outlineCompiler.compile() featureWriters = _replaceDeprecatedFeatureWriters( featureWriters, kernWriterClass, markWriterClass) featureCompiler = featureCompilerClass( ufo, otf, featureWriters=featureWriters, mtiFeatures=_getMtiFeatures(ufo)) featureCompiler.compile() postProcessor = PostProcessor(otf, ufo) otf = postProcessor.process(useProductionNames) return otf def compileInterpolatableTTFs(ufos, preProcessorClass=TTFInterpolatablePreProcessor, outlineCompilerClass=OutlineTTFCompiler, featureCompilerClass=FeatureCompiler, featureWriters=None, glyphOrder=None, useProductionNames=None, cubicConversionError=None, reverseDirection=True, inplace=False): """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. """ preProcessor = preProcessorClass( ufos, inplace=inplace, conversionError=cubicConversionError, reverseDirection=reverseDirection) glyphSets = preProcessor.process() for ufo, glyphSet in zip(ufos, glyphSets): outlineCompiler = outlineCompilerClass( ufo, glyphSet=glyphSet, glyphOrder=glyphOrder) ttf = outlineCompiler.compile() featureCompiler = featureCompilerClass( ufo, ttf, featureWriters=featureWriters, mtiFeatures=_getMtiFeatures(ufo)) featureCompiler.compile() postProcessor = PostProcessor(ttf, ufo) ttf = postProcessor.process(useProductionNames) yield ttf def _getMtiFeatures(ufo): features = {} prefix = "com.github.googlei18n.ufo2ft.mtiFeatures" + os.path.sep for fileName in ufo.data.fileNames: if fileName.startswith(prefix) and fileName.endswith(".mti"): content = tounicode(ufo.data[fileName], encoding="utf-8") features[fileName[len(prefix):-4]] = content return features if len(features) > 0 else None def _deprecateArgument(arg, repl): import warnings warnings.warn("%r is deprecated; use %r instead" % (arg, repl), category=UserWarning, stacklevel=3) def _replaceDeprecatedFeatureWriters(featureWriters, kernWriterClass=None, markWriterClass=None): if not any([kernWriterClass, markWriterClass]): return featureWriters if featureWriters is not None: raise TypeError( "the new 'featureWriters' argument, and the old (deprecated) " "'kernWriterClass' and 'markWriterClass' arguments are mutually " "exclusive.") featureWriters = [] if kernWriterClass is not None: _deprecateArgument("kernWriterClass", "featureWriters") featureWriters.append(kernWriterClass) else: from ufo2ft.featureWriters import KernFeatureWriter featureWriters.append(KernFeatureWriter) if markWriterClass is not None: _deprecateArgument("markWriterClass", "featureWriters") featureWriters.append(markWriterClass) else: from ufo2ft.featureWriters import MarkFeatureWriter featureWriters.append(MarkFeatureWriter) return featureWriters ufo2ft-1.1.0/Lib/ufo2ft/featureCompiler.py000066400000000000000000000140731321574316300203430ustar00rootroot00000000000000from __future__ import \ print_function, division, absolute_import, unicode_literals import logging import os from inspect import isclass from tempfile import NamedTemporaryFile from fontTools import feaLib from fontTools.feaLib.builder import addOpenTypeFeaturesFromString from fontTools import mtiLib from fontTools.misc.py23 import UnicodeIO, tobytes from ufo2ft.featureWriters import DEFAULT_FEATURE_WRITERS from ufo2ft.maxContextCalc import maxCtxFont logger = logging.getLogger(__name__) class FeatureCompiler(object): """Generates OpenType feature tables for a UFO. *featureWriters* argument is a list that can contain either subclasses of BaseFeatureWriter or pre-initialized instances (or a mix of the two). Classes are initialized without arguments so will use default options. Features will be written by each feature writer in the given order. The default value is [KernFeatureWriter, MarkFeatureWriter]. If mtiFeatures is passed to the constructor, it should be a dictionary mapping feature table tags to MTI feature declarations for that table. These are passed to mtiLib for compilation. """ def __init__(self, font, outline, featureWriters=None, mtiFeatures=None): self.font = font self.outline = outline if featureWriters is None: featureWriters = DEFAULT_FEATURE_WRITERS self.featureWriters = [] for writer in featureWriters: if isclass(writer): writer = writer() self.featureWriters.append(writer) self.mtiFeatures = mtiFeatures def compile(self): """Compile the features. Starts by generating feature syntax for the kern, mark, and mkmk features. If they already exist, they will not be overwritten. """ self.setupFile_features() self.setupFile_featureTables() self.postProcess() def setupFile_features(self): """ Make the features source file. If any tables or the kern feature are defined in the font's features, they will not be overwritten. **This should not be called externally.** Subclasses may override this method to handle the file creation in a different way if desired. """ if self.mtiFeatures is not None: return existingFeatures = self._findLayoutFeatures() # build features as necessary autoFeatures = [] # the current MarkFeatureWriter writes both mark and mkmk features # with shared markClass definitions; to prevent duplicate glyphs in # markClass, here we write the features only if none of them is alread # present. # TODO: Support updating pre-existing markClass definitions to allow # writing either mark or mkmk features indipendently from each other # https://github.com/googlei18n/fontmake/issues/319 font = self.font for fw in self.featureWriters: if (fw.mode == "append" or ( fw.mode == "skip" and all(fea not in existingFeatures for fea in fw.features))): autoFeatures.append(fw.write(font)) # write the features self.features = "\n\n".join([font.features.text or ""] + autoFeatures) def _findLayoutFeatures(self): """Returns what OpenType layout feature tags are present in the UFO.""" featxt = self.font.features.text if not featxt: return set() buf = UnicodeIO(featxt) # the path is only used by the lexer to resolve 'include' statements if self.font.path is not None: buf.name = os.path.join(self.font.path, "features.fea") glyphMap = self.outline.getReverseGlyphMap() parser = feaLib.parser.Parser(buf, glyphMap) doc = parser.parse() return {f.name for f in doc.statements if isinstance(f, feaLib.ast.FeatureBlock)} def setupFile_featureTables(self): """ Compile and return 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 self.mtiFeatures is not None: for tag, features in self.mtiFeatures.items(): table = mtiLib.build(features.splitlines(), self.outline) assert table.tableTag == tag self.outline[tag] = table elif self.features.strip(): # the path to features.fea is only used by the lexer to resolve # the relative "include" statements if self.font.path is not None: feapath = os.path.join(self.font.path, "features.fea") else: # in-memory UFO has no path, can't do 'include' either feapath = None # save generated features to a temp file if things go wrong... data = tobytes(self.features, encoding="utf-8") with NamedTemporaryFile(delete=False) as tmp: tmp.write(data) # if compilation succedes or fails for unrelated reasons, clean # up the temporary file try: addOpenTypeFeaturesFromString(self.outline, self.features, filename=feapath) except feaLib.error.FeatureLibError: logger.error("Compilation failed! Inspect temporary file: %r", tmp.name) raise except: os.remove(tmp.name) raise else: os.remove(tmp.name) def postProcess(self): """Make post-compilation calculations. **This should not be called externally.** Subclasses may override this method if desired. """ # only after compiling features can usMaxContext be calculated self.outline['OS/2'].usMaxContext = maxCtxFont(self.outline) ufo2ft-1.1.0/Lib/ufo2ft/featureWriters/000077500000000000000000000000001321574316300176515ustar00rootroot00000000000000ufo2ft-1.1.0/Lib/ufo2ft/featureWriters/__init__.py000066400000000000000000000003411321574316300217600ustar00rootroot00000000000000from .baseFeatureWriter import BaseFeatureWriter from .kernFeatureWriter import KernFeatureWriter from .markFeatureWriter import MarkFeatureWriter DEFAULT_FEATURE_WRITERS = [ KernFeatureWriter, MarkFeatureWriter, ] ufo2ft-1.1.0/Lib/ufo2ft/featureWriters/baseFeatureWriter.py000066400000000000000000000060011321574316300236430ustar00rootroot00000000000000from __future__ import ( print_function, division, absolute_import, unicode_literals) from fontTools.misc.py23 import SimpleNamespace _SUPPORTED_MODES = ("skip", "append") class BaseFeatureWriter(object): """Abstract features writer. The 'features' class attribute defines the list of all the features that this writer supports. If you want to only write some of the available features you can provide a smaller list to 'features' constructor argument. By the default all the features supported by this writer will be outputted. There are currently two possible writing modes: 1) "skip" (default) will not write anything if any of the features listed is already present; 2) "append" will add additional lookups to an existing feature, if it's already present. The 'options' class attribute contains a mapping of option names with their default values. These can be overridden on an instance by passing keword arguments to the constructor. """ features = [] mode = "skip" options = {} def __init__(self, features=None, mode=None, linesep="\n", **kwargs): if features is not None: default_features = set(self.__class__.features) self.features = [] for feat in features: if feat not in default_features: raise ValueError(feat) self.features.append(feat) if mode is not None: if mode not in _SUPPORTED_MODES: raise ValueError(mode) self.mode = mode self.linesep = linesep 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) def set_context(self, font): """ Populate a `self.context` namespace, which is reset before each new call to `_write` method. 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 returns the namepace instance. """ self.context = SimpleNamespace(font=font) return self.context def write(self, font): """Write features and class definitions for this font. Resets the `self.context` and delegates to ``self._write()` method. Returns a string containing the text of the features that are listed in `self.features`. """ self.set_context(font) return self._write() def _write(self): """Subclasses must override this.""" raise NotImplementedError @staticmethod def liststr(glyphs): """Return string representation of a list of glyph names.""" return "[%s]" % " ".join(glyphs) ufo2ft-1.1.0/Lib/ufo2ft/featureWriters/kernFeatureWriter.py000066400000000000000000000443721321574316300237050ustar00rootroot00000000000000from __future__ import ( print_function, division, absolute_import, unicode_literals ) from fontTools.misc.py23 import unichr import collections import re try: import unicodedata2 as unicodedata except ImportError: import unicodedata from ufo2ft.featureWriters import BaseFeatureWriter class KernFeatureWriter(BaseFeatureWriter): """Generates a kerning feature based on glyph class definitions. Uses the kerning rules contained in an UFO's kerning data, as well as glyph classes from parsed feature text. Class-based rules are set based on the existing rules for their key glyphs. Uses class attributes to match glyph class names in feature text as kerning classes, which can be overridden. """ features = ["kern"] leftFeaClassRe = r"@MMK_L_(.+)" rightFeaClassRe = r"@MMK_R_(.+)" options = dict( ignoreMarks=True, ) def set_context(self, font): ctx = super(KernFeatureWriter, self).set_context(font) ctx.kerning = dict(font.kerning) ctx.groups = dict(font.groups) fealines = [] if font.features.text: for line in font.features.text.splitlines(): comment_start = line.find('#') if comment_start >= 0: line = line[:comment_start] line = line.strip() if line: fealines.append(line) ctx.featxt = '\n'.join(fealines) ctx.ltrScripts = collections.OrderedDict() ctx.rtlScripts = collections.OrderedDict() for script, lang in re.findall( r'languagesystem\s+([a-z]{4})\s+([A-Z]+|dflt)\s*;', ctx.featxt): if self._scriptIsRtl(script): ctx.rtlScripts.setdefault(script, []).append(lang) else: ctx.ltrScripts.setdefault(script, []).append(lang) # kerning classes found in existing feature text and UFO groups ctx.leftFeaClasses = {} ctx.rightFeaClasses = {} ctx.leftUfoClasses = {} ctx.rightUfoClasses = {} # kerning rule collections, mapping pairs to values ctx.glyphPairKerning = {} ctx.leftClassKerning = {} ctx.rightClassKerning = {} ctx.classPairKerning = {} return ctx def _write(self): self._collectFeaClasses() self._collectFeaClassKerning() self._cleanupMissingGlyphs() self._correctUfoClassNames() self._collectUfoKerning() self._removeConflictingKerningRules() # write the glyph classes lines = [] self._addGlyphClasses(lines) lines.append("") # split kerning into LTR and RTL lookups, if necessary rtlScripts = self.context.rtlScripts if rtlScripts: self._splitRtlKerning() # write the lookups and feature ltrScripts = self.context.ltrScripts ltrKern = [] if ltrScripts or not rtlScripts: self._addKerning(ltrKern, self.context.glyphPairKerning) self._addKerning(ltrKern, self.context.leftClassKerning, enum=True) self._addKerning(ltrKern, self.context.rightClassKerning, enum=True) self._addKerning(ltrKern, self.context.classPairKerning, ignoreZero=True) if ltrKern: lines.append("lookup kern_ltr {") if self.options.ignoreMarks: lines.append(" lookupflag IgnoreMarks;") lines.extend(ltrKern) lines.append("} kern_ltr;") lines.append("") rtlKern = [] if rtlScripts: self._addKerning(rtlKern, self.context.rtlGlyphPairKerning, rtl=True) self._addKerning(rtlKern, self.context.rtlLeftClassKerning, rtl=True, enum=True) self._addKerning(rtlKern, self.context.rtlRightClassKerning, rtl=True, enum=True) self._addKerning(rtlKern, self.context.rtlClassPairKerning, rtl=True, ignoreZero=True) if rtlKern: lines.append("lookup kern_rtl {") if self.options.ignoreMarks: lines.append(" lookupflag IgnoreMarks;") lines.extend(rtlKern) lines.append("} kern_rtl;") lines.append("") if not (ltrKern or rtlKern): # no kerning pairs, don't write empty feature return "" lines.append("feature kern {") if ltrKern: lines.append(" lookup kern_ltr;") if rtlScripts: if ltrKern: self._addLookupReferences(lines, ltrScripts, "kern_ltr") if rtlKern: self._addLookupReferences(lines, rtlScripts, "kern_rtl") lines.append("} kern;") return self.linesep.join(lines) def _collectFeaClasses(self): """Parse glyph classes from existing feature text.""" featxt = self.context.featxt leftFeaClasses = self.context.leftFeaClasses rightFeaClasses = self.context.rightFeaClasses for name, contents in re.findall( r'(@[\w.]+)\s*=\s*\[([\s\w.@-]*)\]\s*;', featxt, re.M): if re.match(self.leftFeaClassRe, name): leftFeaClasses[name] = contents.split() elif re.match(self.rightFeaClassRe, name): rightFeaClasses[name] = contents.split() def _collectFeaClassKerning(self): """Set up class kerning rules from class definitions in feature text. The first glyph from each class (called it's "key") is used to determine the kerning values associated with that class. """ leftFeaClasses = self.context.leftFeaClasses rightFeaClasses = self.context.rightFeaClasses kerning = self.context.kerning classPairKerning = self.context.classPairKerning leftClassKerning = self.context.leftClassKerning rightClassKerning = self.context.rightClassKerning for leftName, leftContents in leftFeaClasses.items(): leftKey = leftContents[0] # collect rules with two classes for rightName, rightContents in rightFeaClasses.items(): rightKey = rightContents[0] pair = leftKey, rightKey kerningVal = kerning.get(pair) if kerningVal is None: continue classPairKerning[leftName, rightName] = kerningVal del kerning[pair] # collect rules with left class and right glyph for pair, kerningVal in self._getGlyphKerning(leftKey, 0): leftClassKerning[leftName, pair[1]] = kerningVal del kerning[pair] # collect rules with left glyph and right class for rightName, rightContents in rightFeaClasses.items(): rightKey = rightContents[0] for pair, kerningVal in self._getGlyphKerning(rightKey, 1): rightClassKerning[pair[0], rightName] = kerningVal del kerning[pair] def _cleanupMissingGlyphs(self): """Removes glyphs missing in the font from groups or kerning pairs.""" allGlyphs = set(self.context.font.keys()) groups = {} for name, members in self.context.groups.items(): newMembers = [g for g in members if g in allGlyphs] if newMembers: groups[name] = newMembers kerning = {} for glyphPair, val in sorted(self.context.kerning.items()): left, right = glyphPair if left not in groups and left not in allGlyphs: continue if right not in groups and right not in allGlyphs: continue kerning[glyphPair] = val self.context.groups = groups self.context.kerning = kerning def _correctUfoClassNames(self): """Detect and replace illegal class names found in UFO kerning.""" groups = self.context.groups kerning = self.context.kerning for oldName, members in list(groups.items()): newName = self._makeFeaClassName(oldName) if oldName == newName: continue groups[newName] = members del groups[oldName] for oldPair, kerningVal in self._getGlyphKerning(oldName): left, right = oldPair newPair = (newName, right) if left == oldName else (left, newName) kerning[newPair] = kerningVal del kerning[oldPair] def _collectUfoKerning(self): """Sort UFO kerning rules into glyph pair or class rules.""" groups = self.context.groups leftUfoClasses = self.context.leftUfoClasses rightUfoClasses = self.context.rightUfoClasses classPairKerning = self.context.classPairKerning leftClassKerning = self.context.leftClassKerning rightClassKerning = self.context.rightClassKerning glyphPairKerning = self.context.glyphPairKerning for glyphPair, val in sorted(self.context.kerning.items()): left, right = glyphPair leftIsClass = left in groups rightIsClass = right in groups if leftIsClass: leftUfoClasses[left] = groups[left] if rightIsClass: rightUfoClasses[right] = groups[right] classPairKerning[glyphPair] = val else: leftClassKerning[glyphPair] = val elif rightIsClass: rightUfoClasses[right] = groups[right] rightClassKerning[glyphPair] = val else: glyphPairKerning[glyphPair] = val def _removeConflictingKerningRules(self): """Remove any conflicting pair and class rules. If conflicts are detected in a class rule, the offending class members are removed from the rule and the class name is replaced with a list of glyphs (the class members minus the offending members). """ leftClasses, rightClasses = self._getClasses(separate=True) # maintain list of glyph pair rules seen seen = dict(self.context.glyphPairKerning) liststr = self.liststr # remove conflicts in left class / right glyph rules leftClassKerning = self.context.leftClassKerning for (lClass, rGlyph), val in list(leftClassKerning.items()): lGlyphs = leftClasses[lClass] nlGlyphs = [] for lGlyph in lGlyphs: pair = lGlyph, rGlyph if pair not in seen: nlGlyphs.append(lGlyph) seen[pair] = val if nlGlyphs != lGlyphs: if nlGlyphs: leftClassKerning[liststr(nlGlyphs), rGlyph] = val del leftClassKerning[lClass, rGlyph] # remove conflicts in left glyph / right class rules rightClassKerning = self.context.rightClassKerning for (lGlyph, rClass), val in list(rightClassKerning.items()): rGlyphs = rightClasses[rClass] nrGlyphs = [] for rGlyph in rGlyphs: pair = lGlyph, rGlyph if pair not in seen: nrGlyphs.append(rGlyph) seen[pair] = val if nrGlyphs != rGlyphs: if nrGlyphs: rightClassKerning[lGlyph, liststr(nrGlyphs)] = val del rightClassKerning[lGlyph, rClass] def _addGlyphClasses(self, lines): """Add glyph classes for the input font's groups.""" for key, members in sorted(self.context.groups.items()): lines.append("%s = [%s];" % (key, " ".join(members))) def _splitRtlKerning(self): """Split RTL kerning into separate dictionaries.""" self.context.rtlGlyphPairKerning = {} self.context.rtlLeftClassKerning = {} self.context.rtlRightClassKerning = {} self.context.rtlClassPairKerning = {} classes = self._getClasses() allKerning = ( (self.context.glyphPairKerning, self.context.rtlGlyphPairKerning, (False, False)), (self.context.leftClassKerning, self.context.rtlLeftClassKerning, (True, False)), (self.context.rightClassKerning, self.context.rtlRightClassKerning, (False, True)), (self.context.classPairKerning, self.context.rtlClassPairKerning, (True, True))) for origKerning, rtlKerning, classFlags in allKerning: for pair in list(origKerning.keys()): allGlyphs = [] for glyphs, isClass in zip(pair, classFlags): if not isClass: allGlyphs.append(glyphs) elif glyphs.startswith('@'): allGlyphs.extend(classes[glyphs]) else: assert glyphs.startswith('[') and glyphs.endswith(']') allGlyphs.extend(glyphs[1:-1].split()) if any(self._glyphIsRtl(g) for g in allGlyphs): rtlKerning[pair] = origKerning.pop(pair) @staticmethod def _addKerning(lines, kerning, rtl=False, enum=False, ignoreZero=False): """Add kerning rules for a mapping of pairs to values.""" enum = "enum " if enum else "" valstr = "<%(val)d 0 %(val)d 0>" if rtl else "%(val)d" lineFormat = " %spos %%(lhs)s %%(rhs)s %s;" % (enum, valstr) for (left, right), val in sorted(kerning.items()): if val == 0 and ignoreZero: continue lines.append(lineFormat % {'lhs': left, 'rhs': right, 'val': val}) @staticmethod def _addLookupReferences(lines, languageSystems, lookupName): """Add references to lookup for a set of language systems. Language systems are passed in as a dictionary mapping scripts to lists of languages. """ for script, langs in languageSystems.items(): lines.append("script %s;" % script) for lang in langs: lines.append("language %s;" % lang) lines.append("lookup %s;" % lookupName) def _getClasses(self, separate=False): """Return all kerning classes together.""" leftClasses = dict(self.context.leftFeaClasses) leftClasses.update(self.context.leftUfoClasses) rightClasses = dict(self.context.rightFeaClasses) rightClasses.update(self.context.rightUfoClasses) if separate: return leftClasses, rightClasses classes = leftClasses classes.update(rightClasses) return classes def _makeFeaClassName(self, name): """Make a glyph class name which is legal to use in feature text. Ensures the name starts with "@" and only includes characters in "A-Za-z0-9._", and isn't already defined. """ name = "@%s" % re.sub(r"[^A-Za-z0-9._]", r"", name) existingClassNames = set(self._getClasses().keys()) i = 1 origName = name while name in existingClassNames: name = "%s_%d" % (origName, i) i += 1 return name def _getGlyphKerning(self, glyphName, i=None): """Return the kerning rules which include glyphName, optionally only checking one side of each pair if index `i` is provided. """ hits = [] for pair, value in self.context.kerning.items(): if (glyphName in pair) if i is None else (pair[i] == glyphName): hits.append((pair, value)) return hits @staticmethod def _scriptIsRtl(script): """Return whether a script is right-to-left for kerning purposes. References: https://github.com/Tarobish/Jomhuria/blob/a21c41453ea8e3893e003ae9d5bee9ba7ac42d77/tools/getKernFeatureFromUFO.py#L18 https://github.com/behdad/harfbuzz/blob/691086f131cb6c9d97e98730c27673484bf93f87/src/hb-common.cc#L446 http://unicode.org/iso15924/iso15924-codes.html """ return script in ( # Unicode-1.1 additions 'arab', # ARABIC 'hebr', # HEBREW # Unicode-3.0 additions 'syrc', # SYRIAC 'thaa', # THAANA # Unicode-4.0 additions 'cprt', # CYPRIOT # Unicode-4.1 additions 'khar', # KHAROSHTHI # Unicode-5.0 additions 'phnx', # PHOENICIAN 'nkoo', # NKO # Unicode-5.1 additions 'lydi', # LYDIAN # Unicode-5.2 additions 'avst', # AVESTAN 'armi', # IMPERIAL_ARAMAIC 'phli', # INSCRIPTIONAL_PAHLAVI 'prti', # INSCRIPTIONAL_PARTHIAN 'sarb', # OLD_SOUTH_ARABIAN 'orkh', # OLD_TURKIC 'samr', # SAMARITAN # Unicode-6.0 additions 'mand', # MANDAIC # Unicode-6.1 additions 'merc', # MEROITIC_CURSIVE 'mero', # MEROITIC_HIEROGLYPHS # Unicode-7.0 additions 'mani', # MANICHAEAN 'mend', # MENDE_KIKAKUI 'nbat', # NABATAEAN 'narb', # OLD_NORTH_ARABIAN 'palm', # PALMYRENE 'phlp', # PSALTER_PAHLAVI # Unicode-8.0 additions 'hung', # OLD_HUNGARIAN # Unicode-9.0 additions 'adlm', # ADLAM ) def _glyphIsRtl(self, name): """Return whether the closest-associated unicode character is RTL.""" font = self.context.font delims = ('.', '_') uv = font[name].unicode while uv is None and any(d in name for d in delims): name = name[:max(name.rfind(d) for d in delims)] if name in font: uv = font[name].unicode if uv is None: return False return unicodedata.bidirectional(unichr(uv)) in ('R', 'AL') ufo2ft-1.1.0/Lib/ufo2ft/featureWriters/markFeatureWriter.py000066400000000000000000000215301321574316300236670ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import logging from collections import OrderedDict from ufo2ft.featureWriters import BaseFeatureWriter from ufo2ft.util import makeOfficialGlyphOrder logger = logging.getLogger(__name__) class MarkFeatureWriter(BaseFeatureWriter): """Generates a mark or mkmk feature based on glyph anchors. setupAnchorPairs() produces lists of (anchorName, accentAnchorName) tuples for mark and mkmk features, and optionally a list of ((anchorName, ...), accentAnchorName) tuples for a liga2mark feature. """ features = [ "mark", "mkmk", ] def set_context(self, font): ctx = super(MarkFeatureWriter, self).set_context(font) glyphOrder = makeOfficialGlyphOrder(font) ctx.glyphSet = OrderedDict(((gn, font[gn]) for gn in glyphOrder)) ctx.accentGlyphNames = set() self.setupAnchorPairs() return ctx @staticmethod def _generateClassName(accentAnchorName): """Generate a mark class name shared by class definition and positioning statements. """ return "@MC%s" % accentAnchorName def _createAccentGlyphList(self, accentAnchorName): """Return a list of tuples for glyphs containing an anchor with the given accent anchor name. """ glyphList = [] for glyphName, glyph in self.context.glyphSet.items(): for anchor in glyph.anchors: if accentAnchorName == anchor.name: glyphList.append((glyphName, anchor.x, anchor.y)) break return glyphList def _createBaseGlyphList(self, anchorName, isMkmk): """Return a list of tuples for glyphs containing an anchor with the given anchor name. Mark glyphs are included iff this is a mark-to-mark rule. """ glyphList = [] accentGlyphNames = set(self.context.accentGlyphNames) for glyphName, glyph in self.context.glyphSet.items(): if isMkmk != (glyphName in accentGlyphNames): continue for anchor in glyph.anchors: if anchorName == anchor.name: glyphList.append((glyphName, anchor.x, anchor.y)) break return glyphList def _createLigaGlyphList(self, anchorNames): """Return a list of (name, ((x, y), (x, y), ...)) tuples for glyphs containing anchors with given anchor names. """ glyphList = [] for glyphName, glyph in self.context.glyphSet.items(): points = [] for anchorName in anchorNames: found = False for anchor in glyph.anchors: if anchorName == anchor.name: points.append((anchor.x, anchor.y)) found = True break if not found: break if points: glyphList.append((glyphName, tuple(points))) return glyphList def _addClasses(self, lines, doMark, doMkmk): """Write class definitions for anchors used in mark and/or mkmk.""" anchorList = [] if doMark: anchorList.extend(self.context.anchorList) anchorList.extend(self.context.ligaAnchorList) if doMkmk: anchorList.extend(self.context.mkmkAnchorList) added = set() for accentAnchorName in sorted(set(n for _, n in anchorList)): added.add(accentAnchorName) self._addClass(lines, accentAnchorName) def _addClass(self, lines, accentAnchorName): """Write class definition statements for one accent anchor. Remembers the accent glyph names, for use when generating base glyph lists. """ accentGlyphs = self._createAccentGlyphList(accentAnchorName) className = self._generateClassName(accentAnchorName) accentGlyphNames = self.context.accentGlyphNames for accentName, x, y in sorted(accentGlyphs): accentGlyphNames.add(accentName) lines.append( "markClass %s %s;" % (accentName, x, y, className)) lines.append("") def _addMarkLookup(self, lines, lookupName, isMkmk, anchorPair): """Add a mark lookup for one tuple in the writer's anchor list.""" anchorName, accentAnchorName = anchorPair baseGlyphs = self._createBaseGlyphList(anchorName, isMkmk) if not baseGlyphs: return className = self._generateClassName(accentAnchorName) ruleType = "mark" if isMkmk else "base" lines.append(" lookup %s {" % lookupName) if isMkmk: mkAttachCls = "@%sMkAttach" % lookupName lines.append(" %s = %s;" % ( mkAttachCls, self.liststr([className] + [g[0] for g in baseGlyphs]))) lines.append(" lookupflag UseMarkFilteringSet %s;" % mkAttachCls) for baseName, x, y in baseGlyphs: lines.append( " pos %s %s mark %s;" % (ruleType, baseName, x, y, className)) lines.append(" } %s;" % lookupName) def _addMarkToLigaLookup(self, lines, lookupName, anchorPairs): """Add a mark lookup containing mark-to-ligature position rules.""" anchorNames, accentAnchorName = anchorPairs baseGlyphs = self._createLigaGlyphList(anchorNames) if not baseGlyphs: return className = self._generateClassName(accentAnchorName) lines.append(" lookup %s {" % lookupName) for baseName, points in baseGlyphs: lines.append(" pos ligature %s" % baseName) for x, y in points: lines.append(" mark %s" % (x, y, className)) lines.append(" ligComponent") # don't need last ligComponent statement lines.pop() lines.append(" ;") lines.append(" } %s;" % lookupName) def _addFeature(self, lines, isMkmk=False): """Write a single feature.""" anchorList = (self.context.mkmkAnchorList if isMkmk else self.context.anchorList) if not anchorList and (isMkmk or not self.context.ligaAnchorList): # nothing to do, don't write empty feature return featureName = "mkmk" if isMkmk else "mark" feature = [] for i, anchorPair in enumerate(anchorList): lookupName = "%s%d" % (featureName, i + 1) self._addMarkLookup(feature, lookupName, isMkmk, anchorPair) if not isMkmk: for i, anchorPairs in enumerate(self.context.ligaAnchorList): lookupName = "mark2liga%d" % (i + 1) self._addMarkToLigaLookup(feature, lookupName, anchorPairs) if feature: lines.append("feature %s {" % featureName) lines.extend(feature) lines.append("} %s;\n" % featureName) def setupAnchorPairs(self): """ Try to determine the base-accent anchor pairs to use in building the mark and mkmk features. **This should not be called externally.** Subclasses may override this method to set up the anchor pairs in a different way if desired. """ self.context.anchorList = anchorList = [] self.context.ligaAnchorList = ligaAnchorList = [] anchorNames = set() for glyphName, glyph in self.context.glyphSet.items(): for anchor in glyph.anchors: if anchor.name is None: logger.warning("Unnamed anchor discarded in %s", glyph.name) continue anchorNames.add(anchor.name) for baseName in sorted(anchorNames): accentName = "_" + baseName if accentName in anchorNames: anchorList.append((baseName, accentName)) ligaNames = [] i = 1 while True: ligaName = "%s_%d" % (baseName, i) if ligaName not in anchorNames: break ligaNames.append(ligaName) i += 1 if ligaNames: ligaAnchorList.append((tuple(ligaNames), accentName)) self.context.mkmkAnchorList = anchorList def _write(self): """Write mark and mkmk features, and mark class definitions.""" doMark = "mark" in self.features doMkmk = "mkmk" in self.features if not (doMark or doMkmk): return "" lines = [] self._addClasses(lines, doMark, doMkmk) if doMark: self._addFeature(lines, isMkmk=False) if doMkmk: self._addFeature(lines, isMkmk=True) return self.linesep.join(lines) ufo2ft-1.1.0/Lib/ufo2ft/filters/000077500000000000000000000000001321574316300163065ustar00rootroot00000000000000ufo2ft-1.1.0/Lib/ufo2ft/filters/__init__.py000066400000000000000000000201531321574316300204200ustar00rootroot00000000000000from __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 UFO2FT_FILTERS_KEY = "com.github.googlei18n.ufo2ft.filters" 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. """ if glyphSet is None: glyphSet = 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-1.1.0/Lib/ufo2ft/filters/cubicToQuadratic.py000066400000000000000000000026221321574316300221100ustar00rootroot00000000000000from __future__ import ( print_function, division, absolute_import, unicode_literals) from ufo2ft.filters import BaseFilter from cu2qu.ufo import DEFAULT_MAX_ERR from cu2qu.pens import Cu2QuPointPen import logging logger = logging.getLogger(__name__) class CubicToQuadraticFilter(BaseFilter): _kwargs = { 'conversionError': None, 'reverseDirection': True, } 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 * font.info.unitsPerEm ctx.stats = {} return ctx def __call__(self, font, glyphSet=None): if super(CubicToQuadraticFilter, self).__call__(font, glyphSet): stats = self.context.stats logger.info('New spline lengths: %s' % (', '.join( '%s: %d' % (l, stats[l]) for l in sorted(stats.keys())))) 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-1.1.0/Lib/ufo2ft/filters/decomposeComponents.py000066400000000000000000000025321321574316300227060ustar00rootroot00000000000000from __future__ import ( print_function, division, absolute_import, unicode_literals) from fontTools.pens.reverseContourPen import ReverseContourPen from fontTools.misc.transform import Transform, Identity from fontTools.pens.transformPen import TransformPen from ufo2ft.filters import BaseFilter class DecomposeComponentsFilter(BaseFilter): def filter(self, glyph): if not glyph.components: return False _deepCopyContours(self.context.glyphSet, glyph, glyph, Transform()) glyph.clearComponents() return True def _deepCopyContours(glyphSet, parent, component, transformation): """Copy contours from component to parent, including nested components.""" for nested in component.components: _deepCopyContours( glyphSet, parent, glyphSet[nested.baseGlyph], transformation.transform(nested.transformation)) if component != parent: 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) component.draw(pen) ufo2ft-1.1.0/Lib/ufo2ft/filters/removeOverlaps.py000066400000000000000000000012331321574316300216700ustar00rootroot00000000000000from __future__ import ( print_function, division, absolute_import, unicode_literals) from ufo2ft.filters import BaseFilter from booleanOperations import union, BooleanOperationsError import logging logger = logging.getLogger(__name__) class RemoveOverlapsFilter(BaseFilter): def filter(self, glyph): if not len(glyph): return False contours = list(glyph) glyph.clearContours() try: union(contours, glyph.getPointPen()) except BooleanOperationsError: logger.error("Failed to remove overlaps for %s", glyph.name) raise return True ufo2ft-1.1.0/Lib/ufo2ft/filters/transformations.py000066400000000000000000000121241321574316300221110ustar00rootroot00000000000000from __future__ import ( print_function, division, absolute_import, unicode_literals) import math from collections import namedtuple import logging from ufo2ft.filters import BaseFilter from fontTools.misc.py23 import round from fontTools.misc.transform import Transform, Identity from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.transformPen import TransformPen as _TransformPen # make do without the real Enum type, python3 only... :( def IntEnum(typename, field_names): return namedtuple(typename, field_names)._make( range(len(field_names))) 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): Origin = IntEnum( 'Origin', ( 'CAP_HEIGHT', 'HALF_CAP_HEIGHT', 'X_HEIGHT', 'HALF_X_HEIGHT', 'BASELINE', ) ) _kwargs = { 'OffsetX': 0, 'OffsetY': 0, 'ScaleX': 100, 'ScaleY': 100, 'Slant': 0, 'Origin': 4, # BASELINE } def start(self): if self.options.Origin not in self.Origin: raise ValueError("%r is not a valid Origin value" % 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 font.info.capHeight elif origin is self.Origin.HALF_CAP_HEIGHT: return round(font.info.capHeight/2) elif origin is self.Origin.X_HEIGHT: return font.info.xHeight elif origin is self.Origin.HALF_X_HEIGHT: return round(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-1.1.0/Lib/ufo2ft/fontInfoData.py000066400000000000000000000375431321574316300176000ustar00rootroot00000000000000""" 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 import time import unicodedata from fontTools.misc.py23 import tobytes, tostr, tounicode, unichr, round, round2 from fontTools.misc.textTools import binary2num import ufoLib logger = logging.getLogger(__name__) # ----------------- # Special Fallbacks # ----------------- # generic def styleMapFamilyNameFallback(info): """ Fallback to *openTypeNamePreferredFamilyName openTypeNamePreferredSubfamilyName*. """ familyName = getAttrWithFallback(info, "openTypeNamePreferredFamilyName") styleName = getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName") if styleName is None: styleName = "" return (familyName + " " + styleName).strip() # head def dateStringForNow(): year, month, day, hour, minute, second, weekDay, yearDay, isDST = time.localtime() year = str(year) month = str(month).zfill(2) day = str(day).zfill(2) hour = str(hour).zfill(2) minute = str(minute).zfill(2) second = str(second).zfill(2) return "%s/%s/%s %s:%s:%s" % (year, month, day, hour, minute, second) def openTypeHeadCreatedFallback(info): """ Fallback to now. """ return dateStringForNow() # hhea def openTypeHheaAscenderFallback(info): """ Fallback to *ascender + typoLineGap*. """ return info.ascender + getAttrWithFallback(info, "openTypeOS2TypoLineGap") def openTypeHheaDescenderFallback(info): """ Fallback to *descender*. """ return 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 round(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 round(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 info.familyName def openTypeNamePreferredSubfamilyNameFallback(info): """ Fallback to *styleName*. """ return 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 info.ascender def openTypeOS2TypoDescenderFallback(info): """ Fallback to *descender*. """ return info.descender def openTypeOS2TypoLineGapFallback(info): """ Fallback to *UPM * 1.2 - ascender + descender*, or zero if that's negative. """ return max(int(info.unitsPerEm * 1.2) - info.ascender + info.descender, 0) def openTypeOS2WinAscentFallback(info): """ Fallback to *ascender + typoLineGap*. """ return info.ascender + getAttrWithFallback(info, "openTypeOS2TypoLineGap") def openTypeOS2WinDescentFallback(info): """ Fallback to *descender*. """ return abs(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.warning( 'Underline thickness not set in UFO, defaulting to UPM * 0.05') return info.unitsPerEm * 0.05 def postscriptUnderlinePositionFallback(info): """Return UPM * -0.075 (-75 for 1000 UPM) and warn.""" logger.warning( 'Underline position not set in UFO, defaulting to UPM * -0.075') return info.unitsPerEm * -0.075 _postscriptWeightNameOptions = { 100 : "Thin", 200 : "Extra-light", 300 : "Light", 400 : "Normal", 500 : "Medium", 600 : "Semi-bold", 700 : "Bold", 800 : "Extra-bold", 900 : "Black" } def postscriptWeightNameFallback(info): """ Fallback to the closest match of the *openTypeOS2WeightClass* in this table: === =========== 100 Thin 200 Extra-light 300 Light 400 Normal 500 Medium 600 Semi-bold 700 Bold 800 Extra-bold 900 Black === =========== """ value = getAttrWithFallback(info, "openTypeOS2WeightClass") value = int(round2(value, -2)) if value < 100: value = 100 elif value > 900: value = 900 name = _postscriptWeightNameOptions[value] return name 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( styleMapStyleName="regular", versionMajor=0, versionMinor=0, copyright=None, trademark=None, 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=[], openTypeOS2CodePageRanges=[], 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, openTypeVheaCaretSlopeRise=None, openTypeVheaCaretSlopeRun=None, openTypeVheaCaretOffset=None, postscriptUniqueID=None, postscriptIsFixedPitch=False, postscriptBlueValues=[], postscriptOtherBlues=[], postscriptFamilyBlues=[], postscriptFamilyOtherBlues=[], postscriptStemSnapH=[], postscriptStemSnapV=[], postscriptBlueFuzz=0, postscriptBlueShift=7, postscriptForceBold=False, postscriptDefaultWidthX=200, postscriptNominalWidthX=0, # not used in OTF postscriptDefaultCharacter=None, postscriptWindowsCharacterSet=None, # not used in OTF macintoshFONDFamilyID=None, macintoshFONDName=None ) specialFallbacks = dict( styleMapFamilyName=styleMapFamilyNameFallback, 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, postscriptWeightName=postscriptWeightNameFallback, 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 int(time.mktime(t)) except OverflowError: return 0 ufo2ft-1.1.0/Lib/ufo2ft/kernFeatureWriter.py000066400000000000000000000003201321574316300206530ustar00rootroot00000000000000"""This module is deprecated! It's kept here only for backward compatibility. Please import the new ufo2ft.featureWriters module. """ from ufo2ft.featureWriters.kernFeatureWriter import * # pragma: no cover ufo2ft-1.1.0/Lib/ufo2ft/markFeatureWriter.py000066400000000000000000000003201321574316300206460ustar00rootroot00000000000000"""This module is deprecated! It's kept here only for backward compatibility. Please import the new ufo2ft.featureWriters module. """ from ufo2ft.featureWriters.markFeatureWriter import * # pragma: no cover ufo2ft-1.1.0/Lib/ufo2ft/maxContextCalc.py000066400000000000000000000063701321574316300201330ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals __all__ = ['maxCtxFont'] def maxCtxFont(font): """Calculate the usMaxContext value for an entire font.""" maxCtx = 0 for tag in ('GSUB', 'GPOS'): if tag not in font: continue table = font[tag].table if table.LookupList is None: continue for lookup in table.LookupList.Lookup: for st in lookup.SubTable: maxCtx = maxCtxSubtable(maxCtx, tag, lookup.LookupType, st) return maxCtx def maxCtxSubtable(maxCtx, tag, lookupType, st): """Calculate usMaxContext based on a single lookup table (and an existing max value). """ # single positioning, single / multiple substitution if (tag == 'GPOS' and lookupType == 1) or ( tag == 'GSUB' and lookupType in (1, 2, 3)): maxCtx = max(maxCtx, 1) # pair positioning elif tag == 'GPOS' and lookupType == 2: maxCtx = max(maxCtx, 2) # ligatures elif tag == 'GSUB' and lookupType == 4: for ligatures in st.ligatures.values(): for ligature in ligatures: maxCtx = max(maxCtx, ligature.CompCount) # context elif (tag == 'GPOS' and lookupType == 7) or ( tag == 'GSUB' and lookupType == 5): maxCtx = maxCtxContextualSubtable( maxCtx, st, 'Pos' if tag == 'GPOS' else 'Sub') # chained context elif (tag == 'GPOS' and lookupType == 8) or ( tag == 'GSUB' and lookupType == 6): maxCtx = maxCtxContextualSubtable( maxCtx, st, 'Pos' if tag == 'GPOS' else 'Sub', 'Chain') # extensions elif (tag == 'GPOS' and lookupType == 9) or ( tag == 'GSUB' and lookupType == 7): maxCtx = maxCtxSubtable( maxCtx, tag, st.ExtensionLookupType, st.ExtSubTable) # reverse-chained context elif tag == 'GSUB' and lookupType == 8: maxCtx = maxCtxContextualRule(maxCtx, st, 'Reverse') return maxCtx def maxCtxContextualSubtable(maxCtx, st, ruleType, chain=''): """Calculate usMaxContext based on a contextual feature subtable.""" if st.Format == 1: for ruleset in getattr(st, '%s%sRuleSet' % (chain, ruleType)): if ruleset is None: continue for rule in getattr(ruleset, '%s%sRule' % (chain, ruleType)): if rule is None: continue maxCtx = maxCtxContextualRule(maxCtx, rule, chain) elif st.Format == 2: for ruleset in getattr(st, '%s%sClassSet' % (chain, ruleType)): if ruleset is None: continue for rule in getattr(ruleset, '%s%sClassRule' % (chain, ruleType)): if rule is None: continue maxCtx = maxCtxContextualRule(maxCtx, rule, chain) elif st.Format == 3: maxCtx = maxCtxContextualRule(maxCtx, st, chain) return maxCtx def maxCtxContextualRule(maxCtx, st, chain): """Calculate usMaxContext based on a contextual feature rule.""" if not chain: return max(maxCtx, st.GlyphCount) elif chain == 'Reverse': return max(maxCtx, st.GlyphCount + st.LookAheadGlyphCount) return max(maxCtx, st.InputGlyphCount + st.LookAheadGlyphCount) ufo2ft-1.1.0/Lib/ufo2ft/outlineCompiler.py000066400000000000000000001325051321574316300203700ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import tounicode, round, BytesIO import logging import math from collections import Counter, namedtuple 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 ufo2ft.fontInfoData import ( getAttrWithFallback, dateStringToTimeValue, dateStringForNow, intListToNum, normalizeStringForPostscript) from ufo2ft.util import makeOfficialGlyphOrder logger = logging.getLogger(__name__) BoundingBox = namedtuple("BoundingBox", ["xMin", "yMin", "xMax", "yMax"]) def _isNonBMP(s): for c in s: if ord(c) > 65535: return True return False def _getVerticalOrigin(glyph): height = glyph.height if (hasattr(glyph, "verticalOrigin") and glyph.verticalOrigin is not None): verticalOrigin = glyph.verticalOrigin else: verticalOrigin = height return round(verticalOrigin) class BaseOutlineCompiler(object): """Create a feature-less outline binary.""" sfntVersion = None def __init__(self, font, glyphSet=None, glyphOrder=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 reusable glyphs/font bounding boxes self.glyphBoundingBoxes = self.makeGlyphsBoundingBoxes() self.fontBoundingBox = self.makeFontBoundingBox() # make a reusable character mapping self.unicodeToGlyphNameMapping = self.makeUnicodeToGlyphNameMapping() def compile(self): """ Compile the OpenType binary. """ self.otf = TTFont(sfntVersion=self.sfntVersion) self.vertical = False for glyph in self.allGlyphs.values(): if (hasattr(glyph, "verticalOrigin") and glyph.verticalOrigin is not None): self.vertical = True break # 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() self.setupOtherTables() self.importTTX() return self.otf 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. Float values are rounded to integers using the built-in round(). **This should not be called externally.** Subclasses may override this method to handle the bounds creation in a different way if desired. """ def getControlPointBounds(glyph): pen.init() glyph.draw(pen) return pen.bounds glyphBoxes = {} pen = ControlBoundsPen(self.allGlyphs) for glyphName, glyph in self.allGlyphs.items(): bounds = None if glyph or glyph.components: bounds = getControlPointBounds(glyph) if bounds: bounds = BoundingBox(*(round(v) for v in bounds)) glyphBoxes[glyphName] = bounds return glyphBoxes 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. """ if not hasattr(self, "glyphBoundingBoxes"): self.glyphBoundingBoxes = self.makeGlyphsBoundingBoxes() 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 = BoundingBox(0, 0, 0, 0) return fontBox 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. """ mapping = {} for glyphName, glyph in self.allGlyphs.items(): unicodes = glyph.unicodes for uni in unicodes: mapping[uni] = glyphName return mapping @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 = round(getAttrWithFallback(font.info, "unitsPerEm")) ascender = round(getAttrWithFallback(font.info, "ascender")) descender = round(getAttrWithFallback(font.info, "descender")) defaultWidth = round(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): 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. """ 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 = round(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 = round(xMin) head.yMin = round(yMin) head.xMax = round(xMax) head.yMax = round(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 = round(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. """ 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. """ 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. """ self.otf["OS/2"] = os2 = newTable("OS/2") font = self.ufo os2.version = 0x0004 # average glyph width widths = [glyph.width for glyph in self.allGlyphs.values() if glyph.width > 0] os2.xAvgCharWidth = round(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 = round(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptYSize") if v is None: v = unitsPerEm * 0.6 os2.ySubscriptYSize = round(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptYOffset") if v is None: v = unitsPerEm * 0.075 os2.ySubscriptYOffset = round(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptXOffset") if v is None: v = adjustOffset(-os2.ySubscriptYOffset, italicAngle) os2.ySubscriptXOffset = round(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptXSize") if v is None: v = os2.ySubscriptXSize os2.ySuperscriptXSize = round(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptYSize") if v is None: v = os2.ySubscriptYSize os2.ySuperscriptYSize = round(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptYOffset") if v is None: v = unitsPerEm * 0.35 os2.ySuperscriptYOffset = round(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptXOffset") if v is None: v = adjustOffset(os2.ySuperscriptYOffset, italicAngle) os2.ySuperscriptXOffset = round(v) v = getAttrWithFallback(font.info, "openTypeOS2StrikeoutSize") if v is None: v = getAttrWithFallback(font.info, "postscriptUnderlineThickness") os2.yStrikeoutSize = round(v) v = getAttrWithFallback(font.info, "openTypeOS2StrikeoutPosition") if v is None: v = xHeight * 0.6 if xHeight else unitsPerEm * 0.22 os2.yStrikeoutPosition = round(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") os2.ulUnicodeRange1 = intListToNum(uniRanges, 0, 32) os2.ulUnicodeRange2 = intListToNum(uniRanges, 32, 32) os2.ulUnicodeRange3 = intListToNum(uniRanges, 64, 32) os2.ulUnicodeRange4 = intListToNum(uniRanges, 96, 32) # codepage ranges codepageRanges = getAttrWithFallback(font.info, "openTypeOS2CodePageRanges") 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 = round(getAttrWithFallback(font.info, "xHeight")) os2.sCapHeight = round(getAttrWithFallback(font.info, "capHeight")) os2.sTypoAscender = round(getAttrWithFallback(font.info, "openTypeOS2TypoAscender")) os2.sTypoDescender = round(getAttrWithFallback(font.info, "openTypeOS2TypoDescender")) os2.sTypoLineGap = round(getAttrWithFallback(font.info, "openTypeOS2TypoLineGap")) os2.usWinAscent = round(getAttrWithFallback(font.info, "openTypeOS2WinAscent")) os2.usWinDescent = round(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. """ self.otf["hmtx"] = hmtx = newTable("hmtx") hmtx.metrics = {} for glyphName, glyph in self.allGlyphs.items(): width = 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] = (round(width), left) 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.otf["hhea"] = hhea = newTable("hhea") hmtx = self.otf["hmtx"] font = self.ufo hhea.tableVersion = 0x00010000 # vertical metrics hhea.ascent = round(getAttrWithFallback(font.info, "openTypeHheaAscender")) hhea.descent = round(getAttrWithFallback(font.info, "openTypeHheaDescender")) hhea.lineGap = round(getAttrWithFallback(font.info, "openTypeHheaLineGap")) # horizontal metrics widths = [] lefts = [] rights = [] extents = [] for glyphName in self.allGlyphs: width, left = hmtx[glyphName] widths.append(width) bounds = self.glyphBoundingBoxes[glyphName] if bounds is None: continue right = width - left - (bounds.xMax - bounds.xMin) lefts.append(left) rights.append(right) # equation from the hhea spec for calculating xMaxExtent: # Max(lsb + (xMax - xMin)) extent = left + (bounds.xMax - bounds.xMin) extents.append(extent) hhea.advanceWidthMax = max(widths) hhea.minLeftSideBearing = min(lefts) hhea.minRightSideBearing = min(rights) hhea.xMaxExtent = max(extents) # misc hhea.caretSlopeRise = getAttrWithFallback(font.info, "openTypeHheaCaretSlopeRise") hhea.caretSlopeRun = getAttrWithFallback(font.info, "openTypeHheaCaretSlopeRun") hhea.caretOffset = round(getAttrWithFallback(font.info, "openTypeHheaCaretOffset")) hhea.reserved0 = 0 hhea.reserved1 = 0 hhea.reserved2 = 0 hhea.reserved3 = 0 hhea.metricDataFormat = 0 # glyph count hhea.numberOfHMetrics = len(self.allGlyphs) 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. """ self.otf["vmtx"] = vmtx = newTable("vmtx") vmtx.metrics = {} for glyphName, glyph in self.allGlyphs.items(): height = glyph.height verticalOrigin = _getVerticalOrigin(glyph) bounds = self.glyphBoundingBoxes[glyphName] top = bounds.yMax if bounds else 0 vmtx[glyphName] = (round(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. """ self.otf["VORG"] = vorg = newTable("VORG") vorg.majorVersion = 1 vorg.minorVersion = 0 vorg.VOriginRecords = {} # Find the most frequent verticalOrigin vorg_count = Counter(_getVerticalOrigin(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(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.otf["vhea"] = vhea = newTable("vhea") font = self.ufo head = self.otf["head"] vmtx = self.otf["vmtx"] vhea.tableVersion = 0x00011000 # horizontal metrics vhea.ascent = round(getAttrWithFallback(font.info, "openTypeVheaVertTypoAscender")) vhea.descent = round(getAttrWithFallback(font.info, "openTypeVheaVertTypoDescender")) vhea.lineGap = round(getAttrWithFallback(font.info, "openTypeVheaVertTypoLineGap")) # vertical metrics heights = [] tops = [] bottoms = [] for glyphName in self.allGlyphs: height, top = vmtx[glyphName] heights.append(height) bounds = self.glyphBoundingBoxes[glyphName] if bounds is None: continue bottom = height - top - (bounds.yMax - bounds.yMin) tops.append(top) bottoms.append(bottom) vhea.advanceHeightMax = max(heights) vhea.minTopSideBearing = max(tops) vhea.minBottomSideBearing = max(bottoms) vhea.yMaxExtent = vhea.minTopSideBearing - (head.yMax - head.yMin) # misc vhea.caretSlopeRise = getAttrWithFallback(font.info, "openTypeVheaCaretSlopeRise") vhea.caretSlopeRun = getAttrWithFallback(font.info, "openTypeVheaCaretSlopeRun") vhea.caretOffset = getAttrWithFallback(font.info, "openTypeVheaCaretOffset") vhea.reserved0 = 0 vhea.reserved1 = 0 vhea.reserved2 = 0 vhea.reserved3 = 0 vhea.reserved4 = 0 vhea.metricDataFormat = 0 # glyph count vhea.numberOfVMetrics = len(self.allGlyphs) 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. """ 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 = round(underlinePosition) underlineThickness = getAttrWithFallback(font.info, "postscriptUnderlineThickness") post.underlineThickness = round(underlineThickness) # determine if the font has a fixed width widths = set([glyph.width for glyph in self.allGlyphs.values()]) post.isFixedPitch = getAttrWithFallback(font.info, "postscriptIsFixedPitch") # misc post.minMemType42 = 0 post.maxMemType42 = 0 post.minMemType1 = 0 post.maxMemType1 = 0 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" def __init__(self, font, glyphSet=None, glyphOrder=None, roundTolerance=None): 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) 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 getControlPointBounds(glyph): pen.init() glyph.draw(pen) return pen.bounds def toInt(value, else_callback): rounded = round(value) if tolerance >= 0.5 or abs(rounded - value) <= tolerance: return rounded else: return int(else_callback(value)) tolerance = self.roundTolerance glyphBoxes = {} pen = ControlBoundsPen(self.allGlyphs) for glyphName, glyph in self.allGlyphs.items(): bounds = None if glyph or glyph.components: bounds = getControlPointBounds(glyph) if bounds: 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) glyphBoxes[glyphName] = bounds return glyphBoxes def getCharStringForGlyph(self, glyph, private, globalSubrs): """ 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 # subtract the nominal width postscriptNominalWidthX = getAttrWithFallback(self.ufo.info, "postscriptNominalWidthX") if postscriptNominalWidthX: width = width - postscriptNominalWidthX # round width = round(width) pen = T2CharStringPen(width, self.allGlyphs, roundTolerance=self.roundTolerance) glyph.draw(pen) charString = pen.getCharString(private, globalSubrs) return charString def setupTable_maxp(self): """Make the maxp table.""" self.otf["maxp"] = maxp = newTable("maxp") maxp.tableVersion = 0x00005000 def setupOtherTables(self): self.setupTable_CFF() if self.vertical: self.setupTable_VORG() def setupTable_CFF(self): """Make the CFF table.""" 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.warning("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.warning("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") topDict.FontName = psName # populate various numbers topDict.isFixedPitch = getAttrWithFallback(info, "postscriptIsFixedPitch") topDict.ItalicAngle = getAttrWithFallback(info, "italicAngle") underlinePosition = getAttrWithFallback(info, "postscriptUnderlinePosition") topDict.UnderlinePosition = round(underlinePosition) underlineThickness = getAttrWithFallback(info, "postscriptUnderlineThickness") topDict.UnderlineThickness = round(underlineThickness) # populate font matrix unitsPerEm = round(getAttrWithFallback(info, "unitsPerEm")) topDict.FontMatrix = [1.0 / unitsPerEm, 0, 0, 1.0 / unitsPerEm, 0, 0] # populate the width values defaultWidthX = round(getAttrWithFallback(info, "postscriptDefaultWidthX")) if defaultWidthX: private.rawDict["defaultWidthX"] = defaultWidthX nominalWidthX = round(getAttrWithFallback(info, "postscriptNominalWidthX")) if nominalWidthX: private.rawDict["nominalWidthX"] = nominalWidthX # populate hint data blueFuzz = round(getAttrWithFallback(info, "postscriptBlueFuzz")) blueShift = round(getAttrWithFallback(info, "postscriptBlueShift")) blueScale = getAttrWithFallback(info, "postscriptBlueScale") forceBold = getAttrWithFallback(info, "postscriptForceBold") blueValues = getAttrWithFallback(info, "postscriptBlueValues") if isinstance(blueValues, list): blueValues = [round(i) for i in blueValues] otherBlues = getAttrWithFallback(info, "postscriptOtherBlues") if isinstance(otherBlues, list): otherBlues = [round(i) for i in otherBlues] familyBlues = getAttrWithFallback(info, "postscriptFamilyBlues") if isinstance(familyBlues, list): familyBlues = [round(i) for i in familyBlues] familyOtherBlues = getAttrWithFallback(info, "postscriptFamilyOtherBlues") if isinstance(familyOtherBlues, list): familyOtherBlues = [round(i) for i in familyOtherBlues] stemSnapH = getAttrWithFallback(info, "postscriptStemSnapH") if isinstance(stemSnapH, list): stemSnapH = [round(i) for i in stemSnapH] stemSnapV = getAttrWithFallback(info, "postscriptStemSnapV") if isinstance(stemSnapV, list): stemSnapV = [round(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 for glyphName in self.glyphOrder: glyph = self.allGlyphs[glyphName] charString = self.getCharStringForGlyph(glyph, private, 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 # write the glyph order self.otf.setGlyphOrder(self.glyphOrder) class OutlineTTFCompiler(BaseOutlineCompiler): """Compile a .ttf font with TrueType outlines. """ sfntVersion = "\000\001\000\000" def setupTable_maxp(self): """Make the maxp table.""" self.otf["maxp"] = maxp = newTable("maxp") maxp.tableVersion = 0x00010000 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() 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.""" self.otf["loca"] = newTable("loca") self.otf["glyf"] = glyf = newTable("glyf") glyf.glyphs = {} glyf.glyphOrder = self.glyphOrder allGlyphs = self.allGlyphs 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() if ttGlyph.isComposite() and self.autoUseMyMetrics: self.autoUseMyMetrics(ttGlyph, glyph.width, allGlyphs) glyf[name] = ttGlyph @staticmethod def autoUseMyMetrics(ttGlyph, width, glyphSet): """ 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. """ for component in ttGlyph.components: try: baseName, transform = component.getComponentInfo() except AttributeError: # component uses '{first,second}Pt' instead of 'x' and 'y' continue if (glyphSet[baseName].width == 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 = [] 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 def _get_leftMargin(self): if self.bounds is None: return 0 return self.bounds[0] leftMargin = property(_get_leftMargin) def _get_rightMargin(self): bounds = self.bounds if bounds is None: return 0 xMin, yMin, xMax, yMax = bounds return self.width - bounds[2] rightMargin = property(_get_rightMargin) def draw(self, pen): pass def _drawDefaultNotdef(self, pen): width = round(self.unitsPerEm * 0.5) stroke = round(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-1.1.0/Lib/ufo2ft/postProcessor.py000066400000000000000000000101421321574316300200730ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import BytesIO from fontTools.ttLib import TTFont UFO2FT_PREFIX = 'com.github.googlei18n.ufo2ft.' class PostProcessor(object): """Does some post-processing operations on a compiled OpenType font, using info from the source UFO where necessary. """ def __init__(self, otf, ufo): self.ufo = 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: Rename glyphs using using 'public.postscriptNames' in UFO lib, if present. Else, generate uniXXXX names from the glyphs' unicode. If 'com.github.googlei18n.ufo2ft.useProductionNames' key in the UFO lib is present and is set to False, do not modify the glyph names. optimizeCFF: Run compreffor to subroubtinize CFF table, if present. """ if useProductionNames is None: useProductionNames = self.ufo.lib.get( UFO2FT_PREFIX + "useProductionNames", True) if useProductionNames: self._rename_glyphs_from_ufo() if optimizeCFF and 'CFF ' in self.otf: from compreffor import compress compress(self.otf) return self.otf def _rename_glyphs_from_ufo(self): """Rename glyphs using ufo.lib.public.postscriptNames in UFO.""" rename_map = { g.name: self._build_production_name(g) for g in self.ufo} # .notdef may not be present in the original font rename_map[".notdef"] = ".notdef" rename = lambda names: [rename_map[n] for n in names] otf = self.otf otf.setGlyphOrder(rename(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'].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(cff.charset) 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.ufo: return '%s.%s' % ( self._build_production_name(self.ufo[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.ufo for n in liga_parts): unicode_vals = [self.ufo[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.ufo[n]) for n in liga_parts) return glyph.name ufo2ft-1.1.0/Lib/ufo2ft/preProcessor.py000066400000000000000000000143221321574316300177000ustar00rootroot00000000000000from __future__ import ( print_function, division, absolute_import, unicode_literals) from ufo2ft.filters import loadFilters from ufo2ft.filters.decomposeComponents import DecomposeComponentsFilter from copy import deepcopy 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, **kwargs): self.ufo = ufo if inplace: self.glyphSet = {g.name: g for g in ufo} else: self.glyphSet = {g.name: _copyGlyph(g) for g in ufo} self.defaultFilters = self.initDefaultFilters(**kwargs) self.preFilters, self.postFilters = loadFilters(ufo) def initDefaultFilters(self, **kwargs): return [] 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. """ def initDefaultFilters(self, removeOverlaps=False): filters = [DecomposeComponentsFilter()] if removeOverlaps: from ufo2ft.filters.removeOverlaps import RemoveOverlapsFilter 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, 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. """ def initDefaultFilters(self, removeOverlaps=False, convertCubics=True, conversionError=None, reverseDirection=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 filters.append(RemoveOverlapsFilter()) if convertCubics: from ufo2ft.filters.cubicToQuadratic import CubicToQuadraticFilter filters.append( CubicToQuadraticFilter(conversionError=conversionError, reverseDirection=reverseDirection)) 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. Currently, only the conversion from cubic to quadratic, and the decomposition of mixed contour/component glyphs is performed, and no additional custom filter is applied. The ``conversionError`` and ``reverseDirection`` arguments work in the same way as in the ``TTFPreProcessor``. """ def __init__(self, ufos, inplace=False, conversionError=None, reverseDirection=True): from cu2qu.ufo import DEFAULT_MAX_ERR self.ufos = ufos self.glyphSets = [ {g.name: (_copyGlyph(g) if not inplace else g) for g in ufo} for ufo in ufos] self._conversionErrors = [ (conversionError or DEFAULT_MAX_ERR) * ufo.info.unitsPerEm for ufo in ufos] self._reverseDirection = reverseDirection def process(self): from cu2qu.ufo import fonts_to_quadratic fonts_to_quadratic(self.glyphSets, max_err=self._conversionErrors, reverse_direction=self._reverseDirection, dump_stats=True) decompose = DecomposeComponentsFilter(include=lambda g: len(g)) for ufo, glyphSet in zip(self.ufos, self.glyphSets): decompose(ufo, glyphSet) return self.glyphSets def _copyGlyph(glyph): # copy everything except unused attributes: 'guidelines', 'note', 'image' copy = glyph.__class__() copy.name = 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) return copy ufo2ft-1.1.0/Lib/ufo2ft/util.py000066400000000000000000000014551321574316300161720ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import 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 ufo2ft-1.1.0/MANIFEST.in000066400000000000000000000002441321574316300144610ustar00rootroot00000000000000include README.rst include LICENSE include tox.ini include *requirements.txt recursive-include tests *.py recursive-include tests/data *.glif *.plist *.fea *.ttx ufo2ft-1.1.0/README.rst000066400000000000000000000105521321574316300144150ustar00rootroot00000000000000|Travis CI Status| |Appveyor CI Status| |PyPI Version| |Codecov| 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/googlei18n/ufo2ft.svg :target: https://travis-ci.org/googlei18n/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/googlei18n/ufo2ft/branch/master/graph/badge.svg :target: https://codecov.io/gh/googlei18n/ufo2ft ufo2ft-1.1.0/appveyor.yml000066400000000000000000000035061321574316300153170ustar00rootroot00000000000000environment: matrix: - JOB: "2.7 32-bit" PYTHON_HOME: "C:\\Python27" TOXENV: "py27-cov" TOXPYTHON: "C:\\Python27\\python.exe" - JOB: "3.6 32-bit" PYTHON_HOME: "C:\\Python36" TOXENV: "py36-cov" TOXPYTHON: "C:\\Python36\\python.exe" - JOB: "2.7 64-bit" PYTHON_HOME: "C:\\Python27-x64" TOXENV: "py27-cov" TOXPYTHON: "C:\\Python27-x64\\python.exe" - JOB: "3.6 64-bit" PYTHON_HOME: "C:\\Python35-x64" TOXENV: "py36-cov" TOXPYTHON: "C:\\Python36-x64\\python.exe" 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-1.1.0/dev-requirements.txt000066400000000000000000000000461321574316300167630ustar00rootroot00000000000000pytest>=2.8 virtualenv>=15.0 tox>=2.3 ufo2ft-1.1.0/requirements.txt000066400000000000000000000001461321574316300162100ustar00rootroot00000000000000fonttools==3.20.1 ufoLib==2.1.1 defcon==0.3.5 cu2qu==1.3.0 compreffor==0.4.6 booleanOperations==0.7.1 ufo2ft-1.1.0/setup.cfg000066400000000000000000000014561321574316300145520ustar00rootroot00000000000000[bumpversion] current_version = 1.1.0 commit = True tag = False tag_name = v{new_version} parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? serialize = {major}.{minor}.{patch}.{release}{dev} {major}.{minor}.{patch} [bumpversion:part:release] optional_value = final values = dev final [bumpversion:part:dev] [bumpversion:file:Lib/ufo2ft/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" [bumpversion:file:setup.py] search = version="{current_version}" replace = version="{new_version}" [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 = -v -r a ufo2ft-1.1.0/setup.py000066400000000000000000000146661321574316300144520ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import print_function, division, absolute_import import sys from setuptools import setup, find_packages, Command from distutils import log class bump_version(Command): description = "increment the package version and commit the changes" user_options = [ ("major", None, "bump the first digit, for incompatible API changes"), ("minor", None, "bump the second digit, for new backward-compatible features"), ("patch", None, "bump the third digit, for bug fixes (default)"), ] def initialize_options(self): self.minor = False self.major = False self.patch = False def finalize_options(self): part = None for attr in ("major", "minor", "patch"): if getattr(self, attr, False): if part is None: part = attr else: from distutils.errors import DistutilsOptionError raise DistutilsOptionError( "version part options are mutually exclusive") self.part = part or "patch" def bumpversion(self, part, **kwargs): """ Run bumpversion.main() with the specified arguments. """ import bumpversion args = ['--verbose'] if self.verbose > 1 else [] for k, v in kwargs.items(): k = "--{}".format(k.replace("_", "-")) is_bool = isinstance(v, bool) and v is True args.extend([k] if is_bool else [k, str(v)]) args.append(part) log.debug( "$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args)) bumpversion.main(args) def run(self): log.info("bumping '%s' version" % self.part) self.bumpversion(self.part) class release(bump_version): """Drop the developmental release '.devN' suffix from the package version, open the default text $EDITOR to write release notes, commit the changes and generate a git tag. Release notes can also be set with the -m/--message option, or by reading from standard input. """ description = "tag a new release" user_options = [ ("message=", 'm', "message containing the release notes"), ] def initialize_options(self): self.message = None def finalize_options(self): import re current_version = self.distribution.metadata.get_version() if not re.search(r"\.dev[0-9]+", current_version): from distutils.errors import DistutilsSetupError raise DistutilsSetupError( "current version (%s) has no '.devN' suffix.\n " "Run 'setup.py bump_version' with any of " "--major, --minor, --patch options" % current_version) message = self.message if message is None: if sys.stdin.isatty(): # stdin is interactive, use editor to write release notes message = self.edit_release_notes() else: # read release notes from stdin pipe message = sys.stdin.read() if not message.strip(): from distutils.errors import DistutilsSetupError raise DistutilsSetupError("release notes message is empty") self.message = "v{new_version}\n\n%s" % message @staticmethod def edit_release_notes(): """Use the default text $EDITOR to write release notes. If $EDITOR is not set, use 'nano'.""" from tempfile import mkstemp import os import shlex import subprocess text_editor = shlex.split(os.environ.get('EDITOR', 'nano')) fd, tmp = mkstemp(prefix='bumpversion-') try: os.close(fd) with open(tmp, 'w') as f: f.write("\n\n# Write release notes.\n" "# Lines starting with '#' will be ignored.") subprocess.check_call(text_editor + [tmp]) with open(tmp, 'r') as f: changes = "".join( l for l in f.readlines() if not l.startswith('#')) finally: os.remove(tmp) return changes def run(self): log.info("stripping developmental release suffix") # drop '.dev0' suffix, commit with given message and create git tag self.bumpversion("release", tag=True, message="Release {new_version}", tag_message=self.message) 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 [] needs_bump2version = {'release', 'bump_version'}.intersection(sys.argv) bump2version = ['bump2version'] if needs_bump2version else [] with open('README.rst', 'r') as f: long_description = f.read() setup( name="ufo2ft", version="1.1.0", author="Tal Leming, James Godfrey-Kittle", author_email="tal@typesupply.com", maintainer="James Godfrey-Kittle", maintainer_email="jamesgk@google.com", description="A bridge between UFOs and FontTools.", long_description=long_description, url="https://github.com/googlei18n/ufo2ft", package_dir={"": "Lib"}, packages=find_packages("Lib"), include_package_data=True, license="MIT", setup_requires=pytest_runner + wheel + bump2version, tests_require=[ 'pytest>=2.8', ], install_requires=[ "fonttools>=3.17.0", "ufoLib>=2.1.0", "defcon>=0.3.4", "cu2qu>=1.2.0", "compreffor>=0.4.5", "booleanOperations>=0.7.1", ], cmdclass={ "release": release, "bump_version": bump_version, }, 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-1.1.0/tests/000077500000000000000000000000001321574316300140655ustar00rootroot00000000000000ufo2ft-1.1.0/tests/compiler_test.py000066400000000000000000000121231321574316300173070ustar00rootroot00000000000000from __future__ import \ print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import * from defcon import Font from ufo2ft import compileOTF, compileTTF, compileInterpolatableTTFs from ufo2ft.featureWriters import KernFeatureWriter, MarkFeatureWriter import warnings import difflib import os import sys import tempfile import unittest def getpath(filename): dirname = os.path.dirname(__file__) return os.path.join(dirname, 'data', filename) def loadUFO(filename): return Font(getpath(filename)) class CompilerTest(unittest.TestCase): _tempdir, _num_tempfiles = None, 0 _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): self.expectTTX(compileTTF(loadUFO("TestFont.ufo")), "TestFont.ttx") def test_TestFont_CFF(self): self.expectTTX(compileOTF(loadUFO("TestFont.ufo")), "TestFont-CFF.ttx") def test_deprecated_arguments(self): ufo = loadUFO("TestFont.ufo") with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", UserWarning) compileTTF(ufo, kernWriterClass=KernFeatureWriter) self.assertEqual(len(w), 1) self.assertEqual(w[-1].category, UserWarning) self.assertIn( "'kernWriterClass' is deprecated; use 'featureWriters'", str(w[-1].message)) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", UserWarning) compileOTF(ufo, markWriterClass=MarkFeatureWriter) self.assertEqual(len(w), 1) self.assertEqual(w[-1].category, UserWarning) self.assertIn( "'markWriterClass' is deprecated; use 'featureWriters'", str(w[-1].message)) with self.assertRaises(TypeError): compileTTF(ufo, kernWriterClass=KernFeatureWriter, featureWriters=[MarkFeatureWriter]) def test_features(self): """Checks how the compiler handles 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. The compiler should only inject auto-generated features (kern, mark, mkmk) if the UFO does not list them in features.fea. https://github.com/googlei18n/ufo2ft/issues/108 """ self.expectTTX(compileTTF(loadUFO("Bug108.ufo")), "Bug108.ttx", tables=self._layoutTables) def test_mti_features(self): """Checks handling of UFOs with embdedded MTI/Monotype feature files https://github.com/googlei18n/fontmake/issues/289 """ self.expectTTX(compileTTF(loadUFO("MTIFeatures.ufo")), "MTIFeatures.ttx", tables=self._layoutTables) def test_removeOverlaps_CFF(self): self.expectTTX(compileOTF(loadUFO("TestFont.ufo"), removeOverlaps=True), "TestFont-NoOverlaps-CFF.ttx") def test_removeOverlaps(self): self.expectTTX(compileTTF(loadUFO("TestFont.ufo"), removeOverlaps=True), "TestFont-NoOverlaps-TTF.ttx") def test_interpolatableTTFs_lazy(self): # two same UFOs **must** be interpolatable ufos = [loadUFO("TestFont.ufo") for _ in range(2)] ttfs = list(compileInterpolatableTTFs(ufos)) self.expectTTX(ttfs[0], "TestFont.ttx") self.expectTTX(ttfs[1], "TestFont.ttx") def _temppath(self, suffix): if not self._tempdir: self._tempdir = tempfile.mkdtemp() self._num_tempfiles += 1 return os.path.join(self._tempdir, "tmp%d%s" % (self._num_tempfiles, suffix)) def _readTTX(self, path): lines = [] with open(path, "r", encoding="utf-8") as ttx: for line in ttx.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(self, font, expectedTTX, tables=None): expected = self._readTTX(getpath(expectedTTX)) font.recalcTimestamp = False font['head'].created, font['head'].modified = 3570196637, 3601822698 font['head'].checkSumAdjustment = 0x12345678 path = self._temppath(suffix=".ttx") font.saveXML(path, tables=tables) actual = self._readTTX(path) if actual != expected: for line in difflib.unified_diff( expected, actual, fromfile=expectedTTX, tofile=path): sys.stderr.write(line) self.fail("TTX output is different from expected") if __name__ == "__main__": sys.exit(unittest.main()) ufo2ft-1.1.0/tests/data/000077500000000000000000000000001321574316300147765ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/Bug108.ttx000066400000000000000000000030101321574316300164770ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/Bug108.ufo/000077500000000000000000000000001321574316300165345ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/Bug108.ufo/data/000077500000000000000000000000001321574316300174455ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/Bug108.ufo/data/included.fea000066400000000000000000000000461321574316300217110ustar00rootroot00000000000000feature kern { pos a b -10; } kern; ufo2ft-1.1.0/tests/data/Bug108.ufo/features.fea000066400000000000000000000000331321574316300210230ustar00rootroot00000000000000include(data/included.fea) ufo2ft-1.1.0/tests/data/Bug108.ufo/fontinfo.plist000066400000000000000000000012731321574316300214360ustar00rootroot00000000000000 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-1.1.0/tests/data/Bug108.ufo/glyphs/000077500000000000000000000000001321574316300200425ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/Bug108.ufo/glyphs/_notdef.glif000066400000000000000000000007531321574316300223300ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/Bug108.ufo/glyphs/a.glif000066400000000000000000000004361321574316300211300ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/Bug108.ufo/glyphs/b.glif000066400000000000000000000005111321574316300211230ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/Bug108.ufo/glyphs/c.glif000066400000000000000000000006241321574316300211310ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/Bug108.ufo/glyphs/contents.plist000066400000000000000000000006371321574316300227620ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif space space.glif ufo2ft-1.1.0/tests/data/Bug108.ufo/glyphs/space.glif000066400000000000000000000001771321574316300220050ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/Bug108.ufo/groups.plist000066400000000000000000000002761321574316300211350ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/Bug108.ufo/kerning.plist000066400000000000000000000006031321574316300212450ustar00rootroot00000000000000 a a -666 b -666 b a -666 ufo2ft-1.1.0/tests/data/Bug108.ufo/layercontents.plist000066400000000000000000000004231321574316300225020ustar00rootroot00000000000000 public.default glyphs ufo2ft-1.1.0/tests/data/Bug108.ufo/lib.plist000066400000000000000000000005521321574316300203610ustar00rootroot00000000000000 public.glyphOrder .notdef space a b c ufo2ft-1.1.0/tests/data/Bug108.ufo/metainfo.plist000066400000000000000000000003601321574316300214120ustar00rootroot00000000000000 formatVersion 3 ufo2ft-1.1.0/tests/data/MTIFeatures.ttx000066400000000000000000000023311321574316300176660ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/000077500000000000000000000000001321574316300177165ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/data/000077500000000000000000000000001321574316300206275ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/data/com.github.googlei18n.ufo2ft.mtiFeatures/000077500000000000000000000000001321574316300303345ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/data/com.github.googlei18n.ufo2ft.mtiFeatures/GSUB.mti000066400000000000000000000003421321574316300316060ustar00rootroot00000000000000FontDame 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-1.1.0/tests/data/MTIFeatures.ufo/fontinfo.plist000066400000000000000000000013011321574316300226100ustar00rootroot00000000000000 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-1.1.0/tests/data/MTIFeatures.ufo/glyphs/000077500000000000000000000000001321574316300212245ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/glyphs/_notdef.glif000066400000000000000000000007531321574316300235120ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/glyphs/a.glif000066400000000000000000000004361321574316300223120ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/glyphs/b.glif000066400000000000000000000005111321574316300223050ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/glyphs/c.glif000066400000000000000000000006241321574316300223130ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/glyphs/contents.plist000066400000000000000000000006371321574316300241440ustar00rootroot00000000000000 .notdef _notdef.glif a a.glif b b.glif c c.glif space space.glif ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/glyphs/space.glif000066400000000000000000000001771321574316300231670ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/groups.plist000066400000000000000000000002761321574316300223170ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/kerning.plist000066400000000000000000000006031321574316300224270ustar00rootroot00000000000000 a a -666 b -666 b a -666 ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/layercontents.plist000066400000000000000000000004231321574316300236640ustar00rootroot00000000000000 public.default glyphs ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/lib.plist000066400000000000000000000005521321574316300215430ustar00rootroot00000000000000 public.glyphOrder .notdef space a b c ufo2ft-1.1.0/tests/data/MTIFeatures.ufo/metainfo.plist000066400000000000000000000003601321574316300225740ustar00rootroot00000000000000 formatVersion 3 ufo2ft-1.1.0/tests/data/TestFont-CFF.ttx000066400000000000000000000370161321574316300177100ustar00rootroot00000000000000 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-1.1.0/tests/data/TestFont-NoOverlaps-CFF.ttx000066400000000000000000000371671321574316300220050ustar00rootroot00000000000000 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-1.1.0/tests/data/TestFont-NoOverlaps-TTF.ttx000066400000000000000000000444531321574316300220400ustar00rootroot00000000000000 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-1.1.0/tests/data/TestFont.ttx000066400000000000000000000441361321574316300173150ustar00rootroot00000000000000 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-1.1.0/tests/data/TestFont.ufo/000077500000000000000000000000001321574316300173345ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/TestFont.ufo/data/000077500000000000000000000000001321574316300202455ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/TestFont.ufo/data/com.github.fonttools.ttx/000077500000000000000000000000001321574316300251505ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/TestFont.ufo/data/com.github.fonttools.ttx/CUST.ttx000066400000000000000000000002711321574316300264670ustar00rootroot00000000000000 0001beef ufo2ft-1.1.0/tests/data/TestFont.ufo/features.fea000066400000000000000000000000001321574316300216150ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/TestFont.ufo/fontinfo.plist000066400000000000000000000215341321574316300222400ustar00rootroot00000000000000 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-1.1.0/tests/data/TestFont.ufo/glyphs/000077500000000000000000000000001321574316300206425ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/_notdef.glif000066400000000000000000000007531321574316300231300ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/a.glif000066400000000000000000000004361321574316300217300ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/b.glif000066400000000000000000000005111321574316300217230ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/c.glif000066400000000000000000000006241321574316300217310ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/contents.plist000066400000000000000000000014201321574316300235510ustar00rootroot00000000000000 .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-1.1.0/tests/data/TestFont.ufo/glyphs/d.glif000066400000000000000000000011451321574316300217310ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/e.glif000066400000000000000000000010411321574316300217250ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/f.glif000066400000000000000000000010411321574316300217260ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/g.glif000066400000000000000000000002521321574316300217320ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/h.glif000066400000000000000000000003351321574316300217350ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/i.glif000066400000000000000000000006521321574316300217400ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/j.glif000066400000000000000000000007041321574316300217370ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/k.glif000066400000000000000000000003201321574316300217320ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/l.glif000066400000000000000000000003521321574316300217400ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/glyphs/space.glif000066400000000000000000000001771321574316300226050ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/TestFont.ufo/kerning.plist000066400000000000000000000006511321574316300220500ustar00rootroot00000000000000 a a 5 b -10 space 1 b a -7 ufo2ft-1.1.0/tests/data/TestFont.ufo/layercontents.plist000066400000000000000000000004231321574316300233020ustar00rootroot00000000000000 public.default glyphs ufo2ft-1.1.0/tests/data/TestFont.ufo/lib.plist000066400000000000000000000011461321574316300211610ustar00rootroot00000000000000 public.glyphOrder .notdef glyph1 glyph2 space a b c d e f g h i j k l ufo2ft-1.1.0/tests/data/TestFont.ufo/metainfo.plist000066400000000000000000000004531321574316300222150ustar00rootroot00000000000000 creator org.robofab.ufoLib formatVersion 3 ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/000077500000000000000000000000001321574316300201575ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/fontinfo.plist000066400000000000000000000023521321574316300230600ustar00rootroot00000000000000 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-1.1.0/tests/data/UseMyMetrics.ufo/glyphs/000077500000000000000000000000001321574316300214655ustar00rootroot00000000000000ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/glyphs/I_.glif000066400000000000000000000005351321574316300226620ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/glyphs/I_acute.glif000066400000000000000000000003561321574316300237050ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/glyphs/acute.glif000066400000000000000000000005471321574316300234370ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/glyphs/contents.plist000066400000000000000000000005761321574316300244070ustar00rootroot00000000000000 I I_.glif Iacute I_acute.glif acute acute.glif romanthree romanthree.glif ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/glyphs/romanthree.glif000066400000000000000000000004131321574316300244720ustar00rootroot00000000000000 ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/layercontents.plist000066400000000000000000000004111321574316300241220ustar00rootroot00000000000000 foreground glyphs ufo2ft-1.1.0/tests/data/UseMyMetrics.ufo/metainfo.plist000066400000000000000000000004451321574316300230410ustar00rootroot00000000000000 creator org.robofab.ufoLib formatVersion 3 ufo2ft-1.1.0/tests/filters/000077500000000000000000000000001321574316300155355ustar00rootroot00000000000000ufo2ft-1.1.0/tests/filters/filters_test.py000066400000000000000000000144171321574316300206250ustar00rootroot00000000000000from __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 sys import types import pytest class _TempModule(object): """Temporarily replace a module in sys.modules with an empty namespace""" def __init__(self, mod_name): self.mod_name = mod_name self.module = types.ModuleType(mod_name) self._saved_module = [] def __enter__(self): mod_name = self.mod_name try: self._saved_module.append(sys.modules[mod_name]) except KeyError: pass sys.modules[mod_name] = self.module return self def __exit__(self, *args): if self._saved_module: sys.modules[self.mod_name] = self._saved_module[0] else: del sys.modules[self.mod_name] self._saved_module = [] 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-1.1.0/tests/filters/transformations_test.py000066400000000000000000000144221321574316300224020ustar00rootroot00000000000000from __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 defcon 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): font = defcon.Font() 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=TransformationsFilter.Origin._fields, ) 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 == (.5, 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-1.1.0/tests/fontInfoData_test.py000066400000000000000000000122711321574316300200550ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import unittest from ufo2ft.fontInfoData import ( getAttrWithFallback, normalizeStringForPostscript) class GetAttrWithFallbackTest(unittest.TestCase): def test_family_and_style_names(self): info = TestInfoObject() self.assertEqual(getAttrWithFallback(info, "familyName"), "Family Name") self.assertEqual(getAttrWithFallback(info, "styleName"), "Style Name") self.assertEqual( getAttrWithFallback(info, "styleMapFamilyName"), "Family Name Style Name") info.styleMapFamilyName = "Style Map Family Name" self.assertEqual( getAttrWithFallback(info, "styleMapFamilyName"), "Style Map Family Name") self.assertEqual( getAttrWithFallback(info, "openTypeNamePreferredFamilyName"), "Family Name") self.assertEqual( getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName"), "Style Name") self.assertEqual( getAttrWithFallback(info, "openTypeNameCompatibleFullName"), "Style Map Family Name") def test_redundant_metadata(self): info = TestInfoObject() self.assertEqual( getAttrWithFallback(info, "openTypeNameVersion"), "Version 0.000") info.versionMinor = 1 info.versionMajor = 1 self.assertEqual( getAttrWithFallback(info, "openTypeNameVersion"), "Version 1.001") self.assertEqual( getAttrWithFallback(info, "openTypeNameUniqueID"), "1.001;NONE;FamilyName-StyleName") self.assertEqual(getAttrWithFallback(info, "postscriptSlantAngle"), 0) self.assertEqual( getAttrWithFallback(info, "postscriptWeightName"), "Normal") def test_vertical_metrics(self): info = TestInfoObject() self.assertEqual( getAttrWithFallback(info, "openTypeHheaAscender"), 950) self.assertEqual( getAttrWithFallback(info, "openTypeHheaDescender"), -250) self.assertEqual( getAttrWithFallback(info, "openTypeOS2TypoAscender"), 650) self.assertEqual( getAttrWithFallback(info, "openTypeOS2TypoDescender"), -250) self.assertEqual( getAttrWithFallback(info, "openTypeOS2WinAscent"), 950) self.assertEqual( getAttrWithFallback(info, "openTypeOS2WinDescent"), 250) def test_caret_slope(self): info = TestInfoObject() self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRise"), 1) self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRun"), 0) info.italicAngle = -12 self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRise"), 1000) self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRun"), 213) info.italicAngle = 12 self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRise"), 1000) self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRun"), -213) info.openTypeHheaCaretSlopeRise = 2048 self.assertFalse(hasattr(info, "openTypeHheaCaretSlopeRun")) self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRise"), 2048) self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRun"), -435) del info.openTypeHheaCaretSlopeRise info.openTypeHheaCaretSlopeRun = 200 self.assertFalse(hasattr(info, "openTypeHheaCaretSlopeRise")) self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRise"), -941) self.assertEqual( getAttrWithFallback(info, "openTypeHheaCaretSlopeRun"), 200) class PostscriptBlueScaleFallbackTest(unittest.TestCase): def test_without_blue_zones(self): info = TestInfoObject() postscriptBlueScale = getAttrWithFallback(info, "postscriptBlueScale") self.assertEqual(postscriptBlueScale, 0.039625) def test_with_blue_zones(self): info = TestInfoObject() info.postscriptBlueValues = [-13, 0, 470, 483, 534, 547, 556, 569, 654, 667, 677, 690, 738, 758] info.postscriptOtherBlues = [-255, -245] postscriptBlueScale = getAttrWithFallback(info, "postscriptBlueScale") self.assertEqual(postscriptBlueScale, 0.0375) class NormalizeStringForPostscriptTest(unittest.TestCase): def test_no_change(self): self.assertEqual( normalizeStringForPostscript('Sample copyright notice.'), "Sample copyright notice.") class TestInfoObject(object): def __init__(self): 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 self.openTypeHheaCaretSlopeRiseFallback = None self.openTypeHheaCaretSlopeRunFallback = None if __name__ == "__main__": import sys sys.exit(unittest.main()) ufo2ft-1.1.0/tests/kernFeatureWriter_test.py000066400000000000000000000071431321574316300211530ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals from textwrap import dedent import unittest from defcon import Font from ufo2ft.featureCompiler import FeatureCompiler from ufo2ft.featureWriters import KernFeatureWriter from fontTools.misc.py23 import SimpleNamespace class KernFeatureWriterTest(unittest.TestCase): def test_collect_fea_classes(self): text = '@MMK_L_v = [v w y];' expected = {'@MMK_L_v': ['v', 'w', 'y']} ufo = Font() ufo.features.text = text writer = KernFeatureWriter() writer.set_context(ufo) writer._collectFeaClasses() self.assertEquals(writer.context.leftFeaClasses, expected) def test__cleanupMissingGlyphs(self): groups = { "public.kern1.A": ["A", "Aacute", "Abreve", "Acircumflex"], "public.kern2.B": ["B", "D", "E", "F"], } ufo = Font() for glyphs in groups.values(): for glyph in glyphs: ufo.newGlyph(glyph) ufo.groups.update(groups) del ufo["Abreve"] del ufo["D"] writer = KernFeatureWriter() writer.set_context(ufo) self.assertEquals(writer.context.groups, groups) writer._cleanupMissingGlyphs() self.assertEquals(writer.context.groups, { "public.kern1.A": ["A", "Aacute", "Acircumflex"], "public.kern2.B": ["B", "E", "F"]}) def test_ignoreMarks(self): font = Font() 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() kern = writer.write(font) assert kern == 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) kern = writer.write(font) assert kern == dedent(""" lookup kern_ltr { pos four six -55; pos one six -30; } kern_ltr; feature kern { lookup kern_ltr; } kern;""") def test_mode(self): class MockTTFont: def getReverseGlyphMap(self): return {"one": 0, "four": 1, "six": 2, "seven": 3} outline = MockTTFont() ufo = Font() 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" compiler = FeatureCompiler(ufo, outline, featureWriters=[writer]) compiler.setupFile_features() assert compiler.features == existing writer = KernFeatureWriter(mode="append") compiler = FeatureCompiler(ufo, outline, featureWriters=[writer]) compiler.setupFile_features() assert compiler.features == existing + dedent(""" lookup kern_ltr { lookupflag IgnoreMarks; pos seven six 25; } kern_ltr; feature kern { lookup kern_ltr; } kern;""") if __name__ == "__main__": import sys sys.exit(unittest.main()) ufo2ft-1.1.0/tests/markFeatureWriter_test.py000066400000000000000000000044241321574316300211450ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import, unicode_literals import unittest from defcon import Font from ufo2ft.featureWriters import MarkFeatureWriter class PrebuiltMarkFeatureWriter(MarkFeatureWriter): def setupAnchorPairs(self): self.context.anchorList = (('bottom', '_bottom'),) self.context.mkmkAnchorList = () self.context.ligaAnchorList = ((('top_1', 'top_2'), '_top'),) class MarkFeatureWriterTest(unittest.TestCase): def test_add_classes(self): ufo = Font() ufo.newGlyph('grave').appendAnchor( {'name': '_top', 'x': 100, 'y': 200}) ufo.newGlyph('cedilla').appendAnchor( {'name': '_bottom', 'x': 100, 'y': 0}) lines = [] writer = PrebuiltMarkFeatureWriter() writer.set_context(ufo) writer._addClasses(lines, doMark=True, doMkmk=True) self.assertEqual( '\n'.join(lines).strip(), 'markClass cedilla @MC_bottom;\n\n' 'markClass grave @MC_top;') def test_skip_empty_feature(self): ufo = Font() ufo.newGlyph('a').appendAnchor( {'name': 'top', 'x': 100, 'y': 200}) ufo.newGlyph('acutecomb').appendAnchor( {'name': '_top', 'x': 100, 'y': 200}) writer = MarkFeatureWriter() fea = writer.write(ufo) self.assertIn("feature mark", fea) self.assertNotIn("feature mkmk", fea) def test_only_write_one(self): ufo = Font() ufo.newGlyph('a').appendAnchor({'name': 'top', 'x': 100, 'y': 200}) ufo.newGlyph('acutecomb').appendAnchor( {'name': '_top', 'x': 100, 'y': 200}) glyph = ufo.newGlyph('tildecomb') glyph.appendAnchor({'name': '_top', 'x': 100, 'y': 200}) glyph.appendAnchor({'name': 'top', 'x': 100, 'y': 300}) writer = MarkFeatureWriter() # by default both mark + mkmk are built fea = writer.write(ufo) self.assertIn("feature mark", fea) self.assertIn("feature mkmk", fea) writer = MarkFeatureWriter(features=["mkmk"]) # only builds "mkmk" fea = writer.write(ufo) self.assertNotIn("feature mark", fea) self.assertIn("feature mkmk", fea) if __name__ == '__main__': unittest.main() ufo2ft-1.1.0/tests/outlineCompiler_test.py000066400000000000000000000347501321574316300206610ustar00rootroot00000000000000from fontTools.ttLib import TTFont from fontTools.misc.py23 import basestring from defcon import Font from ufo2ft.outlineCompiler import OutlineTTFCompiler, OutlineOTFCompiler from fontTools.ttLib.tables._g_l_y_f import USE_MY_METRICS from ufo2ft import compileTTF import unittest import os def getTestUFO(name='TestFont'): dirname = os.path.dirname(__file__) return Font(os.path.join(dirname, 'data', name+'.ufo')) class OutlineTTFCompilerTest(unittest.TestCase): def setUp(self): self.ufo = getTestUFO() def test_setupTable_gasp(self): compiler = OutlineTTFCompiler(self.ufo) compiler.otf = TTFont() compiler.setupTable_gasp() self.assertTrue('gasp' in compiler.otf) self.assertEqual(compiler.otf['gasp'].gaspRange, {7: 10, 65535: 15}) def test_compile_with_gasp(self): compiler = OutlineTTFCompiler(self.ufo) compiler.compile() self.assertTrue('gasp' in compiler.otf) self.assertEqual(compiler.otf['gasp'].gaspRange, {7: 10, 65535: 15}) def test_compile_without_gasp(self): self.ufo.info.openTypeGaspRangeRecords = None compiler = OutlineTTFCompiler(self.ufo) compiler.compile() self.assertTrue('gasp' not in compiler.otf) def test_compile_empty_gasp(self): # ignore empty gasp self.ufo.info.openTypeGaspRangeRecords = [] compiler = OutlineTTFCompiler(self.ufo) compiler.compile() self.assertTrue('gasp' not in compiler.otf) def test_makeGlyphsBoundingBoxes(self): # the call to 'makeGlyphsBoundingBoxes' happen in the __init__ method compiler = OutlineTTFCompiler(self.ufo) self.assertEqual(compiler.glyphBoundingBoxes['.notdef'], (50, 0, 450, 750)) # no outline data self.assertEqual(compiler.glyphBoundingBoxes['space'], None) # float coordinates are rounded, so is the bbox self.assertEqual(compiler.glyphBoundingBoxes['d'], (90, 77, 211, 197)) def test_autoUseMyMetrics(self): ufo = getTestUFO('UseMyMetrics') compiler = OutlineTTFCompiler(ufo) ttf = compiler.compile() # the first component in the 'Iacute' composite glyph ('acute') # does _not_ have the USE_MY_METRICS flag self.assertFalse( 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 self.assertTrue( 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: self.assertFalse(component.flags & USE_MY_METRICS) def test_autoUseMyMetrics_None(self): ufo = getTestUFO('UseMyMetrics') compiler = OutlineTTFCompiler(ufo) # setting 'autoUseMyMetrics' attribute to None disables the feature compiler.autoUseMyMetrics = None ttf = compiler.compile() self.assertFalse(ttf['glyf']['Iacute'].components[1].flags & USE_MY_METRICS) def test_importTTX(self): compiler = OutlineTTFCompiler(self.ufo) otf = compiler.otf = TTFont() compiler.importTTX() self.assertIn("CUST", otf) self.assertEqual(otf["CUST"].data, b"\x00\x01\xbe\xef") self.assertEqual(otf.sfntVersion, "\x00\x01\x00\x00") class OutlineOTFCompilerTest(unittest.TestCase): def setUp(self): self.ufo = getTestUFO() def test_setupTable_CFF_all_blues_defined(self): self.ufo.info.postscriptBlueFuzz = 2 self.ufo.info.postscriptBlueShift = 8 self.ufo.info.postscriptBlueScale = 0.049736 self.ufo.info.postscriptForceBold = False self.ufo.info.postscriptBlueValues = [-12, 0, 486, 498, 712, 724] self.ufo.info.postscriptOtherBlues = [-217, -205] self.ufo.info.postscriptFamilyBlues = [-12, 0, 486, 498, 712, 724] self.ufo.info.postscriptFamilyOtherBlues = [-217, -205] compiler = OutlineOTFCompiler(self.ufo) compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() cff = compiler.otf["CFF "].cff private = cff[list(cff.keys())[0]].Private self.assertEqual(private.BlueFuzz, 2) self.assertEqual(private.BlueShift, 8) self.assertEqual(private.BlueScale, 0.049736) self.assertEqual(private.ForceBold, False) self.assertEqual(private.BlueValues, [-12, 0, 486, 498, 712, 724]) self.assertEqual(private.OtherBlues, [-217, -205]) self.assertEqual(private.FamilyBlues, [-12, 0, 486, 498, 712, 724]) self.assertEqual(private.FamilyOtherBlues, [-217, -205]) def test_setupTable_CFF_no_blues_defined(self): # no blue values defined self.ufo.info.postscriptBlueValues = [] self.ufo.info.postscriptOtherBlues = [] self.ufo.info.postscriptFamilyBlues = [] self.ufo.info.postscriptFamilyOtherBlues = [] # the following attributes have no effect self.ufo.info.postscriptBlueFuzz = 2 self.ufo.info.postscriptBlueShift = 8 self.ufo.info.postscriptBlueScale = 0.049736 self.ufo.info.postscriptForceBold = False compiler = OutlineOTFCompiler(self.ufo) 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 self.assertEqual(private.BlueFuzz, 1) self.assertEqual(private.BlueShift, 7) self.assertEqual(private.BlueScale, 0.039625) self.assertEqual(private.ForceBold, False) # CFF PrivateDict has no blues attributes self.assertFalse(hasattr(private, "BlueValues")) self.assertFalse(hasattr(private, "OtherBlues")) self.assertFalse(hasattr(private, "FamilyBlues")) self.assertFalse(hasattr(private, "FamilyOtherBlues")) def test_setupTable_CFF_some_blues_defined(self): self.ufo.info.postscriptBlueFuzz = 2 self.ufo.info.postscriptForceBold = True self.ufo.info.postscriptBlueValues = [] self.ufo.info.postscriptOtherBlues = [-217, -205] self.ufo.info.postscriptFamilyBlues = [] self.ufo.info.postscriptFamilyOtherBlues = [] compiler = OutlineOTFCompiler(self.ufo) compiler.otf = TTFont(sfntVersion="OTTO") compiler.setupTable_CFF() cff = compiler.otf["CFF "].cff private = cff[list(cff.keys())[0]].Private self.assertEqual(private.BlueFuzz, 2) self.assertEqual(private.BlueShift, 7) # default self.assertEqual(private.BlueScale, 0.039625) # default self.assertEqual(private.ForceBold, True) self.assertFalse(hasattr(private, "BlueValues")) self.assertEqual(private.OtherBlues, [-217, -205]) self.assertFalse(hasattr(private, "FamilyBlues")) self.assertFalse(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): self.assertEqual(len(expected), len(actual)) for exp_token, act_token in zip(expected, actual): if isinstance(exp_token, basestring): self.assertEqual(exp_token, act_token) else: self.assertNotIsInstance(act_token, basestring) self.assertAlmostEqual(exp_token, act_token) def test_setupTable_CFF_round_all(self): # by default all floats are rounded to integer compiler = OutlineOTFCompiler(self.ufo) 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): # roundTolerance=0 means 'don't round, keep all floats' compiler = OutlineOTFCompiler(self.ufo, 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): # only floats 'close enough' are rounded to integer compiler = OutlineOTFCompiler(self.ufo, 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_makeGlyphsBoundingBoxes(self): # the call to 'makeGlyphsBoundingBoxes' happen in the __init__ method compiler = OutlineOTFCompiler(self.ufo) # with default roundTolerance, all coordinates and hence the bounding # box values are rounded with round() self.assertEqual(compiler.glyphBoundingBoxes['d'], (90, 77, 211, 197)) def test_makeGlyphsBoundingBoxes_floats(self): # 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(self.ufo, roundTolerance=0.1) self.assertEqual(compiler.glyphBoundingBoxes['d'], (90, 77, 211, 198)) def test_importTTX(self): compiler = OutlineOTFCompiler(self.ufo) otf = compiler.otf = TTFont(sfntVersion="OTTO") compiler.importTTX() self.assertIn("CUST", otf) self.assertEqual(otf["CUST"].data, b"\x00\x01\xbe\xef") self.assertEqual(otf.sfntVersion, "OTTO") class TestGlyphOrder(unittest.TestCase): def setUp(self): self.ufo = getTestUFO() def test_compile_original_glyph_order(self): DEFAULT_ORDER = ['.notdef', 'space', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'] compiler = OutlineTTFCompiler(self.ufo) compiler.compile() self.assertEqual(compiler.otf.getGlyphOrder(), DEFAULT_ORDER) def test_compile_tweaked_glyph_order(self): NEW_ORDER = ['.notdef', 'space', 'b', 'a', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'] self.ufo.lib['public.glyphOrder'] = NEW_ORDER compiler = OutlineTTFCompiler(self.ufo) compiler.compile() self.assertEqual(compiler.otf.getGlyphOrder(), NEW_ORDER) def test_compile_strange_glyph_order(self): """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'] self.ufo.lib['public.glyphOrder'] = NEW_ORDER compiler = OutlineTTFCompiler(self.ufo) compiler.compile() self.assertEqual(compiler.otf.getGlyphOrder(), EXPECTED_ORDER) class TestNames(unittest.TestCase): def setUp(self): self.ufo = getTestUFO() def test_compile_without_production_names(self): expected = ['.notdef', 'space', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'] result = compileTTF(self.ufo, useProductionNames=False) self.assertEqual(result.getGlyphOrder(), expected) self.ufo.lib["com.github.googlei18n.ufo2ft.useProductionNames"] = False result = compileTTF(self.ufo) self.assertEqual(result.getGlyphOrder(), expected) def test_compile_with_production_names(self): expected = ['.notdef', 'uni0020', 'uni0061', 'uni0062', 'uni0063', 'uni0064', 'uni0065', 'uni0066', 'uni0067', 'uni0068', 'uni0069', 'uni006A', 'uni006B', 'uni006C'] result = compileTTF(self.ufo) self.assertEqual(result.getGlyphOrder(), expected) result = compileTTF(self.ufo, useProductionNames=True) self.assertEqual(result.getGlyphOrder(), expected) self.ufo.lib["com.github.googlei18n.ufo2ft.useProductionNames"] = True result = compileTTF(self.ufo) self.assertEqual(result.getGlyphOrder(), expected) 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', } def test_compile_with_custom_postscript_names(self): self.ufo.lib['public.postscriptNames'] = self.CUSTOM_POSTSCRIPT_NAMES result = compileTTF(self.ufo, useProductionNames=True) self.assertEqual(sorted(result.getGlyphOrder()), sorted(self.CUSTOM_POSTSCRIPT_NAMES.values())) def test_compile_with_custom_postscript_names_notdef_preserved(self): custom_names = dict(self.CUSTOM_POSTSCRIPT_NAMES) custom_names['.notdef'] = 'defnot' self.ufo.lib['public.postscriptNames'] = custom_names result = compileTTF(self.ufo, useProductionNames=True) self.assertEqual(result.getGlyphOrder(), ['.notdef', 'foo', 'bar', 'baz', 'meh', 'doh', 'bim', 'bum', 'bam', 'bib', 'bob', 'bub', 'kkk', 'lll']) if __name__ == "__main__": import sys sys.exit(unittest.main()) ufo2ft-1.1.0/tox.ini000066400000000000000000000013701321574316300142370ustar00rootroot00000000000000[tox] envlist = py{27,36}-cov, htmlcov [testenv] deps = cov: coverage pytest -rrequirements.txt 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] basepython = python3.6 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