Skip to content

Instantly share code, notes, and snippets.

@kontur
Created January 28, 2022 08:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kontur/75366883d331c749b2dd5ec209d40677 to your computer and use it in GitHub Desktop.
Save kontur/75366883d331c749b2dd5ec209d40677 to your computer and use it in GitHub Desktop.
Sample script to replace glyphs in an OTF/TTF font with SVG input. (fonttools, defcon, ufo2ft, extract-ufo)
"""
Sample script to replace glyphs in an OTF/TTF font with SVG input.
- Round-trips via UFO, so not applicable to Variable fonts.
- Will subset the and remove the replaced glyphs from the resulting font.
"""
import os
import re
import sys
import click
import shutil
import xml.etree.ElementTree as ET
from fontTools.subset import Subsetter
from fontTools.pens.pointPen import SegmentToPointPen
from fontTools.ufoLib.glifLib import writeGlyphToString
from fontTools.svgLib import SVGPath
from fontTools.misc.py23 import SimpleNamespace
from fontTools.ttLib import TTFont
from fontTools.misc.transform import Transform
from defcon import Font
from ufo2ft import compileOTF, compileTTF
from extractor import extractUFO
def font2ufo(path, outpath=None, version=3):
if outpath is None:
outpath = os.path.splitext(path)[0] + ".ufo"
ufo = Font()
extractUFO(path, ufo)
ufo.save(outpath, version)
return outpath
def ufo2font(path, outpath=None):
ufo = Font(path)
if outpath is None:
outpath = os.path.splitext(path)[0] + ".otf"
pathinfo = os.path.splitext(outpath)
if pathinfo[1] == ".otf":
otf = compileOTF(ufo)
otf.save(outpath)
if pathinfo[1] == ".ttf":
ttf = compileTTF(ufo)
ttf.save(outpath)
return outpath
def writeSvgToUfo(svg_path, ufo_path, glyphname):
"""
Convert an svg to an ufo with the svg as glyph new glyph
"""
font = Font(ufo_path)
tmp_ufo = "_tmp.ufo"
with open(svg_path, "r") as f:
svg = f.read()
# the proportions of the SVG to Glyph are:
# SVG viewbox height scaled to UPM, width scaled by same factor
root = ET.fromstring(svg)
viewbox = [float(x) for x in re.split(
r"\s", root.get("viewBox"))] # [x y w h]
height = int(font.info.capHeight)
scale = height / viewbox[3]
width = viewbox[2] * scale
# mirror svg coordinates along the x, scale to height
mirror_transform = Transform(scale, 0, 0, -scale, 0, height)
# the included xml parser breaks on svgs with <?xml ...> beginning declarations
# so let's remove them if present
svg = re.sub(r"^<\?xml[^>]+\>", "", svg)
glif = svg2glif(svg, glyphname, width, height,
transform=mirror_transform, version=1)
# make an empty font, write the glif, save
tmp_font = Font()
demo_glyph = tmp_font.newGlyph(glyphname)
tmp_font.save(tmp_ufo, 2)
with open("%s/glyphs/%s.glif" % (tmp_ufo, glyphname), 'w') as f:
f.write(glif)
# reopen the ufo font, so the hard-copied glif gets read
tmp_font = Font(tmp_ufo)
demo_glyph = tmp_font[glyphname]
font.insertGlyph(demo_glyph)
font.save()
if os.path.isdir(tmp_ufo):
shutil.rmtree(tmp_ufo)
return font
def svg2glif(svg, name, width=0, height=0, unicodes=None, transform=None,
version=2):
""" Convert an SVG outline to a UFO glyph with given 'name', advance
'width' and 'height' (int), and 'unicodes' (list of int).
Return the resulting string in GLIF format (default: version 2).
If 'transform' is provided, apply a transformation matrix before the
conversion (must be tuple of 6 floats, or a FontTools Transform object).
"""
glyph = SimpleNamespace(width=width, height=height, unicodes=unicodes)
outline = SVGPath.fromstring(svg, transform=transform)
# writeGlyphToString takes a callable (usually a glyph's drawPoints
# method) that accepts a PointPen, however SVGPath currently only has
# a draw method that accepts a segment pen. We need to wrap the call
# with a converter pen.
def drawPoints(pointPen):
pen = SegmentToPointPen(pointPen)
outline.draw(pen)
return writeGlyphToString(name,
glyphObject=glyph,
drawPointsFunc=drawPoints,
formatVersion=version)
@click.command()
@click.option("-g", "--glyphname", default="demo")
@click.argument("input", type=click.Path(exists=True))
@click.argument("output", type=click.Path())
@click.argument("svg", type=click.Path(exists=True))
@click.argument("replace", nargs=-1)
def main(glyphname, input, output, svg, replace=None):
if replace is ():
print("Provide one or more characters to replace")
sys.exit()
print("Replace %d glyphs with the content of %s in %s and save it as %s" %
(len(replace), svg, input, output))
TMP_UFO = "./tmp.ufo"
if os.path.isdir(TMP_UFO):
shutil.rmtree(TMP_UFO)
font2ufo(input, TMP_UFO)
writeSvgToUfo(svg, TMP_UFO, glyphname)
ufo2font(TMP_UFO, output)
replace = [ord(c.strip()) for c in replace]
f = TTFont(output)
try:
for t in f["cmap"].tables:
for unicode, name in t.cmap.items():
if unicode in replace:
t.cmap[unicode] = glyphname
except Exception as e:
print(e)
subsetter = Subsetter()
unicodes = f["cmap"].getBestCmap().keys()
subsetter.populate(unicodes=unicodes)
subsetter.subset(f)
f.save(output)
if os.path.isdir(TMP_UFO):
shutil.rmtree(TMP_UFO)
if __name__ == "__main__":
"""
Params: input font, output font, svg path, glyphname, characters to replace comma-separated
"""
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment