Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified xkcd-script/font/xkcd-script.otf
Binary file not shown.
30,963 changes: 18,427 additions & 12,536 deletions xkcd-script/font/xkcd-script.sfd

Large diffs are not rendered by default.

Binary file modified xkcd-script/font/xkcd-script.ttf
Binary file not shown.
Binary file modified xkcd-script/font/xkcd-script.woff
Binary file not shown.
25 changes: 25 additions & 0 deletions xkcd-script/generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# xkcd-script font generation pipeline

Each script runs in order inside the `fontbuilder` Docker image; `run.sh` orchestrates them and accepts an optional starting step (`./run.sh 5` skips pt1–pt4).

## Stages

| # | Script | What it does |
|---|---|---|
| 1 | `pt1_character_extraction.py` | Extract character strokes from `handwriting_minimal.png` (scikit-image). |
| 2 | `pt2_character_classification.py` | Cluster strokes into lines (k-means, fixed seed). |
| 3 | `pt3_ppm_to_svg.py` | Convert per-character PPM → SVG via `potrace`. |
| 4 | `pt4_additional_sources.py` | Trace extra glyphs from comic panels and `extras/`. |
| 5 | `pt5_svg_to_font.py` | Import SVG glyphs into a FontForge SFD; apply stroke normalisation, weight nudges, math-symbol imports. |
| 6 | `pt6_derived_chars.py` | Build derived/composed glyphs: diacritics, ligatures, Greek, IPA, combining marks, math cmap aliases (U+1D400 block via altuni). |
| 7 | `pt7_font_properties.py` | Apply kerning, GPOS anchors, pin CFF hints and OS/2 metrics. Output is `xkcd-script-pt7.sfd` — the **base** font used for everything downstream. |
| 8 | `pt8_derivatives.py` | Orchestrator. Runs each `pt8X_*.py` derivative step in turn. |
| 9 | `pt9_gen_reprod_font.py` | Scrub the SFD for reproducibility, freeze CFF charstrings, generate committed binaries (otf/ttf/woff). |

## Derivatives (pt8)

`pt7` produces a single kitchen-sink base SFD with everything — Latin, Greek, math symbols and aliases, ligatures, combining marks. Each `pt8X_<name>.py` reads that base and either writes its own derivative SFD or extracts data from it to splice elsewhere; `pt8_derivatives.py` runs them with `runpy`.

Today there is no live derivative font: the sole entry, `pt8a_mathjax3.py`, only extracts extensible-glyph outline data into `../xkcd-mathjax3.js`. The display-sized large operators that used to live in a separate mathjax3 WOFF are now stylistic alternates in the base font (`ss01`).

Because derivatives can only subtract or overlay what pt7 already has, **everything plausibly useful belongs in pt7**. Don't pre-strip pt7 for size — that loses the subtractive option.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added xkcd-script/generator/extras/sqrt_vertical.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 14 additions & 2 deletions xkcd-script/generator/pt2_character_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
U N A U T H O R I T A T I V E N E S S L EA T H E R B A R K I N T R A CO L I C M I CR OCH E L I A
O F F S I D ER G LA S S W EE D R O TT O L O A LB E R T I T E H ER M A T O RR H A C H I S
O R G A N O M E T A LL I C S E G R E G A T I ON I S T U N E V A N G E L I C CA M PS TO O L
+ - x * ! ? # @ $ % ¦ & ^ _ — – - ( ) [ ] { } / \ < > ÷ ± √ Σ
+ - × * ! ? # @ $ % ¦ & ^ _ — – - ( ) [ ] { } / \ < > ÷ ± √ Σ
1 2 3 4 5 6 7 8 9 0 ∫ = ≈ ≠ ~ ≤ ≥ |> <| 🎂 . , ; : “ H I ” ’ ‘ C A N ' T ' "
É Ò Å Ü ≪ ≫ ‽ Ē Ő “ ”
""".strip()
Expand All @@ -74,7 +74,7 @@ def merge(img1, img1_bbox, img2, img2_bbox):
shape = bbox[3] - bbox[1], bbox[2] - bbox[0], 3
img1_slice = [slice(img1_bbox[1] - bbox[1], img1_bbox[3] - bbox[1]),
slice(img1_bbox[0] - bbox[0], img1_bbox[2] - bbox[0])]

img2_slice = [slice(img2_bbox[1] - bbox[1], img2_bbox[3] - bbox[1]),
slice(img2_bbox[0] - bbox[0], img2_bbox[2] - bbox[0])]

Expand Down Expand Up @@ -102,6 +102,18 @@ def merge(img1, img1_bbox, img2, img2_bbox):



_LIGATURE_SOURCES = [
(5, 4, 5, 'TH'), # UNAUTHORITATIVENESS: T + H
(5, 31, 32, 'TR'), # INTRACOLIC: T + R
(6, 24, 25, 'TI'), # ALBERTITE: T + I
]
for line_no, left_no, right_no, lig in _LIGATURE_SOURCES:
_, left_bbox, left_img = characters_by_line[line_no][left_no]
_, right_bbox, right_img = characters_by_line[line_no][right_no]
lig_img, lig_bbox = merge(left_img, left_bbox, right_img, right_bbox)
characters_by_line[line_no].append([lig, lig_bbox, lig_img])


import skimage.io

if not os.path.isdir('../generated/characters'):
Expand Down
21 changes: 19 additions & 2 deletions xkcd-script/generator/pt4_additional_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,23 @@ def _clean_potrace_svg(raw_svg_path, clean_svg_path):
os.remove(clean_svg_path + '.sfd')


def extract_symbol(arr, y0, y1, x0, x1, name, exclude=None):
def extract_symbol(arr, y0, y1, x0, x1, name, exclude=None, pad=0):
"""Crop glyph region, upsample, binarise, run potrace, clean, save SVG.

