diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py index 79759acf1..0db40fcf7 100644 --- a/Lib/fontTools/__init__.py +++ b/Lib/fontTools/__init__.py @@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger log = logging.getLogger(__name__) -version = __version__ = "4.2.1.dev0" +version = __version__ = "4.2.3.dev0" __all__ = ["version", "log", "configLogger"] diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index bd6b2e50e..29d58fb6f 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -751,6 +751,20 @@ class FontBuilder(object): from .feaLib.builder import addOpenTypeFeaturesFromString addOpenTypeFeaturesFromString(self.font, features, filename=filename, tables=tables) + def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"): + """Add conditional substitutions to a Variable Font. + + See `fontTools.varLib.featureVars.addFeatureVariations`. + """ + from .varLib import featureVars + + if "fvar" not in self.font: + raise KeyError("'fvar' table is missing; can't add FeatureVariations.") + + featureVars.addFeatureVariations( + self.font, conditionalSubstitutions, featureTag=featureTag + ) + def buildCmapSubTable(cmapping, format, platformID, platEncID): subTable = cmap_classes[format](format) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 581c3948a..7205c347e 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -1307,6 +1307,9 @@ def subset_features(self, feature_indices): self.ensureDecompiled() self.SubstitutionRecord = [r for r in self.SubstitutionRecord if r.FeatureIndex in feature_indices] + # remap feature indices + for r in self.SubstitutionRecord: + r.FeatureIndex = feature_indices.index(r.FeatureIndex) self.SubstitutionCount = len(self.SubstitutionRecord) return bool(self.SubstitutionCount) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 050e8d5dc..cc22ad0a8 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1179,7 +1179,7 @@ class Glyph(object): for end in endPts: end = end + 1 contour = coordinates[start:end] - cFlags = flags[start:end] + cFlags = [flagOnCurve & f for f in flags[start:end]] start = end if 1 not in cFlags: # There is not a single on-curve point on the curve, @@ -1198,7 +1198,10 @@ class Glyph(object): while contour: nextOnCurve = cFlags.index(1) + 1 if nextOnCurve == 1: - pen.lineTo(contour[0]) + # Skip a final lineTo(), as it is implied by + # pen.closePath() + if len(contour) > 1: + pen.lineTo(contour[0]) else: pen.qCurveTo(*contour[:nextOnCurve]) contour = contour[nextOnCurve:] @@ -1230,7 +1233,7 @@ class Glyph(object): # Start with the appropriate segment type based on the final segment segmentType = "line" if cFlags[-1] == 1 else "qcurve" for i, pt in enumerate(contour): - if cFlags[i] == 1: + if cFlags[i] & flagOnCurve == 1: pen.addPoint(pt, segmentType=segmentType) segmentType = "line" else: diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py index 9a83fee40..b18ac4897 100755 --- a/Lib/fontTools/ufoLib/glifLib.py +++ b/Lib/fontTools/ufoLib/glifLib.py @@ -852,7 +852,11 @@ def validateLayerInfoVersion3Data(infoData): # ----------------- def _glifTreeFromFile(aFile): - root = etree.parse(aFile).getroot() + if etree._have_lxml: + tree = etree.parse(aFile, parser=etree.XMLParser(remove_comments=True)) + else: + tree = etree.parse(aFile) + root = tree.getroot() if root.tag != "glyph": raise GlifLibError("The GLIF is not properly formatted.") if root.text and root.text.strip() != '': @@ -862,7 +866,10 @@ def _glifTreeFromFile(aFile): def _glifTreeFromString(aString): data = tobytes(aString, encoding="utf-8") - root = etree.fromstring(data) + if etree._have_lxml: + root = etree.fromstring(data, parser=etree.XMLParser(remove_comments=True)) + else: + root = etree.fromstring(data) if root.tag != "glyph": raise GlifLibError("The GLIF is not properly formatted.") if root.text and root.text.strip() != '': diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index 66dfa3572..b428580d6 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -141,8 +141,14 @@ def _add_avar(font, axes): assert axis.maximum == max(keys) assert axis.default in keys # No duplicates - assert len(set(keys)) == len(keys) - assert len(set(vals)) == len(vals) + assert len(set(keys)) == len(keys), ( + f"{axis.tag} axis: All axis mapping input='...' " + "values must be unique, but we found duplicates." + ) + assert len(set(vals)) == len(vals), ( + f"{axis.tag} axis: All axis mapping output='...' " + "values must be unique, but we found duplicates." + ) # Ascending values assert sorted(vals) == vals diff --git a/MetaTools/buildTableList.py b/MetaTools/buildTableList.py index 9ee164e1e..825f0db1a 100755 --- a/MetaTools/buildTableList.py +++ b/MetaTools/buildTableList.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 import sys import os diff --git a/MetaTools/buildUCD.py b/MetaTools/buildUCD.py index 798ab44e7..16ae150a4 100755 --- a/MetaTools/buildUCD.py +++ b/MetaTools/buildUCD.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ Tools to parse data files from the Unicode Character Database. """ diff --git a/MetaTools/roundTrip.py b/MetaTools/roundTrip.py index 648bc9d90..d02ec4a5e 100755 --- a/MetaTools/roundTrip.py +++ b/MetaTools/roundTrip.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 """usage: ttroundtrip [options] font1 ... fontN diff --git a/NEWS.rst b/NEWS.rst index 37834877b..1a0698a4a 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,21 @@ +4.2.2 (released 2019-12-12) +--------------------------- + +- [subset] Fixed issue with subsetting FeatureVariations table when the index + of features changes as features get dropped. The feature index need to be + remapped to point to index of the remaining features (#1777, #1782). +- [fontBuilder] Added `addFeatureVariations` method to `FontBuilder` class. This + is a shorthand for calling `featureVars.addFeatureVariations` on the builder's + TTFont object (#1781). +- [glyf] Fixed the flags bug in glyph.drawPoints() like we did for glyph.draw() + (#1771, #1774). + +4.2.1 (released 2019-12-06) +--------------------------- + +- [glyf] Use the ``flagOnCurve`` bit mask in ``glyph.draw()``, so that we ignore + the ``overlap`` flag that may be set when instantiating variable fonts (#1771). + 4.2.0 (released 2019-11-28) --------------------------- diff --git a/Snippets/cmap-format.py b/Snippets/cmap-format.py index 16bd6a323..369df0009 100755 --- a/Snippets/cmap-format.py +++ b/Snippets/cmap-format.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # Sample script to convert legacy cmap subtables to format-4 # subtables. Note that this is rarely what one needs. You diff --git a/Snippets/fix-dflt-langsys.py b/Snippets/fix-dflt-langsys.py index d8eccb440..c072117a7 100644 --- a/Snippets/fix-dflt-langsys.py +++ b/Snippets/fix-dflt-langsys.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import logging diff --git a/Snippets/interpolate.py b/Snippets/interpolate.py index 5802d049d..ca4498542 100755 --- a/Snippets/interpolate.py +++ b/Snippets/interpolate.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # Illustrates how a fonttools script can construct variable fonts. # diff --git a/Snippets/layout-features.py b/Snippets/layout-features.py index fb90882b2..60ed20d33 100755 --- a/Snippets/layout-features.py +++ b/Snippets/layout-features.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 from fontTools.misc.py23 import * from fontTools.ttLib import TTFont diff --git a/Snippets/otf2ttf.py b/Snippets/otf2ttf.py index ec8dada57..a144f66e9 100755 --- a/Snippets/otf2ttf.py +++ b/Snippets/otf2ttf.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import logging diff --git a/Snippets/rename-fonts.py b/Snippets/rename-fonts.py index cf87c8d0d..0a43dc2ad 100755 --- a/Snippets/rename-fonts.py +++ b/Snippets/rename-fonts.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """Script to add a suffix to all family names in the input font's `name` table, and to optionally rename the output files with the given suffix. diff --git a/Snippets/subset-fpgm.py b/Snippets/subset-fpgm.py index 6ba92945b..e242a77c4 100755 --- a/Snippets/subset-fpgm.py +++ b/Snippets/subset-fpgm.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 from fontTools.misc.py23 import * from fontTools.ttLib import TTFont diff --git a/Snippets/svg2glif.py b/Snippets/svg2glif.py index d50149c16..22fcc7d1f 100755 --- a/Snippets/svg2glif.py +++ b/Snippets/svg2glif.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ Convert SVG paths to UFO glyphs. """ -__requires__ = ["FontTools", "ufoLib"] +__requires__ = ["fontTools"] from fontTools.misc.py23 import SimpleNamespace from fontTools.svgLib import SVGPath diff --git a/Tests/fontBuilder/data/test_var.ttf.ttx b/Tests/fontBuilder/data/test_var.ttf.ttx index 382d29e10..bc1aae250 100644 --- a/Tests/fontBuilder/data/test_var.ttf.ttx +++ b/Tests/fontBuilder/data/test_var.ttf.ttx @@ -1,5 +1,5 @@ - + @@ -19,7 +19,7 @@ - + @@ -36,7 +36,7 @@ - + @@ -126,7 +126,7 @@ - + @@ -164,12 +164,12 @@ - + - - - - + + + + @@ -269,6 +269,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py index e13f11616..8c5127626 100644 --- a/Tests/fontBuilder/fontBuilder_test.py +++ b/Tests/fontBuilder/fontBuilder_test.py @@ -164,13 +164,20 @@ def test_build_var(tmpdir): pen.lineTo((500, 400)) pen.lineTo((500, 000)) pen.closePath() + glyph1 = pen.glyph() - glyph = pen.glyph() + pen = TTGlyphPen(None) + pen.moveTo((50, 0)) + pen.lineTo((50, 200)) + pen.lineTo((250, 200)) + pen.lineTo((250, 0)) + pen.closePath() + glyph2 = pen.glyph() pen = TTGlyphPen(None) emptyGlyph = pen.glyph() - glyphs = {".notdef": emptyGlyph, "A": glyph, "a": glyph, ".null": emptyGlyph} + glyphs = {".notdef": emptyGlyph, "A": glyph1, "a": glyph2, ".null": emptyGlyph} fb.setupGlyf(glyphs) metrics = {} glyphTable = fb.font["glyf"] @@ -206,6 +213,19 @@ def test_build_var(tmpdir): ] fb.setupGvar(variations) + fb.addFeatureVariations( + [ + ( + [ + {"LEFT": (0.8, 1), "DOWN": (0.8, 1)}, + {"RGHT": (0.8, 1), "UPPP": (0.8, 1)}, + ], + {"A": "a"} + ) + ], + featureTag="rclt", + ) + fb.setupOS2() fb.setupPost() fb.setupDummyDSIG() diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py index 42d302151..3d8c27ecf 100644 --- a/Tests/subset/subset_test.py +++ b/Tests/subset/subset_test.py @@ -1,5 +1,7 @@ +import io from fontTools.misc.py23 import * from fontTools import subset +from fontTools.fontBuilder import FontBuilder from fontTools.ttLib import TTFont, newTable from fontTools.misc.loggingTools import CapturingLogHandler import difflib @@ -728,5 +730,43 @@ class SubsetTest(unittest.TestCase): self.assertEqual(ttf.flavor, None) +def test_subset_feature_variations(): + fb = FontBuilder(unitsPerEm=100) + fb.setupGlyphOrder([".notdef", "f", "f_f", "dollar", "dollar.rvrn"]) + fb.setupCharacterMap({ord("f"): "f", ord("$"): "dollar"}) + fb.setupNameTable({"familyName": "TestFeatureVars", "styleName": "Regular"}) + fb.setupPost() + fb.setupFvar(axes=[("wght", 100, 400, 900, "Weight")], instances=[]) + fb.addOpenTypeFeatures("""\ + feature dlig { + sub f f by f_f; + } dlig; + """) + fb.addFeatureVariations( + [([{"wght": (0.20886, 1.0)}], {"dollar": "dollar.rvrn"})], + featureTag="rvrn" + ) + buf = io.BytesIO() + fb.save(buf) + buf.seek(0) + + font = TTFont(buf) + + options = subset.Options() + subsetter = subset.Subsetter(options) + subsetter.populate(unicodes=[ord("f"), ord("$")]) + subsetter.subset(font) + + featureTags = { + r.FeatureTag for r in font["GSUB"].table.FeatureList.FeatureRecord + } + # 'dlig' is discretionary so it is dropped by default + assert "dlig" not in featureTags + assert "f_f" not in font.getGlyphOrder() + # 'rvrn' is required so it is kept by default + assert "rvrn" in featureTags + assert "dollar.rvrn" in font.getGlyphOrder() + + if __name__ == "__main__": sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 583d7672d..9e2b3555e 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -2,6 +2,8 @@ from fontTools.misc.py23 import * from fontTools.misc.fixedTools import otRound from fontTools.misc.testTools import getXML, parseXML from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen +from fontTools.pens.pointPen import PointToSegmentPen from fontTools.ttLib import TTFont, newTable, TTLibError from fontTools.ttLib.tables._g_l_y_f import ( GlyphCoordinates, @@ -284,6 +286,58 @@ class glyfTableTest(unittest.TestCase): composite.compact(glyfTable) + def test_bit6_draw_to_pen_issue1771(self): + # https://github.com/fonttools/fonttools/issues/1771 + font = TTFont(sfntVersion="\x00\x01\x00\x00") + # glyph00003 contains a bit 6 flag on the first point, + # which triggered the issue + font.importXML(GLYF_TTX) + glyfTable = font['glyf'] + pen = RecordingPen() + glyfTable["glyph00003"].draw(pen, glyfTable=glyfTable) + expected = [('moveTo', ((501, 1430),)), + ('lineTo', ((683, 1430),)), + ('lineTo', ((1172, 0),)), + ('lineTo', ((983, 0),)), + ('lineTo', ((591, 1193),)), + ('lineTo', ((199, 0),)), + ('lineTo', ((12, 0),)), + ('closePath', ()), + ('moveTo', ((249, 514),)), + ('lineTo', ((935, 514),)), + ('lineTo', ((935, 352),)), + ('lineTo', ((249, 352),)), + ('closePath', ())] + self.assertEqual(pen.value, expected) + + def test_bit6_draw_to_pointpen(self): + # https://github.com/fonttools/fonttools/issues/1771 + font = TTFont(sfntVersion="\x00\x01\x00\x00") + # glyph00003 contains a bit 6 flag on the first point + # which triggered the issue + font.importXML(GLYF_TTX) + glyfTable = font['glyf'] + pen = RecordingPointPen() + glyfTable["glyph00003"].drawPoints(pen, glyfTable=glyfTable) + expected = [ + ('beginPath', (), {}), + ('addPoint', ((501, 1430), 'line', False, None), {}), + ('addPoint', ((683, 1430), 'line', False, None), {}), + ('addPoint', ((1172, 0), 'line', False, None), {}), + ('addPoint', ((983, 0), 'line', False, None), {}), + ] + self.assertEqual(pen.value[:len(expected)], expected) + + def test_draw_vs_drawpoints(self): + font = TTFont(sfntVersion="\x00\x01\x00\x00") + font.importXML(GLYF_TTX) + glyfTable = font['glyf'] + pen1 = RecordingPen() + pen2 = RecordingPen() + glyfTable["glyph00003"].draw(pen1, glyfTable) + glyfTable["glyph00003"].drawPoints(PointToSegmentPen(pen2), glyfTable) + self.assertEqual(pen1.value, pen2.value) + class GlyphComponentTest: diff --git a/Tests/ufoLib/glifLib_test.py b/Tests/ufoLib/glifLib_test.py index 06aca9729..a29c76bf4 100644 --- a/Tests/ufoLib/glifLib_test.py +++ b/Tests/ufoLib/glifLib_test.py @@ -161,3 +161,21 @@ class ReadWriteFuncTest(unittest.TestCase): def testXmlDeclaration(self): s = writeGlyphToString("a", _Glyph()) self.assertTrue(s.startswith(XML_DECLARATION % "UTF-8")) + + +def test_parse_xml_remove_comments(): + s = b""" + + + + + + + """ + + g = _Glyph() + readGlyphFromString(s, g) + + assert g.name == "A" + assert g.width == 1290 + assert g.unicodes == [0x0041] diff --git a/fonttools b/fonttools index e914367aa..90bc63f99 100755 --- a/fonttools +++ b/fonttools @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import sys import os.path diff --git a/setup.cfg b/setup.cfg index da9d3f3e8..64283d5bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.2.1.dev0 +current_version = 4.2.3.dev0 commit = True tag = False tag_name = {new_version} diff --git a/setup.py b/setup.py index fb017ed06..a188d599a 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 from __future__ import print_function import io @@ -345,7 +345,7 @@ def find_data_files(manpath="share/man"): setup( name="fonttools", - version="4.2.1.dev0", + version="4.2.3.dev0", description="Tools to manipulate font files", author="Just van Rossum", author_email="just@letterror.com",