The handoff was written as a message to relay information; several decisions lived only there. Split into permanent homes: - docs/spec.md — scope, geometry model, pipeline order, detection, levels, editor, project file, markers, bundle format, noteman's changes, reference values - ADR 0002 — raster only in release 1; SVG slices measured at 40x WebP naive, 2.6x with a bounding-box cull, deferred on risk not size - ADR 0003 — lossless WebP with levels and 16-level alpha; every lossy option and alternative format measured larger for line art - ADR 0004 — detection proposes, the human disposes; no unattended mode - ADR 0005 — PyMuPDF for all PDF access, accepting AGPL - ADR 0006 — systems are found by vertical brackets, not row-darkness gaps, which miscount every page of a 6-voice score Also from testing against the hardest score in the repertoire: scanned PDFs carry their scan as an embedded image and must be extracted at native resolution rather than re-rendered at 600 DPI, and per-page deskew is mandatory (skew varies -2.6 to +1.2 within one PDF).
2.9 KiB
Raster only in release 1; the SVG renderer is deferred
Vector PDFs are most of the newer corpus, and keeping them vector all the way to the viewer was an early goal — sheet music is line art, and SVG stays crisp at any tablet zoom. We measured it before building it, and decided to rasterize vector sources like everything else in release 1 and revisit the SVG renderer once real songs have been cut.
The measurement
One real vector song, 6 pages, 65 systems, rendered both ways:
| Approach | Total | vs WebP |
|---|---|---|
| WebP slices (600 DPI → 1920, ink→alpha, lossless) | 1.19 MB | 1× |
SVG, naive viewBox + clipPath |
26.0 MB | 40× |
SVG, set_cropbox per band |
26.5 MB | 41× |
| SVG, bounding-box cull + glyph subset | 3.09 MB | 2.6× |
- The naive cut is unusable. A
viewBox+clipPathslice contains the entire page's geometry and merely hides eleven-twelfths of it. set_cropboxdoes not help. MuPDF renders full page content regardless of the crop, so there is no free version of the cull.- The cull works. PyMuPDF emits a
<defs>glyph table (111 KB of a 256 KB page) referenced by<use transform="matrix(...)">, plus body<path>elements. Filter both by y-extent, then keep only the glyphs the survivors reference. Roughly 50 lines, 15× improvement.
Why defer, given the cull works
Not size. At 3.1 MB vs 1.2 MB per song — 225 MB vs 87 MB across a 73-song corpus — both are nothing on a homelab. The measurement killed the lazy implementation, not the idea.
What defers it is risk and missing evidence:
- The cull is heuristic parsing: glyph extents bounded at baseline ±14pt,
path extents read from raw
dcoordinates. It is over-inclusive by design, so it fails safe — but "fails safe" still means a slice quietly carrying a neighbour's slur, or a hairline dropped because the y-window was wrong on some publisher's output. That needs eyeballing per song, a QA loop the raster path doesn't have. - Rendering 65 complex SVGs in a scrolling column may be slower than 65 WebPs. Unmeasured.
- The deciding question is unanswerable from here: does 1920px WebP actually feel insufficient when pinch-zooming on a tablet? Cutting real songs answers it; more measurement doesn't.
Vector PDFs are also the clean case for the raster path — deskew is a no-op, detection works best, there are no scan artefacts — so rasterizing them is not a degraded fallback.
Consequences
- The geometry model stays renderer-agnostic, in normalised page coordinates, so adding the SVG renderer later is an output stage rather than a redesign.
- Re-export from the project file regenerates every song's bundle without repeating human work, so songs cut before the SVG renderer exists are not stranded.
- noteman needs no SVG support (
image/svg+xml,.svgcontent type, CSP header on SVG responses) until the renderer ships.