-
Notifications
You must be signed in to change notification settings - Fork 13
/
mkttf.py
executable file
·386 lines (332 loc) · 14.3 KB
/
mkttf.py
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
382
383
384
385
386
#!/usr/bin/env python
#
# This Python script uses FontForge to convert a set of BDF files into a
# TrueType font (TTF) and an SFD file.
#
# Copyright (c) 2013-2023 by Tilman Blumenbach <tilman [AT] ax86 [DOT] net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of the author nor the names of its contributors
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
import argparse
import fontforge
import os.path
import re
import sys
from itertools import dropwhile
# Maps argument names to their font attribute names.
_argNameFontAttrMap = {
'name': 'fontname',
'family': 'familyname',
'display_name': 'fullname',
'weight': 'weight',
'copyright': 'copyright',
'font_version': 'version',
}
# Determines which fsSelection and macStyle bits in the OS/2 table get set
# when a certain font weight is specified and OS/2 table tweaks are enabled.
#
# Use lowercase font weights here. The "italic" font weight is special:
# If the font weight is "medium" and the font name ends with "italic"
# (case-insensitive), then "italic" is used when looking up values in this
# dictionary instead of "medium".
#
# The first value of each tuple contains the bits to set in the fsSelection
# field.
#
# The second value of each tuple contains the bits to set in the macStyle
# field in the OS/2 table.
#
# See https://www.microsoft.com/typography/otspec/os2.htm#fss for details.
_weightToStyleMap = {
# fsSelection: Set bit 6 ("REGULAR").
'normal': (0x40, 0),
# fsSelection: Set bit 6 ("REGULAR").
'medium': (0x40, 0),
# fsSelection: Set bits 0 ("ITALIC") and 9 ("OBLIQUE").
# macStyle: Set bit 1 (which presumably also means "ITALIC").
'italic': (0x201, 0x2),
# fsSelection: Set bit 5 ("BOLD").
# macStyle: Set bit 0 (which presumably also means "BOLD").
'bold': (0x20, 0x1),
# fsSelection: Set bits 0 ("ITALIC"), 9 ("OBLIQUE") and 5 ("BOLD").
# macStyle: Set bits 1 (italic) and 0 (bold).
'bolditalic': (0x221, 0x3),
}
def arg_vendor_id(arg):
if len(arg) != 4 or not re.fullmatch(r'[\x21-\x7E]{,4} *', arg):
raise argparse.ArgumentTypeError(
'Vendor ID must be a four-character string of up to four printable ASCII characters other than space, with '
'trailing spaces for padding.'
)
# All good!
return arg
def initArgumentParser():
"""Initialize and return an argparse.ArgumentParser that parses this program's arguments."""
argParser = argparse.ArgumentParser(
description='Convert a set of BDF files into a TrueType font (TTF). '
'The BDF files have to be sorted by font size in ascending order.'
)
# Positional arguments.
argParser.add_argument(
'bdf_file',
nargs='+',
help='BDF file to process.'
)
# Optional arguments.
argParser.add_argument(
'-n',
'--name',
help='Font name to use for generated font (default: taken from first BDF file).'
)
argParser.add_argument(
'-f',
'--family',
help='Font family to use for generated font (default: taken from first BDF file).'
)
argParser.add_argument(
'-N',
'--display-name',
help='Full font name (for display) to use for generated font (default: taken from first BDF file).'
)
argParser.add_argument(
'-w',
'--weight',
help='Weight to use for generated font (default: taken from first BDF file).'
)
argParser.add_argument(
'-c',
'--copyright',
help='Copyright notice to use for generated font (default: taken from first BDF file).'
)
argParser.add_argument(
'-C',
'--append-copyright',
help='Copyright notice to use for generated font (appends to notice taken from first BDF file).'
)
argParser.add_argument(
'-V',
'--font-version',
help='Font version to use for generated font (default: taken from first BDF file).'
)
argParser.add_argument(
'-e',
'--vendor',
type=arg_vendor_id,
default='PfEd', # "PfEd" is the value FontForge writes when using the GUI
help='OS/2 vendor ID (aka font foundry ID) to include in the generated font. This is a four-character '
'string of printable ASCII characters, possibly padded with trailing spaces. Default: %(default)s'
)
argParser.add_argument(
'-a',
'--prefer-autotrace',
action='store_true',
help='Prefer AutoTrace over Potrace, if possible (default: %(default)s).'
)
argParser.add_argument(
'-t',
'--tracer-path',
metavar='TRACER_PATH',
default=os.path.join(
os.path.dirname(os.path.realpath(sys.argv[0])),
'potrace-wrapper.sh'
),
help='Path to AutoTrace/Potrace. If this is set to the empty string, FontForge will use the value of the '
'"POTRACE" (or "AUTOTRACE") environment variable, and fall back to the value "potrace" (or '
'"autotrace") if the variable is unset. In that case, which of those variables/values is used depends '
'on whether the "--prefer-autotrace" option was specified. Note that if you want this program to be '
'called with Potrace-compatible arguments, %(metavar)s *must* include the string "potrace". This is a '
'FontForge limitation. Default: %(default)s'
)
argParser.add_argument(
'-A',
'--tracer-args',
default='',
help='Additional arguments for AutoTrace/Potrace (default: none).'
)
argParser.add_argument(
'-s',
'--visual-studio-fixes',
action='store_true',
help='Make generated font compatible with Visual Studio (default: %(default)s).'
)
argParser.add_argument(
'-O',
'--os2-table-tweaks',
action='store_true',
help='Tweak OS/2 table according to the font weight. This may be needed for some '
'buggy FontForge versions which do not do this by themselves.'
)
argParser.add_argument(
'--no-background',
action='store_true',
help='Do not import the largest font into the glyph background. This is useful only '
'when the font already has a suitable glyph background, and you do not want to '
'overwrite it. Only for special use cases.'
)
return argParser
def setFontAttrsFromArgs(font, args):
"""Set font attributes from arguments.
If an argument is None, that means that no value was given. In that case, the font attribute
is not modified.
args is an argparse.Namespace.
font is a fontforge.font.
"""
for argName in _argNameFontAttrMap:
argValue = getattr(args, argName)
if argValue is not None:
# User gave a new value for this font attribute.
setattr(
font,
_argNameFontAttrMap[argName],
argValue
)
def setTracerPathFromArgs(args):
"""Tell FontForge which autotrace program to use, based on our own program arguments.
This sets either the AUTOTRACE or the POTRACE environment variable of this process (or no environment variable at
all if we want FontForge to figure out the autotrace program itself).
args is an argparse.Namespace.
"""
if not args.tracer_path:
# Let FontForge figure out the path to the autotrace program itself.
return
if args.prefer_autotrace:
if 'potrace' in args.tracer_path:
sys.exit(
'Error: When "--prefer-autotrace" is specified, "--tracer-path" must NOT include the substring '
'"potrace". This is a FontForge limitation.'
)
os.environ['AUTOTRACE'] = args.tracer_path
elif 'potrace' not in args.tracer_path:
sys.exit(
'Error: When "--prefer-autotrace" is NOT specified, "--tracer-path" MUST include the substring "potrace". '
'This is a FontForge limitation.'
)
else:
os.environ['POTRACE'] = args.tracer_path
# Parse the command line arguments.
args = initArgumentParser().parse_args()
# Set FontForge options.
fontforge.setPrefs("PreferPotrace", not args.prefer_autotrace)
fontforge.setPrefs("AutotraceArgs", args.tracer_args)
setTracerPathFromArgs(args)
# Good, can we open the base font?
try:
baseFont = fontforge.open(args.bdf_file[0])
except EnvironmentError as e:
sys.exit("Could not open base font `%s'!" % args.bdf_file[0])
# Now import all the bitmaps from the other BDF files into this font.
print('Importing bitmaps from %d additional fonts...' % (len(args.bdf_file) - 1))
for fontFile in args.bdf_file[1:]:
try:
baseFont.importBitmaps(fontFile)
except EnvironmentError as e:
sys.exit("Could not import additional font `%s'!" % fontFile)
# Import the last (biggest) BDF font into the glyph background.
if not args.no_background:
try:
print("Importing font `%s' into glyph background..." % args.bdf_file[-1])
baseFont.importBitmaps(args.bdf_file[-1], True)
except EnvironmentError as e:
sys.exit("Could not import font `%s' into glyph background: %s" % (args.bdf_file[-1], e))
else:
print("Skipping import of font `%s' into glyph background, as requested." % args.bdf_file[-1])
# Now set font properties.
setFontAttrsFromArgs(baseFont, args)
# Do we want to append to the current copyright notice?
if args.append_copyright is not None:
baseFont.copyright += args.append_copyright
# FontForge won't write the OS/2 table unless we set a vendor and we set it BEFORE modifying
# the OS/2 table in any way (although this is not documented anywhere...).
baseFont.os2_vendor = args.vendor
# Newer FontForge releases require us to manually set the macStyle
# and fsSelection (aka "StyleMap") fields in the OS/2 table.
if args.os2_table_tweaks:
if not hasattr(baseFont, "os2_stylemap"):
sys.exit("You requested OS/2 table tweaks, but your FontForge version is too old for these "
"tweaks to work.")
os2_weight = baseFont.weight.lower()
if os2_weight == "medium" and baseFont.fontname.lower().endswith("italic"):
os2_weight = "italic"
elif os2_weight == "bold" and baseFont.fontname.lower().endswith("italic"):
os2_weight = "bolditalic"
try:
styleMap, macStyle = _weightToStyleMap[os2_weight]
except KeyError:
sys.exit("Cannot tweak OS/2 table: No tweaks defined for guessed font weight `%s'!" % os2_weight)
print(
"OS/2 table tweaks: Guessed weight is `%s' -> Adding %#x to StyleMap and %#x to macStyle." % (
os2_weight,
styleMap,
macStyle
)
)
baseFont.os2_stylemap |= styleMap
baseFont.macstyle |= macStyle
# AutoTrace all glyphs, add extrema and simplify.
print('Processing glyphs...')
baseFont.selection.all()
baseFont.autoTrace()
baseFont.addExtrema()
baseFont.simplify()
# Do we need to fixup the font for use with Visual Studio?
# Taken from http://www.electronicdissonance.com/2010/01/raster-fonts-in-visual-studio-2010.html
# Really, it's a MESS that one has to use dirty workarounds like this...
if args.visual_studio_fixes:
print('Applying Visual Studio fixes...')
# Make sure the encoding used for indexing is set to UCS.
baseFont.encoding = 'iso10646-1'
# Need to add CP950 (Traditional Chinese) to OS/2 table.
# According to http://www.microsoft.com/typography/otspec/os2.htm#cpr,
# we need to set bit 20 to enable CP950.
baseFont.os2_codepages = (baseFont.os2_codepages[0] | (1 << 20), baseFont.os2_codepages[1])
# The font needs to include glyphs for certain characters.
# Try to find a fitting glyph to substitute for those glyphs which
# the font does not already contain. U+0000 is the "default character";
# it _should_ be displayed instead of missing characters, so it is a good choice.
# If the font does not contain a glyph for U+0000, try other, less optimal glyphs.
try:
selector = next(dropwhile(lambda x: x not in baseFont, [0, 'question', 'space']))
substGlyph = baseFont[selector]
except StopIteration:
sys.exit(' While applying Visual Studio fixes: Could not find a substitution glyph!')
print(" Chose `%s' as substitution glyph." % substGlyph.glyphname)
baseFont.selection.select(substGlyph)
baseFont.copyReference()
for codePoint in [0x3044, 0x3046, 0x304B, 0x3057, 0x306E, 0x3093]:
if codePoint not in baseFont:
baseFont.selection.select(codePoint)
baseFont.paste()
# Finally, save the files!
basename = baseFont.fontname
if baseFont.version != '':
basename += '-' + baseFont.version
print('Saving TTF file...')
baseFont.generate(basename + '.ttf', 'ttf')
print('Saving SFD file...')
baseFont.save(basename + '.sfd')
print('Done!')