-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathcomparisonProof.py
347 lines (276 loc) · 9.74 KB
/
comparisonProof.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
# Copyright 2023 Adobe
# All Rights Reserved.
# NOTICE: Adobe permits you to use, modify, and distribute this file in
# accordance with the terms of the Adobe license agreement accompanying
# it.
'''
Create lines for a string of characters, set in all fonts that support it.
The choice of fonts is either all installed fonts (no argument), or all fonts
in a given folder tree. The font list can be filtered by supplying a regular
expression.
This proof helps solving the question:
“How do other fonts deal with this weird glyph?”
Slow.
'''
import argparse
import drawBot as db
import logging
import re
import subprocess
import CoreText
from fontTools import ttLib
from pathlib import Path
from proofing_helpers.globals import ADOBE_BLANK, FONT_MONO
from proofing_helpers.names import get_overlap_index, get_ps_name
logger = logging.getLogger('fontTools')
logger.setLevel(level=logging.CRITICAL)
# otherwise a lot of irrelevant output:
# 1 extra bytes in post.stringData array
EXCLUDE_FONTS = [
'LastResort',
'AND-Regular',
'AdobeBlank',
]
class FontFileInfo(object):
def __init__(self, font_path, ps_name, font_number=0):
self.path = font_path
self.suffix = font_path.suffix
self.ps_name = ps_name
self.font_number = font_number
def get_installed_font_path(ps_name):
ctFont = CoreText.CTFontDescriptorCreateWithNameAndSize(ps_name, 10)
if ctFont is None:
print(f'The font "{ps_name}" is not installed')
else:
url = CoreText.CTFontDescriptorCopyAttribute(
ctFont, CoreText.kCTFontURLAttribute)
font_path = url.path()
return Path(font_path)
def filter_fonts_by_regex(fonts, regex):
rex = re.compile(regex)
filtered_names = list(
filter(rex.match, [fo.ps_name for fo in fonts]))
if filtered_names:
font_objects = [fo for fo in fonts if fo.ps_name in filtered_names]
else:
print('No match for regular expression.')
font_objects = []
return font_objects
def get_ttc_styles(font_path):
'''
Return a dict of ttc font numbers to PS names
'''
ttc_styles = {}
tt_collection = ttLib.TTCollection(font_path)
for i, ttfont in enumerate(tt_collection.fonts):
ps_name = ttfont['name'].names[6].toUnicode()
ttc_styles[i] = ps_name
return ttc_styles
def get_font_number_for_ps_name(font_path, ps_name):
ttc_style_dict = get_ttc_styles(font_path)
reversed_style_dict = {
value: key for (key, value) in ttc_style_dict.items()}
return reversed_style_dict.get(ps_name, 0)
def get_cmap(font_path, ttc_arg=0):
'''
get the cmap from a font.
TTCs can be accessed via font number or PS name.
'''
if isinstance(ttc_arg, int):
font_number = ttc_arg
ttfont = ttLib.TTFont(font_path, fontNumber=font_number)
else:
ps_name = ttc_arg
font_number = get_font_number_for_ps_name(font_path, ps_name)
ttfont = ttLib.TTFont(font_path, fontNumber=font_number)
try:
return ttfont['cmap'].getBestCmap()
except AssertionError:
return {}
def ffi_supports_characters(characters, ffi):
cmap = get_cmap(ffi.path, ffi.font_number)
if cmap and set([ord(char) for char in characters]) <= cmap.keys():
return True
else:
return False
def font_path_to_ffi(font_path):
'''
Return a list of one (otf/ttf) or multiple (ttc) font file info objects.
'''
ffi_objects = []
if font_path.suffix == '.ttc':
ttc_style_dict = get_ttc_styles(font_path)
for font_number, ps_name in ttc_style_dict.items():
ffi = FontFileInfo(font_path, ps_name, font_number)
ffi_objects.append(ffi)
else:
ps_name = get_ps_name(font_path)
if ps_name is None:
ps_name = font_path.stem
ffi = FontFileInfo(font_path, ps_name)
ffi_objects.append(ffi)
return ffi_objects
def get_available_fonts(args):
'''
Return a list of objects, which contain a font’s path, PS name, and a
font number (if applicable).
'''
available_fonts = []
suffixes = ['.ttf', '.otf', '.ttc']
if args.input_path:
input_path = Path(args.input_path)
if input_path.is_dir(): # find fonts in input_path
font_paths = (
list(input_path.rglob('*.otf')) +
list(input_path.rglob('*.tt[fc]')))
for font_path in font_paths:
if (
font_path.suffix in suffixes and
font_path.parent.name != 'AFE' and
# The AFE folder contains weird fonts w/o outlines
font_path.stem not in EXCLUDE_FONTS
):
available_fonts.extend(font_path_to_ffi(font_path))
elif input_path.is_file():
available_fonts.extend(font_path_to_ffi(input_path))
else:
print(f'{args.input_path} seems to be invalid.')
else: # use installed fonts
for ps_name in db.installedFonts():
if ps_name.startswith('.'):
# don't even try to look at system UI fonts: they don't
# work (sub to Times New Roman) and attempting to access
# causes an ugly warning message to be emitted.
continue
font_path = get_installed_font_path(ps_name)
if (
font_path.suffix in suffixes and
font_path.name not in EXCLUDE_FONTS
):
available_fonts.extend(font_path_to_ffi(font_path))
return available_fonts
def collect_font_objects(args):
available_fonts = get_available_fonts(args)
if args.regex: # filtering by possible regex
available_fonts = filter_fonts_by_regex(available_fonts, args.regex)
# filtering for character support
applicable_fonts = [
ffi for ffi in available_fonts if
ffi_supports_characters(args.characters, ffi)]
# simple sorting by PS name -- this is imperfect but makes sense for
# installed fonts, or when a deep folder tree is parsed.
return sorted(
applicable_fonts,
key=lambda fo: (fo.ps_name, fo.font_number, fo.suffix))
def make_pdf_name(args):
chars_safe = args.characters.replace('/', '_') # remove slash from path
if args.input_path:
short_dir = Path(args.input_path).name
pdf_name = f'comparisonProof {chars_safe} ({short_dir}).pdf'
else:
pdf_name = f'comparisonProof {chars_safe} (installed fonts).pdf'
return pdf_name
def make_line(args, ffi):
'''
Create a FormattedString with a line of content, using a font indicated
by ffi.path. A label with the PS name is added.
'''
fs = db.FormattedString(
args.characters + ' ',
font=ffi.path,
fontSize=int(args.pt),
fontNumber=ffi.font_number,
fallbackFont=ADOBE_BLANK,
)
fs.append(
ffi.ps_name,
font=FONT_MONO,
fontSize=10)
return fs
def make_document(args, formatted_strings):
margin = int(args.pt)
line_number = 0
line_height = int(args.pt) * 1.2
pagespec = f'{args.pagesize}{"Landscape" if args.landscape else ""}'
db.newDrawing()
db.newPage(pagespec)
for fs in formatted_strings:
line_number += 1
current_baseline = (db.height() - margin - line_height * line_number)
db.text(fs, (margin, current_baseline))
if line_number * line_height + 4 * margin >= db.height():
db.newPage(pagespec)
line_number = 0
pdf_name = make_pdf_name(args)
output_path = Path(f'~/Desktop/{pdf_name}').expanduser()
db.saveImage(output_path)
db.endDrawing()
return output_path
def get_options(args=None):
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'characters',
metavar='',
nargs='?',
action='store',
default='abc123',
help='characters to sample')
parser.add_argument(
'-r', '--regex',
action='store',
metavar='REGEX',
type=str,
help='regular expression to filter font list')
parser.add_argument(
'--pt',
action='store',
metavar='POINTS',
default=30,
help='point size')
parser.add_argument(
'--headless',
default=False,
action='store_true',
help='do not open result PDF after generating')
parser.add_argument(
'-d', '--input_path',
action='store',
metavar='FOLDER',
help='folder to crawl (default is using all installed fonts)')
parser.add_argument(
'--pagesize',
choices=[size for size in db.sizes() if "Landscape" not in size],
default="Letter",
help='page size'
)
parser.add_argument(
'--landscape',
default=False,
action='store_true',
help='landscape orientation (default is portrait)')
return parser.parse_args(args)
def main(test_args=None):
args = get_options(test_args)
font_objects = collect_font_objects(args)
all_paths = [str(ffi.path) for ffi in font_objects]
overlap_index = get_overlap_index(all_paths)
formatted_strings = []
used_ps_names = []
for ffi in font_objects:
if ffi.ps_name not in used_ps_names:
formatted_strings.append(make_line(args, ffi))
used_ps_names.append(ffi.ps_name)
print(str(ffi.path)[overlap_index:])
if formatted_strings:
output_path = make_document(args, formatted_strings)
if not args.headless:
if output_path.exists():
print(output_path)
subprocess.call(['open', output_path])
else:
print('No fonts found.')
if __name__ == '__main__':
main()