diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index ba3c4eb9f..c9d6f0e8c 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,9 +59,9 @@ 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.setupHorizontalHeader(ascent=824, descent=200) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() @@ -103,9 +103,9 @@ 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.setupHorizontalHeader(ascent=824, descent=200) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() @@ -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/data/test.otf.ttx b/Tests/fontBuilder/data/test.otf.ttx index 939b3f0e6..aa9fcbec4 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 919eeac60..b2804ccdd 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..a7bdcaf07 --- /dev/null +++ b/Tests/fontBuilder/data/test_var.ttf.ttx @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Left + + + Right + + + Up + + + Down + + + TotallyNormal + + + Right Up + + + HelloTestFont + + + TotallyNormal + + + HelloTestFont-TotallyNormal + + + Left + + + Right + + + Up + + + Down + + + TotallyNormal + + + Right Up + + + 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 e4fafef98..1ce9c4038 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"): @@ -64,9 +66,9 @@ 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.setupHorizontalHeader(ascent=824, descent=200) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() @@ -95,9 +97,9 @@ 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.setupHorizontalHeader(ascent=824, descent=200) fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() @@ -111,3 +113,80 @@ 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 = [ + 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) + + f = TTFont(outPath) + f.saveXML(outPath + ".ttx") + with open(outPath + ".ttx") as f: + testData = strip_VariableItems(f.read()) + refData = strip_VariableItems(getTestData("test_var.ttf.ttx")) + assert refData == testData