Skip to content
Merged
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.
2,498 changes: 1,517 additions & 981 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.
Comment thread
pelson marked this conversation as resolved.
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.
2 changes: 2 additions & 0 deletions xkcd-script/generator/pt4_additional_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ def extract_symbol(arr, y0, y1, x0, x1, name, exclude=None):

# Each entry is (output_name, source_filename_stem).
EXTRAS = [
('notdef', '1913_i_2x__notdef'), # .notdef fallback glyph source
('square', '2251_alignment_chart_2x__square'), # □ U+25A1 source
('eszett', 'eszett'), # ß U+00DF / ẞ U+1E9E source
('lambda', '1145_sky_color_2x__lambda'), # λ U+03BB source
('tau', '2520_symbols_2x__tau'), # τ U+03C4 source
Expand Down
39 changes: 39 additions & 0 deletions xkcd-script/generator/pt5_svg_to_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ def charname(char):
c = font.createMappedChar(32)
c.width = 256

c = font.createChar(0x0000, '.null') # U+0000: null, zero-width; required by OpenType
c.width = 256
c = font.createChar(0x000D, 'nonmarkingreturn') # U+000D: carriage return, zero-width; required by OpenType
c.width = 256


# ---------------------------------------------------------------------------
# Glyphs imported from xkcd comic images
Expand Down Expand Up @@ -391,6 +396,23 @@ def _import_comic_glyph(font, name, svg_path, target_top, weight_delta=0):
return g


# .notdef is shown for any codepoint the font doesn't cover; OpenType requires it.
# Hand-drawn source: xkcd #1913 (I), a sketchy question-mark-in-a-box.
_notdef_svg = os.path.join(_COMIC_CHARS_DIR, 'notdef.svg')
_notdef_src = _import_comic_glyph(font, 'notdef', _notdef_svg, target_top=font.ascent, weight_delta=20)
_bb = _notdef_src.boundingBox()
_notdef_src.transform(psMat.translate(0, -_bb[1]))
_bb = _notdef_src.boundingBox()
if _bb[3] > 0:
_notdef_src.transform(psMat.scale(font.ascent / _bb[3]))
_notdef_src.width = int(round(_notdef_src.boundingBox()[2] + 20))
c = font.createChar(-1, '.notdef')
c.clear()
for _cont in _notdef_src.foreground:
c.foreground += _cont
c.width = _notdef_src.width


# Greek letters vectorised by pt4 from xkcd comic images.
# Each entry: (svg_name, unicode_cp, ref_char_for_height, baseline_snap)
# baseline_snap=True → translate so bb[1]=0 (letters that sit on the baseline).
Expand Down Expand Up @@ -592,6 +614,23 @@ def _import_comic_glyph(font, name, svg_path, target_top, weight_delta=0):
_ch.width = _g.width


# □ U+25A1 WHITE SQUARE — hand-drawn source from extras/square.png.
_square_svg = os.path.join(_COMIC_CHARS_DIR, 'square.svg')
_target_top_sq = font['H'].boundingBox()[3]
_square_src = _import_comic_glyph(font, 'square', _square_svg, target_top=_target_top_sq, weight_delta=45)
_bb = _square_src.boundingBox()
_square_src.transform(psMat.translate(0, -_bb[1]))
_bb = _square_src.boundingBox()
if _bb[3] > 0:
_square_src.transform(psMat.scale(_target_top_sq / _bb[3]))
_square_src.width = int(round(_square_src.boundingBox()[2] + 20))
_ch = font.createMappedChar(0x25A1)
_ch.clear()
for _cont in _square_src.foreground:
_ch.foreground += _cont
_ch.width = _square_src.width


# ---------------------------------------------------------------------------
# Save
# ---------------------------------------------------------------------------
Expand Down
26 changes: 24 additions & 2 deletions xkcd-script/generator/pt6_derived_chars.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,33 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
# Glyph aliases and re-uses
# ---------------------------------------------------------------------------

# U+20DE COMBINING ENCLOSING SQUARE — zero-width mark sized and positioned to
# enclose '?' with a small margin. GPOS would be needed for full generality.
_sq = font[0x25A1]
_sq_bb = _sq.boundingBox()
_sq_cx = (_sq_bb[0] + _sq_bb[2]) / 2
_sq_h = _sq_bb[3] - _sq_bb[1] # sq_bb[1] == 0 after baseline snap
_q_bb = font[ord('?')].boundingBox()
_q_adv = font[ord('?')].width
_margin = 20
_scale_y = (_q_bb[3] - _q_bb[1] + 2 * _margin) / _sq_h
_x_offset = -_q_adv / 2 - _sq_cx
_y_offset = _q_bb[1] - _margin
c = font.createMappedChar(0x20DE)
c.addReference(_sq.glyphname, psMat.compose(
psMat.scale(1, _scale_y),
psMat.translate(_x_offset, _y_offset),
))
c.width = 0

# Vertical pipe: re-use the I glyph (same stroke, same weight).
c = font.createChar(-1, 'I.sansserif')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, it is true that this could be in pt4... is this what you meant @dummy-index?
And we do it the other way around - we define the I.sansserif character, and alias that to I.

Copy link
Copy Markdown
Contributor Author

@dummy-index dummy-index Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove 'I.sansserif' in this PR for now. I just wanted to keep it on the mono side just in case.
pipe.addReference('I', psMat.compose(psMat.scale(1, 1.3), psMat.translate(0, -0.2 * font.ascent)))
(When generating mono, call pipe.unlinkRef() before replacing 'I')

If the '<|' and '|>' in handwriting_minimal are supposed to mean bracket notation in the first place, they should be adopted as separate symbols rather than as ligatures; in other words, the bar glyph actually exists. It's just that it's too short, so it's tricky to decide whether to modify it. (Ideally, for positional relationships, it should be optimized to enclose lowercase letters like in XKCD 2034.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to ship what we have in the PR - happy to discuss in a dedicated issue if we can improve the vertical pipe size 👍

c.addReference('I')
c.width = font['I'].width
pipe = font.createMappedChar(ord('|'))
pipe.clear()
pipe.addReference('I')
pipe.width = font['I'].width
pipe.addReference('I.sansserif', psMat.compose(psMat.scale(1, 1.3), psMat.translate(0, -0.2 * font.ascent)))
pipe.width = font['I.sansserif'].width



Expand Down
Binary file modified xkcd-script/samples/charmap_basic_latin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified xkcd-script/samples/charmap_combining_diacritical_marks.png
Comment thread
pelson marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified xkcd-script/samples/charmap_general_punctuation.png
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 modified xkcd-script/samples/charmap_non_latin_other.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 47 additions & 4 deletions xkcd-script/samples/gen_charmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""
import os
import re
import sys
import unicodedata

import matplotlib
Expand Down Expand Up @@ -43,19 +44,57 @@
(0x0180, 0x0250, "Latin Extended-B"),
(0x0300, 0x0370, "Combining Diacritical Marks"),
(0x0370, 0x0400, "Greek and Coptic"),
(0x2018, 0x2040, "General Punctuation"),
(0x1E00, 0x1F00, "Latin Extended Additional"),
(0x2000, 0x2070, "General Punctuation"),
(0x2190, 0x2200, "Arrows"),
(0x2200, 0x2300, "Mathematical Operators"),
]

