def compileDSToFont(dsPath, ttFolder): doc = DesignSpaceDocument.fromfile(dsPath) doc.findDefault() ufoPathToTTPath = getTTPaths(doc, ttFolder) for source in doc.sources: if source.layerName is None: ttPath = ufoPathToTTPath[source.path] if not os.path.exists(ttPath): raise FileNotFoundError(ttPath) source.font = TTFont(ttPath, lazy=False) assert doc.default.font is not None if "name" not in doc.default.font: doc.default.font["name"] = newTable( "name") # This is the template for the VF, and needs a name table if any(s.layerName is not None for s in doc.sources): fb = FontBuilder(unitsPerEm=doc.default.font["head"].unitsPerEm) fb.setupGlyphOrder(doc.default.font.getGlyphOrder()) fb.setupPost() # This makes sure we store the glyph names font = fb.font for source in doc.sources: if source.font is None: source.font = font ttFont, masterModel, _ = varLib.build( doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT']) # Our client needs the masterModel, so we save a pickle into the font ttFont["MPcl"] = newTable("MPcl") ttFont["MPcl"].data = pickle.dumps(masterModel) return ttFont
def test_format_14(self): subtable = self.makeSubtable(14, 0, 5, 0) subtable.cmap = {} # dummy subtable.uvsDict = { 0xFE00: [(0x0030, "zero.slash")], 0xFE01: [(0x0030, None)], } fb = FontBuilder(1024, isTTF=True) font = fb.font fb.setupGlyphOrder([".notdef", "zero.slash"]) fb.setupMaxp() fb.setupPost() cmap = table__c_m_a_p() cmap.tableVersion = 0 cmap.tables = [subtable] font["cmap"] = cmap f = io.BytesIO() font.save(f) f.seek(0) font = ttLib.TTFont(f) self.assertEqual(font["cmap"].getcmap(0, 5).uvsDict, subtable.uvsDict) f = io.StringIO(newline=None) font.saveXML(f, tables=["cmap"]) ttx = strip_VariableItems(f.getvalue()) with open(CMAP_FORMAT_14_TTX) as f: expected = strip_VariableItems(f.read()) self.assertEqual(ttx, expected) with open(CMAP_FORMAT_14_BW_COMPAT_TTX) as f: font.importXML(f) self.assertEqual(font["cmap"].getcmap(0, 5).uvsDict, subtable.uvsDict)
def build_font(srcs, metadata, filename): ascent = 880 descent = 120 upem = ascent + descent scale = upem / 360.0 transform = Transform(scale, 0, 0, -scale, 0, ascent) glyphs = collect_glyphs(srcs, transform=transform) builder = FontBuilder(1000, isTTF=False) builder.setupGlyphOrder([glyph.name for glyph in glyphs]) builder.setupCharacterMap({0: ".notdef"}) psname = metadata["psName"] builder.setupCFF(psname, {"FullName": psname}, {glyph.name: glyph.charstring for glyph in glyphs}, {}) builder.setupHorizontalMetrics( {glyph.name: glyph.get_hmetrics() for glyph in glyphs}) builder.setupHorizontalHeader(ascent=ascent, descent=-descent) builder.setupNameTable({}) builder.setupOS2() builder.setupPost() builder.setupVerticalMetrics( {glyph.name: glyph.get_vmetrics(ascent=ascent) for glyph in glyphs}) builder.setupVerticalOrigins({}, ascent) builder.setupVerticalHeader(ascent=ascent, descent=-descent) builder.save(filename)
def featureVarsTestFont(): 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) return TTFont(buf)
def empty_svg_font(): glyph_order = [".notdef"] + list(ascii_letters) pen = TTGlyphPen(glyphSet=None) pen.moveTo((0, 0)) pen.lineTo((0, 500)) pen.lineTo((500, 500)) pen.lineTo((500, 0)) pen.closePath() glyph = pen.glyph() glyphs = {g: glyph for g in glyph_order} fb = FontBuilder(unitsPerEm=1024, isTTF=True) fb.setupGlyphOrder(glyph_order) fb.setupCharacterMap({ord(c): c for c in ascii_letters}) fb.setupGlyf(glyphs) fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order}) fb.setupHorizontalHeader() fb.setupOS2() fb.setupPost() fb.setupNameTable({"familyName": "TestSVG", "styleName": "Regular"}) svg_table = newTable("SVG ") svg_table.docList = [] fb.font["SVG "] = svg_table return fb.font
def minimalTTF(): fb = FontBuilder(1024, isTTF=True) fb.updateHead(unitsPerEm=1000, created=0, modified=0) fb.setupGlyphOrder([ ".notdef", ".null", "A", "Aacute", "V", "acutecomb", "gravecomb", "A.alt" ]) fb.setupCharacterMap({ 65: "A", 192: "Aacute", 86: "V", 769: "acutecomb", 768: "gravecomb" }) advanceWidths = { ".notdef": 600, "A": 600, "Aacute": 600, "V": 600, ".null": 600, "acutecomb": 0, "gravecomb": 0, "A.alt": 600 } familyName = "HelloTestFont" styleName = "TotallyNormal" nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"), styleName=dict(en="TotallyNormal", nl="TotaalNormaal")) nameStrings['psName'] = familyName + "-" + styleName glyphs = { ".notdef": test_glyph(), ".null": test_glyph(), "A": test_glyph(), "Aacute": test_glyph(), "V": test_glyph(), "acutecomb": test_glyph(), "gravecomb": test_glyph(), "A.alt": test_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() fb.setupNameTable(nameStrings) fb.setupOS2() fb.setupPost() return fb
def compileDSToFont(dsPath, ttFolder): doc = DesignSpaceDocument.fromfile(dsPath) doc.findDefault() ufoPathToTTPath = getTTPaths(doc, ttFolder) for source in doc.sources: if source.layerName is None: ttPath = ufoPathToTTPath[source.path] if not os.path.exists(ttPath): raise FileNotFoundError(ttPath) source.font = TTFont(ttPath, lazy=False) assert doc.default.font is not None if "name" not in doc.default.font: doc.default.font["name"] = newTable( "name") # This is the template for the VF, and needs a name table if any(s.layerName is not None for s in doc.sources): fb = FontBuilder(unitsPerEm=doc.default.font["head"].unitsPerEm) fb.setupGlyphOrder(doc.default.font.getGlyphOrder()) fb.setupPost() # This makes sure we store the glyph names font = fb.font for source in doc.sources: if source.font is None: source.font = font try: ttFont, masterModel, _ = varLib.build( doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT']) except VarLibError as e: if 'GSUB' in e.args: extraExclude = ['GSUB'] elif 'GPOS' in e.args: extraExclude = ['GPOS', 'GDEF'] else: raise print(f"{e!r}", file=sys.stderr) print( f"Error while building {extraExclude[0]} table, trying again without {' and '.join(extraExclude)}.", file=sys.stderr) ttFont, masterModel, _ = varLib.build( doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT'] + extraExclude) # Our client needs the masterModel, so we save a pickle into the font ttFont["MPcl"] = newTable("MPcl") ttFont["MPcl"].data = pickle.dumps(masterModel) return ttFont
def compileUFOToFont(ufoPath): """Compile the source UFO to a TTF with the smallest amount of tables needed to let HarfBuzz do its work. That would be 'cmap', 'post' and whatever OTL tables are needed for the features. Return the compiled font data. This function may do some redundant work (eg. we need an UFOReader elsewhere, too), but having a picklable argument and return value allows us to run it in a separate process, enabling parallelism. """ reader = UFOReader(ufoPath, validate=False) glyphSet = reader.getGlyphSet() info = SimpleNamespace() reader.readInfo(info) glyphOrder = sorted(glyphSet.keys()) # no need for the "real" glyph order if ".notdef" not in glyphOrder: # We need a .notdef glyph, so let's make one. glyphOrder.insert(0, ".notdef") cmap, revCmap, anchors = fetchCharacterMappingAndAnchors(glyphSet, ufoPath) fb = FontBuilder(round(info.unitsPerEm)) fb.setupGlyphOrder(glyphOrder) fb.setupCharacterMap(cmap) fb.setupPost() # This makes sure we store the glyph names ttFont = fb.font # Store anchors in the font as a private table: this is valuable # data that our parent process can use to do faster reloading upon # changes. ttFont["FGAx"] = newTable("FGAx") ttFont["FGAx"].data = pickle.dumps(anchors) ufo = MinimalFontObject(ufoPath, reader, revCmap, anchors) feaComp = FeatureCompiler(ufo, ttFont) try: feaComp.compile() except FeatureLibError as e: error = f"{e.__class__.__name__}: {e}" except Exception: # This is most likely a bug, and not an input error, so perhaps # we shouldn't even catch it here. error = traceback.format_exc() else: error = None return ttFont, error
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()
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"])
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"])
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) _verifyOutput(outPath)
def build(instance, opts, glyphOrder): font = instance.parent master = font.masters[0] advanceWidths = {} characterMap = {} charStrings = {} colorLayers = {} for name in glyphOrder: glyph = font.glyphs[name] if not glyph.export: continue for layer in glyph.layers: if "colorPalette" in layer.attributes: index = layer.attributes["colorPalette"] if name not in colorLayers: colorLayers[name] = [] if layer.layerId == layer.associatedMasterId: # master layer colorLayers[name].append((name, int(index))) else: assert False, "can’t handle non-master color layers" if glyph.unicode: characterMap[int(glyph.unicode, 16)] = name layer = getLayer(glyph, instance) charStrings[name] = draw(layer, instance).getCharString() advanceWidths[name] = layer.width # XXX glyphOrder.pop(glyphOrder.index(".notdef")) glyphOrder.pop(glyphOrder.index("space")) glyphOrder.insert(0, ".notdef") glyphOrder.insert(1, "space") version = float(opts.version) vendor = get_property(font, "vendorID") names = { "copyright": font.copyright, "familyName": instance.familyName, "styleName": instance.name, "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}", "fullName": instance.fullName, "version": f"Version {version:.03f}", "psName": instance.fontName, "manufacturer": font.manufacturer, "designer": font.designer, "vendorURL": font.manufacturerURL, "designerURL": font.designerURL, "licenseDescription": get_property(font, "licenses"), "licenseInfoURL": get_property(font, "licenseURL"), "sampleText": get_property(font, "sampleTexts"), } fb = FontBuilder(font.upm, isTTF=False) date = font.date.replace(tzinfo=datetime.timezone.utc) stat = opts.glyphs.stat() fb.updateHead( fontRevision=version, created=int(date.timestamp()) - mac_epoch_diff, modified=int(stat.st_mtime) - mac_epoch_diff, ) fb.setupGlyphOrder(glyphOrder) fb.setupCharacterMap(characterMap) fb.setupNameTable(names, mac=False) fb.setupHorizontalHeader( ascent=master.ascender, descent=master.descender, lineGap=master.customParameters["hheaLineGap"], ) if opts.debug: fb.setupCFF(names["psName"], {}, charStrings, {}) fb.font["CFF "].compile(fb.font) else: fb.setupCFF2(charStrings) metrics = {} for name, width in advanceWidths.items(): bounds = charStrings[name].calcBounds(None) or [0] metrics[name] = (width, bounds[0]) fb.setupHorizontalMetrics(metrics) fb.setupPost( underlinePosition=master.customParameters["underlinePosition"], underlineThickness=master.customParameters["underlineThickness"], ) # Compile to get font bbox fb.font["head"].compile(fb.font) codePages = [ CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"] ] fb.setupOS2( version=4, sTypoAscender=master.ascender, sTypoDescender=master.descender, sTypoLineGap=master.customParameters["typoLineGap"], usWinAscent=fb.font["head"].yMax, usWinDescent=-fb.font["head"].yMin, sxHeight=master.xHeight, sCapHeight=master.capHeight, achVendID=vendor, fsType=calcBits(font.customParameters["fsType"], 0, 16), fsSelection=calcFsSelection(instance), ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0, 32), ulCodePageRange1=calcBits(codePages, 0, 32), ) fea = makeFeatures(instance, master, opts, glyphOrder) fb.addOpenTypeFeatures(fea) palettes = master.customParameters["Color Palettes"] palettes = [[tuple(v / 255 for v in c) for c in p] for p in palettes] fb.setupCPAL(palettes) fb.setupCOLR(colorLayers) instance.font = fb.font if opts.debug: fb.font.save(f"{instance.fontName}.otf") return fb.font
def build(instance, opts): font = instance.parent source = font.masters[0] fea, marks = makeFeatures(instance, source) glyphOrder = [] advanceWidths = {} characterMap = {} charStrings = {} source.blueValues = [] source.otherBlues = [] for zone in sorted(source.alignmentZones): pos = zone.position size = zone.size vals = sorted((pos, pos + size)) if pos == 0 or size >= 0: source.blueValues.extend(vals) else: source.otherBlues.extend(vals) fontinfo = f""" FontName {instance.fontName} OrigEmSqUnits {font.upm} DominantV {source.verticalStems} DominantH {source.horizontalStems} BaselineOvershoot {source.blueValues[0]} BaselineYCoord {source.blueValues[1]} LcHeight {source.blueValues[2]} LcOvershoot {source.blueValues[3] - source.blueValues[2]} CapHeight {source.blueValues[4]} CapOvershoot {source.blueValues[5] - source.blueValues[4]} AscenderHeight {source.blueValues[6]} AscenderOvershoot {source.blueValues[7] - source.blueValues[6]} Baseline5 {source.otherBlues[1]} Baseline5Overshoot {source.otherBlues[0] - source.otherBlues[1]} FlexOK true BlueFuzz 1 """ layerSet = {g.name: g.layers[source.id] for g in font.glyphs} for glyph in font.glyphs: if not glyph.export: continue name = glyph.name glyphOrder.append(name) for code in glyph.unicodes: characterMap[int(code, 16)] = name layer = glyph.layers[source.id] width = 0 if name in marks else layer.width # Draw glyph and remove overlaps. path = Path() layer.draw(DecomposePathPen(path, layerSet=layerSet)) path.simplify(fix_winding=True, keep_starting_points=True) # Autohint. pen = BezPen(None, True) path.draw(pen) bez = "\n".join(["% " + name, "sc", *pen.bez, "ed", ""]) hinted = hint_bez_glyph(fontinfo, bez) program = [width] + convertBezToT2(hinted) # Build CharString. charStrings[name] = T2CharString(program=program) advanceWidths[name] = width # Make sure .notdef is glyph index 0. glyphOrder.pop(glyphOrder.index(".notdef")) glyphOrder.insert(0, ".notdef") version = float(opts.version) vendor = font.customParameters["vendorID"] names = { "copyright": font.copyright, "familyName": instance.familyName, "styleName": instance.name, "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}", "fullName": instance.fullName, "version": f"Version {version:.03f}", "psName": instance.fontName, "manufacturer": font.manufacturer, "designer": font.designer, "description": font.customParameters["description"], "vendorURL": font.manufacturerURL, "designerURL": font.designerURL, "licenseDescription": font.customParameters["license"], "licenseInfoURL": font.customParameters["licenseURL"], "sampleText": font.customParameters["sampleText"], } date = int(font.date.timestamp()) - epoch_diff fb = FontBuilder(font.upm, isTTF=False) fb.updateHead(fontRevision=version, created=date, modified=date) fb.setupGlyphOrder(glyphOrder) fb.setupCharacterMap(characterMap) fb.setupNameTable(names, mac=False) fb.setupHorizontalHeader( ascent=source.ascender, descent=source.descender, lineGap=source.customParameters["typoLineGap"], ) privateDict = { "BlueValues": source.blueValues, "OtherBlues": source.otherBlues, "StemSnapH": source.horizontalStems, "StemSnapV": source.verticalStems, "StdHW": source.horizontalStems[0], "StdVW": source.verticalStems[0], } fontInfo = { "FullName": names["fullName"], "Notice": names["copyright"].replace("©", "\(c\)"), "version": f"{version:07.03f}", "Weight": instance.name, } fb.setupCFF(names["psName"], fontInfo, charStrings, privateDict) metrics = {} for i, (name, width) in enumerate(advanceWidths.items()): bounds = charStrings[name].calcBounds(None) or [0] metrics[name] = (width, bounds[0]) fb.setupHorizontalMetrics(metrics) codePages = [ CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"] ] fb.setupOS2( version=4, sTypoAscender=source.ascender, sTypoDescender=source.descender, sTypoLineGap=source.customParameters["typoLineGap"], usWinAscent=source.ascender, usWinDescent=-source.descender, sxHeight=source.xHeight, sCapHeight=source.capHeight, achVendID=vendor, fsType=calcBits(font.customParameters["fsType"], 0, 16), fsSelection=calcFsSelection(instance), ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0, 32), ulCodePageRange1=calcBits(codePages, 0, 32), ) ut = int(source.customParameters["underlineThickness"]) up = int(source.customParameters["underlinePosition"]) fb.setupPost(underlineThickness=ut, underlinePosition=up + ut // 2) meta = newTable("meta") meta.data = {"dlng": "Arab", "slng": "Arab"} fb.font["meta"] = meta fb.addOpenTypeFeatures(fea) return fb.font
def _save_ttfont(bbf): fb = FontBuilder(bbf.info.unitsPerEm, isTTF=True) fb.setupGlyphOrder(bbf.lib.glyphOrder) bbf._build_maps() fb.setupCharacterMap(bbf._unicodemap) glyf = {} metrics = {} for i in [1, 2]: for g in bbf: if i == 1 and g.components: continue if i == 2 and not g.components: continue pen = Cu2QuPen(TTGlyphPen(glyf), bbf.info.unitsPerEm / 1000) try: g.draw(pen) glyf[g.name] = pen.pen.glyph() except Exception as e: pen = Cu2QuPen(TTGlyphPen(glyf), bbf.info.unitsPerEm / 1000) glyf[g.name] = pen.pen.glyph() pass bounds = g.bounds xMin = 0 if bounds: xMin = bounds[0] metrics[g.name] = (g.width, xMin) versionMajor = bbf.info.versionMajor or 1 versionMinor = bbf.info.versionMinor or 0 fb.updateHead( fontRevision=versionMajor + versionMinor / 10**len(str(versionMinor)), created=_ufo_date_to_opentime(bbf.info.openTypeHeadCreated), lowestRecPPEM=bbf.info.openTypeHeadLowestRecPPEM or fb.font["head"].lowestRecPPEM, ) fb.setupGlyf(glyf) fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader( ascent=int(bbf.info.openTypeHheaAscender or bbf.info.ascender or 0), descent=int(bbf.info.openTypeHheaDescender or bbf.info.descender or 0), ) os2 = { "sTypoAscender": bbf.info.openTypeOS2TypoAscender or bbf.info.ascender or 0, "sTypoDescender": bbf.info.openTypeOS2TypoDescender or bbf.info.descender or 0, "sxHeight": bbf.info.xHeight, "sCapHeight": bbf.info.capHeight, } for k in [ "usWidthClass", "usWeightClass", "sTypoLineGap", "usWinAscent", "usWinDescent", "ySubscriptXSize", "ySubscriptYSize", "ySubscriptXOffset", "ySubscriptYOffset", "ySuperscriptXSize", "ySuperscriptYSize", "ySuperscriptXOffset", "ySuperscriptYOffset", "yStrikeoutSize", "yStrikeoutPosition", ]: infokey = k while not infokey[0].isupper(): infokey = infokey[1:] infokey = "openTypeOS2" + infokey if hasattr(bbf.info, infokey): if getattr(bbf.info, infokey): os2[k] = getattr(bbf.info, infokey) fb.setupOS2(**os2) # Name tables nameStrings = dict( familyName=dict(en=bbf.info.familyName), styleName=dict(en=bbf.info.styleName), fullName=f"{bbf.info.familyName} {bbf.info.styleName}", psName=bbf.info.postscriptFontName, version="Version " + str(bbf.info.versionMajor + bbf.info.versionMinor / 10**len(str(bbf.info.versionMinor))), ) if not nameStrings["psName"]: del nameStrings["psName"] fb.setupNameTable(nameStrings) # Kerning # Features fb.setupPost( underlinePosition=(bbf.info.postscriptUnderlinePosition or 0), underlineThickness=(bbf.info.postscriptUnderlineThickness or 0), ) return fb.font
def colrv1_path(tmp_path): base_glyph_names = ["uni%04X" % i for i in range(0xE000, 0xE000 + 10)] layer_glyph_names = ["glyph%05d" % i for i in range(10, 20)] glyph_order = [".notdef"] + base_glyph_names + layer_glyph_names pen = TTGlyphPen(glyphSet=None) pen.moveTo((0, 0)) pen.lineTo((0, 500)) pen.lineTo((500, 500)) pen.lineTo((500, 0)) pen.closePath() glyph = pen.glyph() glyphs = {g: glyph for g in glyph_order} fb = FontBuilder(unitsPerEm=1024, isTTF=True) fb.setupGlyphOrder(glyph_order) fb.setupCharacterMap( {int(name[3:], 16): name for name in base_glyph_names}) fb.setupGlyf(glyphs) fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order}) fb.setupHorizontalHeader() fb.setupOS2() fb.setupPost() fb.setupNameTable({"familyName": "TestCOLRv1", "styleName": "Regular"}) fb.setupCOLR( { "uniE000": ( ot.PaintFormat.PaintColrLayers, [ { "Format": ot.PaintFormat.PaintGlyph, "Paint": (ot.PaintFormat.PaintSolid, 0), "Glyph": "glyph00010", }, { "Format": ot.PaintFormat.PaintGlyph, "Paint": (ot.PaintFormat.PaintSolid, (2, 0.3)), "Glyph": "glyph00011", }, ], ), "uniE001": ( ot.PaintFormat.PaintColrLayers, [ { "Format": ot.PaintFormat.PaintTransform, "Paint": { "Format": ot.PaintFormat.PaintGlyph, "Paint": { "Format": ot.PaintFormat.PaintRadialGradient, "x0": 250, "y0": 250, "r0": 250, "x1": 200, "y1": 200, "r1": 0, "ColorLine": { "ColorStop": [(0.0, 1), (1.0, 2)], "Extend": "repeat", }, }, "Glyph": "glyph00012", }, "Transform": (0.7071, 0.7071, -0.7071, 0.7071, 0, 0), }, { "Format": ot.PaintFormat.PaintGlyph, "Paint": (ot.PaintFormat.PaintSolid, (1, 0.5)), "Glyph": "glyph00013", }, ], ), "uniE002": ( ot.PaintFormat.PaintColrLayers, [ { "Format": ot.PaintFormat.PaintGlyph, "Paint": { "Format": ot.PaintFormat.PaintLinearGradient, "x0": 0, "y0": 0, "x1": 500, "y1": 500, "x2": -500, "y2": 500, "ColorLine": { "ColorStop": [(0.0, 1), (1.0, 2)] }, }, "Glyph": "glyph00014", }, { "Format": ot.PaintFormat.PaintTransform, "Paint": { "Format": ot.PaintFormat.PaintGlyph, "Paint": (ot.PaintFormat.PaintSolid, 1), "Glyph": "glyph00015", }, "Transform": (1, 0, 0, 1, 400, 400), }, ], ), "uniE003": { "Format": ot.PaintFormat.PaintRotate, "Paint": { "Format": ot.PaintFormat.PaintColrGlyph, "Glyph": "uniE001", }, "angle": 45, "centerX": 250, "centerY": 250, }, "uniE004": [ ("glyph00016", 1), ("glyph00017", 2), ], }, ) fb.setupCPAL( [ [ (1.0, 0.0, 0.0, 1.0), # red (0.0, 1.0, 0.0, 1.0), # green (0.0, 0.0, 1.0, 1.0), # blue ], ], ) output_path = tmp_path / "TestCOLRv1.ttf" fb.save(output_path) return output_path
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) _verifyOutput(outPath)
def make_font(file_paths, out_dir, revision, gsub_path, gpos_path, uvs_lst): cmap, gorder, validated_fpaths = {}, deque(), [] # build glyph order for fpath in file_paths: # derive glyph name from file name gname = os.path.splitext(os.path.basename(fpath))[0] # trim extension # validate glyph name if not glyph_name_is_valid(gname, fpath): continue # skip any duplicates and 'space' if gname in gorder or gname == 'space': log.warning("Skipped file '{}'. The glyph name derived from it " "is either a duplicate or 'space'.".format(fpath)) continue # limit the length of glyph name to 31 chars if len(gname) > 31: num = 0 trimmed_gname = get_trimmed_glyph_name(gname, num) while trimmed_gname in gorder: num += 1 trimmed_gname = get_trimmed_glyph_name(trimmed_gname, num) gorder.append(trimmed_gname) log.warning("Glyph name '{}' was trimmed to 31 characters: " "'{}'".format(gname, trimmed_gname)) else: gorder.append(gname) validated_fpaths.append(fpath) # add to cmap if RE_UNICODE.match(gname): uni_int = int(gname[1:], 16) # trim leading 'u' cmap[uni_int] = gname fb = FontBuilder(UPM, isTTF=False) fb.font['head'].fontRevision = float(revision) fb.font['head'].lowestRecPPEM = 12 cs_dict = {} cs_cache = {} for i, svg_file_path in enumerate(validated_fpaths): svg_file_realpath = os.path.realpath(svg_file_path) if svg_file_realpath not in cs_cache: svg_size = get_svg_size(svg_file_realpath) if svg_size is None: cs_dict[gorder[i]] = SPACE_CHARSTRING continue pen = T2CharStringPen(EMOJI_H_ADV, None) svg = SVGPath(svg_file_realpath, transform=(EMOJI_SIZE / svg_size, 0, 0, -EMOJI_SIZE / svg_size, (EMOJI_H_ADV * .5) - (EMOJI_SIZE * .5), EMOJI_H_ADV * ABOVE_BASELINE)) svg.draw(pen) cs = pen.getCharString() cs_cache[svg_file_realpath] = cs else: cs = cs_cache.get(svg_file_realpath) cs_dict[gorder[i]] = cs # add '.notdef', 'space' and zero-width joiner pen = T2CharStringPen(EMOJI_H_ADV, None) draw_notdef(pen) gorder.extendleft(reversed(['.notdef', 'space', 'ZWJ'])) cs_dict.update({ '.notdef': pen.getCharString(), 'space': SPACE_CHARSTRING, 'ZWJ': SPACE_CHARSTRING, }) cmap.update({ 32: 'space', # U+0020 160: 'space', # U+00A0 8205: 'ZWJ', # U+200D }) # add TAG LATIN LETTER glyphs and mappings for cdpt in TAG_LAT_LETTR: tag_gname = f'u{cdpt}' gorder.append(tag_gname) cs_dict[tag_gname] = SPACE_CHARSTRING cmap[int(cdpt, 16)] = tag_gname fb.setupGlyphOrder(list(gorder)) # parts of FontTools require a list fb.setupCharacterMap(cmap, uvs=uvs_lst) fb.setupCFF( PS_NAME, { 'version': revision, 'Notice': TRADEMARK, 'Copyright': COPYRIGHT, 'FullName': FULL_NAME, 'FamilyName': FAMILY_NAME, 'Weight': STYLE_NAME }, cs_dict, {}) glyphs_bearings = {} for gname, cs in cs_dict.items(): gbbox = cs.calcBounds(None) if gbbox: xmin, ymin, _, ymax = gbbox if ymax > ASCENT: log.warning("Top of glyph '{}' may get clipped. " "Glyph's ymax={}; Font's ascent={}".format( gname, ymax, ASCENT)) if ymin < DESCENT: log.warning("Bottom of glyph '{}' may get clipped. " "Glyph's ymin={}; Font's descent={}".format( gname, ymin, DESCENT)) lsb = xmin tsb = EMOJI_V_ADV - ymax - EMOJI_H_ADV * (1 - ABOVE_BASELINE) glyphs_bearings[gname] = (lsb, tsb) else: glyphs_bearings[gname] = (0, 0) h_metrics = {} v_metrics = {} for gname in gorder: h_metrics[gname] = (EMOJI_H_ADV, glyphs_bearings[gname][0]) v_metrics[gname] = (EMOJI_V_ADV, glyphs_bearings[gname][1]) fb.setupHorizontalMetrics(h_metrics) fb.setupVerticalMetrics(v_metrics) fb.setupHorizontalHeader(ascent=ASCENT, descent=DESCENT) v_ascent = EMOJI_H_ADV // 2 v_descent = EMOJI_H_ADV - v_ascent fb.setupVerticalHeader(ascent=v_ascent, descent=-v_descent, caretSlopeRun=1) VERSION_STRING = 'Version {};{}'.format(revision, VENDOR) UNIQUE_ID = '{};{};{}'.format(revision, VENDOR, PS_NAME) name_strings = dict( copyright=COPYRIGHT, # ID 0 familyName=FAMILY_NAME, # ID 1 styleName=STYLE_NAME, # ID 2 uniqueFontIdentifier=UNIQUE_ID, # ID 3 fullName=FULL_NAME, # ID 4 version=VERSION_STRING, # ID 5 psName=PS_NAME, # ID 6 trademark=TRADEMARK, # ID 7 manufacturer=MANUFACTURER, # ID 8 designer=DESIGNER, # ID 9 vendorURL=VENDOR_URL, # ID 11 designerURL=DESIGNER_URL, # ID 12 licenseDescription=LICENSE, # ID 13 licenseInfoURL=LICENSE_URL, # ID 14 ) fb.setupNameTable(name_strings, mac=False) fb.setupOS2( fsType=FSTYPE, achVendID=VENDOR, fsSelection=0x0040, # REGULAR usWinAscent=ASCENT, usWinDescent=-DESCENT, sTypoAscender=ASCENT, sTypoDescender=DESCENT, sCapHeight=ASCENT, ulCodePageRange1=(1 << 1)) # set 1st CP bit if gsub_path: addOpenTypeFeatures(fb.font, gsub_path, tables=['GSUB']) if gpos_path: addOpenTypeFeatures(fb.font, gpos_path, tables=['GPOS']) fb.setupPost(isFixedPitch=1, underlinePosition=UNDERLINE_POSITION, underlineThickness=UNDERLINE_THICKNESS) fb.setupDummyDSIG() fb.save(os.path.join(out_dir, '{}.otf'.format(PS_NAME)))
def build(instance, isTTF, version): font = instance.parent source = font.masters[0] fea, marks = makeFeatures(instance, source) source.blueValues = [] source.otherBlues = [] for zone in sorted(source.alignmentZones): pos = zone.position size = zone.size vals = sorted((pos, pos + size)) if pos == 0 or size >= 0: source.blueValues.extend(vals) else: source.otherBlues.extend(vals) fontinfo = f""" FontName {instance.fontName} OrigEmSqUnits {font.upm} DominantV {source.verticalStems} DominantH {source.horizontalStems} BaselineOvershoot {source.blueValues[0]} BaselineYCoord {source.blueValues[1]} LcHeight {source.blueValues[2]} LcOvershoot {source.blueValues[3] - source.blueValues[2]} CapHeight {source.blueValues[4]} CapOvershoot {source.blueValues[5] - source.blueValues[4]} AscenderHeight {source.blueValues[6]} AscenderOvershoot {source.blueValues[7] - source.blueValues[6]} Baseline5 {source.otherBlues[1]} Baseline5Overshoot {source.otherBlues[0] - source.otherBlues[1]} FlexOK true BlueFuzz 1 """ characterMap = {} glyphs = {} metrics = {} layerSet = {g.name: g.layers[source.id] for g in font.glyphs} if isTTF: from fontTools.pens.cu2quPen import Cu2QuPen from fontTools.pens.recordingPen import RecordingPen for glyph in font.glyphs: layer = glyph.layers[source.id] pen = RecordingPen() layer.draw(pen) layer.paths = [] layer.components = [] pen.replay(Cu2QuPen(layer.getPen(), 1.0, reverse_direction=True)) for glyph in font.glyphs: if not glyph.export and not isTTF: continue name = glyph.name for code in glyph.unicodes: characterMap[int(code, 16)] = name layer = glyph.layers[source.id] width = 0 if name in marks else layer.width pen = BoundsPen(layerSet) layer.draw(pen) metrics[name] = (width, pen.bounds[0] if pen.bounds else 0) if isTTF: from fontTools.pens.ttGlyphPen import TTGlyphPen pen = TTGlyphPen(layerSet) if layer.paths: # Decompose and remove overlaps. path = Path() layer.draw(DecomposePathPen(path, layerSet)) path.simplify(fix_winding=True, keep_starting_points=True) path.draw(pen) else: # Composite-only glyph, no need to decompose. layer.draw(FlattenComponentsPen(pen, layerSet)) glyphs[name] = pen.glyph() else: from fontTools.pens.t2CharStringPen import T2CharStringPen # Draw glyph and remove overlaps. path = Path() layer.draw(DecomposePathPen(path, layerSet)) path.simplify(fix_winding=True, keep_starting_points=True) # Build CharString. pen = T2CharStringPen(width, None) path.draw(pen) glyphs[name] = pen.getCharString() vendor = font.customParameters["vendorID"] names = { "copyright": font.copyright, "familyName": instance.familyName, "styleName": instance.name, "uniqueFontIdentifier": f"{version};{vendor};{instance.fontName}", "fullName": instance.fullName, "version": f"Version {version}", "psName": instance.fontName, "manufacturer": font.manufacturer, "designer": font.designer, "description": font.customParameters["description"], "vendorURL": font.manufacturerURL, "designerURL": font.designerURL, "licenseDescription": font.customParameters["license"], "licenseInfoURL": font.customParameters["licenseURL"], "sampleText": font.customParameters["sampleText"], } date = int(font.date.timestamp()) - epoch_diff fb = FontBuilder(font.upm, isTTF=isTTF) fb.updateHead(fontRevision=version, created=date, modified=date) fb.setupGlyphOrder(font.glyphOrder) fb.setupCharacterMap(characterMap) fb.setupNameTable(names, mac=False) fb.setupHorizontalHeader( ascent=source.ascender, descent=source.descender, lineGap=source.customParameters["typoLineGap"], ) if isTTF: fb.setupGlyf(glyphs) else: privateDict = { "BlueValues": source.blueValues, "OtherBlues": source.otherBlues, "StemSnapH": source.horizontalStems, "StemSnapV": source.verticalStems, "StdHW": source.horizontalStems[0], "StdVW": source.verticalStems[0], } fontInfo = { "FullName": names["fullName"], "Notice": names["copyright"].replace("©", "\(c\)"), "version": f"{version}", "Weight": instance.name, } fb.setupCFF(names["psName"], fontInfo, glyphs, privateDict) fb.setupHorizontalMetrics(metrics) codePages = [CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]] fb.setupOS2( version=4, sTypoAscender=source.ascender, sTypoDescender=source.descender, sTypoLineGap=source.customParameters["typoLineGap"], usWinAscent=source.ascender, usWinDescent=-source.descender, sxHeight=source.xHeight, sCapHeight=source.capHeight, achVendID=vendor, fsType=calcBits(font.customParameters["fsType"], 0, 16), fsSelection=calcFsSelection(instance), ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0, 32), ulCodePageRange1=calcBits(codePages, 0, 32), ) underlineThickness = int(source.customParameters["underlineThickness"]) underlinePosition = int(source.customParameters["underlinePosition"]) fb.setupPost( keepGlyphNames=False, underlineThickness=underlineThickness, underlinePosition=underlinePosition + underlineThickness // 2, ) fb.font["meta"] = meta = newTable("meta") meta.data = {"dlng": "Arab", "slng": "Arab"} fb.addOpenTypeFeatures(fea) if isTTF: from fontTools.ttLib.tables import ttProgram fb.setupDummyDSIG() fb.font["gasp"] = gasp = newTable("gasp") gasp.gaspRange = {0xFFFF: 15} fb.font["prep"] = prep = newTable("prep") prep.program = ttProgram.Program() assembly = ["PUSHW[]", "511", "SCANCTRL[]", "PUSHB[]", "4", "SCANTYPE[]"] prep.program.fromAssembly(assembly) else: from cffsubr import subroutinize subroutinize(fb.font) return fb.font
def test_subset_single_pos_format(): fb = FontBuilder(unitsPerEm=1000) fb.setupGlyphOrder([".notdef", "a", "b", "c"]) fb.setupCharacterMap({ord("a"): "a", ord("b"): "b", ord("c"): "c"}) fb.setupNameTable({ "familyName": "TestSingePosFormat", "styleName": "Regular" }) fb.setupPost() fb.addOpenTypeFeatures(""" feature kern { pos a -50; pos b -40; pos c -50; } kern; """) buf = io.BytesIO() fb.save(buf) buf.seek(0) font = TTFont(buf) # The input font has a SinglePos Format 2 subtable where each glyph has # different ValueRecords assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [ '<Lookup>', ' <LookupType value="1"/>', ' <LookupFlag value="0"/>', ' <!-- SubTableCount=1 -->', ' <SinglePos index="0" Format="2">', ' <Coverage Format="1">', ' <Glyph value="a"/>', ' <Glyph value="b"/>', ' <Glyph value="c"/>', ' </Coverage>', ' <ValueFormat value="4"/>', ' <!-- ValueCount=3 -->', ' <Value index="0" XAdvance="-50"/>', ' <Value index="1" XAdvance="-40"/>', ' <Value index="2" XAdvance="-50"/>', ' </SinglePos>', '</Lookup>', ] options = subset.Options() subsetter = subset.Subsetter(options) subsetter.populate(unicodes=[ord("a"), ord("c")]) subsetter.subset(font) # All the subsetted glyphs from the original SinglePos Format2 subtable # now have the same ValueRecord, so we use a more compact Format 1 subtable. assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [ '<Lookup>', ' <LookupType value="1"/>', ' <LookupFlag value="0"/>', ' <!-- SubTableCount=1 -->', ' <SinglePos index="0" Format="1">', ' <Coverage Format="1">', ' <Glyph value="a"/>', ' <Glyph value="c"/>', ' </Coverage>', ' <ValueFormat value="4"/>', ' <Value XAdvance="-50"/>', ' </SinglePos>', '</Lookup>', ]
class TTFBuilder: @classmethod def from_bdf_path(cls, path: Path): with path.open() as f: bdf = BdfFont.from_bdf(f) return cls(bdf) def __init__(self, bdf: BdfFont): self.bdf = bdf upm = 1000 self.fb = FontBuilder(unitsPerEm=upm) self.ppp = upm / int(self.bdf.meta("SIZE")[0]) w, h, startx, starty = self.bdf.meta("FONTBOUNDINGBOX") self.ascent = int(self.bdf.meta("FONT_ASCENT")[0]) self.descent = int(self.bdf.meta("FONT_DESCENT")[0]) self.w = int(w) self.h = int(h) self.startx = int(startx) self.starty = int(starty) def build(self, output_path: str): glyph_names = { k: glyph.meta("STARTCHAR")[0] for k, glyph in self.bdf.glyphs.items() if k >= 0 } ascent = round(self.ascent * self.ppp) descent = round(self.descent * self.ppp) self.fb.setupGlyphOrder(list(glyph_names.values())) self.fb.setupCharacterMap(glyph_names) advance_widths = { name: int(self.bdf.glyphs[k].meta("SWIDTH")[0]) for k, name in glyph_names.items() } family_name = "CozetteVector" style_name = "Regular" version = get_version() # scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=IWS-Chapter08 namestrings = { "familyName": { "en": family_name }, "styleName": { "en": style_name }, "uniqueFontIdentifier": f"fontBuilder: {family_name}.{style_name}", "fullName": family_name, "psName": f"{family_name}-{style_name}", "version": f"Version {version}", "copyright": "(c) 2020 Slavfox", "manufacturer": "Slavfox", "designer": "Slavfox", "description": "Programming bitmap font optimized for coziness", "vendorURL": "https://github.com/slavfox", "designerURL": "https://github.com/slavfox", "licenseDescription": LICENSE_TEXT, "licenseInfoURL": "https://opensource.org/licenses/MIT", "sampleText": "A wizard’s job is to vex chumps quickly in fog.", } self.fb.setupGlyf({ name: self.bdf.glyphs[k].draw(self.ppp) for k, name in glyph_names.items() }) metrics = { name: (w, self.fb.font["glyf"][name].xMin) for name, w in advance_widths.items() } self.fb.setupHorizontalMetrics(metrics) self.fb.setupHorizontalHeader(ascent=ascent, descent=-descent) self.fb.setupNameTable(namestrings) self.fb.setupOS2( sTypoAscender=ascent, usWinAscent=ascent, usWinDescent=descent, panose={ **_panoseDefaults, "bFamilyType": 2, # Text and display "bProportion": 9 # Monospace }) self.fb.setupPost(isFixedPitch=1) self.fb.save(output_path)
def build(instance, opts): font = instance.parent master = font.masters[0] glyphOrder = [] advanceWidths = {} characterMap = {} charStrings = {} colorLayers = {} for glyph in font.glyphs: if not glyph.export: continue name = glyph.name for layer in glyph.layers: if layer.name.startswith("Color "): _, index = layer.name.split(" ") if name not in colorLayers: colorLayers[name] = [] colorLayers[name].append((name, int(index))) glyphOrder.append(name) if glyph.unicode: characterMap[int(glyph.unicode, 16)] = name layer = getLayer(glyph, instance) charStrings[name] = draw(layer, instance).getCharString() advanceWidths[name] = layer.width # XXX glyphOrder.pop(glyphOrder.index(".notdef")) glyphOrder.pop(glyphOrder.index("space")) glyphOrder.insert(0, ".notdef") glyphOrder.insert(1, "space") version = float(opts.version) vendor = font.customParameters["vendorID"] names = { "copyright": font.copyright, "familyName": instance.familyName, "styleName": instance.name, "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}", "fullName": instance.fullName, "version": f"Version {version:.03f}", "psName": instance.fontName, "manufacturer": font.manufacturer, "designer": font.designer, "vendorURL": font.manufacturerURL, "designerURL": font.designerURL, "licenseDescription": font.customParameters["license"], "licenseInfoURL": font.customParameters["licenseURL"], "sampleText": font.customParameters["sampleText"], } fb = FontBuilder(font.upm, isTTF=False) fb.updateHead(fontRevision=version) fb.setupGlyphOrder(glyphOrder) fb.setupCharacterMap(characterMap) fb.setupNameTable(names, mac=False) fb.setupHorizontalHeader(ascent=master.ascender, descent=master.descender, lineGap=master.customParameters['hheaLineGap']) if opts.debug: fb.setupCFF(names["psName"], {}, charStrings, {}) else: fb.setupCFF2(charStrings) metrics = {} for name, width in advanceWidths.items(): bounds = charStrings[name].calcBounds(None) or [0] metrics[name] = (width, bounds[0]) fb.setupHorizontalMetrics(metrics) fb.setupPost() fea = makeFeatures(instance, master, opts) fb.addOpenTypeFeatures(fea) palettes = master.customParameters["Color Palettes"] palettes = [[tuple(int(v) / 255 for v in c.split(",")) for c in p] for p in palettes] fb.setupCPAL(palettes) fb.setupCOLR(colorLayers) instance.font = fb.font axes = [ instance.weightValue, instance.widthValue, instance.customValue, instance.customValue1, instance.customValue2, instance.customValue3, ] instance.axes = {} for i, axis in enumerate(font.customParameters["Axes"]): instance.axes[axis["Tag"]] = axes[i] if opts.debug: fb.font.save(f"{instance.fontName}.otf") fb.font.saveXML(f"{instance.fontName}.ttx") return fb.font
def obfuscate_plus(plain_text, filename: str, only_ttf: bool, target_path: str = 'output'): """ :param plain_text: 用户看到的内容 :param filename: 不含格式后缀的文件名 :param only_ttf: 是否需要woff、woff2格式 :param target_path: 生成的目标目录 """ if str_has_whitespace(plain_text): raise Exception('明文不允许含有空格') if str_has_emoji(plain_text): raise Exception('明文不允许含有emoji') plain_text = deduplicate_str(plain_text) original_font = TTFont(root / 'base-font/KaiGenGothicCN-Regular.ttf') # https://github.com/fonttools/fonttools/blob/4.0.1/Lib/fontTools/fontBuilder.py#L28 # <class 'dict'>: {32: 'cid00001', 33: 'cid00002', 34: 'cid00003'...} # key 为 ord(字符串) original_cmap: dict = original_font.getBestCmap() try: ensure_cmap_has_all_text(original_cmap, plain_text) except Exception as e: raise e # print('plain_text', plain_text) glyphs, metrics, cmap = {}, {}, {} # Unicode字符平面映射 # https://zh.wikipedia.org/wiki/Unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84 private_codes = random.sample(range(0xE000, 0xF8FF), len(plain_text)) # 中文汉字和常见英文数字等的unicode编码范围实例页面 # https://www.zhangxinxu.com/study/201611/chinese-language-unicode-range.html cjk_codes = random.sample(range(0x4E00, 0x9FA5), len(plain_text)) # print('private_codes', private_codes) # print('cjk_codes', cjk_codes) # https://github.com/fonttools/fonttools/blob/4.0.1/Tests/pens/ttGlyphPen_test.py#L21 glyph_set = original_font.getGlyphSet() pen = TTGlyphPen(glyph_set) glyph_order = original_font.getGlyphOrder() # print('glyph_order', glyph_order) final_shadow_text: list = [] if 'null' in glyph_order: # print('基础字体含有 null') glyph_set['null'].draw(pen) glyphs['null'] = pen.glyph() metrics['null'] = original_font['hmtx']['null'] final_shadow_text += ['null'] if '.notdef' in glyph_order: # print('基础字体含有 .notdef') glyph_set['.notdef'].draw(pen) glyphs['.notdef'] = pen.glyph() metrics['.notdef'] = original_font['hmtx']['.notdef'] final_shadow_text += ['.notdef'] html_entities = [] # 理论上这里还可以再打散一次顺序 for index, plain in enumerate(plain_text): # print('index', index, 'plain', plain) try: shadow_cmap_name = original_cmap[cjk_codes[index]] # print('shadow_cmap_name', shadow_cmap_name) except KeyError: # 遇到基础字库不存在的字会出现这种错误 traceback.print_exc() return obfuscate_plus(filename, plain_text, only_ttf, target_path) final_shadow_text += [shadow_cmap_name] glyph_set[original_cmap[ord(plain)]].draw(pen) glyphs[shadow_cmap_name] = pen.glyph() metrics[shadow_cmap_name] = original_font['hmtx'][original_cmap[ord( plain)]] cmap[private_codes[index]] = shadow_cmap_name html_entities += [hex(private_codes[index]).replace('0x', '&#x')] # print('cmap', cmap) # print('metrics', metrics) # print('final_shadow_text', final_shadow_text) # print('html_entities', html_entities) horizontal_header = { 'ascent': original_font['hhea'].ascent, 'descent': original_font['hhea'].descent, } fb = FontBuilder(original_font['head'].unitsPerEm, isTTF=True) fb.setupGlyphOrder(final_shadow_text) fb.setupCharacterMap(cmap) fb.setupGlyf(glyphs) fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader(**horizontal_header) fb.setupNameTable(NAME_STRING) fb.setupOS2() fb.setupPost() fb.save(f'{root}/{target_path}/{filename}.ttf') # print('创建了新字体文件', f'{root}/{target_path}/{filename}.ttf') result = dict() result['ttf'] = f'{root}/{target_path}/{filename}.ttf' if only_ttf: return result else: woff_and_woff2 = subset_ttf_font(f'{root}/{target_path}/{filename}') return { **result, **woff_and_woff2 }, dict(zip(plain_text, html_entities))
def obfuscate(plain_text, shadow_text, filename: str, only_ttf: bool, target_path: str = 'output') -> dict: """ :param plain_text: 用户看到的内容 :param shadow_text: 爬虫看到的内容 :param filename: 不含格式后缀的文件名 :param only_ttf: 是否需要woff、woff2格式 :param target_path: 生成的目标目录 """ if str_has_whitespace(plain_text) | str_has_whitespace(shadow_text): raise Exception('明文或阴书不允许含有空格') if str_has_emoji(plain_text) | str_has_emoji(shadow_text): raise Exception('明文或阴书不允许含有emoji') plain_text = deduplicate_str(plain_text) shadow_text = deduplicate_str(shadow_text) if plain_text == shadow_text: raise Exception('没有意义的混淆') if len(plain_text) != len(shadow_text): raise Exception('阴书的有效长度需与明文一致') original_font = TTFont(root / 'base-font/KaiGenGothicCN-Regular.ttf') # https://github.com/fonttools/fonttools/blob/4.0.1/Lib/fontTools/fontBuilder.py#L28 # <class 'dict'>: {32: 'cid00001', 33: 'cid00002', 34: 'cid00003'...} # key 为 ord(字符串) original_cmap: dict = original_font.getBestCmap() try: ensure_cmap_has_all_text(original_cmap, plain_text) except Exception as e: raise e # print('plain_text', plain_text) # print('shadow_text', shadow_text) glyphs, metrics, cmap = {}, {}, {} # https://github.com/fonttools/fonttools/blob/4.0.1/Tests/pens/ttGlyphPen_test.py#L21 glyph_set = original_font.getGlyphSet() pen = TTGlyphPen(glyph_set) glyph_order = original_font.getGlyphOrder() # print('glyph_order', glyph_order) final_shadow_text: list = [] if 'null' in glyph_order: # print('基础字体含有 null') glyph_set['null'].draw(pen) glyphs['null'] = pen.glyph() metrics['null'] = original_font['hmtx']['null'] final_shadow_text += ['null'] if '.notdef' in glyph_order: # print('基础字体含有 .notdef') glyph_set['.notdef'].draw(pen) glyphs['.notdef'] = pen.glyph() metrics['.notdef'] = original_font['hmtx']['.notdef'] final_shadow_text += ['.notdef'] for index, (plain, shadow) in enumerate(zip(plain_text, shadow_text)): # print('index', index, 'plain', plain, 'shadow', shadow) shadow_cmap_name = original_cmap[ord(shadow)] # print('shadow_cmap_name', shadow_cmap_name) final_shadow_text += [shadow_cmap_name] glyph_set[original_cmap[ord(plain)]].draw(pen) glyphs[shadow_cmap_name] = pen.glyph() metrics[shadow_cmap_name] = original_font['hmtx'][original_cmap[ord( plain)]] cmap[ord(shadow)] = shadow_cmap_name # print('cmap', cmap) # print('metrics', metrics) # print('final_shadow_text', final_shadow_text) horizontal_header = { 'ascent': original_font['hhea'].ascent, 'descent': original_font['hhea'].descent, } fb = FontBuilder(original_font['head'].unitsPerEm, isTTF=True) fb.setupGlyphOrder(final_shadow_text) fb.setupCharacterMap(cmap) fb.setupGlyf(glyphs) fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader(**horizontal_header) fb.setupNameTable(NAME_STRING) fb.setupOS2() fb.setupPost() # print('创建了新字体文件', f'{target_path}/{filename}.ttf') fb.save(f'{root}/{target_path}/{filename}.ttf') # print('创建了新字体文件', f'{target_path}/{filename}.ttf') result = dict() result['ttf'] = f'{root}/{target_path}/{filename}.ttf' if not only_ttf: woff_and_woff2 = subset_ttf_font(f'{root}/{target_path}/{filename}') result = {**result, **woff_and_woff2} return result
def build(instance, opts): font = instance.parent master = font.masters[0] fea, marks = makeFeatures(instance, master) glyphOrder = [] advanceWidths = {} characterMap = {} charStrings = {} for glyph in font.glyphs: if not glyph.export: continue name = glyph.name glyphOrder.append(name) if glyph.unicode: characterMap[int(glyph.unicode, 16)] = name layer = getLayer(glyph, instance) width = 0 if name in marks else layer.width path = Path() draw(layer, instance, path.getPen()) path.simplify(fix_winding=True, keep_starting_points=True) pen = T2CharStringPen(width, None) path.draw(pen) charStrings[name] = pen.getCharString(optimize=False) advanceWidths[name] = width # XXX glyphOrder.pop(glyphOrder.index(".notdef")) glyphOrder.pop(glyphOrder.index("space")) glyphOrder.insert(0, ".notdef") glyphOrder.insert(1, "space") version = float(opts.version) vendor = font.customParameters["vendorID"] names = { "copyright": font.copyright, "familyName": instance.familyName, "styleName": instance.name, "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}", "fullName": instance.fullName, "version": f"Version {version:.03f}", "psName": instance.fontName, "manufacturer": font.manufacturer, "designer": font.designer, "description": font.customParameters["description"], "vendorURL": font.manufacturerURL, "designerURL": font.designerURL, "licenseDescription": font.customParameters["license"], "licenseInfoURL": font.customParameters["licenseURL"], "sampleText": font.customParameters["sampleText"], } date = int(font.date.timestamp()) - epoch_diff fb = FontBuilder(font.upm, isTTF=False) fb.updateHead(fontRevision=version, created=date, modified=date) fb.setupGlyphOrder(glyphOrder) fb.setupCharacterMap(characterMap) fb.setupNameTable(names, mac=False) fb.setupHorizontalHeader(ascent=master.ascender, descent=master.descender, lineGap=master.customParameters["typoLineGap"]) privateDict = { "BlueValues": [], "OtherBlues": [], "StemSnapH": master.horizontalStems, "StemSnapV": master.verticalStems, "StdHW": master.horizontalStems[0], "StdVW": master.verticalStems[0], } for zone in sorted(master.alignmentZones): pos = zone.position size = zone.size vals = privateDict["BlueValues"] if pos == 0 or size >= 0 else privateDict["OtherBlues"] vals.extend(sorted((pos, pos + size))) fontInfo = { "FullName": names["fullName"], "Notice": names["copyright"].replace("©", "\(c\)"), "version": f"{version:07.03f}", "Weight": instance.name, } fb.setupCFF(names["psName"], fontInfo, charStrings, privateDict) metrics = {} for i, (name, width) in enumerate(advanceWidths.items()): bounds = charStrings[name].calcBounds(None) or [0] metrics[name] = (width, bounds[0]) fb.setupHorizontalMetrics(metrics) codePages = [CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]] fb.setupOS2(version=4, sTypoAscender=master.ascender, sTypoDescender=master.descender, sTypoLineGap=master.customParameters["typoLineGap"], usWinAscent=master.ascender, usWinDescent=-master.descender, sxHeight=master.xHeight, sCapHeight=master.capHeight, achVendID=vendor, fsType=calcBits(font.customParameters["fsType"], 0, 16), fsSelection=calcFsSelection(instance), ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0, 32), ulCodePageRange1=calcBits(codePages, 0, 32)) ut = int(master.customParameters["underlineThickness"]) up = int(master.customParameters["underlinePosition"]) fb.setupPost(underlineThickness=ut, underlinePosition=up + ut//2) fb.addOpenTypeFeatures(fea) cidinfo = f""" FontName ({names["psName"]}) FamilyName ({names["familyName"]}) Weight ({fontInfo["Weight"]}) version ({fontInfo["version"]}) Notice ({fontInfo["Notice"]}) Registry Adobe Ordering Identity Supplement 0 """ cidmap = f"mergefonts {instance.fontName}\n" \ + "\n".join([f"{i} {n}" for i, n in enumerate(glyphOrder)]) return fb.font, cidinfo, cidmap