From 9d73327f3b951647beb2447b4b1fed03c0547173 Mon Sep 17 00:00:00 2001 From: justvanrossum Date: Wed, 5 Dec 2018 13:51:42 +0100 Subject: [PATCH] Some support for CFF2 --- Lib/fontTools/fontBuilder.py | 77 +++++++ Tests/fontBuilder/data/test_var.otf.ttx | 276 ++++++++++++++++++++++++ Tests/fontBuilder/fontBuilder_test.py | 58 +++++ 3 files changed, 411 insertions(+) create mode 100644 Tests/fontBuilder/data/test_var.otf.ttx diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index c9d6f0e8c..bfde5b25e 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -491,6 +491,83 @@ class FontBuilder(object): 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 glypnName, charString in charStringsDict.items(): + charString.private = private + charString.globalSubrs = globalSubrs + charStrings[glypnName] = 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 .ttLib.tables import otTables as ot + from .varLib.builder import buildVarStore, buildVarRegionAxis, buildVarData + 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 = ot.VarRegionList() + varRegionList.RegionAxisCount = len(regions) + varRegionList.Region = [] + for regionDict in regions: + region = ot.VarRegion() + region.VarRegionAxis = [] + for tag in axisTags: + axisSupport = regionDict.get(tag, (0, 0, 0)) + region.VarRegionAxis.append(buildVarRegionAxis(axisSupport)) + region.VarRegionAxisCount = len(region.VarRegionAxis) + varRegionList.Region.append(region) + 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 diff --git a/Tests/fontBuilder/data/test_var.otf.ttx b/Tests/fontBuilder/data/test_var.otf.ttx new file mode 100644 index 000000000..ac97db685 --- /dev/null +++ b/Tests/fontBuilder/data/test_var.otf.ttx @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Axis + + + TotallyNormal + + + TotallyTested + + + HelloTestFont + + + TotallyNormal + + + HelloTestFont-TotallyNormal + + + Test Axis + + + TotallyNormal + + + TotallyTested + + + HalloTestFont + + + TotaalNormaal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + + + 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + + + 200 200 -200 -200 2 blend + rmoveto + 400 400 1 blend + hlineto + 400 400 1 blend + vlineto + -400 -400 1 blend + hlineto + + + 100 100 rmoveto + 900 vlineto + -67 67 66 -33 67 hhcurveto + 67 66 33 67 67 hvcurveto + -900 vlineto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TEST + 0x0 + 0.0 + 0.0 + 100.0 + 256 + + + + + + + + + + + + + + diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py index 1ce9c4038..ea482934f 100644 --- a/Tests/fontBuilder/fontBuilder_test.py +++ b/Tests/fontBuilder/fontBuilder_test.py @@ -10,6 +10,7 @@ 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"): @@ -190,3 +191,60 @@ def test_build_var(tmpdir): testData = strip_VariableItems(f.read()) refData = strip_VariableItems(getTestData("test_var.ttf.ttx")) assert refData == testData + + +def test_build_cff2(tmpdir): + outPath = os.path.join(str(tmpdir), "test_var.otf") + + fb = FontBuilder(1000, isTTF=False) + fb.setupGlyphOrder([".notdef", ".null", "A", "a"]) + fb.setupCharacterMap({65: "A", 97: "a"}) + + advanceWidths = {".notdef": 600, "A": 600, "a": 800, ".null": 600} + + familyName = "HelloTestFont" + styleName = "TotallyNormal" + nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"), + styleName=dict(en="TotallyNormal", nl="TotaalNormaal")) + nameStrings['psName'] = familyName + "-" + styleName + 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) + + f = TTFont(outPath) + f.saveXML(outPath + ".ttx") + with open(outPath + ".ttx") as f: + testData = strip_VariableItems(f.read()) + refData = strip_VariableItems(getTestData("test_var.otf.ttx")) + assert refData == testData