Phase 3: a chord chart as SVG, with text that holds its width on a machine we do not own - #6
Merged
Merged
Conversation
… own `plainsong chart song.song -o chart.svg` renders a chord chart, standalone, with nothing installed. It commits to a repository and embeds with `<img src="chart.svg">`, which is the only way a chart appears in markdown on a platform we do not control: GitHub strips a raw `<svg>`. That one fact decides most of the design. No webfont can be fetched, so the chart is drawn in whatever the reader already has -- and the layout still has to hold. Widths are computed in Python from a table shipped in the package and then declared with `textLength` and `lengthAdjust="spacingAndGlyphs"`, which SVG treats as geometry rather than style, so the browser fits the string to the width we planned for. Plain `spacing` cannot do it: it distributes the n-1 gaps between characters, so a one-character symbol like `C` has nothing to adjust. An `<img>` also cannot inherit the host page's colour, so the chart carries its own background and reads prefers-color-scheme to stay legible in a dark README. Bars are as wide as their contents. Each symbol sits at `unit * width`, so the next one is clear only when `width * (next_unit - unit)` covers this symbol's advance plus a gap; the largest such requirement across the chart solves it exactly, in one pass, no iteration. `| Cmaj7#11 Abm7b5 Db7alt Gbmaj9 |` and `| C . . . |` do not need the same room, and a fixed width either wastes the page or overlaps the symbols depending on the song. Positions come from Arrangement.grid and nowhere else, which is what Phase 1 was for: a chart cannot disagree with the audio about when a chord arrives, because it computes nothing that could disagree. Symbols are read from every row rather than only `Chords:` -- in the relative dialect a row mixing roman numerals with scale degrees reads as melody, and a chart taking only the chord row drew a page of empty bars for a piece whose harmony is written down perfectly plainly. That was visible in the first render and is the reason to look at output rather than at tests. Accidentals fold to ASCII on the way out. Liberation Sans carries U+266F sharp and carries neither U+266D flat nor U+266E natural, which is the wrong half to lose for a songbook full of flats and a parser that accepts E7-flat-9. tools/extract_font_widths.py generates render/fontmetrics.py from a real TrueType file with nothing but `struct` -- head, hhea, hmtx and a format 4 cmap. It measures the bold face separately, because `m`, `b` and `j` differ between the weights and those are precisely what chord symbols are made of; measuring regular and drawing bold made lengthAdjust squeeze every glyph and it was visible as smeared letters in the first screenshot. Reading the emitted SVG also caught a defect no test would have: the left edge of the final bar was drawn thick. That is an interior barline however near the end of the piece it falls, and only the line closing the last bar is heavy. Now tested. It is a chord chart and not an engraver: no noteheads, no staff, no beaming, and docs/chart.md lists what it deliberately does not do rather than leaving someone to find out. 655 tests, 7 specs, ruff clean, 6,340 sources checked, 6,321 files compiling to exactly the music they did. Rendered and inspected in headless Chromium in light and dark, and exercised from a wheel installed into a clean venv outside the source tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A standalone file, so it commits to a repository and embeds with
'<img src="chart.svg">'— the only way a chart appears in markdown on a platformwe do not control, because GitHub strips a raw
<svg>.That one fact decides most of the design:
already has — and the layout still has to hold.
<img>cannot inherit the host page's colour, so the chart carries itsown background. A transparent chart is black ink on black in a dark README.
It reads
prefers-color-schemeand is legible either way.Text that holds its width
Widths are computed in Python from a table shipped in the package, then declared:
SVG treats both as geometry rather than style, so the browser fits the
rendered string to the width we planned for. Plain
spacingcannot do the job —it distributes the n−1 gaps between characters, so a one-character symbol
like
Chas nothing to adjust and renders at whatever width the substitutedfont gives it.
Regular and bold are measured separately.
m,bandjdiffer betweenthe faces and those are precisely what chord symbols are made of. Measuring
regular and drawing bold made
lengthAdjustsqueeze every glyph into a widththe text did not have — visible as smeared letters in the very first screenshot.
Bars are as wide as their contents
Each symbol sits at
unit × width, so the next is clear only whenwidth × (next_unit − unit)covers this symbol's advance plus a gap:Exact, one pass, no iteration.
| Cmaj7#11 Abm7b5 Db7alt Gbmaj9 |and| C . . . |do not need the same room — a fixed width either wastes the pageor overlaps the symbols, and which one depends on the song.
What Phase 1 bought
Positions come from
Arrangement.gridand nowhere else. A chart cannotdisagree with the audio about when a chord arrives, because it computes nothing
that could disagree.
Symbols are read from every row rather than only
Chords:. In the relativedialect a row mixing roman numerals with scale degrees reads as melody, so a
chart taking only the chord row drew a page of empty bars for a piece whose
harmony is written down perfectly plainly. That was visible in the first render,
and is the argument for looking at output rather than only at tests.
Reading the emitted SVG caught a second defect no test would have: the left
edge of the final bar was drawn thick. That is an interior barline however near
the end of the piece it falls. Now fixed and tested.
Accidentals fold to ASCII
Liberation Sans carries U+266F ♯ and carries neither U+266D ♭ nor U+266E ♮ —
the wrong half to lose for a songbook full of flats and a parser that accepts
E7♭9. SoE7♭9draws asE7b9, and the command warns if anything undrawablesomehow reaches the page.
The width table
tools/extract_font_widths.pygeneratesrender/fontmetrics.pyfrom a realTrueType file with nothing but
struct—head,hhea,hmtxand a format-4cmap. The table has to ship: a renderer cannot read a font off the machine itruns on, because it is not there on Windows or macOS and this package hardcodes
no paths.
Widths are Liberation Sans, metric-compatible with Arial — the narrowest
claim the evidence supports. No Helvetica was available to measure, and
Liberation's
Mis 833/1000 em, Arial's figure rather than Helvetica's 889.Engraving constants, from primary sources
thinBarlineThicknessthickBarlineThicknessSMuFL's other figure, 0.2 em, is for text metrics — glyphs set inline in
prose. A chart using Bravura's engraving defaults is on the scoring
registration, so
font-size = 4 × staff_space.Scope
It is a chord chart, not an engraver: no noteheads, no staff, no beaming,
and the melody is not drawn.
docs/chart.mdlists what it deliberately does notdo — repeats, musical line breaking, font subsetting — rather than leaving
someone to discover it.
Verification
655 tests, 7 specs, ruff clean,
ok 6340 file(s) checked, 3 warning(s), and6321 file(s) compile exactly as recorded.Rendered and inspected in headless Chromium in light and dark, and exercised
from a wheel installed into a clean venv outside the source tree — the
width table ships, which is the failure mode this repository has had before with
packaged data.
One thing I could not verify: that GitHub renders the committed
.svgthroughan
<img>as intended. The design follows from the documented behaviour, andthe image at the top of this description is the actual file — so this PR page is
itself the test.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc
Generated by Claude Code