1
1
mirror of https://github.com/rsms/inter.git synced 2024-11-23 11:43:47 +03:00

upgrade misc/tools/gen-metrics-and-svgs.py to new toolchain

This commit is contained in:
Rasmus Andersson 2018-09-03 16:59:55 -07:00
parent 11435926ba
commit 3099bc6495
2 changed files with 300 additions and 140 deletions

View File

@ -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
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 fontbuild.generateGlyph import generateGlyph
from ConfigParser import RawConfigParser
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')

264
misc/tools/svg.py Normal file
View File

@ -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()