2017-08-28 11:36:40 +02:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# encoding: utf8
|
|
|
|
#
|
|
|
|
# Sync glyph shapes between SVG and UFO, creating a bridge between UFO and Figma.
|
|
|
|
#
|
2018-09-04 01:59:55 +02: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
|
2018-09-17 19:05:45 +02:00
|
|
|
from ufo2ft.filters.decomposeComponents import DecomposeComponentsFilter
|
|
|
|
from ufo2ft.filters.removeOverlaps import RemoveOverlapsFilter
|
2017-09-25 04:38:30 +02:00
|
|
|
|
2017-08-28 11:36:40 +02:00
|
|
|
|
2018-09-17 19:05:45 +02:00
|
|
|
ufo = None
|
2017-08-28 11:36:40 +02:00
|
|
|
ufopath = ''
|
|
|
|
effectiveAscender = 0
|
|
|
|
scale = 0.1
|
|
|
|
|
|
|
|
|
|
|
|
def num(s):
|
|
|
|
return int(s) if s.find('.') == -1 else float(s)
|
|
|
|
|
|
|
|
|
|
|
|
def glyphToSVGPath(g, yMul):
|
2018-09-04 01:59:55 +02:00
|
|
|
pen = SVGPathPen(g.getParent(), yMul)
|
|
|
|
g.draw(pen)
|
|
|
|
return pen.getCommands()
|
2017-08-28 11:36:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
def svgWidth(g):
|
2018-09-04 01:59:55 +02: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 11:36:40 +02: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 01:59:55 +02:00
|
|
|
<path d="%(glyphSVGPath)s" transform="translate(%(xoffs)g %(yoffs)g) scale(%(scale)g)"/>
|
2017-08-28 11:36:40 +02:00
|
|
|
</svg>
|
|
|
|
''' % {
|
|
|
|
'name': g.name,
|
|
|
|
'width': int(ceil(width * scale)),
|
2018-09-17 19:05:45 +02:00
|
|
|
'height': int(ceil((effectiveAscender - ufo.info.descender) * scale)),
|
2017-08-28 11:36:40 +02:00
|
|
|
'xoffs': -(xoffs * scale),
|
|
|
|
'yoffs': effectiveAscender * scale,
|
|
|
|
# 'leftMargin': g.leftMargin * scale,
|
|
|
|
# 'rightMargin': g.rightMargin * scale,
|
|
|
|
'glyphSVGPath': glyphToSVGPath(g, -1),
|
2018-09-17 19:05:45 +02:00
|
|
|
# 'ascender': ufo.info.ascender * scale,
|
|
|
|
# 'descender': ufo.info.descender * scale,
|
|
|
|
# 'baselineOffset': (ufo.info.unitsPerEm + ufo.info.descender) * scale,
|
|
|
|
# 'unitsPerEm': ufo.info.unitsPerEm,
|
2018-09-04 01:59:55 +02:00
|
|
|
'scale': scale,
|
2017-08-28 11:36:40 +02: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 01:59:55 +02:00
|
|
|
def genGlyph(glyphName):
|
2018-09-17 19:05:45 +02:00
|
|
|
g = ufo[glyphName]
|
2017-08-28 11:36:40 +02: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
|
|
|
|
|
|
|
|
|
2018-09-17 19:05:45 +02:00
|
|
|
def genKerningInfo(ufo, glyphnames, nameToIdMap):
|
|
|
|
kerning = ufo.kerning
|
2017-08-28 11:36:40 +02:00
|
|
|
|
|
|
|
# load groups
|
2018-09-17 19:05:45 +02:00
|
|
|
filename = os.path.join(ufo.path, 'groups.plist')
|
2021-03-29 00:21:20 +02:00
|
|
|
groups = None
|
|
|
|
with open(filename, 'rb') as f:
|
|
|
|
groups = plistlib.load(f)
|
2017-08-28 11:36:40 +02:00
|
|
|
|
|
|
|
pairs = []
|
|
|
|
for kt in kerning.keys():
|
|
|
|
v = kerning[kt]
|
|
|
|
leftname, rightname = kt
|
|
|
|
leftnames = []
|
|
|
|
rightnames = []
|
|
|
|
|
2021-03-29 00:21:20 +02:00
|
|
|
if leftname.startswith(u'public.kern'):
|
2017-08-28 11:36:40 +02:00
|
|
|
leftnames = groups[leftname]
|
|
|
|
else:
|
|
|
|
leftnames = [leftname]
|
|
|
|
|
2021-03-29 00:21:20 +02:00
|
|
|
if rightname.startswith(u'public.kern'):
|
2017-08-28 11:36:40 +02:00
|
|
|
rightnames = groups[rightname]
|
|
|
|
else:
|
|
|
|
rightnames = [rightname]
|
|
|
|
|
|
|
|
for lname in leftnames:
|
|
|
|
for rname in rightnames:
|
2017-08-28 20:31:42 +02:00
|
|
|
lnameId = nameToIdMap.get(lname)
|
|
|
|
rnameId = nameToIdMap.get(rname)
|
2019-05-27 02:08:05 +02:00
|
|
|
if lnameId is not None and rnameId is not None:
|
2017-08-28 20:31:42 +02:00
|
|
|
pairs.append([lnameId, rnameId, v])
|
2017-08-28 11:36:40 +02: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')
|
|
|
|
|
2023-09-27 02:09:24 +02:00
|
|
|
argparser.add_argument('jsonfile', metavar='<jsonfile>', type=str,
|
|
|
|
help='Output JSON file')
|
|
|
|
argparser.add_argument('htmlfile', metavar='<htmlfile>', type=str,
|
|
|
|
help='Path to HTML file to patch')
|
|
|
|
|
2017-08-28 11:36:40 +02:00
|
|
|
argparser.add_argument('glyphs', metavar='<glyphname>', type=str, nargs='*',
|
|
|
|
help='Only generate specific glyphs.')
|
|
|
|
|
|
|
|
|
|
|
|
args = argparser.parse_args()
|
2017-09-25 04:38:30 +02:00
|
|
|
srcDir = os.path.join(BASEDIR, 'src')
|
2018-09-04 01:59:55 +02:00
|
|
|
deleteNames = set(['.notdef', '.null'])
|
2017-09-25 04:38:30 +02:00
|
|
|
|
2017-08-28 11:36:40 +02:00
|
|
|
if len(args.scale):
|
|
|
|
scale = float(args.scale)
|
|
|
|
|
|
|
|
ufopath = args.ufopath.rstrip('/')
|
|
|
|
|
2018-09-17 19:05:45 +02:00
|
|
|
ufo = Font(ufopath)
|
|
|
|
effectiveAscender = max(ufo.info.ascender, ufo.info.unitsPerEm)
|
|
|
|
|
2019-05-27 02:08:05 +02:00
|
|
|
glyphnames = args.glyphs if len(args.glyphs) else ufo.keys()
|
|
|
|
glyphnameSet = set(glyphnames)
|
|
|
|
|
|
|
|
glyphnames = [gn for gn in glyphnames if gn not in deleteNames]
|
|
|
|
glyphnames.sort()
|
|
|
|
|
|
|
|
nameToIdMap, idToNameMap = genGlyphIDs(glyphnames)
|
|
|
|
|
|
|
|
print('generating kerning pair data')
|
|
|
|
kerning = genKerningInfo(ufo, glyphnames, nameToIdMap)
|
|
|
|
|
|
|
|
|
2018-09-17 19:05:45 +02:00
|
|
|
print('preprocessing glyphs')
|
|
|
|
filters = [
|
|
|
|
DecomposeComponentsFilter(),
|
|
|
|
RemoveOverlapsFilter(backend=RemoveOverlapsFilter.Backend.SKIA_PATHOPS),
|
|
|
|
]
|
|
|
|
glyphSet = {g.name: g for g in ufo}
|
|
|
|
for func in filters:
|
|
|
|
func(ufo, glyphSet)
|
|
|
|
|
|
|
|
print('generating SVGs and metrics data')
|
2017-08-28 11:36:40 +02:00
|
|
|
|
2018-09-17 19:05:45 +02:00
|
|
|
# print('\n'.join(ufo.keys()))
|
2017-08-28 11:36:40 +02:00
|
|
|
# sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
glyphMetrics = {}
|
|
|
|
|
|
|
|
# jsonLines = []
|
|
|
|
svgLines = []
|
|
|
|
for glyphname in glyphnames:
|
|
|
|
generateFrom = None
|
2018-09-04 01:59:55 +02:00
|
|
|
svg, metrics = genGlyph(glyphname)
|
2017-08-28 11:36:40 +02: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)
|
|
|
|
|
2018-09-04 01:59:55 +02:00
|
|
|
html = u''
|
2023-09-27 02:09:24 +02:00
|
|
|
with open(args.htmlfile, 'r', encoding="utf-8") as f:
|
2019-01-02 05:47:16 +01:00
|
|
|
html = f.read()
|
2017-08-28 11:36:40 +02:00
|
|
|
|
2018-09-04 01:59:55 +02:00
|
|
|
startMarker = u'<div id="svgs">'
|
2017-08-28 11:36:40 +02:00
|
|
|
startPos = html.find(startMarker)
|
|
|
|
|
2018-09-04 01:59:55 +02:00
|
|
|
endMarker = u'</div><!--END-SVGS'
|
2017-08-28 11:36:40 +02:00
|
|
|
endPos = html.find(endMarker, startPos + len(startMarker))
|
|
|
|
|
2023-09-27 02:09:24 +02:00
|
|
|
relfilename = os.path.relpath(args.htmlfile, os.getcwd())
|
2017-08-28 11:36:40 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
metaJson = '{\n'
|
|
|
|
metaJson += '"nameids":' + fmtJsonDict(idToNameMap) + ',\n'
|
|
|
|
metaJson += '"metrics":' + fmtJsonDict(glyphMetrics) + ',\n'
|
|
|
|
metaJson += '"kerning":' + fmtJsonList(kerning) + '\n'
|
|
|
|
metaJson += '}'
|
|
|
|
# metaHtml = '<script>var fontMetaData = ' + metaJson + ';</script>'
|
|
|
|
|
2019-01-02 05:47:16 +01:00
|
|
|
html = html[:startPos + len(startMarker)] + '\n' + svgtext + '\n' + html[endPos:]
|
2017-08-28 11:36:40 +02:00
|
|
|
|
|
|
|
print('write', relfilename)
|
2023-09-27 02:09:24 +02:00
|
|
|
with open(args.htmlfile, 'w', encoding="utf-8") as f:
|
2019-01-02 05:47:16 +01:00
|
|
|
f.write(html)
|
2017-08-28 11:36:40 +02:00
|
|
|
|
|
|
|
# JSON
|
2023-09-27 02:09:24 +02:00
|
|
|
jsonFilenameRel = os.path.relpath(args.jsonfile, os.getcwd())
|
2017-08-28 11:36:40 +02:00
|
|
|
print('write', jsonFilenameRel)
|
2023-09-27 02:09:24 +02:00
|
|
|
with open(args.jsonfile, 'w', encoding="utf-8") as f:
|
2017-08-28 11:36:40 +02:00
|
|
|
f.write(metaJson)
|
|
|
|
|
|
|
|
metaJson
|