Skip to content

Commit 2a29df5

Browse files
authored
Merge pull request #59 from pelson/feature/ťkerning
Refine and add diacritics
2 parents f962f6a + 24db8a6 commit 2a29df5

18 files changed

Lines changed: 1991 additions & 532 deletions

xkcd-script/font/xkcd-script.otf

45 KB
Binary file not shown.

xkcd-script/font/xkcd-script.sfd

Lines changed: 1621 additions & 511 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xkcd-script/font/xkcd-script.ttf

19.4 KB
Binary file not shown.

xkcd-script/font/xkcd-script.woff

18.2 KB
Binary file not shown.
11.7 KB
Loading
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Extract hand-drawn glyphs from extras/ and convert them to SVG.
4+
5+
Outputs go to ../generated/additional_chars/ and are consumed by pt6_derived_chars.py.
6+
"""
7+
import os
8+
import subprocess
9+
import tempfile
10+
import numpy as np
11+
from PIL import Image
12+
import fontforge
13+
14+
OUT_DIR = '../generated/additional_chars'
15+
os.makedirs(OUT_DIR, exist_ok=True)
16+
17+
UPSAMPLE = 12 # upscale factor before potrace; higher = more curve detail
18+
THRESHOLD = 160 # pixel value below which a pixel is considered ink
19+
20+
21+
def _clean_potrace_svg(raw_svg_path, clean_svg_path):
22+
"""Remove potrace artefacts from raw_svg_path and write clean_svg_path.
23+
24+
Potrace always emits a background rectangle covering the full canvas, plus
25+
occasional single-pixel noise specks. We load the SVG into FontForge,
26+
drop those contours, and re-export so that pt6 receives a file containing
27+
only the actual ink outlines.
28+
29+
Filtering rules (applied in order):
30+
1. Background rectangle: <= 12 control points AND spans > 80% of the
31+
full bounding box in both axes.
32+
2. Noise specks: bbox smaller than 10% of the remaining ink extent in
33+
both axes simultaneously.
34+
"""
35+
scratch = fontforge.font()
36+
g = scratch.createChar(-1, 'tmp')
37+
g.importOutlines(raw_svg_path)
38+
39+
# Pass 1: drop background rectangle
40+
full_bb = g.boundingBox()
41+
full_w = full_bb[2] - full_bb[0]
42+
full_h = full_bb[3] - full_bb[1]
43+
pass1 = fontforge.layer()
44+
for c in g.foreground:
45+
cb = c.boundingBox()
46+
span_w = (cb[2] - cb[0]) / full_w if full_w else 0
47+
span_h = (cb[3] - cb[1]) / full_h if full_h else 0
48+
if len(list(c)) <= 12 and span_w > 0.8 and span_h > 0.8:
49+
continue
50+
pass1 += c
51+
g.foreground = pass1
52+
53+
# Pass 2: drop noise specks relative to ink extent
54+
ink_bb = g.boundingBox()
55+
ink_w = ink_bb[2] - ink_bb[0]
56+
ink_h = ink_bb[3] - ink_bb[1]
57+
ink = fontforge.layer()
58+
for c in pass1:
59+
cb = c.boundingBox()
60+
if (cb[2] - cb[0]) < ink_w * 0.10 and (cb[3] - cb[1]) < ink_h * 0.10:
61+
continue
62+
ink += c
63+
g.foreground = ink
64+
65+
scratch.save(clean_svg_path + '.sfd') # FontForge can't export single-glyph SVG directly
66+
# Export via generate — write to a temp SFD then export the glyph as SVG
67+
g.export(clean_svg_path)
68+
os.remove(clean_svg_path + '.sfd')
69+
70+
71+
def extract_symbol(arr, r0, r1, c0, c1, name):
72+
"""Crop glyph region, upsample, binarise, run potrace, clean, save SVG."""
73+
crop = arr[r0:r1, c0:c1]
74+
big = Image.fromarray(crop).resize(
75+
(crop.shape[1] * UPSAMPLE, crop.shape[0] * UPSAMPLE),
76+
Image.BILINEAR)
77+
binary = (np.array(big) >= THRESHOLD).astype(np.uint8) * 255
78+
79+
with tempfile.TemporaryDirectory() as tmp:
80+
png_path = os.path.join(tmp, f'{name}.png')
81+
pbm_path = os.path.join(tmp, f'{name}.pbm')
82+
raw_svg = os.path.join(tmp, f'{name}_raw.svg')
83+
Image.fromarray(binary, mode='L').save(png_path)
84+
subprocess.check_call(['convert', png_path, '-threshold', '50%', pbm_path])
85+
subprocess.check_call(['potrace', '-s', pbm_path, '-o', raw_svg])
86+
svg_path = os.path.join(OUT_DIR, f'{name}.svg')
87+
_clean_potrace_svg(raw_svg, svg_path)
88+
89+
print(f' wrote {svg_path}')
90+
return svg_path
91+
92+
93+
# ---------------------------------------------------------------------------
94+
# Hand-drawn extras (generator/extras/*.png)
95+
# Each file is a full-glyph image (no cropping needed). RGBA images are
96+
# composited onto white before thresholding so transparent areas read as white.
97+
# A lower upsample factor is used since these images are already high-res.
98+
# ---------------------------------------------------------------------------
99+
100+
EXTRAS_DIR = 'extras'
101+
102+
EXTRAS = [
103+
'eszett', # ß U+00DF / ẞ U+1E9E source
104+
]
105+
106+
print('Extracting hand-drawn extras...')
107+
for name in EXTRAS:
108+
src_path = os.path.join(EXTRAS_DIR, f'{name}.png')
109+
arr_extra = np.array(Image.open(src_path).convert('L'))
110+
h, w = arr_extra.shape
111+
extract_symbol(arr_extra, 0, h, 0, w, name)
112+

0 commit comments

Comments
 (0)