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 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 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 _make_fontfile_with_OS2(*, version, **kwargs): upem = 1000 glyphOrder = [".notdef", "a"] cmap = {0x61: "a"} glyphs = {gn: Glyph() for gn in glyphOrder} hmtx = {gn: (500, 0) for gn in glyphOrder} names = {"familyName": "TestOS2", "styleName": "Regular"} fb = FontBuilder(unitsPerEm=upem) fb.setupGlyphOrder(glyphOrder) fb.setupCharacterMap(cmap) fb.setupGlyf(glyphs) fb.setupHorizontalMetrics(hmtx) fb.setupHorizontalHeader() fb.setupNameTable(names) fb.setupOS2(version=version, **kwargs) return _compile(fb.font)
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"])
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 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 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 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>', ]
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 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_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 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