#!/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 xml.dom.minidom import parseString as xmlparseString # 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): 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 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): 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, 'glyphSVGPath': glyphToSVGPath(g, -1), 'ascender': font.info.ascender, 'descender': font.info.descender, 'baselineOffset': height + font.info.descender, 'unitsPerEm': font.info.unitsPerEm, } # for kv in d.iteritems(): # if kv[0] == 'glyphSVGPath': # print ' %s: ...' % kv[0] # else: # print ' %s: %r' % kv 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 syncGlyphUFOToSVG(glyphname, svgFile, mtime): print glyphname + ': UFO -> SVG' g = font.getGlyph(glyphname) svg = glyphToSVG(g) writeFileAndMkDirsIfNeeded(svgFile, svg) os.utime(svgFile, (mtime, mtime)) print 'write', svgFile def syncGlyphSVGToUFO(glyphname, svgFile): print glyphname + ': SVG -> UFO' svg = '' with open(svgFile, 'r') as f: svg = f.read() g = font.getGlyph(glyphname) glyphUpdateFromSVG(g, svg) 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) def syncGlyph(glyphname): glyphFile, glyphStat = findGlifFile(glyphname) svgFile = os.path.join(svgdir, glyphname + '.svg') svgStat = stat(svgFile) if glyphStat is None and svgStat is None: raise Exception("glyph %r doesn't exist in UFO or SVG directory" % glyphname) c = cmp( 0 if glyphStat is None else glyphStat.st_mtime, 0 if svgStat is None else svgStat.st_mtime ) if c < 0: syncGlyphSVGToUFO(glyphname, svgFile) return (glyphFile, svgStat.st_mtime) # glif file in UFO change + it's new mtime elif c > 0: syncGlyphUFOToSVG(glyphname, svgFile, glyphStat.st_mtime) # else: # print glyphname + ': up to date' return (None, 0) # UFO did not change # ———————————————————————————————————————————————————————————————————————— # main argparser = argparse.ArgumentParser(description='Convert UFO glyphs to SVG') argparser.add_argument('--svgdir', dest='svgdir', metavar='', type=str, default='', help='Write SVG files to . If not specified, SVG files are' + ' written to: {dirname()/svg//