1
1
mirror of https://github.com/rsms/inter.git synced 2024-12-18 13:11:37 +03:00
inter/misc/gen-metrics-and-svgs.py

449 lines
12 KiB
Python
Executable File

#!/usr/bin/env python
# encoding: utf8
#
# Sync glyph shapes between SVG and UFO, creating a bridge between UFO and Figma.
#
from __future__ import print_function
import os, sys, argparse, re, json, plistlib
from math import ceil, floor
from robofab.objects.objectsRF import OpenFont
from collections import OrderedDict
from fontbuild.generateGlyph import generateGlyph
from ConfigParser import RawConfigParser
BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
font = None # RFont
ufopath = ''
effectiveAscender = 0
scale = 0.1
agl = None
def num(s):
return int(s) if s.find('.') == -1 else float(s)
def parseGlyphComposition(composite):
c = composite.split("=")
d = c[1].split("/")
glyphName = d[0]
if len(d) == 1:
offset = [0, 0]
else:
offset = [int(i) for i in d[1].split(",")]
accentString = c[0]
accents = accentString.split("+")
baseName = accents.pop(0)
accentNames = [i.split(":") for i in accents]
return (glyphName, baseName, accentNames, offset)
def loadGlyphCompositions(filename): # { glyphName => (baseName, accentNames, offset, rawline) }
compositions = OrderedDict()
with open(filename, 'r') as f:
for line in f:
line = line.strip()
if len(line) > 0 and line[0] != '#':
glyphName, baseName, accentNames, offset = parseGlyphComposition(line)
compositions[glyphName] = (baseName, accentNames, offset, line)
return compositions
def loadAGL(filename): # -> { 2126: 'Omega', ... }
m = {}
with open(filename, 'r') as f:
for line in f:
# Omega;2126
# dalethatafpatah;05D3 05B2 # higher-level combinations; ignored
line = line.strip()
if len(line) > 0 and line[0] != '#':
name, uc = tuple([c.strip() for c in line.split(';')])
if uc.find(' ') == -1:
# it's a 1:1 mapping
m[int(uc, 16)] = name
return m
def decomposeGlyph(font, glyph):
"""Moves the components of a glyph to its outline."""
if len(glyph.components):
deepCopyContours(font, glyph, glyph, (0, 0), (1, 1))
glyph.clearComponents()
def deepCopyContours(font, parent, component, offset, scale):
"""Copy contours to parent from component, including nested components."""
for nested in component.components:
deepCopyContours(
font, parent, font[nested.baseGlyph],
(offset[0] + nested.offset[0], offset[1] + nested.offset[1]),
(scale[0] * nested.scale[0], scale[1] * nested.scale[1]))
if component == parent:
return
for contour in component:
contour = contour.copy()
contour.scale(scale)
contour.move(offset)
parent.appendContour(contour)
def glyphToSVGPath(g, yMul):
commands = {'move':'M','line':'L','curve':'Y','offcurve':'X','offCurve':'X'}
svg = ''
contours = []
if len(g.components):
decomposeGlyph(g.getParent(), g) # mutates g
if len(g):
for c in range(len(g)):
contours.append(g[c])
for i in range(len(contours)):
c = contours[i]
contour = end = ''
curve = False
points = c.points
if points[0].type == 'offCurve':
points.append(points.pop(0))
if points[0].type == 'offCurve':
points.append(points.pop(0))
for x in range(len(points)):
p = points[x]
command = commands[str(p.type)]
if command == 'X':
if curve == True:
command = ''
else:
command = 'C'
curve = True
if command == 'Y':
command = ''
curve = False
if x == 0:
command = 'M'
if p.type == 'curve':
end = ' %g %g' % (p.x * scale, (p.y * yMul) * scale)
contour += ' %s%g %g' % (command, p.x * scale, (p.y * yMul) * scale)
svg += ' ' + contour + end + 'z'
if font.has_key('__svgsync'):
font.removeGlyph('__svgsync')
return svg.strip()
def svgWidth(g):
box = g.box
xoffs = box[0]
width = box[2] - box[0]
return width, xoffs
def glyphToSVG(g):
width, xoffs = svgWidth(g)
svg = '''
<svg id="svg-%(name)s" xmlns="http://www.w3.org/2000/svg" width="%(width)d" height="%(height)d">
<path d="%(glyphSVGPath)s" transform="translate(%(xoffs)g %(yoffs)g)"/>
</svg>
''' % {
'name': g.name,
'width': int(ceil(width * scale)),
'height': int(ceil((effectiveAscender - font.info.descender) * scale)),
'xoffs': -(xoffs * scale),
'yoffs': effectiveAscender * scale,
# 'leftMargin': g.leftMargin * scale,
# 'rightMargin': g.rightMargin * scale,
'glyphSVGPath': glyphToSVGPath(g, -1),
# 'ascender': font.info.ascender * scale,
# 'descender': font.info.descender * scale,
# 'baselineOffset': (font.info.unitsPerEm + font.info.descender) * scale,
# 'unitsPerEm': font.info.unitsPerEm,
# 'margin': [g.leftMargin * scale, g.rightMargin * scale],
}
# (width, advance, left, right)
info = (width, g.width, g.leftMargin, g.rightMargin)
return svg.strip(), info
def stat(path):
try:
return os.stat(path)
except OSError as e:
return None
def writeFile(file, s):
with open(file, 'w') as f:
f.write(s)
def writeFileAndMkDirsIfNeeded(file, s):
try:
writeFile(file, s)
except IOError as e:
if e.errno == 2:
os.makedirs(os.path.dirname(file))
writeFile(file, s)
def findGlifFile(glyphname):
# glyphname.glif
# glyphname_.glif
# glyphname__.glif
# glyphname___.glif
for underscoreCount in range(0, 5):
fn = os.path.join(ufopath, 'glyphs', glyphname + ('_' * underscoreCount) + '.glif')
st = stat(fn)
if st is not None:
return fn, st
if glyphname.find('.') != -1:
# glyph_.name.glif
# glyph__.name.glif
# glyph___.name.glif
for underscoreCount in range(0, 5):
nv = glyphname.split('.')
nv[0] = nv[0] + ('_' * underscoreCount)
ns = '.'.join(nv)
fn = os.path.join(ufopath, 'glyphs', ns + '.glif')
st = stat(fn)
if st is not None:
return fn, st
if glyphname.find('_') != -1:
# glyph_name.glif
# glyph_name_.glif
# glyph_name__.glif
# glyph__name.glif
# glyph__name_.glif
# glyph__name__.glif
# glyph___name.glif
# glyph___name_.glif
# glyph___name__.glif
for x in range(0, 4):
for y in range(0, 5):
ns = glyphname.replace('_', '__' + ('_' * x))
fn = os.path.join(ufopath, 'glyphs', ns + ('_' * y) + '.glif')
st = stat(fn)
if st is not None:
return fn, st
return ('', None)
usedSVGNames = set()
def genGlyph(glyphName, generateFrom, force):
# generateFrom = (baseName, accentNames, offset, rawline)
if generateFrom is not None:
generateGlyph(font, generateFrom[3], agl)
g = font.getGlyph(glyphName)
return glyphToSVG(g)
def genGlyphIDs(glyphnames):
nameToIdMap = {}
idToNameMap = {}
nextId = 0
for name in glyphnames:
nameToIdMap[name] = nextId
idToNameMap[nextId] = name
nextId += 1
return nameToIdMap, idToNameMap
def genKerningInfo(font, glyphnames, nameToIdMap):
kerning = font.kerning
# load groups
filename = os.path.join(font.path, 'groups.plist')
groups = plistlib.readPlist(filename)
pairs = []
for kt in kerning.keys():
v = kerning[kt]
leftname, rightname = kt
leftnames = []
rightnames = []
if leftname[0] == '@':
leftnames = groups[leftname]
else:
leftnames = [leftname]
if rightname[0] == '@':
rightnames = groups[rightname]
else:
rightnames = [rightname]
for lname in leftnames:
for rname in rightnames:
lnameId = nameToIdMap.get(lname)
rnameId = nameToIdMap.get(rname)
if lnameId and rnameId:
pairs.append([lnameId, rnameId, v])
# print('pairs: %r' % pairs)
return pairs
def fmtJsonDict(d):
keys = sorted(d.keys())
s = '{'
delim = '\n'
delimNth = ',\n'
for k in keys:
v = d[k]
s += delim + json.dumps(str(k)) + ':' + json.dumps(v)
delim = delimNth
return s + '}'
def fmtJsonList(d):
s = '['
delim = '\n'
delimNth = ',\n'
for t in kerning:
s += delim + json.dumps(t, separators=(',',':'))
delim = delimNth
return s + ']'
# ————————————————————————————————————————————————————————————————————————
# main
argparser = argparse.ArgumentParser(description='Generate SVG glyphs from UFO')
argparser.add_argument('-scale', dest='scale', metavar='<scale>', type=str,
default='',
help='Scale glyph. Should be a number in the range (0-1]. Defaults to %g' % scale)
argparser.add_argument(
'-f', '-force', dest='force', action='store_const', const=True, default=False,
help='Generate glyphs even though they appear to be up-to date.')
argparser.add_argument('ufopath', metavar='<ufopath>', type=str,
help='Path to UFO packages')
argparser.add_argument('glyphs', metavar='<glyphname>', type=str, nargs='*',
help='Only generate specific glyphs.')
args = argparser.parse_args()
srcDir = os.path.join(BASEDIR, 'src')
# load fontbuild config
config = RawConfigParser(dict_type=OrderedDict)
configFilename = os.path.join(srcDir, 'fontbuild.cfg')
config.read(configFilename)
deleteNames = set()
for sectionName, value in config.items('glyphs'):
if sectionName == 'delete':
deleteNames = set(value.split())
if len(args.scale):
scale = float(args.scale)
ufopath = args.ufopath.rstrip('/')
font = OpenFont(ufopath)
effectiveAscender = max(font.info.ascender, font.info.unitsPerEm)
# print('\n'.join(font.keys()))
# sys.exit(0)
agl = loadAGL(os.path.join(srcDir, 'glyphlist.txt')) # { 2126: 'Omega', ... }
deleteNames.add('.notdef')
deleteNames.add('.null')
glyphnames = args.glyphs if len(args.glyphs) else font.keys()
glyphnameSet = set(glyphnames)
generatedGlyphNames = set()
diacriticComps = loadGlyphCompositions(os.path.join(srcDir, 'diacritics.txt'))
for glyphName, comp in diacriticComps.iteritems():
if glyphName not in glyphnameSet:
generatedGlyphNames.add(glyphName)
glyphnames.append(glyphName)
glyphnameSet.add(glyphName)
glyphnames = [gn for gn in glyphnames if gn not in deleteNames]
glyphnames.sort()
nameToIdMap, idToNameMap = genGlyphIDs(glyphnames)
glyphMetrics = {}
# jsonLines = []
svgLines = []
for glyphname in glyphnames:
generateFrom = None
if glyphname in generatedGlyphNames:
generateFrom = diacriticComps[glyphname]
svg, metrics = genGlyph(glyphname, generateFrom, force=args.force)
# metrics: (width, advance, left, right)
glyphMetrics[nameToIdMap[glyphname]] = metrics
svgLines.append(svg.replace('\n', ''))
# print('{\n' + ',\n'.join(jsonLines) + '\n}')
svgtext = '\n'.join(svgLines)
# print(svgtext)
glyphsHtmlFilename = os.path.join(BASEDIR, 'docs', 'glyphs', 'index.html')
html = ''
with open(glyphsHtmlFilename, 'r') as f:
html = f.read()
startMarker = '<div id="svgs">'
startPos = html.find(startMarker)
endMarker = '</div><!--END-SVGS'
endPos = html.find(endMarker, startPos + len(startMarker))
relfilename = os.path.relpath(glyphsHtmlFilename, os.getcwd())
if startPos == -1 or endPos == -1:
msg = 'Could not find `<div id="svgs">...</div><!--END-SVGS` in %s'
print(msg % relfilename, file=sys.stderr)
sys.exit(1)
for name in glyphnames:
if name == 'zero.tnum.slash':
print('FOUND zero.tnum.slash')
kerning = genKerningInfo(font, glyphnames, nameToIdMap)
metaJson = '{\n'
metaJson += '"nameids":' + fmtJsonDict(idToNameMap) + ',\n'
metaJson += '"metrics":' + fmtJsonDict(glyphMetrics) + ',\n'
metaJson += '"kerning":' + fmtJsonList(kerning) + '\n'
metaJson += '}'
# metaHtml = '<script>var fontMetaData = ' + metaJson + ';</script>'
html = html[:startPos + len(startMarker)] + '\n' + svgtext + '\n' + html[endPos:]
print('write', relfilename)
with open(glyphsHtmlFilename, 'w') as f:
f.write(html)
# JSON
jsonFilename = os.path.join(BASEDIR, 'docs', 'glyphs', 'metrics.json')
jsonFilenameRel = os.path.relpath(jsonFilename, os.getcwd())
print('write', jsonFilenameRel)
with open(jsonFilename, 'w') as f:
f.write(metaJson)
metaJson