Frame Sidecars
On disk a photograph is exactly two files: the image (WDR-260504-0001-A.jpg)
and a sidecar beside it with the same stem (WDR-260504-0001-A.json), holding
everything a scanner can't know.
{
"version": 1,
"id": "9d3a7c2e-8b41-4f0a-b6d5-2e7c9a1f4b30",
"addedAt": "2026-05-04T10:22:31.004",
"capturedAt": "2026-04-28T00:00:00.000",
"favorite": true,
"albums": ["7c1e…", "b204…"],
"camera": "Pentax 17",
"lens": "HD Pentax 25mm f/3.5",
"film": "Kodak Gold 200",
"notes": "Pushed one stop.",
"location": { "lat": 41.9028, "lng": 12.4964, "name": "Rome" },
"source": {
"originalName": "scan_0047.jpg",
"half": "left",
"splitAt": 1565,
"rotation": 0
},
"width": 1490,
"height": 2075
}
Fields
| Key | Type | Notes |
|---|---|---|
version | int | Always 1 |
id | string | UUID v4. Falls back to the filename when missing |
addedAt | string | ISO 8601. When Winder filed it. Falls back to the file's modified time |
capturedAt | string | ISO 8601. When the shot was taken. Absent until set |
favorite | bool | Written only when true |
albums | string[] | Album ids. Written only when non-empty |
camera | string | Overrides the roll's, when the frame names its own |
lens | string | Same |
film | string | Same |
notes | string | Free text |
location | object | { lat, lng, name? }. Dropped if lat or lng won't parse |
source | object | Provenance of the cut. See below |
width / height | int | Pixel size, recorded at import so the stream can lay out before decoding |
Anything absent stays absent — Winder never writes empty strings, false
favorites or empty album lists.
Sorting uses capturedAt when present and addedAt otherwise.
Two things are deliberately not here
The roll. Which roll a frame is on is the folder it sits in. Nothing is written, so moving the pair of files into another roll's folder is all it takes to move the frame.
The path. Resolved at load time, never stored.
source — the provenance of a cut
Every split frame remembers where it came from, so no cut is ever a mystery later.
| Key | Type | Notes |
|---|---|---|
originalName | string | The lab scan this came out of |
half | string | left, right or full — matching the A / B / F in the filename |
splitAt | int | Pixel offset of the divider's center in the original, along the split axis. Absent when imported whole |
rotation | int | Clockwise degrees applied at import: 0, 90, 180 or 270. Written only when non-zero |
Album membership
Membership lives here, as ids rather than names, so renaming an album is
one write to albums.json instead of a sweep through every sidecar — and two
albums may share a name without silently merging.
Ids naming an album this library doesn't have are kept exactly as written. They match nothing, so they cost nothing, and a sync that brings the definition back brings the membership back with it.
The EXIF in the file itself
The sidecar is not the only copy. Winder stamps a real EXIF block into the JPEG, so a photograph that leaves the library still says what shot it.
| EXIF tag | From |
|---|---|
Make | The maker, split out of the camera name by the catalog |
Model | The camera name as written |
LensModel | The lens |
DateTimeOriginal | capturedAt |
| GPS latitude / longitude, with hemisphere refs | location |
UserComment | The film — EXIF has no film tag, so it goes where a human will find it |
ImageDescription | notes |
Software | Winder |
Three things trigger a write: filing a new frame (free — the bytes are already in memory), editing a frame in a way that would change the block, and changing the camera, lens or film on a roll, which rewrites every frame on it.
The pixels are never touched. Winder replaces the metadata segment in place rather than decoding and re-encoding, because a re-compress on every metadata edit would quietly grind a library down. Favoriting a frame or adding it to an album rewrites nothing at all.
Every EXIF failure is survivable and none of them is worth losing a photograph over: bytes that aren't a rewritable JPEG, or a file that has gone missing, simply keep whatever they had.
Hand-editing
Go ahead. A sidecar that won't parse never hides its photograph — the frame loads with defaults from the file's own dates, and the next edit in the app writes a clean file back.
Updated 2026-08-06