WINDER

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

KeyTypeNotes
versionintAlways 1
idstringUUID v4. Falls back to the filename when missing
addedAtstringISO 8601. When Winder filed it. Falls back to the file's modified time
capturedAtstringISO 8601. When the shot was taken. Absent until set
favoriteboolWritten only when true
albumsstring[]Album ids. Written only when non-empty
camerastringOverrides the roll's, when the frame names its own
lensstringSame
filmstringSame
notesstringFree text
locationobject{ lat, lng, name? }. Dropped if lat or lng won't parse
sourceobjectProvenance of the cut. See below
width / heightintPixel 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.

KeyTypeNotes
originalNamestringThe lab scan this came out of
halfstringleft, right or full — matching the A / B / F in the filename
splitAtintPixel offset of the divider's center in the original, along the split axis. Absent when imported whole
rotationintClockwise 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 tagFrom
MakeThe maker, split out of the camera name by the catalog
ModelThe camera name as written
LensModelThe lens
DateTimeOriginalcapturedAt
GPS latitude / longitude, with hemisphere refslocation
UserCommentThe film — EXIF has no film tag, so it goes where a human will find it
ImageDescriptionnotes
SoftwareWinder

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