Files
noteman-slicer/docs/adr/0003-lossless-webp-with-levels-and-alpha-quantisation.md
T
Esa Kataja 46ae7e813a Replace the handoff notes with a durable spec and ADRs
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).
2026-07-28 22:05:09 +03:00

59 lines
2.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lossless WebP, with levels and alpha quantised to 16 levels
Slice images are encoded as **lossless WebP**, with the levels adjustment applied
and the alpha channel quantised to 16 levels. About 7 KB per slice, ~450 KB for a
65-system song. Every lossy option and every alternative format measured
*larger* for this content, which is the opposite of the usual intuition — hence
this record.
## The measurement
20 slices of one real song, levels applied throughout, relative to plain lossless
WebP:
| | vs baseline | |
|---|---|---|
| **WebP lossless + alpha quantised to 16** | **68%** | chosen |
| AVIF q60 | 90% | lossy, for 10% |
| WebP lossless | 100% | baseline |
| WebP lossy q85 (alpha) | 107% | |
| AVIF q85 | 114% | |
| JXL lossless | 130133% | |
| WebP lossy q85 (opaque ink-on-white) | 158% | |
| PNG grayscale + alpha | 165% | |
| AVIF lossless | 188% | |
Separately, before levels: applying levels alone takes 338 KB → 211 KB, a 38%
reduction.
## Four results that contradict an instinct
- **Lossy is bigger than lossless here.** Not a quality problem — the measured
difference between q85 and lossless is max 12/255, mean 0.33, i.e. invisible.
Lossy VP8 simply spends more bits on sharp black/white edges than VP8L's
palette and predictor transforms do, and notation is nothing but sharp edges.
The "q85 looks fine" intuition comes from photographs and inverts here.
- **AVIF and JXL both lose**, AVIF lossless by nearly 2×. Their lossless modes
are afterthoughts on photo codecs. WebP's VP8L is close to purpose-built for
flat two-tone line art — sheet music is the content type it is best at. JXL
additionally has no path forward in Chrome.
- **Alpha costs nothing.** Opaque ink-on-white and black-plus-alpha are within
0.1% at lossless, so paper-tint removal and future non-rectangular slices are
free.
- **Levels is the single biggest lever** — 38%, as a side effect of a control
that exists for quality reasons anyway. Pushing the white point below the
paper's luminance sets vast regions to exactly `alpha = 0`, which costs almost
nothing to encode.
Alpha quantisation to 16 levels is imperceptible: antialiased edges span 23 px
at 1920, and 16 steps across that is below notice. 8 levels starts to gamble on
thin strokes.
## Rejected as not worth it
- **Encoder effort tuning** — Pillow's `method=6` buys 3% and a dependency.
- **`alpha_quality=60`** — 24%, for less control than quantisation gives.
- **Grayscale WebP** — no such mode exists. It wouldn't help anyway: the RGB
channels are constant black and compress to nearly nothing, so alpha is the
entire payload.