| from __future__ import print_function, division, absolute_import |
| from __future__ import unicode_literals |
| |
| import os |
| import pytest |
| 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.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 _setupFontBuilderFvar(fb): |
| assert 'name' in fb.font, 'Must run setupNameTable() first.' |
| |
| 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) |
| |
| return fb |
| |
| |
| def _setupFontBuilderCFF2(fb): |
| assert 'fvar' in fb.font, 'Must run _setupFontBuilderFvar() first.' |
| |
| 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)}]) |
| |
| return fb |
| |
| |
| def _verifyOutput(outPath, tables=None): |
| f = TTFont(outPath) |
| f.saveXML(outPath + ".ttx", tables=tables) |
| 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.addOpenTypeFeatures("feature salt { sub A by a; } salt;") |
| 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, {}) |
| |
| lsb = {gn: cs.calcBounds(None)[0] for gn, cs in charStrings.items()} |
| metrics = {} |
| for gn, advanceWidth in advanceWidths.items(): |
| metrics[gn] = (advanceWidth, lsb[gn]) |
| fb.setupHorizontalMetrics(metrics) |
| |
| fb.setupHorizontalHeader(ascent=824, descent=200) |
| fb.setupNameTable(nameStrings) |
| fb.setupOS2() |
| fb.addOpenTypeFeatures("feature kern { pos A a -50; } kern;") |
| fb.setupPost() |
| fb.setupDummyDSIG() |
| |
| fb.save(outPath) |
| |
| _verifyOutput(outPath) |
| |
| |
| def test_build_var(tmpdir): |
| outPath = os.path.join(str(tmpdir), "test_var.ttf") |
| |
| fb, advanceWidths, nameStrings = _setupFontBuilder(True) |
| |
| 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) |
| fb = _setupFontBuilderFvar(fb) |
| fb = _setupFontBuilderCFF2(fb) |
| |
| 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) |
| |
| |
| @pytest.mark.parametrize('is_ttf, keep_glyph_names, make_cff2, post_format', [ |
| (True, True, False, 2), # TTF with post table format 2.0 |
| (True, False, False, 3), # TTF with post table format 3.0 |
| (False, True, False, 3), # CFF with post table format 3.0 |
| (False, False, False, 3), # CFF with post table format 3.0 |
| (False, True, True, 2), # CFF2 with post table format 2.0 |
| (False, False, True, 3), # CFF2 with post table format 3.0 |
| ]) |
| def test_setupPost(is_ttf, keep_glyph_names, make_cff2, post_format): |
| fb, _, nameStrings = _setupFontBuilder(is_ttf) |
| |
| if make_cff2: |
| fb.setupNameTable(nameStrings) |
| fb = _setupFontBuilderCFF2(_setupFontBuilderFvar(fb)) |
| |
| if keep_glyph_names: |
| fb.setupPost() |
| else: |
| fb.setupPost(keepGlyphNames=keep_glyph_names) |
| |
| assert fb.isTTF is is_ttf |
| assert ('CFF2' in fb.font) is make_cff2 |
| assert fb.font["post"].formatType == post_format |
| |
| |
| def test_unicodeVariationSequences(tmpdir): |
| familyName = "UVSTestFont" |
| styleName = "Regular" |
| nameStrings = dict(familyName=familyName, styleName=styleName) |
| nameStrings['psName'] = familyName + "-" + styleName |
| glyphOrder = [".notdef", "space", "zero", "zero.slash"] |
| cmap = {ord(" "): "space", ord("0"): "zero"} |
| uvs = [ |
| (0x0030, 0xFE00, "zero.slash"), |
| (0x0030, 0xFE01, None), # not an official sequence, just testing |
| ] |
| metrics = {gn: (600, 0) for gn in glyphOrder} |
| pen = TTGlyphPen(None) |
| glyph = pen.glyph() # empty placeholder |
| glyphs = {gn: glyph for gn in glyphOrder} |
| |
| fb = FontBuilder(1024, isTTF=True) |
| fb.setupGlyphOrder(glyphOrder) |
| fb.setupCharacterMap(cmap, uvs) |
| fb.setupGlyf(glyphs) |
| fb.setupHorizontalMetrics(metrics) |
| fb.setupHorizontalHeader(ascent=824, descent=200) |
| fb.setupNameTable(nameStrings) |
| fb.setupOS2() |
| fb.setupPost() |
| |
| outPath = os.path.join(str(tmpdir), "test_uvs.ttf") |
| fb.save(outPath) |
| _verifyOutput(outPath, tables=["cmap"]) |
| |
| uvs = [ |
| (0x0030, 0xFE00, "zero.slash"), |
| (0x0030, 0xFE01, "zero"), # should result in the exact same subtable data, due to cmap[0x0030] == "zero" |
| ] |
| fb.setupCharacterMap(cmap, uvs) |
| fb.save(outPath) |
| _verifyOutput(outPath, tables=["cmap"]) |