Created
January 28, 2022 08:49
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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