COLS = 16
COMBINING_CATS = {'Mn', 'Mc', 'Me'}

# Any font glyphs not covered by the defined blocks go in a catch-all block.
# Explicit ordered list of codepoints that fall outside all named blocks above.
# Order here controls layout in the "Non-Latin / Other" charmap table.
# If the font contains a glyph outside every named block that is not listed
# here, the script errors and tells you to add it.
# Use None to reserve a slot (renders as a blank cell) so that removing a
# character doesn't shift subsequent entries and cause a noisy table diff.
EXTRAS_ORDER = [
0x025B, # ɛ LATIN SMALL LETTER OPEN E
0x1F382, # 🎂 BIRTHDAY CAKE
0x20DE, # ⃞ COMBINING ENCLOSING SQUARE
0x25A1, # □ WHITE SQUARE
]

block_covered = set()
for start, end, _ in BLOCKS:
block_covered.update(range(start, end))
extras = sorted(cp for cp in present if cp not in block_covered)

# Control/format characters (Cc, Cf) have no visual representation; skip them.
def _is_invisible(cp):
try:
return unicodedata.category(chr(cp)) in ('Cc', 'Cf')
except (ValueError, OverflowError):
return False

extras_in_block = [cp for cp in EXTRAS_ORDER if cp is not None and cp in block_covered]
if extras_in_block:
lines = [f" U+{cp:04X} {unicodedata.name(chr(cp), '(unknown)')}" for cp in extras_in_block]
raise ValueError(
"EXTRAS_ORDER contains codepoints already covered by a named block:\n"
+ "\n".join(lines)
)

extras_cps = set(cp for cp in EXTRAS_ORDER if cp is not None)
uncovered = sorted(cp for cp in present if cp not in block_covered and cp not in extras_cps and not _is_invisible(cp))
if uncovered:
lines = [f" U+{cp:04X} {unicodedata.name(chr(cp), '(unknown)')}" for cp in uncovered]
raise ValueError(
"Font contains codepoints not in any named block or EXTRAS_ORDER.\n"
"Add them to EXTRAS_ORDER in gen_charmap.py:\n" + "\n".join(lines)
)

extras = list(EXTRAS_ORDER)
if extras:
BLOCKS = list(BLOCKS) + [(None, None, "Non-Latin / Other")]

Expand Down Expand Up @@ -134,14 +173,18 @@ def render_block(label, rows):
y_bottom = y_top - CELL_H
y_center = (y_top + y_bottom) / 2

hex_tag = f'U+{cps[0]:04X}' if cps else ''
first_cp = next((c for c in cps if c is not None), None)
hex_tag = f'U+{first_cp:04X}' if first_cp is not None else ''
ax.text(LABEL_W - 0.05, y_center, hex_tag,
ha='right', va='center', fontproperties=fp_tiny, color='#555555')

for col, cp in enumerate(cps):
x_left = LABEL_W + col * CELL_W
x_center = x_left + CELL_W / 2

if cp is None:
continue

try:
cat = unicodedata.category(chr(cp))
except (ValueError, OverflowError):
Expand Down
Loading