exclude: optional list of (y0, y1, x0, x1) regions in full-image coordinates
to blank out (set to background) before potrace, for removing
artefacts that cannot be separated by tightening the main crop.
pad: white-pixel border added around the crop before upsampling. Use
when the source PNG is tightly cropped and ink touches the edge —
potrace otherwise produces edge artefacts where contours run into
the canvas boundary.
"""
crop = arr[y0:y1, x0:x1].copy()
if exclude:
for ey0, ey1, ex0, ex1 in exclude:
crop[ey0 - y0:ey1 - y0, ex0 - x0:ex1 - x0] = 255
if pad:
crop = np.pad(crop, pad, mode='constant', constant_values=255)
big = Image.fromarray(crop).resize(
(crop.shape[1] * UPSAMPLE, crop.shape[0] * UPSAMPLE),
Image.BILINEAR)
Expand Down Expand Up @@ -163,14 +169,25 @@ def extract_symbol(arr, y0, y1, x0, x1, name, exclude=None):
('AElig', '2763_linguistics_gossip_2x__AE'), # Æ U+00C6 source
('cedilla', '2034_equations_2x__cedilla.hand-tweaked'), # hook cedilla mark source
('epsilon', '2034_equations_2x__epsilon'), # ε U+03B5 source
('Lambda', '2034_equations_2x__Lambda'), # Λ U+039B source
('rounded_d', '2520_symbols_2x__rounded_d'), # ∂ U+2202 source
('infinity', '2343_mathematical_symbol_fight_2x__infinity'), # ∞ U+221E source
('right_double_arrow', '2343_mathematical_symbol_fight_2x__right_double_arrow'), # ⇒ U+21D2 source
('right_half_arrow', '2343_mathematical_symbol_fight_2x__right_half_arrow'), # ⇀ U+21C0 source
('right_lim_arrow', '2343_mathematical_symbol_fight_2x__right_lim_arrow'), # → U+2192 source
('triangle', '2343_mathematical_symbol_fight_2x__triangle'), # △ U+25B3 source
('circled_times', '2034_equations_2x__circled_times'), # ⊗ U+2297 source
('sqrt_vertical', 'sqrt_vertical'), # √ tall surd, unencoded glyph `radical.tall`
('braceleft_tall', '2435_geothmetic_meandian_2x__brace__shortened'), # { tall brace for MathJax stretchy assembly
('parenleft_tall', '2059_modified_bayes_theorem_2x__lparen'), # ( tall paren for MathJax stretchy assembly (\binom, \left(, pmatrix); right paren is mirrored in pt5
]

print('Extracting hand-drawn extras...')
for name, filename in EXTRAS:
src_path = os.path.join(EXTRAS_DIR, f'{filename}.png')
arr_extra = np.array(Image.open(src_path).convert('L'))
h, w = arr_extra.shape
extract_symbol(arr_extra, 0, h, 0, w, name)
extract_symbol(arr_extra, 0, h, 0, w, name, pad=10)


# ---------------------------------------------------------------------------
Expand Down
Loading