-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathnewfontbuild.py
More file actions
381 lines (332 loc) · 12.9 KB
/
Copy pathnewfontbuild.py
File metadata and controls
381 lines (332 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
from fontTools.fontBuilder import FontBuilder
from fontTools.svgLib import SVGPath
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.t2CharStringPen import T2CharStringPen
from fontTools.pens.transformPen import TransformPen
from fontTools.misc.transform import Scale
from picosvg.svg import SVG as picoSVG
from natsort import os_sorted
from types import SimpleNamespace
import string
import os
import csv
import re
import pathlib
from lxml import etree as ET
# draw a .notdef glyph of 1000x1000
def drawNotdefGlyph(type="bezier", upm=1000, ascender=880):
if type == "quad":
ori_pen = TTGlyphPen(None)
else:
ori_pen = T2CharStringPen(1000, None)
t = Scale(upm / 1000).translate(0, ascender * 1000 / upm - 880)
pen = TransformPen(ori_pen, t)
pen.moveTo((100, -120))
pen.lineTo((900, -120))
pen.lineTo((900, 880))
pen.lineTo((100, 880))
pen.lineTo((100, -120))
pen.closePath()
pen.moveTo((150, -29))
pen.lineTo((150, 789))
pen.lineTo((468, 380))
pen.lineTo((150, -29))
pen.closePath()
pen.moveTo((182, -70))
pen.lineTo((500, 339))
pen.lineTo((818, -70))
pen.lineTo((182, -70))
pen.closePath()
pen.moveTo((532, 380))
pen.lineTo((850, 789))
pen.lineTo((850, -29))
pen.lineTo((532, 380))
pen.closePath()
pen.moveTo((500, 421))
pen.lineTo((182, 830))
pen.lineTo((817, 830))
pen.lineTo((500, 421))
pen.closePath()
if type == "quad":
return ori_pen.glyph()
else:
return ori_pen.getCharString()
def getOutlineFromSVG(svg_fulltext, upm: int, ascender: int, picoNormalize):
"""
Input a svg string, and reads the glyph into a T2 glyph
"""
# # if normalize, convert to picoSVG to spec in place
# if picoNormalize:
# # read SVG into string
# svg_fullstring = open(svg_fullpath, "r", encoding="utf-8-sig").read()
# # parse SVG
# picosvg = picoSVG.fromstring(svg_fullstring)
# picosvg.topicosvg(inplace=True, allow_text=True)
# # convert picosvg into svgpen
# svgpen = SVGPath(None)
# svgpen.root = picosvg.svg_root
# # get viewbox
# xori, yori, width, height = picosvg.view_box()
# else:
# svgpen = SVGPath(filename=svg_fullpath)
# # get viewbox
# viewBox_string = svgpen.root.attrib["viewBox"].split(" ")
# xori, yori, width, height = [float(x) for x in viewBox_string]
# parse SVG
picosvg = picoSVG.fromstring(svg_fulltext)
# if normalize, convert to picoSVG to spec in place
if picoNormalize:
# remove image and style
# image not supported
# style not supported for now: https://github.com/googlefonts/picosvg/issues/216
picoerrors = picosvg.checkpicosvg()
if picoerrors:
for i in picoerrors:
# skip if not image and style, prefilter
if not re.search(r"/style\[\d+\]$", i) and not re.search(r"/image\[\d+\]$", i):
continue
# remove style and image elements
for context in picosvg.breadth_first():
if re.search(r"/style\[\d+\]$", context.path) or re.search(r"/image\[\d+\]$", context.path):
context.element.getparent().remove(context.element)
break # only need remove once
picosvg.topicosvg(inplace=True, allow_text=True)
# convert picosvg to svgpath pen
svgpen = SVGPath(None)
# carry svg etree root over
svgpen.root = picosvg.svg_root
# get viewbox
xori, yori, width, height = picosvg.view_box()
scale = upm / height
# prepare transformation
move_height = ascender - (yori * -scale)
t = Scale(scale, -scale).translate(-xori / -scale, move_height / -scale)
# calculate glyph advance with x coord
glyphadv = t.transformPoint((width, 0))[0]
# new pen
# glyphpen = TTGlyphPen(None)
glyphpen = T2CharStringPen(glyphadv, None)
# wrap pen with transform pen
transformPen = TransformPen(glyphpen, t)
# draw glyph
svgpen.draw(transformPen)
# glyph = glyphpen.glyph()
glyph = glyphpen.getCharString()
return (glyph, glyphadv)
def uniName(char):
num = ord(char[0])
if num in aglfn_namelist.values():
name = [key for key, val in aglfn_namelist.items() if val == num][0] # search dict, use first name
elif num < 65536: # bmp character
name = "uni" + hex(num).upper()[2:]
else:
name = "u" + hex(num).upper()[2:]
return (name, num)
def uses_other_chars(s):
return not set(s) <= aglfn_allowedchar
# https://github.com/adobe-type-tools/agl-specification Section 2
def getUniFromAdobeName(name):
"""
Return a tuple of valid Adobe glyph name based on filename, and its corresponding Unicode codepoint. Return None if filename is not valid.
If a single Unicode character is inputted as filename, it will be parsed first and return a valid Adobe glyph name or uniXXXX/uXXXXXX.
input: filename
output: tuple(valid Adobe glyph name, Unicode codepoint integer or None)
"""
# length 1, directly encode character (eg 一.svg, ㄅ.svg)
if len(name.strip()) == 1:
return uniName(name)
# contains invalid Adobe glyph name chars
if uses_other_chars(name):
return (re.sub("[^"+"".join(aglfn_allowedchar)+"]", "", name), None)
# variant glyph names (eg A.smcp, f_i), no unicode
if "." in name or "_" in name:
return (name, None)
# filename is in aglfn predefined list
if name in aglfn_namelist:
return (name, aglfn_namelist[name])
# name format "uniXXXX"
if name.startswith("uni") and len(name.strip()[3:]) == 4:
try:
num = int(name[3:7], 16)
if num in range(0xD800, 0xE001): # UTF-16
return (name, None)
return (name, num)
except ValueError: # not base 16
return (name, None)
# name format "uXXXXXX"
if name.startswith("u"):
if len(name.strip()[1:]) not in (4, 5, 6): # only allow 4-6
return (name, None)
try:
num = int(name.strip()[1:], 16)
if num in range(0xD800, 0xE001) or num > 0x10FFFF: # UTF-16, out of Unicode
return (name, None)
return (name, num)
except ValueError: # not base 16
return (name, None)
# fallback
return (name, None)
def checkDuplicateName(name, list):
"""
Check for same name in list and if so, return a new name that is not in the list by appending a number
"""
if name not in list:
return name
i = 0
tempname = name
while tempname in list:
i += 1
tempname = name + "." + str(i)
return tempname
def getAllFiles(path):
fulllist=[]
for folder, subfolders, files in os.walk(path):
for file in files:
filePath = os.path.normpath(os.path.abspath(os.path.join(folder, file)))
fulllist.append(filePath)
return fulllist
def build_font(folder, csv_mapping, use_CSV, nameStrings, headVersion, outputFile, metrics=None, vendorCode="", picoNormalize=True, strictMode=True):
# return log screen when done
log = ""
# convert svg filename to Unicode character/glyph name
filename_char_map = {}
if use_CSV:
with open(csv_mapping, "r", encoding="utf-8-sig") as csvfile:
reader = csv.reader(csvfile)
for line in reader:
filename = line[0]
# check if absolute path
if not os.path.isabs(filename):
# join path with svg folder
filename = os.path.join(folder, filename)
filename = os.path.normpath(filename)
# verify filename
if filename.lower().endswith(".svg"):
filename_char_map[filename] = line[1]
else:
log += line[0] + " in CSV is ignored: not SVG\n"
if metrics is None:
metrics = dict(
upm = 1000,
ascender = 880,
descender = 120,
line_gap = 0,
safe_ascender = 1100,
safe_descender = 250,
)
metrics = SimpleNamespace(**metrics)
## START PREPROCESSING SVG
# glyph order, add default .notdef
glyphOrder = [".notdef"]
# unicode mapping
charmap = {}
# glyph outline
glyphs = {}
glyphs[".notdef"] = drawNotdefGlyph("bezier", metrics.upm, metrics.ascender)
# glyph advance width
advanceWidths = {}
advanceWidths[".notdef"] = 1000
# loop through svg files, by OS sorting order (os_sorted)
for filename in os_sorted(getAllFiles(folder)):
name, ext = os.path.splitext(os.path.basename(filename))
# check if svg
if ext.lower() != ".svg":
log += (f"%s is not a SVG file\n" % (filename))
continue
name = name.strip()
# use CSV mapping
if use_CSV:
# strict mode, skip filename with *no* predefined filename-character mapping
if strictMode and filename not in filename_char_map:
continue
# find character that is mapped to filename (with .svg extension), or return name without ".svg"
new_char_name = filename_char_map.get(filename, name)
# parse the remapped char name to glyph name
glyphname, uni = getUniFromAdobeName(new_char_name)
# check filename
else:
# directly parse filename to glyph name
glyphname, uni = getUniFromAdobeName(name)
# check if duplicate glyphname, rename if exist
glyphname = checkDuplicateName(glyphname, glyphOrder)
# add glyph to glyph order
glyphOrder.append(glyphname)
# not None, unicode not assigned before
if uni is not None and uni not in charmap:
# assign unicode
charmap[uni] = glyphname
elif uni in charmap:
log += (
f"Unicode %s is already assigned to glyph name %s\n"
% ("U+"+hex(uni).upper()[2:], charmap[uni])
)
# read SVG into string
svg_fulltext = open(os.path.join(folder, filename), "r", encoding="utf-8-sig").read()
# get glyph outline
glyph, glyphadv = getOutlineFromSVG(svg_fulltext, metrics.upm, metrics.ascender, picoNormalize)
# save glyph into glyphs dict
glyphs[glyphname] = glyph
# set glyph advance
advanceWidths[glyphname] = glyphadv
## END PREPROCESSING SVG
# start building
fb = FontBuilder(metrics.upm, isTTF=False)
fb.setupGlyphOrder(glyphOrder)
fb.setupCharacterMap(charmap)
# for glyf (quadratic)
# fb.setupGlyf(glyphs)
# glyphTable = fb.font["glyf"]
# set metrics
# horiMetrics = {}
# for gn, advanceWidth in advanceWidths.items():
# horiMetrics[gn] = (advanceWidth, glyphTable[gn].xMin)
# for CFF (cubic)
fb.setupCFF(nameStrings["psName"]["en"], {"FullName": nameStrings["psName"]["en"]}, glyphs, {})
# calculat lsb
lsb = {gn: cs.calcBounds(None)[0] for gn, cs in glyphs.items()}
horiMetrics = {}
for gn, advanceWidth in advanceWidths.items():
horiMetrics[gn] = (advanceWidth, lsb[gn])
fb.setupHorizontalMetrics(horiMetrics)
fb.setupHorizontalHeader(ascent=metrics.safe_ascender, descent=-metrics.safe_descender, lineGap=metrics.line_gap)
# other font info
fb.setupNameTable(nameStrings)
fb.setupOS2(
sTypoAscender=metrics.ascender,
sTypoDescender=-metrics.descender,
sTypoLineGap=metrics.line_gap,
usWinAscent=metrics.safe_ascender,
usWinDescent=metrics.safe_descender,
achVendID=vendorCode,
)
fb.setupPost()
fb.setupHead(fontRevision=headVersion)
fb.save(outputFile)
if log == "":
log = "Font successfully generated!\nFilename: " + outputFile
return log
global aglfn_allowedchar
aglfn_allowedchar = frozenset(string.ascii_letters + string.digits + '._')
aglfn_namelist = {}
for line in open("aglfn.txt", "r", encoding="utf-8-sig"):
if line.startswith("#"):
continue
uni, name, _ = line.split(";")
aglfn_namelist[name] = int(uni, 16)
# test build_font functionality in this file
# def build_test():
# familyName = "My Font"
# styleName = "Regular"
# nameStrings = dict(
# familyName = dict(en=familyName),
# styleName = dict(en=styleName),
# typographicFamily = dict(en=familyName),
# typographicSubfamily = dict(en=styleName),
# uniqueFontIdentifier = dict(en="SVG2FontBuilder:" + familyName + "." + styleName),
# fullName = dict(en=familyName + "-" + styleName),
# psName = dict(en=familyName + "-" + styleName),
# version="Version 1.000; testing; SVG2FontBuilder",
# )
# build_font("test/svgwechat", "", False, nameStrings, 123, "test/test.otf", picoNormalize=True, strictMode=False)
# if __name__ == "__main__":
# build_test()