#!/usr/bin/env python
# encoding: utf8
#
# Sync glyph shapes between SVG and UFO, creating a bridge between UFO and Figma.
#
import os
import sys
import argparse
import re
from StringIO import StringIO
from hashlib import sha256
from xml.dom.minidom import parseString as xmlparseString
from svgpathtools import svg2paths, parse_path, Path, Line, CubicBezier
from base64 import b64encode
# from robofab.world import world, RFont, RGlyph, OpenFont, NewFont
from robofab.objects.objectsRF import RFont, RGlyph, OpenFont, NewFont, RContour
from robofab.objects.objectsBase import MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE
font = None # RFont
ufopath = ''
svgdir = ''
effectiveAscender = 0
def num(s):
return int(s) if s.find('.') == -1 else float(s)
def glyphToSVGPath(g, yMul=-1):
commands = {'move':'M','line':'L','curve':'Y','offcurve':'X','offCurve':'X'}
svg = ''
contours = []
if len(g.components):
font.newGlyph('__svgsync')
new = font['__svgsync']
new.width = g.width
new.appendGlyph(g)
new.decompose()
g = new
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 = ' ' + str(p.x) + ' ' + str(p.y * yMul)
contour += ' ' + command + str(p.x) + ' ' + str(p.y * yMul)
svg += ' ' + contour + end + 'z'
if font.has_key('__svgsync'):
font.removeGlyph('__svgsync')
return svg.strip()
def vec2(x, y):
return float(x) + float(y) * 1j
def glyphToPaths(g, yMul=-1):
paths = []
contours = []
yOffs = -font.info.unitsPerEm
# decompose components
if len(g.components):
font.newGlyph('__svgsync')
ng = font['__svgsync']
ng.width = g.width
ng.appendGlyph(g)
ng.decompose()
g = ng
for c in g:
curve = False
points = c.points
path = Path()
currentPos = 0j
controlPoints = []
for x in range(len(points)):
p = points[x]
# print 'p#' + str(x) + '.type = ' + repr(p.type)
if p.type == 'move':
currentPos = vec2(p.x, (p.y + yOffs) * yMul)
elif p.type == 'offcurve':
controlPoints.append(p)
elif p.type == 'curve':
pos = vec2(p.x, (p.y + yOffs) * yMul)
if len(controlPoints) == 2:
cp1, cp2 = controlPoints
path.append(CubicBezier(
currentPos,
vec2(cp1.x, (cp1.y + yOffs) * yMul),
vec2(cp2.x, (cp2.y + yOffs) * yMul),
pos))
else:
if len(controlPoints) != 1:
raise Exception('unexpected number of control points for curve')
cp = controlPoints[0]
path.append(QuadraticBezier(currentPos, vec2(cp.x, (cp.y + yOffs) * yMul), pos))
currentPos = pos
controlPoints = []
elif p.type == 'line':
pos = vec2(p.x, (p.y + yOffs) * yMul)
path.append(Line(currentPos, pos))
currentPos = pos
paths.append(path)
if font.has_key('__svgsync'):
font.removeGlyph('__svgsync')
return paths
def maybeAddMove(contour, x, y, smooth):
if len(contour.segments) == 0:
contour.appendSegment(MOVE, [(x, y)], smooth=smooth)
svgPathDataRegEx = re.compile(r'(?:([A-Z])\s*|)([0-9\.\-\+eE]+)')
def drawSVGPath(g, d, tr):
yMul = -1
xOffs = tr[0]
yOffs = -(font.info.unitsPerEm - tr[1])
for pathd in d.split('M'):
pathd = pathd.strip()
# print 'pathd', pathd
if len(pathd) == 0:
continue
i = 0
closePath = False
if pathd[-1] == 'z':
closePath = True
pathd = pathd[0:-1]
pv = []
for m in svgPathDataRegEx.finditer('M' + pathd):
if m.group(1) is not None:
pv.append(m.group(1) + m.group(2))
else:
pv.append(m.group(2))
initX = 0
initY = 0
pen = g.getPen()
while i < len(pv):
pd = pv[i]; i += 1
cmd = pd[0]
x = num(pd[1:]) + xOffs
y = (num(pv[i]) + yOffs) * yMul; i += 1
if cmd == 'M':
# print cmd, x, y, '/', num(pv[i-2][1:])
initX = x
initY = y
pen.moveTo((x, y))
continue
if cmd == 'C':
# Bezier curve: "C x1 y1, x2 y2, x y"
x1 = x
y1 = y
x2 = num(pv[i]) + xOffs; i += 1
y2 = (num(pv[i]) + yOffs) * yMul; i += 1
x = num(pv[i]) + xOffs; i += 1
y = (num(pv[i]) + yOffs) * yMul; i += 1
pen.curveTo((x1, y1), (x2, y2), (x, y))
# print cmd, x1, y1, x2, y2, x, y
elif cmd == 'L':
pen.lineTo((x, y))
else:
raise Exception('unexpected SVG path command %r' % cmd)
if closePath:
pen.closePath()
else:
pen.endPath()
# print 'path ended. closePath:', closePath
def glyphToSVG(g, path, hash):
width = g.width
height = font.info.unitsPerEm
d = {
'name': g.name,
'width': width,
'height': effectiveAscender - font.info.descender,
'effectiveAscender': effectiveAscender,
'leftMargin': g.leftMargin,
'rightMargin': g.rightMargin,
'd': path.d(use_closed_attrib=True),
'ascender': font.info.ascender,
'descender': font.info.descender,
'baselineOffset': height + font.info.descender,
'unitsPerEm': font.info.unitsPerEm,
'hash': hash,
}
svg = '''
''' % d
# print svg
return svg.strip()
def _findPathNodes(n, paths, defs, uses, isDef=False):
for cn in n.childNodes:
if cn.nodeName == 'path':
if isDef:
defs[cn.getAttribute('id')] = cn
else:
paths.append(cn)
elif cn.nodeName == 'use':
uses[cn.getAttribute('xlink:href').lstrip('#')] = {'useNode': cn, 'targetNode': None}
elif cn.nodeName == 'defs':
_findPathNodes(cn, paths, defs, uses, isDef=True)
elif not isinstance(cn, basestring) and cn.childNodes and len(cn.childNodes) > 0:
_findPathNodes(cn, paths, defs, uses, isDef)
# return translate
def findPathNodes(n, isDef=False):
paths = []
defs = {}
uses = {}
#
#
#
#
#
# ...
#
# 0:
m = re.match(r"translate\s*\(\s*(?P[\-\d\.eE]+)[\s,]*(?P[\-\d\.eE]+)\s*\)", tr)
if m is not None:
x += num(m.group('x'))
y += num(m.group('y'))
else:
raise Exception('Unable to handle transform="%s"' % tr)
# m = re.match(r"matrix\s*\(\s*(?P[\-\d\.eE]+)[\s,]*(?P[\-\d\.eE]+)[\s,]*(?P[\-\d\.eE]+)[\s,]*(?P[\-\d\.eE]+)[\s,]*(?P[\-\d\.eE]+)[\s,]*(?P[\-\d\.eE]+)[\s,]*", tr)
# if m is not None:
# a, b, c = num(m.group('a')), num(m.group('b')), num(m.group('c'))
# d, e, f = num(m.group('d')), num(m.group('e')), num(m.group('f'))
# # matrix -1 0 0 -1 -660.719 31947
# print 'matrix', a, b, c, d, e, f
# # matrix(-1 0 -0 -1 -2553 31943)
pn = path.parentNode
if pn is not None and pn.nodeName != '#document':
x, y = nodeTranslation(pn, x, y)
return (x, y)
def glyphUpdateFromSVG(g, svgCode):
doc = xmlparseString(svgCode)
svg = doc.documentElement
paths = findPathNodes(svg)
if len(paths) == 0:
raise Exception('no found in SVG')
path = paths[0]
if len(paths) != 1:
for p in paths:
id = p.getAttribute('id')
if id is not None and id.find('stroke') == -1:
path = p
break
tr = nodeTranslation(path)
d = path.getAttribute('d')
g.clearContours()
drawSVGPath(g, d, tr)
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 findSvgSyncHashInSVG(svgCode):
# with open(svgFile, 'r') as f:
# svgCode = f.readline(512)
r = re.compile(r'^\s*