2017-08-28 12:36:40 +03:00
|
|
|
#!/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
|
2017-09-25 05:38:30 +03:00
|
|
|
|
2018-09-04 02:59:55 +03:00
|
|
|
import os, sys
|
|
|
|
from os.path import dirname, basename, abspath, relpath, join as pjoin
|
|
|
|
sys.path.append(abspath(pjoin(dirname(__file__), 'tools')))
|
|
|
|
from common import BASEDIR
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import json
|
|
|
|
import plistlib
|
|
|
|
import re
|
|
|
|
from collections import OrderedDict
|
|
|
|
from math import ceil, floor
|
|
|
|
from defcon import Font
|
|
|
|
from svg import SVGPathPen
|
2017-09-25 05:38:30 +03:00
|
|
|
|
2017-08-28 12:36:40 +03:00
|
|
|
|
|
|
|
font = None # RFont
|
|
|
|
ufopath = ''
|
|
|
|
effectiveAscender = 0
|
|
|
|
scale = 0.1
|
|
|
|
|
|
|
|
|
|
|
|
def num(s):
|
|
|
|
return int(s) if s.find('.') == -1 else float(s)
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
2018-09-04 02:59:55 +03:00
|
|
|
pen = SVGPathPen(g.getParent(), yMul)
|
|
|
|
g.draw(pen)
|
|
|
|
return pen.getCommands()
|
2017-08-28 12:36:40 +03:00
|
|
|
|
|
|
|
|
|
|
|
def svgWidth(g):
|
2018-09-04 02:59:55 +03:00
|
|
|
bounds = g.bounds # (xMin, yMin, xMax, yMax)
|
|
|
|
if bounds is None:
|
|
|
|
return 0, 0
|
|
|
|
xMin = bounds[0]
|
|
|
|
xMax = bounds[2]
|
|
|
|
width = xMax - xMin
|
|
|
|
return width, xMin
|
2017-08-28 12:36:40 +03:00
|
|
|
|
|
|
|
|
|
|
|
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">
|
2018-09-04 02:59:55 +03:00
|
|
|
<path d="%(glyphSVGPath)s" transform="translate(%(xoffs)g %(yoffs)g) scale(%(scale)g)"/>
|
2017-08-28 12:36:40 +03:00
|
|
|
</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,
|
2018-09-04 02:59:55 +03:00
|
|
|
'scale': scale,
|
2017-08-28 12:36:40 +03:00
|
|
|
|
|
|
|
# '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()
|
|
|
|
|
2018-09-04 02:59:55 +03:00
|
|
|
def genGlyph(glyphName):
|
|
|
|
g = font[glyphName]
|
2017-08-28 12:36:40 +03:00
|
|
|
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:
|
2017-08-28 21:31:42 +03:00
|
|
|
lnameId = nameToIdMap.get(lname)
|
|
|
|
rnameId = nameToIdMap.get(rname)
|
|
|
|
if lnameId and rnameId:
|
|
|
|
pairs.append([lnameId, rnameId, v])
|
2017-08-28 12:36:40 +03:00
|
|
|
|
|
|
|
# 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('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()
|
2017-09-25 05:38:30 +03:00
|
|
|
srcDir = os.path.join(BASEDIR, 'src')
|
2018-09-04 02:59:55 +03:00
|
|
|
deleteNames = set(['.notdef', '.null'])
|
2017-09-25 05:38:30 +03:00
|
|
|
|
2017-08-28 12:36:40 +03:00
|
|
|
if len(args.scale):
|
|
|
|
scale = float(args.scale)
|
|
|
|
|
|
|
|
ufopath = args.ufopath.rstrip('/')
|
|
|
|
|
2018-09-04 02:59:55 +03:00
|
|
|
font = Font(ufopath)
|
2017-08-28 12:36:40 +03:00
|
|
|
effectiveAscender = max(font.info.ascender, font.info.unitsPerEm)
|
|
|
|
|
|
|
|
# print('\n'.join(font.keys()))
|
|
|
|
# sys.exit(0)
|
|
|
|
|
2017-09-25 05:38:30 +03:00
|
|
|
deleteNames.add('.notdef')
|
|
|
|
deleteNames.add('.null')
|
2017-08-28 12:36:40 +03:00
|
|
|
|
|
|
|
glyphnames = args.glyphs if len(args.glyphs) else font.keys()
|
|
|
|
glyphnameSet = set(glyphnames)
|
|
|
|
|
2017-09-25 05:38:30 +03:00
|
|
|
glyphnames = [gn for gn in glyphnames if gn not in deleteNames]
|
2017-08-28 12:36:40 +03:00
|
|
|
glyphnames.sort()
|
|
|
|
|
|
|
|
nameToIdMap, idToNameMap = genGlyphIDs(glyphnames)
|
|
|
|
|
|
|
|
glyphMetrics = {}
|
|
|
|
|
|
|
|
# jsonLines = []
|
|
|
|
svgLines = []
|
|
|
|
for glyphname in glyphnames:
|
|
|
|
generateFrom = None
|
2018-09-04 02:59:55 +03:00
|
|
|
svg, metrics = genGlyph(glyphname)
|
2017-08-28 12:36:40 +03:00
|
|
|
# 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)
|
|
|
|
|
2017-09-25 05:38:30 +03:00
|
|
|
glyphsHtmlFilename = os.path.join(BASEDIR, 'docs', 'glyphs', 'index.html')
|
2017-08-28 12:36:40 +03:00
|
|
|
|
2018-09-04 02:59:55 +03:00
|
|
|
html = u''
|
2017-08-28 12:36:40 +03:00
|
|
|
with open(glyphsHtmlFilename, 'r') as f:
|
2018-09-04 02:59:55 +03:00
|
|
|
html = f.read().decode('utf8')
|
2017-08-28 12:36:40 +03:00
|
|
|
|
2018-09-04 02:59:55 +03:00
|
|
|
startMarker = u'<div id="svgs">'
|
2017-08-28 12:36:40 +03:00
|
|
|
startPos = html.find(startMarker)
|
|
|
|
|
2018-09-04 02:59:55 +03:00
|
|
|
endMarker = u'</div><!--END-SVGS'
|
2017-08-28 12:36:40 +03:00
|
|
|
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)
|
|
|
|
|
|
|
|
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>'
|
|
|
|
|
2018-09-04 02:59:55 +03:00
|
|
|
html = html[:startPos + len(startMarker)] + '\n' + svgtext.decode('utf8') + '\n' + html[endPos:]
|
2017-08-28 12:36:40 +03:00
|
|
|
|
|
|
|
print('write', relfilename)
|
|
|
|
with open(glyphsHtmlFilename, 'w') as f:
|
2018-09-04 02:59:55 +03:00
|
|
|
f.write(html.encode('utf8'))
|
2017-08-28 12:36:40 +03:00
|
|
|
|
|
|
|
# JSON
|
2017-09-25 05:38:30 +03:00
|
|
|
jsonFilename = os.path.join(BASEDIR, 'docs', 'glyphs', 'metrics.json')
|
2017-08-28 12:36:40 +03:00
|
|
|
jsonFilenameRel = os.path.relpath(jsonFilename, os.getcwd())
|
|
|
|
print('write', jsonFilenameRel)
|
|
|
|
with open(jsonFilename, 'w') as f:
|
|
|
|
f.write(metaJson)
|
|
|
|
|
|
|
|
metaJson
|