diff options
author | Haibo Huang <hhb@google.com> | 2019-01-10 11:08:18 -0800 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2019-01-10 11:08:18 -0800 |
commit | 0ed57b6ef49adea72c7f51bb57f812334a605420 (patch) | |
tree | feec98f7887269a73346929e7d1a295df78936c8 | |
parent | 7b66ac5cf864ac90df7593ed3b560a8e16cedcc9 (diff) | |
parent | 7ce473d6ec491a3e7a87fa646f025e85ac31ab2c (diff) | |
download | fonttools-0ed57b6ef49adea72c7f51bb57f812334a605420.tar.gz |
Merge "Upgrade fonttools from 3.31.0 to 3.35.0"
am: 7ce473d6ec
Change-Id: I0d9a7d9381f534fcefbd0f775fc600d0c36af36b
147 files changed, 10900 insertions, 1407 deletions
diff --git a/.appveyor.yml b/.appveyor.yml index e6d53c66..f4b2933c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -3,12 +3,17 @@ environment: - JOB: "2.7 32-bit" PYTHON_HOME: "C:\\Python27" - - JOB: "3.6 64-bit" - PYTHON_HOME: "C:\\Python36-x64" - - JOB: "3.7 64-bit" PYTHON_HOME: "C:\\Python37-x64" +branches: + only: + - master + # We want to build wip/* branches since these are not usually used for PRs + - /^wip\/.*$/ + # We want to build version tags as well. + - /^\d+\.\d+.*$/ + 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 diff --git a/.travis.yml b/.travis.yml index 4223e959..0951eaf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,14 @@ env: - TWINE_USERNAME="anthrotype" - secure: PJuCmlDuwnojiw3QuDhfNAaU4f/yeJcEcRzJAudA66bwZK7hvxV7Tiy9A17Bm6yO0HbJmmyjsIr8h2e7/PyY6QCaV8RqcMDkQ0UraU16pRsihp0giVXJoWscj2sCP4cNDOBVwSaGAX8yZ2OONc5srESywghzcy8xmgw6O+XFqx4= +branches: + only: + - master + # We want to build wip/* branches since these are not usually used for PRs + - /^wip\/.*$/ + # We want to build version tags as well. + - /^\d+\.\d+.*$/ + matrix: fast_finish: true exclude: @@ -18,7 +26,7 @@ matrix: env: TOXENV=py35-cov - python: 3.6 env: - - TOXENV=py36-cov + - TOXENV=py36-cov,package_readme - BUILD_DIST=true - python: 3.7 env: TOXENV=py37-cov diff --git a/.travis/after_success.sh b/.travis/after_success.sh index 07bcab5e..07bcab5e 100755..100644 --- a/.travis/after_success.sh +++ b/.travis/after_success.sh diff --git a/.travis/before_install.sh b/.travis/before_install.sh index 8cc4edba..8cc4edba 100755..100644 --- a/.travis/before_install.sh +++ b/.travis/before_install.sh diff --git a/.travis/install.sh b/.travis/install.sh index f2a0717f..f2a0717f 100755..100644 --- a/.travis/install.sh +++ b/.travis/install.sh diff --git a/.travis/run.sh b/.travis/run.sh index c6c1fea9..ffb0ef79 100755..100644 --- a/.travis/run.sh +++ b/.travis/run.sh @@ -11,4 +11,10 @@ tox # re-run all the XML-related tests, this time without lxml but using the # built-in ElementTree library. -tox -e ${TOXENV:-py}-nolxml -- Tests/ufoLib Tests/misc/etree_test.py Tests/misc/plistlib_test.py +if [ -z "$TOXENV" ]; then + TOXENV="py-nolxml" +else + # strip additional tox envs after the comma, add -nolxml factor + TOXENV="${TOXENV%,*}-nolxml" +fi +tox -e $TOXENV -- Tests/ufoLib Tests/misc/etree_test.py Tests/misc/plistlib_test.py diff --git a/Doc/source/designspaceLib/readme.rst b/Doc/source/designspaceLib/readme.rst index 5af5d636..06bf46ff 100644 --- a/Doc/source/designspaceLib/readme.rst +++ b/Doc/source/designspaceLib/readme.rst @@ -278,16 +278,17 @@ AxisDescriptor object - ``labelNames``: dict. When defining a non-registered axis, it will be necessary to define user-facing readable names for the axis. Keyed by xml:lang code. Varlib. -- ``minimum``: number. The minimum value for this axis. MutatorMath + - Varlib. -- ``maximum``: number. The maximum value for this axis. MutatorMath + - Varlib. -- ``default``: number. The default value for this axis, i.e. when a new - location is created, this is the value this axis will get. +- ``minimum``: number. The minimum value for this axis in user space. MutatorMath + Varlib. -- ``map``: list of input / output values that can describe a warp of - user space to designspace coordinates. If no map values are present, - it is assumed it is [(minimum, minimum), (maximum, maximum)]. Varlib. +- ``maximum``: number. The maximum value for this axis in user space. + MutatorMath + Varlib. +- ``default``: number. The default value for this axis, i.e. when a new + location is created, this is the value this axis will get in user + space. MutatorMath + Varlib. +- ``map``: list of input / output values that can describe a warp of user space + to design space coordinates. If no map values are present, it is assumed user + space is the same as design space, as in [(minimum, minimum), (maximum, maximum)]. + Varlib. .. code:: python diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py index 10eab303..e922c48e 100644 --- a/Lib/fontTools/__init__.py +++ b/Lib/fontTools/__init__.py @@ -5,6 +5,6 @@ from fontTools.misc.loggingTools import configLogger log = logging.getLogger(__name__) -version = __version__ = "3.31.0" +version = __version__ = "3.35.0" __all__ = ["version", "log", "configLogger"] diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index 83526ed5..4ad0e442 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -623,11 +623,6 @@ class GlobalSubrsIndex(Index): self.fdSelect = fdSelect if fdArray: self.fdArray = fdArray - if isCFF2: - # CFF2Subr's can have numeric arguments on the stack after the last operator. - self.subrClass = psCharStrings.CFF2Subr - self.charStringClass = psCharStrings.CFF2Subr - def produceItem(self, index, data, file, offset): if self.private is not None: @@ -2336,7 +2331,7 @@ class TopDict(BaseDict): def decompileAllCharStrings(self): # Make sure that all the Private Dicts have been instantiated. - for charString in self.CharStrings.values(): + for i, charString in enumerate(self.CharStrings.values()): try: charString.decompile() except: @@ -2424,10 +2419,19 @@ class PrivateDict(BaseDict): if isCFF2: self.defaults = buildDefaults(privateDictOperators2) self.order = buildOrder(privateDictOperators2) + # Provide dummy values. This avoids needing to provide + # an isCFF2 state in a lot of places. + self.nominalWidthX = self.defaultWidthX = None else: self.defaults = buildDefaults(privateDictOperators) self.order = buildOrder(privateDictOperators) + def __getattr__(self, name): + if name == "in_cff2": + return self._isCFF2 + value = BaseDict.__getattr__(self, name) + return value + def getNumRegions(self, vi=None): # called from misc/psCharStrings.py # if getNumRegions is being called, we can assume that VarStore exists. if vi is None: diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py index 5a6942d3..caf8c3b3 100644 --- a/Lib/fontTools/cffLib/specializer.py +++ b/Lib/fontTools/cffLib/specializer.py @@ -21,6 +21,7 @@ def stringToProgram(string): program.append(token) return program + def programToString(program): return ' '.join(str(x) for x in program) @@ -62,6 +63,7 @@ def programToCommands(program): commands.append(('', stack)) return commands + def commandsToProgram(commands): """Takes a commands list as returned by programToCommands() and converts it back to a T2CharString program list.""" @@ -415,7 +417,9 @@ def specializeCommands(commands, continue # Merge adjacent hlineto's and vlineto's. - if i and op in {'hlineto', 'vlineto'} and op == commands[i-1][0]: + if (i and op in {'hlineto', 'vlineto'} and + (op == commands[i-1][0]) and + (not isinstance(args[0], list))): _, other_args = commands[i-1] assert len(args) == 1 and len(other_args) == 1 commands[i-1] = (op, [other_args[0]+args[0]]) diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py index 24c2247f..d9da48c0 100644 --- a/Lib/fontTools/designspaceLib/__init__.py +++ b/Lib/fontTools/designspaceLib/__init__.py @@ -673,12 +673,6 @@ class BaseDocReader(LogMixin): self.readInstances() self.readLib() - def getSourcePaths(self, makeGlyphs=True, makeKerning=True, makeInfo=True): - paths = [] - for name in self.documentObject.sources.keys(): - paths.append(self.documentObject.sources[name][0].path) - return paths - def readRules(self): # we also need to read any conditions that are outside of a condition set. rules = [] @@ -1053,6 +1047,8 @@ class DesignSpaceDocument(LogMixin, AsDictMixin): return f.getvalue() def read(self, path): + if hasattr(path, "__fspath__"): # support os.PathLike objects + path = path.__fspath__() self.path = path self.filename = os.path.basename(path) reader = self.readerClass(path, self) @@ -1061,6 +1057,8 @@ class DesignSpaceDocument(LogMixin, AsDictMixin): self.findDefault() def write(self, path): + if hasattr(path, "__fspath__"): # support os.PathLike objects + path = path.__fspath__() self.path = path self.filename = os.path.basename(path) self.updatePaths() @@ -1113,21 +1111,11 @@ class DesignSpaceDocument(LogMixin, AsDictMixin): """ + assert self.path is not None for descriptor in self.sources + self.instances: - # check what the relative path really should be? - expectedFilename = None - if descriptor.path is not None and self.path is not None: - expectedFilename = self._posixRelativePath(descriptor.path) - - # 3 - if descriptor.filename is None and descriptor.path is not None and self.path is not None: + if descriptor.path is not None: + # case 3 and 4: filename gets updated and relativized descriptor.filename = self._posixRelativePath(descriptor.path) - continue - - # 4 - if descriptor.filename is not None and descriptor.path is not None and self.path is not None: - if descriptor.filename is not expectedFilename: - descriptor.filename = expectedFilename def addSource(self, sourceDescriptor): self.sources.append(sourceDescriptor) diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py new file mode 100644 index 00000000..70519822 --- /dev/null +++ b/Lib/fontTools/fontBuilder.py @@ -0,0 +1,765 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals + +__all__ = ["FontBuilder"] + +""" +This module is *experimental*, meaning it still may evolve and change. + +The `FontBuilder` class is a convenient helper to construct working TTF or +OTF fonts from scratch. + +Note that the various setup methods cannot be called in arbitrary order, +due to various interdependencies between OpenType tables. Here is an order +that works: + + fb = FontBuilder(...) + fb.setupGlyphOrder(...) + fb.setupCharacterMap(...) + fb.setupGlyf(...) --or-- fb.setupCFF(...) + fb.setupHorizontalMetrics(...) + fb.setupHorizontalHeader() + fb.setupNameTable(...) + fb.setupOS2() + fb.setupPost() + fb.save(...) + +Here is how to build a minimal TTF: + +```python +from fontTools.fontBuilder import FontBuilder +from fontTools.pens.ttGlyphPen import TTGlyphPen + +def drawTestGlyph(pen): + pen.moveTo((100, 100)) + pen.lineTo((100, 1000)) + pen.qCurveTo((200, 900), (400, 900), (500, 1000)) + pen.lineTo((500, 100)) + pen.closePath() + +fb = FontBuilder(1024, isTTF=True) +fb.setupGlyphOrder([".notdef", ".null", "A", "a"]) +fb.setupCharacterMap({65: "A", 97: "a"}) + +advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600} + +familyName = "HelloTestFont" +styleName = "TotallyNormal" +nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"), + styleName=dict(en="TotallyNormal", nl="TotaalNormaal")) +nameStrings['psName'] = familyName + "-" + styleName + +pen = TTGlyphPen(None) +drawTestGlyph(pen) +glyph = pen.glyph() +glyphs = {".notdef": glyph, "A": glyph, "a": glyph, ".null": glyph} +fb.setupGlyf(glyphs) + +metrics = {} +glyphTable = fb.font["glyf"] +for gn, advanceWidth in advanceWidths.items(): + metrics[gn] = (advanceWidth, glyphTable[gn].xMin) +fb.setupHorizontalMetrics(metrics) + +fb.setupHorizontalHeader(ascent=824, descent=200) +fb.setupNameTable(nameStrings) +fb.setupOS2() +fb.setupPost() + +fb.save("test.ttf") +``` + +And here's how to build a minimal OTF: + +```python +from fontTools.fontBuilder import FontBuilder +from fontTools.pens.t2CharStringPen import T2CharStringPen + +def drawTestGlyph(pen): + pen.moveTo((100, 100)) + pen.lineTo((100, 1000)) + pen.curveTo((200, 900), (400, 900), (500, 1000)) + pen.lineTo((500, 100)) + pen.closePath() + +fb = FontBuilder(1024, isTTF=False) +fb.setupGlyphOrder([".notdef", ".null", "A", "a"]) +fb.setupCharacterMap({65: "A", 97: "a"}) + +advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600} + +familyName = "HelloTestFont" +styleName = "TotallyNormal" +nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"), + styleName=dict(en="TotallyNormal", nl="TotaalNormaal")) +nameStrings['psName'] = familyName + "-" + styleName + +pen = T2CharStringPen(600, None) +drawTestGlyph(pen) +charString = pen.getCharString() +charStrings = {".notdef": charString, "A": charString, "a": charString, ".null": charString} +fb.setupCFF(nameStrings['psName'], {"FullName": nameStrings['psName']}, charStrings, {}) + +metrics = {} +for gn, advanceWidth in advanceWidths.items(): + metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph +fb.setupHorizontalMetrics(metrics) + +fb.setupHorizontalHeader(ascent=824, descent=200) +fb.setupNameTable(nameStrings) +fb.setupOS2() +fb.setupPost() + +fb.save("test.otf") +``` +""" + +from .misc.py23 import * +from .ttLib import TTFont, newTable +from .ttLib.tables._c_m_a_p import cmap_classes +from .ttLib.tables._n_a_m_e import NameRecord, makeName +from .misc.timeTools import timestampNow +import struct + + +_headDefaults = dict( + tableVersion = 1.0, + fontRevision = 1.0, + checkSumAdjustment = 0, + magicNumber = 0x5F0F3CF5, + flags = 0x0003, + unitsPerEm = 1000, + created = 0, + modified = 0, + xMin = 0, + yMin = 0, + xMax = 0, + yMax = 0, + macStyle = 0, + lowestRecPPEM = 3, + fontDirectionHint = 2, + indexToLocFormat = 0, + glyphDataFormat = 0, +) + +_maxpDefaultsTTF = dict( + tableVersion = 0x00010000, + numGlyphs = 0, + maxPoints = 0, + maxContours = 0, + maxCompositePoints = 0, + maxCompositeContours = 0, + maxZones = 2, + maxTwilightPoints = 0, + maxStorage = 0, + maxFunctionDefs = 0, + maxInstructionDefs = 0, + maxStackElements = 0, + maxSizeOfInstructions = 0, + maxComponentElements = 0, + maxComponentDepth = 0, +) +_maxpDefaultsOTF = dict( + tableVersion = 0x00005000, + numGlyphs = 0, +) + +_postDefaults = dict( + formatType = 3.0, + italicAngle = 0, + underlinePosition = 0, + underlineThickness = 0, + isFixedPitch = 0, + minMemType42 = 0, + maxMemType42 = 0, + minMemType1 = 0, + maxMemType1 = 0, +) + +_hheaDefaults = dict( + tableVersion = 0x00010000, + ascent = 0, + descent = 0, + lineGap = 0, + advanceWidthMax = 0, + minLeftSideBearing = 0, + minRightSideBearing = 0, + xMaxExtent = 0, + caretSlopeRise = 1, + caretSlopeRun = 0, + caretOffset = 0, + reserved0 = 0, + reserved1 = 0, + reserved2 = 0, + reserved3 = 0, + metricDataFormat = 0, + numberOfHMetrics = 0, +) + +_vheaDefaults = dict( + tableVersion = 0x00010000, + ascent = 0, + descent = 0, + lineGap = 0, + advanceHeightMax = 0, + minTopSideBearing = 0, + minBottomSideBearing = 0, + yMaxExtent = 0, + caretSlopeRise = 0, + caretSlopeRun = 0, + reserved0 = 0, + reserved1 = 0, + reserved2 = 0, + reserved3 = 0, + reserved4 = 0, + metricDataFormat = 0, + numberOfVMetrics = 0, +) + +_nameIDs = dict( + copyright = 0, + familyName = 1, + styleName = 2, + uniqueFontIdentifier = 3, + fullName = 4, + version = 5, + psName = 6, + trademark = 7, + manufacturer = 8, + designer = 9, + description = 10, + vendorURL = 11, + designerURL = 12, + licenseDescription = 13, + licenseInfoURL = 14, + # reserved = 15, + typographicFamily = 16, + typographicSubfamily = 17, + compatibleFullName = 18, + sampleText = 19, + postScriptCIDFindfontName = 20, + wwsFamilyName = 21, + wwsSubfamilyName = 22, + lightBackgroundPalette = 23, + darkBackgroundPalette = 24, + variationsPostScriptNamePrefix = 25, +) + +# to insert in setupNameTable doc string: +# print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1]))) + +_panoseDefaults = dict( + bFamilyType = 0, + bSerifStyle = 0, + bWeight = 0, + bProportion = 0, + bContrast = 0, + bStrokeVariation = 0, + bArmStyle = 0, + bLetterForm = 0, + bMidline = 0, + bXHeight = 0, +) + +_OS2Defaults = dict( + version = 3, + xAvgCharWidth = 0, + usWeightClass = 400, + usWidthClass = 5, + fsType = 0x0004, # default: Preview & Print embedding + ySubscriptXSize = 0, + ySubscriptYSize = 0, + ySubscriptXOffset = 0, + ySubscriptYOffset = 0, + ySuperscriptXSize = 0, + ySuperscriptYSize = 0, + ySuperscriptXOffset = 0, + ySuperscriptYOffset = 0, + yStrikeoutSize = 0, + yStrikeoutPosition = 0, + sFamilyClass = 0, + panose = _panoseDefaults, + ulUnicodeRange1 = 0, + ulUnicodeRange2 = 0, + ulUnicodeRange3 = 0, + ulUnicodeRange4 = 0, + achVendID = "????", + fsSelection = 0, + usFirstCharIndex = 0, + usLastCharIndex = 0, + sTypoAscender = 0, + sTypoDescender = 0, + sTypoLineGap = 0, + usWinAscent = 0, + usWinDescent = 0, + ulCodePageRange1 = 0, + ulCodePageRange2 = 0, + sxHeight = 0, + sCapHeight = 0, + usDefaultChar = 0, # .notdef + usBreakChar = 32, # space + usMaxContext = 2, # just kerning + usLowerOpticalPointSize = 0, + usUpperOpticalPointSize = 0, +) + + +class FontBuilder(object): + + def __init__(self, unitsPerEm=None, font=None, isTTF=True): + """Initialize a FontBuilder instance. + + If the `font` argument is not given, a new `TTFont` will be + constructed, and `unitsPerEm` must be given. If `isTTF` is True, + the font will be a glyf-based TTF; if `isTTF` is False it will be + a CFF-based OTF. + + If `font` is given, it must be a `TTFont` instance and `unitsPerEm` + must _not_ be given. The `isTTF` argument will be ignored. + """ + if font is None: + self.font = TTFont(recalcTimestamp=False) + self.isTTF = isTTF + now = timestampNow() + assert unitsPerEm is not None + self.setupHead(unitsPerEm=unitsPerEm, created=now, modified=now) + self.setupMaxp() + else: + assert unitsPerEm is None + self.font = font + self.isTTF = "glyf" in font + + def save(self, file): + """Save the font. The 'file' argument can be either a pathname or a + writable file object. + """ + self.font.save(file) + + def _initTableWithValues(self, tableTag, defaults, values): + table = self.font[tableTag] = newTable(tableTag) + for k, v in defaults.items(): + setattr(table, k, v) + for k, v in values.items(): + setattr(table, k, v) + return table + + def _updateTableWithValues(self, tableTag, values): + table = self.font[tableTag] + for k, v in values.items(): + setattr(table, k, v) + + def setupHead(self, **values): + """Create a new `head` table and initialize it with default values, + which can be overridden by keyword arguments. + """ + self._initTableWithValues("head", _headDefaults, values) + + def updateHead(self, **values): + """Update the head table with the fields and values passed as + keyword arguments. + """ + self._updateTableWithValues("head", values) + + def setupGlyphOrder(self, glyphOrder): + """Set the glyph order for the font.""" + self.font.setGlyphOrder(glyphOrder) + + def setupCharacterMap(self, cmapping, allowFallback=False): + """Build the `cmap` table for the font. The `cmapping` argument should + be a dict mapping unicode code points as integers to glyph names. + """ + subTables = [] + highestUnicode = max(cmapping) + if highestUnicode > 0xffff: + cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000) + subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10) + subTables.append(subTable_3_10) + else: + cmapping_3_1 = cmapping + format = 4 + subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1) + try: + subTable_3_1.compile(self.font) + except struct.error: + # format 4 overflowed, fall back to format 12 + if not allowFallback: + raise ValueError("cmap format 4 subtable overflowed; sort glyph order by unicode to fix.") + format = 12 + subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1) + subTables.append(subTable_3_1) + subTable_0_3 = buildCmapSubTable(cmapping_3_1, format, 0, 3) + subTables.append(subTable_0_3) + + self.font["cmap"] = newTable("cmap") + self.font["cmap"].tableVersion = 0 + self.font["cmap"].tables = subTables + + def setupNameTable(self, nameStrings, windows=True, mac=True): + """Create the `name` table for the font. The `nameStrings` argument must + be a dict, mapping nameIDs or descriptive names for the nameIDs to name + record values. A value is either a string, or a dict, mapping language codes + to strings, to allow localized name table entries. + + By default, both Windows (platformID=3) and Macintosh (platformID=1) name + records are added, unless any of `windows` or `mac` arguments is False. + + The following descriptive names are available for nameIDs: + + copyright (nameID 0) + familyName (nameID 1) + styleName (nameID 2) + uniqueFontIdentifier (nameID 3) + fullName (nameID 4) + version (nameID 5) + psName (nameID 6) + trademark (nameID 7) + manufacturer (nameID 8) + designer (nameID 9) + description (nameID 10) + vendorURL (nameID 11) + designerURL (nameID 12) + licenseDescription (nameID 13) + licenseInfoURL (nameID 14) + typographicFamily (nameID 16) + typographicSubfamily (nameID 17) + compatibleFullName (nameID 18) + sampleText (nameID 19) + postScriptCIDFindfontName (nameID 20) + wwsFamilyName (nameID 21) + wwsSubfamilyName (nameID 22) + lightBackgroundPalette (nameID 23) + darkBackgroundPalette (nameID 24) + variationsPostScriptNamePrefix (nameID 25) + """ + nameTable = self.font["name"] = newTable("name") + nameTable.names = [] + + for nameName, nameValue in nameStrings.items(): + if isinstance(nameName, int): + nameID = nameName + else: + nameID = _nameIDs[nameName] + if isinstance(nameValue, basestring): + nameValue = dict(en=nameValue) + nameTable.addMultilingualName( + nameValue, ttFont=self.font, nameID=nameID, windows=windows, mac=mac + ) + + def setupOS2(self, **values): + """Create a new `OS/2` table and initialize it with default values, + which can be overridden by keyword arguments. + """ + if "xAvgCharWidth" not in values: + gs = self.font.getGlyphSet() + widths = [gs[glyphName].width for glyphName in gs.keys() if gs[glyphName].width > 0] + values["xAvgCharWidth"] = int(round(sum(widths) / float(len(widths)))) + self._initTableWithValues("OS/2", _OS2Defaults, values) + if not ("ulUnicodeRange1" in values or "ulUnicodeRange2" in values or + "ulUnicodeRange3" in values or "ulUnicodeRange3" in values): + assert "cmap" in self.font, "the 'cmap' table must be setup before the 'OS/2' table" + self.font["OS/2"].recalcUnicodeRanges(self.font) + + def setupCFF(self, psName, fontInfo, charStringsDict, privateDict): + from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \ + GlobalSubrsIndex, PrivateDict + + assert not self.isTTF + self.font.sfntVersion = "OTTO" + fontSet = CFFFontSet() + fontSet.major = 1 + fontSet.minor = 0 + fontSet.fontNames = [psName] + fontSet.topDictIndex = TopDictIndex() + + globalSubrs = GlobalSubrsIndex() + fontSet.GlobalSubrs = globalSubrs + private = PrivateDict() + for key, value in privateDict.items(): + setattr(private, key, value) + fdSelect = None + fdArray = None + + topDict = TopDict() + topDict.charset = self.font.getGlyphOrder() + topDict.Private = private + for key, value in fontInfo.items(): + setattr(topDict, key, value) + if "FontMatrix" not in fontInfo: + scale = 1 / self.font["head"].unitsPerEm + topDict.FontMatrix = [scale, 0, 0, scale, 0, 0] + + charStrings = CharStrings(None, topDict.charset, globalSubrs, private, fdSelect, fdArray) + for glyphName, charString in charStringsDict.items(): + charString.private = private + charString.globalSubrs = globalSubrs + charStrings[glyphName] = charString + topDict.CharStrings = charStrings + + fontSet.topDictIndex.append(topDict) + + self.font["CFF "] = newTable("CFF ") + self.font["CFF "].cff = fontSet + + def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None): + from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \ + GlobalSubrsIndex, PrivateDict, FDArrayIndex, FontDict + + assert not self.isTTF + self.font.sfntVersion = "OTTO" + fontSet = CFFFontSet() + fontSet.major = 2 + fontSet.minor = 0 + + cff2GetGlyphOrder = self.font.getGlyphOrder + fontSet.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None) + + globalSubrs = GlobalSubrsIndex() + fontSet.GlobalSubrs = globalSubrs + + if fdArrayList is None: + fdArrayList = [{}] + fdSelect = None + fdArray = FDArrayIndex() + fdArray.strings = None + fdArray.GlobalSubrs = globalSubrs + for privateDict in fdArrayList: + fontDict = FontDict() + fontDict.setCFF2(True) + private = PrivateDict() + for key, value in privateDict.items(): + setattr(private, key, value) + fontDict.Private = private + fdArray.append(fontDict) + + topDict = TopDict() + topDict.cff2GetGlyphOrder = cff2GetGlyphOrder + topDict.FDArray = fdArray + scale = 1 / self.font["head"].unitsPerEm + topDict.FontMatrix = [scale, 0, 0, scale, 0, 0] + + private = fdArray[0].Private + charStrings = CharStrings(None, None, globalSubrs, private, fdSelect, fdArray) + for glyphName, charString in charStringsDict.items(): + charString.private = private + charString.globalSubrs = globalSubrs + charStrings[glyphName] = charString + topDict.CharStrings = charStrings + + fontSet.topDictIndex.append(topDict) + + self.font["CFF2"] = newTable("CFF2") + self.font["CFF2"].cff = fontSet + + if regions: + self.setupCFF2Regions(regions) + + def setupCFF2Regions(self, regions): + from .varLib.builder import buildVarRegionList, buildVarData, buildVarStore + from .cffLib import VarStoreData + + assert "fvar" in self.font, "fvar must to be set up first" + assert "CFF2" in self.font, "CFF2 must to be set up first" + axisTags = [a.axisTag for a in self.font["fvar"].axes] + varRegionList = buildVarRegionList(regions, axisTags) + varData = buildVarData(list(range(len(regions))), None, optimize=False) + varStore = buildVarStore(varRegionList, [varData]) + vstore = VarStoreData(otVarStore=varStore) + self.font["CFF2"].cff.topDictIndex[0].VarStore = vstore + + def setupGlyf(self, glyphs, calcGlyphBounds=True): + """Create the `glyf` table from a dict, that maps glyph names + to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example + as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`. + + If `calcGlyphBounds` is True, the bounds of all glyphs will be + calculated. Only pass False if your glyph objects already have + their bounding box values set. + """ + assert self.isTTF + self.font["loca"] = newTable("loca") + self.font["glyf"] = newTable("glyf") + self.font["glyf"].glyphs = glyphs + if hasattr(self.font, "glyphOrder"): + self.font["glyf"].glyphOrder = self.font.glyphOrder + if calcGlyphBounds: + self.calcGlyphBounds() + + def setupFvar(self, axes, instances): + addFvar(self.font, axes, instances) + + def setupGvar(self, variations): + gvar = self.font["gvar"] = newTable('gvar') + gvar.version = 1 + gvar.reserved = 0 + gvar.variations = variations + + def calcGlyphBounds(self): + """Calculate the bounding boxes of all glyphs in the `glyf` table. + This is usually not called explicitly by client code. + """ + glyphTable = self.font["glyf"] + for glyph in glyphTable.glyphs.values(): + glyph.recalcBounds(glyphTable) + + def setupHorizontalMetrics(self, metrics): + """Create a new `hmtx` table, for horizontal metrics. + + The `metrics` argument must be a dict, mapping glyph names to + `(width, leftSidebearing)` tuples. + """ + self.setupMetrics('hmtx', metrics) + + def setupVerticalMetrics(self, metrics): + """Create a new `vmtx` table, for horizontal metrics. + + The `metrics` argument must be a dict, mapping glyph names to + `(height, topSidebearing)` tuples. + """ + self.setupMetrics('vmtx', metrics) + + def setupMetrics(self, tableTag, metrics): + """See `setupHorizontalMetrics()` and `setupVerticalMetrics()`.""" + assert tableTag in ("hmtx", "vmtx") + mtxTable = self.font[tableTag] = newTable(tableTag) + roundedMetrics = {} + for gn in metrics: + w, lsb = metrics[gn] + roundedMetrics[gn] = int(round(w)), int(round(lsb)) + mtxTable.metrics = roundedMetrics + + def setupHorizontalHeader(self, **values): + """Create a new `hhea` table initialize it with default values, + which can be overridden by keyword arguments. + """ + self._initTableWithValues("hhea", _hheaDefaults, values) + + def setupVerticalHeader(self, **values): + """Create a new `vhea` table initialize it with default values, + which can be overridden by keyword arguments. + """ + self._initTableWithValues("vhea", _vheaDefaults, values) + + def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None): + """Create a new `VORG` table. The `verticalOrigins` argument must be + a dict, mapping glyph names to vertical origin values. + + The `defaultVerticalOrigin` argument should be the most common vertical + origin value. If omitted, this value will be derived from the actual + values in the `verticalOrigins` argument. + """ + if defaultVerticalOrigin is None: + # find the most frequent vorg value + bag = {} + for gn in verticalOrigins: + vorg = verticalOrigins[gn] + if vorg not in bag: + bag[vorg] = 1 + else: + bag[vorg] += 1 + defaultVerticalOrigin = sorted(bag, key=lambda vorg: bag[vorg], reverse=True)[0] + self._initTableWithValues("VORG", {}, dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin)) + vorgTable = self.font["VORG"] + vorgTable.majorVersion = 1 + vorgTable.minorVersion = 0 + for gn in verticalOrigins: + vorgTable[gn] = verticalOrigins[gn] + + def setupPost(self, keepGlyphNames=True, **values): + """Create a new `post` table and initialize it with default values, + which can be overridden by keyword arguments. + """ + postTable = self._initTableWithValues("post", _postDefaults, values) + if self.isTTF and keepGlyphNames: + postTable.formatType = 2.0 + postTable.extraNames = [] + postTable.mapping = {} + else: + postTable.formatType = 3.0 + + def setupMaxp(self): + """Create a new `maxp` table. This is called implicitly by FontBuilder + itself and is usually not called by client code. + """ + if self.isTTF: + defaults = _maxpDefaultsTTF + else: + defaults = _maxpDefaultsOTF + self._initTableWithValues("maxp", defaults, {}) + + def setupDummyDSIG(self): + """This adds a dummy DSIG table to the font to make some MS applications + happy. This does not properly sign the font. + """ + from .ttLib.tables.D_S_I_G_ import SignatureRecord + + sig = SignatureRecord() + sig.ulLength = 20 + sig.cbSignature = 12 + sig.usReserved2 = 0 + sig.usReserved1 = 0 + sig.pkcs7 = b'\xd3M4\xd3M5\xd3M4\xd3M4' + sig.ulFormat = 1 + sig.ulOffset = 20 + + values = dict( + ulVersion = 1, + usFlag = 1, + usNumSigs = 1, + signatureRecords = [sig], + ) + self._initTableWithValues("DSIG", {}, values) + + def addOpenTypeFeatures(self, features, filename=None, tables=None): + """Add OpenType features to the font from a string containing + Feature File syntax. + + The `filename` argument is used in error messages and to determine + where to look for "include" files. + + The optional `tables` argument can be a list of OTL tables tags to + build, allowing the caller to only build selected OTL tables. See + `fontTools.feaLib` for details. + """ + from .feaLib.builder import addOpenTypeFeaturesFromString + addOpenTypeFeaturesFromString(self.font, features, filename=filename, tables=tables) + + +def buildCmapSubTable(cmapping, format, platformID, platEncID): + subTable = cmap_classes[format](format) + subTable.cmap = cmapping + subTable.platformID = platformID + subTable.platEncID = platEncID + subTable.language = 0 + return subTable + + +def addFvar(font, axes, instances): + from .misc.py23 import Tag, tounicode + from .ttLib.tables._f_v_a_r import Axis, NamedInstance + + assert axes + + fvar = newTable('fvar') + nameTable = font['name'] + + for tag, minValue, defaultValue, maxValue, name in axes: + axis = Axis() + axis.axisTag = Tag(tag) + axis.minValue, axis.defaultValue, axis.maxValue = minValue, defaultValue, maxValue + axis.axisNameID = nameTable.addName(tounicode(name)) + fvar.axes.append(axis) + + for instance in instances: + coordinates = instance['location'] + name = tounicode(instance['stylename']) + psname = instance.get('postscriptfontname') + + inst = NamedInstance() + inst.subfamilyNameID = nameTable.addName(name) + if psname is not None: + psname = tounicode(psname) + inst.postscriptNameID = nameTable.addName(psname) + inst.coordinates = coordinates + fvar.instances.append(inst) + + font['fvar'] = fvar diff --git a/Lib/fontTools/merge.py b/Lib/fontTools/merge.py index 7dcb8b52..1005b859 100644 --- a/Lib/fontTools/merge.py +++ b/Lib/fontTools/merge.py @@ -498,7 +498,11 @@ def mergeScripts(lst): self = otTables.Script() self.LangSysRecord = lsrecords self.LangSysCount = len(lsrecords) - self.DefaultLangSys = mergeLangSyses([s.DefaultLangSys for s in lst if s.DefaultLangSys]) + dfltLangSyses = [s.DefaultLangSys for s in lst if s.DefaultLangSys] + if dfltLangSyses: + self.DefaultLangSys = mergeLangSyses(dfltLangSyses) + else: + self.DefaultLangSys = None return self def mergeScriptRecords(lst): diff --git a/Lib/fontTools/misc/dictTools.py b/Lib/fontTools/misc/dictTools.py new file mode 100644 index 00000000..db5d6587 --- /dev/null +++ b/Lib/fontTools/misc/dictTools.py @@ -0,0 +1,68 @@ +"""Misc dict tools.""" + +from __future__ import print_function, absolute_import, division +from fontTools.misc.py23 import * + +__all__ = ['hashdict'] + +# https://stackoverflow.com/questions/1151658/python-hashable-dicts +class hashdict(dict): + """ + hashable dict implementation, suitable for use as a key into + other dicts. + + >>> h1 = hashdict({"apples": 1, "bananas":2}) + >>> h2 = hashdict({"bananas": 3, "mangoes": 5}) + >>> h1+h2 + hashdict(apples=1, bananas=3, mangoes=5) + >>> d1 = {} + >>> d1[h1] = "salad" + >>> d1[h1] + 'salad' + >>> d1[h2] + Traceback (most recent call last): + ... + KeyError: hashdict(bananas=3, mangoes=5) + + based on answers from + http://stackoverflow.com/questions/1151658/python-hashable-dicts + + """ + def __key(self): + return tuple(sorted(self.items())) + def __repr__(self): + return "{0}({1})".format(self.__class__.__name__, + ", ".join("{0}={1}".format( + str(i[0]),repr(i[1])) for i in self.__key())) + + def __hash__(self): + return hash(self.__key()) + def __setitem__(self, key, value): + raise TypeError("{0} does not support item assignment" + .format(self.__class__.__name__)) + def __delitem__(self, key): + raise TypeError("{0} does not support item assignment" + .format(self.__class__.__name__)) + def clear(self): + raise TypeError("{0} does not support item assignment" + .format(self.__class__.__name__)) + def pop(self, *args, **kwargs): + raise TypeError("{0} does not support item assignment" + .format(self.__class__.__name__)) + def popitem(self, *args, **kwargs): + raise TypeError("{0} does not support item assignment" + .format(self.__class__.__name__)) + def setdefault(self, *args, **kwargs): + raise TypeError("{0} does not support item assignment" + .format(self.__class__.__name__)) + def update(self, *args, **kwargs): + raise TypeError("{0} does not support item assignment" + .format(self.__class__.__name__)) + # update is not ok because it mutates the object + # __add__ is ok because it creates a new object + # while the new object is under construction, it's ok to mutate it + def __add__(self, right): + result = hashdict(self) + dict.update(result, right) + return result + diff --git a/Lib/fontTools/misc/intTools.py b/Lib/fontTools/misc/intTools.py new file mode 100644 index 00000000..9eb2f0f9 --- /dev/null +++ b/Lib/fontTools/misc/intTools.py @@ -0,0 +1,18 @@ +"""Misc integer tools.""" + +from __future__ import print_function, absolute_import, division +from fontTools.misc.py23 import * + +__all__ = ['popCount'] + + +def popCount(v): + """Return number of 1 bits in an integer.""" + + if v > 0xFFFFFFFF: + return popCount(v >> 32) + popCount(v & 0xFFFFFFFF) + + # HACKMEM 169 + y = (v >> 1) & 0xDB6DB6DB + y = v - y - ((y >> 1) & 0xDB6DB6DB) + return (((y + (y >> 3)) & 0xC71C71C7) % 0x3F) diff --git a/Lib/fontTools/misc/macRes.py b/Lib/fontTools/misc/macRes.py index 20bfa717..db832ec5 100644 --- a/Lib/fontTools/misc/macRes.py +++ b/Lib/fontTools/misc/macRes.py @@ -33,6 +33,8 @@ class ResourceReader(MutableMapping): @staticmethod def openResourceFork(path): + if hasattr(path, "__fspath__"): # support os.PathLike objects + path = path.__fspath__() with open(path + '/..namedfork/rsrc', 'rb') as resfork: data = resfork.read() infile = BytesIO(data) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index fe695936..a0e1003f 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -7,6 +7,11 @@ from base64 import b64encode, b64decode from numbers import Integral try: + from collections.abc import Mapping # python >= 3.3 +except ImportError: + from collections import Mapping + +try: from functools import singledispatch except ImportError: try: @@ -384,7 +389,7 @@ if singledispatch is not None: _make_element.register(bool)(_bool_element) _make_element.register(Integral)(_integer_element) _make_element.register(float)(_real_element) - _make_element.register(dict)(_dict_element) + _make_element.register(Mapping)(_dict_element) _make_element.register(list)(_array_element) _make_element.register(tuple)(_array_element) _make_element.register(datetime)(_date_element) @@ -404,7 +409,7 @@ else: return _integer_element(value, ctx) elif isinstance(value, float): return _real_element(value, ctx) - elif isinstance(value, dict): + elif isinstance(value, Mapping): return _dict_element(value, ctx) elif isinstance(value, (list, tuple)): return _array_element(value, ctx) diff --git a/Lib/fontTools/misc/psCharStrings.py b/Lib/fontTools/misc/psCharStrings.py index a92802b0..68810744 100644 --- a/Lib/fontTools/misc/psCharStrings.py +++ b/Lib/fontTools/misc/psCharStrings.py @@ -223,9 +223,16 @@ def encodeFixed(f, pack=struct.pack): else: return b"\xff" + pack(">l", value) # encode the entire fixed point value + +realZeroBytes = bytechr(30) + bytechr(0xf) + def encodeFloat(f): # For CFF only, used in cffLib - s = str(f).upper() + if f == 0.0: # 0.0 == +0.0 == -0.0 + return realZeroBytes + # Note: 14 decimal digits seems to be the limitation for CFF real numbers + # in macOS. However, we use 8 here to match the implementation of AFDKO. + s = "%.8G" % f if s[:2] == "0.": s = s[1:] elif s[:3] == "-0.": @@ -234,9 +241,13 @@ def encodeFloat(f): while s: c = s[0] s = s[1:] - if c == "E" and s[:1] == "-": - s = s[1:] - c = "E-" + if c == "E": + c2 = s[:1] + if c2 == "-": + s = s[1:] + c = "E-" + elif c2 == "+": + s = s[1:] nibbles.append(realNibblesDict[c]) nibbles.append(0xf) if len(nibbles) % 2: @@ -267,22 +278,6 @@ class SimpleT2Decompiler(object): self.hintMaskBytes = 0 self.numRegions = 0 - def check_program(self, program): - if not hasattr(self, 'private') or self.private is None: - # Type 1 charstrings don't have self.private. - # Type2 CFF charstrings may have self.private == None. - # In both cases, they are not CFF2 charstrings - isCFF2 = False - else: - isCFF2 = self.private._isCFF2 - if isCFF2: - if program: - assert program[-1] not in ("seac",), "illegal CharString Terminator" - else: - assert program, "illegal CharString: decompiled to empty program" - assert program[-1] in ("endchar", "return", "callsubr", "callgsubr", - "seac"), "illegal CharString" - def execute(self, charString): self.callingStack.append(charString) needsDecompilation = charString.needsDecompilation() @@ -311,7 +306,6 @@ class SimpleT2Decompiler(object): else: pushToStack(token) if needsDecompilation: - self.check_program(program) charString.setProgram(program) del self.callingStack[-1] @@ -424,7 +418,7 @@ class SimpleT2Decompiler(object): numBlends = self.pop() numOps = numBlends * (self.numRegions + 1) blendArgs = self.operandStack[-numOps:] - del self.operandStack[:-(numOps-numBlends)] # Leave the default operands on the stack. + del self.operandStack[-(numOps-numBlends):] # Leave the default operands on the stack. def op_vsindex(self, index): vi = self.pop() @@ -463,8 +457,8 @@ t1Operators = [ class T2WidthExtractor(SimpleT2Decompiler): - def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): - SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs) + def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None): + SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private) self.nominalWidthX = nominalWidthX self.defaultWidthX = defaultWidthX @@ -477,6 +471,8 @@ class T2WidthExtractor(SimpleT2Decompiler): args = self.popall() if not self.gotWidth: if evenOdd ^ (len(args) % 2): + # For CFF2 charstrings, this should never happen + assert self.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value" self.width = self.nominalWidthX + args[0] args = args[1:] else: @@ -503,9 +499,9 @@ class T2WidthExtractor(SimpleT2Decompiler): class T2OutlineExtractor(T2WidthExtractor): - def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): + def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None): T2WidthExtractor.__init__( - self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) + self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private) self.pen = pen def reset(self): @@ -705,12 +701,6 @@ class T2OutlineExtractor(T2WidthExtractor): self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) - # - # MultipleMaster. Well... - # - def op_blend(self, index): - self.popall() - # misc def op_and(self, index): raise NotImplementedError @@ -947,7 +937,6 @@ class T2CharString(object): operators, opcodes = buildOperatorDict(t2Operators) decompilerClass = SimpleT2Decompiler outlineExtractor = T2OutlineExtractor - isCFF2 = False def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None): if program is None: @@ -979,7 +968,8 @@ class T2CharString(object): def draw(self, pen): subrs = getattr(self.private, "Subrs", []) extractor = self.outlineExtractor(pen, subrs, self.globalSubrs, - self.private.nominalWidthX, self.private.defaultWidthX) + self.private.nominalWidthX, self.private.defaultWidthX, + self.private) extractor.execute(self) self.width = extractor.width @@ -988,20 +978,11 @@ class T2CharString(object): self.draw(boundsPen) return boundsPen.bounds - def check_program(self, program, isCFF2=False): - if isCFF2: - if self.program: - assert self.program[-1] not in ("seac",), "illegal CFF2 CharString Termination" - else: - assert self.program, "illegal CharString: decompiled to empty program" - assert self.program[-1] in ("endchar", "return", "callsubr", "callgsubr", "seac"), "illegal CharString" - def compile(self, isCFF2=False): if self.bytecode is not None: return opcodes = self.opcodes program = self.program - self.check_program(program, isCFF2=isCFF2) bytecode = [] encodeInt = self.getIntEncoder() encodeFixed = self.getFixedEncoder() @@ -1148,9 +1129,6 @@ class T2CharString(object): program.append(token) self.setProgram(program) -class CFF2Subr(T2CharString): - isCFF2 = True - class T1CharString(T2CharString): operandEncoding = t1OperandEncoding diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py index d4f4880a..beec5cb5 100644 --- a/Lib/fontTools/mtiLib/__init__.py +++ b/Lib/fontTools/mtiLib/__init__.py @@ -1174,7 +1174,8 @@ def main(args=None, font=None): del args[0] for f in args: log.debug("Processing %s", f) - table = build(open(f, 'rt', encoding="utf-8"), font, tableTag=tableTag) + with open(f, 'rt', encoding="utf-8") as f: + table = build(f, font, tableTag=tableTag) blob = table.compile(font) # Make sure it compiles decompiled = table.__class__() decompiled.decompile(blob, font) # Make sure it decompiles! diff --git a/Lib/fontTools/pens/pointPen.py b/Lib/fontTools/pens/pointPen.py index 641eb446..415972f6 100644 --- a/Lib/fontTools/pens/pointPen.py +++ b/Lib/fontTools/pens/pointPen.py @@ -61,7 +61,7 @@ class BasePointToSegmentPen(AbstractPointPen): def __init__(self): self.currentPath = None - def beginPath(self, **kwargs): + def beginPath(self, identifier=None, **kwargs): assert self.currentPath is None self.currentPath = [] @@ -140,7 +140,8 @@ class BasePointToSegmentPen(AbstractPointPen): self._flushContour(segments) - def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + def addPoint(self, pt, segmentType=None, smooth=False, name=None, + identifier=None, **kwargs): self.currentPath.append((pt, segmentType, smooth, name, kwargs)) @@ -313,22 +314,29 @@ class GuessSmoothPointPen(AbstractPointPen): for pt, segmentType, smooth, name, kwargs in points: self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) - def beginPath(self): + def beginPath(self, identifier=None, **kwargs): assert self._points is None self._points = [] - self._outPen.beginPath() + if identifier is not None: + kwargs["identifier"] = identifier + self._outPen.beginPath(**kwargs) def endPath(self): self._flushContour() self._outPen.endPath() self._points = None - def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + def addPoint(self, pt, segmentType=None, smooth=False, name=None, + identifier=None, **kwargs): + if identifier is not None: + kwargs["identifier"] = identifier self._points.append((pt, segmentType, False, name, kwargs)) - def addComponent(self, glyphName, transformation): + def addComponent(self, glyphName, transformation, identifier=None, **kwargs): assert self._points is None - self._outPen.addComponent(glyphName, transformation) + if identifier is not None: + kwargs["identifier"] = identifier + self._outPen.addComponent(glyphName, transformation, **kwargs) class ReverseContourPointPen(AbstractPointPen): diff --git a/Lib/fontTools/pens/t2CharStringPen.py b/Lib/fontTools/pens/t2CharStringPen.py index 8cc5e088..2025fd55 100644 --- a/Lib/fontTools/pens/t2CharStringPen.py +++ b/Lib/fontTools/pens/t2CharStringPen.py @@ -9,26 +9,26 @@ from fontTools.pens.basePen import BasePen from fontTools.cffLib.specializer import specializeCommands, commandsToProgram +def t2c_round(number, tolerance=0.5): + if tolerance == 0: + return number # no-op + rounded = otRound(number) + # return rounded integer if the tolerance >= 0.5, or if the absolute + # difference between the original float and the rounded integer is + # within the tolerance + if tolerance >= .5 or abs(rounded - number) <= tolerance: + return rounded + else: + # else return the value un-rounded + return number + def makeRoundFunc(tolerance): if tolerance < 0: raise ValueError("Rounding tolerance must be positive") - def _round(number): - if tolerance == 0: - return number # no-op - rounded = otRound(number) - # return rounded integer if the tolerance >= 0.5, or if the absolute - # difference between the original float and the rounded integer is - # within the tolerance - if tolerance >= .5 or abs(rounded - number) <= tolerance: - return rounded - else: - # else return the value un-rounded - return number - def roundPoint(point): x, y = point - return _round(x), _round(y) + return t2c_round(x, tolerance), t2c_round(y, tolerance) return roundPoint diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 6ed54e5f..2ed17cc9 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -7,9 +7,9 @@ from fontTools.misc.py23 import * from fontTools.misc.fixedTools import otRound from fontTools import ttLib from fontTools.ttLib.tables import otTables -from fontTools.misc import psCharStrings from fontTools.pens.basePen import NullPen from fontTools.misc.loggingTools import Timer +from fontTools.subset.cff import * from fontTools.varLib import varStore import sys import struct @@ -378,12 +378,6 @@ def _add_method(*clazzes): def _uniq_sort(l): return sorted(set(l)) -def _set_update(s, *others): - # Jython's set.update only takes one other argument. - # Emulate real set.update... - for other in others: - s.update(other) - def _dict_subset(d, glyphs): return {g:d[g] for g in glyphs} @@ -457,7 +451,7 @@ def subset_glyphs(self, s): def closure_glyphs(self, s, cur_glyphs): for glyph, subst in self.mapping.items(): if glyph in cur_glyphs: - _set_update(s.glyphs, subst) + s.glyphs.update(subst) @_add_method(otTables.MultipleSubst) def subset_glyphs(self, s): @@ -467,8 +461,8 @@ def subset_glyphs(self, s): @_add_method(otTables.AlternateSubst) def closure_glyphs(self, s, cur_glyphs): - _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items() - if g in cur_glyphs)) + s.glyphs.update(*(vlist for g,vlist in self.alternates.items() + if g in cur_glyphs)) @_add_method(otTables.AlternateSubst) def subset_glyphs(self, s): @@ -480,10 +474,10 @@ def subset_glyphs(self, s): @_add_method(otTables.LigatureSubst) def closure_glyphs(self, s, cur_glyphs): - _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs - if all(c in s.glyphs for c in seq.Component)] - for g,seqs in self.ligatures.items() - if g in cur_glyphs)) + s.glyphs.update(*([seq.LigGlyph for seq in seqs + if all(c in s.glyphs for c in seq.Component)] + for g,seqs in self.ligatures.items() + if g in cur_glyphs)) @_add_method(otTables.LigatureSubst) def subset_glyphs(self, s): @@ -2087,517 +2081,6 @@ def prune_post_subset(self, font, options): return True -class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler): - - def __init__(self, components, localSubrs, globalSubrs): - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - self.components = components - - def op_endchar(self, index): - args = self.popall() - if len(args) >= 4: - from fontTools.encodings.StandardEncoding import StandardEncoding - # endchar can do seac accent bulding; The T2 spec says it's deprecated, - # but recent software that shall remain nameless does output it. - adx, ady, bchar, achar = args[-4:] - baseGlyph = StandardEncoding[bchar] - accentGlyph = StandardEncoding[achar] - self.components.add(baseGlyph) - self.components.add(accentGlyph) - -@_add_method(ttLib.getTableClass('CFF ')) -def closure_glyphs(self, s): - cff = self.cff - assert len(cff) == 1 - font = cff[cff.keys()[0]] - glyphSet = font.CharStrings - - decompose = s.glyphs - while decompose: - components = set() - for g in decompose: - if g not in glyphSet: - continue - gl = glyphSet[g] - - subrs = getattr(gl.private, "Subrs", []) - decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs) - decompiler.execute(gl) - components -= s.glyphs - s.glyphs.update(components) - decompose = components - -@_add_method(ttLib.getTableClass('CFF ')) -def prune_pre_subset(self, font, options): - cff = self.cff - # CFF table must have one font only - cff.fontNames = cff.fontNames[:1] - - if options.notdef_glyph and not options.notdef_outline: - for fontname in cff.keys(): - font = cff[fontname] - c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef') - if hasattr(font, 'FDArray') and font.FDArray is not None: - private = font.FDArray[fdSelectIndex].Private - else: - private = font.Private - dfltWdX = private.defaultWidthX - nmnlWdX = private.nominalWidthX - pen = NullPen() - c.draw(pen) # this will set the charstring's width - if c.width != dfltWdX: - c.program = [c.width - nmnlWdX, 'endchar'] - else: - c.program = ['endchar'] - - # Clear useless Encoding - for fontname in cff.keys(): - font = cff[fontname] - # https://github.com/behdad/fonttools/issues/620 - font.Encoding = "StandardEncoding" - - return True # bool(cff.fontNames) - -@_add_method(ttLib.getTableClass('CFF ')) -def subset_glyphs(self, s): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - - # Load all glyphs - for g in font.charset: - if g not in s.glyphs: continue - c, _ = cs.getItemAndSelector(g) - - if cs.charStringsAreIndexed: - indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] - csi = cs.charStringsIndex - csi.items = [csi.items[i] for i in indices] - del csi.file, csi.offsets - if hasattr(font, "FDSelect"): - sel = font.FDSelect - # XXX We want to set sel.format to None, such that the - # most compact format is selected. However, OTS was - # broken and couldn't parse a FDSelect format 0 that - # happened before CharStrings. As such, always force - # format 3 until we fix cffLib to always generate - # FDSelect after CharStrings. - # https://github.com/khaledhosny/ots/pull/31 - #sel.format = None - sel.format = 3 - sel.gidArray = [sel.gidArray[i] for i in indices] - cs.charStrings = {g:indices.index(v) - for g,v in cs.charStrings.items() - if g in s.glyphs} - else: - cs.charStrings = {g:v - for g,v in cs.charStrings.items() - if g in s.glyphs} - font.charset = [g for g in font.charset if g in s.glyphs] - font.numGlyphs = len(font.charset) - - return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) - -@_add_method(psCharStrings.T2CharString) -def subset_subroutines(self, subrs, gsubrs): - p = self.program - assert len(p) - for i in range(1, len(p)): - if p[i] == 'callsubr': - assert isinstance(p[i-1], int) - p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias - elif p[i] == 'callgsubr': - assert isinstance(p[i-1], int) - p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias - -@_add_method(psCharStrings.T2CharString) -def drop_hints(self): - hints = self._hints - - if hints.deletions: - p = self.program - for idx in reversed(hints.deletions): - del p[idx-2:idx] - - if hints.has_hint: - assert not hints.deletions or hints.last_hint <= hints.deletions[0] - self.program = self.program[hints.last_hint:] - if hasattr(self, 'width'): - # Insert width back if needed - if self.width != self.private.defaultWidthX: - self.program.insert(0, self.width - self.private.nominalWidthX) - - if hints.has_hintmask: - i = 0 - p = self.program - while i < len(p): - if p[i] in ['hintmask', 'cntrmask']: - assert i + 1 <= len(p) - del p[i:i+2] - continue - i += 1 - - assert len(self.program) - - del self._hints - -class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): - - def __init__(self, localSubrs, globalSubrs): - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - for subrs in [localSubrs, globalSubrs]: - if subrs and not hasattr(subrs, "_used"): - subrs._used = set() - - def op_callsubr(self, index): - self.localSubrs._used.add(self.operandStack[-1]+self.localBias) - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - - def op_callgsubr(self, index): - self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) - -class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): - - class Hints(object): - def __init__(self): - # Whether calling this charstring produces any hint stems - # Note that if a charstring starts with hintmask, it will - # have has_hint set to True, because it *might* produce an - # implicit vstem if called under certain conditions. - self.has_hint = False - # Index to start at to drop all hints - self.last_hint = 0 - # Index up to which we know more hints are possible. - # Only relevant if status is 0 or 1. - self.last_checked = 0 - # The status means: - # 0: after dropping hints, this charstring is empty - # 1: after dropping hints, there may be more hints - # continuing after this - # 2: no more hints possible after this charstring - self.status = 0 - # Has hintmask instructions; not recursive - self.has_hintmask = False - # List of indices of calls to empty subroutines to remove. - self.deletions = [] - pass - - def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): - self._css = css - psCharStrings.T2WidthExtractor.__init__( - self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) - - def execute(self, charString): - old_hints = charString._hints if hasattr(charString, '_hints') else None - charString._hints = self.Hints() - - psCharStrings.T2WidthExtractor.execute(self, charString) - - hints = charString._hints - - if hints.has_hint or hints.has_hintmask: - self._css.add(charString) - - if hints.status != 2: - # Check from last_check, make sure we didn't have any operators. - for i in range(hints.last_checked, len(charString.program) - 1): - if isinstance(charString.program[i], str): - hints.status = 2 - break - else: - hints.status = 1 # There's *something* here - hints.last_checked = len(charString.program) - - if old_hints: - assert hints.__dict__ == old_hints.__dict__ - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1]+self.localBias] - psCharStrings.T2WidthExtractor.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] - psCharStrings.T2WidthExtractor.op_callgsubr(self, index) - self.processSubr(index, subr) - - def op_hstem(self, index): - psCharStrings.T2WidthExtractor.op_hstem(self, index) - self.processHint(index) - def op_vstem(self, index): - psCharStrings.T2WidthExtractor.op_vstem(self, index) - self.processHint(index) - def op_hstemhm(self, index): - psCharStrings.T2WidthExtractor.op_hstemhm(self, index) - self.processHint(index) - def op_vstemhm(self, index): - psCharStrings.T2WidthExtractor.op_vstemhm(self, index) - self.processHint(index) - def op_hintmask(self, index): - rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) - self.processHintmask(index) - return rv - def op_cntrmask(self, index): - rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) - self.processHintmask(index) - return rv - - def processHintmask(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hintmask = True - if hints.status != 2: - # Check from last_check, see if we may be an implicit vstem - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break - else: - # We are an implicit vstem - hints.has_hint = True - hints.last_hint = index + 1 - hints.status = 0 - hints.last_checked = index + 1 - - def processHint(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hint = True - hints.last_hint = index - hints.last_checked = index - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - hints = cs._hints - subr_hints = subr._hints - - # Check from last_check, make sure we didn't have - # any operators. - if hints.status != 2: - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break - hints.last_checked = index - - if hints.status != 2: - if subr_hints.has_hint: - hints.has_hint = True - - # Decide where to chop off from - if subr_hints.status == 0: - hints.last_hint = index - else: - hints.last_hint = index - 2 # Leave the subr call in - elif subr_hints.status == 0: - hints.deletions.append(index) - - hints.status = max(hints.status, subr_hints.status) - -class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler): - - def __init__(self, localSubrs, globalSubrs): - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - - def execute(self, charString): - # Note: Currently we recompute _desubroutinized each time. - # This is more robust in some cases, but in other places we assume - # that each subroutine always expands to the same code, so - # maybe it doesn't matter. To speed up we can just not - # recompute _desubroutinized if it's there. For now I just - # double-check that it desubroutinized to the same thing. - old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None - - charString._patches = [] - psCharStrings.SimpleT2Decompiler.execute(self, charString) - desubroutinized = charString.program[:] - for idx,expansion in reversed (charString._patches): - assert idx >= 2 - assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1] - assert type(desubroutinized[idx - 2]) == int - if expansion[-1] == 'return': - expansion = expansion[:-1] - desubroutinized[idx-2:idx] = expansion - if 'endchar' in desubroutinized: - # Cut off after first endchar - desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1] - else: - if not len(desubroutinized) or desubroutinized[-1] != 'return': - desubroutinized.append('return') - - charString._desubroutinized = desubroutinized - del charString._patches - - if old_desubroutinized: - assert desubroutinized == old_desubroutinized - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1]+self.localBias] - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) - self.processSubr(index, subr) - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - cs._patches.append((index, subr._desubroutinized)) - - -@_add_method(ttLib.getTableClass('CFF ')) -def prune_post_subset(self, font, options): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - - # Drop unused FontDictionaries - if hasattr(font, "FDSelect"): - sel = font.FDSelect - indices = _uniq_sort(sel.gidArray) - sel.gidArray = [indices.index (ss) for ss in sel.gidArray] - arr = font.FDArray - arr.items = [arr[i] for i in indices] - del arr.file, arr.offsets - - # Desubroutinize if asked for - if options.desubroutinize: - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs) - decompiler.execute(c) - c.program = c._desubroutinized - - # Drop hints if not needed - if not options.hinting: - - # This can be tricky, but doesn't have to. What we do is: - # - # - Run all used glyph charstrings and recurse into subroutines, - # - For each charstring (including subroutines), if it has any - # of the hint stem operators, we mark it as such. - # Upon returning, for each charstring we note all the - # subroutine calls it makes that (recursively) contain a stem, - # - Dropping hinting then consists of the following two ops: - # * Drop the piece of the program in each charstring before the - # last call to a stem op or a stem-calling subroutine, - # * Drop all hintmask operations. - # - It's trickier... A hintmask right after hints and a few numbers - # will act as an implicit vstemhm. As such, we track whether - # we have seen any non-hint operators so far and do the right - # thing, recursively... Good luck understanding that :( - css = set() - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs, - c.private.nominalWidthX, - c.private.defaultWidthX) - decompiler.execute(c) - c.width = decompiler.width - for charstring in css: - charstring.drop_hints() - del css - - # Drop font-wide hinting values - all_privs = [] - if hasattr(font, 'FDSelect'): - all_privs.extend(fd.Private for fd in font.FDArray) - else: - all_privs.append(font.Private) - for priv in all_privs: - for k in ['BlueValues', 'OtherBlues', - 'FamilyBlues', 'FamilyOtherBlues', - 'BlueScale', 'BlueShift', 'BlueFuzz', - 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW', - 'ForceBold', 'LanguageGroup', 'ExpansionFactor']: - if hasattr(priv, k): - setattr(priv, k, None) - - # Renumber subroutines to remove unused ones - - # Mark all used subroutines - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) - decompiler.execute(c) - - all_subrs = [font.GlobalSubrs] - if hasattr(font, 'FDSelect'): - all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) - elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: - all_subrs.append(font.Private.Subrs) - - subrs = set(subrs) # Remove duplicates - - # Prepare - for subrs in all_subrs: - if not hasattr(subrs, '_used'): - subrs._used = set() - subrs._used = _uniq_sort(subrs._used) - subrs._old_bias = psCharStrings.calcSubrBias(subrs) - subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) - - # Renumber glyph charstrings - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - c.subset_subroutines (subrs, font.GlobalSubrs) - - # Renumber subroutines themselves - for subrs in all_subrs: - if subrs == font.GlobalSubrs: - if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): - local_subrs = font.Private.Subrs - else: - local_subrs = [] - else: - local_subrs = subrs - - subrs.items = [subrs.items[i] for i in subrs._used] - if hasattr(subrs, 'file'): - del subrs.file - if hasattr(subrs, 'offsets'): - del subrs.offsets - - for subr in subrs.items: - subr.subset_subroutines (local_subrs, font.GlobalSubrs) - - # Delete local SubrsIndex if empty - if hasattr(font, 'FDSelect'): - for fd in font.FDArray: - _delete_empty_subrs(fd.Private) - else: - _delete_empty_subrs(font.Private) - - # Cleanup - for subrs in all_subrs: - del subrs._used, subrs._old_bias, subrs._new_bias - - return True - - -def _delete_empty_subrs(private_dict): - if hasattr(private_dict, 'Subrs') and not private_dict.Subrs: - if 'Subrs' in private_dict.rawDict: - del private_dict.rawDict['Subrs'] - del private_dict.Subrs - - @_add_method(ttLib.getTableClass('cmap')) def closure_glyphs(self, s): tables = [t for t in self.tables if t.isUnicode()] @@ -2691,7 +2174,8 @@ def prune_pre_subset(self, font, options): if inst.postscriptNameID != 0xFFFF]) stat = font.get('STAT') if stat: - nameIDs.update([val_rec.ValueNameID for val_rec in stat.table.AxisValueArray.AxisValue]) + if stat.table.AxisValueArray: + nameIDs.update([val_rec.ValueNameID for val_rec in stat.table.AxisValueArray.AxisValue]) nameIDs.update([axis_rec.AxisNameID for axis_rec in stat.table.DesignAxisRecord.Axis]) if '*' not in options.name_IDs: self.names = [n for n in self.names if n.nameID in nameIDs] @@ -3257,7 +2741,8 @@ def main(args=None): text += g[7:] continue if g.startswith('--text-file='): - text += open(g[12:], encoding='utf-8').read().replace('\n', '') + with open(g[12:], encoding='utf-8') as f: + text += f.read().replace('\n', '') continue if g.startswith('--unicodes='): if g[11:] == '*': @@ -3266,15 +2751,17 @@ def main(args=None): unicodes.extend(parse_unicodes(g[11:])) continue if g.startswith('--unicodes-file='): - for line in open(g[16:]).readlines(): - unicodes.extend(parse_unicodes(line.split('#')[0])) + with open(g[16:]) as f: + for line in f.readlines(): + unicodes.extend(parse_unicodes(line.split('#')[0])) continue if g.startswith('--gids='): gids.extend(parse_gids(g[7:])) continue if g.startswith('--gids-file='): - for line in open(g[12:]).readlines(): - gids.extend(parse_gids(line.split('#')[0])) + with open(g[12:]) as f: + for line in f.readlines(): + gids.extend(parse_gids(line.split('#')[0])) continue if g.startswith('--glyphs='): if g[9:] == '*': @@ -3283,8 +2770,9 @@ def main(args=None): glyphs.extend(parse_glyphs(g[9:])) continue if g.startswith('--glyphs-file='): - for line in open(g[14:]).readlines(): - glyphs.extend(parse_glyphs(line.split('#')[0])) + with open(g[14:]) as f: + for line in f.readlines(): + glyphs.extend(parse_glyphs(line.split('#')[0])) continue glyphs.append(g) diff --git a/Lib/fontTools/subset/cff.py b/Lib/fontTools/subset/cff.py new file mode 100644 index 00000000..9a2b77e4 --- /dev/null +++ b/Lib/fontTools/subset/cff.py @@ -0,0 +1,579 @@ +from fontTools.misc import psCharStrings +from fontTools import ttLib +from fontTools.pens.basePen import NullPen +from fontTools.misc.fixedTools import otRound +from fontTools.varLib.varStore import VarStoreInstancer + +def _add_method(*clazzes): + """Returns a decorator function that adds a new method to one or + more classes.""" + def wrapper(method): + done = [] + for clazz in clazzes: + if clazz in done: continue # Support multiple names of a clazz + done.append(clazz) + assert clazz.__name__ != 'DefaultTable', \ + 'Oops, table class not found.' + assert not hasattr(clazz, method.__name__), \ + "Oops, class '%s' has method '%s'." % (clazz.__name__, + method.__name__) + setattr(clazz, method.__name__, method) + return None + return wrapper + +def _uniq_sort(l): + return sorted(set(l)) + +class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler): + + def __init__(self, components, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + self.components = components + + def op_endchar(self, index): + args = self.popall() + if len(args) >= 4: + from fontTools.encodings.StandardEncoding import StandardEncoding + # endchar can do seac accent bulding; The T2 spec says it's deprecated, + # but recent software that shall remain nameless does output it. + adx, ady, bchar, achar = args[-4:] + baseGlyph = StandardEncoding[bchar] + accentGlyph = StandardEncoding[achar] + self.components.add(baseGlyph) + self.components.add(accentGlyph) + +@_add_method(ttLib.getTableClass('CFF ')) +def closure_glyphs(self, s): + cff = self.cff + assert len(cff) == 1 + font = cff[cff.keys()[0]] + glyphSet = font.CharStrings + + decompose = s.glyphs + while decompose: + components = set() + for g in decompose: + if g not in glyphSet: + continue + gl = glyphSet[g] + + subrs = getattr(gl.private, "Subrs", []) + decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs) + decompiler.execute(gl) + components -= s.glyphs + s.glyphs.update(components) + decompose = components + +@_add_method(ttLib.getTableClass('CFF ')) +def prune_pre_subset(self, font, options): + cff = self.cff + # CFF table must have one font only + cff.fontNames = cff.fontNames[:1] + + if options.notdef_glyph and not options.notdef_outline: + for fontname in cff.keys(): + font = cff[fontname] + c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef') + if hasattr(font, 'FDArray') and font.FDArray is not None: + private = font.FDArray[fdSelectIndex].Private + else: + private = font.Private + dfltWdX = private.defaultWidthX + nmnlWdX = private.nominalWidthX + pen = NullPen() + c.draw(pen) # this will set the charstring's width + if c.width != dfltWdX: + c.program = [c.width - nmnlWdX, 'endchar'] + else: + c.program = ['endchar'] + + # Clear useless Encoding + for fontname in cff.keys(): + font = cff[fontname] + # https://github.com/behdad/fonttools/issues/620 + font.Encoding = "StandardEncoding" + + return True # bool(cff.fontNames) + +@_add_method(ttLib.getTableClass('CFF ')) +def subset_glyphs(self, s): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + + # Load all glyphs + for g in font.charset: + if g not in s.glyphs: continue + c, _ = cs.getItemAndSelector(g) + + if cs.charStringsAreIndexed: + indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] + csi = cs.charStringsIndex + csi.items = [csi.items[i] for i in indices] + del csi.file, csi.offsets + if hasattr(font, "FDSelect"): + sel = font.FDSelect + # XXX We want to set sel.format to None, such that the + # most compact format is selected. However, OTS was + # broken and couldn't parse a FDSelect format 0 that + # happened before CharStrings. As such, always force + # format 3 until we fix cffLib to always generate + # FDSelect after CharStrings. + # https://github.com/khaledhosny/ots/pull/31 + #sel.format = None + sel.format = 3 + sel.gidArray = [sel.gidArray[i] for i in indices] + cs.charStrings = {g:indices.index(v) + for g,v in cs.charStrings.items() + if g in s.glyphs} + else: + cs.charStrings = {g:v + for g,v in cs.charStrings.items() + if g in s.glyphs} + font.charset = [g for g in font.charset if g in s.glyphs] + font.numGlyphs = len(font.charset) + + return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) + +@_add_method(psCharStrings.T2CharString) +def subset_subroutines(self, subrs, gsubrs): + p = self.program + for i in range(1, len(p)): + if p[i] == 'callsubr': + assert isinstance(p[i-1], int) + p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias + elif p[i] == 'callgsubr': + assert isinstance(p[i-1], int) + p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias + +@_add_method(psCharStrings.T2CharString) +def drop_hints(self): + hints = self._hints + + if hints.deletions: + p = self.program + for idx in reversed(hints.deletions): + del p[idx-2:idx] + + if hints.has_hint: + assert not hints.deletions or hints.last_hint <= hints.deletions[0] + self.program = self.program[hints.last_hint:] + if not self.program: + # TODO CFF2 no need for endchar. + self.program.append('endchar') + if hasattr(self, 'width'): + # Insert width back if needed + if self.width != self.private.defaultWidthX: + # For CFF2 charstrings, this should never happen + assert self.private.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value" + self.program.insert(0, self.width - self.private.nominalWidthX) + + if hints.has_hintmask: + i = 0 + p = self.program + while i < len(p): + if p[i] in ['hintmask', 'cntrmask']: + assert i + 1 <= len(p) + del p[i:i+2] + continue + i += 1 + + assert len(self.program) + + del self._hints + +class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): + + def __init__(self, localSubrs, globalSubrs, private): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs, + private) + for subrs in [localSubrs, globalSubrs]: + if subrs and not hasattr(subrs, "_used"): + subrs._used = set() + + def op_callsubr(self, index): + self.localSubrs._used.add(self.operandStack[-1]+self.localBias) + psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) + + def op_callgsubr(self, index): + self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + +class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): + + class Hints(object): + def __init__(self): + # Whether calling this charstring produces any hint stems + # Note that if a charstring starts with hintmask, it will + # have has_hint set to True, because it *might* produce an + # implicit vstem if called under certain conditions. + self.has_hint = False + # Index to start at to drop all hints + self.last_hint = 0 + # Index up to which we know more hints are possible. + # Only relevant if status is 0 or 1. + self.last_checked = 0 + # The status means: + # 0: after dropping hints, this charstring is empty + # 1: after dropping hints, there may be more hints + # continuing after this, or there might be + # other things. Not clear yet. + # 2: no more hints possible after this charstring + self.status = 0 + # Has hintmask instructions; not recursive + self.has_hintmask = False + # List of indices of calls to empty subroutines to remove. + self.deletions = [] + pass + + def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None): + self._css = css + psCharStrings.T2WidthExtractor.__init__( + self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) + self.private = private + + def execute(self, charString): + old_hints = charString._hints if hasattr(charString, '_hints') else None + charString._hints = self.Hints() + + psCharStrings.T2WidthExtractor.execute(self, charString) + + hints = charString._hints + + if hints.has_hint or hints.has_hintmask: + self._css.add(charString) + + if hints.status != 2: + # Check from last_check, make sure we didn't have any operators. + for i in range(hints.last_checked, len(charString.program) - 1): + if isinstance(charString.program[i], str): + hints.status = 2 + break + else: + hints.status = 1 # There's *something* here + hints.last_checked = len(charString.program) + + if old_hints: + assert hints.__dict__ == old_hints.__dict__ + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1]+self.localBias] + psCharStrings.T2WidthExtractor.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] + psCharStrings.T2WidthExtractor.op_callgsubr(self, index) + self.processSubr(index, subr) + + def op_hstem(self, index): + psCharStrings.T2WidthExtractor.op_hstem(self, index) + self.processHint(index) + def op_vstem(self, index): + psCharStrings.T2WidthExtractor.op_vstem(self, index) + self.processHint(index) + def op_hstemhm(self, index): + psCharStrings.T2WidthExtractor.op_hstemhm(self, index) + self.processHint(index) + def op_vstemhm(self, index): + psCharStrings.T2WidthExtractor.op_vstemhm(self, index) + self.processHint(index) + def op_hintmask(self, index): + rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) + self.processHintmask(index) + return rv + def op_cntrmask(self, index): + rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) + self.processHintmask(index) + return rv + + def processHintmask(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hintmask = True + if hints.status != 2: + # Check from last_check, see if we may be an implicit vstem + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + else: + # We are an implicit vstem + hints.has_hint = True + hints.last_hint = index + 1 + hints.status = 0 + hints.last_checked = index + 1 + + def processHint(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hint = True + hints.last_hint = index + hints.last_checked = index + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + hints = cs._hints + subr_hints = subr._hints + + # Check from last_check, make sure we didn't have + # any operators. + if hints.status != 2: + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + hints.last_checked = index + + if hints.status != 2: + if subr_hints.has_hint: + hints.has_hint = True + + # Decide where to chop off from + if subr_hints.status == 0: + hints.last_hint = index + else: + hints.last_hint = index - 2 # Leave the subr call in + + elif subr_hints.status == 0: + hints.deletions.append(index) + + hints.status = max(hints.status, subr_hints.status) + +class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler): + + def __init__(self, localSubrs, globalSubrs, private=None): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs, private) + + def execute(self, charString): + if hasattr(charString, '_desubroutinized'): + return + + charString._patches = [] + psCharStrings.SimpleT2Decompiler.execute(self, charString) + desubroutinized = charString.program[:] + for idx,expansion in reversed (charString._patches): + assert idx >= 2 + assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1] + assert type(desubroutinized[idx - 2]) == int + if expansion[-1] == 'return': + expansion = expansion[:-1] + desubroutinized[idx-2:idx] = expansion + if not self.private.in_cff2: + if 'endchar' in desubroutinized: + # Cut off after first endchar + desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1] + else: + if not len(desubroutinized) or desubroutinized[-1] != 'return': + desubroutinized.append('return') + + charString._desubroutinized = desubroutinized + del charString._patches + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1]+self.localBias] + psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + self.processSubr(index, subr) + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + cs._patches.append((index, subr._desubroutinized)) + + +@_add_method(ttLib.getTableClass('CFF ')) +def prune_post_subset(self, ttfFont, options): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + + # Drop unused FontDictionaries + if hasattr(font, "FDSelect"): + sel = font.FDSelect + indices = _uniq_sort(sel.gidArray) + sel.gidArray = [indices.index (ss) for ss in sel.gidArray] + arr = font.FDArray + arr.items = [arr[i] for i in indices] + del arr.file, arr.offsets + + # Desubroutinize if asked for + if options.desubroutinize: + self.desubroutinize() + else: + for fontname in cff.keys(): + font = cff[fontname] + self.remove_unused_subroutines() + + # Drop hints if not needed + if not options.hinting: + self.remove_hints() + + + return True + + +def _delete_empty_subrs(private_dict): + if hasattr(private_dict, 'Subrs') and not private_dict.Subrs: + if 'Subrs' in private_dict.rawDict: + del private_dict.rawDict['Subrs'] + del private_dict.Subrs + +@_add_method(ttLib.getTableClass('CFF ')) +def desubroutinize(self): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private) + decompiler.execute(c) + c.program = c._desubroutinized + del c._desubroutinized + # Delete All the Subrs!!! + if font.GlobalSubrs: + del font.GlobalSubrs + if hasattr(font, 'FDArray'): + for fd in font.FDArray: + pd = fd.Private + if hasattr(pd, 'Subrs'): + del pd.Subrs + if 'Subrs' in pd.rawDict: + del pd.rawDict['Subrs'] + self.remove_unused_subroutines() + + +@_add_method(ttLib.getTableClass('CFF ')) +def remove_hints(self): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + # This can be tricky, but doesn't have to. What we do is: + # + # - Run all used glyph charstrings and recurse into subroutines, + # - For each charstring (including subroutines), if it has any + # of the hint stem operators, we mark it as such. + # Upon returning, for each charstring we note all the + # subroutine calls it makes that (recursively) contain a stem, + # - Dropping hinting then consists of the following two ops: + # * Drop the piece of the program in each charstring before the + # last call to a stem op or a stem-calling subroutine, + # * Drop all hintmask operations. + # - It's trickier... A hintmask right after hints and a few numbers + # will act as an implicit vstemhm. As such, we track whether + # we have seen any non-hint operators so far and do the right + # thing, recursively... Good luck understanding that :( + css = set() + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs, + c.private.nominalWidthX, + c.private.defaultWidthX, + c.private) + decompiler.execute(c) + c.width = decompiler.width + for charstring in css: + charstring.drop_hints() + del css + + # Drop font-wide hinting values + all_privs = [] + if hasattr(font, 'FDArray'): + all_privs.extend(fd.Private for fd in font.FDArray) + else: + all_privs.append(font.Private) + for priv in all_privs: + for k in ['BlueValues', 'OtherBlues', + 'FamilyBlues', 'FamilyOtherBlues', + 'BlueScale', 'BlueShift', 'BlueFuzz', + 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW', + 'ForceBold', 'LanguageGroup', 'ExpansionFactor']: + if hasattr(priv, k): + setattr(priv, k, None) + self.remove_unused_subroutines() + + +@_add_method(ttLib.getTableClass('CFF ')) +def remove_unused_subroutines(self): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + # Renumber subroutines to remove unused ones + + # Mark all used subroutines + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + subrs = getattr(c.private, "Subrs", []) + decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private) + decompiler.execute(c) + + all_subrs = [font.GlobalSubrs] + if hasattr(font, 'FDArray'): + all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) + elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: + all_subrs.append(font.Private.Subrs) + + subrs = set(subrs) # Remove duplicates + + # Prepare + for subrs in all_subrs: + if not hasattr(subrs, '_used'): + subrs._used = set() + subrs._used = _uniq_sort(subrs._used) + subrs._old_bias = psCharStrings.calcSubrBias(subrs) + subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) + + # Renumber glyph charstrings + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + subrs = getattr(c.private, "Subrs", []) + c.subset_subroutines (subrs, font.GlobalSubrs) + + # Renumber subroutines themselves + for subrs in all_subrs: + if subrs == font.GlobalSubrs: + if not hasattr(font, 'FDArray') and hasattr(font.Private, 'Subrs'): + local_subrs = font.Private.Subrs + else: + local_subrs = [] + else: + local_subrs = subrs + + subrs.items = [subrs.items[i] for i in subrs._used] + if hasattr(subrs, 'file'): + del subrs.file + if hasattr(subrs, 'offsets'): + del subrs.offsets + + for subr in subrs.items: + subr.subset_subroutines (local_subrs, font.GlobalSubrs) + + # Delete local SubrsIndex if empty + if hasattr(font, 'FDArray'): + for fd in font.FDArray: + _delete_empty_subrs(fd.Private) + else: + _delete_empty_subrs(font.Private) + + # Cleanup + for subrs in all_subrs: + del subrs._used, subrs._old_bias, subrs._new_bias + diff --git a/Lib/fontTools/t1Lib/__init__.py b/Lib/fontTools/t1Lib/__init__.py index 5da9cea4..db5189a5 100644 --- a/Lib/fontTools/t1Lib/__init__.py +++ b/Lib/fontTools/t1Lib/__init__.py @@ -108,11 +108,12 @@ class T1Font(object): def read(path, onlyHeader=False): """reads any Type 1 font file, returns raw data""" - normpath = path.lower() + _, ext = os.path.splitext(path) + ext = ext.lower() creator, typ = getMacCreatorAndType(path) if typ == 'LWFN': return readLWFN(path, onlyHeader), 'LWFN' - if normpath[-4:] == '.pfb': + if ext == '.pfb': return readPFB(path, onlyHeader), 'PFB' else: return readOther(path), 'OTHER' @@ -164,9 +165,8 @@ def readLWFN(path, onlyHeader=False): elif code in [3, 5]: break elif code == 4: - f = open(path, "rb") - data.append(f.read()) - f.close() + with open(path, "rb") as f: + data.append(f.read()) elif code == 0: pass # comment, ignore else: @@ -179,35 +179,32 @@ def readLWFN(path, onlyHeader=False): def readPFB(path, onlyHeader=False): """reads a PFB font file, returns raw data""" - f = open(path, "rb") data = [] - while True: - if f.read(1) != bytechr(128): - raise T1Error('corrupt PFB file') - code = byteord(f.read(1)) - if code in [1, 2]: - chunklen = stringToLong(f.read(4)) - chunk = f.read(chunklen) - assert len(chunk) == chunklen - data.append(chunk) - elif code == 3: - break - else: - raise T1Error('bad chunk code: ' + repr(code)) - if onlyHeader: - break - f.close() + with open(path, "rb") as f: + while True: + if f.read(1) != bytechr(128): + raise T1Error('corrupt PFB file') + code = byteord(f.read(1)) + if code in [1, 2]: + chunklen = stringToLong(f.read(4)) + chunk = f.read(chunklen) + assert len(chunk) == chunklen + data.append(chunk) + elif code == 3: + break + else: + raise T1Error('bad chunk code: ' + repr(code)) + if onlyHeader: + break data = bytesjoin(data) assertType1(data) return data def readOther(path): """reads any (font) file, returns raw data""" - f = open(path, "rb") - data = f.read() - f.close() + with open(path, "rb") as f: + data = f.read() assertType1(data) - chunks = findEncryptedChunks(data) data = [] for isEncrypted, chunk in chunks: @@ -244,8 +241,7 @@ def writeLWFN(path, data): def writePFB(path, data): chunks = findEncryptedChunks(data) - f = open(path, "wb") - try: + with open(path, "wb") as f: for isEncrypted, chunk in chunks: if isEncrypted: code = 2 @@ -255,13 +251,10 @@ def writePFB(path, data): f.write(longToString(len(chunk))) f.write(chunk) f.write(bytechr(128) + bytechr(3)) - finally: - f.close() def writeOther(path, data, dohex=False): chunks = findEncryptedChunks(data) - f = open(path, "wb") - try: + with open(path, "wb") as f: hexlinelen = HEXLINELENGTH // 2 for isEncrypted, chunk in chunks: if isEncrypted: @@ -275,8 +268,6 @@ def writeOther(path, data, dohex=False): chunk = chunk[hexlinelen:] else: f.write(chunk) - finally: - f.close() # decryption tools diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py index 6dc48baf..1c11de72 100644 --- a/Lib/fontTools/ttLib/sfnt.py +++ b/Lib/fontTools/ttLib/sfnt.py @@ -123,6 +123,30 @@ class SFNTReader(object): def close(self): self.file.close() + def __deepcopy__(self, memo): + """Overrides the default deepcopy of SFNTReader object, to make it work + in the case when TTFont is loaded with lazy=True, and thus reader holds a + reference to a file object which is not pickleable. + We work around it by manually copying the data into a in-memory stream. + """ + from copy import deepcopy + + cls = self.__class__ + obj = cls.__new__(cls) + for k, v in self.__dict__.items(): + if k == "file": + pos = v.tell() + v.seek(0) + buf = BytesIO(v.read()) + v.seek(pos) + buf.seek(pos) + if hasattr(v, "name"): + buf.name = v.name + obj.file = buf + else: + obj.__dict__[k] = deepcopy(v, memo) + return obj + # default compression level for WOFF 1.0 tables and metadata ZLIB_COMPRESSION_LEVEL = 6 diff --git a/Lib/fontTools/ttLib/tables/F__e_a_t.py b/Lib/fontTools/ttLib/tables/F__e_a_t.py index 22be4f67..ec497f22 100644 --- a/Lib/fontTools/ttLib/tables/F__e_a_t.py +++ b/Lib/fontTools/ttLib/tables/F__e_a_t.py @@ -58,8 +58,8 @@ class table_F__e_a_t(DefaultTable.DefaultTable): fobj.default = vid def compile(self, ttFont): - fdat = "" - vdat = "" + fdat = b"" + vdat = b"" offset = 0 for f, v in sorted(self.features.items(), key=lambda x:x[1].index): fnum = grUtils.tag2num(f) diff --git a/Lib/fontTools/ttLib/tables/G__l_a_t.py b/Lib/fontTools/ttLib/tables/G__l_a_t.py index 36ed6df3..7d1f7350 100644 --- a/Lib/fontTools/ttLib/tables/G__l_a_t.py +++ b/Lib/fontTools/ttLib/tables/G__l_a_t.py @@ -139,11 +139,11 @@ class table_G__l_a_t(DefaultTable.DefaultTable): return data def compileAttributes12(self, attrs, fmt): - data = [] + data = b"" for e in grUtils.entries(attrs): - data.extend(sstruct.pack(fmt, {'attNum' : e[0], 'num' : e[1]})) - data.extend(struct.pack(('>%dh' % len(e[2])), *e[2])) - return "".join(data) + data += sstruct.pack(fmt, {'attNum' : e[0], 'num' : e[1]}) + \ + struct.pack(('>%dh' % len(e[2])), *e[2]) + return data def compileAttributes3(self, attrs): if self.hasOctaboxes: @@ -168,7 +168,7 @@ class table_G__l_a_t(DefaultTable.DefaultTable): vals = {} for k in names: if k == 'subboxBitmap': continue - vals[k] = "{:.3f}%".format(getattr(o, k) * 100. / 256) + vals[k] = "{:.3f}%".format(getattr(o, k) * 100. / 255) vals['bitmap'] = "{:0X}".format(o.subboxBitmap) writer.begintag('octaboxes', **vals) writer.newline() @@ -176,7 +176,7 @@ class table_G__l_a_t(DefaultTable.DefaultTable): for s in o.subboxes: vals = {} for k in names: - vals[k] = "{:.3f}%".format(getattr(s, k) * 100. / 256) + vals[k] = "{:.3f}%".format(getattr(s, k) * 100. / 255) writer.simpletag('octabox', **vals) writer.newline() writer.endtag('octaboxes') @@ -190,6 +190,7 @@ class table_G__l_a_t(DefaultTable.DefaultTable): def fromXML(self, name, attrs, content, ttFont): if name == 'version' : self.version = float(safeEval(attrs['version'])) + self.scheme = int(safeEval(attrs['compressionScheme'])) if name != 'glyph' : return if not hasattr(self, 'attributes'): self.attributes = {} @@ -209,13 +210,13 @@ class table_G__l_a_t(DefaultTable.DefaultTable): o.subboxes = [] del attrs['bitmap'] for k, v in attrs.items(): - setattr(o, k, int(float(v[:-1]) * 256. / 100. + 0.5)) + setattr(o, k, int(float(v[:-1]) * 255. / 100. + 0.5)) for element in subcontent: if not isinstance(element, tuple): continue (tag, attrs, subcontent) = element so = _Object() for k, v in attrs.items(): - setattr(so, k, int(float(v[:-1]) * 256. / 100. + 0.5)) + setattr(so, k, int(float(v[:-1]) * 255. / 100. + 0.5)) o.subboxes.append(so) attributes.octabox = o self.attributes[gname] = attributes diff --git a/Lib/fontTools/ttLib/tables/S__i_l_f.py b/Lib/fontTools/ttLib/tables/S__i_l_f.py index 44dd69b0..e68b9b2e 100644 --- a/Lib/fontTools/ttLib/tables/S__i_l_f.py +++ b/Lib/fontTools/ttLib/tables/S__i_l_f.py @@ -6,6 +6,7 @@ from itertools import * from . import DefaultTable from . import grUtils from array import array +from functools import reduce import struct, operator, warnings, re, sys Silf_hdr_format = ''' @@ -220,23 +221,23 @@ def disassemble(aCode): instre = re.compile("^\s*([^(]+)\s*(?:\(([^)]+)\))?") def assemble(instrs): - res = [] + res = b"" for inst in instrs: m = instre.match(inst) if not m or not m.group(1) in aCode_map: continue opcode, parmfmt = aCode_map[m.group(1)] - res.append(struct.pack("B", opcode)) + res += struct.pack("B", opcode) if m.group(2): if parmfmt == 0: continue parms = [int(x) for x in re.split(",\s*", m.group(2))] if parmfmt == -1: l = len(parms) - res.append(struct.pack(("%dB" % (l+1)), l, *parms)) + res += struct.pack(("%dB" % (l+1)), l, *parms) else: - res.append(struct.pack(parmfmt, *parms)) - return b"".join(res) + res += struct.pack(parmfmt, *parms) + return res def writecode(tag, writer, instrs): writer.begintag(tag) @@ -334,7 +335,7 @@ class table_S__i_l_f(DefaultTable.DefaultTable): else: hdr = sstruct.pack(Silf_hdr_format_3, self) offset = len(hdr) + 4 * self.numSilf - data = "" + data = b"" for s in self.silfs: hdr += struct.pack(">L", offset) subdata = s.compile(ttFont, self.version) @@ -427,7 +428,7 @@ class Silf(object): self.numJLevels = len(self.jLevels) self.numCritFeatures = len(self.critFeatures) numPseudo = len(self.pMap) - data = "" + data = b"" if version >= 3.0: hdroffset = sstruct.calcsize(Silf_part1_format_v3) else: @@ -453,8 +454,8 @@ class Silf(object): u, ttFont.getGlyphID(p)) data1 += self.classes.compile(ttFont, version) currpos += len(data1) - data2 = "" - datao = "" + data2 = b"" + datao = b"" for i, p in enumerate(self.passes): base = currpos + len(data2) datao += struct.pack(">L", base) @@ -464,7 +465,7 @@ class Silf(object): if version >= 3.0: data3 = sstruct.pack(Silf_part1_format_v3, self) else: - data3 = "" + data3 = b"" return data3 + data + datao + data1 + data2 @@ -592,8 +593,8 @@ class Classes(object): oClasses = struct.unpack((">%dH" % (self.numClass+1)), data[4:6+2*self.numClass]) for s,e in zip(oClasses[:self.numLinear], oClasses[1:self.numLinear+1]): - self.linear.append(map(ttFont.getGlyphName, - struct.unpack((">%dH" % ((e-s)/2)), data[s:e]))) + self.linear.append(ttFont.getGlyphName(x) for x in + struct.unpack((">%dH" % ((e-s)/2)), data[s:e])) for s,e in zip(oClasses[self.numLinear:self.numClass], oClasses[self.numLinear+1:self.numClass+1]): nonLinids = [struct.unpack(">HH", data[x:x+4]) for x in range(s+8, e, 4)] @@ -601,7 +602,7 @@ class Classes(object): self.nonLinear.append(nonLin) def compile(self, ttFont, version=2.0): - data = "" + data = b"" oClasses = [] if version >= 4.0: offset = 8 + 4 * (len(self.linear) + len(self.nonLinear)) @@ -609,13 +610,13 @@ class Classes(object): offset = 6 + 2 * (len(self.linear) + len(self.nonLinear)) for l in self.linear: oClasses.append(len(data) + offset) - gs = map(ttFont.getGlyphID, l) + gs = [ttFont.getGlyphID(x) for x in l] data += struct.pack((">%dH" % len(l)), *gs) for l in self.nonLinear: oClasses.append(len(data) + offset) gs = [(ttFont.getGlyphID(x[0]), x[1]) for x in l.items()] data += grUtils.bininfo(len(gs)) - data += "".join([struct.pack(">HH", *x) for x in sorted(gs)]) + data += b"".join([struct.pack(">HH", *x) for x in sorted(gs)]) oClasses.append(len(data) + offset) self.numClass = len(oClasses) - 1 self.numLinear = len(self.linear) @@ -680,7 +681,7 @@ class Pass(object): self.rulePreContexts = [] self.ruleSortKeys = [] self.ruleConstraints = [] - self.passConstraints = "" + self.passConstraints = b"" self.actions = [] self.stateTrans = [] self.startStates = [] @@ -725,7 +726,7 @@ class Pass(object): for i in range(len(oConstraints)-2,-1,-1): if oConstraints[i] == 0 : oConstraints[i] = oConstraints[i+1] - self.ruleConstraints = [(data[s:e] if (e-s > 1) else "") for (s,e) in zip(oConstraints, oConstraints[1:])] + self.ruleConstraints = [(data[s:e] if (e-s > 1) else b"") for (s,e) in zip(oConstraints, oConstraints[1:])] data = data[oConstraints[-1]:] self.actions = [(data[s:e] if (e-s > 1) else "") for (s,e) in zip(oActions, oActions[1:])] data = data[oActions[-1]:] @@ -733,9 +734,9 @@ class Pass(object): def compile(self, ttFont, base, version=2.0): # build it all up backwards - oActions = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.actions + [""], (0, []))[1] - oConstraints = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.ruleConstraints + [""], (1, []))[1] - constraintCode = "\000" + "".join(self.ruleConstraints) + oActions = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.actions + [b""], (0, []))[1] + oConstraints = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.ruleConstraints + [b""], (1, []))[1] + constraintCode = b"\000" + b"".join(self.ruleConstraints) transes = [] for t in self.stateTrans: if sys.byteorder != "big": t.byteswap() @@ -761,7 +762,7 @@ class Pass(object): # now generate output data = sstruct.pack(Silf_pass_format, self) data += grUtils.bininfo(len(passRanges), 6) - data += "".join(struct.pack(">3H", *p) for p in passRanges) + data += b"".join(struct.pack(">3H", *p) for p in passRanges) data += struct.pack((">%dH" % len(oRuleMap)), *oRuleMap) flatrules = reduce(lambda a,x: a+x, self.rules, []) data += struct.pack((">%dH" % oRuleMap[-1]), *flatrules) @@ -772,8 +773,8 @@ class Pass(object): data += struct.pack(">BH", self.collisionThreshold, len(self.passConstraints)) data += struct.pack((">%dH" % (self.numRules+1)), *oConstraints) data += struct.pack((">%dH" % (self.numRules+1)), *oActions) - return data + "".join(transes) + struct.pack("B", 0) + \ - self.passConstraints + constraintCode + "".join(self.actions) + return data + b"".join(transes) + struct.pack("B", 0) + \ + self.passConstraints + constraintCode + b"".join(self.actions) def toXML(self, writer, ttFont, version=2.0): writesimple('info', self, writer, *pass_attrs_info) @@ -839,7 +840,7 @@ class Pass(object): if not isinstance(e, tuple): continue tag, a, c = e if tag == 'state': - self.rules.append(map(int, a['rules'].split(" "))) + self.rules.append([int(x) for x in a['rules'].split(" ")]) elif name == 'rules': for element in content: if not isinstance(element, tuple): continue @@ -847,8 +848,8 @@ class Pass(object): if tag != 'rule': continue self.rulePreContexts.append(int(a['precontext'])) self.ruleSortKeys.append(int(a['sortkey'])) - con = "" - act = "" + con = b"" + act = b"" for e in c: if not isinstance(e, tuple): continue tag, a, subc = e diff --git a/Lib/fontTools/ttLib/tables/S__i_l_l.py b/Lib/fontTools/ttLib/tables/S__i_l_l.py index 7acef1df..4671e137 100644 --- a/Lib/fontTools/ttLib/tables/S__i_l_l.py +++ b/Lib/fontTools/ttLib/tables/S__i_l_l.py @@ -42,8 +42,8 @@ class table_S__i_l_l(DefaultTable.DefaultTable): self.langs[c].append(finfo[i]) def compile(self, ttFont): - ldat = "" - fdat = "" + ldat = b"" + fdat = b"" offset = 0 for c, inf in sorted(self.langs.items()): ldat += struct.pack(">4sHH", c.encode('utf8'), len(inf), 8 * (offset + len(self.langs) + 1)) diff --git a/Lib/fontTools/ttLib/tables/_k_e_r_n.py b/Lib/fontTools/ttLib/tables/_k_e_r_n.py index 98eb7092..1e5140bc 100644 --- a/Lib/fontTools/ttLib/tables/_k_e_r_n.py +++ b/Lib/fontTools/ttLib/tables/_k_e_r_n.py @@ -48,6 +48,19 @@ class table__k_e_r_n(DefaultTable.DefaultTable): # This "version" is always 0 so we ignore it here _, length, subtableFormat, coverage = struct.unpack( ">HHBB", data[:6]) + if nTables == 1 and subtableFormat == 0: + # The "length" value is ignored since some fonts + # (like OpenSans and Calibri) have a subtable larger than + # its value. + nPairs, = struct.unpack(">H", data[6:8]) + calculated_length = (nPairs * 6) + 14 + if length != calculated_length: + log.warning( + "'kern' subtable longer than defined: " + "%d bytes instead of %d bytes" % + (calculated_length, length) + ) + length = calculated_length if subtableFormat not in kern_classes: subtable = KernTable_format_unkown(subtableFormat) else: @@ -128,7 +141,6 @@ class KernTable_format_0(object): ">HHHH", data[:8]) data = data[8:] - nPairs = min(nPairs, len(data) // 6) datas = array.array("H", data[:6 * nPairs]) if sys.byteorder != "big": datas.byteswap() it = iter(datas) @@ -153,6 +165,7 @@ class KernTable_format_0(object): def compile(self, ttFont): nPairs = len(self.kernTable) searchRange, entrySelector, rangeShift = getSearchRange(nPairs, 6) + searchRange &= 0xFFFF data = struct.pack( ">HHHH", nPairs, searchRange, entrySelector, rangeShift) @@ -175,6 +188,10 @@ class KernTable_format_0(object): if not self.apple: version = 0 length = len(data) + 6 + if length >= 0x10000: + log.warning('"kern" subtable overflow, ' + 'truncating length value while preserving pairs.') + length &= 0xFFFF header = struct.pack( ">HHBB", version, length, self.format, self.coverage) else: diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index a30291cc..488c4ea5 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -161,7 +161,8 @@ class table__n_a_m_e(DefaultTable.DefaultTable): raise ValueError("nameID must be less than 32768") return nameID - def addMultilingualName(self, names, ttFont=None, nameID=None): + def addMultilingualName(self, names, ttFont=None, nameID=None, + windows=True, mac=True): """Add a multilingual name, returning its name ID 'names' is a dictionary with the name in multiple languages, @@ -176,6 +177,9 @@ class table__n_a_m_e(DefaultTable.DefaultTable): 'nameID' is the name ID to be used, or None to let the library pick an unused name ID. + + If 'windows' is True, a platformID=3 name record will be added. + If 'mac' is True, a platformID=1 name record will be added. """ if not hasattr(self, 'names'): self.names = [] @@ -184,15 +188,16 @@ class table__n_a_m_e(DefaultTable.DefaultTable): # TODO: Should minimize BCP 47 language codes. # https://github.com/fonttools/fonttools/issues/930 for lang, name in sorted(names.items()): - # Apple platforms have been recognizing Windows names - # since early OSX (~2001), so we only add names - # for the Macintosh platform when we cannot not make - # a Windows name. This can happen for exotic BCP47 - # language tags that have no Windows language code. - windowsName = _makeWindowsName(name, nameID, lang) - if windowsName is not None: - self.names.append(windowsName) - else: + if windows: + windowsName = _makeWindowsName(name, nameID, lang) + if windowsName is not None: + self.names.append(windowsName) + else: + # We cannot not make a Windows name: make sure we add a + # Mac name as a fallback. This can happen for exotic + # BCP47 language tags that have no Windows language code. + mac = True + if mac: macName = _makeMacName(name, nameID, lang, ttFont) if macName is not None: self.names.append(macName) diff --git a/Lib/fontTools/ttLib/tables/grUtils.py b/Lib/fontTools/ttLib/tables/grUtils.py index 1ce2c9e9..d11ac4b1 100644 --- a/Lib/fontTools/ttLib/tables/grUtils.py +++ b/Lib/fontTools/ttLib/tables/grUtils.py @@ -13,7 +13,7 @@ def decompress(data): if scheme == 0: pass elif scheme == 1 and lz4: - res = lz4.decompress(struct.pack("<L", size) + data[8:]) + res = lz4.block.decompress(struct.pack("<L", size) + data[8:]) if len(res) != size: warnings.warn("Table decompression failed.") else: @@ -27,8 +27,8 @@ def compress(scheme, data): if scheme == 0 : return data elif scheme == 1 and lz4: - res = lz4.compress(hdr + data) - return res + res = lz4.block.compress(data, mode='high_compression', compression=16, store_size=False) + return hdr + res else: warnings.warn("Table failed to compress by unsupported compression scheme") return data @@ -47,7 +47,7 @@ def _entries(attrs, sameval): yield (ak - len(vals) + 1, len(vals), vals) def entries(attributes, sameval = False): - g = _entries(sorted(attributes.iteritems(), key=lambda x:int(x[0])), sameval) + g = _entries(sorted(attributes.items(), key=lambda x:int(x[0])), sameval) return g def bininfo(num, size=1): @@ -59,7 +59,7 @@ def bininfo(num, size=1): srange *= 2 select += 1 select -= 1 - srange /= 2 + srange //= 2 srange *= size shift = num * size - srange return struct.pack(">4H", num, srange, select, shift) diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index d08bcc57..d08bcc57 100755..100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py diff --git a/Lib/fontTools/ttLib/tables/ttProgram.py b/Lib/fontTools/ttLib/tables/ttProgram.py index 182982f2..7ffb37a0 100644 --- a/Lib/fontTools/ttLib/tables/ttProgram.py +++ b/Lib/fontTools/ttLib/tables/ttProgram.py @@ -63,6 +63,7 @@ instructions = [ (0x66, 'FLOOR', 0, 'Floor', 1, 1), # n floor(n) (0x46, 'GC', 1, 'GetCoordOnPVector', 1, 1), # p c (0x88, 'GETINFO', 0, 'GetInfo', 1, 1), # selector result + (0x91, 'GETVARIATION', 0, 'GetVariation', 0, -1), # - a1,..,an (0x0d, 'GFV', 0, 'GetFVector', 0, 2), # - px, py (0x0c, 'GPV', 0, 'GetPVector', 0, 2), # - px, py (0x52, 'GT', 0, 'GreaterThan', 2, 1), # e2, e1 b @@ -158,7 +159,7 @@ def bitRepr(value, bits): return s -_mnemonicPat = re.compile("[A-Z][A-Z0-9]*$") +_mnemonicPat = re.compile(r"[A-Z][A-Z0-9]*$") def _makeDict(instructionList): opcodeDict = {} @@ -194,8 +195,8 @@ _whiteRE = re.compile(r"\s*") _pushCountPat = re.compile(r"[A-Z][A-Z0-9]*\s*\[.*?\]\s*/\* ([0-9]+).*?\*/") -_indentRE = re.compile("^FDEF|IF|ELSE\[ \]\t.+") -_unindentRE = re.compile("^ELSE|ENDF|EIF\[ \]\t.+") +_indentRE = re.compile(r"^FDEF|IF|ELSE\[ \]\t.+") +_unindentRE = re.compile(r"^ELSE|ENDF|EIF\[ \]\t.+") def _skipWhite(data, pos): m = _whiteRE.match(data, pos) diff --git a/Lib/fontTools/ttx.py b/Lib/fontTools/ttx.py index 0e3eaf39..a785325e 100644 --- a/Lib/fontTools/ttx.py +++ b/Lib/fontTools/ttx.py @@ -293,11 +293,11 @@ def ttCompile(input, output, options): def guessFileType(fileName): base, ext = os.path.splitext(fileName) try: - f = open(fileName, "rb") + with open(fileName, "rb") as f: + header = f.read(256) except IOError: return None - header = f.read(256) - f.close() + if header.startswith(b'\xef\xbb\xbf<?xml'): header = header.lstrip(b'\xef\xbb\xbf') cr, tp = getMacCreatorAndType(fileName) diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py index b1ece9cf..d9a57c5d 100755..100644 --- a/Lib/fontTools/ufoLib/__init__.py +++ b/Lib/fontTools/ufoLib/__init__.py @@ -5,6 +5,7 @@ from copy import deepcopy import logging import zipfile import enum +from collections import OrderedDict import fs import fs.base import fs.subfs @@ -57,6 +58,7 @@ __all__ = [ "UFOLibError", "UFOReader", "UFOWriter", + "UFOReaderWriter", "UFOFileStructure", "fontInfoAttributesVersion1", "fontInfoAttributesVersion2", @@ -105,104 +107,92 @@ class UFOFileStructure(enum.Enum): # -------------- -def _getFileModificationTime(self, path): - """ - Returns the modification time for the file at the given path, as a - floating point number giving the number of seconds since the epoch. - The path must be relative to the UFO path. - Returns None if the file does not exist. - """ - try: - dt = self.fs.getinfo(fsdecode(path), namespaces=["details"]).modified - except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound): - return None - else: - return datetimeAsTimestamp(dt) - - -def _readBytesFromPath(self, path): - """ - Returns the bytes in the file at the given path. - The path must be relative to the UFO's filesystem root. - Returns None if the file does not exist. - """ - try: - return self.fs.getbytes(fsdecode(path)) - except fs.errors.ResourceNotFound: - return None +class _UFOBaseIO(object): - -def _getPlist(self, fileName, default=None): - """ - Read a property list relative to the UFO filesystem's root. - Raises UFOLibError if the file is missing and default is None, - otherwise default is returned. - - The errors that could be raised during the reading of a plist are - unpredictable and/or too large to list, so, a blind try: except: - is done. If an exception occurs, a UFOLibError will be raised. - """ - try: - with self.fs.open(fileName, "rb") as f: - return plistlib.load(f) - except fs.errors.ResourceNotFound: - if default is None: - raise UFOLibError( - "'%s' is missing on %s. This file is required" - % (fileName, self.fs) - ) + def getFileModificationTime(self, path): + """ + Returns the modification time for the file at the given path, as a + floating point number giving the number of seconds since the epoch. + The path must be relative to the UFO path. + Returns None if the file does not exist. + """ + try: + dt = self.fs.getinfo(fsdecode(path), namespaces=["details"]).modified + except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound): + return None else: - return default - except Exception as e: - # TODO(anthrotype): try to narrow this down a little - raise UFOLibError( - "'%s' could not be read on %s: %s" % (fileName, self.fs, e) - ) - - -def _writePlist(self, fileName, obj): - """ - Write a property list to a file relative to the UFO filesystem's root. + return datetimeAsTimestamp(dt) - Do this sort of atomically, making it harder to corrupt existing files, - for example when plistlib encounters an error halfway during write. - This also checks to see if text matches the text that is already in the - file at path. If so, the file is not rewritten so that the modification - date is preserved. + def _getPlist(self, fileName, default=None): + """ + Read a property list relative to the UFO filesystem's root. + Raises UFOLibError if the file is missing and default is None, + otherwise default is returned. - The errors that could be raised during the writing of a plist are - unpredictable and/or too large to list, so, a blind try: except: is done. - If an exception occurs, a UFOLibError will be raised. - """ - if self._havePreviousFile: + The errors that could be raised during the reading of a plist are + unpredictable and/or too large to list, so, a blind try: except: + is done. If an exception occurs, a UFOLibError will be raised. + """ try: - data = plistlib.dumps(obj) + with self.fs.open(fileName, "rb") as f: + return plistlib.load(f) + except fs.errors.ResourceNotFound: + if default is None: + raise UFOLibError( + "'%s' is missing on %s. This file is required" + % (fileName, self.fs) + ) + else: + return default except Exception as e: + # TODO(anthrotype): try to narrow this down a little raise UFOLibError( - "'%s' could not be written on %s because " - "the data is not properly formatted: %s" - % (fileName, self.fs, e) + "'%s' could not be read on %s: %s" % (fileName, self.fs, e) ) - if self.fs.exists(fileName) and data == self.fs.getbytes(fileName): - return - self.fs.setbytes(fileName, data) - else: - with self.fs.openbin(fileName, mode="w") as fp: + + def _writePlist(self, fileName, obj): + """ + Write a property list to a file relative to the UFO filesystem's root. + + Do this sort of atomically, making it harder to corrupt existing files, + for example when plistlib encounters an error halfway during write. + This also checks to see if text matches the text that is already in the + file at path. If so, the file is not rewritten so that the modification + date is preserved. + + The errors that could be raised during the writing of a plist are + unpredictable and/or too large to list, so, a blind try: except: is done. + If an exception occurs, a UFOLibError will be raised. + """ + if self._havePreviousFile: try: - plistlib.dump(obj, fp) + data = plistlib.dumps(obj) except Exception as e: raise UFOLibError( "'%s' could not be written on %s because " "the data is not properly formatted: %s" % (fileName, self.fs, e) ) + if self.fs.exists(fileName) and data == self.fs.getbytes(fileName): + return + self.fs.setbytes(fileName, data) + else: + with self.fs.openbin(fileName, mode="w") as fp: + try: + plistlib.dump(obj, fp) + except Exception as e: + raise UFOLibError( + "'%s' could not be written on %s because " + "the data is not properly formatted: %s" + % (fileName, self.fs, e) + ) # ---------- # UFO Reader # ---------- -class UFOReader(object): +class UFOReader(_UFOBaseIO): """ Read the various components of the .ufo. @@ -280,6 +270,18 @@ class UFOReader(object): # properties + def _get_path(self): + import warnings + + warnings.warn( + "The 'path' attribute is deprecated; use the 'fs' attribute instead", + DeprecationWarning, + stacklevel=2, + ) + return self._path + + path = property(_get_path, doc="The path of the UFO (DEPRECATED).") + def _get_formatVersion(self): return self._formatVersion @@ -291,7 +293,7 @@ class UFOReader(object): fileStructure = property( _get_fileStructure, doc=( - "The current file structure of the UFO: " + "The file structure of the UFO: " "either UFOFileStructure.ZIP or UFOFileStructure.PACKAGE" ) ) @@ -347,9 +349,16 @@ class UFOReader(object): # support methods - _getPlist = _getPlist - getFileModificationTime = _getFileModificationTime - readBytesFromPath = _readBytesFromPath + def readBytesFromPath(self, path): + """ + Returns the bytes in the file at the given path. + The path must be relative to the UFO's filesystem root. + Returns None if the file does not exist. + """ + try: + return self.fs.getbytes(fsdecode(path)) + except fs.errors.ResourceNotFound: + return None def getReadFileForPath(self, path, encoding=None): """ @@ -796,7 +805,7 @@ class UFOReader(object): # UFO Writer # ---------- -class UFOWriter(object): +class UFOWriter(UFOReader): """ Write the various components of the .ufo. @@ -821,6 +830,8 @@ class UFOWriter(object): path = path.__fspath__() if isinstance(path, basestring): + # normalize path by removing trailing or double slashes + path = os.path.normpath(path) havePreviousFile = os.path.exists(path) if havePreviousFile: # ensure we use the same structure as the destination @@ -946,7 +957,7 @@ class UFOWriter(object): self.layerContents = {} if previousFormatVersion is not None and previousFormatVersion >= 3: # already exists - self._readLayerContents(validate=validate) + self.layerContents = OrderedDict(self._readLayerContents(validate)) else: # previous < 3 # imply the layer contents @@ -957,46 +968,13 @@ class UFOWriter(object): # properties - def _get_path(self): - import warnings - - warnings.warn( - "The 'path' attribute is deprecated; use the 'fs' attribute instead", - DeprecationWarning, - stacklevel=2, - ) - return self._path - - path = property(_get_path, doc="The path the UFO is being written to (DEPRECATED).") - - def _get_formatVersion(self): - return self._formatVersion - - formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is set into metainfo.plist during __init__.") - def _get_fileCreator(self): return self._fileCreator fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.") - def _get_fileStructure(self): - return self._fileStructure - - fileStructure = property( - _get_fileStructure, - doc=( - "The file structure of the destination UFO: " - "either UFOFileStrucure.ZIP or UFOFileStructure.PACKAGE" - ) - ) - # support methods for file system interaction - _getPlist = _getPlist - _writePlist = _writePlist - readBytesFromPath = _readBytesFromPath - getFileModificationTime = _getFileModificationTime - def copyFromReader(self, reader, sourcePath, destPath): """ Copy the sourcePath in the provided UFOReader to destPath @@ -1332,25 +1310,6 @@ class UFOWriter(object): # glyph sets & layers - def _readLayerContents(self, validate): - """ - Rebuild the layer contents list by checking what glyph sets - are available on disk. - - ``validate`` will validate the data. - """ - # read the file on disk - raw = self._getPlist(LAYERCONTENTS_FILENAME) - contents = {} - if validate: - valid, error = layerContentsValidator(raw, self.fs) - if not valid: - raise UFOLibError(error) - for entry in raw: - layerName, directoryName = entry - contents[layerName] = directoryName - self.layerContents = contents - def writeLayerContents(self, layerOrder=None, validate=None): """ Write the layercontents.plist file. This method *must* be called @@ -1412,7 +1371,7 @@ class UFOWriter(object): raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion) # locate a layer name when None has been given if layerName is None and defaultLayer: - for existingLayerName, directory in list(self.layerContents.items()): + for existingLayerName, directory in self.layerContents.items(): if directory == DEFAULT_GLYPHS_DIRNAME: layerName = existingLayerName if layerName is None: @@ -1460,10 +1419,13 @@ class UFOWriter(object): # matches the default being written. also make sure that this layer # name is not already linked to a non-default layer. if defaultLayer: - for existingLayerName, directory in list(self.layerContents.items()): + for existingLayerName, directory in self.layerContents.items(): if directory == DEFAULT_GLYPHS_DIRNAME: if existingLayerName != layerName: - raise UFOLibError("Another layer is already mapped to the default directory.") + raise UFOLibError( + "Another layer ('%s') is already mapped to the default directory." + % existingLayerName + ) elif existingLayerName == layerName: raise UFOLibError("The layer name is already mapped to a non-default layer.") # get an existing directory name @@ -1476,7 +1438,7 @@ class UFOWriter(object): else: # not caching this could be slightly expensive, # but caching it will be cumbersome - existing = [d.lower() for d in list(self.layerContents.values())] + existing = {d.lower() for d in self.layerContents.values()} if not isinstance(layerName, unicode): try: layerName = unicode(layerName) @@ -1524,14 +1486,14 @@ class UFOWriter(object): if newLayerName in self.layerContents: raise UFOLibError("A layer named %s already exists." % newLayerName) # make sure the default layer doesn't already exist - if defaultLayer and DEFAULT_GLYPHS_DIRNAME in list(self.layerContents.values()): + if defaultLayer and DEFAULT_GLYPHS_DIRNAME in self.layerContents.values(): raise UFOLibError("A default layer already exists.") # get the paths oldDirectory = self._findDirectoryForLayerName(layerName) if defaultLayer: newDirectory = DEFAULT_GLYPHS_DIRNAME else: - existing = [name.lower() for name in list(self.layerContents.values())] + existing = {name.lower() for name in self.layerContents.values()} newDirectory = userNameToFileName(newLayerName, existing=existing, prefix="glyphs.") # update the internal mapping del self.layerContents[layerName] @@ -1612,14 +1574,11 @@ class UFOWriter(object): rootDir = os.path.splitext(os.path.basename(self._path))[0] + ".ufo" with fs.zipfs.ZipFS(self._path, write=True, encoding="utf-8") as destFS: fs.copy.copy_fs(self.fs, destFS.makedir(rootDir)) - if self._shouldClose: - self.fs.close() + super(UFOWriter, self).close() - def __enter__(self): - return self - def __exit__(self, exc_type, exc_value, exc_tb): - self.close() +# just an alias, makes it more explicit +UFOReaderWriter = UFOWriter # ---------------- diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py index eae300b2..f2648b8c 100755..100644 --- a/Lib/fontTools/ufoLib/glifLib.py +++ b/Lib/fontTools/ufoLib/glifLib.py @@ -34,6 +34,7 @@ from fontTools.ufoLib.validators import ( glyphLibValidator, ) from fontTools.misc import etree +from fontTools.ufoLib import _UFOBaseIO from fontTools.ufoLib.utils import integerTypes, numberTypes @@ -88,7 +89,7 @@ class Glyph(object): # Glyph Set # --------- -class GlyphSet(object): +class GlyphSet(_UFOBaseIO): """ GlyphSet manages a set of .glif files inside one directory. @@ -169,9 +170,6 @@ class GlyphSet(object): self.rebuildContents() - # here we reuse the same methods from UFOReader/UFOWriter - from fontTools.ufoLib import _getPlist, _writePlist, _getFileModificationTime - def rebuildContents(self, validateRead=None): """ Rebuild the contents dict by loading contents.plist. @@ -308,7 +306,7 @@ class GlyphSet(object): Raises KeyError if the glyphName is not in contents.plist. """ fileName = self.contents[glyphName] - return self._getFileModificationTime(fileName) + return self.getFileModificationTime(fileName) # reading/writing API diff --git a/Lib/fontTools/unicode.py b/Lib/fontTools/unicode.py index 50dfc539..ef3ed695 100644 --- a/Lib/fontTools/unicode.py +++ b/Lib/fontTools/unicode.py @@ -18,8 +18,11 @@ class _UnicodeCustom(object): def __init__(self, f): if isinstance(f, basestring): - f = open(f) - self.codes = _makeunicodes(f) + with open(f) as fd: + codes = _makeunicodes(fd) + else: + codes = _makeunicodes(f) + self.codes = codes def __getitem__(self, charCode): try: diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index 437324e0..37d8d334 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -23,7 +23,7 @@ from __future__ import unicode_literals from fontTools.misc.py23 import * from fontTools.misc.fixedTools import otRound from fontTools.misc.arrayTools import Vector -from fontTools.ttLib import TTFont, newTable +from fontTools.ttLib import TTFont, newTable, TTLibError from fontTools.ttLib.tables._n_a_m_e import NameRecord from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates @@ -32,7 +32,7 @@ from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables.otBase import OTTableWriter from fontTools.varLib import builder, models, varStore -from fontTools.varLib.merger import VariationMerger, _all_equal +from fontTools.varLib.merger import VariationMerger from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib.iup import iup_delta_optimize from fontTools.varLib.featureVars import addFeatureVariations @@ -40,6 +40,7 @@ from fontTools.designspaceLib import DesignSpaceDocument, AxisDescriptor from collections import OrderedDict, namedtuple import os.path import logging +from copy import deepcopy from pprint import pformat log = logging.getLogger("fontTools.varLib") @@ -184,7 +185,7 @@ def _add_stat(font, axes): STAT = font["STAT"] = newTable('STAT') stat = STAT.table = ot.STAT() - stat.Version = 0x00010002 + stat.Version = 0x00010001 axisRecords = [] for i, a in enumerate(fvarTable.axes): @@ -280,7 +281,7 @@ def _SetCoordinates(font, glyphName, coord): # XXX Handle vertical font["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing -def _add_gvar(font, model, master_ttfs, tolerance=0.5, optimize=True): +def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True): assert tolerance >= 0 @@ -291,13 +292,19 @@ def _add_gvar(font, model, master_ttfs, tolerance=0.5, optimize=True): gvar.reserved = 0 gvar.variations = {} + glyf = font['glyf'] + for glyph in font.getGlyphOrder(): + isComposite = glyf[glyph].isComposite() + allData = [_GetCoordinates(m, glyph) for m in master_ttfs] + model, allData = masterModel.getSubModel(allData) + allCoords = [d[0] for d in allData] allControls = [d[1] for d in allData] control = allControls[0] - if (any(c != control for c in allControls)): + if not models.allEqual(allControls): log.warning("glyph %s has incompatible masters; skipping" % glyph) continue del allControls @@ -313,13 +320,23 @@ def _add_gvar(font, model, master_ttfs, tolerance=0.5, optimize=True): endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): - if all(abs(v) <= tolerance for v in delta.array): + if all(abs(v) <= tolerance for v in delta.array) and not isComposite: continue var = TupleVariation(support, delta) if optimize: delta_opt = iup_delta_optimize(delta, origCoords, endPts, tolerance=tolerance) if None in delta_opt: + """In composite glyphs, there should be one 0 entry + to make sure the gvar entry is written to the font. + + This is to work around an issue with macOS 10.14 and can be + removed once the behaviour of macOS is changed. + + https://github.com/fonttools/fonttools/issues/1381 + """ + if all(d is None for d in delta_opt): + delta_opt = [(0, 0)] + [None] * (len(delta_opt) - 1) # Use "optimized" version only if smaller... var_opt = TupleVariation(support, delta_opt) @@ -344,7 +361,7 @@ def _remove_TTHinting(font): font["glyf"].removeHinting() # TODO: Modify gasp table to deactivate gridfitting for all ranges? -def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5): +def _merge_TTHinting(font, masterModel, master_ttfs, tolerance=0.5): log.info("Merging TT hinting") assert "cvar" not in font @@ -372,7 +389,7 @@ def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5): all_pgms = [ m["glyf"][name].program for m in master_ttfs - if hasattr(m["glyf"][name], "program") + if name in m['glyf'] and hasattr(m["glyf"][name], "program") ] if not any(all_pgms): continue @@ -383,24 +400,21 @@ def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5): font_pgm = Program() if any(pgm != font_pgm for pgm in all_pgms if pgm): log.warning("Masters have incompatible glyph programs in glyph '%s', hinting is discarded." % name) + # TODO Only drop hinting from this glyph. _remove_TTHinting(font) return # cvt table - all_cvs = [Vector(m["cvt "].values) for m in master_ttfs if "cvt " in m] - - if len(all_cvs) == 0: - # There is no cvt table to make a cvar table from, we're done here. - return + all_cvs = [Vector(m["cvt "].values) if 'cvt ' in m else None + for m in master_ttfs] - if len(all_cvs) != len(master_ttfs): - log.warning("Some masters have no cvt table, hinting is discarded.") - _remove_TTHinting(font) + nonNone_cvs = models.nonNone(all_cvs) + if not nonNone_cvs: + # There is no cvt table to make a cvar table from, we're done here. return - num_cvt0 = len(all_cvs[0]) - if (any(len(c) != num_cvt0 for c in all_cvs)): + if not models.allEqual(len(c) for c in nonNone_cvs): log.warning("Masters have incompatible cvt tables, hinting is discarded.") _remove_TTHinting(font) return @@ -411,8 +425,7 @@ def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5): cvar.version = 1 cvar.variations = [] - deltas = model.getDeltas(all_cvs) - supports = model.supports + deltas, supports = masterModel.getDeltasAndSupports(all_cvs) for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): delta = [otRound(d) for d in delta] if all(abs(v) <= tolerance for v in delta): @@ -420,54 +433,59 @@ def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5): var = TupleVariation(support, delta) cvar.variations.append(var) -def _add_HVAR(font, model, master_ttfs, axisTags): +def _add_HVAR(font, masterModel, master_ttfs, axisTags): log.info("Generating HVAR") - hAdvanceDeltas = {} + glyphOrder = font.getGlyphOrder() + + hAdvanceDeltasAndSupports = {} metricses = [m["hmtx"].metrics for m in master_ttfs] - for glyph in font.getGlyphOrder(): - hAdvances = [metrics[glyph][0] for metrics in metricses] - # TODO move round somewhere else? - hAdvanceDeltas[glyph] = tuple(otRound(d) for d in model.getDeltas(hAdvances)[1:]) - - # Direct mapping - supports = model.supports[1:] - varTupleList = builder.buildVarRegionList(supports, axisTags) - varTupleIndexes = list(range(len(supports))) - n = len(supports) - items = [] - for glyphName in font.getGlyphOrder(): - items.append(hAdvanceDeltas[glyphName]) - - # Build indirect mapping to save on duplicates, compare both sizes - uniq = list(set(items)) - mapper = {v:i for i,v in enumerate(uniq)} - mapping = [mapper[item] for item in items] - advanceMapping = builder.buildVarIdxMap(mapping, font.getGlyphOrder()) - - # Direct - varData = builder.buildVarData(varTupleIndexes, items) - directStore = builder.buildVarStore(varTupleList, [varData]) - - # Indirect - varData = builder.buildVarData(varTupleIndexes, uniq) - indirectStore = builder.buildVarStore(varTupleList, [varData]) - mapping = indirectStore.optimize() - advanceMapping.mapping = {k:mapping[v] for k,v in advanceMapping.mapping.items()} - - # Compile both, see which is more compact - - writer = OTTableWriter() - directStore.compile(writer, font) - directSize = len(writer.getAllData()) - - writer = OTTableWriter() - indirectStore.compile(writer, font) - advanceMapping.compile(writer, font) - indirectSize = len(writer.getAllData()) - - use_direct = directSize < indirectSize + for glyph in glyphOrder: + hAdvances = [metrics[glyph][0] if glyph in metrics else None for metrics in metricses] + hAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(hAdvances) + + singleModel = models.allEqual(id(v[1]) for v in hAdvanceDeltasAndSupports.values()) + + directStore = None + if singleModel: + # Build direct mapping + + supports = next(iter(hAdvanceDeltasAndSupports.values()))[1][1:] + varTupleList = builder.buildVarRegionList(supports, axisTags) + varTupleIndexes = list(range(len(supports))) + varData = builder.buildVarData(varTupleIndexes, [], optimize=False) + for glyphName in glyphOrder: + varData.addItem(hAdvanceDeltasAndSupports[glyphName][0]) + varData.optimize() + directStore = builder.buildVarStore(varTupleList, [varData]) + + # Build optimized indirect mapping + storeBuilder = varStore.OnlineVarStoreBuilder(axisTags) + mapping = {} + for glyphName in glyphOrder: + deltas,supports = hAdvanceDeltasAndSupports[glyphName] + storeBuilder.setSupports(supports) + mapping[glyphName] = storeBuilder.storeDeltas(deltas) + indirectStore = storeBuilder.finish() + mapping2 = indirectStore.optimize() + mapping = [mapping2[mapping[g]] for g in glyphOrder] + advanceMapping = builder.buildVarIdxMap(mapping, glyphOrder) + + use_direct = False + if directStore: + # Compile both, see which is more compact + + writer = OTTableWriter() + directStore.compile(writer, font) + directSize = len(writer.getAllData()) + + writer = OTTableWriter() + indirectStore.compile(writer, font) + advanceMapping.compile(writer, font) + indirectSize = len(writer.getAllData()) + + use_direct = directSize < indirectSize # Done; put it all together. assert "HVAR" not in font @@ -482,12 +500,11 @@ def _add_HVAR(font, model, master_ttfs, axisTags): hvar.VarStore = indirectStore hvar.AdvWidthMap = advanceMapping -def _add_MVAR(font, model, master_ttfs, axisTags): +def _add_MVAR(font, masterModel, master_ttfs, axisTags): log.info("Generating MVAR") store_builder = varStore.OnlineVarStoreBuilder(axisTags) - store_builder.setModel(model) records = [] lastTableTag = None @@ -497,17 +514,20 @@ def _add_MVAR(font, model, master_ttfs, axisTags): if tableTag != lastTableTag: tables = fontTable = None if tableTag in font: - # TODO Check all masters have same table set? fontTable = font[tableTag] - tables = [master[tableTag] for master in master_ttfs] + tables = [master[tableTag] if tableTag in master else None + for master in master_ttfs] lastTableTag = tableTag if tables is None: continue # TODO support gasp entries + model, tables = masterModel.getSubModel(tables) + store_builder.setModel(model) + master_values = [getattr(table, itemName) for table in tables] - if _all_equal(master_values): + if models.allEqual(master_values): base, varIdx = master_values[0], None else: base, varIdx = store_builder.storeMasters(master_values) @@ -545,9 +565,7 @@ def _merge_OTL(font, model, master_fonts, axisTags): log.info("Merging OpenType Layout tables") merger = VariationMerger(model, axisTags, font) - merger.mergeTables(font, master_fonts, ['GPOS']) - # TODO Merge GSUB - # TODO Merge GDEF itself! + merger.mergeTables(font, master_fonts, ['GSUB', 'GDEF', 'GPOS']) store = merger.store_builder.finish() if not store.VarData: return @@ -587,8 +605,14 @@ def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules): space = {} for condition in conditions: axis_name = condition["name"] - minimum = normalize(axis_name, condition["minimum"]) - maximum = normalize(axis_name, condition["maximum"]) + if condition["minimum"] is not None: + minimum = normalize(axis_name, condition["minimum"]) + else: + minimum = -1.0 + if condition["maximum"] is not None: + maximum = normalize(axis_name, condition["maximum"]) + else: + maximum = 1.0 tag = axis_tags[axis_name] space[tag] = (minimum, maximum) region.append(space) @@ -614,9 +638,24 @@ _DesignSpaceData = namedtuple( ) -def load_designspace(designspace_filename): +def _add_CFF2(varFont, model, master_fonts): + from .cff import (convertCFFtoCFF2, addCFFVarStore, merge_region_fonts) + glyphOrder = varFont.getGlyphOrder() + convertCFFtoCFF2(varFont) + ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping) + # re-ordering the master list simplifies building the CFF2 data item lists. + addCFFVarStore(varFont, model) # Add VarStore to the CFF2 font. + merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder) + + +def load_designspace(designspace): + # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument, + # never a file path, as that's already handled by caller + if hasattr(designspace, "sources"): # Assume a DesignspaceDocument + ds = designspace + else: # Assume a file path + ds = DesignSpaceDocument.fromfile(designspace) - ds = DesignSpaceDocument.fromfile(designspace_filename) masters = ds.sources if not masters: raise VarLibError("no sources found in .designspace") @@ -698,7 +737,7 @@ def load_designspace(designspace_filename): ) -def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=True): +def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True): """ Build variation font from a designspace file. @@ -706,16 +745,27 @@ def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=T filename as found in designspace file and map it to master font binary as to be opened (eg. .ttf or .otf). """ + if hasattr(designspace, "sources"): # Assume a DesignspaceDocument + pass + else: # Assume a file path + designspace = DesignSpaceDocument.fromfile(designspace) - ds = load_designspace(designspace_filename) - + ds = load_designspace(designspace) log.info("Building variable font") + log.info("Loading master fonts") - basedir = os.path.dirname(designspace_filename) - master_ttfs = [master_finder(os.path.join(basedir, m.filename)) for m in ds.masters] - master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] - # Reload base font as target font - vf = TTFont(master_ttfs[ds.base_idx]) + master_fonts = load_masters(designspace, master_finder) + + # TODO: 'master_ttfs' is unused except for return value, remove later + master_ttfs = [] + for master in master_fonts: + try: + master_ttfs.append(master.reader.file.name) + except AttributeError: + master_ttfs.append(None) # in-memory fonts have no path + + # Copy the base master to work from it + vf = deepcopy(master_fonts[ds.base_idx]) # TODO append masters as named-instances as well; needs .designspace change. fvar = _add_fvar(vf, ds.axes, ds.instances) @@ -748,14 +798,65 @@ def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=T _merge_TTHinting(vf, model, master_fonts) if 'GSUB' not in exclude and ds.rules: _add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules) + if 'CFF2' not in exclude and 'CFF ' in vf: + _add_CFF2(vf, model, master_fonts) for tag in exclude: if tag in vf: del vf[tag] + # TODO: Only return vf for 4.0+, the rest is unused. return vf, model, master_ttfs +def load_masters(designspace, master_finder=lambda s: s): + """Ensure that all SourceDescriptor.font attributes have an appropriate TTFont + object loaded, or else open TTFont objects from the SourceDescriptor.path + attributes. + + The paths can point to either an OpenType font or to a UFO. In the latter case, + use the provided master_finder callable to map from UFO paths to the respective + master font binaries (e.g. .ttf or .otf). + + Return list of master TTFont objects in the same order they are listed in the + DesignSpaceDocument. + """ + master_fonts = [] + + for master in designspace.sources: + # 1. If the caller already supplies a TTFont for a source, just take it. + if master.font: + font = master.font + master_fonts.append(font) + else: + # If a SourceDescriptor has a layer name, demand that the compiled TTFont + # be supplied by the caller. This spares us from modifying MasterFinder. + if master.layerName: + raise AttributeError( + "Designspace source '%s' specified a layer name but lacks the " + "required TTFont object in the 'font' attribute." + % (master.name or "<Unknown>") + ) + else: + if master.path is None: + raise AttributeError( + "Designspace source '%s' has neither 'font' nor 'path' " + "attributes" % (master.name or "<Unknown>") + ) + # 2. A SourceDescriptor's path might point to a UFO or an OpenType + # binary. Find out the hard way. + master_path = os.path.normpath(master.path) + try: + font = TTFont(master_path) + except (IOError, TTLibError): + # 3. Not an OpenType binary, fall back to the master finder. + master_path = master_finder(master_path) + font = TTFont(master_path) + master_fonts.append(font) + + return master_fonts + + class MasterFinder(object): def __init__(self, template): @@ -828,7 +929,7 @@ def main(args=None): if outfile is None: outfile = os.path.splitext(designspace_filename)[0] + '-VF.ttf' - vf, model, master_ttfs = build( + vf, _, _ = build( designspace_filename, finder, exclude=options.exclude, diff --git a/Lib/fontTools/varLib/builder.py b/Lib/fontTools/varLib/builder.py index 90e33375..e923b800 100644 --- a/Lib/fontTools/varLib/builder.py +++ b/Lib/fontTools/varLib/builder.py @@ -39,7 +39,7 @@ def _reorderItem(lst, narrows, zeroes): out.append(lst[i]) return out -def VarData_CalculateNumShorts(self, optimize=True): +def VarData_calculateNumShorts(self, optimize=False): count = self.VarRegionCount items = self.Item narrows = set(range(count)) @@ -55,14 +55,29 @@ def VarData_CalculateNumShorts(self, optimize=True): # Reorder columns such that all SHORT columns come before UINT8 self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows, zeroes) self.VarRegionCount = len(self.VarRegionIndex) - for i in range(self.ItemCount): + for i in range(len(items)): items[i] = _reorderItem(items[i], narrows, zeroes) self.NumShorts = count - len(narrows) else: wides = set(range(count)) - narrows self.NumShorts = 1+max(wides) if wides else 0 + self.VarRegionCount = len(self.VarRegionIndex) return self +ot.VarData.calculateNumShorts = VarData_calculateNumShorts + +def VarData_CalculateNumShorts(self, optimize=True): + """Deprecated name for VarData_calculateNumShorts() which + defaults to optimize=True. Use varData.calculateNumShorts() + or varData.optimize().""" + return VarData_calculateNumShorts(self, optimize=optimize) + +def VarData_optimize(self): + return VarData_calculateNumShorts(self, optimize=True) + +ot.VarData.optimize = VarData_optimize + + def buildVarData(varRegionIndices, items, optimize=True): self = ot.VarData() self.VarRegionIndex = list(varRegionIndices) @@ -73,7 +88,7 @@ def buildVarData(varRegionIndices, items, optimize=True): assert len(item) == regionCount records.append(list(item)) self.ItemCount = len(self.Item) - VarData_CalculateNumShorts(self, optimize=optimize) + self.calculateNumShorts(optimize=optimize) return self diff --git a/Lib/fontTools/varLib/cff.py b/Lib/fontTools/varLib/cff.py new file mode 100644 index 00000000..a000dd48 --- /dev/null +++ b/Lib/fontTools/varLib/cff.py @@ -0,0 +1,502 @@ +import os +from fontTools.misc.py23 import BytesIO +from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor +from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round +from fontTools.cffLib import ( + maxStackLimit, + TopDictIndex, + buildOrder, + topDictOperators, + topDictOperators2, + privateDictOperators, + privateDictOperators2, + FDArrayIndex, + FontDict, + VarStoreData +) +from fontTools.cffLib.specializer import (commandsToProgram, specializeCommands) +from fontTools.ttLib import newTable +from fontTools import varLib +from fontTools.varLib.models import allEqual + + +def addCFFVarStore(varFont, varModel): + supports = varModel.supports[1:] + fvarTable = varFont['fvar'] + axisKeys = [axis.axisTag for axis in fvarTable.axes] + varTupleList = varLib.builder.buildVarRegionList(supports, axisKeys) + varTupleIndexes = list(range(len(supports))) + varDeltasCFFV = varLib.builder.buildVarData(varTupleIndexes, None, False) + varStoreCFFV = varLib.builder.buildVarStore(varTupleList, [varDeltasCFFV]) + + topDict = varFont['CFF2'].cff.topDictIndex[0] + topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV) + + +def lib_convertCFFToCFF2(cff, otFont): + # This assumes a decompiled CFF table. + cff2GetGlyphOrder = cff.otFont.getGlyphOrder + topDictData = TopDictIndex(None, cff2GetGlyphOrder, None) + topDictData.items = cff.topDictIndex.items + cff.topDictIndex = topDictData + topDict = topDictData[0] + if hasattr(topDict, 'Private'): + privateDict = topDict.Private + else: + privateDict = None + opOrder = buildOrder(topDictOperators2) + topDict.order = opOrder + topDict.cff2GetGlyphOrder = cff2GetGlyphOrder + if not hasattr(topDict, "FDArray"): + fdArray = topDict.FDArray = FDArrayIndex() + fdArray.strings = None + fdArray.GlobalSubrs = topDict.GlobalSubrs + topDict.GlobalSubrs.fdArray = fdArray + charStrings = topDict.CharStrings + if charStrings.charStringsAreIndexed: + charStrings.charStringsIndex.fdArray = fdArray + else: + charStrings.fdArray = fdArray + fontDict = FontDict() + fontDict.setCFF2(True) + fdArray.append(fontDict) + fontDict.Private = privateDict + privateOpOrder = buildOrder(privateDictOperators2) + for entry in privateDictOperators: + key = entry[1] + if key not in privateOpOrder: + if key in privateDict.rawDict: + # print "Removing private dict", key + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + # print "Removing privateDict attr", key + else: + # clean up the PrivateDicts in the fdArray + fdArray = topDict.FDArray + privateOpOrder = buildOrder(privateDictOperators2) + for fontDict in fdArray: + fontDict.setCFF2(True) + for key in list(fontDict.rawDict.keys()): + if key not in fontDict.order: + del fontDict.rawDict[key] + if hasattr(fontDict, key): + delattr(fontDict, key) + + privateDict = fontDict.Private + for entry in privateDictOperators: + key = entry[1] + if key not in privateOpOrder: + if key in privateDict.rawDict: + # print "Removing private dict", key + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + # print "Removing privateDict attr", key + # Now delete up the decrecated topDict operators from CFF 1.0 + for entry in topDictOperators: + key = entry[1] + if key not in opOrder: + if key in topDict.rawDict: + del topDict.rawDict[key] + if hasattr(topDict, key): + delattr(topDict, key) + + # At this point, the Subrs and Charstrings are all still T2Charstring class + # easiest to fix this by compiling, then decompiling again + cff.major = 2 + file = BytesIO() + cff.compile(file, otFont, isCFF2=True) + file.seek(0) + cff.decompile(file, otFont, isCFF2=True) + + +def convertCFFtoCFF2(varFont): + # Convert base font to a single master CFF2 font. + cffTable = varFont['CFF '] + lib_convertCFFToCFF2(cffTable.cff, varFont) + newCFF2 = newTable("CFF2") + newCFF2.cff = cffTable.cff + varFont['CFF2'] = newCFF2 + del varFont['CFF '] + + +class MergeDictError(TypeError): + def __init__(self, key, value, values): + error_msg = ["For the Private Dict key '{}', ".format(key), + "the default font value list:", + "\t{}".format(value), + "had a different number of values than a region font:"] + error_msg += ["\t{}".format(region_value) for region_value in values] + error_msg = os.linesep.join(error_msg) + + +def conv_to_int(num): + if num % 1 == 0: + return int(num) + return num + + +pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues", + "FamilyOtherBlues", "BlueScale", "BlueShift", + "BlueFuzz", "StdHW", "StdVW", "StemSnapH", + "StemSnapV") + + +def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model): + if hasattr(region_top_dicts[0], 'FDArray'): + regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts] + else: + regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts] + for fd_index, font_dict in enumerate(topDict.FDArray): + private_dict = font_dict.Private + pds = [private_dict] + [ + regionFDArray[fd_index].Private for regionFDArray in regionFDArrays + ] + for key, value in private_dict.rawDict.items(): + if key not in pd_blend_fields: + continue + if isinstance(value, list): + try: + values = [pd.rawDict[key] for pd in pds] + except KeyError: + del private_dict.rawDict[key] + print( + b"Warning: {key} in default font Private dict is " + b"missing from another font, and was " + b"discarded.".format(key=key)) + continue + try: + values = zip(*values) + except IndexError: + raise MergeDictError(key, value, values) + """ + Row 0 contains the first value from each master. + Convert each row from absolute values to relative + values from the previous row. + e.g for three masters, a list of values was: + master 0 OtherBlues = [-217,-205] + master 1 OtherBlues = [-234,-222] + master 1 OtherBlues = [-188,-176] + The call to zip() converts this to: + [(-217, -234, -188), (-205, -222, -176)] + and is converted finally to: + OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]] + """ + dataList = [] + prev_val_list = [0] * num_masters + any_points_differ = False + for val_list in values: + rel_list = [(val - prev_val_list[i]) for ( + i, val) in enumerate(val_list)] + if (not any_points_differ) and not allEqual(rel_list): + any_points_differ = True + prev_val_list = val_list + deltas = var_model.getDeltas(rel_list) + # Convert numbers with no decimal part to an int. + deltas = [conv_to_int(delta) for delta in deltas] + # For PrivateDict BlueValues, the default font + # values are absolute, not relative to the prior value. + deltas[0] = val_list[0] + dataList.append(deltas) + # If there are no blend values,then + # we can collapse the blend lists. + if not any_points_differ: + dataList = [data[0] for data in dataList] + else: + values = [pd.rawDict[key] for pd in pds] + if not allEqual(values): + dataList = var_model.getDeltas(values) + else: + dataList = values[0] + private_dict.rawDict[key] = dataList + + +def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder): + topDict = varFont['CFF2'].cff.topDictIndex[0] + default_charstrings = topDict.CharStrings + region_fonts = ordered_fonts_list[1:] + region_top_dicts = [ + ttFont['CFF '].cff.topDictIndex[0] for ttFont in region_fonts + ] + num_masters = len(model.mapping) + merge_PrivateDicts(topDict, region_top_dicts, num_masters, model) + merge_charstrings(default_charstrings, + glyphOrder, + num_masters, + region_top_dicts, model) + + +def merge_charstrings(default_charstrings, + glyphOrder, + num_masters, + region_top_dicts, + var_model): + for gname in glyphOrder: + default_charstring = default_charstrings[gname] + var_pen = CFF2CharStringMergePen([], gname, num_masters, 0) + default_charstring.outlineExtractor = CFFToCFF2OutlineExtractor + default_charstring.draw(var_pen) + for region_idx, region_td in enumerate(region_top_dicts, start=1): + region_charstrings = region_td.CharStrings + region_charstring = region_charstrings[gname] + var_pen.restart(region_idx) + region_charstring.draw(var_pen) + new_charstring = var_pen.getCharString( + private=default_charstring.private, + globalSubrs=default_charstring.globalSubrs, + var_model=var_model, optimize=True) + default_charstrings[gname] = new_charstring + + +class MergeTypeError(TypeError): + def __init__(self, point_type, pt_index, m_index, default_type, glyphName): + self.error_msg = [ + "In glyph '{gname}' " + "'{point_type}' at point index {pt_index} in master " + "index {m_index} differs from the default font point " + "type '{default_type}'" + "".format(gname=glyphName, + point_type=point_type, pt_index=pt_index, + m_index=m_index, default_type=default_type) + ][0] + super(MergeTypeError, self).__init__(self.error_msg) + + +def makeRoundNumberFunc(tolerance): + if tolerance < 0: + raise ValueError("Rounding tolerance must be positive") + + def roundNumber(val): + return t2c_round(val, tolerance) + + return roundNumber + + +class CFFToCFF2OutlineExtractor(T2OutlineExtractor): + """ This class is used to remove the initial width + from the CFF charstring without adding the width + to self.nominalWidthX, which is None. + """ + def popallWidth(self, evenOdd=0): + args = self.popall() + if not self.gotWidth: + if evenOdd ^ (len(args) % 2): + args = args[1:] + self.width = self.defaultWidthX + self.gotWidth = 1 + return args + + +class CFF2CharStringMergePen(T2CharStringPen): + """Pen to merge Type 2 CharStrings. + """ + def __init__(self, default_commands, + glyphName, num_masters, master_idx, roundTolerance=0.5): + super( + CFF2CharStringMergePen, + self).__init__(width=None, + glyphSet=None, CFF2=True, + roundTolerance=roundTolerance) + self.pt_index = 0 + self._commands = default_commands + self.m_index = master_idx + self.num_masters = num_masters + self.prev_move_idx = 0 + self.glyphName = glyphName + self.roundNumber = makeRoundNumberFunc(roundTolerance) + + def _p(self, pt): + """ Unlike T2CharstringPen, this class stores absolute values. + This is to allow the logic in check_and_fix_closepath() to work, + where the current or previous absolute point has to be compared to + the path start-point. + """ + self._p0 = pt + return list(self._p0) + + def add_point(self, point_type, pt_coords): + if self.m_index == 0: + self._commands.append([point_type, [pt_coords]]) + else: + cmd = self._commands[self.pt_index] + if cmd[0] != point_type: + # Fix some issues that show up in some + # CFF workflows, even when fonts are + # topologically merge compatible. + success, pt_coords = self.check_and_fix_flat_curve( + cmd, point_type, pt_coords) + if not success: + success = self.check_and_fix_closepath( + cmd, point_type, pt_coords) + if success: + # We may have incremented self.pt_index + cmd = self._commands[self.pt_index] + if cmd[0] != point_type: + success = False + if not success: + raise MergeTypeError(point_type, + self.pt_index, len(cmd[1]), + cmd[0], self.glyphName) + cmd[1].append(pt_coords) + self.pt_index += 1 + + def _moveTo(self, pt): + pt_coords = self._p(pt) + self.add_point('rmoveto', pt_coords) + # I set prev_move_idx here because add_point() + # can change self.pt_index. + self.prev_move_idx = self.pt_index - 1 + + def _lineTo(self, pt): + pt_coords = self._p(pt) + self.add_point('rlineto', pt_coords) + + def _curveToOne(self, pt1, pt2, pt3): + _p = self._p + pt_coords = _p(pt1)+_p(pt2)+_p(pt3) + self.add_point('rrcurveto', pt_coords) + + def _closePath(self): + pass + + def _endPath(self): + pass + + def restart(self, region_idx): + self.pt_index = 0 + self.m_index = region_idx + self._p0 = (0, 0) + + def getCommands(self): + return self._commands + + def reorder_blend_args(self, commands): + """ + We first re-order the master coordinate values. + For a moveto to lineto, the args are now arranged as: + [ [master_0 x,y], [master_1 x,y], [master_2 x,y] ] + We re-arrange this to + [ [master_0 x, master_1 x, master_2 x], + [master_0 y, master_1 y, master_2 y] + ] + We also make the value relative. + If the master values are all the same, we collapse the list to + as single value instead of a list. + """ + for cmd in commands: + # arg[i] is the set of arguments for this operator from master i. + args = cmd[1] + m_args = zip(*args) + # m_args[n] is now all num_master args for the i'th argument + # for this operation. + cmd[1] = m_args + + # Now convert from absolute to relative + x0 = [0]*self.num_masters + y0 = [0]*self.num_masters + for cmd in self._commands: + is_x = True + coords = cmd[1] + rel_coords = [] + for coord in coords: + prev_coord = x0 if is_x else y0 + rel_coord = [pt[0] - pt[1] for pt in zip(coord, prev_coord)] + + if allEqual(rel_coord): + rel_coord = rel_coord[0] + rel_coords.append(rel_coord) + if is_x: + x0 = coord + else: + y0 = coord + is_x = not is_x + cmd[1] = rel_coords + return commands + + @staticmethod + def mergeCommandsToProgram(commands, var_model, round_func): + """ + Takes a commands list as returned by programToCommands() and + converts it back to a T2CharString or CFF2Charstring program list. I + need to use this rather than specialize.commandsToProgram, as the + commands produced by CFF2CharStringMergePen initially contains a + list of coordinate values, one for each master, wherever a single + coordinate value is expected by the regular logic. The problem with + doing using the specialize.py functions is that a commands list is + expected to be a op name with its associated argument list. For the + commands list here, some of the arguments may need to be converted + to a new argument list and opcode. + This version will convert each list of master arguments to a blend + op and its arguments, and will also combine successive blend ops up + to the stack limit. + """ + program = [] + for op, args in commands: + num_args = len(args) + # some of the args may be blend lists, and some may be + # single coordinate values. + i = 0 + stack_use = 0 + while i < num_args: + arg = args[i] + if not isinstance(arg, list): + program.append(arg) + i += 1 + stack_use += 1 + else: + prev_stack_use = stack_use + """ The arg is a tuple of blend values. + These are each (master 0,master 1..master n) + Combine as many successive tuples as we can, + up to the max stack limit. + """ + num_masters = len(arg) + blendlist = [arg] + i += 1 + stack_use += 1 + num_masters # 1 for the num_blends arg + while (i < num_args) and isinstance(args[i], list): + blendlist.append(args[i]) + i += 1 + stack_use += num_masters + if stack_use + num_masters > maxStackLimit: + # if we are here, max stack is is the CFF2 max stack. + break + num_blends = len(blendlist) + # append the 'num_blends' default font values + for arg in blendlist: + if round_func: + arg[0] = round_func(arg[0]) + program.append(arg[0]) + for arg in blendlist: + # for each coordinate tuple, append the region deltas + if len(arg) != 3: + print(arg) + import pdb + pdb.set_trace() + deltas = var_model.getDeltas(arg) + if round_func: + deltas = [round_func(delta) for delta in deltas] + # First item in 'deltas' is the default master value; + # for CFF2 data, that has already been written. + program.extend(deltas[1:]) + program.append(num_blends) + program.append('blend') + stack_use = prev_stack_use + num_blends + if op: + program.append(op) + return program + + + def getCharString(self, private=None, globalSubrs=None, + var_model=None, optimize=True): + commands = self._commands + commands = self.reorder_blend_args(commands) + if optimize: + commands = specializeCommands(commands, generalizeFirst=False, + maxstack=maxStackLimit) + program = self.mergeCommandsToProgram(commands, var_model=var_model, + round_func=self.roundNumber) + charString = T2CharString(program=program, private=private, + globalSubrs=globalSubrs) + return charString diff --git a/Lib/fontTools/varLib/featureVars.py b/Lib/fontTools/varLib/featureVars.py index 60336215..de2808db 100644 --- a/Lib/fontTools/varLib/featureVars.py +++ b/Lib/fontTools/varLib/featureVars.py @@ -4,11 +4,13 @@ https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariat NOTE: The API is experimental and subject to change. """ from __future__ import print_function, absolute_import, division - +from fontTools.misc.py23 import * +from fontTools.misc.dictTools import hashdict +from fontTools.misc.intTools import popCount from fontTools.ttLib import newTable from fontTools.ttLib.tables import otTables as ot from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable -import itertools +from collections import OrderedDict def addFeatureVariations(font, conditionalSubstitutions): @@ -17,160 +19,236 @@ def addFeatureVariations(font, conditionalSubstitutions): The `conditionalSubstitutions` argument is a list of (Region, Substitutions) tuples. - A Region is a list of Spaces. A Space is a dict mapping axisTags to - (minValue, maxValue) tuples. Irrelevant axes may be omitted. - A Space represents a 'rectangular' subset of an N-dimensional design space. + A Region is a list of Boxes. A Box is a dict mapping axisTags to + (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are + interpretted as extending to end of axis in each direction. A Box represents + an orthogonal 'rectangular' subset of an N-dimensional design space. A Region represents a more complex subset of an N-dimensional design space, - ie. the union of all the Spaces in the Region. - For efficiency, Spaces within a Region should ideally not overlap, but + ie. the union of all the Boxes in the Region. + For efficiency, Boxes within a Region should ideally not overlap, but functionality is not compromised if they do. The minimum and maximum values are expressed in normalized coordinates. A Substitution is a dict mapping source glyph names to substitute glyph names. + + Example: + + # >>> f = TTFont(srcPath) + # >>> condSubst = [ + # ... # A list of (Region, Substitution) tuples. + # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), + # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), + # ... ] + # >>> addFeatureVariations(f, condSubst) + # >>> f.save(dstPath) """ - # Example: - # - # >>> f = TTFont(srcPath) - # >>> condSubst = [ - # ... # A list of (Region, Substitution) tuples. - # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), - # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), - # ... ] - # >>> addFeatureVariations(f, condSubst) - # >>> f.save(dstPath) - - # Since the FeatureVariations table will only ever match one rule at a time, - # we will make new rules for all possible combinations of our input, so we - # can indirectly support overlapping rules. - explodedConditionalSubstitutions = [] - for combination in iterAllCombinations(len(conditionalSubstitutions)): - regions = [] - lookups = [] - for index in combination: - regions.append(conditionalSubstitutions[index][0]) - lookups.append(conditionalSubstitutions[index][1]) - if not regions: - continue - intersection = regions[0] - for region in regions[1:]: - intersection = intersectRegions(intersection, region) - for space in intersection: - # Remove default values, so we don't generate redundant ConditionSets - space = cleanupSpace(space) - if space: - explodedConditionalSubstitutions.append((space, lookups)) - - addFeatureVariationsRaw(font, explodedConditionalSubstitutions) - - -def iterAllCombinations(numRules): - """Given a number of rules, yield all the combinations of indices, sorted - by decreasing length, so we get the most specialized rules first. - - >>> list(iterAllCombinations(0)) - [] - >>> list(iterAllCombinations(1)) - [(0,)] - >>> list(iterAllCombinations(2)) - [(0, 1), (0,), (1,)] - >>> list(iterAllCombinations(3)) - [(0, 1, 2), (0, 1), (0, 2), (1, 2), (0,), (1,), (2,)] + addFeatureVariationsRaw(font, + overlayFeatureVariations(conditionalSubstitutions)) + +def overlayFeatureVariations(conditionalSubstitutions): + """Compute overlaps between all conditional substitutions. + + The `conditionalSubstitutions` argument is a list of (Region, Substitutions) + tuples. + + A Region is a list of Boxes. A Box is a dict mapping axisTags to + (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are + interpretted as extending to end of axis in each direction. A Box represents + an orthogonal 'rectangular' subset of an N-dimensional design space. + A Region represents a more complex subset of an N-dimensional design space, + ie. the union of all the Boxes in the Region. + For efficiency, Boxes within a Region should ideally not overlap, but + functionality is not compromised if they do. + + The minimum and maximum values are expressed in normalized coordinates. + + A Substitution is a dict mapping source glyph names to substitute glyph names. + + Returns data is in similar but different format. Overlaps of distinct + substitution Boxes (*not* Regions) are explicitly listed as distinct rules, + and rules with the same Box merged. The more specific rules appear earlier + in the resulting list. Moreover, instead of just a dictionary of substitutions, + a list of dictionaries is returned for substitutions corresponding to each + uniq space, with each dictionary being identical to one of the input + substitution dictionaries. These dictionaries are not merged to allow data + sharing when they are converted into font tables. + + Example: + >>> condSubst = [ + ... # A list of (Region, Substitution) tuples. + ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), + ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), + ... ] + >>> from pprint import pprint + >>> pprint(overlayFeatureVariations(condSubst)) + [({'wdth': (0.5, 1.0), 'wght': (0.5, 1.0)}, + [{'dollar': 'dollar.rvrn'}, {'cent': 'cent.rvrn'}]), + ({'wdth': (0.5, 1.0)}, [{'cent': 'cent.rvrn'}]), + ({'wght': (0.5, 1.0)}, [{'dollar': 'dollar.rvrn'}])] """ - indices = range(numRules) - for length in range(numRules, 0, -1): - for combinations in itertools.combinations(indices, length): - yield combinations + + # Merge same-substitutions rules, as this creates fewer number oflookups. + merged = OrderedDict() + for value,key in conditionalSubstitutions: + key = hashdict(key) + if key in merged: + merged[key].extend(value) + else: + merged[key] = value + conditionalSubstitutions = [(v,dict(k)) for k,v in merged.items()] + del merged + + # Merge same-region rules, as this is cheaper. + # Also convert boxes to hashdict() + # + # Reversing is such that earlier entries win in case of conflicting substitution + # rules for the same region. + merged = OrderedDict() + for key,value in reversed(conditionalSubstitutions): + key = tuple(sorted(hashdict(cleanupBox(k)) for k in key)) + if key in merged: + merged[key].update(value) + else: + merged[key] = dict(value) + conditionalSubstitutions = list(reversed(merged.items())) + del merged + + # Overlay + # + # Rank is the bit-set of the index of all contributing layers. + initMapInit = ((hashdict(),0),) # Initializer representing the entire space + boxMap = OrderedDict(initMapInit) # Map from Box to Rank + for i,(currRegion,_) in enumerate(conditionalSubstitutions): + newMap = OrderedDict(initMapInit) + currRank = 1<<i + for box,rank in boxMap.items(): + for currBox in currRegion: + intersection, remainder = overlayBox(currBox, box) + if intersection is not None: + intersection = hashdict(intersection) + newMap[intersection] = newMap.get(intersection, 0) | rank|currRank + if remainder is not None: + remainder = hashdict(remainder) + newMap[remainder] = newMap.get(remainder, 0) | rank + boxMap = newMap + del boxMap[hashdict()] + + # Generate output + items = [] + for box,rank in sorted(boxMap.items(), + key=(lambda BoxAndRank: -popCount(BoxAndRank[1]))): + substsList = [] + i = 0 + while rank: + if rank & 1: + substsList.append(conditionalSubstitutions[i][1]) + rank >>= 1 + i += 1 + items.append((dict(box),substsList)) + return items -# -# Region and Space support # # Terminology: # -# A 'Space' is a dict representing a "rectangular" bit of N-dimensional space. +# A 'Box' is a dict representing an orthogonal "rectangular" bit of N-dimensional space. # The keys in the dict are axis tags, the values are (minValue, maxValue) tuples. # Missing dimensions (keys) are substituted by the default min and max values # from the corresponding axes. # -# A 'Region' is a list of Space dicts, representing the union of the Spaces, -# therefore representing a more complex subset of design space. -# -def intersectRegions(region1, region2): - """Return the region intersecting `region1` and `region2`. - - >>> intersectRegions([], []) - [] - >>> intersectRegions([{'wdth': (0.0, 1.0)}], []) - [] - >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-1.0, 0.0)}] - >>> expected == intersectRegions([{'wdth': (0.0, 1.0)}], [{'wght': (-1.0, 0.0)}]) - True - >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.0)}] - >>> expected == intersectRegions([{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], [{'wght': (-1.0, 0.0)}]) - True - >>> intersectRegions( - ... [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], - ... [{'wdth': (-1.0, 0.0), 'wght': (-1.0, 0.0)}]) - [] +def overlayBox(top, bot): + """Overlays `top` box on top of `bot` box. + Returns two items: + - Box for intersection of `top` and `bot`, or None if they don't intersect. + - Box for remainder of `bot`. Remainder box might not be exact (since the + remainder might not be a simple box), but is inclusive of the exact + remainder. """ - region = [] - for space1 in region1: - for space2 in region2: - space = intersectSpaces(space1, space2) - if space is not None: - region.append(space) - return region - -def intersectSpaces(space1, space2): - """Return the space intersected by `space1` and `space2`, or None if there - is no intersection. - - >>> intersectSpaces({}, {}) - {} - >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {}) - {'wdth': (-0.5, 0.5)} - >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {'wdth': (0.0, 1.0)}) - {'wdth': (0.0, 0.5)} - >>> expected = {'wdth': (0.0, 0.5), 'wght': (0.25, 0.5)} - >>> expected == intersectSpaces({'wdth': (-0.5, 0.5), 'wght': (0.0, 0.5)}, {'wdth': (0.0, 1.0), 'wght': (0.25, 0.75)}) - True - >>> expected = {'wdth': (-0.5, 0.5), 'wght': (0.0, 1.0)} - >>> expected == intersectSpaces({'wdth': (-0.5, 0.5)}, {'wght': (0.0, 1.0)}) - True - >>> intersectSpaces({'wdth': (-0.5, 0)}, {'wdth': (0.1, 0.5)}) - - """ - space = {} - space.update(space1) - space.update(space2) - for axisTag in set(space1) & set(space2): - min1, max1 = space1[axisTag] - min2, max2 = space2[axisTag] + # Intersection + intersection = {} + intersection.update(top) + intersection.update(bot) + for axisTag in set(top) & set(bot): + min1, max1 = top[axisTag] + min2, max2 = bot[axisTag] minimum = max(min1, min2) maximum = min(max1, max2) if not minimum < maximum: - return None - space[axisTag] = minimum, maximum - return space - - -def cleanupSpace(space): - """Return a sparse copy of `space`, without redundant (default) values. + return None, bot # Do not intersect + intersection[axisTag] = minimum,maximum - >>> cleanupSpace({}) + # Remainder + # + # Remainder is empty if bot's each axis range lies within that of intersection. + # + # Remainder is shrank if bot's each, except for exactly one, axis range lies + # within that of intersection, and that one axis, it spills out of the + # intersection only on one side. + # + # Bot is returned in full as remainder otherwise, as true remainder is not + # representable as a single box. + + remainder = dict(bot) + exactlyOne = False + fullyInside = False + for axisTag in bot: + if axisTag not in intersection: + fullyInside = False + continue # Axis range lies fully within + min1, max1 = intersection[axisTag] + min2, max2 = bot[axisTag] + if min1 <= min2 and max2 <= max1: + continue # Axis range lies fully within + + # Bot's range doesn't fully lie within that of top's for this axis. + # We know they intersect, so it cannot lie fully without either; so they + # overlap. + + # If we have had an overlapping axis before, remainder is not + # representable as a box, so return full bottom and go home. + if exactlyOne: + return intersection, bot + exactlyOne = True + fullyInside = False + + # Otherwise, cut remainder on this axis and continue. + if min1 <= min2: + # Right side survives. + minimum = max(max1, min2) + maximum = max2 + elif max2 <= max1: + # Left side survives. + minimum = min2 + maximum = min(min1, max2) + else: + # Remainder leaks out from both sides. Can't cut either. + return intersection, bot + + remainder[axisTag] = minimum,maximum + + if fullyInside: + # bot is fully within intersection. Remainder is empty. + return intersection, None + + return intersection, remainder + +def cleanupBox(box): + """Return a sparse copy of `box`, without redundant (default) values. + + >>> cleanupBox({}) {} - >>> cleanupSpace({'wdth': (0.0, 1.0)}) + >>> cleanupBox({'wdth': (0.0, 1.0)}) {'wdth': (0.0, 1.0)} - >>> cleanupSpace({'wdth': (-1.0, 1.0)}) + >>> cleanupBox({'wdth': (-1.0, 1.0)}) {} """ - return {tag: limit for tag, limit in space.items() if limit != (-1.0, 1.0)} + return {tag: limit for tag, limit in box.items() if limit != (-1.0, 1.0)} # @@ -210,7 +288,8 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions): rvrnFeatureIndex = gsub.FeatureList.FeatureRecord.index(rvrnFeature) for scriptRecord in gsub.ScriptList.ScriptRecord: - for langSys in [scriptRecord.Script.DefaultLangSys] + scriptRecord.Script.LangSysRecord: + langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord] + for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems: langSys.FeatureIndex.append(rvrnFeatureIndex) # setup lookups @@ -327,7 +406,7 @@ def buildFeatureVariationRecord(conditionTable, substitutionRecords): fvr.ConditionSet = ot.ConditionSet() fvr.ConditionSet.ConditionTable = conditionTable fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution() - fvr.FeatureTableSubstitution.Version = 0x00010001 + fvr.FeatureTableSubstitution.Version = 0x00010000 fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords return fvr @@ -388,5 +467,5 @@ def _remapLangSys(langSys, featureRemap): if __name__ == "__main__": - import doctest - doctest.testmod() + import doctest, sys + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/varLib/interpolate_layout.py b/Lib/fontTools/varLib/interpolate_layout.py index 1ce3fafb..f252149a 100644 --- a/Lib/fontTools/varLib/interpolate_layout.py +++ b/Lib/fontTools/varLib/interpolate_layout.py @@ -4,16 +4,17 @@ Interpolate OpenType Layout tables (GDEF / GPOS / GSUB). from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.ttLib import TTFont -from fontTools.varLib import models, VarLibError, load_designspace +from fontTools.varLib import models, VarLibError, load_designspace, load_masters from fontTools.varLib.merger import InstancerMerger import os.path import logging +from copy import deepcopy from pprint import pformat log = logging.getLogger("fontTools.varLib.interpolate_layout") -def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapped=False): +def interpolate_layout(designspace, loc, master_finder=lambda s:s, mapped=False): """ Interpolate GPOS from a designspace file and location. @@ -26,18 +27,18 @@ def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapp it is assumed that location is in designspace's internal space and no mapping is performed. """ - ds = load_designspace(designspace_filename) + if hasattr(designspace, "sources"): # Assume a DesignspaceDocument + pass + else: # Assume a file path + from fontTools.designspaceLib import DesignSpaceDocument + designspace = DesignSpaceDocument.fromfile(designspace) + ds = load_designspace(designspace) log.info("Building interpolated font") - log.info("Loading master fonts") - basedir = os.path.dirname(designspace_filename) - master_ttfs = [ - master_finder(os.path.join(basedir, m.filename)) for m in ds.masters - ] - master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] - #font = master_fonts[ds.base_idx] - font = TTFont(master_ttfs[ds.base_idx]) + log.info("Loading master fonts") + master_fonts = load_masters(designspace, master_finder) + font = deepcopy(master_fonts[ds.base_idx]) log.info("Location: %s", pformat(loc)) if not mapped: @@ -53,6 +54,7 @@ def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapp merger = InstancerMerger(font, model, loc) log.info("Building interpolated tables") + # TODO GSUB/GDEF merger.mergeTables(font, master_fonts, ['GPOS']) return font diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py index aaee3118..688790c5 100644 --- a/Lib/fontTools/varLib/merger.py +++ b/Lib/fontTools/varLib/merger.py @@ -8,7 +8,8 @@ from fontTools.misc import classifyTools from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables import otBase as otBase from fontTools.ttLib.tables.DefaultTable import DefaultTable -from fontTools.varLib import builder, varStore +from fontTools.varLib import builder, models, varStore +from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo from fontTools.varLib.varStore import VarStoreInstancer from functools import reduce @@ -75,8 +76,7 @@ class Merger(object): raise def mergeLists(self, out, lst): - count = len(out) - assert all(count == len(v) for v in lst), (count, [len(v) for v in lst]) + assert allEqualTo(out, lst, len), (len(out), [len(v) for v in lst]) for i,(value,values) in enumerate(zip(out, zip(*lst))): try: self.mergeThings(value, values) @@ -85,9 +85,8 @@ class Merger(object): raise def mergeThings(self, out, lst): - clazz = type(out) try: - assert all(type(item) == clazz for item in lst), (out, lst) + assert allEqualTo(out, lst, type), (out, lst) mergerFunc = self.mergersFor(out).get(None, None) if mergerFunc is not None: mergerFunc(self, out, lst) @@ -96,16 +95,17 @@ class Merger(object): elif isinstance(out, list): self.mergeLists(out, lst) else: - assert all(out == v for v in lst), (out, lst) + assert allEqualTo(out, lst), (out, lst) except Exception as e: - e.args = e.args + (clazz.__name__,) + e.args = e.args + (type(out).__name__,) raise - def mergeTables(self, font, master_ttfs, tables): + def mergeTables(self, font, master_ttfs, tableTags): - for tag in tables: + for tag in tableTags: if tag not in font: continue - self.mergeThings(font[tag], [m[tag] for m in master_ttfs]) + self.mergeThings(font[tag], [m[tag] if tag in m else None + for m in master_ttfs]) # # Aligning merger @@ -113,6 +113,27 @@ class Merger(object): class AligningMerger(Merger): pass +@AligningMerger.merger(ot.GDEF, "GlyphClassDef") +def merge(merger, self, lst): + if self is None: + assert allNone(lst), (lst) + return + + self.classDefs = {} + # We only care about the .classDefs + self = self.classDefs + lst = [l.classDefs for l in lst] + + allKeys = set() + allKeys.update(*[l.keys() for l in lst]) + for k in allKeys: + allValues = nonNone(l.get(k) for l in lst) + assert allEqual(allValues), allValues + if not allValues: + self[k] = None + else: + self[k] = allValues[0] + def _SinglePosUpgradeToFormat2(self): if self.Format == 2: return self @@ -201,7 +222,8 @@ def merge(merger, self, lst): assert len(lst) == 1 or (valueFormat & ~0xF == 0), valueFormat # If all have same coverage table and all are format 1, - if all(v.Format == 1 for v in lst) and all(self.Coverage.glyphs == v.Coverage.glyphs for v in lst): + coverageGlyphs = self.Coverage.glyphs + if all(v.Format == 1 for v in lst) and all(coverageGlyphs == v.Coverage.glyphs for v in lst): self.Value = otBase.ValueRecord(valueFormat) merger.mergeThings(self.Value, [v.Value for v in lst]) self.ValueFormat = self.Value.getFormat() @@ -276,7 +298,7 @@ def merge(merger, self, lst): merger.mergeLists(self.PairValueRecord, padded) def _PairPosFormat1_merge(self, lst, merger): - assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools." + assert allEqual([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools." # Merge everything else; makes sure Format is the same. merger.mergeObjects(self, lst, @@ -330,19 +352,22 @@ def _ClassDef_invert(self, allGlyphs=None): return ret -def _ClassDef_merge_classify(lst, allGlyphs=None): +def _ClassDef_merge_classify(lst, allGlyphses=None): self = ot.ClassDef() self.classDefs = classDefs = {} + allGlyphsesWasNone = allGlyphses is None + if allGlyphsesWasNone: + allGlyphses = [None] * len(lst) classifier = classifyTools.Classifier() - for l in lst: - sets = _ClassDef_invert(l, allGlyphs=allGlyphs) + for classDef,allGlyphs in zip(lst, allGlyphses): + sets = _ClassDef_invert(classDef, allGlyphs) if allGlyphs is None: sets = sets[1:] classifier.update(sets) classes = classifier.getClasses() - if allGlyphs is None: + if allGlyphsesWasNone: classes.insert(0, set()) for i,classSet in enumerate(classes): @@ -353,6 +378,9 @@ def _ClassDef_merge_classify(lst, allGlyphs=None): return self, classes +# It's stupid that we need to do this here. Just need to, to match test +# expecatation results, since ttx prints out format of ClassDef (and Coverage) +# even though it should not. def _ClassDef_calculate_Format(self, font): fmt = 2 ranges = self._getClassRanges(font) @@ -370,7 +398,7 @@ def _PairPosFormat2_align_matrices(self, lst, font, transparent=False): matrices = [l.Class1Record for l in lst] # Align first classes - self.ClassDef1, classes = _ClassDef_merge_classify([l.ClassDef1 for l in lst], allGlyphs=set(self.Coverage.glyphs)) + self.ClassDef1, classes = _ClassDef_merge_classify([l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst]) _ClassDef_calculate_Format(self.ClassDef1, font) self.Class1Count = len(classes) new_matrices = [] @@ -382,6 +410,11 @@ def _PairPosFormat2_align_matrices(self, lst, font, transparent=False): for classSet in classes: exemplarGlyph = next(iter(classSet)) if exemplarGlyph not in coverage: + # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f, + # Fixes https://github.com/googlei18n/fontmake/issues/470 + # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9 + # when merger becomes selfless. + nullRow = None if nullRow is None: nullRow = ot.Class1Record() class2records = nullRow.Class2Record = [] @@ -431,7 +464,7 @@ def _PairPosFormat2_align_matrices(self, lst, font, transparent=False): return matrices def _PairPosFormat2_merge(self, lst, merger): - assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools." + assert allEqual([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools." merger.mergeObjects(self, lst, exclude=('Coverage', @@ -500,6 +533,105 @@ def merge(merger, self, lst): self.ValueFormat1 = vf1 self.ValueFormat2 = vf2 +def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'): + self.ClassCount = max(l.ClassCount for l in lst) + + MarkCoverageGlyphs, MarkRecords = \ + _merge_GlyphOrders(merger.font, + [getattr(l, Mark+'Coverage').glyphs for l in lst], + [getattr(l, Mark+'Array').MarkRecord for l in lst]) + getattr(self, Mark+'Coverage').glyphs = MarkCoverageGlyphs + + BaseCoverageGlyphs, BaseRecords = \ + _merge_GlyphOrders(merger.font, + [getattr(l, Base+'Coverage').glyphs for l in lst], + [getattr(getattr(l, Base+'Array'), Base+'Record') for l in lst]) + getattr(self, Base+'Coverage').glyphs = BaseCoverageGlyphs + + # MarkArray + records = [] + for g,glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)): + allClasses = [r.Class for r in glyphRecords if r is not None] + + # TODO Right now we require that all marks have same class in + # all masters that cover them. This is not required. + # + # We can relax that by just requiring that all marks that have + # the same class in a master, have the same class in every other + # master. Indeed, if, say, a sparse master only covers one mark, + # that mark probably will get class 0, which would possibly be + # different from its class in other masters. + # + # We can even go further and reclassify marks to support any + # input. But, since, it's unlikely that two marks being both, + # say, "top" in one master, and one being "top" and other being + # "top-right" in another master, we shouldn't do that, as any + # failures in that case will probably signify mistakes in the + # input masters. + + assert allEqual(allClasses), allClasses + if not allClasses: + rec = None + else: + rec = ot.MarkRecord() + rec.Class = allClasses[0] + allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords] + if allNone(allAnchors): + anchor = None + else: + anchor = ot.Anchor() + anchor.Format = 1 + merger.mergeThings(anchor, allAnchors) + rec.MarkAnchor = anchor + records.append(rec) + array = ot.MarkArray() + array.MarkRecord = records + array.MarkCount = len(records) + setattr(self, Mark+"Array", array) + + # BaseArray + records = [] + for g,glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)): + if allNone(glyphRecords): + rec = None + else: + rec = getattr(ot, Base+'Record')() + anchors = [] + setattr(rec, Base+'Anchor', anchors) + glyphAnchors = [[] if r is None else getattr(r, Base+'Anchor') + for r in glyphRecords] + for l in glyphAnchors: + l.extend([None] * (self.ClassCount - len(l))) + for allAnchors in zip(*glyphAnchors): + if allNone(allAnchors): + anchor = None + else: + anchor = ot.Anchor() + anchor.Format = 1 + merger.mergeThings(anchor, allAnchors) + anchors.append(anchor) + records.append(rec) + array = getattr(ot, Base+'Array')() + setattr(array, Base+'Record', records) + setattr(array, Base+'Count', len(records)) + setattr(self, Base+'Array', array) + +@AligningMerger.merger(ot.MarkBasePos) +def merge(merger, self, lst): + assert allEqualTo(self.Format, (l.Format for l in lst)) + if self.Format == 1: + _MarkBasePosFormat1_merge(self, lst, merger) + else: + assert False + +@AligningMerger.merger(ot.MarkMarkPos) +def merge(merger, self, lst): + assert allEqualTo(self.Format, (l.Format for l in lst)) + if self.Format == 1: + _MarkBasePosFormat1_merge(self, lst, merger, 'Mark1', 'Mark2') + else: + assert False + def _PairSet_flatten(lst, font): self = ot.PairSet() @@ -525,7 +657,7 @@ def _PairSet_flatten(lst, font): return self def _Lookup_PairPosFormat1_subtables_flatten(lst, font): - assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools." + assert allEqual([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools." self = ot.PairPos() self.Format = 1 @@ -546,7 +678,7 @@ def _Lookup_PairPosFormat1_subtables_flatten(lst, font): return self def _Lookup_PairPosFormat2_subtables_flatten(lst, font): - assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools." + assert allEqual([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools." self = ot.PairPos() self.Format = 2 @@ -603,8 +735,8 @@ def merge(merger, self, lst): if not sts: continue if sts[0].__class__.__name__.startswith('Extension'): - assert _all_equal([st.__class__ for st in sts]) - assert _all_equal([st.ExtensionLookupType for st in sts]) + assert allEqual([st.__class__ for st in sts]) + assert allEqual([st.ExtensionLookupType for st in sts]) l.LookupType = sts[0].ExtensionLookupType new_sts = [st.ExtSubTable for st in sts] del sts[:] @@ -655,8 +787,17 @@ class InstancerMerger(AligningMerger): self.location = location self.scalars = model.getScalars(location) +@InstancerMerger.merger(ot.CaretValue) +def merge(merger, self, lst): + assert self.Format == 1 + Coords = [a.Coordinate for a in lst] + model = merger.model + scalars = merger.scalars + self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars)) + @InstancerMerger.merger(ot.Anchor) def merge(merger, self, lst): + assert self.Format == 1 XCoords = [a.XCoordinate for a in lst] YCoords = [a.YCoordinate for a in lst] model = merger.model @@ -688,7 +829,9 @@ def merge(merger, self, lst): class MutatorMerger(AligningMerger): """A merger that takes a variable font, and instantiates - an instance.""" + an instance. While there's no "merging" to be done per se, + the operation can benefit from many operations that the + aligning merger does.""" def __init__(self, font, location): Merger.__init__(self, font) @@ -702,59 +845,32 @@ class MutatorMerger(AligningMerger): self.instancer = VarStoreInstancer(store, font['fvar'].axes, location) - def instantiate(self): - font = self.font - - for tableTag in 'GSUB','GPOS': - if not tableTag in font: - continue - table = font[tableTag].table - if not hasattr(table, 'FeatureVariations'): - continue - variations = table.FeatureVariations - for record in variations.FeatureVariationRecord: - applies = True - for condition in record.ConditionSet.ConditionTable: - if condition.Format == 1: - axisIdx = condition.AxisIndex - axisTag = self.font['fvar'].axes[axisIdx].axisTag - Min = condition.FilterRangeMinValue - Max = condition.FilterRangeMaxValue - loc = self.location[axisTag] - if not (Min <= loc <= Max): - applies = False - else: - applies = False - if not applies: - break - - if applies: - assert record.FeatureTableSubstitution.Version == 0x00010000 - for rec in record.FeatureTableSubstitution.SubstitutionRecord: - table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature - break - del table.FeatureVariations - - - self.mergeTables(font, [font], ['GPOS']) +@MutatorMerger.merger(ot.CaretValue) +def merge(merger, self, lst): + + # Hack till we become selfless. + self.__dict__ = lst[0].__dict__.copy() - if 'GDEF' in font: - gdef = font['GDEF'].table - if gdef.Version >= 0x00010003: - del gdef.VarStore - gdef.Version = 0x00010002 - if gdef.MarkGlyphSetsDef is None: - del gdef.MarkGlyphSetsDef - gdef.Version = 0x00010000 - if not (gdef.LigCaretList or - gdef.MarkAttachClassDef or - gdef.GlyphClassDef or - gdef.AttachList or - (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)): - del font['GDEF'] + if self.Format != 3: + return + + instancer = merger.instancer + dev = self.DeviceTable + del self.DeviceTable + if dev: + assert dev.DeltaFormat == 0x8000 + varidx = (dev.StartSize << 16) + dev.EndSize + delta = otRound(instancer[varidx]) + self.Coordinate += delta + + self.Format = 1 @MutatorMerger.merger(ot.Anchor) def merge(merger, self, lst): + + # Hack till we become selfless. + self.__dict__ = lst[0].__dict__.copy() + if self.Format != 3: return @@ -780,9 +896,7 @@ def merge(merger, self, lst): @MutatorMerger.merger(otBase.ValueRecord) def merge(merger, self, lst): - # All other structs are merged with self pointing to a copy of base font, - # except for ValueRecords which are sometimes created later and initialized - # to have 0/None members. Hence the copy. + # Hack till we become selfless. self.__dict__ = lst[0].__dict__.copy() instancer = merger.instancer @@ -816,26 +930,43 @@ class VariationMerger(AligningMerger): def __init__(self, model, axisTags, font): Merger.__init__(self, font) - self.model = model self.store_builder = varStore.OnlineVarStoreBuilder(axisTags) + self.setModel(model) + + def setModel(self, model): + self.model = model self.store_builder.setModel(model) -def _all_equal(lst): - if not lst: - return True - it = iter(lst) - v0 = next(it) - for v in it: - if v0 != v: - return False - return True + def mergeThings(self, out, lst): + masterModel = None + if None in lst: + if allNone(lst): + assert out is None, (out, lst) + return + masterModel = self.model + model, lst = masterModel.getSubModel(lst) + self.setModel(model) + + super(VariationMerger, self).mergeThings(out, lst) + + if masterModel: + self.setModel(masterModel) + def buildVarDevTable(store_builder, master_values): - if _all_equal(master_values): + if allEqual(master_values): return master_values[0], None base, varIdx = store_builder.storeMasters(master_values) return base, builder.buildVarDevTable(varIdx) +@VariationMerger.merger(ot.CaretValue) +def merge(merger, self, lst): + assert self.Format == 1 + self.Coordinate, DeviceTable = buildVarDevTable(merger.store_builder, [a.Coordinate for a in lst]) + if DeviceTable: + self.Format = 3 + self.DeviceTable = DeviceTable + @VariationMerger.merger(ot.Anchor) def merge(merger, self, lst): assert self.Format == 1 diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py index 653b75ff..207e30f3 100644 --- a/Lib/fontTools/varLib/models.py +++ b/Lib/fontTools/varLib/models.py @@ -2,9 +2,36 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -__all__ = ['normalizeValue', 'normalizeLocation', 'supportScalar', 'VariationModel'] +__all__ = ['nonNone', 'allNone', 'allEqual', 'allEqualTo', 'subList', + 'normalizeValue', 'normalizeLocation', + 'supportScalar', + 'VariationModel'] +def nonNone(lst): + return [l for l in lst if l is not None] + +def allNone(lst): + return all(l is None for l in lst) + +def allEqualTo(ref, lst, mapper=None): + if mapper is None: + return all(ref == item for item in lst) + else: + mapped = mapper(ref) + return all(mapped == mapper(item) for item in lst) + +def allEqual(lst, mapper=None): + if not lst: + return True + it = iter(lst) + first = next(it) + return allEqualTo(first, it, mapper=mapper) + +def subList(truth, lst): + assert len(truth) == len(lst) + return [l for l,t in zip(lst,truth) if t] + def normalizeValue(v, triple): """Normalizes value based on a min/default/max triple. >>> normalizeValue(400, (100, 400, 900)) @@ -163,6 +190,9 @@ class VariationModel(object): """ def __init__(self, locations, axisOrder=[]): + self.origLocations = locations + self.axisOrder = axisOrder + locations = [{k:v for k,v in loc.items() if v != 0.} for loc in locations] keyFunc = self.getMasterLocationsSortKeyFunc(locations, axisOrder=axisOrder) axisPoints = keyFunc.axisPoints @@ -172,6 +202,17 @@ class VariationModel(object): self.reverseMapping = [locations.index(l) for l in self.locations] # Reverse of above self._computeMasterSupports(axisPoints, axisOrder) + self._subModels = {} + + def getSubModel(self, items): + if None not in items: + return self, items + key = tuple(v is not None for v in items) + subModel = self._subModels.get(key) + if subModel is None: + subModel = VariationModel(subList(key, self.origLocations), self.axisOrder) + self._subModels[key] = subModel + return subModel, subList(key, items) @staticmethod def getMasterLocationsSortKeyFunc(locations, axisOrder=[]): @@ -223,24 +264,37 @@ class VariationModel(object): return min(v for v in lst if v > value) else: return value + def reorderMasters(self, master_list, mapping): + # For changing the master data order without + # recomputing supports and deltaWeights. + new_list = [master_list[idx] for idx in mapping] + self.origLocations = [self.origLocations[idx] for idx in mapping] + locations = [{k:v for k,v in loc.items() if v != 0.} + for loc in self.origLocations] + self.mapping = [self.locations.index(l) for l in locations] + self.reverseMapping = [locations.index(l) for l in self.locations] + self._subModels = {} + return new_list def _computeMasterSupports(self, axisPoints, axisOrder): supports = [] deltaWeights = [] locations = self.locations + # Compute min/max across each axis, use it as total range. + # TODO Take this as input from outside? + minV = {} + maxV = {} + for l in locations: + for k,v in l.items(): + minV[k] = min(v, minV.get(k, v)) + maxV[k] = max(v, maxV.get(k, v)) for i,loc in enumerate(locations): box = {} - - # Account for axisPoints first - # TODO Use axis min/max instead? Isn't that always -1/+1? - for axis,values in axisPoints.items(): - if not axis in loc: - continue - locV = loc[axis] + for axis,locV in loc.items(): if locV > 0: - box[axis] = (0, locV, max({locV}|values)) + box[axis] = (0, locV, maxV[axis]) else: - box[axis] = (min({locV}|values), locV, 0) + box[axis] = (minV[axis], locV, 0) locAxes = set(loc.keys()) # Walk over previous masters now @@ -258,12 +312,15 @@ class VariationModel(object): continue # Split the box for new master; split in whatever direction - # that has largest range ratio. See commit for details. - orderedAxes = [axis for axis in axisOrder if axis in m.keys()] - orderedAxes.extend([axis for axis in sorted(m.keys()) if axis not in axisOrder]) - bestAxis = None + # that has largest range ratio. + # + # For symmetry, we actually cut across multiple axes + # if they have the largest, equal, ratio. + # https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804 + + bestAxes = {} bestRatio = -1 - for axis in orderedAxes: + for axis in m.keys(): val = m[axis] assert axis in box lower,locV,upper = box[axis] @@ -278,14 +335,13 @@ class VariationModel(object): # Can't split box in this direction. continue if ratio > bestRatio: + bestAxes = {} bestRatio = ratio - bestAxis = axis - bestLower = newLower - bestUpper = newUpper - bestLocV = locV + if ratio == bestRatio: + bestAxes[axis] = (newLower, locV, newUpper) - if bestAxis: - box[bestAxis] = (bestLower,bestLocV,bestUpper) + for axis,triple in bestAxes.items (): + box[axis] = triple supports.append(box) deltaWeight = {} @@ -310,6 +366,10 @@ class VariationModel(object): out.append(delta) return out + def getDeltasAndSupports(self, items): + model, items = self.getSubModel(items) + return model.getDeltas(items), model.supports + def getScalars(self, loc): return [supportScalar(loc, support) for support in self.supports] diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py index 7a03e454..1a3b7389 100644 --- a/Lib/fontTools/varLib/mutator.py +++ b/Lib/fontTools/varLib/mutator.py @@ -5,17 +5,22 @@ $ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85 """ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.fixedTools import floatToFixedToFloat, otRound -from fontTools.ttLib import TTFont +from fontTools.misc.fixedTools import floatToFixedToFloat, otRound, floatToFixed +from fontTools.pens.boundsPen import BoundsPen +from fontTools.ttLib import TTFont, newTable +from fontTools.ttLib.tables import ttProgram from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates from fontTools.varLib import _GetCoordinates, _SetCoordinates from fontTools.varLib.models import ( - supportScalar, normalizeLocation, piecewiseLinearMap + supportScalar, + normalizeLocation, + piecewiseLinearMap, ) from fontTools.varLib.merger import MutatorMerger from fontTools.varLib.varStore import VarStoreInstancer from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib.iup import iup_delta +import fontTools.subset.cff import os.path import logging @@ -30,6 +35,116 @@ for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1): OS2_WIDTH_CLASS_VALUES[half] = i +def interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas): + pd_blend_lists = ("BlueValues", "OtherBlues", "FamilyBlues", + "FamilyOtherBlues", "StemSnapH", + "StemSnapV") + pd_blend_values = ("BlueScale", "BlueShift", + "BlueFuzz", "StdHW", "StdVW") + for fontDict in topDict.FDArray: + pd = fontDict.Private + vsindex = pd.vsindex if (hasattr(pd, 'vsindex')) else 0 + for key, value in pd.rawDict.items(): + if (key in pd_blend_values) and isinstance(value, list): + delta = interpolateFromDeltas(vsindex, value[1:]) + pd.rawDict[key] = otRound(value[0] + delta) + elif (key in pd_blend_lists) and isinstance(value[0], list): + """If any argument in a BlueValues list is a blend list, + then they all are. The first value of each list is an + absolute value. The delta tuples are calculated from + relative master values, hence we need to append all the + deltas to date to each successive absolute value.""" + delta = 0 + for i, val_list in enumerate(value): + delta += otRound(interpolateFromDeltas(vsindex, + val_list[1:])) + value[i] = val_list[0] + delta + + +def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder): + charstrings = topDict.CharStrings + for gname in glyphOrder: + # Interpolate charstring + charstring = charstrings[gname] + pd = charstring.private + vsindex = pd.vsindex if (hasattr(pd, 'vsindex')) else 0 + num_regions = pd.getNumRegions(vsindex) + numMasters = num_regions + 1 + new_program = [] + last_i = 0 + for i, token in enumerate(charstring.program): + if token == 'blend': + num_args = charstring.program[i - 1] + """ The stack is now: + ..args for following operations + num_args values from the default font + num_args tuples, each with numMasters-1 delta values + num_blend_args + 'blend' + """ + argi = i - (num_args*numMasters + 1) + end_args = tuplei = argi + num_args + while argi < end_args: + next_ti = tuplei + num_regions + deltas = charstring.program[tuplei:next_ti] + delta = interpolateFromDeltas(vsindex, deltas) + charstring.program[argi] += otRound(delta) + tuplei = next_ti + argi += 1 + new_program.extend(charstring.program[last_i:end_args]) + last_i = i + 1 + if last_i != 0: + new_program.extend(charstring.program[last_i:]) + charstring.program = new_program + + +def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc): + """Unlike TrueType glyphs, neither advance width nor bounding box + info is stored in a CFF2 charstring. The width data exists only in + the hmtx and HVAR tables. Since LSB data cannot be interpolated + reliably from the master LSB values in the hmtx table, we traverse + the charstring to determine the actual bound box. """ + + charstrings = topDict.CharStrings + boundsPen = BoundsPen(glyphOrder) + hmtx = varfont['hmtx'] + hvar_table = None + if 'HVAR' in varfont: + hvar_table = varfont['HVAR'].table + fvar = varfont['fvar'] + varStoreInstancer = VarStoreInstancer(hvar_table.VarStore, fvar.axes, loc) + + for gid, gname in enumerate(glyphOrder): + entry = list(hmtx[gname]) + # get width delta. + if hvar_table: + if hvar_table.AdvWidthMap: + width_idx = hvar_table.AdvWidthMap.mapping[gname] + else: + width_idx = gid + width_delta = otRound(varStoreInstancer[width_idx]) + else: + width_delta = 0 + + # get LSB. + boundsPen.init() + charstring = charstrings[gname] + charstring.draw(boundsPen) + if boundsPen.bounds is None: + # Happens with non-marking glyphs + lsb_delta = 0 + else: + lsb = boundsPen.bounds[0] + lsb_delta = entry[1] - lsb + + if lsb_delta or width_delta: + if width_delta: + entry[0] += width_delta + if lsb_delta: + entry[1] = lsb + hmtx[gname] = tuple(entry) + + def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. @@ -58,31 +173,34 @@ def instantiateVariableFont(varfont, location, inplace=False): # Location is normalized now log.info("Normalized location: %s", loc) - log.info("Mutating glyf/gvar tables") - gvar = varfont['gvar'] - glyf = varfont['glyf'] - # get list of glyph names in gvar sorted by component depth - glyphnames = sorted( - gvar.variations.keys(), - key=lambda name: ( - glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() else 0, - name)) - for glyphname in glyphnames: - variations = gvar.variations[glyphname] - coordinates,_ = _GetCoordinates(varfont, glyphname) - origCoords, endPts = None, None - for var in variations: - scalar = supportScalar(loc, var.axes) - if not scalar: continue - delta = var.coordinates - if None in delta: - if origCoords is None: - origCoords,control = _GetCoordinates(varfont, glyphname) - endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) - delta = iup_delta(delta, origCoords, endPts) - coordinates += GlyphCoordinates(delta) * scalar - _SetCoordinates(varfont, glyphname, coordinates) + if 'gvar' in varfont: + log.info("Mutating glyf/gvar tables") + gvar = varfont['gvar'] + glyf = varfont['glyf'] + # get list of glyph names in gvar sorted by component depth + glyphnames = sorted( + gvar.variations.keys(), + key=lambda name: ( + glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth + if glyf[name].isComposite() else 0, + name)) + for glyphname in glyphnames: + variations = gvar.variations[glyphname] + coordinates,_ = _GetCoordinates(varfont, glyphname) + origCoords, endPts = None, None + for var in variations: + scalar = supportScalar(loc, var.axes) + if not scalar: continue + delta = var.coordinates + if None in delta: + if origCoords is None: + origCoords,control = _GetCoordinates(varfont, glyphname) + endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) + delta = iup_delta(delta, origCoords, endPts) + coordinates += GlyphCoordinates(delta) * scalar + _SetCoordinates(varfont, glyphname, coordinates) + else: + glyf = None if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") @@ -98,6 +216,20 @@ def instantiateVariableFont(varfont, location, inplace=False): for i, delta in deltas.items(): cvt[i] += otRound(delta) + if 'CFF2' in varfont: + log.info("Mutating CFF2 table") + glyphOrder = varfont.getGlyphOrder() + CFF2 = varfont['CFF2'] + topDict = CFF2.cff.topDictIndex[0] + vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc) + interpolateFromDeltas = vsInstancer.interpolateFromDeltas + interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas) + CFF2.desubroutinize() + interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder) + interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc) + del topDict.rawDict['VarStore'] + del topDict.VarStore + if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table @@ -114,12 +246,98 @@ def instantiateVariableFont(varfont, location, inplace=False): setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) - if 'GDEF' in varfont: + log.info("Mutating FeatureVariations") + for tableTag in 'GSUB','GPOS': + if not tableTag in varfont: + continue + table = varfont[tableTag].table + if not hasattr(table, 'FeatureVariations'): + continue + variations = table.FeatureVariations + for record in variations.FeatureVariationRecord: + applies = True + for condition in record.ConditionSet.ConditionTable: + if condition.Format == 1: + axisIdx = condition.AxisIndex + axisTag = fvar.axes[axisIdx].axisTag + Min = condition.FilterRangeMinValue + Max = condition.FilterRangeMaxValue + v = loc[axisTag] + if not (Min <= v <= Max): + applies = False + else: + applies = False + if not applies: + break + + if applies: + assert record.FeatureTableSubstitution.Version == 0x00010000 + for rec in record.FeatureTableSubstitution.SubstitutionRecord: + table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature + break + del table.FeatureVariations + + if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003: log.info("Mutating GDEF/GPOS/GSUB tables") + gdef = varfont['GDEF'].table + instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc) + merger = MutatorMerger(varfont, loc) + merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS']) + + # Downgrade GDEF. + del gdef.VarStore + gdef.Version = 0x00010002 + if gdef.MarkGlyphSetsDef is None: + del gdef.MarkGlyphSetsDef + gdef.Version = 0x00010000 + + if not (gdef.LigCaretList or + gdef.MarkAttachClassDef or + gdef.GlyphClassDef or + gdef.AttachList or + (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)): + del varfont['GDEF'] + + addidef = False + if glyf: + for glyph in glyf.glyphs.values(): + if hasattr(glyph, "program"): + instructions = glyph.program.getAssembly() + # If GETVARIATION opcode is used in bytecode of any glyph add IDEF + addidef = any(op.startswith("GETVARIATION") for op in instructions) + if addidef: + break + if addidef: + log.info("Adding IDEF to fpgm table for GETVARIATION opcode") + asm = [] + if 'fpgm' in varfont: + fpgm = varfont['fpgm'] + asm = fpgm.program.getAssembly() + else: + fpgm = newTable('fpgm') + fpgm.program = ttProgram.Program() + varfont['fpgm'] = fpgm + asm.append("PUSHB[000] 145") + asm.append("IDEF[ ]") + args = [str(len(loc))] + for a in fvar.axes: + args.append(str(floatToFixed(loc[a.axisTag], 14))) + asm.append("NPUSHW[ ] " + ' '.join(args)) + asm.append("ENDF[ ]") + fpgm.program.fromAssembly(asm) - log.info("Building interpolated tables") - merger.instantiate() + # Change maxp attributes as IDEF is added + if 'maxp' in varfont: + maxp = varfont['maxp'] + if hasattr(maxp, "maxInstructionDefs"): + maxp.maxInstructionDefs += 1 + else: + setattr(maxp, "maxInstructionDefs", 1) + if hasattr(maxp, "maxStackElements"): + maxp.maxStackElements = max(len(loc), maxp.maxStackElements) + else: + setattr(maxp, "maxInstructionDefs", len(loc)) if 'name' in varfont: log.info("Pruning name table") diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py index a1ca7df6..66d0c95a 100644 --- a/Lib/fontTools/varLib/varStore.py +++ b/Lib/fontTools/varLib/varStore.py @@ -4,8 +4,7 @@ from fontTools.misc.fixedTools import otRound from fontTools.ttLib.tables import otTables as ot from fontTools.varLib.models import supportScalar from fontTools.varLib.builder import (buildVarRegionList, buildVarStore, - buildVarRegion, buildVarData, - VarData_CalculateNumShorts) + buildVarRegion, buildVarData) from functools import partial from collections import defaultdict from array import array @@ -24,25 +23,36 @@ class OnlineVarStoreBuilder(object): self._store = buildVarStore(self._regionList, []) self._data = None self._model = None + self._supports = None + self._varDataIndices = {} + self._varDataCaches = {} self._cache = {} def setModel(self, model): + self.setSupports(model.supports) self._model = model - self._cache = {} # Empty cached items + + def setSupports(self, supports): + self._model = None + self._supports = list(supports) + if not self._supports[0]: + del self._supports[0] # Drop base master support + self._cache = {} + self._data = None def finish(self, optimize=True): self._regionList.RegionCount = len(self._regionList.Region) self._store.VarDataCount = len(self._store.VarData) for data in self._store.VarData: data.ItemCount = len(data.Item) - VarData_CalculateNumShorts(data, optimize) + data.calculateNumShorts(optimize=optimize) return self._store def _add_VarData(self): regionMap = self._regionMap regionList = self._regionList - regions = self._model.supports[1:] + regions = self._supports regionIndices = [] for region in regions: key = _getLocationKey(region) @@ -53,17 +63,46 @@ class OnlineVarStoreBuilder(object): regionList.Region.append(varRegion) regionIndices.append(idx) - data = self._data = buildVarData(regionIndices, [], optimize=False) - self._outer = len(self._store.VarData) - self._store.VarData.append(data) + # Check if we have one already... + key = tuple(regionIndices) + varDataIdx = self._varDataIndices.get(key) + if varDataIdx is not None: + self._outer = varDataIdx + self._data = self._store.VarData[varDataIdx] + self._cache = self._varDataCaches[key] + if len(self._data.Item) == 0xFFF: + # This is full. Need new one. + varDataIdx = None + + if varDataIdx is None: + self._data = buildVarData(regionIndices, [], optimize=False) + self._outer = len(self._store.VarData) + self._store.VarData.append(self._data) + self._varDataIndices[key] = self._outer + if key not in self._varDataCaches: + self._varDataCaches[key] = {} + self._cache = self._varDataCaches[key] + def storeMasters(self, master_values): - deltas = [otRound(d) for d in self._model.getDeltas(master_values)] - base = deltas.pop(0) - deltas = tuple(deltas) + deltas = self._model.getDeltas(master_values) + base = otRound(deltas.pop(0)) + return base, self.storeDeltas(deltas) + + def storeDeltas(self, deltas): + # Pity that this exists here, since VarData_addItem + # does the same. But to look into our cache, it's + # good to adjust deltas here as well... + deltas = [otRound(d) for d in deltas] + if len(deltas) == len(self._supports) + 1: + deltas = tuple(deltas[1:]) + else: + assert len(deltas) == len(self._supports) + deltas = tuple(deltas) + varIdx = self._cache.get(deltas) if varIdx is not None: - return base, varIdx + return varIdx if not self._data: self._add_VarData() @@ -71,18 +110,34 @@ class OnlineVarStoreBuilder(object): if inner == 0xFFFF: # Full array. Start new one. self._add_VarData() - return self.storeMasters(master_values) - self._data.Item.append(deltas) + return self.storeDeltas(deltas) + self._data.addItem(deltas) varIdx = (self._outer << 16) + inner self._cache[deltas] = varIdx - return base, varIdx + return varIdx + +def VarData_addItem(self, deltas): + deltas = [otRound(d) for d in deltas] + + countUs = self.VarRegionCount + countThem = len(deltas) + if countUs + 1 == countThem: + deltas = tuple(deltas[1:]) + else: + assert countUs == countThem, (countUs, countThem) + deltas = tuple(deltas) + self.Item.append(list(deltas)) + self.ItemCount = len(self.Item) +ot.VarData.addItem = VarData_addItem def VarRegion_get_support(self, fvar_axes): return {fvar_axes[i].axisTag: (reg.StartCoord,reg.PeakCoord,reg.EndCoord) for i,reg in enumerate(self.VarRegionAxis)} +ot.VarRegion.get_support = VarRegion_get_support + class VarStoreInstancer(object): def __init__(self, varstore, fvar_axes, location={}): @@ -102,23 +157,31 @@ class VarStoreInstancer(object): def _getScalar(self, regionIdx): scalar = self._scalars.get(regionIdx) if scalar is None: - support = VarRegion_get_support(self._regions[regionIdx], self.fvar_axes) + support = self._regions[regionIdx].get_support(self.fvar_axes) scalar = supportScalar(self.location, support) self._scalars[regionIdx] = scalar return scalar - def __getitem__(self, varidx): + @staticmethod + def interpolateFromDeltasAndScalars(deltas, scalars): + delta = 0. + for d,s in zip(deltas, scalars): + if not s: continue + delta += d * s + return delta + def __getitem__(self, varidx): major, minor = varidx >> 16, varidx & 0xFFFF - varData = self._varData scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex] - deltas = varData[major].Item[minor] - delta = 0. - for d,s in zip(deltas, scalars): - delta += d * s - return delta + return self.interpolateFromDeltasAndScalars(deltas, scalars) + + def interpolateFromDeltas(self, varDataIndex, deltas): + varData = self._varData + scalars = [self._getScalar(ri) for ri in + varData[varDataIndex].VarRegionIndex] + return self.interpolateFromDeltasAndScalars(deltas, scalars) # @@ -149,7 +212,7 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True): usedMinors = used.get(major) if usedMinors is None: continue - newMajor = varDataMap[major] = len(newVarData) + newMajor = len(newVarData) newVarData.append(data) items = data.Item @@ -162,8 +225,7 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True): data.Item = newItems data.ItemCount = len(data.Item) - if optimize: - VarData_CalculateNumShorts(data) + data.calculateNumShorts(optimize=optimize) self.VarData = newVarData self.VarDataCount = len(self.VarData) @@ -201,27 +263,26 @@ def VarStore_prune_regions(self): ot.VarStore.prune_regions = VarStore_prune_regions -def _visit(self, objType, func): - """Recurse down from self, if type of an object is objType, - call func() on it. Only works for otData-style classes.""" +def _visit(self, func): + """Recurse down from self, if type of an object is ot.Device, + call func() on it. Works on otData-style classes.""" - if type(self) == objType: + if type(self) == ot.Device: func(self) - return # We don't recurse down; don't need to. - if isinstance(self, list): + elif isinstance(self, list): for that in self: - _visit(that, objType, func) + _visit(that, func) - if hasattr(self, 'getConverters'): + elif hasattr(self, 'getConverters') and not hasattr(self, 'postRead'): for conv in self.getConverters(): that = getattr(self, conv.name, None) if that is not None: - _visit(that, objType, func) + _visit(that, func) - if isinstance(self, ot.ValueRecord): + elif isinstance(self, ot.ValueRecord): for that in self.__dict__.values(): - _visit(that, objType, func) + _visit(that, func) def _Device_recordVarIdx(self, s): """Add VarIdx in this Device table (if any) to the set s.""" @@ -230,13 +291,13 @@ def _Device_recordVarIdx(self, s): def Object_collect_device_varidxes(self, varidxes): adder = partial(_Device_recordVarIdx, s=varidxes) - _visit(self, ot.Device, adder) + _visit(self, adder) ot.GDEF.collect_device_varidxes = Object_collect_device_varidxes ot.GPOS.collect_device_varidxes = Object_collect_device_varidxes def _Device_mapVarIdx(self, mapping, done): - """Add VarIdx in this Device table (if any) to the set s.""" + """Map VarIdx in this Device table (if any) through mapping.""" if id(self) in done: return done.add(id(self)) @@ -247,7 +308,7 @@ def _Device_mapVarIdx(self, mapping, done): def Object_remap_device_varidxes(self, varidxes_map): mapper = partial(_Device_mapVarIdx, mapping=varidxes_map, done=set()) - _visit(self, ot.Device, mapper) + _visit(self, mapper) ot.GDEF.remap_device_varidxes = Object_remap_device_varidxes ot.GPOS.remap_device_varidxes = Object_remap_device_varidxes @@ -464,7 +525,7 @@ def VarStore_optimize(self): self.VarDataCount = len(self.VarData) for data in self.VarData: data.ItemCount = len(data.Item) - VarData_CalculateNumShorts(data) + data.optimize() return varidx_map diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO index ee3a3772..32f95682 100644 --- a/Lib/fonttools.egg-info/PKG-INFO +++ b/Lib/fonttools.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: fonttools -Version: 3.31.0 +Version: 3.35.0 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -28,6 +28,13 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4 or later. + **NOTE** After January 1 2019, until no later than June 30 2019, the support + for *Python 2.7* will be limited to only bug fixes, and no new features will + be added to the ``py27`` branch. The upcoming FontTools 4.x series will require + *Python 3.5* or above. You can read more `here <https://python3statement.org>`__ + and `here <https://github.com/fonttools/fonttools/issues/765>`__ for the + reasons behind this decision. + The package is listed in the Python Package Index (PyPI), so you can install it with `pip <https://pip.pypa.io>`__: @@ -255,6 +262,14 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St *Extra:* ``interpolatable`` + - ``Lib/fontTools/varLib/plot.py`` + + Module for visualizing DesignSpaceDocument and resulting VariationModel. + + * `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library. + + *Extra:* ``plot`` + - ``Lib/fontTools/misc/symfont.py`` Advanced module for symbolic font statistics analysis; it requires: @@ -314,7 +329,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St installed ``fontTools`` package, or the first one found in the ``PYTHONPATH``. - You can also use `tox <https://testrun.org/tox/latest/>`__ to + You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to automatically run tests on different Python versions in isolated virtual environments. @@ -415,6 +430,82 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St Changelog ~~~~~~~~~ + 3.35.0 (released 2019-01-07) + ---------------------------- + + - [psCharStrings] In ``encodeFloat`` function, use float's "general format" with + 8 digits of precision (i.e. ``%8g``) instead of ``str()``. This works around + a macOS rendering issue when real numbers in CFF table are too long, and + also makes sure that floats are encoded with the same precision in python 2.7 + and 3.x (#1430, googlei18n/ufo2ft#306). + - [_n_a_m_e/fontBuilder] Make ``_n_a_m_e_table.addMultilingualName`` also add + Macintosh (platformID=1) names by default. Added options to ``FontBuilder`` + ``setupNameTable`` method to optionally disable Macintosh or Windows names. + (#1359, #1431). + - [varLib] Make ``build`` optionally accept a ``DesignSpaceDocument`` object, + instead of a designspace file path. The caller can now set the ``font`` + attribute of designspace's sources to a TTFont object, thus allowing to + skip filenames manipulation altogether (#1416, #1425). + - [sfnt] Allow SFNTReader objects to be deep-copied. + - Require typing>=3.6.4 on py27 to fix issue with singledispatch (#1423). + - [designspaceLib/t1Lib/macRes] Fixed some cases where pathlib.Path objects were + not accepted (#1421). + - [varLib] Fixed merging of multiple PairPosFormat2 subtables (#1411). + - [varLib] The default STAT table version is now set to 1.1, to improve + compatibility with legacy applications (#1413). + + 3.34.2 (released 2018-12-17) + ---------------------------- + + - [merge] Fixed AssertionError when none of the script tables in GPOS/GSUB have + a DefaultLangSys record (#1408, 135a4a1). + + 3.34.1 (released 2018-12-17) + ---------------------------- + + - [varLib] Work around macOS rendering issue for composites without gvar entry (#1381). + + 3.34.0 (released 2018-12-14) + ---------------------------- + + - [varLib] Support generation of CFF2 variable fonts. ``model.reorderMasters()`` + now supports arbitrary mapping. Fix handling of overlapping ranges for feature + variations (#1400). + - [cffLib, subset] Code clean-up and fixing related to CFF2 support. + - [ttLib.tables.ttProgram] Use raw strings for regex patterns (#1389). + - [fontbuilder] Initial support for building CFF2 fonts. Set CFF's + ``FontMatrix`` automatically from unitsPerEm. + - [plistLib] Accept the more general ``collections.Mapping`` instead of the + specific ``dict`` class to support custom data classes that should serialize + to dictionaries. + + 3.33.0 (released 2018-11-30) + ---------------------------- + - [subset] subsetter bug fix with variable fonts. + - [varLib.featureVar] Improve FeatureVariations generation with many rules. + - [varLib] Enable sparse masters when building variable fonts: + https://github.com/fonttools/fonttools/pull/1368#issuecomment-437257368 + - [varLib.mutator] Add IDEF for GETVARIATION opcode, for handling hints in an + instance. + - [ttLib] Ignore the length of kern table subtable format 0 + + 3.32.0 (released 2018-11-01) + ---------------------------- + + - [ufoLib] Make ``UFOWriter`` a subclass of ``UFOReader``, and use mixins + for shared methods (#1344). + - [featureVars] Fixed normalization error when a condition's minimum/maximum + attributes are missing in designspace ``<rule>`` (#1366). + - [setup.py] Added ``[plot]`` to extras, to optionally install ``matplotlib``, + needed to use the ``fonTools.varLib.plot`` module. + - [varLib] Take total bounding box into account when resolving model (7ee81c8). + If multiple axes have the same range ratio, cut across both (62003f4). + - [subset] Don't error if ``STAT`` has no ``AxisValue`` tables. + - [fontBuilder] Added a new submodule which contains a ``FontBuilder`` wrapper + class around ``TTFont`` that makes it easier to create a working TTF or OTF + font from scratch with code. NOTE: the API is still experimental and may + change in future versions. + 3.31.0 (released 2018-10-21) ---------------------------- @@ -1491,11 +1582,13 @@ Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Text Processing :: Fonts Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion -Provides-Extra: type1 +Provides-Extra: all +Provides-Extra: interpolatable +Provides-Extra: woff Provides-Extra: lxml Provides-Extra: unicode -Provides-Extra: symfont -Provides-Extra: all +Provides-Extra: graphite Provides-Extra: ufo -Provides-Extra: woff -Provides-Extra: interpolatable +Provides-Extra: type1 +Provides-Extra: plot +Provides-Extra: symfont diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt index ef8491df..04db0956 100644 --- a/Lib/fonttools.egg-info/SOURCES.txt +++ b/Lib/fonttools.egg-info/SOURCES.txt @@ -87,6 +87,7 @@ Lib/fontTools/__init__.py Lib/fontTools/__main__.py Lib/fontTools/afmLib.py Lib/fontTools/agl.py +Lib/fontTools/fontBuilder.py Lib/fontTools/inspect.py Lib/fontTools/merge.py Lib/fontTools/ttx.py @@ -111,11 +112,13 @@ Lib/fontTools/misc/arrayTools.py Lib/fontTools/misc/bezierTools.py Lib/fontTools/misc/classifyTools.py Lib/fontTools/misc/cliTools.py +Lib/fontTools/misc/dictTools.py Lib/fontTools/misc/eexec.py Lib/fontTools/misc/encodingTools.py Lib/fontTools/misc/etree.py Lib/fontTools/misc/filenames.py Lib/fontTools/misc/fixedTools.py +Lib/fontTools/misc/intTools.py Lib/fontTools/misc/loggingTools.py Lib/fontTools/misc/macCreatorType.py Lib/fontTools/misc/macRes.py @@ -159,6 +162,7 @@ Lib/fontTools/pens/ttGlyphPen.py Lib/fontTools/pens/wxPen.py Lib/fontTools/subset/__init__.py Lib/fontTools/subset/__main__.py +Lib/fontTools/subset/cff.py Lib/fontTools/svgLib/__init__.py Lib/fontTools/svgLib/path/__init__.py Lib/fontTools/svgLib/path/parser.py @@ -285,6 +289,7 @@ Lib/fontTools/unicodedata/__init__.py Lib/fontTools/varLib/__init__.py Lib/fontTools/varLib/__main__.py Lib/fontTools/varLib/builder.py +Lib/fontTools/varLib/cff.py Lib/fontTools/varLib/featureVars.py Lib/fontTools/varLib/interpolatable.py Lib/fontTools/varLib/interpolate_layout.py @@ -520,6 +525,11 @@ Tests/feaLib/data/include/include6.fea Tests/feaLib/data/include/includemissingfile.fea Tests/feaLib/data/include/includeself.fea Tests/feaLib/data/include/subdir/include2.fea +Tests/fontBuilder/fontBuilder_test.py +Tests/fontBuilder/data/test.otf.ttx +Tests/fontBuilder/data/test.ttf.ttx +Tests/fontBuilder/data/test_var.otf.ttx +Tests/fontBuilder/data/test_var.ttf.ttx Tests/misc/arrayTools_test.py Tests/misc/bezierTools_test.py Tests/misc/classifyTools_test.py @@ -609,6 +619,7 @@ Tests/pens/basePen_test.py Tests/pens/boundsPen_test.py Tests/pens/perimeterPen_test.py Tests/pens/pointInsidePen_test.py +Tests/pens/pointPen_test.py Tests/pens/recordingPen_test.py Tests/pens/reverseContourPen_test.py Tests/pens/t2CharStringPen_test.py @@ -1383,6 +1394,7 @@ Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/fo Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt Tests/varLib/__init__.py Tests/varLib/builder_test.py +Tests/varLib/featureVars_test.py Tests/varLib/interpolatable_test.py Tests/varLib/interpolate_layout_test.py Tests/varLib/models_test.py @@ -1392,10 +1404,17 @@ Tests/varLib/data/Build.designspace Tests/varLib/data/BuildAvarEmptyAxis.designspace Tests/varLib/data/BuildAvarIdentityMaps.designspace Tests/varLib/data/BuildAvarSingleAxis.designspace +Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace Tests/varLib/data/FeatureVars.designspace Tests/varLib/data/InterpolateLayout.designspace Tests/varLib/data/InterpolateLayout2.designspace Tests/varLib/data/InterpolateLayout3.designspace +Tests/varLib/data/TestCFF2.designspace +Tests/varLib/data/TestCFF2VF.otf +Tests/varLib/data/master_cff2/TestCFF2_Black.otf +Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otf +Tests/varLib/data/master_cff2/TestCFF2_Regular.otf +Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master0.ttx Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master1.ttx Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master0.ttx @@ -1413,6 +1432,8 @@ Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedSemiBold.tt Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Light.ttx Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Regular.ttx Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-SemiBold.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/features.fea Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/fontinfo.plist @@ -1635,11 +1656,53 @@ Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/n.glif Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/o.glif Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/s.glif Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif +Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif +Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif Tests/varLib/data/test_results/Build.ttx Tests/varLib/data/test_results/BuildAvarEmptyAxis.ttx Tests/varLib/data/test_results/BuildAvarIdentityMaps.ttx Tests/varLib/data/test_results/BuildAvarSingleAxis.ttx +Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx Tests/varLib/data/test_results/BuildMain.ttx +Tests/varLib/data/test_results/BuildTestCFF2.ttx Tests/varLib/data/test_results/FeatureVars.ttx Tests/varLib/data/test_results/InterpolateLayout.ttx Tests/varLib/data/test_results/InterpolateLayout2.ttx @@ -1664,7 +1727,9 @@ Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx Tests/varLib/data/test_results/InterpolateLayoutMain.ttx +Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx Tests/varLib/data/test_results/Mutator.ttx +Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx Tests/varLib/data/test_results/Mutator_IUP-instance.ttx Tests/voltLib/lexer_test.py Tests/voltLib/parser_test.py
\ No newline at end of file diff --git a/Lib/fonttools.egg-info/requires.txt b/Lib/fonttools.egg-info/requires.txt index c7eb2e13..9f927643 100644 --- a/Lib/fonttools.egg-info/requires.txt +++ b/Lib/fonttools.egg-info/requires.txt @@ -3,6 +3,8 @@ fs<3,>=2.1.1 lxml<5,>=4.0 zopfli>=0.1.4 +lz4>=1.7.4.2 +matplotlib sympy [all:platform_python_implementation != "PyPy"] @@ -16,6 +18,7 @@ munkres [all:python_version < "3.4"] enum34>=1.1.6 singledispatch>=3.4.0.3 +typing>=3.6.4 [all:python_version < "3.7" and platform_python_implementation != "PyPy"] unicodedata2>=11.0.0 @@ -23,6 +26,9 @@ unicodedata2>=11.0.0 [all:sys_platform == "darwin"] xattr +[graphite] +lz4>=1.7.4.2 + [interpolatable] [interpolatable:platform_python_implementation != "PyPy"] @@ -36,6 +42,10 @@ lxml<5,>=4.0 [lxml:python_version < "3.4"] singledispatch>=3.4.0.3 +typing>=3.6.4 + +[plot] +matplotlib [symfont] sympy @@ -7,12 +7,12 @@ third_party { } url { type: ARCHIVE - value: "https://github.com/fonttools/fonttools/releases/download/3.31.0/fonttools-3.31.0.zip" + value: "https://github.com/fonttools/fonttools/releases/download/3.35.0/fonttools-3.35.0.zip" } - version: "3.31.0" + version: "3.35.0" last_upgrade_date { - year: 2018 - month: 10 - day: 30 + year: 2019 + month: 1 + day: 8 } } diff --git a/MetaTools/buildTableList.py b/MetaTools/buildTableList.py index de8a039d..eb9fb858 100755..100644 --- a/MetaTools/buildTableList.py +++ b/MetaTools/buildTableList.py @@ -30,9 +30,9 @@ modules.sort() tables.sort() -file = open(os.path.join(tablesDir, "__init__.py"), "w") +with open(os.path.join(tablesDir, "__init__.py"), "w") as file: -file.write(''' + file.write(''' from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * @@ -45,21 +45,20 @@ def _moduleFinderHint(): """ ''') -for module in modules: - file.write("\tfrom . import %s\n" % module) + for module in modules: + file.write("\tfrom . import %s\n" % module) -file.write(''' + file.write(''' if __name__ == "__main__": import doctest, sys sys.exit(doctest.testmod().failed) ''') -file.close() - begin = ".. begin table list\n.. code::\n" end = ".. end table list" -doc = open(docFile).read() +with open(docFile) as f: + doc = f.read() beginPos = doc.find(begin) assert beginPos > 0 beginPos = beginPos + len(begin) + 1 @@ -70,4 +69,5 @@ blockquote = "\n".join(" "*4 + line for line in lines) + "\n" doc = doc[:beginPos] + blockquote + doc[endPos:] -open(docFile, "w").write(doc) +with open(docFile, "w") as f: + f.write(doc) diff --git a/MetaTools/buildUCD.py b/MetaTools/buildUCD.py index 12bd58f1..12bd58f1 100755..100644 --- a/MetaTools/buildUCD.py +++ b/MetaTools/buildUCD.py diff --git a/MetaTools/roundTrip.py b/MetaTools/roundTrip.py index 122b39b4..648bc9d9 100755..100644 --- a/MetaTools/roundTrip.py +++ b/MetaTools/roundTrip.py @@ -74,23 +74,22 @@ def main(args): if not files: usage() - report = open("report.txt", "a+") - options = ttx.Options(rawOptions, len(files)) - for ttFile in files: - try: - roundTrip(ttFile, options, report) - except KeyboardInterrupt: - print("(Cancelled)") - break - except: - print("*** round tripping aborted ***") - traceback.print_exc() - report.write("=============================================================\n") - report.write(" An exception occurred while round tripping") - report.write(" \"%s\"\n" % ttFile) - traceback.print_exc(file=report) - report.write("-------------------------------------------------------------\n") - report.close() + with open("report.txt", "a+") as report: + options = ttx.Options(rawOptions, len(files)) + for ttFile in files: + try: + roundTrip(ttFile, options, report) + except KeyboardInterrupt: + print("(Cancelled)") + break + except: + print("*** round tripping aborted ***") + traceback.print_exc() + report.write("=============================================================\n") + report.write(" An exception occurred while round tripping") + report.write(" \"%s\"\n" % ttFile) + traceback.print_exc(file=report) + report.write("-------------------------------------------------------------\n") main(sys.argv[1:]) @@ -1,3 +1,79 @@ +3.35.0 (released 2019-01-07) +---------------------------- + +- [psCharStrings] In ``encodeFloat`` function, use float's "general format" with + 8 digits of precision (i.e. ``%8g``) instead of ``str()``. This works around + a macOS rendering issue when real numbers in CFF table are too long, and + also makes sure that floats are encoded with the same precision in python 2.7 + and 3.x (#1430, googlei18n/ufo2ft#306). +- [_n_a_m_e/fontBuilder] Make ``_n_a_m_e_table.addMultilingualName`` also add + Macintosh (platformID=1) names by default. Added options to ``FontBuilder`` + ``setupNameTable`` method to optionally disable Macintosh or Windows names. + (#1359, #1431). +- [varLib] Make ``build`` optionally accept a ``DesignSpaceDocument`` object, + instead of a designspace file path. The caller can now set the ``font`` + attribute of designspace's sources to a TTFont object, thus allowing to + skip filenames manipulation altogether (#1416, #1425). +- [sfnt] Allow SFNTReader objects to be deep-copied. +- Require typing>=3.6.4 on py27 to fix issue with singledispatch (#1423). +- [designspaceLib/t1Lib/macRes] Fixed some cases where pathlib.Path objects were + not accepted (#1421). +- [varLib] Fixed merging of multiple PairPosFormat2 subtables (#1411). +- [varLib] The default STAT table version is now set to 1.1, to improve + compatibility with legacy applications (#1413). + +3.34.2 (released 2018-12-17) +---------------------------- + +- [merge] Fixed AssertionError when none of the script tables in GPOS/GSUB have + a DefaultLangSys record (#1408, 135a4a1). + +3.34.1 (released 2018-12-17) +---------------------------- + +- [varLib] Work around macOS rendering issue for composites without gvar entry (#1381). + +3.34.0 (released 2018-12-14) +---------------------------- + +- [varLib] Support generation of CFF2 variable fonts. ``model.reorderMasters()`` + now supports arbitrary mapping. Fix handling of overlapping ranges for feature + variations (#1400). +- [cffLib, subset] Code clean-up and fixing related to CFF2 support. +- [ttLib.tables.ttProgram] Use raw strings for regex patterns (#1389). +- [fontbuilder] Initial support for building CFF2 fonts. Set CFF's + ``FontMatrix`` automatically from unitsPerEm. +- [plistLib] Accept the more general ``collections.Mapping`` instead of the + specific ``dict`` class to support custom data classes that should serialize + to dictionaries. + +3.33.0 (released 2018-11-30) +---------------------------- +- [subset] subsetter bug fix with variable fonts. +- [varLib.featureVar] Improve FeatureVariations generation with many rules. +- [varLib] Enable sparse masters when building variable fonts: + https://github.com/fonttools/fonttools/pull/1368#issuecomment-437257368 +- [varLib.mutator] Add IDEF for GETVARIATION opcode, for handling hints in an + instance. +- [ttLib] Ignore the length of kern table subtable format 0 + +3.32.0 (released 2018-11-01) +---------------------------- + +- [ufoLib] Make ``UFOWriter`` a subclass of ``UFOReader``, and use mixins + for shared methods (#1344). +- [featureVars] Fixed normalization error when a condition's minimum/maximum + attributes are missing in designspace ``<rule>`` (#1366). +- [setup.py] Added ``[plot]`` to extras, to optionally install ``matplotlib``, + needed to use the ``fonTools.varLib.plot`` module. +- [varLib] Take total bounding box into account when resolving model (7ee81c8). + If multiple axes have the same range ratio, cut across both (62003f4). +- [subset] Don't error if ``STAT`` has no ``AxisValue`` tables. +- [fontBuilder] Added a new submodule which contains a ``FontBuilder`` wrapper + class around ``TTFont`` that makes it easier to create a working TTF or OTF + font from scratch with code. NOTE: the API is still experimental and may + change in future versions. + 3.31.0 (released 2018-10-21) ---------------------------- @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: fonttools -Version: 3.31.0 +Version: 3.35.0 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -28,6 +28,13 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4 or later. + **NOTE** After January 1 2019, until no later than June 30 2019, the support + for *Python 2.7* will be limited to only bug fixes, and no new features will + be added to the ``py27`` branch. The upcoming FontTools 4.x series will require + *Python 3.5* or above. You can read more `here <https://python3statement.org>`__ + and `here <https://github.com/fonttools/fonttools/issues/765>`__ for the + reasons behind this decision. + The package is listed in the Python Package Index (PyPI), so you can install it with `pip <https://pip.pypa.io>`__: @@ -255,6 +262,14 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St *Extra:* ``interpolatable`` + - ``Lib/fontTools/varLib/plot.py`` + + Module for visualizing DesignSpaceDocument and resulting VariationModel. + + * `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library. + + *Extra:* ``plot`` + - ``Lib/fontTools/misc/symfont.py`` Advanced module for symbolic font statistics analysis; it requires: @@ -314,7 +329,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St installed ``fontTools`` package, or the first one found in the ``PYTHONPATH``. - You can also use `tox <https://testrun.org/tox/latest/>`__ to + You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to automatically run tests on different Python versions in isolated virtual environments. @@ -415,6 +430,82 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St Changelog ~~~~~~~~~ + 3.35.0 (released 2019-01-07) + ---------------------------- + + - [psCharStrings] In ``encodeFloat`` function, use float's "general format" with + 8 digits of precision (i.e. ``%8g``) instead of ``str()``. This works around + a macOS rendering issue when real numbers in CFF table are too long, and + also makes sure that floats are encoded with the same precision in python 2.7 + and 3.x (#1430, googlei18n/ufo2ft#306). + - [_n_a_m_e/fontBuilder] Make ``_n_a_m_e_table.addMultilingualName`` also add + Macintosh (platformID=1) names by default. Added options to ``FontBuilder`` + ``setupNameTable`` method to optionally disable Macintosh or Windows names. + (#1359, #1431). + - [varLib] Make ``build`` optionally accept a ``DesignSpaceDocument`` object, + instead of a designspace file path. The caller can now set the ``font`` + attribute of designspace's sources to a TTFont object, thus allowing to + skip filenames manipulation altogether (#1416, #1425). + - [sfnt] Allow SFNTReader objects to be deep-copied. + - Require typing>=3.6.4 on py27 to fix issue with singledispatch (#1423). + - [designspaceLib/t1Lib/macRes] Fixed some cases where pathlib.Path objects were + not accepted (#1421). + - [varLib] Fixed merging of multiple PairPosFormat2 subtables (#1411). + - [varLib] The default STAT table version is now set to 1.1, to improve + compatibility with legacy applications (#1413). + + 3.34.2 (released 2018-12-17) + ---------------------------- + + - [merge] Fixed AssertionError when none of the script tables in GPOS/GSUB have + a DefaultLangSys record (#1408, 135a4a1). + + 3.34.1 (released 2018-12-17) + ---------------------------- + + - [varLib] Work around macOS rendering issue for composites without gvar entry (#1381). + + 3.34.0 (released 2018-12-14) + ---------------------------- + + - [varLib] Support generation of CFF2 variable fonts. ``model.reorderMasters()`` + now supports arbitrary mapping. Fix handling of overlapping ranges for feature + variations (#1400). + - [cffLib, subset] Code clean-up and fixing related to CFF2 support. + - [ttLib.tables.ttProgram] Use raw strings for regex patterns (#1389). + - [fontbuilder] Initial support for building CFF2 fonts. Set CFF's + ``FontMatrix`` automatically from unitsPerEm. + - [plistLib] Accept the more general ``collections.Mapping`` instead of the + specific ``dict`` class to support custom data classes that should serialize + to dictionaries. + + 3.33.0 (released 2018-11-30) + ---------------------------- + - [subset] subsetter bug fix with variable fonts. + - [varLib.featureVar] Improve FeatureVariations generation with many rules. + - [varLib] Enable sparse masters when building variable fonts: + https://github.com/fonttools/fonttools/pull/1368#issuecomment-437257368 + - [varLib.mutator] Add IDEF for GETVARIATION opcode, for handling hints in an + instance. + - [ttLib] Ignore the length of kern table subtable format 0 + + 3.32.0 (released 2018-11-01) + ---------------------------- + + - [ufoLib] Make ``UFOWriter`` a subclass of ``UFOReader``, and use mixins + for shared methods (#1344). + - [featureVars] Fixed normalization error when a condition's minimum/maximum + attributes are missing in designspace ``<rule>`` (#1366). + - [setup.py] Added ``[plot]`` to extras, to optionally install ``matplotlib``, + needed to use the ``fonTools.varLib.plot`` module. + - [varLib] Take total bounding box into account when resolving model (7ee81c8). + If multiple axes have the same range ratio, cut across both (62003f4). + - [subset] Don't error if ``STAT`` has no ``AxisValue`` tables. + - [fontBuilder] Added a new submodule which contains a ``FontBuilder`` wrapper + class around ``TTFont`` that makes it easier to create a working TTF or OTF + font from scratch with code. NOTE: the API is still experimental and may + change in future versions. + 3.31.0 (released 2018-10-21) ---------------------------- @@ -1491,11 +1582,13 @@ Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Text Processing :: Fonts Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion -Provides-Extra: type1 +Provides-Extra: all +Provides-Extra: interpolatable +Provides-Extra: woff Provides-Extra: lxml Provides-Extra: unicode -Provides-Extra: symfont -Provides-Extra: all +Provides-Extra: graphite Provides-Extra: ufo -Provides-Extra: woff -Provides-Extra: interpolatable +Provides-Extra: type1 +Provides-Extra: plot +Provides-Extra: symfont @@ -18,6 +18,13 @@ Installation FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4 or later. +**NOTE** After January 1 2019, until no later than June 30 2019, the support +for *Python 2.7* will be limited to only bug fixes, and no new features will +be added to the ``py27`` branch. The upcoming FontTools 4.x series will require +*Python 3.5* or above. You can read more `here <https://python3statement.org>`__ +and `here <https://github.com/fonttools/fonttools/issues/765>`__ for the +reasons behind this decision. + The package is listed in the Python Package Index (PyPI), so you can install it with `pip <https://pip.pypa.io>`__: @@ -245,6 +252,14 @@ are required to unlock the extra features named "ufo", etc. *Extra:* ``interpolatable`` +- ``Lib/fontTools/varLib/plot.py`` + + Module for visualizing DesignSpaceDocument and resulting VariationModel. + + * `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library. + + *Extra:* ``plot`` + - ``Lib/fontTools/misc/symfont.py`` Advanced module for symbolic font statistics analysis; it requires: @@ -304,7 +319,7 @@ When you run the ``pytest`` command, the tests will run against the installed ``fontTools`` package, or the first one found in the ``PYTHONPATH``. -You can also use `tox <https://testrun.org/tox/latest/>`__ to +You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to automatically run tests on different Python versions in isolated virtual environments. diff --git a/Snippets/cmap-format.py b/Snippets/cmap-format.py index 0cee39c5..0cee39c5 100755..100644 --- a/Snippets/cmap-format.py +++ b/Snippets/cmap-format.py diff --git a/Snippets/interpolate.py b/Snippets/interpolate.py index 7ed822d2..7ed822d2 100755..100644 --- a/Snippets/interpolate.py +++ b/Snippets/interpolate.py diff --git a/Snippets/layout-features.py b/Snippets/layout-features.py index 25522cda..25522cda 100755..100644 --- a/Snippets/layout-features.py +++ b/Snippets/layout-features.py diff --git a/Snippets/otf2ttf.py b/Snippets/otf2ttf.py index 62b4f735..62b4f735 100755..100644 --- a/Snippets/otf2ttf.py +++ b/Snippets/otf2ttf.py diff --git a/Snippets/rename-fonts.py b/Snippets/rename-fonts.py index ddfce103..ddfce103 100755..100644 --- a/Snippets/rename-fonts.py +++ b/Snippets/rename-fonts.py diff --git a/Snippets/subset-fpgm.py b/Snippets/subset-fpgm.py index c20c05fc..c20c05fc 100755..100644 --- a/Snippets/subset-fpgm.py +++ b/Snippets/subset-fpgm.py diff --git a/Snippets/svg2glif.py b/Snippets/svg2glif.py index 2dd64027..2dd64027 100755..100644 --- a/Snippets/svg2glif.py +++ b/Snippets/svg2glif.py diff --git a/Snippets/woff2_compress.py b/Snippets/woff2_compress.py index 689ebdcc..689ebdcc 100755..100644 --- a/Snippets/woff2_compress.py +++ b/Snippets/woff2_compress.py diff --git a/Snippets/woff2_decompress.py b/Snippets/woff2_decompress.py index e7c1beaa..e7c1beaa 100755..100644 --- a/Snippets/woff2_decompress.py +++ b/Snippets/woff2_decompress.py diff --git a/Tests/designspaceLib/designspace_test.py b/Tests/designspaceLib/designspace_test.py index b5d0b70d..2044f00b 100644 --- a/Tests/designspaceLib/designspace_test.py +++ b/Tests/designspaceLib/designspace_test.py @@ -4,6 +4,7 @@ from __future__ import (print_function, division, absolute_import, unicode_literals) import os +import sys import pytest import warnings @@ -237,12 +238,10 @@ def test_unicodes(tmpdir): new.read(testDocPath) new.write(testDocPath2) # compare the file contents - f1 = open(testDocPath, 'r', encoding='utf-8') - t1 = f1.read() - f1.close() - f2 = open(testDocPath2, 'r', encoding='utf-8') - t2 = f2.read() - f2.close() + with open(testDocPath, 'r', encoding='utf-8') as f1: + t1 = f1.read() + with open(testDocPath2, 'r', encoding='utf-8') as f2: + t2 = f2.read() assert t1 == t2 # check the unicode values read from the document assert new.instances[0].glyphs['arrow']['unicodes'] == [100,200,300] @@ -337,12 +336,10 @@ def test_localisedNames(tmpdir): new = DesignSpaceDocument() new.read(testDocPath) new.write(testDocPath2) - f1 = open(testDocPath, 'r', encoding='utf-8') - t1 = f1.read() - f1.close() - f2 = open(testDocPath2, 'r', encoding='utf-8') - t2 = f2.read() - f2.close() + with open(testDocPath, 'r', encoding='utf-8') as f1: + t1 = f1.read() + with open(testDocPath2, 'r', encoding='utf-8') as f2: + t2 = f2.read() assert t1 == t2 @@ -761,14 +758,12 @@ def _addUnwrappedCondition(path): # only for testing, so we can make an invalid designspace file # older designspace files may have conditions that are not wrapped in a conditionset # These can be read into a new conditionset. - f = open(path, 'r', encoding='utf-8') - d = f.read() + with open(path, 'r', encoding='utf-8') as f: + d = f.read() print(d) - f.close() d = d.replace('<rule name="named.rule.1">', '<rule name="named.rule.1">\n\t<condition maximum="22" minimum="33" name="axisName_a" />') - f = open(path, 'w', encoding='utf-8') - f.write(d) - f.close() + with open(path, 'w', encoding='utf-8') as f: + f.write(d) def test_documentLib(tmpdir): # roundtrip test of the document lib with some nested data @@ -791,3 +786,64 @@ def test_documentLib(tmpdir): assert dummyKey in new.lib assert new.lib[dummyKey] == dummyData + +def test_updatePaths(tmpdir): + doc = DesignSpaceDocument() + doc.path = str(tmpdir / "foo" / "bar" / "MyDesignspace.designspace") + + s1 = SourceDescriptor() + doc.addSource(s1) + + doc.updatePaths() + + # expect no changes + assert s1.path is None + assert s1.filename is None + + name1 = "../masters/Source1.ufo" + path1 = posix(str(tmpdir / "foo" / "masters" / "Source1.ufo")) + + s1.path = path1 + s1.filename = None + + doc.updatePaths() + + assert s1.path == path1 + assert s1.filename == name1 # empty filename updated + + name2 = "../masters/Source2.ufo" + s1.filename = name2 + + doc.updatePaths() + + # conflicting filename discarded, path always gets precedence + assert s1.path == path1 + assert s1.filename == "../masters/Source1.ufo" + + s1.path = None + s1.filename = name2 + + doc.updatePaths() + + # expect no changes + assert s1.path is None + assert s1.filename == name2 + + +@pytest.mark.skipif(sys.version_info[:2] < (3, 6), reason="pathlib is only tested on 3.6 and up") +def test_read_with_path_object(): + import pathlib + source = (pathlib.Path(__file__) / "../data/test.designspace").resolve() + assert source.exists() + doc = DesignSpaceDocument() + doc.read(source) + + +@pytest.mark.skipif(sys.version_info[:2] < (3, 6), reason="pathlib is only tested on 3.6 and up") +def test_with_with_path_object(tmpdir): + import pathlib + tmpdir = str(tmpdir) + dest = pathlib.Path(tmpdir) / "test.designspace" + doc = DesignSpaceDocument() + doc.write(dest) + assert dest.exists() diff --git a/Tests/fontBuilder/data/test.otf.ttx b/Tests/fontBuilder/data/test.otf.ttx new file mode 100644 index 00000000..4c9a2a75 --- /dev/null +++ b/Tests/fontBuilder/data/test.otf.ttx @@ -0,0 +1,253 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="OTTO" ttLibVersion="3.31"> + + <GlyphOrder> + <!-- The 'id' attribute is only for humans; it is ignored when parsed. --> + <GlyphID id="0" name=".notdef"/> + <GlyphID id="1" name=".null"/> + <GlyphID id="2" name="A"/> + <GlyphID id="3" name="a"/> + </GlyphOrder> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.0"/> + <checkSumAdjustment value="0x9198bee"/> + <magicNumber value="0x5f0f3cf5"/> + <flags value="00000000 00000011"/> + <unitsPerEm value="1024"/> + <created value="Wed Oct 31 19:40:57 2018"/> + <modified value="Wed Oct 31 19:40:57 2018"/> + <xMin value="100"/> + <yMin value="100"/> + <xMax value="500"/> + <yMax value="1000"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="3"/> + <fontDirectionHint value="2"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="824"/> + <descent value="200"/> + <lineGap value="0"/> + <advanceWidthMax value="600"/> + <minLeftSideBearing value="100"/> + <minRightSideBearing value="100"/> + <xMaxExtent value="500"/> + <caretSlopeRise value="1"/> + <caretSlopeRun value="0"/> + <caretOffset value="0"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="1"/> + </hhea> + + <maxp> + <tableVersion value="0x5000"/> + <numGlyphs value="4"/> + </maxp> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="3"/> + <xAvgCharWidth value="600"/> + <usWeightClass value="400"/> + <usWidthClass value="5"/> + <fsType value="00000000 00000100"/> + <ySubscriptXSize value="0"/> + <ySubscriptYSize value="0"/> + <ySubscriptXOffset value="0"/> + <ySubscriptYOffset value="0"/> + <ySuperscriptXSize value="0"/> + <ySuperscriptYSize value="0"/> + <ySuperscriptXOffset value="0"/> + <ySuperscriptYOffset value="0"/> + <yStrikeoutSize value="0"/> + <yStrikeoutPosition value="0"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="0"/> + <bSerifStyle value="0"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="0"/> + <bStrokeVariation value="0"/> + <bArmStyle value="0"/> + <bLetterForm value="0"/> + <bMidline value="0"/> + <bXHeight value="0"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/> + <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="????"/> + <fsSelection value="00000000 00000000"/> + <usFirstCharIndex value="65"/> + <usLastCharIndex value="97"/> + <sTypoAscender value="0"/> + <sTypoDescender value="0"/> + <sTypoLineGap value="0"/> + <usWinAscent value="0"/> + <usWinDescent value="0"/> + <ulCodePageRange1 value="00000000 00000000 00000000 00000000"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="0"/> + <sCapHeight value="0"/> + <usDefaultChar value="0"/> + <usBreakChar value="32"/> + <usMaxContext value="2"/> + </OS_2> + + <name> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True"> + HelloTestFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True"> + TotallyNormal + </namerecord> + <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True"> + HelloTestFont-TotallyNormal + </namerecord> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True"> + HalloTestFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x4" unicode="True"> + TotaalNormaal + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + HelloTestFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409"> + TotallyNormal + </namerecord> + <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409"> + HelloTestFont-TotallyNormal + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413"> + HalloTestFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x413"> + TotaalNormaal + </namerecord> + </name> + + <cmap> + <tableVersion version="0"/> + <cmap_format_4 platformID="0" platEncID="3" language="0"> + <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + </cmap_format_4> + <cmap_format_4 platformID="3" platEncID="1" language="0"> + <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + </cmap_format_4> + </cmap> + + <post> + <formatType value="3.0"/> + <italicAngle value="0.0"/> + <underlinePosition value="0"/> + <underlineThickness value="0"/> + <isFixedPitch value="0"/> + <minMemType42 value="0"/> + <maxMemType42 value="0"/> + <minMemType1 value="0"/> + <maxMemType1 value="0"/> + </post> + + <CFF> + <major value="1"/> + <minor value="0"/> + <CFFFont name="HelloTestFont-TotallyNormal"> + <FullName value="HelloTestFont-TotallyNormal"/> + <isFixedPitch value="0"/> + <ItalicAngle value="0"/> + <UnderlinePosition value="-100"/> + <UnderlineThickness value="50"/> + <PaintType value="0"/> + <CharstringType value="2"/> + <FontMatrix value="0.0009765625 0 0 0.0009765625 0 0"/> + <FontBBox value="100 100 500 1000"/> + <StrokeWidth value="0"/> + <!-- charset is dumped separately as the 'GlyphOrder' element --> + <Encoding name="StandardEncoding"/> + <Private> + <BlueScale value="0.039625"/> + <BlueShift value="7"/> + <BlueFuzz value="1"/> + <ForceBold value="0"/> + <LanguageGroup value="0"/> + <ExpansionFactor value="0.06"/> + <initialRandomSeed value="0"/> + <defaultWidthX value="0"/> + <nominalWidthX value="0"/> + </Private> + <CharStrings> + <CharString name=".notdef"> + 600 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + endchar + </CharString> + <CharString name=".null"> + 600 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + endchar + </CharString> + <CharString name="A"> + 600 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + endchar + </CharString> + <CharString name="a"> + 600 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + endchar + </CharString> + </CharStrings> + </CFFFont> + + <GlobalSubrs> + <!-- The 'index' attribute is only for humans; it is ignored when parsed. --> + </GlobalSubrs> + </CFF> + + <hmtx> + <mtx name=".notdef" width="600" lsb="100"/> + <mtx name=".null" width="600" lsb="100"/> + <mtx name="A" width="600" lsb="100"/> + <mtx name="a" width="600" lsb="100"/> + </hmtx> + + <DSIG> + <!-- note that the Digital Signature will be invalid after recompilation! --> + <tableHeader flag="0x1" numSigs="1" version="1"/> + <SignatureRecord format="1"> +-----BEGIN PKCS7----- +0000000100000000 +-----END PKCS7----- + </SignatureRecord> + </DSIG> + +</ttFont> diff --git a/Tests/fontBuilder/data/test.ttf.ttx b/Tests/fontBuilder/data/test.ttf.ttx new file mode 100644 index 00000000..b2804ccd --- /dev/null +++ b/Tests/fontBuilder/data/test.ttf.ttx @@ -0,0 +1,270 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.31"> + + <GlyphOrder> + <!-- The 'id' attribute is only for humans; it is ignored when parsed. --> + <GlyphID id="0" name=".notdef"/> + <GlyphID id="1" name=".null"/> + <GlyphID id="2" name="A"/> + <GlyphID id="3" name="a"/> + </GlyphOrder> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.0"/> + <checkSumAdjustment value="0x7adb7b76"/> + <magicNumber value="0x5f0f3cf5"/> + <flags value="00000000 00000011"/> + <unitsPerEm value="1024"/> + <created value="Wed Oct 31 19:40:57 2018"/> + <modified value="Wed Oct 31 19:40:57 2018"/> + <xMin value="100"/> + <yMin value="100"/> + <xMax value="500"/> + <yMax value="1000"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="3"/> + <fontDirectionHint value="2"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="824"/> + <descent value="200"/> + <lineGap value="0"/> + <advanceWidthMax value="600"/> + <minLeftSideBearing value="100"/> + <minRightSideBearing value="100"/> + <xMaxExtent value="500"/> + <caretSlopeRise value="1"/> + <caretSlopeRun value="0"/> + <caretOffset value="0"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="1"/> + </hhea> + + <maxp> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="0x10000"/> + <numGlyphs value="4"/> + <maxPoints value="6"/> + <maxContours value="1"/> + <maxCompositePoints value="0"/> + <maxCompositeContours value="0"/> + <maxZones value="2"/> + <maxTwilightPoints value="0"/> + <maxStorage value="0"/> + <maxFunctionDefs value="0"/> + <maxInstructionDefs value="0"/> + <maxStackElements value="0"/> + <maxSizeOfInstructions value="0"/> + <maxComponentElements value="0"/> + <maxComponentDepth value="0"/> + </maxp> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="3"/> + <xAvgCharWidth value="600"/> + <usWeightClass value="400"/> + <usWidthClass value="5"/> + <fsType value="00000000 00000100"/> + <ySubscriptXSize value="0"/> + <ySubscriptYSize value="0"/> + <ySubscriptXOffset value="0"/> + <ySubscriptYOffset value="0"/> + <ySuperscriptXSize value="0"/> + <ySuperscriptYSize value="0"/> + <ySuperscriptXOffset value="0"/> + <ySuperscriptYOffset value="0"/> + <yStrikeoutSize value="0"/> + <yStrikeoutPosition value="0"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="0"/> + <bSerifStyle value="0"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="0"/> + <bStrokeVariation value="0"/> + <bArmStyle value="0"/> + <bLetterForm value="0"/> + <bMidline value="0"/> + <bXHeight value="0"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/> + <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="????"/> + <fsSelection value="00000000 00000000"/> + <usFirstCharIndex value="65"/> + <usLastCharIndex value="97"/> + <sTypoAscender value="0"/> + <sTypoDescender value="0"/> + <sTypoLineGap value="0"/> + <usWinAscent value="0"/> + <usWinDescent value="0"/> + <ulCodePageRange1 value="00000000 00000000 00000000 00000000"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="0"/> + <sCapHeight value="0"/> + <usDefaultChar value="0"/> + <usBreakChar value="32"/> + <usMaxContext value="2"/> + </OS_2> + + <hmtx> + <mtx name=".notdef" width="600" lsb="100"/> + <mtx name=".null" width="600" lsb="100"/> + <mtx name="A" width="600" lsb="100"/> + <mtx name="a" width="600" lsb="100"/> + </hmtx> + + <cmap> + <tableVersion version="0"/> + <cmap_format_4 platformID="0" platEncID="3" language="0"> + <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + </cmap_format_4> + <cmap_format_4 platformID="3" platEncID="1" language="0"> + <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + </cmap_format_4> + </cmap> + + <loca> + <!-- The 'loca' table will be calculated by the compiler --> + </loca> + + <glyf> + + <!-- The xMin, yMin, xMax and yMax values + will be recalculated by the compiler. --> + + <TTGlyph name=".notdef" xMin="100" yMin="100" xMax="500" yMax="1000"> + <contour> + <pt x="100" y="100" on="1"/> + <pt x="100" y="1000" on="1"/> + <pt x="200" y="900" on="0"/> + <pt x="400" y="900" on="0"/> + <pt x="500" y="1000" on="1"/> + <pt x="500" y="100" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name=".null" xMin="100" yMin="100" xMax="500" yMax="1000"> + <contour> + <pt x="100" y="100" on="1"/> + <pt x="100" y="1000" on="1"/> + <pt x="200" y="900" on="0"/> + <pt x="400" y="900" on="0"/> + <pt x="500" y="1000" on="1"/> + <pt x="500" y="100" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="A" xMin="100" yMin="100" xMax="500" yMax="1000"> + <contour> + <pt x="100" y="100" on="1"/> + <pt x="100" y="1000" on="1"/> + <pt x="200" y="900" on="0"/> + <pt x="400" y="900" on="0"/> + <pt x="500" y="1000" on="1"/> + <pt x="500" y="100" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="a" xMin="100" yMin="100" xMax="500" yMax="1000"> + <contour> + <pt x="100" y="100" on="1"/> + <pt x="100" y="1000" on="1"/> + <pt x="200" y="900" on="0"/> + <pt x="400" y="900" on="0"/> + <pt x="500" y="1000" on="1"/> + <pt x="500" y="100" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + </glyf> + + <name> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True"> + HelloTestFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True"> + TotallyNormal + </namerecord> + <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True"> + HelloTestFont-TotallyNormal + </namerecord> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True"> + HalloTestFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x4" unicode="True"> + TotaalNormaal + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + HelloTestFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409"> + TotallyNormal + </namerecord> + <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409"> + HelloTestFont-TotallyNormal + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413"> + HalloTestFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x413"> + TotaalNormaal + </namerecord> + </name> + + <post> + <formatType value="2.0"/> + <italicAngle value="0.0"/> + <underlinePosition value="0"/> + <underlineThickness value="0"/> + <isFixedPitch value="0"/> + <minMemType42 value="0"/> + <maxMemType42 value="0"/> + <minMemType1 value="0"/> + <maxMemType1 value="0"/> + <psNames> + <!-- This file uses unique glyph names based on the information + found in the 'post' table. Since these names might not be unique, + we have to invent artificial names in case of clashes. In order to + be able to retain the original information, we need a name to + ps name mapping for those cases where they differ. That's what + you see below. + --> + </psNames> + <extraNames> + <!-- following are the name that are not taken from the standard Mac glyph order --> + </extraNames> + </post> + + <DSIG> + <!-- note that the Digital Signature will be invalid after recompilation! --> + <tableHeader flag="0x1" numSigs="1" version="1"/> + <SignatureRecord format="1"> +-----BEGIN PKCS7----- +0000000100000000 +-----END PKCS7----- + </SignatureRecord> + </DSIG> + +</ttFont> diff --git a/Tests/fontBuilder/data/test_var.otf.ttx b/Tests/fontBuilder/data/test_var.otf.ttx new file mode 100644 index 00000000..c9465d23 --- /dev/null +++ b/Tests/fontBuilder/data/test_var.otf.ttx @@ -0,0 +1,291 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="OTTO" ttLibVersion="3.33"> + + <GlyphOrder> + <!-- The 'id' attribute is only for humans; it is ignored when parsed. --> + <GlyphID id="0" name=".notdef"/> + <GlyphID id="1" name="glyph00001"/> + <GlyphID id="2" name="A"/> + <GlyphID id="3" name="a"/> + </GlyphOrder> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.0"/> + <checkSumAdjustment value="0xed07360f"/> + <magicNumber value="0x5f0f3cf5"/> + <flags value="00000000 00000011"/> + <unitsPerEm value="1000"/> + <created value="Wed Dec 5 11:55:26 2018"/> + <modified value="Wed Dec 5 11:55:26 2018"/> + <xMin value="0"/> + <yMin value="0"/> + <xMax value="0"/> + <yMax value="0"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="3"/> + <fontDirectionHint value="2"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="824"/> + <descent value="200"/> + <lineGap value="0"/> + <advanceWidthMax value="0"/> + <minLeftSideBearing value="0"/> + <minRightSideBearing value="0"/> + <xMaxExtent value="0"/> + <caretSlopeRise value="1"/> + <caretSlopeRun value="0"/> + <caretOffset value="0"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="1"/> + </hhea> + + <maxp> + <tableVersion value="0x5000"/> + <numGlyphs value="4"/> + </maxp> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="3"/> + <xAvgCharWidth value="600"/> + <usWeightClass value="400"/> + <usWidthClass value="5"/> + <fsType value="00000000 00000100"/> + <ySubscriptXSize value="0"/> + <ySubscriptYSize value="0"/> + <ySubscriptXOffset value="0"/> + <ySubscriptYOffset value="0"/> + <ySuperscriptXSize value="0"/> + <ySuperscriptYSize value="0"/> + <ySuperscriptXOffset value="0"/> + <ySuperscriptYOffset value="0"/> + <yStrikeoutSize value="0"/> + <yStrikeoutPosition value="0"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="0"/> + <bSerifStyle value="0"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="0"/> + <bStrokeVariation value="0"/> + <bArmStyle value="0"/> + <bLetterForm value="0"/> + <bMidline value="0"/> + <bXHeight value="0"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/> + <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="????"/> + <fsSelection value="00000000 00000000"/> + <usFirstCharIndex value="65"/> + <usLastCharIndex value="97"/> + <sTypoAscender value="825"/> + <sTypoDescender value="200"/> + <sTypoLineGap value="0"/> + <usWinAscent value="824"/> + <usWinDescent value="200"/> + <ulCodePageRange1 value="00000000 00000000 00000000 00000000"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="0"/> + <sCapHeight value="0"/> + <usDefaultChar value="0"/> + <usBreakChar value="32"/> + <usMaxContext value="2"/> + </OS_2> + + <hmtx> + <mtx name=".notdef" width="600" lsb="0"/> + <mtx name="A" width="600" lsb="0"/> + <mtx name="a" width="600" lsb="0"/> + <mtx name="glyph00001" width="600" lsb="0"/> + </hmtx> + + <cmap> + <tableVersion version="0"/> + <cmap_format_4 platformID="0" platEncID="3" language="0"> + <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + </cmap_format_4> + <cmap_format_4 platformID="3" platEncID="1" language="0"> + <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + </cmap_format_4> + </cmap> + + <name> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True"> + HelloTestFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True"> + TotallyNormal + </namerecord> + <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True"> + HelloTestFont-TotallyNormal + </namerecord> + <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Test Axis + </namerecord> + <namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True"> + TotallyNormal + </namerecord> + <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True"> + TotallyTested + </namerecord> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True"> + HalloTestFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x4" unicode="True"> + TotaalNormaal + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + HelloTestFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409"> + TotallyNormal + </namerecord> + <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409"> + HelloTestFont-TotallyNormal + </namerecord> + <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409"> + Test Axis + </namerecord> + <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409"> + TotallyNormal + </namerecord> + <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409"> + TotallyTested + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413"> + HalloTestFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x413"> + TotaalNormaal + </namerecord> + </name> + + <post> + <formatType value="3.0"/> + <italicAngle value="0.0"/> + <underlinePosition value="0"/> + <underlineThickness value="0"/> + <isFixedPitch value="0"/> + <minMemType42 value="0"/> + <maxMemType42 value="0"/> + <minMemType1 value="0"/> + <maxMemType1 value="0"/> + </post> + + <CFF2> + <major value="2"/> + <minor value="0"/> + <CFFFont name="CFF2Font"> + <FontMatrix value="0.001 0 0 0.001 0 0"/> + <FDArray> + <FontDict index="0"> + <Private> + <BlueScale value="0.039625"/> + <BlueShift value="7"/> + <BlueFuzz value="1"/> + </Private> + </FontDict> + </FDArray> + <CharStrings> + <CharString name=".notdef"> + 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + </CharString> + <CharString name="A"> + 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + </CharString> + <CharString name="a"> + 200 200 -200 -200 2 blend + rmoveto + 400 400 1 blend + hlineto + 400 400 1 blend + vlineto + -400 -400 1 blend + hlineto + </CharString> + <CharString name="glyph00001"> + 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + </CharString> + </CharStrings> + <VarStore Format="1"> + <Format value="1"/> + <VarRegionList> + <!-- RegionAxisCount=1 --> + <!-- RegionCount=1 --> + <Region index="0"> + <VarRegionAxis index="0"> + <StartCoord value="0.0"/> + <PeakCoord value="1.0"/> + <EndCoord value="1.0"/> + </VarRegionAxis> + </Region> + </VarRegionList> + <!-- VarDataCount=1 --> + <VarData index="0"> + <!-- ItemCount=0 --> + <NumShorts value="0"/> + <!-- VarRegionCount=1 --> + <VarRegionIndex index="0" value="0"/> + </VarData> + </VarStore> + </CFFFont> + + <GlobalSubrs> + <!-- The 'index' attribute is only for humans; it is ignored when parsed. --> + </GlobalSubrs> + </CFF2> + + <fvar> + + <!-- Test Axis --> + <Axis> + <AxisTag>TEST</AxisTag> + <Flags>0x0</Flags> + <MinValue>0.0</MinValue> + <DefaultValue>0.0</DefaultValue> + <MaxValue>100.0</MaxValue> + <AxisNameID>256</AxisNameID> + </Axis> + + <!-- TotallyNormal --> + <NamedInstance flags="0x0" subfamilyNameID="257"> + <coord axis="TEST" value="0.0"/> + </NamedInstance> + + <!-- TotallyTested --> + <NamedInstance flags="0x0" subfamilyNameID="258"> + <coord axis="TEST" value="100.0"/> + </NamedInstance> + </fvar> + +</ttFont> diff --git a/Tests/fontBuilder/data/test_var.ttf.ttx b/Tests/fontBuilder/data/test_var.ttf.ttx new file mode 100644 index 00000000..760e65a5 --- /dev/null +++ b/Tests/fontBuilder/data/test_var.ttf.ttx @@ -0,0 +1,376 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.32"> + + <GlyphOrder> + <!-- The 'id' attribute is only for humans; it is ignored when parsed. --> + <GlyphID id="0" name=".notdef"/> + <GlyphID id="1" name=".null"/> + <GlyphID id="2" name="A"/> + <GlyphID id="3" name="a"/> + </GlyphOrder> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.0"/> + <checkSumAdjustment value="0x18e72247"/> + <magicNumber value="0x5f0f3cf5"/> + <flags value="00000000 00000011"/> + <unitsPerEm value="1024"/> + <created value="Thu Nov 1 20:29:01 2018"/> + <modified value="Thu Nov 1 20:29:01 2018"/> + <xMin value="100"/> + <yMin value="0"/> + <xMax value="500"/> + <yMax value="400"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="3"/> + <fontDirectionHint value="2"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="824"/> + <descent value="200"/> + <lineGap value="0"/> + <advanceWidthMax value="600"/> + <minLeftSideBearing value="100"/> + <minRightSideBearing value="100"/> + <xMaxExtent value="500"/> + <caretSlopeRise value="1"/> + <caretSlopeRun value="0"/> + <caretOffset value="0"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="1"/> + </hhea> + + <maxp> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="0x10000"/> + <numGlyphs value="4"/> + <maxPoints value="4"/> + <maxContours value="1"/> + <maxCompositePoints value="0"/> + <maxCompositeContours value="0"/> + <maxZones value="2"/> + <maxTwilightPoints value="0"/> + <maxStorage value="0"/> + <maxFunctionDefs value="0"/> + <maxInstructionDefs value="0"/> + <maxStackElements value="0"/> + <maxSizeOfInstructions value="0"/> + <maxComponentElements value="0"/> + <maxComponentDepth value="0"/> + </maxp> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="3"/> + <xAvgCharWidth value="600"/> + <usWeightClass value="400"/> + <usWidthClass value="5"/> + <fsType value="00000000 00000100"/> + <ySubscriptXSize value="0"/> + <ySubscriptYSize value="0"/> + <ySubscriptXOffset value="0"/> + <ySubscriptYOffset value="0"/> + <ySuperscriptXSize value="0"/> + <ySuperscriptYSize value="0"/> + <ySuperscriptXOffset value="0"/> + <ySuperscriptYOffset value="0"/> + <yStrikeoutSize value="0"/> + <yStrikeoutPosition value="0"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="0"/> + <bSerifStyle value="0"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="0"/> + <bStrokeVariation value="0"/> + <bArmStyle value="0"/> + <bLetterForm value="0"/> + <bMidline value="0"/> + <bXHeight value="0"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/> + <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="????"/> + <fsSelection value="00000000 00000000"/> + <usFirstCharIndex value="65"/> + <usLastCharIndex value="97"/> + <sTypoAscender value="0"/> + <sTypoDescender value="0"/> + <sTypoLineGap value="0"/> + <usWinAscent value="0"/> + <usWinDescent value="0"/> + <ulCodePageRange1 value="00000000 00000000 00000000 00000000"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="0"/> + <sCapHeight value="0"/> + <usDefaultChar value="0"/> + <usBreakChar value="32"/> + <usMaxContext value="2"/> + </OS_2> + + <hmtx> + <mtx name=".notdef" width="600" lsb="0"/> + <mtx name=".null" width="600" lsb="0"/> + <mtx name="A" width="600" lsb="100"/> + <mtx name="a" width="600" lsb="100"/> + </hmtx> + + <cmap> + <tableVersion version="0"/> + <cmap_format_4 platformID="0" platEncID="3" language="0"> + <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + </cmap_format_4> + <cmap_format_4 platformID="3" platEncID="1" language="0"> + <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + </cmap_format_4> + </cmap> + + <loca> + <!-- The 'loca' table will be calculated by the compiler --> + </loca> + + <glyf> + + <!-- The xMin, yMin, xMax and yMax values + will be recalculated by the compiler. --> + + <TTGlyph name=".notdef"/><!-- contains no outline data --> + + <TTGlyph name=".null"/><!-- contains no outline data --> + + <TTGlyph name="A" xMin="100" yMin="0" xMax="500" yMax="400"> + <contour> + <pt x="100" y="0" on="1"/> + <pt x="100" y="400" on="1"/> + <pt x="500" y="400" on="1"/> + <pt x="500" y="0" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="a" xMin="100" yMin="0" xMax="500" yMax="400"> + <contour> + <pt x="100" y="0" on="1"/> + <pt x="100" y="400" on="1"/> + <pt x="500" y="400" on="1"/> + <pt x="500" y="0" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + </glyf> + + <name> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True"> + HelloTestFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True"> + TotallyNormal + </namerecord> + <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True"> + HelloTestFont-TotallyNormal + </namerecord> + <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Left + </namerecord> + <namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Right + </namerecord> + <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Up + </namerecord> + <namerecord nameID="259" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Down + </namerecord> + <namerecord nameID="260" platformID="1" platEncID="0" langID="0x0" unicode="True"> + TotallyNormal + </namerecord> + <namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Right Up + </namerecord> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True"> + HalloTestFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x4" unicode="True"> + TotaalNormaal + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + HelloTestFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409"> + TotallyNormal + </namerecord> + <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409"> + HelloTestFont-TotallyNormal + </namerecord> + <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409"> + Left + </namerecord> + <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409"> + Right + </namerecord> + <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409"> + Up + </namerecord> + <namerecord nameID="259" platformID="3" platEncID="1" langID="0x409"> + Down + </namerecord> + <namerecord nameID="260" platformID="3" platEncID="1" langID="0x409"> + TotallyNormal + </namerecord> + <namerecord nameID="261" platformID="3" platEncID="1" langID="0x409"> + Right Up + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413"> + HalloTestFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x413"> + TotaalNormaal + </namerecord> + </name> + + <post> + <formatType value="2.0"/> + <italicAngle value="0.0"/> + <underlinePosition value="0"/> + <underlineThickness value="0"/> + <isFixedPitch value="0"/> + <minMemType42 value="0"/> + <maxMemType42 value="0"/> + <minMemType1 value="0"/> + <maxMemType1 value="0"/> + <psNames> + <!-- This file uses unique glyph names based on the information + found in the 'post' table. Since these names might not be unique, + we have to invent artificial names in case of clashes. In order to + be able to retain the original information, we need a name to + ps name mapping for those cases where they differ. That's what + you see below. + --> + </psNames> + <extraNames> + <!-- following are the name that are not taken from the standard Mac glyph order --> + </extraNames> + </post> + + <fvar> + + <!-- Left --> + <Axis> + <AxisTag>LEFT</AxisTag> + <Flags>0x0</Flags> + <MinValue>0.0</MinValue> + <DefaultValue>0.0</DefaultValue> + <MaxValue>100.0</MaxValue> + <AxisNameID>256</AxisNameID> + </Axis> + + <!-- Right --> + <Axis> + <AxisTag>RGHT</AxisTag> + <Flags>0x0</Flags> + <MinValue>0.0</MinValue> + <DefaultValue>0.0</DefaultValue> + <MaxValue>100.0</MaxValue> + <AxisNameID>257</AxisNameID> + </Axis> + + <!-- Up --> + <Axis> + <AxisTag>UPPP</AxisTag> + <Flags>0x0</Flags> + <MinValue>0.0</MinValue> + <DefaultValue>0.0</DefaultValue> + <MaxValue>100.0</MaxValue> + <AxisNameID>258</AxisNameID> + </Axis> + + <!-- Down --> + <Axis> + <AxisTag>DOWN</AxisTag> + <Flags>0x0</Flags> + <MinValue>0.0</MinValue> + <DefaultValue>0.0</DefaultValue> + <MaxValue>100.0</MaxValue> + <AxisNameID>259</AxisNameID> + </Axis> + + <!-- TotallyNormal --> + <NamedInstance flags="0x0" subfamilyNameID="260"> + <coord axis="LEFT" value="0.0"/> + <coord axis="RGHT" value="0.0"/> + <coord axis="UPPP" value="0.0"/> + <coord axis="DOWN" value="0.0"/> + </NamedInstance> + + <!-- Right Up --> + <NamedInstance flags="0x0" subfamilyNameID="261"> + <coord axis="LEFT" value="0.0"/> + <coord axis="RGHT" value="100.0"/> + <coord axis="UPPP" value="100.0"/> + <coord axis="DOWN" value="0.0"/> + </NamedInstance> + </fvar> + + <gvar> + <version value="1"/> + <reserved value="0"/> + <glyphVariations glyph="a"> + <tuple> + <coord axis="RGHT" value="1.0"/> + <delta pt="0" x="0" y="0"/> + <delta pt="1" x="0" y="0"/> + <delta pt="2" x="200" y="0"/> + <delta pt="3" x="200" y="0"/> + </tuple> + <tuple> + <coord axis="LEFT" value="1.0"/> + <delta pt="0" x="-200" y="0"/> + <delta pt="1" x="-200" y="0"/> + <delta pt="2" x="0" y="0"/> + <delta pt="3" x="0" y="0"/> + </tuple> + <tuple> + <coord axis="UPPP" value="1.0"/> + <delta pt="0" x="0" y="0"/> + <delta pt="1" x="0" y="200"/> + <delta pt="2" x="0" y="200"/> + <delta pt="3" x="0" y="0"/> + </tuple> + <tuple> + <coord axis="DOWN" value="1.0"/> + <delta pt="0" x="0" y="-200"/> + <delta pt="1" x="0" y="0"/> + <delta pt="2" x="0" y="0"/> + <delta pt="3" x="0" y="-200"/> + </tuple> + </glyphVariations> + </gvar> + + <DSIG> + <!-- note that the Digital Signature will be invalid after recompilation! --> + <tableHeader flag="0x1" numSigs="1" version="1"/> + <SignatureRecord format="1"> +-----BEGIN PKCS7----- +0000000100000000 +-----END PKCS7----- + </SignatureRecord> + </DSIG> + +</ttFont> diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py new file mode 100644 index 00000000..d23f0f17 --- /dev/null +++ b/Tests/fontBuilder/fontBuilder_test.py @@ -0,0 +1,246 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals + +import os +import shutil +import re +from fontTools.ttLib import TTFont +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.pens.t2CharStringPen import T2CharStringPen +from fontTools.fontBuilder import FontBuilder +from fontTools.ttLib.tables.TupleVariation import TupleVariation +from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates +from fontTools.misc.psCharStrings import T2CharString + + +def getTestData(fileName, mode="r"): + path = os.path.join(os.path.dirname(__file__), "data", fileName) + with open(path, mode) as f: + return f.read() + + +def strip_VariableItems(string): + # ttlib changes with the fontTools version + string = re.sub(' ttLibVersion=".*"', '', string) + # head table checksum and creation and mod date changes with each save. + string = re.sub('<checkSumAdjustment value="[^"]+"/>', '', string) + string = re.sub('<modified value="[^"]+"/>', '', string) + string = re.sub('<created value="[^"]+"/>', '', string) + return string + + +def drawTestGlyph(pen): + pen.moveTo((100, 100)) + pen.lineTo((100, 1000)) + pen.qCurveTo((200, 900), (400, 900), (500, 1000)) + pen.lineTo((500, 100)) + pen.closePath() + + +def _setupFontBuilder(isTTF, unitsPerEm=1024): + fb = FontBuilder(unitsPerEm, isTTF=isTTF) + fb.setupGlyphOrder([".notdef", ".null", "A", "a"]) + fb.setupCharacterMap({65: "A", 97: "a"}) + + advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600} + + familyName = "HelloTestFont" + styleName = "TotallyNormal" + nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"), + styleName=dict(en="TotallyNormal", nl="TotaalNormaal")) + nameStrings['psName'] = familyName + "-" + styleName + + return fb, advanceWidths, nameStrings + + +def _verifyOutput(outPath): + f = TTFont(outPath) + f.saveXML(outPath + ".ttx") + with open(outPath + ".ttx") as f: + testData = strip_VariableItems(f.read()) + refData = strip_VariableItems(getTestData(os.path.basename(outPath) + ".ttx")) + assert refData == testData + + +def test_build_ttf(tmpdir): + outPath = os.path.join(str(tmpdir), "test.ttf") + + fb, advanceWidths, nameStrings = _setupFontBuilder(True) + + pen = TTGlyphPen(None) + drawTestGlyph(pen) + glyph = pen.glyph() + glyphs = {".notdef": glyph, "A": glyph, "a": glyph, ".null": glyph} + fb.setupGlyf(glyphs) + metrics = {} + glyphTable = fb.font["glyf"] + for gn, advanceWidth in advanceWidths.items(): + metrics[gn] = (advanceWidth, glyphTable[gn].xMin) + fb.setupHorizontalMetrics(metrics) + + fb.setupHorizontalHeader(ascent=824, descent=200) + fb.setupNameTable(nameStrings) + fb.setupOS2() + fb.setupPost() + fb.setupDummyDSIG() + + fb.save(outPath) + + _verifyOutput(outPath) + + +def test_build_otf(tmpdir): + outPath = os.path.join(str(tmpdir), "test.otf") + + fb, advanceWidths, nameStrings = _setupFontBuilder(False) + + pen = T2CharStringPen(600, None) + drawTestGlyph(pen) + charString = pen.getCharString() + charStrings = {".notdef": charString, "A": charString, "a": charString, ".null": charString} + fb.setupCFF(nameStrings['psName'], {"FullName": nameStrings['psName']}, charStrings, {}) + metrics = {} + for gn, advanceWidth in advanceWidths.items(): + metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph + fb.setupHorizontalMetrics(metrics) + + fb.setupHorizontalHeader(ascent=824, descent=200) + fb.setupNameTable(nameStrings) + fb.setupOS2() + fb.setupPost() + fb.setupDummyDSIG() + + fb.save(outPath) + + _verifyOutput(outPath) + + +def test_build_var(tmpdir): + outPath = os.path.join(str(tmpdir), "test_var.ttf") + + fb = FontBuilder(1024, isTTF=True) + fb.setupGlyphOrder([".notdef", ".null", "A", "a"]) + fb.setupCharacterMap({65: "A", 97: "a"}) + + advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600} + + familyName = "HelloTestFont" + styleName = "TotallyNormal" + nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"), + styleName=dict(en="TotallyNormal", nl="TotaalNormaal")) + nameStrings['psName'] = familyName + "-" + styleName + + pen = TTGlyphPen(None) + pen.moveTo((100, 0)) + pen.lineTo((100, 400)) + pen.lineTo((500, 400)) + pen.lineTo((500, 000)) + pen.closePath() + + glyph = pen.glyph() + + pen = TTGlyphPen(None) + emptyGlyph = pen.glyph() + + glyphs = {".notdef": emptyGlyph, "A": glyph, "a": glyph, ".null": emptyGlyph} + fb.setupGlyf(glyphs) + metrics = {} + glyphTable = fb.font["glyf"] + for gn, advanceWidth in advanceWidths.items(): + metrics[gn] = (advanceWidth, glyphTable[gn].xMin) + fb.setupHorizontalMetrics(metrics) + + fb.setupHorizontalHeader(ascent=824, descent=200) + fb.setupNameTable(nameStrings) + + axes = [ + ('LEFT', 0, 0, 100, "Left"), + ('RGHT', 0, 0, 100, "Right"), + ('UPPP', 0, 0, 100, "Up"), + ('DOWN', 0, 0, 100, "Down"), + ] + instances = [ + dict(location=dict(LEFT=0, RGHT=0, UPPP=0, DOWN=0), stylename="TotallyNormal"), + dict(location=dict(LEFT=0, RGHT=100, UPPP=100, DOWN=0), stylename="Right Up"), + ] + fb.setupFvar(axes, instances) + variations = {} + # Four (x, y) pairs and four phantom points: + leftDeltas = [(-200, 0), (-200, 0), (0, 0), (0, 0), None, None, None, None] + rightDeltas = [(0, 0), (0, 0), (200, 0), (200, 0), None, None, None, None] + upDeltas = [(0, 0), (0, 200), (0, 200), (0, 0), None, None, None, None] + downDeltas = [(0, -200), (0, 0), (0, 0), (0, -200), None, None, None, None] + variations['a'] = [ + TupleVariation(dict(RGHT=(0, 1, 1)), rightDeltas), + TupleVariation(dict(LEFT=(0, 1, 1)), leftDeltas), + TupleVariation(dict(UPPP=(0, 1, 1)), upDeltas), + TupleVariation(dict(DOWN=(0, 1, 1)), downDeltas), + ] + fb.setupGvar(variations) + + fb.setupOS2() + fb.setupPost() + fb.setupDummyDSIG() + + fb.save(outPath) + + _verifyOutput(outPath) + + +def test_build_cff2(tmpdir): + outPath = os.path.join(str(tmpdir), "test_var.otf") + + fb, advanceWidths, nameStrings = _setupFontBuilder(False, 1000) + + fb.setupNameTable(nameStrings) + + axes = [ + ('TEST', 0, 0, 100, "Test Axis"), + ] + instances = [ + dict(location=dict(TEST=0), stylename="TotallyNormal"), + dict(location=dict(TEST=100), stylename="TotallyTested"), + ] + fb.setupFvar(axes, instances) + + pen = T2CharStringPen(None, None, CFF2=True) + drawTestGlyph(pen) + charString = pen.getCharString() + + program = [ + 200, 200, -200, -200, 2, "blend", "rmoveto", + 400, 400, 1, "blend", "hlineto", + 400, 400, 1, "blend", "vlineto", + -400, -400, 1, "blend", "hlineto" + ] + charStringVariable = T2CharString(program=program) + + charStrings = {".notdef": charString, "A": charString, "a": charStringVariable, ".null": charString} + fb.setupCFF2(charStrings, regions=[{"TEST": (0, 1, 1)}]) + + metrics = {gn: (advanceWidth, 0) for gn, advanceWidth in advanceWidths.items()} + fb.setupHorizontalMetrics(metrics) + + fb.setupHorizontalHeader(ascent=824, descent=200) + fb.setupOS2(sTypoAscender=825, sTypoDescender=200, usWinAscent=824, usWinDescent=200) + fb.setupPost() + + fb.save(outPath) + + _verifyOutput(outPath) + + +def test_setupNameTable_no_mac(): + fb, _, nameStrings = _setupFontBuilder(True) + fb.setupNameTable(nameStrings, mac=False) + + assert all(n for n in fb.font["name"].names if n.platformID == 3) + assert not any(n for n in fb.font["name"].names if n.platformID == 1) + + +def test_setupNameTable_no_windows(): + fb, _, nameStrings = _setupFontBuilder(True) + fb.setupNameTable(nameStrings, windows=False) + + assert all(n for n in fb.font["name"].names if n.platformID == 1) + assert not any(n for n in fb.font["name"].names if n.platformID == 3) diff --git a/Tests/misc/plistlib_test.py b/Tests/misc/plistlib_test.py index 041f3328..c8665298 100644 --- a/Tests/misc/plistlib_test.py +++ b/Tests/misc/plistlib_test.py @@ -14,6 +14,10 @@ from fontTools.ufoLib.plistlib import ( ) import pytest +try: + from collections.abc import Mapping # python >= 3.3 +except ImportError: + from collections import Mapping PY2 = sys.version_info < (3,) if PY2: @@ -530,6 +534,25 @@ def test_non_ascii_bytes(): plistlib.dumps("\U0001f40d".encode("utf-8"), use_builtin_types=False) +class CustomMapping(Mapping): + a = {"a": 1, "b": 2} + + def __getitem__(self, key): + return self.a[key] + + def __iter__(self): + return iter(self.a) + + def __len__(self): + return len(self.a) + + +def test_custom_mapping(): + test_mapping = CustomMapping() + data = plistlib.dumps(test_mapping) + assert plistlib.loads(data) == {"a": 1, "b": 2} + + if __name__ == "__main__": import sys diff --git a/Tests/misc/psCharStrings_test.py b/Tests/misc/psCharStrings_test.py index 3f35ac85..f69f7481 100644 --- a/Tests/misc/psCharStrings_test.py +++ b/Tests/misc/psCharStrings_test.py @@ -1,7 +1,7 @@ from __future__ import print_function, division, absolute_import from fontTools.cffLib import PrivateDict from fontTools.cffLib.specializer import stringToProgram -from fontTools.misc.psCharStrings import T2CharString +from fontTools.misc.psCharStrings import T2CharString, encodeFloat, read_realNumber import unittest @@ -47,6 +47,47 @@ class T2CharStringTest(unittest.TestCase): cs2.program, [100, 'rmoveto', -50, -150, 200.5, 0, -50, 150, 'rrcurveto']) + def test_encodeFloat(self): + import sys + def hexenc(s): + return ' '.join('%02x' % ord(x) for x in s) + if sys.version_info[0] >= 3: + def hexenc_py3(s): + return ' '.join('%02x' % x for x in s) + hexenc = hexenc_py3 + + testNums = [ + # value expected result + (-9.399999999999999, '1e e9 a4 ff'), # -9.4 + (9.399999999999999999, '1e 9a 4f'), # 9.4 + (456.8, '1e 45 6a 8f'), # 456.8 + (0.0, '1e 0f'), # 0 + (-0.0, '1e 0f'), # 0 + (1.0, '1e 1f'), # 1 + (-1.0, '1e e1 ff'), # -1 + (98765.37e2, '1e 98 76 53 7f'), # 9876537 + (1234567890.0, '1e 1a 23 45 67 9b 09 ff'), # 1234567890 + (9.876537e-4, '1e a0 00 98 76 53 7f'), # 9.876537e-24 + (9.876537e+4, '1e 98 76 5a 37 ff'), # 9.876537e+24 + ] + + for sample in testNums: + encoded_result = encodeFloat(sample[0]) + + # check to see if we got the expected bytes + self.assertEqual(hexenc(encoded_result), sample[1]) + + # check to see if we get the same value by decoding the data + decoded_result = read_realNumber( + None, + None, + encoded_result, + 1, + ) + self.assertEqual(decoded_result[0], float('%.8g' % sample[0])) + # We limit to 8 digits of precision to match the implementation + # of encodeFloat. + if __name__ == "__main__": import sys diff --git a/Tests/pens/pointPen_test.py b/Tests/pens/pointPen_test.py new file mode 100644 index 00000000..9c71c5e2 --- /dev/null +++ b/Tests/pens/pointPen_test.py @@ -0,0 +1,412 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import CapturingLogHandler +import unittest + +from fontTools.pens.basePen import AbstractPen +from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen, \ + SegmentToPointPen, GuessSmoothPointPen, ReverseContourPointPen + + +class _TestSegmentPen(AbstractPen): + + def __init__(self): + self._commands = [] + + def __repr__(self): + return " ".join(self._commands) + + def moveTo(self, pt): + self._commands.append("%s %s moveto" % (pt[0], pt[1])) + + def lineTo(self, pt): + self._commands.append("%s %s lineto" % (pt[0], pt[1])) + + def curveTo(self, *pts): + pts = ["%s %s" % pt for pt in pts] + self._commands.append("%s curveto" % " ".join(pts)) + + def qCurveTo(self, *pts): + pts = ["%s %s" % pt if pt is not None else "None" for pt in pts] + self._commands.append("%s qcurveto" % " ".join(pts)) + + def closePath(self): + self._commands.append("closepath") + + def endPath(self): + self._commands.append("endpath") + + def addComponent(self, glyphName, transformation): + self._commands.append("'%s' %s addcomponent" % (glyphName, transformation)) + + +def _reprKwargs(kwargs): + items = [] + for key in sorted(kwargs): + value = kwargs[key] + if isinstance(value, basestring): + items.append("%s='%s'" % (key, value)) + else: + items.append("%s=%s" % (key, value)) + return items + + +class _TestPointPen(AbstractPointPen): + + def __init__(self): + self._commands = [] + + def __repr__(self): + return " ".join(self._commands) + + def beginPath(self, identifier=None, **kwargs): + items = [] + if identifier is not None: + items.append("identifier='%s'" % identifier) + items.extend(_reprKwargs(kwargs)) + self._commands.append("beginPath(%s)" % ", ".join(items)) + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, + identifier=None, **kwargs): + items = ["%s" % (pt,)] + if segmentType is not None: + items.append("segmentType='%s'" % segmentType) + if smooth: + items.append("smooth=True") + if name is not None: + items.append("name='%s'" % name) + if identifier is not None: + items.append("identifier='%s'" % identifier) + items.extend(_reprKwargs(kwargs)) + self._commands.append("addPoint(%s)" % ", ".join(items)) + + def endPath(self): + self._commands.append("endPath()") + + def addComponent(self, glyphName, transform, identifier=None, **kwargs): + items = ["'%s'" % glyphName, "%s" % transform] + if identifier is not None: + items.append("identifier='%s'" % identifier) + items.extend(_reprKwargs(kwargs)) + self._commands.append("addComponent(%s)" % ", ".join(items)) + + +class PointToSegmentPenTest(unittest.TestCase): + + def test_open(self): + pen = _TestSegmentPen() + ppen = PointToSegmentPen(pen) + ppen.beginPath() + ppen.addPoint((10, 10), "move") + ppen.addPoint((10, 20), "line") + ppen.endPath() + self.assertEqual("10 10 moveto 10 20 lineto endpath", repr(pen)) + + def test_closed(self): + pen = _TestSegmentPen() + ppen = PointToSegmentPen(pen) + ppen.beginPath() + ppen.addPoint((10, 10), "line") + ppen.addPoint((10, 20), "line") + ppen.addPoint((20, 20), "line") + ppen.endPath() + self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(pen)) + + def test_cubic(self): + pen = _TestSegmentPen() + ppen = PointToSegmentPen(pen) + ppen.beginPath() + ppen.addPoint((10, 10), "line") + ppen.addPoint((10, 20)) + ppen.addPoint((20, 20)) + ppen.addPoint((20, 40), "curve") + ppen.endPath() + self.assertEqual("10 10 moveto 10 20 20 20 20 40 curveto closepath", repr(pen)) + + def test_quad(self): + pen = _TestSegmentPen() + ppen = PointToSegmentPen(pen) + ppen.beginPath(identifier='foo') + ppen.addPoint((10, 10), "line") + ppen.addPoint((10, 40)) + ppen.addPoint((40, 40)) + ppen.addPoint((10, 40), "qcurve") + ppen.endPath() + self.assertEqual("10 10 moveto 10 40 40 40 10 40 qcurveto closepath", repr(pen)) + + def test_quad_onlyOffCurvePoints(self): + pen = _TestSegmentPen() + ppen = PointToSegmentPen(pen) + ppen.beginPath() + ppen.addPoint((10, 10)) + ppen.addPoint((10, 40)) + ppen.addPoint((40, 40)) + ppen.endPath() + self.assertEqual("10 10 10 40 40 40 None qcurveto closepath", repr(pen)) + + def test_roundTrip1(self): + tpen = _TestPointPen() + ppen = PointToSegmentPen(SegmentToPointPen(tpen)) + ppen.beginPath() + ppen.addPoint((10, 10), "line") + ppen.addPoint((10, 20)) + ppen.addPoint((20, 20)) + ppen.addPoint((20, 40), "curve") + ppen.endPath() + self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') addPoint((10, 20)) " + "addPoint((20, 20)) addPoint((20, 40), segmentType='curve') endPath()", + repr(tpen)) + + +class TestSegmentToPointPen(unittest.TestCase): + + def test_move(self): + tpen = _TestPointPen() + pen = SegmentToPointPen(tpen) + pen.moveTo((10, 10)) + pen.endPath() + self.assertEqual("beginPath() addPoint((10, 10), segmentType='move') endPath()", + repr(tpen)) + + def test_poly(self): + tpen = _TestPointPen() + pen = SegmentToPointPen(tpen) + pen.moveTo((10, 10)) + pen.lineTo((10, 20)) + pen.lineTo((20, 20)) + pen.closePath() + self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') " + "addPoint((10, 20), segmentType='line') " + "addPoint((20, 20), segmentType='line') endPath()", + repr(tpen)) + + def test_cubic(self): + tpen = _TestPointPen() + pen = SegmentToPointPen(tpen) + pen.moveTo((10, 10)) + pen.curveTo((10, 20), (20, 20), (20, 10)) + pen.closePath() + self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') " + "addPoint((10, 20)) addPoint((20, 20)) addPoint((20, 10), " + "segmentType='curve') endPath()", repr(tpen)) + + def test_quad(self): + tpen = _TestPointPen() + pen = SegmentToPointPen(tpen) + pen.moveTo((10, 10)) + pen.qCurveTo((10, 20), (20, 20), (20, 10)) + pen.closePath() + self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') " + "addPoint((10, 20)) addPoint((20, 20)) " + "addPoint((20, 10), segmentType=qcurve) endPath()", + repr(tpen)) + + def test_quad(self): + tpen = _TestPointPen() + pen = SegmentToPointPen(tpen) + pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None) + pen.closePath() + self.assertEqual("beginPath() addPoint((10, 20)) addPoint((20, 20)) " + "addPoint((20, 10)) addPoint((10, 10)) endPath()", + repr(tpen)) + + def test_roundTrip1(self): + spen = _TestSegmentPen() + pen = SegmentToPointPen(PointToSegmentPen(spen)) + pen.moveTo((10, 10)) + pen.lineTo((10, 20)) + pen.lineTo((20, 20)) + pen.closePath() + self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(spen)) + + def test_roundTrip2(self): + spen = _TestSegmentPen() + pen = SegmentToPointPen(PointToSegmentPen(spen)) + pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None) + pen.closePath() + pen.addComponent('base', [1, 0, 0, 1, 0, 0]) + self.assertEqual("10 20 20 20 20 10 10 10 None qcurveto closepath " + "'base' [1, 0, 0, 1, 0, 0] addcomponent", + repr(spen)) + + +class TestGuessSmoothPointPen(unittest.TestCase): + + def test_guessSmooth_exact(self): + tpen = _TestPointPen() + pen = GuessSmoothPointPen(tpen) + pen.beginPath(identifier="foo") + pen.addPoint((0, 100), segmentType="curve") + pen.addPoint((0, 200)) + pen.addPoint((400, 200), identifier='bar') + pen.addPoint((400, 100), segmentType="curve") + pen.addPoint((400, 0)) + pen.addPoint((0, 0)) + pen.endPath() + self.assertEqual("beginPath(identifier='foo') " + "addPoint((0, 100), segmentType='curve', smooth=True) " + "addPoint((0, 200)) addPoint((400, 200), identifier='bar') " + "addPoint((400, 100), segmentType='curve', smooth=True) " + "addPoint((400, 0)) addPoint((0, 0)) endPath()", + repr(tpen)) + + def test_guessSmooth_almost(self): + tpen = _TestPointPen() + pen = GuessSmoothPointPen(tpen) + pen.beginPath() + pen.addPoint((0, 100), segmentType="curve") + pen.addPoint((1, 200)) + pen.addPoint((395, 200)) + pen.addPoint((400, 100), segmentType="curve") + pen.addPoint((400, 0)) + pen.addPoint((0, 0)) + pen.endPath() + self.assertEqual("beginPath() addPoint((0, 100), segmentType='curve', smooth=True) " + "addPoint((1, 200)) addPoint((395, 200)) " + "addPoint((400, 100), segmentType='curve', smooth=True) " + "addPoint((400, 0)) addPoint((0, 0)) endPath()", + repr(tpen)) + + def test_guessSmooth_tangent(self): + tpen = _TestPointPen() + pen = GuessSmoothPointPen(tpen) + pen.beginPath() + pen.addPoint((0, 0), segmentType="move") + pen.addPoint((0, 100), segmentType="line") + pen.addPoint((3, 200)) + pen.addPoint((300, 200)) + pen.addPoint((400, 200), segmentType="curve") + pen.endPath() + self.assertEqual("beginPath() addPoint((0, 0), segmentType='move') " + "addPoint((0, 100), segmentType='line', smooth=True) " + "addPoint((3, 200)) addPoint((300, 200)) " + "addPoint((400, 200), segmentType='curve') endPath()", + repr(tpen)) + +class TestReverseContourPointPen(unittest.TestCase): + + def test_singlePoint(self): + tpen = _TestPointPen() + pen = ReverseContourPointPen(tpen) + pen.beginPath() + pen.addPoint((0, 0), segmentType="move") + pen.endPath() + self.assertEqual("beginPath() " + "addPoint((0, 0), segmentType='move') " + "endPath()", + repr(tpen)) + + def test_line(self): + tpen = _TestPointPen() + pen = ReverseContourPointPen(tpen) + pen.beginPath() + pen.addPoint((0, 0), segmentType="move") + pen.addPoint((0, 100), segmentType="line") + pen.endPath() + self.assertEqual("beginPath() " + "addPoint((0, 100), segmentType='move') " + "addPoint((0, 0), segmentType='line') " + "endPath()", + repr(tpen)) + + def test_triangle(self): + tpen = _TestPointPen() + pen = ReverseContourPointPen(tpen) + pen.beginPath() + pen.addPoint((0, 0), segmentType="line") + pen.addPoint((0, 100), segmentType="line") + pen.addPoint((100, 100), segmentType="line") + pen.endPath() + self.assertEqual("beginPath() " + "addPoint((0, 0), segmentType='line') " + "addPoint((100, 100), segmentType='line') " + "addPoint((0, 100), segmentType='line') " + "endPath()", + repr(tpen)) + + def test_cubicOpen(self): + tpen = _TestPointPen() + pen = ReverseContourPointPen(tpen) + pen.beginPath() + pen.addPoint((0, 0), segmentType="move") + pen.addPoint((0, 100)) + pen.addPoint((100, 200)) + pen.addPoint((200, 200), segmentType="curve") + pen.endPath() + self.assertEqual("beginPath() " + "addPoint((200, 200), segmentType='move') " + "addPoint((100, 200)) " + "addPoint((0, 100)) " + "addPoint((0, 0), segmentType='curve') " + "endPath()", + repr(tpen)) + + def test_quadOpen(self): + tpen = _TestPointPen() + pen = ReverseContourPointPen(tpen) + pen.beginPath() + pen.addPoint((0, 0), segmentType="move") + pen.addPoint((0, 100)) + pen.addPoint((100, 200)) + pen.addPoint((200, 200), segmentType="qcurve") + pen.endPath() + self.assertEqual("beginPath() " + "addPoint((200, 200), segmentType='move') " + "addPoint((100, 200)) " + "addPoint((0, 100)) " + "addPoint((0, 0), segmentType='qcurve') " + "endPath()", + repr(tpen)) + + def test_cubicClosed(self): + tpen = _TestPointPen() + pen = ReverseContourPointPen(tpen) + pen.beginPath() + pen.addPoint((0, 0), segmentType="line") + pen.addPoint((0, 100)) + pen.addPoint((100, 200)) + pen.addPoint((200, 200), segmentType="curve") + pen.endPath() + self.assertEqual("beginPath() " + "addPoint((0, 0), segmentType='curve') " + "addPoint((200, 200), segmentType='line') " + "addPoint((100, 200)) " + "addPoint((0, 100)) " + "endPath()", + repr(tpen)) + + def test_quadClosedOffCurveStart(self): + tpen = _TestPointPen() + pen = ReverseContourPointPen(tpen) + pen.beginPath() + pen.addPoint((100, 200)) + pen.addPoint((200, 200), segmentType="qcurve") + pen.addPoint((0, 0), segmentType="line") + pen.addPoint((0, 100)) + pen.endPath() + self.assertEqual("beginPath() " + "addPoint((100, 200)) " + "addPoint((0, 100)) " + "addPoint((0, 0), segmentType='qcurve') " + "addPoint((200, 200), segmentType='line') " + "endPath()", + repr(tpen)) + + def test_quadNoOnCurve(self): + tpen = _TestPointPen() + pen = ReverseContourPointPen(tpen) + pen.beginPath(identifier='bar') + pen.addPoint((0, 0)) + pen.addPoint((0, 100), identifier='foo', arbitrary='foo') + pen.addPoint((100, 200), arbitrary=123) + pen.addPoint((200, 200)) + pen.endPath() + pen.addComponent("base", [1, 0, 0, 1, 0, 0], identifier='foo') + self.assertEqual("beginPath(identifier='bar') " + "addPoint((0, 0)) " + "addPoint((200, 200)) " + "addPoint((100, 200), arbitrary=123) " + "addPoint((0, 100), identifier='foo', arbitrary='foo') " + "endPath() " + "addComponent('base', [1, 0, 0, 1, 0, 0], identifier='foo')", + repr(tpen)) diff --git a/Tests/t1Lib/t1Lib_test.py b/Tests/t1Lib/t1Lib_test.py index f5e934de..385dad0d 100644 --- a/Tests/t1Lib/t1Lib_test.py +++ b/Tests/t1Lib/t1Lib_test.py @@ -2,6 +2,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * import unittest import os +import sys from fontTools import t1Lib from fontTools.pens.basePen import NullPen import random @@ -50,6 +51,11 @@ class ReadWriteTest(unittest.TestCase): data = self.write(font, 'OTHER', dohex=True) self.assertEqual(font.getData(), data) + @unittest.skipIf(sys.version_info[:2] < (3, 6), "pathlib is only tested on 3.6 and up") + def test_read_with_path(self): + import pathlib + font = t1Lib.T1Font(pathlib.Path(PFB)) + @staticmethod def write(font, outtype, dohex=False): temp = os.path.join(DATADIR, 'temp.' + outtype.lower()) diff --git a/Tests/ttLib/tables/_k_e_r_n_test.py b/Tests/ttLib/tables/_k_e_r_n_test.py index 8ab1b1cc..b37748ad 100644 --- a/Tests/ttLib/tables/_k_e_r_n_test.py +++ b/Tests/ttLib/tables/_k_e_r_n_test.py @@ -5,6 +5,7 @@ from fontTools.ttLib.tables._k_e_r_n import ( KernTable_format_0, KernTable_format_unkown) from fontTools.misc.textTools import deHexStr from fontTools.misc.testTools import FakeFont, getXML, parseXML +import itertools import pytest @@ -120,12 +121,32 @@ KERN_VER_1_FMT_UNKNOWN_XML = [ '</kernsubtable>', ] +KERN_VER_0_FMT_0_OVERFLOWING_DATA = deHexStr( + '0000 ' # 0: version=0 + '0001 ' # 2: nTables=1 + '0000 ' # 4: version=0 (bogus field, unused) + '0274 ' # 6: length=628 (bogus value for 66164 % 0x10000) + '00 ' # 8: format=0 + '01 ' # 9: coverage=1 + '2B11 ' # 10: nPairs=11025 + 'C000 ' # 12: searchRange=49152 + '000D ' # 14: entrySelector=13 + '4266 ' # 16: rangeShift=16998 +) + deHexStr(' '.join( + '%04X %04X %04X' % (a, b, 0) + for (a, b) in itertools.product(range(105), repeat=2) +)) + @pytest.fixture def font(): return FakeFont(list("ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz")) +@pytest.fixture +def overflowing_font(): + return FakeFont(["glyph%i" % i for i in range(105)]) + class KernTableTest(object): @@ -364,6 +385,36 @@ class KernTable_format_0_Test(object): assert subtable[("B", "D")] == 1 assert subtable[("B", "glyph65535")] == 2 + def test_compileOverflowingSubtable(self, overflowing_font): + font = overflowing_font + kern = newTable("kern") + kern.version = 0 + st = KernTable_format_0(0) + kern.kernTables = [st] + st.coverage = 1 + st.tupleIndex = None + st.kernTable = { + (a, b): 0 + for (a, b) in itertools.product( + font.getGlyphOrder(), repeat=2) + } + assert len(st.kernTable) == 11025 + data = kern.compile(font) + assert data == KERN_VER_0_FMT_0_OVERFLOWING_DATA + + def test_decompileOverflowingSubtable(self, overflowing_font): + font = overflowing_font + data = KERN_VER_0_FMT_0_OVERFLOWING_DATA + kern = newTable("kern") + kern.decompile(data, font) + + st = kern.kernTables[0] + assert st.kernTable == { + (a, b): 0 + for (a, b) in itertools.product( + font.getGlyphOrder(), repeat=2) + } + if __name__ == "__main__": import sys diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py index a27e3c1a..fde14bc8 100644 --- a/Tests/ttLib/tables/_n_a_m_e_test.py +++ b/Tests/ttLib/tables/_n_a_m_e_test.py @@ -93,12 +93,12 @@ class NameTableTest(unittest.TestCase): "en": "Width", "de-CH": "Breite", "gsw-LI": "Bräiti", - }, ttFont=font) + }, ttFont=font, mac=False) self.assertEqual(widthID, 256) xHeightID = nameTable.addMultilingualName({ "en": "X-Height", "gsw-LI": "X-Hööchi" - }, ttFont=font) + }, ttFont=font, mac=False) self.assertEqual(xHeightID, 257) captor.assertRegex("cannot add Windows name in language gsw-LI") self.assertEqual(names(nameTable), [ diff --git a/Tests/ufoLib/testSupport.py b/Tests/ufoLib/testSupport.py index 2982ce84..2982ce84 100755..100644 --- a/Tests/ufoLib/testSupport.py +++ b/Tests/ufoLib/testSupport.py diff --git a/Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace b/Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace new file mode 100644 index 00000000..b47227f4 --- /dev/null +++ b/Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace @@ -0,0 +1,22 @@ +<?xml version='1.0' encoding='UTF-8'?> +<designspace format="4.0"> + <axes> + <axis tag="slnt" name="Slant" minimum="-15" maximum="0" default="0"/> + </axes> + <sources> + <source filename="master_ufo/TestFamily4-Regular.ufo" name="master_0" familyname="Test Family 4" stylename="Regular"> + <lib copy="1"/> + <groups copy="1"/> + <features copy="1"/> + <info copy="1"/> + <location> + <dimension name="Slant" xvalue="0"/> + </location> + </source> + <source filename="master_ufo/TestFamily4-Italic15.ufo" name="master_1" familyname="Test Family 4" stylename="Italic"> + <location> + <dimension name="Slant" xvalue="-15"/> + </location> + </source> + </sources> +</designspace> diff --git a/Tests/varLib/data/FeatureVars.designspace b/Tests/varLib/data/FeatureVars.designspace index d641ba21..9c958a5a 100644 --- a/Tests/varLib/data/FeatureVars.designspace +++ b/Tests/varLib/data/FeatureVars.designspace @@ -9,7 +9,7 @@ <rules> <rule name="dollar-stroke"> <conditionset> - <condition name="weight" minimum="500" maximum="1000" /> + <condition name="weight" minimum="500" /> <!-- intentionally omitted maximum --> </conditionset> <sub name="uni0024" with="uni0024.nostroke" /> </rule> diff --git a/Tests/varLib/data/TestCFF2.designspace b/Tests/varLib/data/TestCFF2.designspace new file mode 100644 index 00000000..92c45baa --- /dev/null +++ b/Tests/varLib/data/TestCFF2.designspace @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8"?> +<designspace format="3"> + <axes> + <axis default="400.0" maximum="900.0" minimum="200.0" name="weight" tag="wght"> + <map input="200" output="0" /> <!-- ExtraLight --> + <map input="300" output="100" /> <!-- Light --> + <map input="400" output="368" /> <!-- Regular --> + <map input="500" output="486" /> <!-- Medium --> + <map input="600" output="600" /> <!-- Semibold --> + <map input="700" output="824" /> <!-- Bold --> + <map input="900" output="1000" /><!-- Black --> + </axis> + </axes> + <rules> + <rule name="named.rule.1"> + <conditionset> + <condition maximum="600" minimum="0" name="weight" /> + </conditionset> + <sub name="dollar" with="dollar.a" /> + </rule> + </rules> + <sources> + <source filename="master_cff2/TestCFF2_ExtraLight.ufo" name="master_0"> + <lib copy="1" /> + <location> + <dimension name="weight" xvalue="0" /> + </location> + </source> + <source filename="master_cff2/TestCFF2_Regular.ufo" name="master_1"> + <glyph mute="1" name="T" /> + <info copy="1" /> + <location> + <dimension name="weight" xvalue="368" /> + </location> + </source> + <source filename="master_cff2/TestCFF2_Black.ufo" name="master_2"> + <location> + <dimension name="weight" xvalue="1000" /> + </location> + </source> + </sources> + <instances> + <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-ExtraLight" stylename="ExtraLight"> + <location> + <dimension name="weight" xvalue="0" /> + </location> + <kerning /> + <info /> + </instance> + <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Light" stylename="Light"> + <location> + <dimension name="weight" xvalue="100" /> + </location> + <kerning /> + <info /> + </instance> + <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Regular" stylename="Regular"> + <location> + <dimension name="weight" xvalue="368" /> + </location> + <kerning /> + <info /> + </instance> + <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Medium" stylename="Medium"> + <location> + <dimension name="weight" xvalue="486" /> + </location> + <kerning /> + <info /> + </instance> + <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Semibold" stylename="Semibold"> + <location> + <dimension name="weight" xvalue="600" /> + </location> + <kerning /> + <info /> + </instance> + <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Bold" stylename="Bold"> + <location> + <dimension name="weight" xvalue="824" /> + </location> + <kerning /> + <info /> + </instance> + <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Black" stylename="Black"> + <location> + <dimension name="weight" xvalue="1000" /> + </location> + <kerning /> + <info /> + </instance> + </instances> +</designspace> diff --git a/Tests/varLib/data/TestCFF2VF.otf b/Tests/varLib/data/TestCFF2VF.otf Binary files differnew file mode 100644 index 00000000..590ad271 --- /dev/null +++ b/Tests/varLib/data/TestCFF2VF.otf diff --git a/Tests/varLib/data/master_cff2/TestCFF2_Black.otf b/Tests/varLib/data/master_cff2/TestCFF2_Black.otf Binary files differnew file mode 100644 index 00000000..b4249e10 --- /dev/null +++ b/Tests/varLib/data/master_cff2/TestCFF2_Black.otf diff --git a/Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otf b/Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otf Binary files differnew file mode 100644 index 00000000..4464791a --- /dev/null +++ b/Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otf diff --git a/Tests/varLib/data/master_cff2/TestCFF2_Regular.otf b/Tests/varLib/data/master_cff2/TestCFF2_Regular.otf Binary files differnew file mode 100644 index 00000000..c87342e4 --- /dev/null +++ b/Tests/varLib/data/master_cff2/TestCFF2_Regular.otf diff --git a/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx b/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx new file mode 100644 index 00000000..5360fe40 --- /dev/null +++ b/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx @@ -0,0 +1,555 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.10"> + + <GlyphOrder> + <!-- The 'id' attribute is only for humans; it is ignored when parsed. --> + <GlyphID id="0" name=".notdef"/> + <GlyphID id="1" name="NULL"/> + <GlyphID id="2" name="nonmarkingreturn"/> + <GlyphID id="3" name="space"/> + <GlyphID id="4" name="b"/> + <GlyphID id="5" name="q"/> + <GlyphID id="6" name="a"/> + </GlyphOrder> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.0"/> + <checkSumAdjustment value="0xe59c28a1"/> + <magicNumber value="0x5f0f3cf5"/> + <flags value="00000000 00001011"/> + <unitsPerEm value="1000"/> + <created value="Thu Apr 27 12:41:42 2017"/> + <modified value="Tue May 2 16:43:12 2017"/> + <xMin value="38"/> + <yMin value="-152"/> + <xMax value="456"/> + <yMax value="608"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="9"/> + <fontDirectionHint value="2"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="750"/> + <descent value="-250"/> + <lineGap value="9"/> + <advanceWidthMax value="494"/> + <minLeftSideBearing value="38"/> + <minRightSideBearing value="38"/> + <xMaxExtent value="456"/> + <caretSlopeRise value="1"/> + <caretSlopeRun value="0"/> + <caretOffset value="0"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="5"/> + </hhea> + + <maxp> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="0x10000"/> + <numGlyphs value="7"/> + <maxPoints value="20"/> + <maxContours value="2"/> + <maxCompositePoints value="20"/> + <maxCompositeContours value="2"/> + <maxZones value="1"/> + <maxTwilightPoints value="0"/> + <maxStorage value="0"/> + <maxFunctionDefs value="0"/> + <maxInstructionDefs value="0"/> + <maxStackElements value="0"/> + <maxSizeOfInstructions value="0"/> + <maxComponentElements value="1"/> + <maxComponentDepth value="1"/> + </maxp> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="3"/> + <xAvgCharWidth value="347"/> + <usWeightClass value="400"/> + <usWidthClass value="5"/> + <fsType value="00000000 00000100"/> + <ySubscriptXSize value="700"/> + <ySubscriptYSize value="650"/> + <ySubscriptXOffset value="0"/> + <ySubscriptYOffset value="140"/> + <ySuperscriptXSize value="700"/> + <ySuperscriptYSize value="650"/> + <ySuperscriptXOffset value="0"/> + <ySuperscriptYOffset value="477"/> + <yStrikeoutSize value="50"/> + <yStrikeoutPosition value="250"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="0"/> + <bSerifStyle value="0"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="0"/> + <bStrokeVariation value="0"/> + <bArmStyle value="0"/> + <bLetterForm value="0"/> + <bMidline value="0"/> + <bXHeight value="0"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/> + <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="LuFo"/> + <fsSelection value="00000000 01000000"/> + <usFirstCharIndex value="0"/> + <usLastCharIndex value="113"/> + <sTypoAscender value="750"/> + <sTypoDescender value="-250"/> + <sTypoLineGap value="0"/> + <usWinAscent value="608"/> + <usWinDescent value="152"/> + <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="456"/> + <sCapHeight value="608"/> + <usDefaultChar value="0"/> + <usBreakChar value="32"/> + <usMaxContext value="0"/> + </OS_2> + + <hmtx> + <mtx name=".notdef" width="200" lsb="0"/> + <mtx name="NULL" width="0" lsb="0"/> + <mtx name="a" width="494" lsb="38"/> + <mtx name="b" width="494" lsb="76"/> + <mtx name="nonmarkingreturn" width="200" lsb="0"/> + <mtx name="q" width="494" lsb="38"/> + <mtx name="space" width="200" lsb="0"/> + </hmtx> + + <cmap> + <tableVersion version="0"/> + <cmap_format_4 platformID="0" platEncID="3" language="0"> + <map code="0x0" name="NULL"/><!-- ???? --> + <map code="0xd" name="nonmarkingreturn"/><!-- ???? --> + <map code="0x20" name="space"/><!-- SPACE --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + <map code="0x62" name="b"/><!-- LATIN SMALL LETTER B --> + <map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q --> + </cmap_format_4> + <cmap_format_6 platformID="1" platEncID="0" language="0"> + <map code="0x0" name="NULL"/> + <map code="0xd" name="nonmarkingreturn"/> + <map code="0x20" name="space"/> + <map code="0x61" name="a"/> + <map code="0x62" name="b"/> + <map code="0x71" name="q"/> + </cmap_format_6> + <cmap_format_4 platformID="3" platEncID="1" language="0"> + <map code="0x0" name="NULL"/><!-- ???? --> + <map code="0xd" name="nonmarkingreturn"/><!-- ???? --> + <map code="0x20" name="space"/><!-- SPACE --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + <map code="0x62" name="b"/><!-- LATIN SMALL LETTER B --> + <map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q --> + </cmap_format_4> + </cmap> + + <loca> + <!-- The 'loca' table will be calculated by the compiler --> + </loca> + + <glyf> + + <!-- The xMin, yMin, xMax and yMax values + will be recalculated by the compiler. --> + + <TTGlyph name=".notdef"/><!-- contains no outline data --> + + <TTGlyph name="NULL"/><!-- contains no outline data --> + + <TTGlyph name="a" xMin="38" yMin="-12" xMax="418" yMax="468"> + <contour> + <pt x="342" y="0" on="1"/> + <pt x="342" y="64" on="1"/> + <pt x="264" y="-12" on="1"/> + <pt x="190" y="-12" on="1"/> + <pt x="38" y="140" on="1"/> + <pt x="38" y="316" on="1"/> + <pt x="190" y="468" on="1"/> + <pt x="266" y="468" on="1"/> + <pt x="342" y="392" on="1"/> + <pt x="342" y="456" on="1"/> + <pt x="418" y="456" on="1"/> + <pt x="418" y="0" on="1"/> + </contour> + <contour> + <pt x="266" y="64" on="1"/> + <pt x="342" y="140" on="1"/> + <pt x="342" y="316" on="1"/> + <pt x="266" y="392" on="1"/> + <pt x="190" y="392" on="1"/> + <pt x="114" y="316" on="1"/> + <pt x="114" y="140" on="1"/> + <pt x="190" y="64" on="1"/> + </contour> + <instructions> + <assembly> + GETVARIATION[] + </assembly> + </instructions> + </TTGlyph> + + <TTGlyph name="b" xMin="76" yMin="-12" xMax="456" yMax="608"> + <contour> + <pt x="228" y="468" on="1"/> + <pt x="304" y="468" on="1"/> + <pt x="456" y="316" on="1"/> + <pt x="456" y="140" on="1"/> + <pt x="304" y="-12" on="1"/> + <pt x="230" y="-12" on="1"/> + <pt x="152" y="64" on="1"/> + <pt x="152" y="0" on="1"/> + <pt x="76" y="0" on="1"/> + <pt x="76" y="608" on="1"/> + <pt x="152" y="608" on="1"/> + <pt x="152" y="392" on="1"/> + </contour> + <contour> + <pt x="152" y="316" on="1"/> + <pt x="152" y="140" on="1"/> + <pt x="228" y="64" on="1"/> + <pt x="304" y="64" on="1"/> + <pt x="380" y="140" on="1"/> + <pt x="380" y="316" on="1"/> + <pt x="304" y="392" on="1"/> + <pt x="228" y="392" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="nonmarkingreturn"/><!-- contains no outline data --> + + <TTGlyph name="q" xMin="38" yMin="-152" xMax="418" yMax="468"> + <component glyphName="b" x="494" y="456" scale="-0.99994" flags="0x4"/> + </TTGlyph> + + <TTGlyph name="space"/><!-- contains no outline data --> + + </glyf> + + <name> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True"> + VarFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Regular + </namerecord> + <namerecord nameID="3" platformID="1" platEncID="0" langID="0x0" unicode="True"> + VarFont Regular: 2017 + </namerecord> + <namerecord nameID="4" platformID="1" platEncID="0" langID="0x0" unicode="True"> + VarFont Regular + </namerecord> + <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True"> + VarFont-Regular + </namerecord> + <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Width + </namerecord> + <namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Ascender + </namerecord> + <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Regular + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + VarFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409"> + Regular + </namerecord> + <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409"> + VarFont Regular: 2017 + </namerecord> + <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409"> + VarFont Regular + </namerecord> + <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409"> + VarFont-Regular + </namerecord> + <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409"> + Width + </namerecord> + <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409"> + Ascender + </namerecord> + <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409"> + Regular + </namerecord> + </name> + + <post> + <formatType value="2.0"/> + <italicAngle value="0.0"/> + <underlinePosition value="-75"/> + <underlineThickness value="50"/> + <isFixedPitch value="0"/> + <minMemType42 value="0"/> + <maxMemType42 value="0"/> + <minMemType1 value="0"/> + <maxMemType1 value="0"/> + <psNames> + <!-- This file uses unique glyph names based on the information + found in the 'post' table. Since these names might not be unique, + we have to invent artificial names in case of clashes. In order to + be able to retain the original information, we need a name to + ps name mapping for those cases where they differ. That's what + you see below. + --> + </psNames> + <extraNames> + <!-- following are the name that are not taken from the standard Mac glyph order --> + <psName name="NULL"/> + </extraNames> + </post> + + <GDEF> + <Version value="0x00010003"/> + <VarStore Format="1"> + <Format value="1"/> + <VarRegionList> + <!-- RegionAxisCount=2 --> + <!-- RegionCount=2 --> + <Region index="0"> + <VarRegionAxis index="0"> + <StartCoord value="0.0"/> + <PeakCoord value="0.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + <VarRegionAxis index="1"> + <StartCoord value="0.0"/> + <PeakCoord value="1.0"/> + <EndCoord value="1.0"/> + </VarRegionAxis> + </Region> + <Region index="1"> + <VarRegionAxis index="0"> + <StartCoord value="-1.0"/> + <PeakCoord value="-1.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + <VarRegionAxis index="1"> + <StartCoord value="0.0"/> + <PeakCoord value="0.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + </Region> + </VarRegionList> + <!-- VarDataCount=1 --> + <VarData index="0"> + <!-- ItemCount=0 --> + <NumShorts value="0"/> + <!-- VarRegionCount=2 --> + <VarRegionIndex index="0" value="0"/> + <VarRegionIndex index="1" value="1"/> + </VarData> + </VarStore> + </GDEF> + + <HVAR> + <Version value="0x00010000"/> + <VarStore Format="1"> + <Format value="1"/> + <VarRegionList> + <!-- RegionAxisCount=2 --> + <!-- RegionCount=2 --> + <Region index="0"> + <VarRegionAxis index="0"> + <StartCoord value="0.0"/> + <PeakCoord value="0.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + <VarRegionAxis index="1"> + <StartCoord value="0.0"/> + <PeakCoord value="1.0"/> + <EndCoord value="1.0"/> + </VarRegionAxis> + </Region> + <Region index="1"> + <VarRegionAxis index="0"> + <StartCoord value="-1.0"/> + <PeakCoord value="-1.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + <VarRegionAxis index="1"> + <StartCoord value="0.0"/> + <PeakCoord value="0.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + </Region> + </VarRegionList> + <!-- VarDataCount=1 --> + <VarData index="0"> + <!-- ItemCount=2 --> + <NumShorts value="0"/> + <!-- VarRegionCount=2 --> + <VarRegionIndex index="0" value="0"/> + <VarRegionIndex index="1" value="1"/> + <Item index="0" value="[0, -60]"/> + <Item index="1" value="[0, 0]"/> + </VarData> + </VarStore> + <AdvWidthMap> + <Map glyph=".notdef" outer="0" inner="1"/> + <Map glyph="NULL" outer="0" inner="1"/> + <Map glyph="a" outer="0" inner="0"/> + <Map glyph="b" outer="0" inner="0"/> + <Map glyph="q" outer="0" inner="0"/> + <Map glyph="nonmarkingreturn" outer="0" inner="1"/> + <Map glyph="space" outer="0" inner="1"/> + </AdvWidthMap> + </HVAR> + + <MVAR> + <Version value="0x00010000"/> + <Reserved value="0"/> + <ValueRecordSize value="0"/> + <!-- ValueRecordCount=0 --> + <VarStore Format="1"> + <Format value="1"/> + <VarRegionList> + <!-- RegionAxisCount=2 --> + <!-- RegionCount=2 --> + <Region index="0"> + <VarRegionAxis index="0"> + <StartCoord value="0.0"/> + <PeakCoord value="0.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + <VarRegionAxis index="1"> + <StartCoord value="0.0"/> + <PeakCoord value="1.0"/> + <EndCoord value="1.0"/> + </VarRegionAxis> + </Region> + <Region index="1"> + <VarRegionAxis index="0"> + <StartCoord value="-1.0"/> + <PeakCoord value="-1.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + <VarRegionAxis index="1"> + <StartCoord value="0.0"/> + <PeakCoord value="0.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + </Region> + </VarRegionList> + <!-- VarDataCount=1 --> + <VarData index="0"> + <!-- ItemCount=0 --> + <NumShorts value="0"/> + <!-- VarRegionCount=2 --> + <VarRegionIndex index="0" value="0"/> + <VarRegionIndex index="1" value="1"/> + </VarData> + </VarStore> + </MVAR> + + <fvar> + + <!-- Width --> + <Axis> + <AxisTag>wdth</AxisTag> + <MinValue>60.0</MinValue> + <DefaultValue>100.0</DefaultValue> + <MaxValue>100.0</MaxValue> + <AxisNameID>256</AxisNameID> + </Axis> + + <!-- Ascender --> + <Axis> + <AxisTag>ASCN</AxisTag> + <MinValue>608.0</MinValue> + <DefaultValue>608.0</DefaultValue> + <MaxValue>648.0</MaxValue> + <AxisNameID>257</AxisNameID> + </Axis> + + <!-- Regular --> + <NamedInstance subfamilyNameID="258"> + <coord axis="wdth" value="100.0"/> + <coord axis="ASCN" value="608.0"/> + </NamedInstance> + </fvar> + + <gvar> + <version value="1"/> + <reserved value="0"/> + <glyphVariations glyph="b"> + <tuple> + <coord axis="ASCN" value="1.0"/> + <delta pt="8" x="0" y="0"/> + <delta pt="9" x="0" y="40"/> + <delta pt="10" x="0" y="40"/> + <delta pt="11" x="0" y="0"/> + <delta pt="12" x="0" y="0"/> + <delta pt="22" x="0" y="40"/> + </tuple> + <tuple> + <coord axis="wdth" value="-1.0"/> + <delta pt="0" x="-20" y="0"/> + <delta pt="1" x="-40" y="0"/> + <delta pt="2" x="-60" y="0"/> + <delta pt="3" x="-60" y="0"/> + <delta pt="4" x="-40" y="0"/> + <delta pt="5" x="-20" y="0"/> + <delta pt="6" x="0" y="0"/> + <delta pt="11" x="0" y="0"/> + <delta pt="12" x="0" y="0"/> + <delta pt="17" x="-60" y="0"/> + <delta pt="21" x="-60" y="0"/> + </tuple> + </glyphVariations> + <glyphVariations glyph="q"> + <tuple> + <coord axis="ASCN" value="1.0"/> + <delta pt="4" x="0" y="40"/> + </tuple> + <tuple> + <coord axis="wdth" value="-1.0"/> + <delta pt="0" x="-60" y="0"/> + <delta pt="2" x="-60" y="0"/> + </tuple> + </glyphVariations> + <glyphVariations glyph="a"> + <tuple> + <coord axis="wdth" value="-1.0"/> + <delta pt="0" x="-60" y="0"/> + <delta pt="1" x="-60" y="0"/> + <delta pt="2" x="-40" y="0"/> + <delta pt="3" x="-20" y="0"/> + <delta pt="4" x="0" y="0"/> + <delta pt="5" x="0" y="0"/> + <delta pt="6" x="-20" y="0"/> + <delta pt="7" x="-40" y="0"/> + <delta pt="8" x="-60" y="0"/> + <delta pt="14" x="-60" y="0"/> + <delta pt="20" x="0" y="0"/> + <delta pt="21" x="-60" y="0"/> + <delta pt="22" x="0" y="0"/> + <delta pt="23" x="0" y="0"/> + </tuple> + </glyphVariations> + </gvar> + +</ttFont> diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx new file mode 100644 index 00000000..f7aa15fc --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx @@ -0,0 +1,662 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.34"> + + <GlyphOrder> + <!-- The 'id' attribute is only for humans; it is ignored when parsed. --> + <GlyphID id="0" name=".notdef"/> + <GlyphID id="1" name="N"/> + <GlyphID id="2" name="O"/> + <GlyphID id="3" name="Odieresis"/> + <GlyphID id="4" name="n"/> + <GlyphID id="5" name="o"/> + <GlyphID id="6" name="odieresis"/> + <GlyphID id="7" name="uni0308"/> + </GlyphOrder> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.0"/> + <checkSumAdjustment value="0xc48e411d"/> + <magicNumber value="0x5f0f3cf5"/> + <flags value="00000000 00000011"/> + <unitsPerEm value="750"/> + <created value="Sat Oct 21 13:31:38 2017"/> + <modified value="Mon Dec 17 12:42:07 2018"/> + <xMin value="6"/> + <yMin value="-150"/> + <xMax value="436"/> + <yMax value="650"/> + <macStyle value="00000000 00000010"/> + <lowestRecPPEM value="6"/> + <fontDirectionHint value="2"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="750"/> + <descent value="-150"/> + <lineGap value="0"/> + <advanceWidthMax value="408"/> + <minLeftSideBearing value="6"/> + <minRightSideBearing value="-333"/> + <xMaxExtent value="436"/> + <caretSlopeRise value="1000"/> + <caretSlopeRun value="268"/> + <caretOffset value="0"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="8"/> + </hhea> + + <maxp> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="0x10000"/> + <numGlyphs value="8"/> + <maxPoints value="36"/> + <maxContours value="2"/> + <maxCompositePoints value="64"/> + <maxCompositeContours value="4"/> + <maxZones value="1"/> + <maxTwilightPoints value="0"/> + <maxStorage value="0"/> + <maxFunctionDefs value="0"/> + <maxInstructionDefs value="0"/> + <maxStackElements value="0"/> + <maxSizeOfInstructions value="0"/> + <maxComponentElements value="2"/> + <maxComponentDepth value="1"/> + </maxp> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="4"/> + <xAvgCharWidth value="375"/> + <usWeightClass value="400"/> + <usWidthClass value="5"/> + <fsType value="00000000 00001000"/> + <ySubscriptXSize value="488"/> + <ySubscriptYSize value="450"/> + <ySubscriptXOffset value="-15"/> + <ySubscriptYOffset value="56"/> + <ySuperscriptXSize value="488"/> + <ySuperscriptYSize value="450"/> + <ySuperscriptXOffset value="70"/> + <ySuperscriptYOffset value="263"/> + <yStrikeoutSize value="50"/> + <yStrikeoutPosition value="210"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="0"/> + <bSerifStyle value="0"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="0"/> + <bStrokeVariation value="0"/> + <bArmStyle value="0"/> + <bLetterForm value="0"/> + <bMidline value="0"/> + <bXHeight value="0"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000000 01000011"/> + <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="jens"/> + <fsSelection value="00000000 00000001"/> + <usFirstCharIndex value="78"/> + <usLastCharIndex value="776"/> + <sTypoAscender value="600"/> + <sTypoDescender value="-150"/> + <sTypoLineGap value="150"/> + <usWinAscent value="700"/> + <usWinDescent value="250"/> + <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="350"/> + <sCapHeight value="500"/> + <usDefaultChar value="0"/> + <usBreakChar value="32"/> + <usMaxContext value="1"/> + </OS_2> + + <hmtx> + <mtx name=".notdef" width="375" lsb="38"/> + <mtx name="N" width="408" lsb="11"/> + <mtx name="O" width="404" lsb="33"/> + <mtx name="Odieresis" width="404" lsb="33"/> + <mtx name="n" width="348" lsb="6"/> + <mtx name="o" width="342" lsb="22"/> + <mtx name="odieresis" width="342" lsb="22"/> + <mtx name="uni0308" width="0" lsb="123"/> + </hmtx> + + <cmap> + <tableVersion version="0"/> + <cmap_format_4 platformID="0" platEncID="3" language="0"> + <map code="0x4e" name="N"/><!-- LATIN CAPITAL LETTER N --> + <map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O --> + <map code="0x6e" name="n"/><!-- LATIN SMALL LETTER N --> + <map code="0x6f" name="o"/><!-- LATIN SMALL LETTER O --> + <map code="0xd6" name="Odieresis"/><!-- LATIN CAPITAL LETTER O WITH DIAERESIS --> + <map code="0xf6" name="odieresis"/><!-- LATIN SMALL LETTER O WITH DIAERESIS --> + <map code="0x308" name="uni0308"/><!-- COMBINING DIAERESIS --> + </cmap_format_4> + <cmap_format_4 platformID="3" platEncID="1" language="0"> + <map code="0x4e" name="N"/><!-- LATIN CAPITAL LETTER N --> + <map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O --> + <map code="0x6e" name="n"/><!-- LATIN SMALL LETTER N --> + <map code="0x6f" name="o"/><!-- LATIN SMALL LETTER O --> + <map code="0xd6" name="Odieresis"/><!-- LATIN CAPITAL LETTER O WITH DIAERESIS --> + <map code="0xf6" name="odieresis"/><!-- LATIN SMALL LETTER O WITH DIAERESIS --> + <map code="0x308" name="uni0308"/><!-- COMBINING DIAERESIS --> + </cmap_format_4> + </cmap> + + <loca> + <!-- The 'loca' table will be calculated by the compiler --> + </loca> + + <glyf> + + <!-- The xMin, yMin, xMax and yMax values + will be recalculated by the compiler. --> + + <TTGlyph name=".notdef" xMin="38" yMin="-150" xMax="337" yMax="600"> + <contour> + <pt x="38" y="-150" on="1"/> + <pt x="337" y="-150" on="1"/> + <pt x="337" y="600" on="1"/> + <pt x="38" y="600" on="1"/> + </contour> + <contour> + <pt x="76" y="-112" on="1"/> + <pt x="76" y="562" on="1"/> + <pt x="299" y="562" on="1"/> + <pt x="299" y="-112" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="N" xMin="11" yMin="-4" xMax="436" yMax="504"> + <contour> + <pt x="38" y="-4" on="1"/> + <pt x="26" y="-4" on="0"/> + <pt x="11" y="16" on="0"/> + <pt x="14" y="28" on="1"/> + <pt x="136" y="486" on="1"/> + <pt x="138" y="495" on="0"/> + <pt x="152" y="504" on="0"/> + <pt x="159" y="504" on="1"/> + <pt x="167" y="504" on="0"/> + <pt x="182" y="495" on="0"/> + <pt x="184" y="486" on="1"/> + <pt x="287" y="116" on="1"/> + <pt x="386" y="485" on="1"/> + <pt x="388" y="493" on="0"/> + <pt x="401" y="504" on="0"/> + <pt x="410" y="504" on="1"/> + <pt x="425" y="504" on="0"/> + <pt x="436" y="480" on="0"/> + <pt x="434" y="472" on="1"/> + <pt x="312" y="14" on="1"/> + <pt x="310" y="5" on="0"/> + <pt x="296" y="-4" on="0"/> + <pt x="288" y="-4" on="1"/> + <pt x="281" y="-4" on="0"/> + <pt x="266" y="5" on="0"/> + <pt x="264" y="14" on="1"/> + <pt x="161" y="384" on="1"/> + <pt x="62" y="15" on="1"/> + <pt x="60" y="7" on="0"/> + <pt x="47" y="-4" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="O" xMin="33" yMin="-10" xMax="412" yMax="510"> + <contour> + <pt x="159" y="-10" on="1"/> + <pt x="116" y="-10" on="0"/> + <pt x="57" y="33" on="0"/> + <pt x="33" y="105" on="0"/> + <pt x="44" y="146" on="1"/> + <pt x="103" y="366" on="1"/> + <pt x="114" y="406" on="0"/> + <pt x="169" y="471" on="0"/> + <pt x="245" y="510" on="0"/> + <pt x="285" y="510" on="1"/> + <pt x="328" y="510" on="0"/> + <pt x="388" y="467" on="0"/> + <pt x="412" y="396" on="0"/> + <pt x="401" y="354" on="1"/> + <pt x="342" y="133" on="1"/> + <pt x="332" y="94" on="0"/> + <pt x="275" y="29" on="0"/> + <pt x="199" y="-10" on="0"/> + </contour> + <contour> + <pt x="159" y="40" on="1"/> + <pt x="188" y="40" on="0"/> + <pt x="244" y="69" on="0"/> + <pt x="286" y="117" on="0"/> + <pt x="294" y="146" on="1"/> + <pt x="353" y="366" on="1"/> + <pt x="360" y="393" on="0"/> + <pt x="347" y="435" on="0"/> + <pt x="312" y="460" on="0"/> + <pt x="285" y="460" on="1"/> + <pt x="256" y="460" on="0"/> + <pt x="200" y="431" on="0"/> + <pt x="159" y="383" on="0"/> + <pt x="151" y="354" on="1"/> + <pt x="92" y="133" on="1"/> + <pt x="85" y="107" on="0"/> + <pt x="98" y="65" on="0"/> + <pt x="133" y="40" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="Odieresis" xMin="33" yMin="-10" xMax="425" yMax="650"> + <component glyphName="O" x="0" y="0" flags="0x204"/> + <component glyphName="uni0308" x="92" y="150" flags="0x4"/> + </TTGlyph> + + <TTGlyph name="n" xMin="6" yMin="-4" xMax="327" yMax="350"> + <contour> + <pt x="34" y="-4" on="1"/> + <pt x="22" y="-4" on="0"/> + <pt x="6" y="15" on="0"/> + <pt x="10" y="28" on="1"/> + <pt x="90" y="329" on="1"/> + <pt x="96" y="350" on="0"/> + <pt x="118" y="350" on="1"/> + <pt x="238" y="350" on="1"/> + <pt x="269" y="350" on="0"/> + <pt x="311" y="321" on="0"/> + <pt x="327" y="272" on="0"/> + <pt x="319" y="242" on="1"/> + <pt x="258" y="15" on="1"/> + <pt x="256" y="6" on="0"/> + <pt x="243" y="-4" on="0"/> + <pt x="234" y="-4" on="1"/> + <pt x="222" y="-4" on="0"/> + <pt x="206" y="15" on="0"/> + <pt x="210" y="28" on="1"/> + <pt x="271" y="255" on="1"/> + <pt x="276" y="275" on="0"/> + <pt x="259" y="300" on="0"/> + <pt x="238" y="300" on="1"/> + <pt x="134" y="300" on="1"/> + <pt x="58" y="15" on="1"/> + <pt x="56" y="7" on="0"/> + <pt x="43" y="-4" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="o" xMin="22" yMin="-10" xMax="322" yMax="360"> + <contour> + <pt x="129" y="-10" on="1"/> + <pt x="91" y="-10" on="0"/> + <pt x="40" y="25" on="0"/> + <pt x="22" y="86" on="0"/> + <pt x="32" y="124" on="1"/> + <pt x="64" y="240" on="1"/> + <pt x="74" y="276" on="0"/> + <pt x="119" y="330" on="0"/> + <pt x="180" y="360" on="0"/> + <pt x="215" y="360" on="1"/> + <pt x="253" y="360" on="0"/> + <pt x="304" y="325" on="0"/> + <pt x="323" y="265" on="0"/> + <pt x="312" y="226" on="1"/> + <pt x="280" y="110" on="1"/> + <pt x="270" y="75" on="0"/> + <pt x="225" y="20" on="0"/> + <pt x="164" y="-10" on="0"/> + </contour> + <contour> + <pt x="129" y="40" on="1"/> + <pt x="165" y="40" on="0"/> + <pt x="222" y="86" on="0"/> + <pt x="232" y="124" on="1"/> + <pt x="264" y="240" on="1"/> + <pt x="273" y="273" on="0"/> + <pt x="247" y="310" on="0"/> + <pt x="215" y="310" on="1"/> + <pt x="179" y="310" on="0"/> + <pt x="123" y="264" on="0"/> + <pt x="112" y="226" on="1"/> + <pt x="80" y="110" on="1"/> + <pt x="71" y="77" on="0"/> + <pt x="97" y="40" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="odieresis" xMin="22" yMin="-10" xMax="354" yMax="500"> + <component glyphName="o" x="0" y="0" flags="0x204"/> + <component glyphName="uni0308" x="21" y="0" flags="0x4"/> + </TTGlyph> + + <TTGlyph name="uni0308" xMin="123" yMin="425" xMax="333" yMax="500"> + <contour> + <pt x="300" y="425" on="1"/> + <pt x="288" y="425" on="0"/> + <pt x="273" y="445" on="0"/> + <pt x="276" y="456" on="1"/> + <pt x="282" y="480" on="1"/> + <pt x="284" y="489" on="0"/> + <pt x="297" y="500" on="0"/> + <pt x="306" y="500" on="1"/> + <pt x="318" y="500" on="0"/> + <pt x="333" y="480" on="0"/> + <pt x="330" y="469" on="1"/> + <pt x="324" y="445" on="1"/> + <pt x="322" y="436" on="0"/> + <pt x="309" y="425" on="0"/> + </contour> + <contour> + <pt x="150" y="425" on="1"/> + <pt x="138" y="425" on="0"/> + <pt x="123" y="445" on="0"/> + <pt x="126" y="456" on="1"/> + <pt x="132" y="480" on="1"/> + <pt x="134" y="489" on="0"/> + <pt x="147" y="500" on="0"/> + <pt x="156" y="500" on="1"/> + <pt x="168" y="500" on="0"/> + <pt x="183" y="480" on="0"/> + <pt x="180" y="469" on="1"/> + <pt x="174" y="445" on="1"/> + <pt x="172" y="436" on="0"/> + <pt x="159" y="425" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + </glyf> + + <name> + <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409"> + Copyright 2017 by Jens Kutilek + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + Test Family 4 15 + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409"> + Italic + </namerecord> + <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409"> + 1.000;jens;TestFamily4-Italic15 + </namerecord> + <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409"> + Test Family 4 Italic 15 + </namerecord> + <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409"> + Version 1.000 + </namerecord> + <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409"> + TestFamily4-Italic15 + </namerecord> + <namerecord nameID="8" platformID="3" platEncID="1" langID="0x409"> + Jens Kutilek + </namerecord> + <namerecord nameID="9" platformID="3" platEncID="1" langID="0x409"> + Jens Kutilek after the ISO 3098 standard + </namerecord> + <namerecord nameID="11" platformID="3" platEncID="1" langID="0x409"> + https://www.kutilek.de/ + </namerecord> + <namerecord nameID="12" platformID="3" platEncID="1" langID="0x409"> + https://www.kutilek.de/ + </namerecord> + <namerecord nameID="16" platformID="3" platEncID="1" langID="0x409"> + Test Family 4 + </namerecord> + <namerecord nameID="17" platformID="3" platEncID="1" langID="0x409"> + Italic 15 + </namerecord> + </name> + + <post> + <formatType value="2.0"/> + <italicAngle value="-15.0"/> + <underlinePosition value="-100"/> + <underlineThickness value="50"/> + <isFixedPitch value="0"/> + <minMemType42 value="0"/> + <maxMemType42 value="0"/> + <minMemType1 value="0"/> + <maxMemType1 value="0"/> + <psNames> + <!-- This file uses unique glyph names based on the information + found in the 'post' table. Since these names might not be unique, + we have to invent artificial names in case of clashes. In order to + be able to retain the original information, we need a name to + ps name mapping for those cases where they differ. That's what + you see below. + --> + </psNames> + <extraNames> + <!-- following are the name that are not taken from the standard Mac glyph order --> + <psName name="dieresiscomb"/> + <psName name="uni0308"/> + </extraNames> + </post> + + <GDEF> + <Version value="0x00010002"/> + <GlyphClassDef Format="2"> + <ClassDef glyph="N" class="1"/> + <ClassDef glyph="O" class="1"/> + <ClassDef glyph="Odieresis" class="1"/> + <ClassDef glyph="n" class="1"/> + <ClassDef glyph="o" class="1"/> + <ClassDef glyph="odieresis" class="1"/> + <ClassDef glyph="uni0308" class="3"/> + </GlyphClassDef> + <MarkGlyphSetsDef> + <MarkSetTableFormat value="1"/> + <!-- MarkSetCount=1 --> + <Coverage index="0" Format="1"> + <Glyph value="uni0308"/> + </Coverage> + </MarkGlyphSetsDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=2 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="1"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </DefaultLangSys> + <!-- LangSysCount=1 --> + <LangSysRecord index="0"> + <LangSysTag value="NLD "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </LangSys> + </LangSysRecord> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=3 --> + <FeatureRecord index="0"> + <FeatureTag value="cpsp"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="mark"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="2"> + <FeatureTag value="mkmk"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="2"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=3 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage Format="1"> + <Glyph value="N"/> + <Glyph value="O"/> + <Glyph value="Odieresis"/> + </Coverage> + <ValueFormat value="5"/> + <Value XPlacement="25" XAdvance="50"/> + </SinglePos> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkBasePos index="0" Format="1"> + <MarkCoverage Format="1"> + <Glyph value="uni0308"/> + </MarkCoverage> + <BaseCoverage Format="2"> + <Glyph value="N"/> + <Glyph value="O"/> + <Glyph value="Odieresis"/> + <Glyph value="n"/> + <Glyph value="o"/> + <Glyph value="odieresis"/> + </BaseCoverage> + <!-- ClassCount=1 --> + <MarkArray> + <!-- MarkCount=1 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="197"/> + <YCoordinate value="350"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <BaseArray> + <!-- BaseCount=6 --> + <BaseRecord index="0"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="291"/> + <YCoordinate value="500"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="1"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="289"/> + <YCoordinate value="500"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="2"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="322"/> + <YCoordinate value="625"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="3"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="222"/> + <YCoordinate value="350"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="4"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="218"/> + <YCoordinate value="350"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="5"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="251"/> + <YCoordinate value="475"/> + </BaseAnchor> + </BaseRecord> + </BaseArray> + </MarkBasePos> + </Lookup> + <Lookup index="2"> + <LookupType value="6"/> + <LookupFlag value="16"/> + <!-- SubTableCount=1 --> + <MarkMarkPos index="0" Format="1"> + <Mark1Coverage Format="1"> + <Glyph value="uni0308"/> + </Mark1Coverage> + <Mark2Coverage Format="1"> + <Glyph value="uni0308"/> + </Mark2Coverage> + <!-- ClassCount=1 --> + <Mark1Array> + <!-- MarkCount=1 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="197"/> + <YCoordinate value="350"/> + </MarkAnchor> + </MarkRecord> + </Mark1Array> + <Mark2Array> + <!-- Mark2Count=1 --> + <Mark2Record index="0"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="230"/> + <YCoordinate value="475"/> + </Mark2Anchor> + </Mark2Record> + </Mark2Array> + </MarkMarkPos> + <MarkFilteringSet value="0"/> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx new file mode 100644 index 00000000..2e354b0a --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx @@ -0,0 +1,656 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.34"> + + <GlyphOrder> + <!-- The 'id' attribute is only for humans; it is ignored when parsed. --> + <GlyphID id="0" name=".notdef"/> + <GlyphID id="1" name="N"/> + <GlyphID id="2" name="O"/> + <GlyphID id="3" name="Odieresis"/> + <GlyphID id="4" name="n"/> + <GlyphID id="5" name="o"/> + <GlyphID id="6" name="odieresis"/> + <GlyphID id="7" name="uni0308"/> + </GlyphOrder> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.0"/> + <checkSumAdjustment value="0x20783e4b"/> + <magicNumber value="0x5f0f3cf5"/> + <flags value="00000000 00000011"/> + <unitsPerEm value="750"/> + <created value="Sat Oct 21 13:31:38 2017"/> + <modified value="Mon Dec 17 12:42:07 2018"/> + <xMin value="38"/> + <yMin value="-150"/> + <xMax value="354"/> + <yMax value="650"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="6"/> + <fontDirectionHint value="2"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="750"/> + <descent value="-150"/> + <lineGap value="0"/> + <advanceWidthMax value="408"/> + <minLeftSideBearing value="38"/> + <minRightSideBearing value="-250"/> + <xMaxExtent value="354"/> + <caretSlopeRise value="1"/> + <caretSlopeRun value="0"/> + <caretOffset value="0"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="8"/> + </hhea> + + <maxp> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="0x10000"/> + <numGlyphs value="8"/> + <maxPoints value="36"/> + <maxContours value="2"/> + <maxCompositePoints value="64"/> + <maxCompositeContours value="4"/> + <maxZones value="1"/> + <maxTwilightPoints value="0"/> + <maxStorage value="0"/> + <maxFunctionDefs value="0"/> + <maxInstructionDefs value="0"/> + <maxStackElements value="0"/> + <maxSizeOfInstructions value="0"/> + <maxComponentElements value="2"/> + <maxComponentDepth value="1"/> + </maxp> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="4"/> + <xAvgCharWidth value="375"/> + <usWeightClass value="400"/> + <usWidthClass value="5"/> + <fsType value="00000000 00001000"/> + <ySubscriptXSize value="488"/> + <ySubscriptYSize value="450"/> + <ySubscriptXOffset value="0"/> + <ySubscriptYOffset value="56"/> + <ySuperscriptXSize value="488"/> + <ySuperscriptYSize value="450"/> + <ySuperscriptXOffset value="0"/> + <ySuperscriptYOffset value="263"/> + <yStrikeoutSize value="50"/> + <yStrikeoutPosition value="210"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="0"/> + <bSerifStyle value="0"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="0"/> + <bStrokeVariation value="0"/> + <bArmStyle value="0"/> + <bLetterForm value="0"/> + <bMidline value="0"/> + <bXHeight value="0"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000000 01000011"/> + <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="jens"/> + <fsSelection value="00000000 01000000"/> + <usFirstCharIndex value="78"/> + <usLastCharIndex value="776"/> + <sTypoAscender value="600"/> + <sTypoDescender value="-150"/> + <sTypoLineGap value="150"/> + <usWinAscent value="700"/> + <usWinDescent value="250"/> + <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="350"/> + <sCapHeight value="500"/> + <usDefaultChar value="0"/> + <usBreakChar value="32"/> + <usMaxContext value="1"/> + </OS_2> + + <hmtx> + <mtx name=".notdef" width="375" lsb="38"/> + <mtx name="N" width="408" lsb="54"/> + <mtx name="O" width="404" lsb="52"/> + <mtx name="Odieresis" width="404" lsb="52"/> + <mtx name="n" width="348" lsb="50"/> + <mtx name="o" width="342" lsb="46"/> + <mtx name="odieresis" width="342" lsb="46"/> + <mtx name="uni0308" width="0" lsb="50"/> + </hmtx> + + <cmap> + <tableVersion version="0"/> + <cmap_format_4 platformID="0" platEncID="3" language="0"> + <map code="0x4e" name="N"/><!-- LATIN CAPITAL LETTER N --> + <map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O --> + <map code="0x6e" name="n"/><!-- LATIN SMALL LETTER N --> + <map code="0x6f" name="o"/><!-- LATIN SMALL LETTER O --> + <map code="0xd6" name="Odieresis"/><!-- LATIN CAPITAL LETTER O WITH DIAERESIS --> + <map code="0xf6" name="odieresis"/><!-- LATIN SMALL LETTER O WITH DIAERESIS --> + <map code="0x308" name="uni0308"/><!-- COMBINING DIAERESIS --> + </cmap_format_4> + <cmap_format_4 platformID="3" platEncID="1" language="0"> + <map code="0x4e" name="N"/><!-- LATIN CAPITAL LETTER N --> + <map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O --> + <map code="0x6e" name="n"/><!-- LATIN SMALL LETTER N --> + <map code="0x6f" name="o"/><!-- LATIN SMALL LETTER O --> + <map code="0xd6" name="Odieresis"/><!-- LATIN CAPITAL LETTER O WITH DIAERESIS --> + <map code="0xf6" name="odieresis"/><!-- LATIN SMALL LETTER O WITH DIAERESIS --> + <map code="0x308" name="uni0308"/><!-- COMBINING DIAERESIS --> + </cmap_format_4> + </cmap> + + <loca> + <!-- The 'loca' table will be calculated by the compiler --> + </loca> + + <glyf> + + <!-- The xMin, yMin, xMax and yMax values + will be recalculated by the compiler. --> + + <TTGlyph name=".notdef" xMin="38" yMin="-150" xMax="337" yMax="600"> + <contour> + <pt x="38" y="-150" on="1"/> + <pt x="337" y="-150" on="1"/> + <pt x="337" y="600" on="1"/> + <pt x="38" y="600" on="1"/> + </contour> + <contour> + <pt x="76" y="-112" on="1"/> + <pt x="76" y="562" on="1"/> + <pt x="299" y="562" on="1"/> + <pt x="299" y="-112" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="N" xMin="54" yMin="-4" xMax="354" yMax="504"> + <contour> + <pt x="79" y="-4" on="1"/> + <pt x="69" y="-4" on="0"/> + <pt x="54" y="11" on="0"/> + <pt x="54" y="21" on="1"/> + <pt x="54" y="479" on="1"/> + <pt x="54" y="490" on="0"/> + <pt x="69" y="504" on="0"/> + <pt x="79" y="504" on="1"/> + <pt x="88" y="504" on="0"/> + <pt x="98" y="496" on="0"/> + <pt x="101" y="491" on="1"/> + <pt x="304" y="119" on="1"/> + <pt x="304" y="479" on="1"/> + <pt x="304" y="490" on="0"/> + <pt x="319" y="504" on="0"/> + <pt x="329" y="504" on="1"/> + <pt x="340" y="504" on="0"/> + <pt x="354" y="490" on="0"/> + <pt x="354" y="479" on="1"/> + <pt x="354" y="21" on="1"/> + <pt x="354" y="11" on="0"/> + <pt x="340" y="-4" on="0"/> + <pt x="329" y="-4" on="1"/> + <pt x="320" y="-4" on="0"/> + <pt x="310" y="4" on="0"/> + <pt x="307" y="9" on="1"/> + <pt x="104" y="381" on="1"/> + <pt x="104" y="21" on="1"/> + <pt x="104" y="11" on="0"/> + <pt x="90" y="-4" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="O" xMin="52" yMin="-10" xMax="352" yMax="510"> + <contour> + <pt x="202" y="-10" on="1"/> + <pt x="161" y="-10" on="0"/> + <pt x="93" y="31" on="0"/> + <pt x="52" y="99" on="0"/> + <pt x="52" y="140" on="1"/> + <pt x="52" y="360" on="1"/> + <pt x="52" y="401" on="0"/> + <pt x="93" y="469" on="0"/> + <pt x="161" y="510" on="0"/> + <pt x="202" y="510" on="1"/> + <pt x="243" y="510" on="0"/> + <pt x="311" y="469" on="0"/> + <pt x="352" y="401" on="0"/> + <pt x="352" y="360" on="1"/> + <pt x="352" y="140" on="1"/> + <pt x="352" y="99" on="0"/> + <pt x="311" y="31" on="0"/> + <pt x="243" y="-10" on="0"/> + </contour> + <contour> + <pt x="202" y="40" on="1"/> + <pt x="230" y="40" on="0"/> + <pt x="275" y="67" on="0"/> + <pt x="302" y="113" on="0"/> + <pt x="302" y="140" on="1"/> + <pt x="302" y="360" on="1"/> + <pt x="302" y="388" on="0"/> + <pt x="275" y="433" on="0"/> + <pt x="230" y="460" on="0"/> + <pt x="202" y="460" on="1"/> + <pt x="175" y="460" on="0"/> + <pt x="129" y="433" on="0"/> + <pt x="102" y="388" on="0"/> + <pt x="102" y="360" on="1"/> + <pt x="102" y="140" on="1"/> + <pt x="102" y="113" on="0"/> + <pt x="129" y="67" on="0"/> + <pt x="175" y="40" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="Odieresis" xMin="52" yMin="-10" xMax="352" yMax="650"> + <component glyphName="O" x="0" y="0" flags="0x204"/> + <component glyphName="uni0308" x="52" y="150" flags="0x4"/> + </TTGlyph> + + <TTGlyph name="n" xMin="50" yMin="-4" xMax="300" yMax="350"> + <contour> + <pt x="75" y="-4" on="1"/> + <pt x="65" y="-4" on="0"/> + <pt x="50" y="11" on="0"/> + <pt x="50" y="21" on="1"/> + <pt x="50" y="325" on="1"/> + <pt x="50" y="350" on="0"/> + <pt x="75" y="350" on="1"/> + <pt x="198" y="350" on="1"/> + <pt x="228" y="350" on="0"/> + <pt x="274" y="324" on="0"/> + <pt x="300" y="278" on="0"/> + <pt x="300" y="248" on="1"/> + <pt x="300" y="21" on="1"/> + <pt x="300" y="11" on="0"/> + <pt x="286" y="-4" on="0"/> + <pt x="275" y="-4" on="1"/> + <pt x="265" y="-4" on="0"/> + <pt x="250" y="11" on="0"/> + <pt x="250" y="21" on="1"/> + <pt x="250" y="248" on="1"/> + <pt x="250" y="271" on="0"/> + <pt x="221" y="300" on="0"/> + <pt x="198" y="300" on="1"/> + <pt x="100" y="300" on="1"/> + <pt x="100" y="21" on="1"/> + <pt x="100" y="11" on="0"/> + <pt x="86" y="-4" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="o" xMin="46" yMin="-10" xMax="296" yMax="360"> + <contour> + <pt x="171" y="-10" on="1"/> + <pt x="135" y="-10" on="0"/> + <pt x="78" y="23" on="0"/> + <pt x="46" y="80" on="0"/> + <pt x="46" y="117" on="1"/> + <pt x="46" y="233" on="1"/> + <pt x="46" y="270" on="0"/> + <pt x="78" y="327" on="0"/> + <pt x="135" y="360" on="0"/> + <pt x="171" y="360" on="1"/> + <pt x="208" y="360" on="0"/> + <pt x="264" y="327" on="0"/> + <pt x="296" y="270" on="0"/> + <pt x="296" y="233" on="1"/> + <pt x="296" y="117" on="1"/> + <pt x="296" y="80" on="0"/> + <pt x="264" y="23" on="0"/> + <pt x="208" y="-10" on="0"/> + </contour> + <contour> + <pt x="171" y="40" on="1"/> + <pt x="205" y="40" on="0"/> + <pt x="246" y="82" on="0"/> + <pt x="246" y="117" on="1"/> + <pt x="246" y="233" on="1"/> + <pt x="246" y="268" on="0"/> + <pt x="205" y="310" on="0"/> + <pt x="171" y="310" on="1"/> + <pt x="137" y="310" on="0"/> + <pt x="96" y="268" on="0"/> + <pt x="96" y="233" on="1"/> + <pt x="96" y="117" on="1"/> + <pt x="96" y="82" on="0"/> + <pt x="137" y="40" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="odieresis" xMin="46" yMin="-10" xMax="296" yMax="500"> + <component glyphName="o" x="0" y="0" flags="0x204"/> + <component glyphName="uni0308" x="21" y="0" flags="0x4"/> + </TTGlyph> + + <TTGlyph name="uni0308" xMin="50" yMin="425" xMax="250" yMax="500"> + <contour> + <pt x="225" y="425" on="1"/> + <pt x="215" y="425" on="0"/> + <pt x="200" y="440" on="0"/> + <pt x="200" y="450" on="1"/> + <pt x="200" y="475" on="1"/> + <pt x="200" y="486" on="0"/> + <pt x="215" y="500" on="0"/> + <pt x="225" y="500" on="1"/> + <pt x="236" y="500" on="0"/> + <pt x="250" y="486" on="0"/> + <pt x="250" y="475" on="1"/> + <pt x="250" y="450" on="1"/> + <pt x="250" y="440" on="0"/> + <pt x="236" y="425" on="0"/> + </contour> + <contour> + <pt x="75" y="425" on="1"/> + <pt x="65" y="425" on="0"/> + <pt x="50" y="440" on="0"/> + <pt x="50" y="450" on="1"/> + <pt x="50" y="475" on="1"/> + <pt x="50" y="486" on="0"/> + <pt x="65" y="500" on="0"/> + <pt x="75" y="500" on="1"/> + <pt x="86" y="500" on="0"/> + <pt x="100" y="486" on="0"/> + <pt x="100" y="475" on="1"/> + <pt x="100" y="450" on="1"/> + <pt x="100" y="440" on="0"/> + <pt x="86" y="425" on="0"/> + </contour> + <instructions/> + </TTGlyph> + + </glyf> + + <name> + <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409"> + Copyright 2017 by Jens Kutilek + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + Test Family 4 + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409"> + Regular + </namerecord> + <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409"> + 1.000;jens;TestFamily4-Regular + </namerecord> + <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409"> + Test Family 4 Regular + </namerecord> + <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409"> + Version 1.000 + </namerecord> + <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409"> + TestFamily4-Regular + </namerecord> + <namerecord nameID="8" platformID="3" platEncID="1" langID="0x409"> + Jens Kutilek + </namerecord> + <namerecord nameID="9" platformID="3" platEncID="1" langID="0x409"> + Jens Kutilek after the ISO 3098 standard + </namerecord> + <namerecord nameID="11" platformID="3" platEncID="1" langID="0x409"> + https://www.kutilek.de/ + </namerecord> + <namerecord nameID="12" platformID="3" platEncID="1" langID="0x409"> + https://www.kutilek.de/ + </namerecord> + </name> + + <post> + <formatType value="2.0"/> + <italicAngle value="0.0"/> + <underlinePosition value="-100"/> + <underlineThickness value="50"/> + <isFixedPitch value="0"/> + <minMemType42 value="0"/> + <maxMemType42 value="0"/> + <minMemType1 value="0"/> + <maxMemType1 value="0"/> + <psNames> + <!-- This file uses unique glyph names based on the information + found in the 'post' table. Since these names might not be unique, + we have to invent artificial names in case of clashes. In order to + be able to retain the original information, we need a name to + ps name mapping for those cases where they differ. That's what + you see below. + --> + </psNames> + <extraNames> + <!-- following are the name that are not taken from the standard Mac glyph order --> + <psName name="dieresiscomb"/> + <psName name="uni0308"/> + </extraNames> + </post> + + <GDEF> + <Version value="0x00010002"/> + <GlyphClassDef Format="2"> + <ClassDef glyph="N" class="1"/> + <ClassDef glyph="O" class="1"/> + <ClassDef glyph="Odieresis" class="1"/> + <ClassDef glyph="n" class="1"/> + <ClassDef glyph="o" class="1"/> + <ClassDef glyph="odieresis" class="1"/> + <ClassDef glyph="uni0308" class="3"/> + </GlyphClassDef> + <MarkGlyphSetsDef> + <MarkSetTableFormat value="1"/> + <!-- MarkSetCount=1 --> + <Coverage index="0" Format="1"> + <Glyph value="uni0308"/> + </Coverage> + </MarkGlyphSetsDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=2 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="1"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </DefaultLangSys> + <!-- LangSysCount=1 --> + <LangSysRecord index="0"> + <LangSysTag value="NLD "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </LangSys> + </LangSysRecord> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=3 --> + <FeatureRecord index="0"> + <FeatureTag value="cpsp"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="mark"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="2"> + <FeatureTag value="mkmk"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="2"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=3 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage Format="1"> + <Glyph value="N"/> + <Glyph value="O"/> + <Glyph value="Odieresis"/> + </Coverage> + <ValueFormat value="5"/> + <Value XPlacement="25" XAdvance="50"/> + </SinglePos> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkBasePos index="0" Format="1"> + <MarkCoverage Format="1"> + <Glyph value="uni0308"/> + </MarkCoverage> + <BaseCoverage Format="2"> + <Glyph value="N"/> + <Glyph value="O"/> + <Glyph value="Odieresis"/> + <Glyph value="n"/> + <Glyph value="o"/> + <Glyph value="odieresis"/> + </BaseCoverage> + <!-- ClassCount=1 --> + <MarkArray> + <!-- MarkCount=1 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="350"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <BaseArray> + <!-- BaseCount=6 --> + <BaseRecord index="0"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="204"/> + <YCoordinate value="500"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="1"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="202"/> + <YCoordinate value="500"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="2"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="202"/> + <YCoordinate value="625"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="3"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="175"/> + <YCoordinate value="350"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="4"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="171"/> + <YCoordinate value="350"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="5"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="171"/> + <YCoordinate value="475"/> + </BaseAnchor> + </BaseRecord> + </BaseArray> + </MarkBasePos> + </Lookup> + <Lookup index="2"> + <LookupType value="6"/> + <LookupFlag value="16"/> + <!-- SubTableCount=1 --> + <MarkMarkPos index="0" Format="1"> + <Mark1Coverage Format="1"> + <Glyph value="uni0308"/> + </Mark1Coverage> + <Mark2Coverage Format="1"> + <Glyph value="uni0308"/> + </Mark2Coverage> + <!-- ClassCount=1 --> + <Mark1Array> + <!-- MarkCount=1 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="350"/> + </MarkAnchor> + </MarkRecord> + </Mark1Array> + <Mark2Array> + <!-- Mark2Count=1 --> + <Mark2Record index="0"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="475"/> + </Mark2Anchor> + </Mark2Record> + </Mark2Array> + </MarkMarkPos> + <MarkFilteringSet value="0"/> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx b/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx index 23c240ef..23c240ef 100755..100644 --- a/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx +++ b/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/features.fea new file mode 100644 index 00000000..88e4c41a --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/features.fea @@ -0,0 +1,23 @@ +# automatic +@Uppercase = [ N O Odieresis ]; + +# Prefix: Languagesystems +# automatic +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn NLD; + + +feature cpsp { +pos @Uppercase <25 0 50 0>; + +} cpsp; + +table GDEF { + # automatic + GlyphClassDef + [N O Odieresis n o odieresis], # Base + , # Liga + [dieresiscomb], # Mark + ; +} GDEF; diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist new file mode 100644 index 00000000..b909a16c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist @@ -0,0 +1,93 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>ascender</key> + <real>600.0</real> + <key>capHeight</key> + <real>500.0</real> + <key>copyright</key> + <string>Copyright 2017 by Jens Kutilek</string> + <key>descender</key> + <real>-150.0</real> + <key>familyName</key> + <string>Test Family 4</string> + <key>guidelines</key> + <array> + <dict> + <key>angle</key> + <real>180.0</real> + <key>x</key> + <real>125.0</real> + <key>y</key> + <real>175.0</real> + </dict> + </array> + <key>italicAngle</key> + <real>-15.0</real> + <key>openTypeHeadCreated</key> + <string>2017/10/21 13:31:38</string> + <key>openTypeNameDesigner</key> + <string>Jens Kutilek after the ISO 3098 standard</string> + <key>openTypeNameDesignerURL</key> + <string>https://www.kutilek.de/</string> + <key>openTypeNameManufacturer</key> + <string>Jens Kutilek</string> + <key>openTypeNameManufacturerURL</key> + <string>https://www.kutilek.de/</string> + <key>openTypeOS2Type</key> + <array> + <integer>3</integer> + </array> + <key>openTypeOS2VendorID</key> + <string>jens</string> + <key>openTypeOS2WinAscent</key> + <integer>700</integer> + <key>openTypeOS2WinDescent</key> + <integer>250</integer> + <key>postscriptBlueValues</key> + <array> + <real>-10.0</real> + <real>0.0</real> + <real>350.0</real> + <real>360.0</real> + <real>500.0</real> + <real>510.0</real> + </array> + <key>postscriptFamilyBlues</key> + <array/> + <key>postscriptFamilyOtherBlues</key> + <array/> + <key>postscriptOtherBlues</key> + <array> + <real>-160.0</real> + <real>-150.0</real> + </array> + <key>postscriptStemSnapH</key> + <array> + <integer>50</integer> + </array> + <key>postscriptStemSnapV</key> + <array> + <integer>50</integer> + </array> + <key>postscriptUnderlinePosition</key> + <integer>-100</integer> + <key>postscriptUnderlineThickness</key> + <integer>50</integer> + <key>styleMapFamilyName</key> + <string>Test Family 4 15</string> + <key>styleMapStyleName</key> + <string>italic</string> + <key>styleName</key> + <string>Italic 15</string> + <key>unitsPerEm</key> + <integer>750</integer> + <key>versionMajor</key> + <integer>1</integer> + <key>versionMinor</key> + <integer>0</integer> + <key>xHeight</key> + <real>350.0</real> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif new file mode 100644 index 00000000..ef809034 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif @@ -0,0 +1,41 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="N" format="2"> + <anchor x="153.0" y="0.0" name="bottom"/> + <anchor x="287.0" y="500.0" name="top"/> + <outline> + <contour> + <point x="34.0" y="21.0" type="move"/> + <point x="156.0" y="479.0" type="line"/> + <point x="284.0" y="21.0" type="line"/> + <point x="406.0" y="479.0" type="line"/> + </contour> + <contour> + <point x="156.0" y="454.0" type="curve" smooth="yes"/> + <point x="170.0" y="454.0"/> + <point x="181.0" y="465.0"/> + <point x="181.0" y="479.0" type="curve" smooth="yes"/> + <point x="181.0" y="493.0"/> + <point x="170.0" y="504.0"/> + <point x="156.0" y="504.0" type="curve" smooth="yes"/> + <point x="142.0" y="504.0"/> + <point x="131.0" y="493.0"/> + <point x="131.0" y="479.0" type="curve" smooth="yes"/> + <point x="131.0" y="465.0"/> + <point x="142.0" y="454.0"/> + </contour> + <contour> + <point x="284.0" y="-4.0" type="curve" smooth="yes"/> + <point x="298.0" y="-4.0"/> + <point x="309.0" y="7.0"/> + <point x="309.0" y="21.0" type="curve" smooth="yes"/> + <point x="309.0" y="35.0"/> + <point x="298.0" y="46.0"/> + <point x="284.0" y="46.0" type="curve" smooth="yes"/> + <point x="270.0" y="46.0"/> + <point x="259.0" y="35.0"/> + <point x="259.0" y="21.0" type="curve" smooth="yes"/> + <point x="259.0" y="7.0"/> + <point x="270.0" y="-4.0"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif new file mode 100644 index 00000000..ac3160a6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif @@ -0,0 +1,27 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="O" format="2"> + <anchor x="153.0" y="0.0" name="bottom"/> + <anchor x="220.0" y="250.0" name="center"/> + <anchor x="316.0" y="10.0" name="ogonek"/> + <anchor x="287.0" y="500.0" name="top"/> + <anchor x="107.0" y="500.0" name="topleft"/> + <anchor x="467.0" y="500.0" name="topright"/> + <outline> + <contour> + <point x="66.0" y="140.0" type="curve"/> + <point x="125.0" y="360.0" type="line"/> + <point x="143.0" y="429.0"/> + <point x="214.0" y="485.0"/> + <point x="283.0" y="485.0" type="curve"/> + <point x="352.0" y="485.0"/> + <point x="393.0" y="429.0"/> + <point x="375.0" y="360.0" type="curve"/> + <point x="316.0" y="140.0" type="line"/> + <point x="297.0" y="71.0"/> + <point x="226.0" y="15.0"/> + <point x="157.0" y="15.0" type="curve"/> + <point x="88.0" y="15.0"/> + <point x="47.0" y="71.0"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist new file mode 100644 index 00000000..e7bf8356 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>N</key> + <string>N_.glif</string> + <key>O</key> + <string>O_.glif</string> + <key>dieresiscomb</key> + <string>dieresiscomb.glif</string> + <key>o</key> + <string>o.glif</string> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif new file mode 100644 index 00000000..8dac7b72 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif @@ -0,0 +1,15 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="dieresiscomb" format="2"> + <anchor x="197.0" y="350.0" name="_top"/> + <anchor x="230.0" y="475.0" name="top"/> + <outline> + <contour> + <point x="155.0" y="475.0" type="move"/> + <point x="149.0" y="450.0" type="line"/> + </contour> + <contour> + <point x="305.0" y="475.0" type="move"/> + <point x="299.0" y="450.0" type="line"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif new file mode 100644 index 00000000..6a2ce9fd --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif @@ -0,0 +1,37 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="o" format="2"> + <outline> + <contour> + <point x="128.0" y="-10.0" type="curve" smooth="yes"/> + <point x="194.0" y="-10.0"/> + <point x="262.0" y="43.0"/> + <point x="279.0" y="108.0" type="curve" smooth="yes"/> + <point x="311.0" y="229.0" type="line" smooth="yes"/> + <point x="330.0" y="299.0"/> + <point x="286.0" y="360.0"/> + <point x="214.0" y="360.0" type="curve" smooth="yes"/> + <point x="148.0" y="360.0"/> + <point x="80.0" y="307.0"/> + <point x="63.0" y="242.0" type="curve" smooth="yes"/> + <point x="31.0" y="121.0" type="line" smooth="yes"/> + <point x="12.0" y="51.0"/> + <point x="56.0" y="-10.0"/> + </contour> + <contour> + <point x="128.0" y="40.0" type="curve" smooth="yes"/> + <point x="88.0" y="40.0"/> + <point x="69.0" y="71.0"/> + <point x="79.0" y="108.0" type="curve" smooth="yes"/> + <point x="111.0" y="229.0" type="line" smooth="yes"/> + <point x="123.0" y="273.0"/> + <point x="170.0" y="310.0"/> + <point x="214.0" y="310.0" type="curve" smooth="yes"/> + <point x="254.0" y="310.0"/> + <point x="273.0" y="279.0"/> + <point x="263.0" y="242.0" type="curve" smooth="yes"/> + <point x="231.0" y="121.0" type="line" smooth="yes"/> + <point x="219.0" y="77.0"/> + <point x="172.0" y="40.0"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif new file mode 100644 index 00000000..d289e0d1 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif @@ -0,0 +1,47 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="N" format="2"> + <advance width="408.0"/> + <unicode hex="004E"/> + <anchor x="157.0" y="0.0" name="bottom"/> + <anchor x="291.0" y="500.0" name="top"/> + <outline> + <contour> + <point x="38.0" y="-4.0" type="curve" smooth="yes"/> + <point x="50.0" y="-4.0"/> + <point x="59.0" y="4.0"/> + <point x="62.0" y="15.0" type="curve" smooth="yes"/> + <point x="161.0" y="384.0" type="line"/> + <point x="264.0" y="14.0" type="line" smooth="yes"/> + <point x="267.0" y="2.0"/> + <point x="278.0" y="-4.0"/> + <point x="288.0" y="-4.0" type="curve" smooth="yes"/> + <point x="299.0" y="-4.0"/> + <point x="309.0" y="2.0"/> + <point x="312.0" y="14.0" type="curve" smooth="yes"/> + <point x="434.0" y="472.0" type="line" smooth="yes"/> + <point x="437.0" y="483.0"/> + <point x="430.0" y="504.0"/> + <point x="410.0" y="504.0" type="curve" smooth="yes"/> + <point x="398.0" y="504.0"/> + <point x="389.0" y="496.0"/> + <point x="386.0" y="485.0" type="curve" smooth="yes"/> + <point x="287.0" y="116.0" type="line"/> + <point x="184.0" y="486.0" type="line" smooth="yes"/> + <point x="181.0" y="498.0"/> + <point x="170.0" y="504.0"/> + <point x="159.0" y="504.0" type="curve" smooth="yes"/> + <point x="149.0" y="504.0"/> + <point x="139.0" y="498.0"/> + <point x="136.0" y="486.0" type="curve" smooth="yes"/> + <point x="14.0" y="28.0" type="line" smooth="yes"/> + <point x="10.0" y="12.0"/> + <point x="22.0" y="-4.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2018/11/20 10:52:27</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif new file mode 100644 index 00000000..aafee0a9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif @@ -0,0 +1,51 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="O" format="2"> + <advance width="404.0"/> + <unicode hex="004F"/> + <anchor x="155.0" y="0.0" name="bottom"/> + <anchor x="222.0" y="250.0" name="center"/> + <anchor x="318.0" y="10.0" name="ogonek"/> + <anchor x="289.0" y="500.0" name="top"/> + <anchor x="109.0" y="500.0" name="topleft"/> + <anchor x="469.0" y="500.0" name="topright"/> + <outline> + <contour> + <point x="159.0" y="-10.0" type="curve" smooth="yes"/> + <point x="239.0" y="-10.0"/> + <point x="321.0" y="54.0"/> + <point x="342.0" y="133.0" type="curve" smooth="yes"/> + <point x="401.0" y="354.0" type="line" smooth="yes"/> + <point x="423.0" y="437.0"/> + <point x="371.0" y="510.0"/> + <point x="285.0" y="510.0" type="curve" smooth="yes"/> + <point x="205.0" y="510.0"/> + <point x="124.0" y="446.0"/> + <point x="103.0" y="366.0" type="curve" smooth="yes"/> + <point x="44.0" y="146.0" type="line" smooth="yes"/> + <point x="22.0" y="63.0"/> + <point x="73.0" y="-10.0"/> + </contour> + <contour> + <point x="159.0" y="40.0" type="curve" smooth="yes"/> + <point x="106.0" y="40.0"/> + <point x="78.0" y="81.0"/> + <point x="92.0" y="133.0" type="curve" smooth="yes"/> + <point x="151.0" y="354.0" type="line" smooth="yes"/> + <point x="166.0" y="412.0"/> + <point x="227.0" y="460.0"/> + <point x="285.0" y="460.0" type="curve" smooth="yes"/> + <point x="338.0" y="460.0"/> + <point x="367.0" y="419.0"/> + <point x="353.0" y="366.0" type="curve" smooth="yes"/> + <point x="294.0" y="146.0" type="line" smooth="yes"/> + <point x="278.0" y="88.0"/> + <point x="217.0" y="40.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2017/10/27 21:28:25</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif new file mode 100644 index 00000000..93bca95f --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif @@ -0,0 +1,23 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="Odieresis" format="2"> + <advance width="404.0"/> + <unicode hex="00D6"/> + <anchor x="155.0" y="0.0" name="bottom"/> + <anchor x="222.0" y="250.0" name="center"/> + <anchor x="318.0" y="10.0" name="ogonek"/> + <anchor x="322.0" y="625.0" name="top"/> + <anchor x="109.0" y="500.0" name="topleft"/> + <anchor x="469.0" y="500.0" name="topright"/> + <outline> + <component base="O"/> + <component base="dieresiscomb" xOffset="92.0" yOffset="150.0"/> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2017/10/27 21:28:35</string> + <key>public.markColor</key> + <string>0,0.67,0.91,1</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist new file mode 100644 index 00000000..6a3662fd --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>N</key> + <string>N_.glif</string> + <key>O</key> + <string>O_.glif</string> + <key>Odieresis</key> + <string>O_dieresis.glif</string> + <key>dieresiscomb</key> + <string>dieresiscomb.glif</string> + <key>n</key> + <string>n.glif</string> + <key>o</key> + <string>o.glif</string> + <key>odieresis</key> + <string>odieresis.glif</string> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif new file mode 100644 index 00000000..224df4d5 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif @@ -0,0 +1,48 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="dieresiscomb" format="2"> + <unicode hex="0308"/> + <anchor x="197.0" y="350.0" name="_top"/> + <anchor x="230.0" y="475.0" name="top"/> + <outline> + <contour> + <point x="300.0" y="425.0" type="curve" smooth="yes"/> + <point x="312.0" y="425.0"/> + <point x="321.0" y="433.0"/> + <point x="324.0" y="445.0" type="curve" smooth="yes"/> + <point x="330.0" y="469.0" type="line" smooth="yes"/> + <point x="334.0" y="484.0"/> + <point x="322.0" y="500.0"/> + <point x="306.0" y="500.0" type="curve" smooth="yes"/> + <point x="294.0" y="500.0"/> + <point x="285.0" y="492.0"/> + <point x="282.0" y="480.0" type="curve" smooth="yes"/> + <point x="276.0" y="456.0" type="line" smooth="yes"/> + <point x="272.0" y="441.0"/> + <point x="284.0" y="425.0"/> + </contour> + <contour> + <point x="150.0" y="425.0" type="curve" smooth="yes"/> + <point x="162.0" y="425.0"/> + <point x="171.0" y="433.0"/> + <point x="174.0" y="445.0" type="curve" smooth="yes"/> + <point x="180.0" y="469.0" type="line" smooth="yes"/> + <point x="184.0" y="484.0"/> + <point x="172.0" y="500.0"/> + <point x="156.0" y="500.0" type="curve" smooth="yes"/> + <point x="144.0" y="500.0"/> + <point x="135.0" y="492.0"/> + <point x="132.0" y="480.0" type="curve" smooth="yes"/> + <point x="126.0" y="456.0" type="line" smooth="yes"/> + <point x="122.0" y="441.0"/> + <point x="134.0" y="425.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2018/11/20 11:01:28</string> + <key>com.schriftgestaltung.Glyphs.originalWidth</key> + <real>300.0</real> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif new file mode 100644 index 00000000..1313dd47 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif @@ -0,0 +1,44 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="n" format="2"> + <advance width="348.0"/> + <unicode hex="006E"/> + <anchor x="128.0" y="0.0" name="bottom"/> + <anchor x="222.0" y="350.0" name="top"/> + <outline> + <contour> + <point x="34.0" y="-4.0" type="curve" smooth="yes"/> + <point x="46.0" y="-4.0"/> + <point x="55.0" y="4.0"/> + <point x="58.0" y="15.0" type="curve" smooth="yes"/> + <point x="134.0" y="300.0" type="line"/> + <point x="238.0" y="300.0" type="line" smooth="yes"/> + <point x="266.0" y="300.0"/> + <point x="278.0" y="282.0"/> + <point x="271.0" y="255.0" type="curve" smooth="yes"/> + <point x="210.0" y="28.0" type="line" smooth="yes"/> + <point x="205.0" y="11.0"/> + <point x="218.0" y="-4.0"/> + <point x="234.0" y="-4.0" type="curve" smooth="yes"/> + <point x="246.0" y="-4.0"/> + <point x="255.0" y="3.0"/> + <point x="258.0" y="15.0" type="curve" smooth="yes"/> + <point x="319.0" y="242.0" type="line" smooth="yes"/> + <point x="335.0" y="301.0"/> + <point x="299.0" y="350.0"/> + <point x="238.0" y="350.0" type="curve" smooth="yes"/> + <point x="118.0" y="350.0" type="line" smooth="yes"/> + <point x="102.993" y="350.0"/> + <point x="94.0" y="343.0"/> + <point x="90.0" y="329.0" type="curve" smooth="yes"/> + <point x="10.0" y="28.0" type="line" smooth="yes"/> + <point x="5.0" y="11.0"/> + <point x="18.0" y="-4.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2017/10/27 21:28:25</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif new file mode 100644 index 00000000..ac3ff7e1 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif @@ -0,0 +1,50 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="o" format="2"> + <advance width="342.0"/> + <unicode hex="006F"/> + <anchor x="124.0" y="0.0" name="bottom"/> + <anchor x="171.0" y="175.0" name="center"/> + <anchor x="267.0" y="10.0" name="ogonek"/> + <anchor x="218.0" y="350.0" name="top"/> + <anchor x="373.0" y="350.0" name="topright"/> + <outline> + <contour> + <point x="129.0" y="-10.0" type="curve" smooth="yes"/> + <point x="199.0" y="-10.0"/> + <point x="260.0" y="39.0"/> + <point x="280.0" y="110.0" type="curve" smooth="yes"/> + <point x="312.0" y="226.0" type="line" smooth="yes"/> + <point x="333.0" y="303.0"/> + <point x="291.0" y="360.0"/> + <point x="215.0" y="360.0" type="curve" smooth="yes"/> + <point x="145.0" y="360.0"/> + <point x="84.0" y="311.0"/> + <point x="64.0" y="240.0" type="curve" smooth="yes"/> + <point x="32.0" y="124.0" type="line" smooth="yes"/> + <point x="11.0" y="47.0"/> + <point x="53.0" y="-10.0"/> + </contour> + <contour> + <point x="129.0" y="40.0" type="curve" smooth="yes"/> + <point x="86.0" y="40.0"/> + <point x="68.0" y="66.0"/> + <point x="80.0" y="110.0" type="curve" smooth="yes"/> + <point x="112.0" y="226.0" type="line" smooth="yes"/> + <point x="126.0" y="277.0"/> + <point x="167.0" y="310.0"/> + <point x="215.0" y="310.0" type="curve" smooth="yes"/> + <point x="258.0" y="310.0"/> + <point x="276.0" y="284.0"/> + <point x="264.0" y="240.0" type="curve" smooth="yes"/> + <point x="232.0" y="124.0" type="line" smooth="yes"/> + <point x="218.0" y="73.0"/> + <point x="177.0" y="40.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2018/11/20 11:01:11</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif new file mode 100644 index 00000000..42c39d1a --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif @@ -0,0 +1,22 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="odieresis" format="2"> + <advance width="342.0"/> + <unicode hex="00F6"/> + <anchor x="124.0" y="0.0" name="bottom"/> + <anchor x="171.0" y="175.0" name="center"/> + <anchor x="267.0" y="10.0" name="ogonek"/> + <anchor x="251.0" y="475.0" name="top"/> + <anchor x="373.0" y="350.0" name="topright"/> + <outline> + <component base="o"/> + <component base="dieresiscomb" xOffset="21.0"/> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2018/11/20 10:59:58</string> + <key>public.markColor</key> + <string>0,0.67,0.91,1</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist new file mode 100644 index 00000000..f9e8ed31 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist @@ -0,0 +1,34 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>public.kern1.O</key> + <array> + <string>O</string> + <string>Odieresis</string> + </array> + <key>public.kern1.n</key> + <array> + <string>n</string> + </array> + <key>public.kern1.o</key> + <array> + <string>o</string> + <string>odieresis</string> + </array> + <key>public.kern2.O</key> + <array> + <string>O</string> + <string>Odieresis</string> + </array> + <key>public.kern2.n</key> + <array> + <string>n</string> + </array> + <key>public.kern2.o</key> + <array> + <string>o</string> + <string>odieresis</string> + </array> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist new file mode 100644 index 00000000..7120d0ba --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <array> + <array> + <string>public.default</string> + <string>glyphs</string> + </array> + <array> + <string>public.background</string> + <string>glyphs.public.background</string> + </array> + </array> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist new file mode 100644 index 00000000..b9f2b929 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist @@ -0,0 +1,86 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>com.schriftgestaltung.appVersion</key> + <string>1179</string> + <key>com.schriftgestaltung.customName</key> + <string>Italic 15</string> + <key>com.schriftgestaltung.customParameter.GSFont.Axes</key> + <array> + <dict> + <key>Name</key> + <string>Slant</string> + <key>Tag</key> + <string>slnt</string> + </dict> + </array> + <key>com.schriftgestaltung.customParameter.GSFont.DisplayStrings</key> + <array> + <string>o/dieresiscomb ö</string> + </array> + <key>com.schriftgestaltung.customParameter.GSFont.Variation Font Origin</key> + <string>EB3D7718-A203-47FB-ABD4-8B7A501887ED</string> + <key>com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment</key> + <false/> + <key>com.schriftgestaltung.customParameter.GSFont.useNiceNames</key> + <integer>1</integer> + <key>com.schriftgestaltung.customParameter.GSFontMaster.Link Metrics With Master</key> + <string>Regular</string> + <key>com.schriftgestaltung.customParameter.GSFontMaster.Master Name</key> + <string>Italic 15</string> + <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue</key> + <real>-15.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue1</key> + <real>16.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue2</key> + <real>0.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue3</key> + <real>0.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.iconName</key> + <string></string> + <key>com.schriftgestaltung.customParameter.GSFontMaster.weightValue</key> + <real>-15.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.widthValue</key> + <real>100.0</real> + <key>com.schriftgestaltung.customValue</key> + <real>-15.0</real> + <key>com.schriftgestaltung.customValue1</key> + <real>16.0</real> + <key>com.schriftgestaltung.fontMasterOrder</key> + <integer>1</integer> + <key>com.schriftgestaltung.glyphOrder</key> + <false/> + <key>com.schriftgestaltung.keyboardIncrement</key> + <integer>1</integer> + <key>com.schriftgestaltung.weight</key> + <string>Regular</string> + <key>com.schriftgestaltung.weightValue</key> + <real>-15.0</real> + <key>com.schriftgestaltung.width</key> + <string>Regular</string> + <key>com.schriftgestaltung.widthValue</key> + <real>100.0</real> + <key>noodleExtremesAndInflections</key> + <integer>0</integer> + <key>noodleRemoveOverlap</key> + <integer>1</integer> + <key>noodleThickness</key> + <string>50.0</string> + <key>public.glyphOrder</key> + <array> + <string>N</string> + <string>O</string> + <string>Odieresis</string> + <string>n</string> + <string>o</string> + <string>odieresis</string> + <string>dieresiscomb</string> + </array> + <key>public.postscriptNames</key> + <dict> + <key>dieresiscomb</key> + <string>uni0308</string> + </dict> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist new file mode 100644 index 00000000..7b8b34ac --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist @@ -0,0 +1,10 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>creator</key> + <string>com.github.fonttools.ufoLib</string> + <key>formatVersion</key> + <integer>3</integer> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea new file mode 100644 index 00000000..88e4c41a --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea @@ -0,0 +1,23 @@ +# automatic +@Uppercase = [ N O Odieresis ]; + +# Prefix: Languagesystems +# automatic +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn NLD; + + +feature cpsp { +pos @Uppercase <25 0 50 0>; + +} cpsp; + +table GDEF { + # automatic + GlyphClassDef + [N O Odieresis n o odieresis], # Base + , # Liga + [dieresiscomb], # Mark + ; +} GDEF; diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist new file mode 100644 index 00000000..7ff1edb1 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist @@ -0,0 +1,93 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>ascender</key> + <real>600.0</real> + <key>capHeight</key> + <real>500.0</real> + <key>copyright</key> + <string>Copyright 2017 by Jens Kutilek</string> + <key>descender</key> + <real>-150.0</real> + <key>familyName</key> + <string>Test Family 4</string> + <key>guidelines</key> + <array> + <dict> + <key>angle</key> + <real>180.0</real> + <key>x</key> + <real>125.0</real> + <key>y</key> + <real>175.0</real> + </dict> + </array> + <key>italicAngle</key> + <real>-0.0</real> + <key>openTypeHeadCreated</key> + <string>2017/10/21 13:31:38</string> + <key>openTypeNameDesigner</key> + <string>Jens Kutilek after the ISO 3098 standard</string> + <key>openTypeNameDesignerURL</key> + <string>https://www.kutilek.de/</string> + <key>openTypeNameManufacturer</key> + <string>Jens Kutilek</string> + <key>openTypeNameManufacturerURL</key> + <string>https://www.kutilek.de/</string> + <key>openTypeOS2Type</key> + <array> + <integer>3</integer> + </array> + <key>openTypeOS2VendorID</key> + <string>jens</string> + <key>openTypeOS2WinAscent</key> + <integer>700</integer> + <key>openTypeOS2WinDescent</key> + <integer>250</integer> + <key>postscriptBlueValues</key> + <array> + <real>-10.0</real> + <real>0.0</real> + <real>350.0</real> + <real>360.0</real> + <real>500.0</real> + <real>510.0</real> + </array> + <key>postscriptFamilyBlues</key> + <array/> + <key>postscriptFamilyOtherBlues</key> + <array/> + <key>postscriptOtherBlues</key> + <array> + <real>-160.0</real> + <real>-150.0</real> + </array> + <key>postscriptStemSnapH</key> + <array> + <integer>50</integer> + </array> + <key>postscriptStemSnapV</key> + <array> + <integer>50</integer> + </array> + <key>postscriptUnderlinePosition</key> + <integer>-100</integer> + <key>postscriptUnderlineThickness</key> + <integer>50</integer> + <key>styleMapFamilyName</key> + <string>Test Family 4</string> + <key>styleMapStyleName</key> + <string>regular</string> + <key>styleName</key> + <string>Regular</string> + <key>unitsPerEm</key> + <integer>750</integer> + <key>versionMajor</key> + <integer>1</integer> + <key>versionMinor</key> + <integer>0</integer> + <key>xHeight</key> + <real>350.0</real> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif new file mode 100644 index 00000000..941a558e --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif @@ -0,0 +1,31 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="N" format="2"> + <outline> + <contour> + <point x="324.0" y="-22.0" type="line" smooth="yes"/> + <point x="336.0" y="-44.0"/> + <point x="354.0" y="-40.0"/> + <point x="354.0" y="-14.0" type="curve" smooth="yes"/> + <point x="354.0" y="479.0" type="line" smooth="yes"/> + <point x="354.0" y="493.0"/> + <point x="343.0" y="504.0"/> + <point x="329.0" y="504.0" type="curve" smooth="yes"/> + <point x="315.0" y="504.0"/> + <point x="304.0" y="493.0"/> + <point x="304.0" y="479.0" type="curve" smooth="yes"/> + <point x="304.0" y="119.0" type="line"/> + <point x="84.0" y="522.0" type="line" smooth="yes"/> + <point x="72.0" y="544.0"/> + <point x="54.0" y="540.0"/> + <point x="54.0" y="514.0" type="curve" smooth="yes"/> + <point x="54.0" y="21.0" type="line" smooth="yes"/> + <point x="54.0" y="7.0"/> + <point x="65.0" y="-4.0"/> + <point x="79.0" y="-4.0" type="curve" smooth="yes"/> + <point x="93.0" y="-4.0"/> + <point x="104.0" y="7.0"/> + <point x="104.0" y="21.0" type="curve" smooth="yes"/> + <point x="104.0" y="381.0" type="line"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif new file mode 100644 index 00000000..dc9a72ed --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif @@ -0,0 +1,28 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="O" format="2"> + <anchor x="202.0" y="0.0" name="bottom"/> + <anchor x="202.0" y="250.0" name="center"/> + <anchor x="362.0" y="10.0" name="ogonek"/> + <anchor x="202.0" y="500.0" name="top"/> + <anchor x="22.0" y="500.0" name="topleft"/> + <anchor x="382.0" y="500.0" name="topright"/> + <outline> + <contour> + <point x="77.0" y="150.0" type="curve"/> + <point x="77.0" y="350.0" type="line"/> + <point x="77.0" y="419.0"/> + <point x="133.0" y="475.0"/> + <point x="202.0" y="475.0" type="curve"/> + <point x="271.0" y="475.0"/> + <point x="327.0" y="419.0"/> + <point x="327.0" y="350.0" type="curve"/> + <point x="327.0" y="250.0" type="line"/> + <point x="327.0" y="150.0" type="line"/> + <point x="327.0" y="81.0"/> + <point x="271.0" y="25.0"/> + <point x="202.0" y="25.0" type="curve"/> + <point x="133.0" y="25.0"/> + <point x="77.0" y="81.0"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist new file mode 100644 index 00000000..8382c790 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist @@ -0,0 +1,16 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>N</key> + <string>N_.glif</string> + <key>O</key> + <string>O_.glif</string> + <key>dieresiscomb</key> + <string>dieresiscomb.glif</string> + <key>n</key> + <string>n.glif</string> + <key>o</key> + <string>o.glif</string> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif new file mode 100644 index 00000000..11fb86ed --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif @@ -0,0 +1,15 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="dieresiscomb" format="2"> + <anchor x="150.0" y="350.0" name="_top"/> + <anchor x="150.0" y="475.0" name="top"/> + <outline> + <contour> + <point x="75.0" y="475.0" type="move"/> + <point x="75.0" y="450.0" type="line"/> + </contour> + <contour> + <point x="225.0" y="475.0" type="move"/> + <point x="225.0" y="450.0" type="line"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif new file mode 100644 index 00000000..2d1a13e8 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="n" format="2"> + <outline> + <contour> + <point x="75.0" y="21.0" type="move"/> + <point x="75.0" y="325.0" type="line"/> + <point x="198.0" y="325.0" type="line" smooth="yes"/> + <point x="243.0" y="325.0"/> + <point x="275.0" y="293.0"/> + <point x="275.0" y="248.0" type="curve" smooth="yes"/> + <point x="275.0" y="21.0" type="line"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif new file mode 100644 index 00000000..2e9a61bc --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif @@ -0,0 +1,37 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="o" format="2"> + <outline> + <contour> + <point x="171.0" y="-10.0" type="curve" smooth="yes"/> + <point x="240.0" y="-10.0"/> + <point x="296.0" y="46.0"/> + <point x="296.0" y="115.0" type="curve" smooth="yes"/> + <point x="296.0" y="235.0" type="line" smooth="yes"/> + <point x="296.0" y="304.0"/> + <point x="240.0" y="360.0"/> + <point x="171.0" y="360.0" type="curve" smooth="yes"/> + <point x="102.0" y="360.0"/> + <point x="46.0" y="304.0"/> + <point x="46.0" y="235.0" type="curve" smooth="yes"/> + <point x="46.0" y="115.0" type="line" smooth="yes"/> + <point x="46.0" y="46.0"/> + <point x="102.0" y="-10.0"/> + </contour> + <contour> + <point x="171.0" y="40.0" type="curve" smooth="yes"/> + <point x="130.0" y="40.0"/> + <point x="96.0" y="74.0"/> + <point x="96.0" y="115.0" type="curve" smooth="yes"/> + <point x="96.0" y="235.0" type="line" smooth="yes"/> + <point x="96.0" y="276.0"/> + <point x="130.0" y="310.0"/> + <point x="171.0" y="310.0" type="curve" smooth="yes"/> + <point x="212.0" y="310.0"/> + <point x="246.0" y="276.0"/> + <point x="246.0" y="235.0" type="curve" smooth="yes"/> + <point x="246.0" y="115.0" type="line" smooth="yes"/> + <point x="246.0" y="74.0"/> + <point x="212.0" y="40.0"/> + </contour> + </outline> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif new file mode 100644 index 00000000..a588dac2 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif @@ -0,0 +1,47 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="N" format="2"> + <advance width="408.0"/> + <unicode hex="004E"/> + <anchor x="204.0" y="0.0" name="bottom"/> + <anchor x="204.0" y="500.0" name="top"/> + <outline> + <contour> + <point x="79.0" y="-4.0" type="curve" smooth="yes"/> + <point x="93.0" y="-4.0"/> + <point x="104.0" y="7.0"/> + <point x="104.0" y="21.0" type="curve" smooth="yes"/> + <point x="104.0" y="381.0" type="line"/> + <point x="307.0" y="9.0" type="line" smooth="yes"/> + <point x="311.0" y="2.0"/> + <point x="317.0" y="-4.0"/> + <point x="329.0" y="-4.0" type="curve" smooth="yes"/> + <point x="343.0" y="-4.0"/> + <point x="354.0" y="7.0"/> + <point x="354.0" y="21.0" type="curve" smooth="yes"/> + <point x="354.0" y="479.0" type="line" smooth="yes"/> + <point x="354.0" y="493.0"/> + <point x="343.0" y="504.0"/> + <point x="329.0" y="504.0" type="curve" smooth="yes"/> + <point x="315.0" y="504.0"/> + <point x="304.0" y="493.0"/> + <point x="304.0" y="479.0" type="curve" smooth="yes"/> + <point x="304.0" y="119.0" type="line"/> + <point x="101.0" y="491.0" type="line" smooth="yes"/> + <point x="97.0" y="498.0"/> + <point x="91.0" y="504.0"/> + <point x="79.0" y="504.0" type="curve" smooth="yes"/> + <point x="65.0" y="504.0"/> + <point x="54.0" y="493.0"/> + <point x="54.0" y="479.0" type="curve" smooth="yes"/> + <point x="54.0" y="21.0" type="line" smooth="yes"/> + <point x="54.0" y="7.0"/> + <point x="65.0" y="-4.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2018/11/20 10:52:27</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif new file mode 100644 index 00000000..f3979ed5 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif @@ -0,0 +1,51 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="O" format="2"> + <advance width="404.0"/> + <unicode hex="004F"/> + <anchor x="202.0" y="0.0" name="bottom"/> + <anchor x="202.0" y="250.0" name="center"/> + <anchor x="362.0" y="10.0" name="ogonek"/> + <anchor x="202.0" y="500.0" name="top"/> + <anchor x="22.0" y="500.0" name="topleft"/> + <anchor x="382.0" y="500.0" name="topright"/> + <outline> + <contour> + <point x="202.0" y="-10.0" type="curve" smooth="yes"/> + <point x="284.0" y="-10.0"/> + <point x="352.0" y="58.0"/> + <point x="352.0" y="140.0" type="curve" smooth="yes"/> + <point x="352.0" y="360.0" type="line" smooth="yes"/> + <point x="352.0" y="442.0"/> + <point x="284.0" y="510.0"/> + <point x="202.0" y="510.0" type="curve" smooth="yes"/> + <point x="120.0" y="510.0"/> + <point x="52.0" y="442.0"/> + <point x="52.0" y="360.0" type="curve" smooth="yes"/> + <point x="52.0" y="140.0" type="line" smooth="yes"/> + <point x="52.0" y="58.0"/> + <point x="120.0" y="-10.0"/> + </contour> + <contour> + <point x="202.0" y="40.0" type="curve" smooth="yes"/> + <point x="147.0" y="40.0"/> + <point x="102.0" y="85.0"/> + <point x="102.0" y="140.0" type="curve" smooth="yes"/> + <point x="102.0" y="360.0" type="line" smooth="yes"/> + <point x="102.0" y="415.0"/> + <point x="147.0" y="460.0"/> + <point x="202.0" y="460.0" type="curve" smooth="yes"/> + <point x="257.0" y="460.0"/> + <point x="302.0" y="415.0"/> + <point x="302.0" y="360.0" type="curve" smooth="yes"/> + <point x="302.0" y="140.0" type="line" smooth="yes"/> + <point x="302.0" y="85.0"/> + <point x="257.0" y="40.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2017/10/27 21:28:25</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif new file mode 100644 index 00000000..22fcd379 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif @@ -0,0 +1,23 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="Odieresis" format="2"> + <advance width="404.0"/> + <unicode hex="00D6"/> + <anchor x="202.0" y="0.0" name="bottom"/> + <anchor x="202.0" y="250.0" name="center"/> + <anchor x="362.0" y="10.0" name="ogonek"/> + <anchor x="202.0" y="625.0" name="top"/> + <anchor x="22.0" y="500.0" name="topleft"/> + <anchor x="382.0" y="500.0" name="topright"/> + <outline> + <component base="O"/> + <component base="dieresiscomb" xOffset="52.0" yOffset="150.0"/> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2017/10/27 21:28:35</string> + <key>public.markColor</key> + <string>0,0.67,0.91,1</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist new file mode 100644 index 00000000..6a3662fd --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>N</key> + <string>N_.glif</string> + <key>O</key> + <string>O_.glif</string> + <key>Odieresis</key> + <string>O_dieresis.glif</string> + <key>dieresiscomb</key> + <string>dieresiscomb.glif</string> + <key>n</key> + <string>n.glif</string> + <key>o</key> + <string>o.glif</string> + <key>odieresis</key> + <string>odieresis.glif</string> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif new file mode 100644 index 00000000..b3cfef51 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif @@ -0,0 +1,48 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="dieresiscomb" format="2"> + <unicode hex="0308"/> + <anchor x="150.0" y="350.0" name="_top"/> + <anchor x="150.0" y="475.0" name="top"/> + <outline> + <contour> + <point x="225.0" y="425.0" type="curve" smooth="yes"/> + <point x="239.0" y="425.0"/> + <point x="250.0" y="436.0"/> + <point x="250.0" y="450.0" type="curve" smooth="yes"/> + <point x="250.0" y="475.0" type="line" smooth="yes"/> + <point x="250.0" y="489.0"/> + <point x="239.0" y="500.0"/> + <point x="225.0" y="500.0" type="curve" smooth="yes"/> + <point x="211.0" y="500.0"/> + <point x="200.0" y="489.0"/> + <point x="200.0" y="475.0" type="curve" smooth="yes"/> + <point x="200.0" y="450.0" type="line" smooth="yes"/> + <point x="200.0" y="436.0"/> + <point x="211.0" y="425.0"/> + </contour> + <contour> + <point x="75.0" y="425.0" type="curve" smooth="yes"/> + <point x="89.0" y="425.0"/> + <point x="100.0" y="436.0"/> + <point x="100.0" y="450.0" type="curve" smooth="yes"/> + <point x="100.0" y="475.0" type="line" smooth="yes"/> + <point x="100.0" y="489.0"/> + <point x="89.0" y="500.0"/> + <point x="75.0" y="500.0" type="curve" smooth="yes"/> + <point x="61.0" y="500.0"/> + <point x="50.0" y="489.0"/> + <point x="50.0" y="475.0" type="curve" smooth="yes"/> + <point x="50.0" y="450.0" type="line" smooth="yes"/> + <point x="50.0" y="436.0"/> + <point x="61.0" y="425.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2018/11/20 11:01:28</string> + <key>com.schriftgestaltung.Glyphs.originalWidth</key> + <real>300.0</real> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif new file mode 100644 index 00000000..221ce82c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif @@ -0,0 +1,44 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="n" format="2"> + <advance width="348.0"/> + <unicode hex="006E"/> + <anchor x="175.0" y="0.0" name="bottom"/> + <anchor x="175.0" y="350.0" name="top"/> + <outline> + <contour> + <point x="75.0" y="-4.0" type="curve" smooth="yes"/> + <point x="89.0" y="-4.0"/> + <point x="100.0" y="7.0"/> + <point x="100.0" y="21.0" type="curve" smooth="yes"/> + <point x="100.0" y="300.0" type="line"/> + <point x="198.0" y="300.0" type="line" smooth="yes"/> + <point x="229.0" y="300.0"/> + <point x="250.0" y="279.0"/> + <point x="250.0" y="248.0" type="curve" smooth="yes"/> + <point x="250.0" y="21.0" type="line" smooth="yes"/> + <point x="250.0" y="7.0"/> + <point x="261.0" y="-4.0"/> + <point x="275.0" y="-4.0" type="curve" smooth="yes"/> + <point x="289.0" y="-4.0"/> + <point x="300.0" y="7.0"/> + <point x="300.0" y="21.0" type="curve" smooth="yes"/> + <point x="300.0" y="248.0" type="line" smooth="yes"/> + <point x="300.0" y="307.0"/> + <point x="257.0" y="350.0"/> + <point x="198.0" y="350.0" type="curve" smooth="yes"/> + <point x="75.0" y="350.0" type="line" smooth="yes"/> + <point x="59.0" y="350.0"/> + <point x="50.0" y="341.0"/> + <point x="50.0" y="325.0" type="curve" smooth="yes"/> + <point x="50.0" y="21.0" type="line" smooth="yes"/> + <point x="50.0" y="7.0"/> + <point x="61.0" y="-4.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2017/10/27 21:28:25</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif new file mode 100644 index 00000000..417e4ff8 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif @@ -0,0 +1,50 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="o" format="2"> + <advance width="342.0"/> + <unicode hex="006F"/> + <anchor x="171.0" y="0.0" name="bottom"/> + <anchor x="171.0" y="175.0" name="center"/> + <anchor x="311.0" y="10.0" name="ogonek"/> + <anchor x="171.0" y="350.0" name="top"/> + <anchor x="326.0" y="350.0" name="topright"/> + <outline> + <contour> + <point x="171.0" y="-10.0" type="curve" smooth="yes"/> + <point x="244.0" y="-10.0"/> + <point x="296.0" y="43.0"/> + <point x="296.0" y="117.0" type="curve" smooth="yes"/> + <point x="296.0" y="233.0" type="line" smooth="yes"/> + <point x="296.0" y="307.0"/> + <point x="244.0" y="360.0"/> + <point x="171.0" y="360.0" type="curve" smooth="yes"/> + <point x="98.0" y="360.0"/> + <point x="46.0" y="307.0"/> + <point x="46.0" y="233.0" type="curve" smooth="yes"/> + <point x="46.0" y="117.0" type="line" smooth="yes"/> + <point x="46.0" y="43.0"/> + <point x="98.0" y="-10.0"/> + </contour> + <contour> + <point x="171.0" y="40.0" type="curve" smooth="yes"/> + <point x="126.0" y="40.0"/> + <point x="96.0" y="70.0"/> + <point x="96.0" y="117.0" type="curve" smooth="yes"/> + <point x="96.0" y="233.0" type="line" smooth="yes"/> + <point x="96.0" y="280.0"/> + <point x="126.0" y="310.0"/> + <point x="171.0" y="310.0" type="curve" smooth="yes"/> + <point x="216.0" y="310.0"/> + <point x="246.0" y="280.0"/> + <point x="246.0" y="233.0" type="curve" smooth="yes"/> + <point x="246.0" y="117.0" type="line" smooth="yes"/> + <point x="246.0" y="70.0"/> + <point x="216.0" y="40.0"/> + </contour> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2018/11/20 11:01:11</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif new file mode 100644 index 00000000..b0f2a597 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif @@ -0,0 +1,22 @@ +<?xml version='1.0' encoding='UTF-8'?> +<glyph name="odieresis" format="2"> + <advance width="342.0"/> + <unicode hex="00F6"/> + <anchor x="171.0" y="0.0" name="bottom"/> + <anchor x="171.0" y="175.0" name="center"/> + <anchor x="311.0" y="10.0" name="ogonek"/> + <anchor x="171.0" y="475.0" name="top"/> + <anchor x="326.0" y="350.0" name="topright"/> + <outline> + <component base="o"/> + <component base="dieresiscomb" xOffset="21.0"/> + </outline> + <lib> + <dict> + <key>com.schriftgestaltung.Glyphs.lastChange</key> + <string>2018/11/20 10:59:58</string> + <key>public.markColor</key> + <string>0,0.67,0.91,1</string> + </dict> + </lib> +</glyph> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist new file mode 100644 index 00000000..f9e8ed31 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist @@ -0,0 +1,34 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>public.kern1.O</key> + <array> + <string>O</string> + <string>Odieresis</string> + </array> + <key>public.kern1.n</key> + <array> + <string>n</string> + </array> + <key>public.kern1.o</key> + <array> + <string>o</string> + <string>odieresis</string> + </array> + <key>public.kern2.O</key> + <array> + <string>O</string> + <string>Odieresis</string> + </array> + <key>public.kern2.n</key> + <array> + <string>n</string> + </array> + <key>public.kern2.o</key> + <array> + <string>o</string> + <string>odieresis</string> + </array> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist new file mode 100644 index 00000000..8da68f2e --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist @@ -0,0 +1,468 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>public.kern1.A</key> + <dict> + <key>public.kern2.O</key> + <real>-5.0</real> + <key>public.kern2.T</key> + <real>-40.0</real> + <key>public.kern2.V</key> + <real>-40.0</real> + <key>public.kern2.W</key> + <real>-25.0</real> + <key>public.kern2.Y</key> + <real>-50.0</real> + <key>public.kern2.f</key> + <real>-15.0</real> + <key>public.kern2.hyphen</key> + <real>-10.0</real> + <key>public.kern2.quote</key> + <real>-80.0</real> + <key>public.kern2.w</key> + <real>-25.0</real> + <key>public.kern2.y</key> + <real>-30.0</real> + </dict> + <key>public.kern1.B</key> + <dict> + <key>public.kern2.quote</key> + <real>-10.0</real> + </dict> + <key>public.kern1.C</key> + <dict> + <key>public.kern2.O</key> + <real>-5.0</real> + </dict> + <key>public.kern1.E</key> + <dict> + <key>public.kern2.O</key> + <real>-5.0</real> + </dict> + <key>public.kern1.F</key> + <dict> + <key>public.kern2.A</key> + <real>-35.0</real> + <key>public.kern2.a</key> + <real>-25.0</real> + <key>public.kern2.o</key> + <real>-20.0</real> + <key>public.kern2.period</key> + <real>-40.0</real> + <key>public.kern2.u</key> + <real>-15.0</real> + </dict> + <key>public.kern1.K</key> + <dict> + <key>public.kern2.O</key> + <real>-10.0</real> + </dict> + <key>public.kern1.L</key> + <dict> + <key>public.kern2.O</key> + <real>-20.0</real> + <key>public.kern2.T</key> + <real>-60.0</real> + <key>public.kern2.V</key> + <real>-65.0</real> + <key>public.kern2.W</key> + <real>-50.0</real> + <key>public.kern2.Y</key> + <real>-70.0</real> + <key>public.kern2.hyphen</key> + <real>-30.0</real> + <key>public.kern2.quote</key> + <real>-130.0</real> + </dict> + <key>public.kern1.O</key> + <dict> + <key>public.kern2.A</key> + <real>-5.0</real> + <key>public.kern2.J</key> + <real>-15.0</real> + <key>public.kern2.T</key> + <real>-15.0</real> + <key>public.kern2.V</key> + <real>-5.0</real> + <key>public.kern2.W</key> + <real>-15.0</real> + <key>public.kern2.X</key> + <real>-15.0</real> + <key>public.kern2.Y</key> + <real>-20.0</real> + <key>public.kern2.quote</key> + <real>-5.0</real> + </dict> + <key>public.kern1.P</key> + <dict> + <key>public.kern2.A</key> + <real>-30.0</real> + <key>public.kern2.J</key> + <real>-80.0</real> + <key>public.kern2.period</key> + <real>-50.0</real> + <key>public.kern2.quote</key> + <real>15.0</real> + </dict> + <key>public.kern1.R</key> + <dict> + <key>public.kern2.T</key> + <real>-15.0</real> + <key>public.kern2.W</key> + <real>-5.0</real> + <key>public.kern2.Y</key> + <real>-10.0</real> + <key>public.kern2.o</key> + <real>-10.0</real> + <key>public.kern2.quote</key> + <real>10.0</real> + </dict> + <key>public.kern1.S</key> + <dict> + <key>public.kern2.quote</key> + <real>-10.0</real> + </dict> + <key>public.kern1.T</key> + <dict> + <key>public.kern2.A</key> + <real>-40.0</real> + <key>public.kern2.J</key> + <real>-50.0</real> + <key>public.kern2.O</key> + <real>-15.0</real> + <key>public.kern2.a</key> + <real>-50.0</real> + <key>public.kern2.hyphen</key> + <real>-50.0</real> + <key>public.kern2.n</key> + <real>-40.0</real> + <key>public.kern2.o</key> + <real>-50.0</real> + <key>public.kern2.period</key> + <real>-60.0</real> + <key>public.kern2.s</key> + <real>-50.0</real> + <key>public.kern2.u</key> + <real>-40.0</real> + <key>public.kern2.w</key> + <real>-40.0</real> + <key>public.kern2.y</key> + <real>-40.0</real> + </dict> + <key>public.kern1.V</key> + <dict> + <key>public.kern2.A</key> + <real>-40.0</real> + <key>public.kern2.O</key> + <real>-5.0</real> + <key>public.kern2.a</key> + <real>-45.0</real> + <key>public.kern2.f</key> + <real>-15.0</real> + <key>public.kern2.hyphen</key> + <real>-30.0</real> + <key>public.kern2.n</key> + <real>-30.0</real> + <key>public.kern2.o</key> + <real>-45.0</real> + <key>public.kern2.period</key> + <real>-70.0</real> + <key>public.kern2.quote</key> + <real>-10.0</real> + <key>public.kern2.s</key> + <real>-35.0</real> + <key>public.kern2.u</key> + <real>-35.0</real> + <key>public.kern2.w</key> + <real>-35.0</real> + <key>public.kern2.x</key> + <real>-40.0</real> + <key>public.kern2.y</key> + <real>-35.0</real> + <key>public.kern2.z</key> + <real>-40.0</real> + </dict> + <key>public.kern1.W</key> + <dict> + <key>public.kern2.A</key> + <real>-25.0</real> + <key>public.kern2.O</key> + <real>-15.0</real> + <key>public.kern2.a</key> + <real>-25.0</real> + <key>public.kern2.f</key> + <real>-15.0</real> + <key>public.kern2.hyphen</key> + <real>-20.0</real> + <key>public.kern2.n</key> + <real>-20.0</real> + <key>public.kern2.o</key> + <real>-30.0</real> + <key>public.kern2.period</key> + <real>-40.0</real> + <key>public.kern2.quote</key> + <real>-10.0</real> + <key>public.kern2.s</key> + <real>-20.0</real> + <key>public.kern2.u</key> + <real>-30.0</real> + <key>public.kern2.w</key> + <real>-25.0</real> + <key>public.kern2.x</key> + <real>-30.0</real> + <key>public.kern2.y</key> + <real>-35.0</real> + <key>public.kern2.z</key> + <real>-25.0</real> + </dict> + <key>public.kern1.X</key> + <dict> + <key>public.kern2.O</key> + <real>-15.0</real> + <key>public.kern2.quote</key> + <real>-10.0</real> + </dict> + <key>public.kern1.Y</key> + <dict> + <key>public.kern2.A</key> + <real>-50.0</real> + <key>public.kern2.O</key> + <real>-20.0</real> + <key>public.kern2.a</key> + <real>-60.0</real> + <key>public.kern2.f</key> + <real>-20.0</real> + <key>public.kern2.hyphen</key> + <real>-60.0</real> + <key>public.kern2.n</key> + <real>-40.0</real> + <key>public.kern2.o</key> + <real>-60.0</real> + <key>public.kern2.period</key> + <real>-60.0</real> + <key>public.kern2.quote</key> + <real>-5.0</real> + <key>public.kern2.s</key> + <real>-45.0</real> + <key>public.kern2.u</key> + <real>-40.0</real> + </dict> + <key>public.kern1.Z</key> + <dict> + <key>public.kern2.quote</key> + <real>-5.0</real> + </dict> + <key>public.kern1.c</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-10.0</real> + <key>public.kern2.quote</key> + <real>20.0</real> + </dict> + <key>public.kern1.f</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-15.0</real> + <key>public.kern2.o</key> + <real>-15.0</real> + <key>public.kern2.period</key> + <real>-40.0</real> + <key>public.kern2.quote</key> + <real>45.0</real> + </dict> + <key>public.kern1.hyphen</key> + <dict> + <key>public.kern2.A</key> + <real>-10.0</real> + <key>public.kern2.T</key> + <real>-50.0</real> + <key>public.kern2.V</key> + <real>-30.0</real> + <key>public.kern2.W</key> + <real>-20.0</real> + <key>public.kern2.Y</key> + <real>-60.0</real> + <key>public.kern2.f</key> + <real>-5.0</real> + <key>public.kern2.j</key> + <real>-5.0</real> + <key>public.kern2.period</key> + <real>-20.0</real> + <key>public.kern2.quote</key> + <real>-40.0</real> + <key>public.kern2.s</key> + <real>-5.0</real> + <key>public.kern2.w</key> + <real>-10.0</real> + <key>public.kern2.x</key> + <real>-25.0</real> + <key>public.kern2.y</key> + <real>-10.0</real> + <key>public.kern2.z</key> + <real>-20.0</real> + </dict> + <key>public.kern1.k</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-5.0</real> + </dict> + <key>public.kern1.l</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-10.0</real> + <key>public.kern2.period</key> + <real>15.0</real> + <key>public.kern2.quote</key> + <real>-15.0</real> + </dict> + <key>public.kern1.n</key> + <dict> + <key>public.kern2.quote</key> + <real>-10.0</real> + <key>public.kern2.y</key> + <real>-5.0</real> + </dict> + <key>public.kern1.o</key> + <dict> + <key>public.kern2.period</key> + <real>-10.0</real> + <key>public.kern2.quote</key> + <real>-10.0</real> + <key>public.kern2.w</key> + <real>-8.0</real> + <key>public.kern2.y</key> + <real>-8.0</real> + </dict> + <key>public.kern1.period</key> + <dict> + <key>public.kern2.T</key> + <real>-60.0</real> + <key>public.kern2.V</key> + <real>-70.0</real> + <key>public.kern2.W</key> + <real>-40.0</real> + <key>public.kern2.Y</key> + <real>-60.0</real> + <key>public.kern2.f</key> + <real>-20.0</real> + <key>public.kern2.hyphen</key> + <real>-20.0</real> + <key>public.kern2.l</key> + <real>-5.0</real> + <key>public.kern2.o</key> + <real>-10.0</real> + <key>public.kern2.quote</key> + <real>-60.0</real> + <key>public.kern2.u</key> + <real>-10.0</real> + <key>public.kern2.w</key> + <real>-30.0</real> + <key>public.kern2.x</key> + <real>10.0</real> + <key>public.kern2.y</key> + <real>-30.0</real> + <key>public.kern2.z</key> + <real>5.0</real> + </dict> + <key>public.kern1.quote</key> + <dict> + <key>public.kern2.A</key> + <real>-90.0</real> + <key>public.kern2.J</key> + <real>-110.0</real> + <key>public.kern2.O</key> + <real>-10.0</real> + <key>public.kern2.T</key> + <real>10.0</real> + <key>public.kern2.a</key> + <real>-30.0</real> + <key>public.kern2.f</key> + <real>5.0</real> + <key>public.kern2.hyphen</key> + <real>-90.0</real> + <key>public.kern2.n</key> + <real>-10.0</real> + <key>public.kern2.o</key> + <real>-25.0</real> + <key>public.kern2.period</key> + <real>-80.0</real> + <key>public.kern2.s</key> + <real>-15.0</real> + <key>public.kern2.u</key> + <real>-5.0</real> + <key>public.kern2.z</key> + <real>-5.0</real> + </dict> + <key>public.kern1.r</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-5.0</real> + <key>public.kern2.period</key> + <real>-35.0</real> + <key>public.kern2.quote</key> + <real>20.0</real> + </dict> + <key>public.kern1.s</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-10.0</real> + </dict> + <key>public.kern1.t</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-15.0</real> + <key>public.kern2.o</key> + <real>-15.0</real> + <key>public.kern2.period</key> + <real>-40.0</real> + <key>public.kern2.quote</key> + <real>20.0</real> + </dict> + <key>public.kern1.u</key> + <dict> + <key>public.kern2.quote</key> + <real>15.0</real> + </dict> + <key>public.kern1.w</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-10.0</real> + <key>public.kern2.o</key> + <real>-8.0</real> + <key>public.kern2.period</key> + <real>-30.0</real> + <key>public.kern2.quote</key> + <real>15.0</real> + </dict> + <key>public.kern1.x</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-25.0</real> + <key>public.kern2.period</key> + <real>10.0</real> + <key>public.kern2.quote</key> + <real>15.0</real> + </dict> + <key>public.kern1.y</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-10.0</real> + <key>public.kern2.o</key> + <real>-8.0</real> + <key>public.kern2.period</key> + <real>-30.0</real> + <key>public.kern2.quote</key> + <real>15.0</real> + </dict> + <key>public.kern1.z</key> + <dict> + <key>public.kern2.hyphen</key> + <real>-10.0</real> + <key>public.kern2.period</key> + <real>5.0</real> + <key>public.kern2.quote</key> + <real>20.0</real> + </dict> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist new file mode 100644 index 00000000..7120d0ba --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <array> + <array> + <string>public.default</string> + <string>glyphs</string> + </array> + <array> + <string>public.background</string> + <string>glyphs.public.background</string> + </array> + </array> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist new file mode 100644 index 00000000..7e5ced48 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist @@ -0,0 +1,80 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>com.schriftgestaltung.appVersion</key> + <string>1179</string> + <key>com.schriftgestaltung.customParameter.GSFont.Axes</key> + <array> + <dict> + <key>Name</key> + <string>Slant</string> + <key>Tag</key> + <string>slnt</string> + </dict> + </array> + <key>com.schriftgestaltung.customParameter.GSFont.DisplayStrings</key> + <array> + <string>o/dieresiscomb ö</string> + </array> + <key>com.schriftgestaltung.customParameter.GSFont.Variation Font Origin</key> + <string>EB3D7718-A203-47FB-ABD4-8B7A501887ED</string> + <key>com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment</key> + <false/> + <key>com.schriftgestaltung.customParameter.GSFont.useNiceNames</key> + <integer>1</integer> + <key>com.schriftgestaltung.customParameter.GSFontMaster.Master Name</key> + <string>Regular</string> + <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue</key> + <real>0.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue1</key> + <real>16.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue2</key> + <real>0.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue3</key> + <real>0.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.iconName</key> + <string></string> + <key>com.schriftgestaltung.customParameter.GSFontMaster.weightValue</key> + <real>0.0</real> + <key>com.schriftgestaltung.customParameter.GSFontMaster.widthValue</key> + <real>100.0</real> + <key>com.schriftgestaltung.customValue1</key> + <real>16.0</real> + <key>com.schriftgestaltung.fontMasterOrder</key> + <integer>0</integer> + <key>com.schriftgestaltung.glyphOrder</key> + <false/> + <key>com.schriftgestaltung.keyboardIncrement</key> + <integer>1</integer> + <key>com.schriftgestaltung.weight</key> + <string>Regular</string> + <key>com.schriftgestaltung.weightValue</key> + <real>0.0</real> + <key>com.schriftgestaltung.width</key> + <string>Regular</string> + <key>com.schriftgestaltung.widthValue</key> + <real>100.0</real> + <key>noodleExtremesAndInflections</key> + <integer>0</integer> + <key>noodleRemoveOverlap</key> + <integer>1</integer> + <key>noodleThickness</key> + <string>50.0</string> + <key>public.glyphOrder</key> + <array> + <string>N</string> + <string>O</string> + <string>Odieresis</string> + <string>n</string> + <string>o</string> + <string>odieresis</string> + <string>dieresiscomb</string> + </array> + <key>public.postscriptNames</key> + <dict> + <key>dieresiscomb</key> + <string>uni0308</string> + </dict> + </dict> +</plist> diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist new file mode 100644 index 00000000..7b8b34ac --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist @@ -0,0 +1,10 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>creator</key> + <string>com.github.fonttools.ufoLib</string> + <key>formatVersion</key> + <integer>3</integer> + </dict> +</plist> diff --git a/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx b/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx new file mode 100644 index 00000000..ce5b55d2 --- /dev/null +++ b/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx @@ -0,0 +1,229 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40"> + + <gvar> + <version value="1"/> + <reserved value="0"/> + <glyphVariations glyph="N"> + <tuple> + <coord axis="slnt" value="-1.0"/> + <delta pt="0" x="-41" y="0"/> + <delta pt="1" x="-43" y="0"/> + <delta pt="2" x="-43" y="5"/> + <delta pt="3" x="-40" y="7"/> + <delta pt="4" x="82" y="7"/> + <delta pt="5" x="84" y="5"/> + <delta pt="6" x="83" y="0"/> + <delta pt="7" x="80" y="0"/> + <delta pt="8" x="79" y="0"/> + <delta pt="9" x="84" y="-1"/> + <delta pt="10" x="83" y="-5"/> + <delta pt="11" x="-17" y="-3"/> + <delta pt="12" x="82" y="6"/> + <delta pt="13" x="84" y="3"/> + <delta pt="14" x="82" y="0"/> + <delta pt="15" x="81" y="0"/> + <delta pt="16" x="85" y="0"/> + <delta pt="17" x="82" y="-10"/> + <delta pt="18" x="80" y="-7"/> + <delta pt="19" x="-42" y="-7"/> + <delta pt="20" x="-44" y="-6"/> + <delta pt="21" x="-44" y="0"/> + <delta pt="22" x="-41" y="0"/> + <delta pt="23" x="-39" y="0"/> + <delta pt="24" x="-44" y="1"/> + <delta pt="25" x="-43" y="5"/> + <delta pt="26" x="57" y="3"/> + <delta pt="27" x="-42" y="-6"/> + <delta pt="28" x="-44" y="-4"/> + <delta pt="29" x="-43" y="0"/> + <delta pt="30" x="0" y="0"/> + <delta pt="31" x="0" y="0"/> + <delta pt="32" x="0" y="0"/> + <delta pt="33" x="0" y="0"/> + </tuple> + </glyphVariations> + <glyphVariations glyph="O"> + <tuple> + <coord axis="slnt" value="-1.0"/> + <delta pt="0" x="-43" y="0"/> + <delta pt="1" x="-45" y="0"/> + <delta pt="2" x="-36" y="2"/> + <delta pt="3" x="-19" y="6"/> + <delta pt="4" x="-8" y="6"/> + <delta pt="5" x="51" y="6"/> + <delta pt="6" x="62" y="5"/> + <delta pt="7" x="76" y="2"/> + <delta pt="8" x="84" y="0"/> + <delta pt="9" x="83" y="0"/> + <delta pt="10" x="85" y="0"/> + <delta pt="11" x="77" y="-2"/> + <delta pt="12" x="60" y="-5"/> + <delta pt="13" x="49" y="-6"/> + <delta pt="14" x="-10" y="-7"/> + <delta pt="15" x="-20" y="-5"/> + <delta pt="16" x="-36" y="-2"/> + <delta pt="17" x="-44" y="0"/> + <delta pt="18" x="-43" y="0"/> + <delta pt="19" x="-42" y="0"/> + <delta pt="20" x="-31" y="2"/> + <delta pt="21" x="-16" y="4"/> + <delta pt="22" x="-8" y="6"/> + <delta pt="23" x="51" y="6"/> + <delta pt="24" x="58" y="5"/> + <delta pt="25" x="72" y="2"/> + <delta pt="26" x="82" y="0"/> + <delta pt="27" x="83" y="0"/> + <delta pt="28" x="81" y="0"/> + <delta pt="29" x="71" y="-2"/> + <delta pt="30" x="57" y="-5"/> + <delta pt="31" x="49" y="-6"/> + <delta pt="32" x="-10" y="-7"/> + <delta pt="33" x="-17" y="-6"/> + <delta pt="34" x="-31" y="-2"/> + <delta pt="35" x="-42" y="0"/> + <delta pt="36" x="0" y="0"/> + <delta pt="37" x="0" y="0"/> + <delta pt="38" x="0" y="0"/> + <delta pt="39" x="0" y="0"/> + </tuple> + </glyphVariations> + <glyphVariations glyph="Odieresis"> + <tuple> + <coord axis="slnt" value="-1.0"/> + <delta pt="0" x="0" y="0"/> + <delta pt="1" x="40" y="0"/> + <delta pt="2" x="0" y="0"/> + <delta pt="3" x="0" y="0"/> + <delta pt="4" x="0" y="0"/> + <delta pt="5" x="0" y="0"/> + </tuple> + </glyphVariations> + <glyphVariations glyph="n"> + <tuple> + <coord axis="slnt" value="-1.0"/> + <delta pt="0" x="-41" y="0"/> + <delta pt="1" x="-43" y="0"/> + <delta pt="2" x="-44" y="4"/> + <delta pt="3" x="-40" y="7"/> + <delta pt="4" x="40" y="4"/> + <delta pt="5" x="46" y="0"/> + <delta pt="6" x="43" y="0"/> + <delta pt="7" x="40" y="0"/> + <delta pt="8" x="41" y="0"/> + <delta pt="9" x="37" y="-3"/> + <delta pt="10" x="27" y="-6"/> + <delta pt="11" x="19" y="-6"/> + <delta pt="12" x="-42" y="-6"/> + <delta pt="13" x="-44" y="-5"/> + <delta pt="14" x="-43" y="0"/> + <delta pt="15" x="-41" y="0"/> + <delta pt="16" x="-43" y="0"/> + <delta pt="17" x="-44" y="4"/> + <delta pt="18" x="-40" y="7"/> + <delta pt="19" x="21" y="7"/> + <delta pt="20" x="26" y="4"/> + <delta pt="21" x="38" y="0"/> + <delta pt="22" x="40" y="0"/> + <delta pt="23" x="34" y="0"/> + <delta pt="24" x="-42" y="-6"/> + <delta pt="25" x="-44" y="-4"/> + <delta pt="26" x="-43" y="0"/> + <delta pt="27" x="0" y="0"/> + <delta pt="28" x="0" y="0"/> + <delta pt="29" x="0" y="0"/> + <delta pt="30" x="0" y="0"/> + </tuple> + </glyphVariations> + <glyphVariations glyph="o"> + <tuple> + <coord axis="slnt" value="-1.0"/> + <delta pt="0" x="-42" y="0"/> + <delta pt="1" x="-44" y="0"/> + <delta pt="2" x="-38" y="2"/> + <delta pt="3" x="-24" y="6"/> + <delta pt="4" x="-14" y="7"/> + <delta pt="5" x="18" y="7"/> + <delta pt="6" x="28" y="6"/> + <delta pt="7" x="41" y="3"/> + <delta pt="8" x="45" y="0"/> + <delta pt="9" x="44" y="0"/> + <delta pt="10" x="45" y="0"/> + <delta pt="11" x="40" y="-2"/> + <delta pt="12" x="27" y="-5"/> + <delta pt="13" x="16" y="-7"/> + <delta pt="14" x="-16" y="-7"/> + <delta pt="15" x="-26" y="-5"/> + <delta pt="16" x="-39" y="-3"/> + <delta pt="17" x="-44" y="0"/> + <delta pt="18" x="-42" y="0"/> + <delta pt="19" x="-40" y="0"/> + <delta pt="20" x="-24" y="4"/> + <delta pt="21" x="-14" y="7"/> + <delta pt="22" x="18" y="7"/> + <delta pt="23" x="27" y="5"/> + <delta pt="24" x="42" y="0"/> + <delta pt="25" x="44" y="0"/> + <delta pt="26" x="42" y="0"/> + <delta pt="27" x="27" y="-4"/> + <delta pt="28" x="16" y="-7"/> + <delta pt="29" x="-16" y="-7"/> + <delta pt="30" x="-25" y="-5"/> + <delta pt="31" x="-40" y="0"/> + <delta pt="32" x="0" y="0"/> + <delta pt="33" x="0" y="0"/> + <delta pt="34" x="0" y="0"/> + <delta pt="35" x="0" y="0"/> + </tuple> + </glyphVariations> + <glyphVariations glyph="odieresis"> + <tuple> + <coord axis="slnt" value="-1.0"/> + <delta pt="0" x="0" y="0"/> + <delta pt="1" x="0" y="0"/> + <delta pt="2" x="0" y="0"/> + <delta pt="3" x="0" y="0"/> + <delta pt="4" x="0" y="0"/> + <delta pt="5" x="0" y="0"/> + </tuple> + </glyphVariations> + <glyphVariations glyph="uni0308"> + <tuple> + <coord axis="slnt" value="-1.0"/> + <delta pt="0" x="75" y="0"/> + <delta pt="1" x="73" y="0"/> + <delta pt="2" x="73" y="5"/> + <delta pt="3" x="76" y="6"/> + <delta pt="4" x="82" y="5"/> + <delta pt="5" x="84" y="3"/> + <delta pt="6" x="82" y="0"/> + <delta pt="7" x="81" y="0"/> + <delta pt="8" x="82" y="0"/> + <delta pt="9" x="83" y="-6"/> + <delta pt="10" x="80" y="-6"/> + <delta pt="11" x="74" y="-5"/> + <delta pt="12" x="72" y="-4"/> + <delta pt="13" x="73" y="0"/> + <delta pt="14" x="75" y="0"/> + <delta pt="15" x="73" y="0"/> + <delta pt="16" x="73" y="5"/> + <delta pt="17" x="76" y="6"/> + <delta pt="18" x="82" y="5"/> + <delta pt="19" x="84" y="3"/> + <delta pt="20" x="82" y="0"/> + <delta pt="21" x="81" y="0"/> + <delta pt="22" x="82" y="0"/> + <delta pt="23" x="83" y="-6"/> + <delta pt="24" x="80" y="-6"/> + <delta pt="25" x="74" y="-5"/> + <delta pt="26" x="72" y="-4"/> + <delta pt="27" x="73" y="0"/> + <delta pt="28" x="0" y="0"/> + <delta pt="29" x="0" y="0"/> + <delta pt="30" x="0" y="0"/> + <delta pt="31" x="0" y="0"/> + </tuple> + </glyphVariations> + </gvar> + +</ttFont> diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx index 33ebbd4a..66c80326 100644 --- a/Tests/varLib/data/test_results/BuildMain.ttx +++ b/Tests/varLib/data/test_results/BuildMain.ttx @@ -782,7 +782,7 @@ </MVAR> <STAT> - <Version value="0x00010002"/> + <Version value="0x00010001"/> <DesignAxisRecordSize value="8"/> <!-- DesignAxisCount=2 --> <DesignAxisRecord> diff --git a/Tests/varLib/data/test_results/BuildTestCFF2.ttx b/Tests/varLib/data/test_results/BuildTestCFF2.ttx new file mode 100644 index 00000000..a4e859f1 --- /dev/null +++ b/Tests/varLib/data/test_results/BuildTestCFF2.ttx @@ -0,0 +1,265 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="OTTO" ttLibVersion="3.32"> + + <fvar> + + <!-- Weight --> + <Axis> + <AxisTag>wght</AxisTag> + <Flags>0x0</Flags> + <MinValue>200.0</MinValue> + <DefaultValue>400.0</DefaultValue> + <MaxValue>900.0</MaxValue> + <AxisNameID>256</AxisNameID> + </Axis> + + <!-- ExtraLight --> + <!-- PostScript: TestCFF2Roman-ExtraLight --> + <NamedInstance flags="0x0" postscriptNameID="258" subfamilyNameID="257"> + <coord axis="wght" value="200.0"/> + </NamedInstance> + + <!-- Light --> + <!-- PostScript: TestCFF2Roman-Light --> + <NamedInstance flags="0x0" postscriptNameID="260" subfamilyNameID="259"> + <coord axis="wght" value="300.0"/> + </NamedInstance> + + <!-- Regular --> + <!-- PostScript: TestCFF2Roman-Regular --> + <NamedInstance flags="0x0" postscriptNameID="262" subfamilyNameID="261"> + <coord axis="wght" value="400.0"/> + </NamedInstance> + + <!-- Medium --> + <!-- PostScript: TestCFF2Roman-Medium --> + <NamedInstance flags="0x0" postscriptNameID="264" subfamilyNameID="263"> + <coord axis="wght" value="500.0"/> + </NamedInstance> + + <!-- Semibold --> + <!-- PostScript: TestCFF2Roman-Semibold --> + <NamedInstance flags="0x0" postscriptNameID="266" subfamilyNameID="265"> + <coord axis="wght" value="600.0"/> + </NamedInstance> + + <!-- Bold --> + <!-- PostScript: TestCFF2Roman-Bold --> + <NamedInstance flags="0x0" postscriptNameID="268" subfamilyNameID="267"> + <coord axis="wght" value="700.0"/> + </NamedInstance> + + <!-- Black --> + <!-- PostScript: TestCFF2Roman-Black --> + <NamedInstance flags="0x0" postscriptNameID="270" subfamilyNameID="269"> + <coord axis="wght" value="900.0"/> + </NamedInstance> + </fvar> + + <CFF2> + <major value="2"/> + <minor value="0"/> + <CFFFont name="CFF2Font"> + <FontMatrix value="0.001 0 0 0.001 0 0"/> + <FDArray> + <FontDict index="0"> + <Private> + <BlueValues> + <blend value="-12 0 0"/> + <blend value="0 0 0"/> + <blend value="486 -8 14"/> + <blend value="498 0 0"/> + <blend value="574 4 -8"/> + <blend value="586 0 0"/> + <blend value="638 6 -10"/> + <blend value="650 0 0"/> + <blend value="656 2 -2"/> + <blend value="668 0 0"/> + <blend value="712 6 -10"/> + <blend value="724 0 0"/> + </BlueValues> + <OtherBlues> + <blend value="-217 -17 29"/> + <blend value="-205 0 0"/> + </OtherBlues> + <BlueScale value="0.0625"/> + <BlueShift value="7"/> + <BlueFuzz value="0"/> + <StdHW> + <blend value="67 -39.0 67.0"/> + </StdHW> + <StdVW> + <blend value="85 -51.0 87.0"/> + </StdVW> + </Private> + </FontDict> + </FDArray> + <CharStrings> + <CharString name=".notdef"> + 62 22 -38 1 blend + hmoveto + 476 -44 76 1 blend + 660 -476 44 -76 1 blend + -660 hlineto + 109 59 -61 103 -27 45 2 blend + rmoveto + 73 131 54 102 29 -47 45 -75 10 -18 4 -6 4 blend + 4 0 52 -102 73 -131 10 -16 -4 6 27 -47 -45 75 4 blend + rlineto + -256 -76 128 1 blend + hlineto + -44 52 34 -56 -10 16 2 blend + rmoveto + 461 75 -125 1 blend + vlineto + 127 -232 -127 -229 27 -45 -38 64 -27 45 -37 61 4 blend + rlineto + 171 277 5 -9 15 -25 2 blend + rmoveto + -50 93 -66 119 234 -6 10 -1 3 -28 48 49 -83 68 -114 5 blend + 0 -65 -119 -49 -93 -29 47 -49 83 -5 9 1 -3 4 blend + rlineto + -4 hlineto + 48 -48 -22 36 22 -36 2 blend + rmoveto + 126 232 26 -44 38 -64 2 blend + 0 -461 -126 229 -75 125 -26 44 37 -61 3 blend + rlineto + </CharString> + <CharString name="A"> + 31 19 -31 1 blend + hmoveto + 86 -54 90 1 blend + hlineto + 115 366 23 73 21 72 21 76 25 -42 30 -50 5 -9 7 -11 3 -4 -4 6 3 -7 6 -10 8 blend + rlinecurve + 4 hlineto + 20 -76 22 -72 23 -73 4 -6 -6 10 2 -3 4 -6 5 -9 -7 11 6 blend + rrcurveto + 113 -366 90 25 -40 -30 50 -56 92 3 blend + 0 -221 656 -96 -15 25 4 -6 68 -112 3 blend + 0 -221 -656 -15 25 -4 6 2 blend + rlineto + 117 199 -15 24 37 -61 2 blend + rmoveto + 301 68 -301 -68 -8 15 -40 65 8 -15 40 -65 4 blend + hlineto + </CharString> + <CharString name="T"> + 258 26 -44 1 blend + hmoveto + 84 585 217 71 -518 -71 217 -585 -52 88 47 -79 17 -30 -43 73 18 -28 43 -73 17 -30 -47 79 8 blend + hlineto + </CharString> + <CharString name="dollar"> + 248 35 -3 12 -28 4 2 blend + rmoveto + -39 -45 5 18 -46 -26 -26 6 17 10 6 32 6 0 -3 5 blend + hvcurveto + 53 -36 -17 76 -17 36 -12 -17 -11 2 24 13 4 blend + rlineto + 53 -12 -22 13 -24 -37 -1 8 3 10 0 -9 5 13 -19 5 blend + hhcurveto + -22 -14 -11 -20 -9 8 -4 6 -13 4 -3 6 -18 8 -5 5 blend + hvcurveto + -87 4 81 -59 107 2 -3 20 -4 -20 -10 8 5 0 32 5 blend + hhcurveto + 136 82 76 107 82 -41 65 -135 47 -45 27 8 17 -23 8 4 10 -12 16 15 -17 1 3 1 -7 10 -2 9 blend + hvcurveto + -38 13 19 5 -5 -3 2 blend + rlineto + -71 23 -40 35 64 -22 -1 16 -1 -2 16 14 -11 4 -15 5 blend + vvcurveto + 75 57 37 74 30 36 -5 -17 42 16 -14 3 -10 11 -14 14 -7 26 12 -1 -9 -9 1 -33 -7 2 10 9 blend + vhcurveto + -52 36 17 -76 14 -33 11 11 11 -7 -24 9 4 blend + rlineto + -52 12 25 -14 22 37 -23 -6 -1 -15 12 9 0 -11 17 5 blend + hhcurveto + 19 17 10 21 8 -5 7 -9 12 -3 5 -7 20 -7 -3 5 blend + hvcurveto + 86 -6 -80 60 -101 2 2 -18 -2 13 4 -12 -12 17 -20 5 blend + hhcurveto + -115 -83 -80 -102 -100 62 -54 105 -37 23 -43 1 -2 29 0 -6 -13 20 7 -17 4 1 -15 -13 16 -5 -2 9 blend + hvcurveto + 37 -13 0 -5 -4 2 2 blend + rlineto + 85 -30 36 -30 -63 29 -5 -22 2 -10 -13 -16 11 -2 10 5 blend + vvcurveto + -74 -53 -42 -82 -18 19 -12 10 -12 3 -8 10 4 blend + vhcurveto + 31 287 -13 33 40 -12 2 blend + rmoveto + 428 -40 -428 40 0 -11 18 -31 0 11 -18 31 4 blend + vlineto + -41 -437 19 -38 -12 8 2 blend + rmoveto + 40 437 -40 -437 -18 31 12 -8 18 -31 -12 8 4 blend + hlineto + </CharString> + <CharString name="glyph00003"> + 304 7 -12 1 blend + 34 rmoveto + 125 86 65 96 -22 38 2 -3 -9 15 -2 4 4 blend + hvcurveto + 183 -324 -21 110 1 -1 -14 22 -11 17 32 -54 4 blend + vvcurveto + 50 42 32 67 68 36 -21 -36 47 18 -29 15 -24 12 -21 18 -31 8 -13 -2 3 -3 5 -2 4 -3 5 9 blend + vhcurveto + 44 49 -24 40 -29 49 2 blend + rlineto + 44 -46 -54 33 -89 -6 8 5 -7 9 -15 -1 3 4 -8 5 blend + hhcurveto + -115 -81 -59 -94 16 -26 3 -7 5 -9 6 -10 4 blend + hvcurveto + -174 324 22 -124 8 -14 14 -22 6 -10 -32 56 4 blend + vvcurveto + -51 -42 -35 -78 -76 -62 31 37 -52 -19 31 -14 23 -15 25 -25 41 -9 15 -4 7 7 -11 -3 3 12 -20 9 blend + vhcurveto + -39 -58 21 -35 36 -58 2 blend + rlineto + -43 52 84 -36 83 5 -11 -7 13 -11 17 -4 8 8 -13 5 blend + hhcurveto + -51 -147 -19 32 1 -3 2 blend + rmoveto + 159 857 -56 7 -159 -858 56 -6 -1 1 3 -3 26 -44 -3 5 1 -1 -2 4 -26 44 2 -6 8 blend + rlineto + </CharString> + </CharStrings> + <VarStore Format="1"> + <Format value="1"/> + <VarRegionList> + <!-- RegionAxisCount=1 --> + <!-- RegionCount=2 --> + <Region index="0"> + <VarRegionAxis index="0"> + <StartCoord value="-1.0"/> + <PeakCoord value="-1.0"/> + <EndCoord value="0.0"/> + </VarRegionAxis> + </Region> + <Region index="1"> + <VarRegionAxis index="0"> + <StartCoord value="0.0"/> + <PeakCoord value="1.0"/> + <EndCoord value="1.0"/> + </VarRegionAxis> + </Region> + </VarRegionList> + <!-- VarDataCount=1 --> + <VarData index="0"> + <!-- ItemCount=0 --> + <NumShorts value="0"/> + <!-- VarRegionCount=2 --> + <VarRegionIndex index="0" value="0"/> + <VarRegionIndex index="1" value="1"/> + </VarData> + </VarStore> + </CFFFont> + + <GlobalSubrs> + <!-- The 'index' attribute is only for humans; it is ignored when parsed. --> + </GlobalSubrs> + </CFF2> + +</ttFont> diff --git a/Tests/varLib/data/test_results/FeatureVars.ttx b/Tests/varLib/data/test_results/FeatureVars.ttx index 0764bb84..f2f6b05d 100644 --- a/Tests/varLib/data/test_results/FeatureVars.ttx +++ b/Tests/varLib/data/test_results/FeatureVars.ttx @@ -95,7 +95,7 @@ </ConditionTable> </ConditionSet> <FeatureTableSubstitution> - <Version value="0x00010001"/> + <Version value="0x00010000"/> <!-- SubstitutionCount=1 --> <SubstitutionRecord index="0"> <FeatureIndex value="0"/> @@ -109,21 +109,26 @@ </FeatureVariationRecord> <FeatureVariationRecord index="1"> <ConditionSet> - <!-- ConditionCount=1 --> + <!-- ConditionCount=2 --> <ConditionTable index="0" Format="1"> + <AxisIndex value="1"/> + <FilterRangeMinValue value="0.0"/> + <FilterRangeMaxValue value="0.25"/> + </ConditionTable> + <ConditionTable index="1" Format="1"> <AxisIndex value="0"/> - <FilterRangeMinValue value="0.20886"/> - <FilterRangeMaxValue value="1.0"/> + <FilterRangeMinValue value="-1.0"/> + <FilterRangeMaxValue value="-0.45654"/> </ConditionTable> </ConditionSet> <FeatureTableSubstitution> - <Version value="0x00010001"/> + <Version value="0x00010000"/> <!-- SubstitutionCount=1 --> <SubstitutionRecord index="0"> <FeatureIndex value="0"/> <Feature> <!-- LookupCount=1 --> - <LookupListIndex index="0" value="0"/> + <LookupListIndex index="0" value="2"/> </Feature> </SubstitutionRecord> </FeatureTableSubstitution> @@ -138,7 +143,7 @@ </ConditionTable> </ConditionSet> <FeatureTableSubstitution> - <Version value="0x00010001"/> + <Version value="0x00010000"/> <!-- SubstitutionCount=1 --> <SubstitutionRecord index="0"> <FeatureIndex value="0"/> @@ -151,26 +156,21 @@ </FeatureVariationRecord> <FeatureVariationRecord index="3"> <ConditionSet> - <!-- ConditionCount=2 --> + <!-- ConditionCount=1 --> <ConditionTable index="0" Format="1"> - <AxisIndex value="1"/> - <FilterRangeMinValue value="0.0"/> - <FilterRangeMaxValue value="0.25"/> - </ConditionTable> - <ConditionTable index="1" Format="1"> <AxisIndex value="0"/> - <FilterRangeMinValue value="-1.0"/> - <FilterRangeMaxValue value="-0.45654"/> + <FilterRangeMinValue value="0.20886"/> + <FilterRangeMaxValue value="1.0"/> </ConditionTable> </ConditionSet> <FeatureTableSubstitution> - <Version value="0x00010001"/> + <Version value="0x00010000"/> <!-- SubstitutionCount=1 --> <SubstitutionRecord index="0"> <FeatureIndex value="0"/> <Feature> <!-- LookupCount=1 --> - <LookupListIndex index="0" value="2"/> + <LookupListIndex index="0" value="0"/> </Feature> </SubstitutionRecord> </FeatureTableSubstitution> diff --git a/Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx b/Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx new file mode 100644 index 00000000..e2d0f71c --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="OTTO" ttLibVersion="3.32"> + + <hmtx> + <mtx name=".notdef" width="600" lsb="84"/> + <mtx name="A" width="600" lsb="50"/> + <mtx name="T" width="600" lsb="50"/> + <mtx name="dollar" width="600" lsb="102"/> + <mtx name="glyph00003" width="600" lsb="102"/> + </hmtx> + + <CFF2> + <major value="2"/> + <minor value="0"/> + <CFFFont name="CFF2Font"> + <FontMatrix value="0.001 0 0 0.001 0 0"/> + <FDArray> + <FontDict index="0"> + <Private> + <BlueValues value="-12 0 478 490 570 582 640 652 660 672 722 734"/> + <OtherBlues value="-234 -222"/> + <BlueScale value="0.0625"/> + <BlueShift value="7"/> + <BlueFuzz value="0"/> + <StdHW value="28"/> + <StdVW value="34"/> + </Private> + </FontDict> + </FDArray> + <CharStrings> + <CharString name=".notdef"> + 84 hmoveto + 432 660 -432 hlineto + 48 -628 rmoveto + 102 176 64 106 rlineto + 4 hlineto + 62 -106 100 -176 rlineto + -342 42 rmoveto + 536 vlineto + 154 -270 rlineto + 22 26 rmoveto + -56 92 -94 168 rlineto + 302 hlineto + -94 -168 -54 -92 rlineto + 22 -26 rmoveto + 152 270 rlineto + -536 vlineto + </CharString> + <CharString name="A"> + 50 hmoveto + 32 hlineto + 140 396 28 80 24 68 24 82 rlinecurve + 4 hlineto + 24 -82 24 -68 28 -80 138 -396 rcurveline + 34 hlineto + -236 660 rlineto + -28 hlineto + -134 -424 rmoveto + 293 28 -293 hlineto + </CharString> + <CharString name="T"> + 284 hmoveto + 32 632 234 28 -500 -28 234 hlineto + </CharString> + <CharString name="dollar"> + 311 34 rmoveto + 103 88 56 94 hvcurveto + 184 -338 -32 142 vvcurveto + 68 57 44 85 76 34 -24 -38 44 vhcurveto + 20 20 rlineto + 38 -41 -45 32 -85 hhcurveto + -99 -78 -54 -88 hvcurveto + -166 338 28 -156 vvcurveto + -70 -56 -50 -103 -85 -66 38 34 -40 vhcurveto + -18 -22 45 -38 73 -40 91 0 rlinecurve + -18 566 rmoveto + 30 hlineto + 50 0 50 50 vvcurveto + -30 hlineto + -50 0 -50 -50 vvcurveto + -562 vmoveto + -148 30 148 vlineto + </CharString> + <CharString name="glyph00003"> + 311 34 rmoveto + 103 88 56 94 hvcurveto + 184 -338 -32 142 vvcurveto + 68 57 44 85 76 34 -24 -38 44 vhcurveto + 20 20 rlineto + 38 -41 -45 32 -85 hhcurveto + -99 -78 -54 -88 hvcurveto + -166 338 28 -156 vvcurveto + -70 -56 -50 -103 -85 -66 38 34 -40 vhcurveto + -18 -22 rlineto + -38 45 73 -40 91 hhcurveto + -70 -146 rmoveto + 158 860 -30 4 -158 -860 rlineto + </CharString> + </CharStrings> + </CFFFont> + + <GlobalSubrs> + <!-- The 'index' attribute is only for humans; it is ignored when parsed. --> + </GlobalSubrs> + </CFF2> + +</ttFont> diff --git a/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx b/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx new file mode 100644 index 00000000..a20f0e4f --- /dev/null +++ b/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx @@ -0,0 +1,297 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.32"> + + <GlyphOrder> + <!-- The 'id' attribute is only for humans; it is ignored when parsed. --> + <GlyphID id="0" name=".notdef"/> + <GlyphID id="1" name="NULL"/> + <GlyphID id="2" name="nonmarkingreturn"/> + <GlyphID id="3" name="space"/> + <GlyphID id="4" name="b"/> + <GlyphID id="5" name="q"/> + <GlyphID id="6" name="a"/> + </GlyphOrder> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="750"/> + <descent value="-250"/> + <lineGap value="9"/> + <advanceWidthMax value="464"/> + <minLeftSideBearing value="38"/> + <minRightSideBearing value="38"/> + <xMaxExtent value="426"/> + <caretSlopeRise value="1"/> + <caretSlopeRun value="0"/> + <caretOffset value="0"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="5"/> + </hhea> + + <maxp> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="0x10000"/> + <numGlyphs value="7"/> + <maxPoints value="20"/> + <maxContours value="2"/> + <maxCompositePoints value="20"/> + <maxCompositeContours value="2"/> + <maxZones value="1"/> + <maxTwilightPoints value="0"/> + <maxStorage value="0"/> + <maxFunctionDefs value="0"/> + <maxInstructionDefs value="1"/> + <maxStackElements value="2"/> + <maxSizeOfInstructions value="0"/> + <maxComponentElements value="1"/> + <maxComponentDepth value="1"/> + </maxp> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="3"/> + <xAvgCharWidth value="347"/> + <usWeightClass value="400"/> + <usWidthClass value="3"/> + <fsType value="00000000 00000100"/> + <ySubscriptXSize value="700"/> + <ySubscriptYSize value="650"/> + <ySubscriptXOffset value="0"/> + <ySubscriptYOffset value="140"/> + <ySuperscriptXSize value="700"/> + <ySuperscriptYSize value="650"/> + <ySuperscriptXOffset value="0"/> + <ySuperscriptYOffset value="477"/> + <yStrikeoutSize value="50"/> + <yStrikeoutPosition value="250"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="0"/> + <bSerifStyle value="0"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="0"/> + <bStrokeVariation value="0"/> + <bArmStyle value="0"/> + <bLetterForm value="0"/> + <bMidline value="0"/> + <bXHeight value="0"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/> + <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="LuFo"/> + <fsSelection value="00000000 01000000"/> + <usFirstCharIndex value="0"/> + <usLastCharIndex value="113"/> + <sTypoAscender value="750"/> + <sTypoDescender value="-250"/> + <sTypoLineGap value="0"/> + <usWinAscent value="608"/> + <usWinDescent value="152"/> + <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="456"/> + <sCapHeight value="608"/> + <usDefaultChar value="0"/> + <usBreakChar value="32"/> + <usMaxContext value="0"/> + </OS_2> + + <hmtx> + <mtx name=".notdef" width="200" lsb="0"/> + <mtx name="NULL" width="0" lsb="0"/> + <mtx name="a" width="464" lsb="38"/> + <mtx name="b" width="464" lsb="76"/> + <mtx name="nonmarkingreturn" width="200" lsb="0"/> + <mtx name="q" width="464" lsb="38"/> + <mtx name="space" width="200" lsb="0"/> + </hmtx> + + <cmap> + <tableVersion version="0"/> + <cmap_format_4 platformID="0" platEncID="3" language="0"> + <map code="0x0" name="NULL"/><!-- ???? --> + <map code="0xd" name="nonmarkingreturn"/><!-- ???? --> + <map code="0x20" name="space"/><!-- SPACE --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + <map code="0x62" name="b"/><!-- LATIN SMALL LETTER B --> + <map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q --> + </cmap_format_4> + <cmap_format_6 platformID="1" platEncID="0" language="0"> + <map code="0x0" name="NULL"/> + <map code="0xd" name="nonmarkingreturn"/> + <map code="0x20" name="space"/> + <map code="0x61" name="a"/> + <map code="0x62" name="b"/> + <map code="0x71" name="q"/> + </cmap_format_6> + <cmap_format_4 platformID="3" platEncID="1" language="0"> + <map code="0x0" name="NULL"/><!-- ???? --> + <map code="0xd" name="nonmarkingreturn"/><!-- ???? --> + <map code="0x20" name="space"/><!-- SPACE --> + <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A --> + <map code="0x62" name="b"/><!-- LATIN SMALL LETTER B --> + <map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q --> + </cmap_format_4> + </cmap> + + <fpgm> + <assembly> + PUSHB[ ] /* 1 value pushed */ + 145 + IDEF[ ] /* InstructionDefinition */ + NPUSHW[ ] /* 3 values pushed */ + 2 -8192 8192 + ENDF[ ] /* EndFunctionDefinition */ + </assembly> + </fpgm> + + <loca> + <!-- The 'loca' table will be calculated by the compiler --> + </loca> + + <glyf> + + <!-- The xMin, yMin, xMax and yMax values + will be recalculated by the compiler. --> + + <TTGlyph name=".notdef"/><!-- contains no outline data --> + + <TTGlyph name="NULL"/><!-- contains no outline data --> + + <TTGlyph name="a" xMin="38" yMin="-12" xMax="388" yMax="468"> + <contour> + <pt x="312" y="0" on="1"/> + <pt x="312" y="64" on="1"/> + <pt x="244" y="-12" on="1"/> + <pt x="180" y="-12" on="1"/> + <pt x="38" y="140" on="1"/> + <pt x="38" y="316" on="1"/> + <pt x="180" y="468" on="1"/> + <pt x="246" y="468" on="1"/> + <pt x="312" y="392" on="1"/> + <pt x="312" y="456" on="1"/> + <pt x="388" y="456" on="1"/> + <pt x="388" y="0" on="1"/> + </contour> + <contour> + <pt x="236" y="64" on="1"/> + <pt x="312" y="140" on="1"/> + <pt x="312" y="316" on="1"/> + <pt x="236" y="392" on="1"/> + <pt x="160" y="392" on="1"/> + <pt x="84" y="316" on="1"/> + <pt x="84" y="140" on="1"/> + <pt x="160" y="64" on="1"/> + </contour> + <instructions> + <assembly> + GETVARIATION[ ] /* GetVariation */ + </assembly> + </instructions> + </TTGlyph> + + <TTGlyph name="b" xMin="76" yMin="-12" xMax="426" yMax="628"> + <contour> + <pt x="218" y="468" on="1"/> + <pt x="284" y="468" on="1"/> + <pt x="426" y="316" on="1"/> + <pt x="426" y="140" on="1"/> + <pt x="284" y="-12" on="1"/> + <pt x="220" y="-12" on="1"/> + <pt x="152" y="64" on="1"/> + <pt x="152" y="0" on="1"/> + <pt x="76" y="0" on="1"/> + <pt x="76" y="628" on="1"/> + <pt x="152" y="628" on="1"/> + <pt x="152" y="392" on="1"/> + </contour> + <contour> + <pt x="152" y="316" on="1"/> + <pt x="152" y="140" on="1"/> + <pt x="218" y="64" on="1"/> + <pt x="284" y="64" on="1"/> + <pt x="350" y="140" on="1"/> + <pt x="350" y="316" on="1"/> + <pt x="284" y="392" on="1"/> + <pt x="218" y="392" on="1"/> + </contour> + <instructions/> + </TTGlyph> + + <TTGlyph name="nonmarkingreturn"/><!-- contains no outline data --> + + <TTGlyph name="q" xMin="38" yMin="-172" xMax="388" yMax="468"> + <component glyphName="b" x="464" y="456" scale="-0.99994" flags="0x4"/> + </TTGlyph> + + <TTGlyph name="space"/><!-- contains no outline data --> + + </glyf> + + <name> + <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True"> + VarFont + </namerecord> + <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Regular + </namerecord> + <namerecord nameID="3" platformID="1" platEncID="0" langID="0x0" unicode="True"> + VarFont Regular: 2017 + </namerecord> + <namerecord nameID="4" platformID="1" platEncID="0" langID="0x0" unicode="True"> + VarFont Regular + </namerecord> + <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True"> + VarFont-Regular + </namerecord> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + VarFont + </namerecord> + <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409"> + Regular + </namerecord> + <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409"> + VarFont Regular: 2017 + </namerecord> + <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409"> + VarFont Regular + </namerecord> + <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409"> + VarFont-Regular + </namerecord> + </name> + + <post> + <formatType value="2.0"/> + <italicAngle value="0.0"/> + <underlinePosition value="-75"/> + <underlineThickness value="50"/> + <isFixedPitch value="0"/> + <minMemType42 value="0"/> + <maxMemType42 value="0"/> + <minMemType1 value="0"/> + <maxMemType1 value="0"/> + <psNames> + <!-- This file uses unique glyph names based on the information + found in the 'post' table. Since these names might not be unique, + we have to invent artificial names in case of clashes. In order to + be able to retain the original information, we need a name to + ps name mapping for those cases where they differ. That's what + you see below. + --> + </psNames> + <extraNames> + <!-- following are the name that are not taken from the standard Mac glyph order --> + <psName name="NULL"/> + </extraNames> + </post> + +</ttFont> diff --git a/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx b/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx index 1800479b..1800479b 100755..100644 --- a/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx +++ b/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx diff --git a/Tests/varLib/featureVars_test.py b/Tests/varLib/featureVars_test.py new file mode 100644 index 00000000..4db2e625 --- /dev/null +++ b/Tests/varLib/featureVars_test.py @@ -0,0 +1,123 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.varLib.featureVars import ( + overlayFeatureVariations) + + +def test_linear(n = 10): + conds = [] + for i in range(n): + end = i / n + start = end - 1. + region = [{'X': (start, end)}] + subst = {'g%.2g'%start: 'g%.2g'%end} + conds.append((region, subst)) + overlaps = overlayFeatureVariations(conds) + assert len(overlaps) == 2 * n - 1, overlaps + return conds, overlaps + +def test_quadratic(n = 10): + conds = [] + for i in range(1, n + 1): + region = [{'X': (0, i / n), + 'Y': (0, (n + 1 - i) / n)}] + subst = {str(i): str(n + 1 - i)} + conds.append((region, subst)) + overlaps = overlayFeatureVariations(conds) + assert len(overlaps) == n * (n + 1) // 2, overlaps + return conds, overlaps + +def _merge_substitutions(substitutions): + merged = {} + for subst in substitutions: + merged.update(subst) + return merged + +def _match_condition(location, overlaps): + for box, substitutions in overlaps: + for tag, coord in location.items(): + start, end = box[tag] + if start <= coord <= end: + return _merge_substitutions(substitutions) + return {} # no match + +def test_overlaps_1(): + # https://github.com/fonttools/fonttools/issues/1400 + conds = [ + ([{'abcd': (4, 9)}], {0: 0}), + ([{'abcd': (5, 10)}], {1: 1}), + ([{'abcd': (0, 8)}], {2: 2}), + ([{'abcd': (3, 7)}], {3: 3}), + ] + overlaps = overlayFeatureVariations(conds) + subst = _match_condition({'abcd': 0}, overlaps) + assert subst == {2: 2} + subst = _match_condition({'abcd': 1}, overlaps) + assert subst == {2: 2} + subst = _match_condition({'abcd': 3}, overlaps) + assert subst == {2: 2, 3: 3} + subst = _match_condition({'abcd': 4}, overlaps) + assert subst == {0: 0, 2: 2, 3: 3} + subst = _match_condition({'abcd': 5}, overlaps) + assert subst == {0: 0, 1: 1, 2: 2, 3: 3} + subst = _match_condition({'abcd': 7}, overlaps) + assert subst == {0: 0, 1: 1, 2: 2, 3: 3} + subst = _match_condition({'abcd': 8}, overlaps) + assert subst == {0: 0, 1: 1, 2: 2} + subst = _match_condition({'abcd': 9}, overlaps) + assert subst == {0: 0, 1: 1} + subst = _match_condition({'abcd': 10}, overlaps) + assert subst == {1: 1} + +def test_overlaps_2(): + # https://github.com/fonttools/fonttools/issues/1400 + conds = [ + ([{'abcd': (1, 9)}], {0: 0}), + ([{'abcd': (8, 10)}], {1: 1}), + ([{'abcd': (3, 4)}], {2: 2}), + ([{'abcd': (1, 10)}], {3: 3}), + ] + overlaps = overlayFeatureVariations(conds) + subst = _match_condition({'abcd': 0}, overlaps) + assert subst == {} + subst = _match_condition({'abcd': 1}, overlaps) + assert subst == {0: 0, 3: 3} + subst = _match_condition({'abcd': 2}, overlaps) + assert subst == {0: 0, 3: 3} + subst = _match_condition({'abcd': 3}, overlaps) + assert subst == {0: 0, 2: 2, 3: 3} + subst = _match_condition({'abcd': 5}, overlaps) + assert subst == {0: 0, 3: 3} + subst = _match_condition({'abcd': 10}, overlaps) + assert subst == {1: 1, 3: 3} + + +def run(test, n, quiet): + + print() + print("%s:" % test.__name__) + input, output = test(n) + if quiet: + print(len(output)) + else: + print() + print("Input:") + pprint(input) + print() + print("Output:") + pprint(output) + print() + +if __name__ == "__main__": + import sys + from pprint import pprint + quiet = False + n = 3 + if len(sys.argv) > 1 and sys.argv[1] == '-q': + quiet = True + del sys.argv[1] + if len(sys.argv) > 1: + n = int(sys.argv[1]) + + run(test_linear, n=n, quiet=quiet) + run(test_quadratic, n=n, quiet=quiet) diff --git a/Tests/varLib/mutator_test.py b/Tests/varLib/mutator_test.py index de794f0f..8625de3c 100644 --- a/Tests/varLib/mutator_test.py +++ b/Tests/varLib/mutator_test.py @@ -3,6 +3,7 @@ from fontTools.misc.py23 import * from fontTools.ttLib import TTFont from fontTools.varLib import build from fontTools.varLib.mutator import main as mutator +from fontTools.varLib.mutator import instantiateVariableFont as make_instance import difflib import os import shutil @@ -117,6 +118,27 @@ class MutatorTest(unittest.TestCase): expected_ttx_path = self.get_test_output(varfont_name + '.ttx') self.expect_ttx(instfont, expected_ttx_path, tables) + def test_varlib_mutator_getvar_ttf(self): + suffix = '.ttf' + ttx_dir = self.get_test_input('master_ttx_getvar_ttf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'Mutator_Getvar') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + varfont_name = 'Mutator_Getvar' + varfont_path = os.path.join(self.tempdir, varfont_name + suffix) + + args = [varfont_path, 'wdth=80', 'ASCN=628'] + mutator(args) + + instfont_path = os.path.splitext(varfont_path)[0] + '-instance' + suffix + instfont = TTFont(instfont_path) + tables = [table_tag for table_tag in instfont.keys() if table_tag != 'head'] + expected_ttx_path = self.get_test_output(varfont_name + '-instance.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + def test_varlib_mutator_iup_ttf(self): suffix = '.ttf' ufo_dir = self.get_test_input('master_ufo') @@ -139,6 +161,18 @@ class MutatorTest(unittest.TestCase): expected_ttx_path = self.get_test_output(varfont_name + '-instance.ttx') self.expect_ttx(instfont, expected_ttx_path, tables) + def test_varlib_mutator_CFF2(self): + + otf_vf_path = self.get_test_input('TestCFF2VF.otf') + expected_ttx_name = 'InterpolateTestCFF2VF' + tables = ["hmtx", "CFF2"] + loc = {'wght':float(200)} + + varfont = TTFont(otf_vf_path) + new_font = make_instance(varfont, loc) + expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') + self.expect_ttx(new_font, expected_ttx_path, tables) + if __name__ == "__main__": sys.exit(unittest.main()) diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py index 2bfe0f2d..6638ea36 100644 --- a/Tests/varLib/varLib_test.py +++ b/Tests/varLib/varLib_test.py @@ -2,14 +2,25 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.ttLib import TTFont from fontTools.varLib import build -from fontTools.varLib import main as varLib_main -from fontTools.designspaceLib import DesignSpaceDocumentError +from fontTools.varLib import main as varLib_main, load_masters +from fontTools.designspaceLib import ( + DesignSpaceDocumentError, DesignSpaceDocument, SourceDescriptor, +) import difflib import os import shutil import sys import tempfile import unittest +import pytest + + +def reload_font(font): + """(De)serialize to get final binary layout.""" + buf = BytesIO() + font.save(buf) + buf.seek(0) + return TTFont(buf) class BuildTest(unittest.TestCase): @@ -113,10 +124,7 @@ class BuildTest(unittest.TestCase): # some data (e.g. counts printed in TTX inline comments) is only # calculated at compile time, so before we can compare the TTX # dumps we need to save to a temporary stream, and realod the font - buf = BytesIO() - varfont.save(buf) - buf.seek(0) - varfont = TTFont(buf) + varfont = reload_font(varfont) expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') self.expect_ttx(varfont, expected_ttx_path, tables) @@ -160,7 +168,7 @@ class BuildTest(unittest.TestCase): avar segment will not be empty but will contain the default axis value maps: {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}. - This is to to work around an issue with some rasterizers: + This is to work around an issue with some rasterizers: https://github.com/googlei18n/fontmake/issues/295 https://github.com/fonttools/fonttools/issues/1011 """ @@ -180,7 +188,7 @@ class BuildTest(unittest.TestCase): resulting avar segment still contains the default axis value maps: {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}. - This is again to to work around an issue with some rasterizers: + This is again to work around an issue with some rasterizers: https://github.com/googlei18n/fontmake/issues/295 https://github.com/fonttools/fonttools/issues/1011 """ @@ -204,6 +212,38 @@ class BuildTest(unittest.TestCase): save_before_dump=True, ) + def test_varlib_gvar_explicit_delta(self): + """The variable font contains a composite glyph odieresis which does not + need a gvar entry, because all its deltas are 0, but it must be added + anyway to work around an issue with macOS 10.14. + + https://github.com/fonttools/fonttools/issues/1381 + """ + test_name = 'BuildGvarCompositeExplicitDelta' + self._run_varlib_build_test( + designspace_name=test_name, + font_name='TestFamily4', + tables=['gvar'], + expected_ttx_name=test_name + ) + + def test_varlib_build_CFF2(self): + ds_path = self.get_test_input('TestCFF2.designspace') + suffix = '.otf' + expected_ttx_name = 'BuildTestCFF2' + tables = ["fvar", "CFF2"] + + finder = lambda s: s.replace('.ufo', suffix) + varfont, model, _ = build(ds_path, finder) + # some data (e.g. counts printed in TTX inline comments) is only + # calculated at compile time, so before we can compare the TTX + # dumps we need to save to a temporary stream, and realod the font + varfont = reload_font(varfont) + + expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') + self.expect_ttx(varfont, expected_ttx_path, tables) + self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix) + def test_varlib_main_ttf(self): """Mostly for testing varLib.main() """ @@ -250,6 +290,44 @@ class BuildTest(unittest.TestCase): expected_ttx_path = self.get_test_output('BuildMain.ttx') self.expect_ttx(varfont, expected_ttx_path, tables) + def test_varlib_build_from_ds_object(self): + ds_path = self.get_test_input("Build.designspace") + ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") + expected_ttx_path = self.get_test_output("BuildMain.ttx") + + self.temp_dir() + for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'): + self.compile_font(path, ".ttf", self.tempdir) + + ds = DesignSpaceDocument.fromfile(ds_path) + for source in ds.sources: + filename = os.path.join( + self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf") + ) + source.font = TTFont( + filename, recalcBBoxes=False, recalcTimestamp=False, lazy=True + ) + source.filename = None # Make sure no file path gets into build() + + varfont, _, _ = build(ds) + varfont = reload_font(varfont) + tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] + self.expect_ttx(varfont, expected_ttx_path, tables) + + +def test_load_masters_layerName_without_required_font(): + ds = DesignSpaceDocument() + s = SourceDescriptor() + s.font = None + s.layerName = "Medium" + ds.addSource(s) + + with pytest.raises( + AttributeError, + match="specified a layer name but lacks the required TTFont object", + ): + load_masters(ds) + if __name__ == "__main__": sys.exit(unittest.main()) diff --git a/fonttools b/fonttools index 92b390e7..92b390e7 100755..100644 --- a/fonttools +++ b/fonttools diff --git a/post_update.sh b/post_update.sh new file mode 100755 index 00000000..7ea7d90a --- /dev/null +++ b/post_update.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# $1 Path to the new version. +# $2 Path to the old version. + +cp -a -n $2/Lib/fontTools/Android.bp $1/Lib/fontTools/ diff --git a/requirements.txt b/requirements.txt index 0e5f5bab..c9ac4f39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ # we use the official Brotli module on CPython and the CFFI-based # extension 'brotlipy' on PyPy -brotli==1.0.1; platform_python_implementation != "PyPy" +brotli==1.0.7; platform_python_implementation != "PyPy" brotlipy==0.7.0; platform_python_implementation == "PyPy" unicodedata2==11.0.0; python_version < '3.7' and platform_python_implementation != "PyPy" -scipy==1.1.0; platform_python_implementation != "PyPy" +scipy==1.2.0; platform_python_implementation != "PyPy" munkres==1.0.12; platform_python_implementation == "PyPy" -zopfli==0.1.4 -fs==2.1.1 +zopfli==0.1.6 +fs==2.1.3 diff --git a/run-tests.sh b/run-tests.sh index f10c1b01..f10c1b01 100755..100644 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.31.0 +current_version = 3.35.0 commit = True tag = False tag_name = {new_version} @@ -37,7 +37,6 @@ license_file = LICENSE minversion = 3.0 testpaths = Tests - fontTools python_files = *_test.py python_classes = @@ -47,6 +46,9 @@ addopts = --doctest-modules --doctest-ignore-import-errors --pyargs +doctest_optionflags = + ALLOW_UNICODE + ELLIPSIS filterwarnings = ignore:tostring:DeprecationWarning ignore:fromstring:DeprecationWarning @@ -39,6 +39,11 @@ extras_require = { "lxml": [ "lxml >= 4.0, < 5", "singledispatch >= 3.4.0.3; python_version < '3.4'", + # typing >= 3.6.4 is required when using ABC collections with the + # singledispatch backport, see: + # https://github.com/fonttools/fonttools/issues/1423 + # https://github.com/python/typing/issues/484 + "typing >= 3.6.4; python_version < '3.4'", ], # for fontTools.sfnt and fontTools.woff2: to compress/uncompress # WOFF 1.0 and WOFF 2.0 webfonts. @@ -58,6 +63,10 @@ extras_require = { "python_version < '3.7' and platform_python_implementation != 'PyPy'" ), ], + # for graphite type tables in ttLib/tables (Silf, Glat, Gloc) + "graphite": [ + "lz4 >= 1.7.4.2" + ], # for fontTools.interpolatable: to solve the "minimum weight perfect # matching problem in bipartite graphs" (aka Assignment problem) "interpolatable": [ @@ -65,6 +74,12 @@ extras_require = { "scipy; platform_python_implementation != 'PyPy'", "munkres; platform_python_implementation == 'PyPy'", ], + # for fontTools.varLib.plot, to visualize DesignSpaceDocument and resulting + # VariationModel + "plot": [ + # TODO: figure out the minimum version of matplotlib that we need + "matplotlib", + ], # for fontTools.misc.symfont, module for symbolic font statistics analysis "symfont": [ "sympy", @@ -337,7 +352,7 @@ def find_data_files(manpath="share/man"): setup( name="fonttools", - version="3.31.0", + version="3.35.0", description="Tools to manipulate font files", author="Just van Rossum", author_email="just@letterror.com", @@ -15,8 +15,8 @@ extras = !nolxml: lxml commands = # test with or without coverage, passing extra positonal args to pytest - cov: coverage run --parallel-mode -m pytest {posargs} - !cov: pytest {posargs} + cov: coverage run --parallel-mode -m pytest {posargs:Tests fontTools} + !cov: pytest {posargs:Tests fontTools} [testenv:htmlcov] deps = @@ -37,10 +37,17 @@ commands = coverage combine codecov --env TOXENV +[testenv:package_readme] +description = check that the long description is valid (need for PyPi) +deps = twine >= 1.12.1 + pip >= 18.0.0 +skip_install = true +extras = +commands = pip wheel -w {envtmpdir}/build --no-deps . + twine check {envtmpdir}/build/* + [testenv:bdist] deps = - pygments - docutils setuptools wheel skip_install = true @@ -50,8 +57,6 @@ install_command = whitelist_externals = rm commands = - # check metadata and rst long_description - python setup.py check --restructuredtext --strict # clean up build/ and dist/ folders python -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)' python setup.py clean --all |