summaryrefslogtreecommitdiff
path: root/misc
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2018-09-04 02:59:55 +0300
committerRasmus Andersson <rasmus@notion.se>2018-09-04 02:59:55 +0300
commit3099bc6495489489feca8f28d2132c38ced8776a (patch)
treefe8e8484c24d8ff15bfec5ed30b67d6453bcc2ef /misc
parent11435926ba33c024670acbfdc7bb35d1991ef614 (diff)
downloadinter-3099bc6495489489feca8f28d2132c38ced8776a.tar.xz
upgrade misc/tools/gen-metrics-and-svgs.py to new toolchain
Diffstat (limited to 'misc')
-rwxr-xr-xmisc/tools/gen-metrics-and-svgs.py176
-rw-r--r--misc/tools/svg.py264
2 files changed, 300 insertions, 140 deletions
diff --git a/misc/tools/gen-metrics-and-svgs.py b/misc/tools/gen-metrics-and-svgs.py
index ac100eb1c..109dca4d9 100755
--- a/misc/tools/gen-metrics-and-svgs.py
+++ b/misc/tools/gen-metrics-and-svgs.py
@@ -4,67 +4,32 @@
# Sync glyph shapes between SVG and UFO, creating a bridge between UFO and Figma.
#
from __future__ import print_function
-import os, sys, argparse, re, json, plistlib
-from math import ceil, floor
-from robofab.objects.objectsRF import OpenFont
-from collections import OrderedDict
-from fontbuild.generateGlyph import generateGlyph
-from ConfigParser import RawConfigParser
+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
-BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
font = None # RFont
ufopath = ''
effectiveAscender = 0
scale = 0.1
-agl = None
def num(s):
return int(s) if s.find('.') == -1 else float(s)
-def parseGlyphComposition(composite):
- c = composite.split("=")
- d = c[1].split("/")
- glyphName = d[0]
- if len(d) == 1:
- offset = [0, 0]
- else:
- offset = [int(i) for i in d[1].split(",")]
- accentString = c[0]
- accents = accentString.split("+")
- baseName = accents.pop(0)
- accentNames = [i.split(":") for i in accents]
- return (glyphName, baseName, accentNames, offset)
-
-
-def loadGlyphCompositions(filename): # { glyphName => (baseName, accentNames, offset, rawline) }
- compositions = OrderedDict()
- with open(filename, 'r') as f:
- for line in f:
- line = line.strip()
- if len(line) > 0 and line[0] != '#':
- glyphName, baseName, accentNames, offset = parseGlyphComposition(line)
- compositions[glyphName] = (baseName, accentNames, offset, line)
- return compositions
-
-
-def loadAGL(filename): # -> { 2126: 'Omega', ... }
- m = {}
- with open(filename, 'r') as f:
- for line in f:
- # Omega;2126
- # dalethatafpatah;05D3 05B2 # higher-level combinations; ignored
- line = line.strip()
- if len(line) > 0 and line[0] != '#':
- name, uc = tuple([c.strip() for c in line.split(';')])
- if uc.find(' ') == -1:
- # it's a 1:1 mapping
- m[int(uc, 16)] = name
- return m
-
def decomposeGlyph(font, glyph):
"""Moves the components of a glyph to its outline."""
if len(glyph.components):
@@ -92,55 +57,19 @@ def deepCopyContours(font, parent, component, offset, scale):
def glyphToSVGPath(g, yMul):
- commands = {'move':'M','line':'L','curve':'Y','offcurve':'X','offCurve':'X'}
- svg = ''
- contours = []
-
- if len(g.components):
- decomposeGlyph(g.getParent(), g) # mutates g
-
- 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 = ' %g %g' % (p.x * scale, (p.y * yMul) * scale)
- contour += ' %s%g %g' % (command, p.x * scale, (p.y * yMul) * scale)
- svg += ' ' + contour + end + 'z'
-
- if font.has_key('__svgsync'):
- font.removeGlyph('__svgsync')
- return svg.strip()
+ pen = SVGPathPen(g.getParent(), yMul)
+ g.draw(pen)
+ return pen.getCommands()
def svgWidth(g):
- box = g.box
- xoffs = box[0]
- width = box[2] - box[0]
- return width, xoffs
+ 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
def glyphToSVG(g):
@@ -148,7 +77,7 @@ def glyphToSVG(g):
svg = '''
<svg id="svg-%(name)s" xmlns="http://www.w3.org/2000/svg" width="%(width)d" height="%(height)d">
-<path d="%(glyphSVGPath)s" transform="translate(%(xoffs)g %(yoffs)g)"/>
+<path d="%(glyphSVGPath)s" transform="translate(%(xoffs)g %(yoffs)g) scale(%(scale)g)"/>
</svg>
''' % {
'name': g.name,
@@ -163,6 +92,7 @@ def glyphToSVG(g):
# 'descender': font.info.descender * scale,
# 'baselineOffset': (font.info.unitsPerEm + font.info.descender) * scale,
# 'unitsPerEm': font.info.unitsPerEm,
+ 'scale': scale,
# 'margin': [g.leftMargin * scale, g.rightMargin * scale],
}
@@ -242,13 +172,8 @@ def findGlifFile(glyphname):
usedSVGNames = set()
-def genGlyph(glyphName, generateFrom, force):
- # generateFrom = (baseName, accentNames, offset, rawline)
- if generateFrom is not None:
- generateGlyph(font, generateFrom[3], agl)
-
- g = font.getGlyph(glyphName)
-
+def genGlyph(glyphName):
+ g = font[glyphName]
return glyphToSVG(g)
@@ -328,10 +253,6 @@ 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(
- '-f', '-force', dest='force', action='store_const', const=True, default=False,
- help='Generate glyphs even though they appear to be up-to date.')
-
argparser.add_argument('ufopath', metavar='<ufopath>', type=str,
help='Path to UFO packages')
@@ -340,44 +261,25 @@ argparser.add_argument('glyphs', metavar='<glyphname>', type=str, nargs='*',
args = argparser.parse_args()
-
srcDir = os.path.join(BASEDIR, 'src')
-
-# load fontbuild config
-config = RawConfigParser(dict_type=OrderedDict)
-configFilename = os.path.join(srcDir, 'fontbuild.cfg')
-config.read(configFilename)
-deleteNames = set()
-for sectionName, value in config.items('glyphs'):
- if sectionName == 'delete':
- deleteNames = set(value.split())
+deleteNames = set(['.notdef', '.null'])
if len(args.scale):
scale = float(args.scale)
ufopath = args.ufopath.rstrip('/')
-font = OpenFont(ufopath)
+font = Font(ufopath)
effectiveAscender = max(font.info.ascender, font.info.unitsPerEm)
# print('\n'.join(font.keys()))
# sys.exit(0)
-agl = loadAGL(os.path.join(srcDir, 'glyphlist.txt')) # { 2126: 'Omega', ... }
-
deleteNames.add('.notdef')
deleteNames.add('.null')
glyphnames = args.glyphs if len(args.glyphs) else font.keys()
glyphnameSet = set(glyphnames)
-generatedGlyphNames = set()
-
-diacriticComps = loadGlyphCompositions(os.path.join(srcDir, 'diacritics.txt'))
-for glyphName, comp in diacriticComps.iteritems():
- if glyphName not in glyphnameSet:
- generatedGlyphNames.add(glyphName)
- glyphnames.append(glyphName)
- glyphnameSet.add(glyphName)
glyphnames = [gn for gn in glyphnames if gn not in deleteNames]
glyphnames.sort()
@@ -390,9 +292,7 @@ glyphMetrics = {}
svgLines = []
for glyphname in glyphnames:
generateFrom = None
- if glyphname in generatedGlyphNames:
- generateFrom = diacriticComps[glyphname]
- svg, metrics = genGlyph(glyphname, generateFrom, force=args.force)
+ svg, metrics = genGlyph(glyphname)
# metrics: (width, advance, left, right)
glyphMetrics[nameToIdMap[glyphname]] = metrics
svgLines.append(svg.replace('\n', ''))
@@ -404,14 +304,14 @@ svgtext = '\n'.join(svgLines)
glyphsHtmlFilename = os.path.join(BASEDIR, 'docs', 'glyphs', 'index.html')
-html = ''
+html = u''
with open(glyphsHtmlFilename, 'r') as f:
- html = f.read()
+ html = f.read().decode('utf8')
-startMarker = '<div id="svgs">'
+startMarker = u'<div id="svgs">'
startPos = html.find(startMarker)
-endMarker = '</div><!--END-SVGS'
+endMarker = u'</div><!--END-SVGS'
endPos = html.find(endMarker, startPos + len(startMarker))
relfilename = os.path.relpath(glyphsHtmlFilename, os.getcwd())
@@ -421,10 +321,6 @@ if startPos == -1 or endPos == -1:
print(msg % relfilename, file=sys.stderr)
sys.exit(1)
-for name in glyphnames:
- if name == 'zero.tnum.slash':
- print('FOUND zero.tnum.slash')
-
kerning = genKerningInfo(font, glyphnames, nameToIdMap)
metaJson = '{\n'
metaJson += '"nameids":' + fmtJsonDict(idToNameMap) + ',\n'
@@ -433,11 +329,11 @@ metaJson += '"kerning":' + fmtJsonList(kerning) + '\n'
metaJson += '}'
# metaHtml = '<script>var fontMetaData = ' + metaJson + ';</script>'
-html = html[:startPos + len(startMarker)] + '\n' + svgtext + '\n' + html[endPos:]
+html = html[:startPos + len(startMarker)] + '\n' + svgtext.decode('utf8') + '\n' + html[endPos:]
print('write', relfilename)
with open(glyphsHtmlFilename, 'w') as f:
- f.write(html)
+ f.write(html.encode('utf8'))
# JSON
jsonFilename = os.path.join(BASEDIR, 'docs', 'glyphs', 'metrics.json')
diff --git a/misc/tools/svg.py b/misc/tools/svg.py
new file mode 100644
index 000000000..e09eed7c1
--- /dev/null
+++ b/misc/tools/svg.py
@@ -0,0 +1,264 @@
+# encoding: utf8
+#
+# The MIT License
+#
+# Copyright (c) 2010 Type Supply LLC
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# https://github.com/typesupply/ufo2svg
+#
+from __future__ import absolute_import
+from fontTools.pens.basePen import BasePen
+
+
+def valueToString(v):
+ """
+ >>> valueToString(0)
+ '0'
+ >>> valueToString(10)
+ '10'
+ >>> valueToString(-10)
+ '-10'
+ >>> valueToString(0.1)
+ '0.1'
+ >>> valueToString(0.0001)
+ '0.0001'
+ >>> valueToString(0.00001)
+ '0'
+ >>> valueToString(10.0001)
+ '10.0001'
+ >>> valueToString(10.00001)
+ '10'
+ """
+ if int(v) == v:
+ v = "%d" % (int(v))
+ else:
+ v = "%.4f" % v
+ # strip unnecessary zeros
+ # there is probably an easier way to do this
+ compiled = []
+ for c in reversed(v):
+ if not compiled and c == "0":
+ continue
+ compiled.append(c)
+ v = "".join(reversed(compiled))
+ if v.endswith("."):
+ v = v[:-1]
+ if not v:
+ v = "0"
+ return v
+
+
+def pointToString(pt):
+ # return " ".join([valueToString(i) for i in pt])
+ return valueToString(pt[0]) + " " + valueToString(pt[1])
+
+
+class SVGPathPen(BasePen):
+
+ def __init__(self, glyphSet, yMul):
+ BasePen.__init__(self, glyphSet)
+ self._commands = []
+ self._lastCommand = None
+ self._lastX = None
+ self._lastY = None
+ self._yMul = yMul
+
+ # def pointToString(self, pt):
+ # pts = []
+ # n = 0
+ # for i in pt:
+ # if n == 1:
+ # i = i * self._yMul
+ # pts.append(valueToString(i))
+ # n = n + 1
+ # return " ".join(pts)
+
+
+ def pt(self, pt1):
+ return (pt1[0], pt1[1] * self._yMul)
+
+
+ def _handleAnchor(self):
+ """
+ >>> pen = SVGPathPen(None)
+ >>> pen.moveTo((0, 0))
+ >>> pen.moveTo((10, 10))
+ >>> pen._commands
+ ['M10 10']
+ """
+ if self._lastCommand == "M":
+ self._commands.pop(-1)
+
+ def _moveTo(self, pt):
+ """
+ >>> pen = SVGPathPen(None)
+ >>> pen.moveTo((0, 0))
+ >>> pen._commands
+ ['M0 0']
+
+ >>> pen = SVGPathPen(None)
+ >>> pen.moveTo((10, 0))
+ >>> pen._commands
+ ['M10 0']
+
+ >>> pen = SVGPathPen(None)
+ >>> pen.moveTo((0, 10))
+ >>> pen._commands
+ ['M0 10']
+ """
+ pt = self.pt(pt)
+ self._handleAnchor()
+ t = "M%s" % (pointToString(pt))
+ self._commands.append(t)
+ self._lastCommand = "M"
+ self._lastX, self._lastY = pt
+
+ def _lineTo(self, pt):
+ """
+ # duplicate point
+ >>> pen = SVGPathPen(None)
+ >>> pen.moveTo((10, 10))
+ >>> pen.lineTo((10, 10))
+ >>> pen._commands
+ ['M10 10']
+
+ # vertical line
+ >>> pen = SVGPathPen(None)
+ >>> pen.moveTo((10, 10))
+ >>> pen.lineTo((10, 0))
+ >>> pen._commands
+ ['M10 10', 'V0']
+
+ # horizontal line
+ >>> pen = SVGPathPen(None)
+ >>> pen.moveTo((10, 10))
+ >>> pen.lineTo((0, 10))
+ >>> pen._commands
+ ['M10 10', 'H0']
+
+ # basic
+ >>> pen = SVGPathPen(None)
+ >>> pen.lineTo((70, 80))
+ >>> pen._commands
+ ['L70 80']
+
+ # basic following a moveto
+ >>> pen = SVGPathPen(None)
+ >>> pen.moveTo((0, 0))
+ >>> pen.lineTo((10, 10))
+ >>> pen._commands
+ ['M0 0', ' 10 10']
+ """
+ pt = self.pt(pt)
+ x, y = pt
+ # duplicate point
+ if x == self._lastX and y == self._lastY:
+ return
+ # vertical line
+ elif x == self._lastX:
+ cmd = "V"
+ pts = valueToString(y)
+ # horizontal line
+ elif y == self._lastY:
+ cmd = "H"
+ pts = valueToString(x)
+ # previous was a moveto
+ elif self._lastCommand == "M":
+ cmd = None
+ pts = " " + pointToString(pt)
+ # basic
+ else:
+ cmd = "L"
+ pts = pointToString(pt)
+ # write the string
+ t = ""
+ if cmd:
+ t += cmd
+ self._lastCommand = cmd
+ t += pts
+ self._commands.append(t)
+ # store for future reference
+ self._lastX, self._lastY = pt
+
+ def _curveToOne(self, pt1, pt2, pt3):
+ """
+ >>> pen = SVGPathPen(None)
+ >>> pen.curveTo((10, 20), (30, 40), (50, 60))
+ >>> pen._commands
+ ['C10 20 30 40 50 60']
+ """
+ pt1 = self.pt(pt1)
+ pt2 = self.pt(pt2)
+ pt3 = self.pt(pt3)
+ t = "C"
+ t += pointToString(pt1) + " "
+ t += pointToString(pt2) + " "
+ t += pointToString(pt3)
+ self._commands.append(t)
+ self._lastCommand = "C"
+ self._lastX, self._lastY = pt3
+
+ def _qCurveToOne(self, pt1, pt2):
+ """
+ >>> pen = SVGPathPen(None)
+ >>> pen.qCurveTo((10, 20), (30, 40))
+ >>> pen._commands
+ ['Q10 20 30 40']
+ """
+ assert pt2 is not None
+ pt1 = self.pt(pt1)
+ pt2 = self.pt(pt2)
+ t = "Q"
+ t += pointToString(pt1) + " "
+ t += pointToString(pt2)
+ self._commands.append(t)
+ self._lastCommand = "Q"
+ self._lastX, self._lastY = pt2
+
+ def _closePath(self):
+ """
+ >>> pen = SVGPathPen(None)
+ >>> pen.closePath()
+ >>> pen._commands
+ ['Z']
+ """
+ self._commands.append("Z")
+ self._lastCommand = "Z"
+ self._lastX = self._lastY = None
+
+ def _endPath(self):
+ """
+ >>> pen = SVGPathPen(None)
+ >>> pen.endPath()
+ >>> pen._commands
+ ['Z']
+ """
+ self._closePath()
+ self._lastCommand = None
+ self._lastX = self._lastY = None
+
+ def getCommands(self):
+ return "".join(self._commands)
+
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()