Skip to content

Commit e7b2682

Browse files
author
Josh Hadley
committed
Add Unicode chart proof
1 parent ab53f59 commit e7b2682

5 files changed

Lines changed: 368 additions & 0 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,24 @@ Input: folder containing fonts, or single font file.
187187

188188
----
189189

190+
### `unicodeChartProof.py`
191+
192+
Creates character charts similar to the Unicode.org charts for The Unicode
193+
Standard, but using the supplied font (and only the characters present in the
194+
font).
195+
196+
Input: font file or folder containing font files
197+
198+
CLI Inputs: see help
199+
200+
![unicodeChartProof.py](_images/unicodeChartProof_1.png)
201+
202+
![unicodeChartProof.py](_images/unicodeChartProof_2.png)
203+
204+
![unicodeChartProof.py](_images/unicodeChartProof_3.png)
205+
206+
----
207+
190208
### `verticalMetricsComparisonProof.py`
191209

192210
Creates pages with example characters to visualize the variation

_images/unicodeChartProof_1.png

374 KB
Loading

_images/unicodeChartProof_2.png

347 KB
Loading

_images/unicodeChartProof_3.png

331 KB
Loading

unicodeChartProof.py

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
# Copyright 2023 Adobe
2+
# All Rights Reserved.
3+
4+
# NOTICE: Adobe permits you to use, modify, and distribute this file in
5+
# accordance with the terms of the Adobe license agreement accompanying
6+
# it.
7+
8+
'''
9+
Creates character charts similar to the Unicode.org charts for The Unicode
10+
Standard, but using the supplied font (and only the characters present in the
11+
font).
12+
13+
Input: font file or folder containing font files
14+
15+
CLI Inputs: see help
16+
17+
'''
18+
19+
import argparse
20+
from collections import defaultdict
21+
from functools import partial
22+
from math import floor, ceil
23+
from pathlib import Path
24+
import subprocess
25+
import sys
26+
27+
import drawBot as db
28+
from fontTools.ttLib import TTFont
29+
import unicodedataplus
30+
31+
from proofing_helpers.files import get_font_paths
32+
from proofing_helpers.fontSorter import sort_fonts
33+
34+
IN_UI = 'drawBot.ui' in sys.modules
35+
36+
if IN_UI:
37+
from vanilla.dialogs import getFileOrFolder # noqa: F401
38+
39+
40+
def get_options(args=None, description=__doc__):
41+
parser = argparse.ArgumentParser(
42+
description=description,
43+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
44+
)
45+
46+
parser.add_argument(
47+
'input_dir',
48+
action='store',
49+
metavar='FOLDER',
50+
help='folder to crawl')
51+
52+
parser.add_argument(
53+
'-o', '--output_file_name',
54+
action='store',
55+
metavar='PDF',
56+
help='output file name')
57+
58+
parser.add_argument(
59+
'--pagesize',
60+
choices=["A4", "Letter"],
61+
default="Letter",
62+
help="Desired page size")
63+
64+
parser.add_argument(
65+
'-v', '--varfont_axes',
66+
type=_parse_vf_args,
67+
help="Variable font axis info e.g. 'wght:400,wdth:100'")
68+
69+
parser.add_argument(
70+
'-s', '--size',
71+
type=int,
72+
default=CHART_FONT_SIZE,
73+
help="Chart font pointsize override value")
74+
75+
return parser.parse_args(args)
76+
77+
78+
FONTS_FOLDER = Path(__file__).parent / "_fonts"
79+
BLOCK_TITLE_FONT = FONTS_FOLDER / "SourceSans3-Bold.otf"
80+
BLOCK_TITLE_SIZE = 20
81+
BLOCK_FONTNAME_SIZE = 14
82+
83+
CHART_LABEL_FONT = FONTS_FOLDER / "SourceSans3-Regular.otf"
84+
CHART_LABEL_SIZE = 11
85+
86+
CHART_COL_WIDTH = 32
87+
CHART_ROW_HEIGHT = 42
88+
CHART_ROW_LABEL_Y_OFFSET = 6
89+
CHART_COL_LABEL_Y_OFFSET = -3
90+
CHART_FONT_SIZE = 24
91+
CHART_FONT_BL = CHART_ROW_HEIGHT * 0.52
92+
BLOCK_FONTNAME_Y_OFFSET = 67
93+
94+
CODE_LABEL_FONT = FONTS_FOLDER / "SourceSans3-Light.otf"
95+
CODE_LABEL_SIZE = 7
96+
CODE_LABEL_Y_OFFSET = 11
97+
98+
# line thicknesses
99+
THICK = 1.5
100+
THIN = 0.1
101+
102+
103+
def _parse_vf_args(axisstr):
104+
vffunc = partial(db.fontVariations)
105+
for axs in axisstr.split(","):
106+
ax, axv = axs.split(':')
107+
vffunc.keywords[ax] = float(axv)
108+
109+
return vffunc
110+
111+
112+
def get_uni_blocks():
113+
all_ranges = defaultdict(int)
114+
for u in range(0, 0x110000):
115+
uc = chr(u)
116+
gc = unicodedataplus.category(uc)
117+
if gc in ('Cc', 'Cn'): # exclude controls and non-characters
118+
continue
119+
block = unicodedataplus.block(uc)
120+
all_ranges[block] += 1
121+
122+
return all_ranges
123+
124+
125+
def draw_gauge(x, y, pct, w=25, h=12):
126+
with db.savedState():
127+
db.linearGradient(
128+
startPoint=(x, y),
129+
endPoint=(x + w, y),
130+
colors=[[.9, .9, .9], [0, 0, 0]],
131+
locations=[0, 0.75])
132+
db.rect(x, y, w, h)
133+
db.strokeWidth(0)
134+
db.fill(1)
135+
wpct = (pct / 100) * w
136+
db.rect(x + wpct, y, w - wpct, h)
137+
db.stroke(0)
138+
db.strokeWidth(0.3)
139+
db.fill(1, alpha=0)
140+
db.rect(x, y, w, h)
141+
142+
143+
def make_chart_doc(font_file, args):
144+
db.newDrawing()
145+
146+
set_vf = args.varfont_axes or (lambda: None)
147+
in_font = TTFont(font_file)
148+
fontname = in_font['name'].getDebugName(4)
149+
umap = in_font['cmap'].getBestCmap()
150+
umapset = set(umap)
151+
uni_blocks = get_uni_blocks()
152+
153+
# calculate textBox first baseline offset
154+
db.font(font_file, args.size)
155+
bls = db.textBoxBaselines(" ", (0, 0, CHART_FONT_SIZE * 2, CHART_FONT_SIZE * 2))
156+
if len(bls) == 0:
157+
print(f"Unable to fit font; re-try using the --size option with value < {args.size}.")
158+
sys.exit(1)
159+
tbfbo = 0 - bls[0][1]
160+
161+
NSMARKBASE = db.FormattedString(
162+
chr(0x25CC),
163+
font=font_file,
164+
fallbackFont=FONTS_FOLDER / "SourceCodePro-Regular.otf",
165+
fontSize=args.size,
166+
align="center",
167+
fill=(0.75),
168+
)
169+
170+
if 0x25CC in umap:
171+
NSMARKBASE.font = font_file
172+
173+
font_blocks = defaultdict(set)
174+
175+
for u in umap:
176+
cu = chr(u)
177+
gc = unicodedataplus.category(cu)
178+
if gc in ('Cc', 'Cn'):
179+
continue
180+
bk = unicodedataplus.block(cu)
181+
if bk != "No_Block":
182+
font_blocks[bk].add(u)
183+
184+
# make index pages -- 64 blocks per page
185+
for ipg in range(ceil(len(font_blocks)/64)):
186+
db.newPage(args.pagesize)
187+
db.linkDestination(f"index{ipg}", (0, db.height()))
188+
db.fill(0)
189+
db.font(BLOCK_TITLE_FONT, 20)
190+
db.textBox(f'{fontname}',
191+
(0, db.height() - 60, db.width(), 28),
192+
align="center")
193+
194+
# go through the blocks, break into <= 16 col pages
195+
for _bki, (blockname, blockcodes) in enumerate(font_blocks.items()):
196+
blockmin = min(blockcodes)
197+
blockmax = max(blockcodes)
198+
block_nmin = (blockmin // 16) * 16
199+
block_nmax = ((blockmax // 16) * 16) + 15
200+
201+
for _bpi, blockpagemin in enumerate(range(block_nmin, block_nmax, 16 * 16)):
202+
# first page of block, make index entry
203+
pct = len(font_blocks[blockname]) / uni_blocks[blockname] * 100
204+
if _bpi == 0:
205+
ipi = floor(_bki / 64) # which index page
206+
with db.pages()[ipi]:
207+
db.font(CODE_LABEL_FONT, 13)
208+
tc = floor((_bki % 64) / 32)
209+
tr = _bki % 32
210+
cw = db.width() / 2
211+
db.text(
212+
blockname,
213+
(20 + (tc * cw) + 30, db.height() - (tr * 20) - 80))
214+
db.linkRect(
215+
blockname,
216+
(20 + (tc * cw), db.height() - (tr * 20) - 81, cw, 16))
217+
draw_gauge(20 + (tc * cw), db.height() - (tr * 20) - 82, pct)
218+
219+
blockpagemax = min(block_nmax, blockpagemin + 255)
220+
221+
# if page would be empty, skip
222+
pageset = set(range(blockpagemin, blockpagemin + 256))
223+
if not pageset.intersection(umapset):
224+
continue
225+
226+
db.newPage(args.pagesize)
227+
db.font(BLOCK_TITLE_FONT, BLOCK_TITLE_SIZE)
228+
db.textBox(f'{blockname} (U+{blockpagemin:04X}-{blockpagemax:04X})',
229+
(0, db.height() - 60, db.width(), 36),
230+
align="center")
231+
if _bpi == 0:
232+
db.linkDestination(blockname, (0, db.height()))
233+
234+
db.font(CHART_LABEL_FONT, BLOCK_FONTNAME_SIZE)
235+
db.textBox(f'{fontname}',
236+
(0, db.height() - BLOCK_FONTNAME_Y_OFFSET, db.width(), 20),
237+
align="center")
238+
db.linkRect("index0",
239+
(0, db.height() - BLOCK_FONTNAME_Y_OFFSET, db.width(), 18))
240+
241+
npagecols = min(ceil((block_nmax - blockpagemin) / 16), 16)
242+
chart_width = npagecols * CHART_COL_WIDTH
243+
chart_left = (db.width() / 2) - (chart_width / 2)
244+
db.stroke(0)
245+
246+
CHART_BOX_TOP = db.height() - 80
247+
CHART_BOX_BOT = CHART_BOX_TOP - (CHART_ROW_HEIGHT * 16)
248+
xpos = 0
249+
250+
for col in range(npagecols):
251+
db.lineCap("square")
252+
db.strokeWidth(THICK if col == 0 else THIN)
253+
xpos = chart_left + col * CHART_COL_WIDTH
254+
db.line((xpos, CHART_BOX_TOP), (xpos, CHART_BOX_BOT))
255+
colheader = (blockpagemin + (16 * col)) // 16
256+
db.strokeWidth(0)
257+
db.font(CHART_LABEL_FONT, CHART_LABEL_SIZE)
258+
# top column label
259+
ypos = CHART_BOX_TOP
260+
db.textBox(f'{colheader:03X}',
261+
(xpos, ypos + CHART_COL_LABEL_Y_OFFSET, CHART_COL_WIDTH, 16),
262+
align="center")
263+
264+
# the actual font characters
265+
db.strokeWidth(THICK)
266+
db.line((chart_left + 1, ypos),
267+
(chart_left + chart_width, ypos))
268+
ypos -= CHART_ROW_HEIGHT
269+
db.font(CHART_LABEL_FONT, CHART_LABEL_SIZE)
270+
set_vf()
271+
for uc in range(colheader * 16, (colheader * 16) + 16):
272+
if uc // 16 == colheader:
273+
db.strokeWidth(THICK if uc % 16 == 15 else THIN)
274+
db.line((chart_left, ypos), (chart_left + chart_width - 1, ypos))
275+
db.strokeWidth(0)
276+
db.font(CHART_LABEL_FONT, CHART_LABEL_SIZE)
277+
# left-side row label
278+
db.textBox(f'{uc % 16:X}',
279+
(chart_left - 12, ypos + CHART_ROW_LABEL_Y_OFFSET, 8, 20),
280+
align="right")
281+
if uc in umap:
282+
# code label
283+
db.strokeWidth(0)
284+
db.font(CODE_LABEL_FONT, CODE_LABEL_SIZE)
285+
db.textBox(f'{uc:04X}',
286+
(xpos, ypos - CODE_LABEL_Y_OFFSET, CHART_COL_WIDTH - 2, 20),
287+
align="center")
288+
ypos -= CHART_ROW_HEIGHT
289+
290+
# draw font glyphs all in one go
291+
ypos = CHART_BOX_TOP - CHART_ROW_HEIGHT
292+
db.strokeWidth(0)
293+
db.font(font_file, CHART_FONT_SIZE)
294+
for uc in range(colheader * 16, (colheader * 16) + 16):
295+
ucc = chr(uc)
296+
gc = unicodedataplus.category(ucc)
297+
298+
if gc == "Cn":
299+
# for Unassigned characters (gc=Cn), fill gray
300+
with db.savedState():
301+
db.fill(0, alpha=.25)
302+
db.rect(xpos, ypos, CHART_COL_WIDTH, CHART_ROW_HEIGHT)
303+
304+
if uc in umap:
305+
bl = ypos + CHART_FONT_BL + tbfbo
306+
txt = db.FormattedString(
307+
chr(uc),
308+
font=font_file,
309+
fontSize=args.size,
310+
align="center",
311+
fontVariations=set_vf())
312+
sc = unicodedataplus.script(chr(uc))
313+
if gc == "Mn" and sc == "Inherited":
314+
txt = NSMARKBASE + txt # dotted circle base
315+
db.textBox(txt,
316+
(xpos, bl, CHART_COL_WIDTH, CHART_ROW_HEIGHT),
317+
align="center")
318+
ypos -= CHART_ROW_HEIGHT
319+
320+
# right line
321+
db.strokeWidth(THICK if blockpagemax // 16 == block_nmax // 16 else THIN)
322+
db.line((xpos + CHART_COL_WIDTH, CHART_BOX_TOP),
323+
(xpos + CHART_COL_WIDTH, CHART_BOX_BOT))
324+
325+
326+
def save_document(output_path):
327+
db.saveImage(output_path)
328+
print('saved PDF to', output_path)
329+
subprocess.call(['open', output_path])
330+
db.endDrawing()
331+
332+
333+
if __name__ == '__main__':
334+
if IN_UI:
335+
file_or_folder = getFileOrFolder(allowsMultipleSelection=False)
336+
input_dir = str(file_or_folder[0])
337+
args = get_options([input_dir])
338+
339+
else:
340+
args = get_options()
341+
342+
font_paths = get_font_paths(args.input_dir)
343+
sorted_font_paths = sort_fonts(font_paths)
344+
345+
for font_path in sorted_font_paths:
346+
make_chart_doc(font_path, args)
347+
348+
if not IN_UI:
349+
out_name = args.output_file_name or font_path.parent / f'{font_path.stem}_chart.pdf'
350+
save_document(out_name)

0 commit comments

Comments
 (0)