From beaf0432a96bf1fb3f94647067498a375a87b36d Mon Sep 17 00:00:00 2001 From: justvanrossum Date: Thu, 1 Nov 2018 20:30:21 +0100 Subject: [PATCH 1/5] added many docstrings, and added setupHorizontalMetrics and setupVerticalMetrics methods to replace setupMetrics --- Lib/fontTools/fontBuilder.py | 157 +++++++++++++++++++++++--- Tests/fontBuilder/fontBuilder_test.py | 4 +- 2 files changed, 142 insertions(+), 19 deletions(-) diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index ba3c4eb9f..c4b5016b5 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -17,7 +17,7 @@ that works: fb.setupGlyphOrder(...) fb.setupCharacterMap(...) fb.setupGlyf(...) --or-- fb.setupCFF(...) - fb.setupMetrics("hmtx", ...) + fb.setupHorizontalMetrics(...) fb.setupHorizontalHeader() fb.setupNameTable(...) fb.setupOS2() @@ -59,7 +59,7 @@ metrics = {} glyphTable = fb.font["glyf"] for gn, advanceWidth in advanceWidths.items(): metrics[gn] = (advanceWidth, glyphTable[gn].xMin) -fb.setupMetrics("hmtx", metrics) +fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader() fb.setupNameTable(nameStrings) @@ -103,7 +103,7 @@ fb.setupCFF(nameStrings['psName'], {"FullName": nameStrings['psName']}, charStri metrics = {} for gn, advanceWidth in advanceWidths.items(): metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph -fb.setupMetrics("hmtx", metrics) +fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader() fb.setupNameTable(nameStrings) @@ -217,20 +217,37 @@ _vheaDefaults = dict( ) _nameIDs = dict( - copyright = 0, - familyName = 1, - styleName = 2, - identifier = 3, - fullName = 4, - version = 5, - psName = 6, - trademark = 7, - manufacturer = 8, - typographicFamily = 16, - typographicSubfamily = 17, -# XXX this needs to be extended with legal things, etc. + 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, @@ -312,8 +329,11 @@ class FontBuilder(object): self.font = font self.isTTF = "glyf" in font - def save(self, path): - self.font.save(path) + 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) @@ -329,15 +349,25 @@ class FontBuilder(object): 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: @@ -365,6 +395,39 @@ class FontBuilder(object): self.font["cmap"].tables = subTables def setupNameTable(self, nameStrings): + """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. + + 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 = [] @@ -378,6 +441,9 @@ class FontBuilder(object): nameTable.addMultilingualName(nameValue, ttFont=self.font, nameID=nameID) 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] @@ -426,6 +492,14 @@ class FontBuilder(object): self.font["CFF "].cff = fontSet 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") @@ -445,11 +519,31 @@ class FontBuilder(object): 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 = {} @@ -459,12 +553,25 @@ class FontBuilder(object): 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 = {} @@ -483,6 +590,9 @@ class FontBuilder(object): 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 @@ -492,6 +602,9 @@ class FontBuilder(object): 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: @@ -522,6 +635,16 @@ class FontBuilder(object): 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) diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py index e4fafef98..5f2252171 100644 --- a/Tests/fontBuilder/fontBuilder_test.py +++ b/Tests/fontBuilder/fontBuilder_test.py @@ -64,7 +64,7 @@ def test_build_ttf(tmpdir): glyphTable = fb.font["glyf"] for gn, advanceWidth in advanceWidths.items(): metrics[gn] = (advanceWidth, glyphTable[gn].xMin) - fb.setupMetrics("hmtx", metrics) + fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader() fb.setupNameTable(nameStrings) @@ -95,7 +95,7 @@ def test_build_otf(tmpdir): metrics = {} for gn, advanceWidth in advanceWidths.items(): metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph - fb.setupMetrics("hmtx", metrics) + fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader() fb.setupNameTable(nameStrings) From 6e299a1bd55c8f0c244f7aa003d538492f907a8a Mon Sep 17 00:00:00 2001 From: justvanrossum Date: Thu, 1 Nov 2018 21:33:08 +0100 Subject: [PATCH 2/5] set reasonable values for ascent and descent, test toy variation font --- Tests/fontBuilder/data/test.otf.ttx | 4 +- Tests/fontBuilder/data/test.ttf.ttx | 4 +- Tests/fontBuilder/data/test_var.ttf.ttx | 333 ++++++++++++++++++++++++ Tests/fontBuilder/fontBuilder_test.py | 81 +++++- 4 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 Tests/fontBuilder/data/test_var.ttf.ttx diff --git a/Tests/fontBuilder/data/test.otf.ttx b/Tests/fontBuilder/data/test.otf.ttx index 29eaee168..4a4434a48 100644 --- a/Tests/fontBuilder/data/test.otf.ttx +++ b/Tests/fontBuilder/data/test.otf.ttx @@ -32,8 +32,8 @@ - - + + diff --git a/Tests/fontBuilder/data/test.ttf.ttx b/Tests/fontBuilder/data/test.ttf.ttx index 17e4ce0d7..34b655e3a 100644 --- a/Tests/fontBuilder/data/test.ttf.ttx +++ b/Tests/fontBuilder/data/test.ttf.ttx @@ -32,8 +32,8 @@ - - + + diff --git a/Tests/fontBuilder/data/test_var.ttf.ttx b/Tests/fontBuilder/data/test_var.ttf.ttx new file mode 100644 index 000000000..6ebf24d53 --- /dev/null +++ b/Tests/fontBuilder/data/test_var.ttf.ttx @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Left + + + Right + + + Up + + + Down + + + HelloTestFont + + + TotallyNormal + + + HelloTestFont-TotallyNormal + + + Left + + + Right + + + Up + + + Down + + + HalloTestFont + + + TotaalNormaal + + + + + + + + + + + + + + + + + + + + + + + + + + LEFT + 0x0 + 0.0 + 0.0 + 100.0 + 256 + + + + + RGHT + 0x0 + 0.0 + 0.0 + 100.0 + 257 + + + + + UPPP + 0x0 + 0.0 + 0.0 + 100.0 + 258 + + + + + DOWN + 0x0 + 0.0 + 0.0 + 100.0 + 259 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-----BEGIN PKCS7----- +0000000100000000 +-----END PKCS7----- + + + + diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py index 5f2252171..edf28d063 100644 --- a/Tests/fontBuilder/fontBuilder_test.py +++ b/Tests/fontBuilder/fontBuilder_test.py @@ -8,6 +8,8 @@ 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 def getTestData(fileName, mode="r"): @@ -66,7 +68,7 @@ def test_build_ttf(tmpdir): metrics[gn] = (advanceWidth, glyphTable[gn].xMin) fb.setupHorizontalMetrics(metrics) - fb.setupHorizontalHeader() + fb.setupHorizontalHeader(ascent=824, descent=200) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() @@ -97,7 +99,7 @@ def test_build_otf(tmpdir): metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph fb.setupHorizontalMetrics(metrics) - fb.setupHorizontalHeader() + fb.setupHorizontalHeader(ascent=824, descent=200) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() @@ -111,3 +113,78 @@ def test_build_otf(tmpdir): testData = strip_VariableItems(f.read()) refData = strip_VariableItems(getTestData("test.otf.ttx")) assert refData == testData + + +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 = [] + 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) + + f = TTFont(outPath) + f.saveXML(outPath + ".ttx") + f.saveXML('test_var.ttf.ttx') # XXXX + with open(outPath + ".ttx") as f: + testData = strip_VariableItems(f.read()) + refData = strip_VariableItems(getTestData("test_var.ttf.ttx")) + assert refData == testData From 8298169dfbe76520f9d8deceaf894ca9038ed9fc Mon Sep 17 00:00:00 2001 From: justvanrossum Date: Thu, 1 Nov 2018 21:33:56 +0100 Subject: [PATCH 3/5] at least set these ascent/descent values in the example --- Lib/fontTools/fontBuilder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index c4b5016b5..c9d6f0e8c 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -61,7 +61,7 @@ for gn, advanceWidth in advanceWidths.items(): metrics[gn] = (advanceWidth, glyphTable[gn].xMin) fb.setupHorizontalMetrics(metrics) -fb.setupHorizontalHeader() +fb.setupHorizontalHeader(ascent=824, descent=200) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() @@ -105,7 +105,7 @@ for gn, advanceWidth in advanceWidths.items(): metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph fb.setupHorizontalMetrics(metrics) -fb.setupHorizontalHeader() +fb.setupHorizontalHeader(ascent=824, descent=200) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() From 0bfee639c65fcc719bcac08864e9426a48b4617b Mon Sep 17 00:00:00 2001 From: justvanrossum Date: Fri, 2 Nov 2018 08:02:34 +0100 Subject: [PATCH 4/5] test named instances --- Tests/fontBuilder/data/test_var.ttf.ttx | 28 +++++++++++++++++++++++++ Tests/fontBuilder/fontBuilder_test.py | 5 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Tests/fontBuilder/data/test_var.ttf.ttx b/Tests/fontBuilder/data/test_var.ttf.ttx index 6ebf24d53..a7bdcaf07 100644 --- a/Tests/fontBuilder/data/test_var.ttf.ttx +++ b/Tests/fontBuilder/data/test_var.ttf.ttx @@ -189,6 +189,12 @@ Down + + TotallyNormal + + + Right Up + HelloTestFont @@ -210,6 +216,12 @@ Down + + TotallyNormal + + + Right Up + HalloTestFont @@ -283,6 +295,22 @@ 100.0 259 + + + + + + + + + + + + + + + + diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py index edf28d063..161fdfe35 100644 --- a/Tests/fontBuilder/fontBuilder_test.py +++ b/Tests/fontBuilder/fontBuilder_test.py @@ -159,7 +159,10 @@ def test_build_var(tmpdir): ('UPPP', 0, 0, 100, "Up"), ('DOWN', 0, 0, 100, "Down"), ] - instances = [] + 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: From cc540c41e9f0c45d51c8ef5daedb10ce14e90784 Mon Sep 17 00:00:00 2001 From: justvanrossum Date: Fri, 2 Nov 2018 11:59:07 +0100 Subject: [PATCH 5/5] oops, removed leftover debug turd --- Tests/fontBuilder/fontBuilder_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py index 161fdfe35..1ce9c4038 100644 --- a/Tests/fontBuilder/fontBuilder_test.py +++ b/Tests/fontBuilder/fontBuilder_test.py @@ -186,7 +186,6 @@ def test_build_var(tmpdir): f = TTFont(outPath) f.saveXML(outPath + ".ttx") - f.saveXML('test_var.ttf.ttx') # XXXX with open(outPath + ".ttx") as f: testData = strip_VariableItems(f.read()) refData = strip_VariableItems(getTestData("test_var.ttf.ttx"))