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_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 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)