diff options
author | Rasmus Andersson <rasmus@notion.se> | 2018-09-03 22:55:49 +0300 |
---|---|---|
committer | Rasmus Andersson <rasmus@notion.se> | 2018-09-03 22:55:49 +0300 |
commit | c833e252c925e8dd68108660710ca835d95daa6f (patch) | |
tree | 6b2e28264ed45efd7f054e453b622098d0d875b8 /misc/tools/svgsync2.py | |
parent | 8c1a4c181ef12000179dfec541f1af87e9b03122 (diff) | |
download | inter-c833e252c925e8dd68108660710ca835d95daa6f.tar.xz |
Major overhaul, moving from UFO2 to Glyphs and UFO3, plus a brand new and much simpler fontbuild
Diffstat (limited to 'misc/tools/svgsync2.py')
-rwxr-xr-x | misc/tools/svgsync2.py | 626 |
1 files changed, 626 insertions, 0 deletions
diff --git a/misc/tools/svgsync2.py b/misc/tools/svgsync2.py new file mode 100755 index 000000000..992d6d314 --- /dev/null +++ b/misc/tools/svgsync2.py @@ -0,0 +1,626 @@ +#!/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 = ''' +<svg xmlns="http://www.w3.org/2000/svg" width="%(width)d" height="%(height)d" data-svgsync-hash="%(hash)s"> + <g id="%(name)s"> + <path d="%(d)s" transform="translate(0 %(effectiveAscender)d)" /> + <rect x="0" y="0" width="%(width)d" height="%(height)d" fill="" stroke="black" /> + </g> +</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 = {} + # <g id="Canvas" transform="translate(-3677 -24988)"> + # <g id="six 2"> + # <g id="six"> + # <g id="Vector"> + # <use xlink:href="#path0_fill" transform="translate(3886 25729)"/> + # ... + # <defs> + # <path id="path0_fill" ... + # + _findPathNodes(n, paths, defs, uses) + + # flatten uses & defs + for k in uses.keys(): + dfNode = defs.get(k) + if dfNode is not None: + v = uses[k] + v['targetNode'] = dfNode + if dfNode.nodeName == 'path': + useNode = v['useNode'] + useNode.parentNode.replaceChild(dfNode, useNode) + attrs = useNode.attributes + for k in attrs.keys(): + if k != 'xlink:href': + dfNode.setAttribute(k, attrs[k]) + paths.append(dfNode) + + else: + del defs[k] + + return paths + + +def nodeTranslation(path, x=0, y=0): + tr = path.getAttribute('transform') + if tr is not None: + if not isinstance(tr, basestring): + tr = tr.value + if len(tr) > 0: + m = re.match(r"translate\s*\(\s*(?P<x>[\-\d\.eE]+)[\s,]*(?P<y>[\-\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<a>[\-\d\.eE]+)[\s,]*(?P<b>[\-\d\.eE]+)[\s,]*(?P<c>[\-\d\.eE]+)[\s,]*(?P<d>[\-\d\.eE]+)[\s,]*(?P<e>[\-\d\.eE]+)[\s,]*(?P<f>[\-\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 <path> 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*<svg[^>]+data-svgsync-hash="([^"]*)".+') + m = r.match(svgCode) + if m is not None: + return m.group(1) + return None + + +def computeSVGHashFromSVG(g): + # h = sha256() + return 'abc123' + + +def encodePath(o, path): + o.write(path.d()) + + +def hashPaths(paths): + h = sha256() + for path in paths: + h.update(path.d()+';') + return b64encode(h.digest(), '-_') + + +def svgGetPaths(svgCode): + doc = xmlparseString(svgCode) + svg = doc.documentElement + paths = findPathNodes(svg) + isFigmaSVG = svgCode.find('Figma</desc>') != -1 + + if len(paths) == 0: + return paths, (0,0) + + paths2 = [] + for path in paths: + id = path.getAttribute('id') + if not isFigmaSVG or (id is None or id.find('stroke') == -1): + tr = nodeTranslation(path) + d = path.getAttribute('d') + paths2.append((d, tr)) + + return paths2, isFigmaSVG + + +def translatePath(path, trX, trY): + pass + + +def parseSVG(svgFile): + svgCode = None + with open(svgFile, 'r') as f: + svgCode = f.read() + + existingSvgHash = findSvgSyncHashInSVG(svgCode) + print 'hash in SVG file:', existingSvgHash + + svgPathDefs, isFigmaSVG = svgGetPaths(svgCode) + paths = [] + for pathDef, tr in svgPathDefs: + print 'pathDef:', pathDef, 'tr:', tr + path = parse_path(pathDef) + if tr[0] != 0 or tr[1] != 0: + path = path.translated(vec2(*tr)) + paths.append(path) + + return paths, existingSvgHash + + +def syncGlyphUFOToSVG(g, glyphFile, svgFile, mtime, hasSvgFile): + # # Let's print out the first path object and the color it was in the SVG + # # We'll see it is composed of two CubicBezier objects and, in the SVG file it + # # came from, it was red + # paths, attributes, svg_attributes = svg2paths(svgFile, return_svg_attributes=True) + # print('svg_attributes:', repr(svg_attributes)) + # # redpath = paths[0] + # # redpath_attribs = attributes[0] + # print(paths) + # print(attributes) + # wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename=svgFile + '-x.svg') + + # existingSVGHash = readSVGHash(svgFile) + svgPaths = None + existingSVGHash = None + if hasSvgFile: + svgPaths, existingSVGHash = parseSVG(svgFile) + print 'existingSVGHash:', existingSVGHash + print 'svgPaths:\n', '\n'.join([p.d() for p in svgPaths]) + svgHash = hashPaths(svgPaths) + print 'hash(SVG-glyph) =>', svgHash + + # computedSVGHash = computeSVGHashFromSVG(svgFile) + # print 'computeSVGHashFromSVG:', computedSVGHash + + ufoPaths = glyphToPaths(g) + print 'ufoPaths:\n', '\n'.join([p.d() for p in ufoPaths]) + ufoGlyphHash = hashPaths(ufoPaths) + print 'hash(UFO-glyph) =>', ufoGlyphHash + + # svg = glyphToSVG(g, ufoGlyphHash) + + # with open('/Users/rsms/src/interface/_local/svgPaths.txt', 'w') as f: + # f.write(svgPaths[0].d()) + # with open('/Users/rsms/src/interface/_local/ufoPaths.txt', 'w') as f: + # f.write(ufoPaths[0].d()) + # print svgPaths[0].d() == ufoPaths[0].d() + + # svgHash = hashPaths() + # print 'hash(UFO-glyph) =>', pathHash + + sys.exit(1) + if pathHash == existingSVGHash: + return (None, 0) # did not change + + svg = glyphToSVG(g, pathHash) + sys.exit(1) + + writeFileAndMkDirsIfNeeded(svgFile, svg) + os.utime(svgFile, (mtime, mtime)) + print 'svgsync write', svgFile + + g.lib['svgsync.hash'] = pathHash + return (glyphFile, mtime) + + +def syncGlyphSVGToUFO(glyphname, svgFile): + print glyphname + ': SVG -> UFO' + sys.exit(1) + 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, createSVG=False): # => (glyphname, mtime) or (None, 0) if noop + 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 + ) + + g = font.getGlyph(glyphname) + ufoPathHash = g.lib['svgsync.hash'] if 'svgsync.hash' in g.lib else None + print '[syncGlyph] g.lib["svgsync.hash"] =', ufoPathHash + + c = 1 # XXX DEBUG + + if c < 0: + syncGlyphSVGToUFO(glyphname, svgFile) + return (glyphFile, svgStat.st_mtime) # glif file in UFO change + it's new mtime + elif c > 0 and (svgStat is not None or createSVG): + print glyphname + ': UFO -> SVG' + return syncGlyphUFOToSVG( + g, + glyphFile, + svgFile, + glyphStat.st_mtime, + hasSvgFile=svgStat is not None + ) + + return (None, 0) # UFO did not change + + +# ———————————————————————————————————————————————————————————————————————— +# main + +argparser = argparse.ArgumentParser(description='Convert UFO glyphs to SVG') + +argparser.add_argument('--svgdir', dest='svgdir', metavar='<dir>', type=str, + default='', + help='Write SVG files to <dir>. If not specified, SVG files are' + + ' written to: {dirname(<ufopath>)/svg/<familyname>/<style>') + +argparser.add_argument('ufopath', metavar='<ufopath>', type=str, + help='Path to UFO packages') + +argparser.add_argument('glyphs', metavar='<glyphname>', type=str, nargs='*', + help='Glyphs to convert. Converts all if none specified.') + +args = argparser.parse_args() + +ufopath = args.ufopath.rstrip('/') + +font = OpenFont(ufopath) +effectiveAscender = max(font.info.ascender, font.info.unitsPerEm) + +svgdir = args.svgdir +if len(svgdir) == 0: + svgdir = os.path.join( + os.path.dirname(ufopath), + 'svg', + font.info.familyName, + font.info.styleName + ) + +print 'svgsync sync %s (%s)' % (font.info.familyName, font.info.styleName) + +createSVGs = len(args.glyphs) > 0 +glyphnames = args.glyphs if len(args.glyphs) else font.keys() + +modifiedGlifFiles = [] +for glyphname in glyphnames: + glyphFile, mtime = syncGlyph(glyphname, createSVG=createSVGs) + if glyphFile is not None: + modifiedGlifFiles.append((glyphFile, mtime)) + +if len(modifiedGlifFiles) > 0: + font.save() + for glyphFile, mtime in modifiedGlifFiles: + os.utime(glyphFile, (mtime, mtime)) + print 'svgsync write', glyphFile + |