text: #!/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
  
  