pdf-quiz-generator/docs/image-overlays.md
Daniel 809ac2fca6 feat: figures are thumbnails that open into a viewer, with source and overlay
A 2,000px radiograph written into an article rendered at whatever width it
happened to be — a wall of greyscale in the middle of a sentence, four
megabytes to draw it, and no way to look at it properly. Every image in prose
is now a 256px thumbnail with the author's label under it, and a click gives it
the screen.

The viewer puts what is known about the figure beside it: its title, what it
shows, and where it came from. `media_assets` gains `source` and `source_url`
for that — a citation belongs to the file, because the same figure used in
three articles is cited the same way in all three, and a licence that turns out
to be wrong is one row to fix rather than three paragraphs to find. Asked for
when the figure is opened, not when the page is drawn.

And `overlay`: the regions an educator has marked, as vector shapes in
normalised coordinates on the unit square, so one drawing is correct in a
thumbnail, in the viewer and on a projector. Off until the learner turns it on
— marks shown before they have looked answer the question for them. Vectors
rather than a second burnt-in picture, for four reasons written down in
docs/image-overlays.md. The tool that draws them is next; this is the storage,
the contract and the reader's half.

On a narrow screen the description stacks above the image rather than beside
it, where it can be read before scrolling to the picture.

Also here: `classify_question_difficulty`, which labelled all 2,924 questions
in batches of twenty-five against a written rubric — 622 easy, 1,634 medium,
668 hard, no failed batches. The column had been NULL on every row since it
existed. Migration l2c3d4e5f6a7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 20:33:00 +02:00

60 lines
2.4 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.

# Image overlays
An educator can mark regions on an image — the lead lines on a knee film, the
level of a narrowing, the border of a lesion — and a learner sees them only when
they ask. This is the contract for how those marks are stored and drawn.
## Why vectors and not a second picture
The obvious implementation is a second PNG with the marks burnt in, shown
instead of the original. It fails four ways: it cannot be turned off (which is
the whole point — the learner should look first and check second), it cannot be
corrected without redrawing, it doubles the storage for every marked image, and
it is correct at exactly one size. Vectors have none of those problems and draw
as crisply on a phone as on a monitor.
## The shape of the data
`media_assets.overlay`, JSON, null when nothing is marked:
```json
{
"shapes": [
{
"kind": "path",
"points": [[0.31, 0.52], [0.34, 0.51], [0.38, 0.53]],
"color": "#5eead4",
"width": 0.006,
"label": "Dense metaphyseal bands"
},
{ "kind": "rect", "x": 0.12, "y": 0.4, "w": 0.2, "h": 0.15, "color": "#5eead4" },
{ "kind": "ellipse", "cx": 0.5, "cy": 0.5, "rx": 0.1, "ry": 0.08, "color": "#f0a53d" },
{ "kind": "arrow", "points": [[0.2, 0.2], [0.4, 0.35]], "color": "#5eead4" }
]
}
```
**Every coordinate is normalised to 01** against the image's own width and
height — never pixels. That is what makes one drawing correct in a 256px
thumbnail, in the viewer, and on a projector. `width` is normalised too, against
the image width, so a stroke stays the same relative weight.
Four kinds, and no more without a reason: `path` (freehand or polyline, the one
that traces an anatomical edge), `rect`, `ellipse`, `arrow` (a `path` of exactly
two points, drawn with a head). `label` is optional on any shape and is what a
screen reader is given.
## Drawing it
An SVG with `viewBox="0 0 1 1"` and `preserveAspectRatio="none"`, absolutely
positioned over the image at the same size. Because the viewBox is the unit
square, the stored numbers are the SVG's own coordinates and no conversion is
needed at any size.
## Rules
- **Off by default.** The learner sees the image, and turns the overlay on.
Marks shown before the learner has looked answer the question for them.
- **The original is never modified.** Nothing here writes to the image file.
- **An overlay is not a caption.** What the marks *mean* belongs in the
description; the overlay says where.