The proxy's largest visible error against the original was the JPEG encoder's
quantised 8x8 DCT DC - each block's own mean, luma and chroma - which shows as
block steps across the frame and a small constant colour shift, and grows with
saturation. Quality 92 -> 97 shrinks those quantisers to about a third (block
-mean dE76 0.138 vs 0.202 on the fidelity test's smooth scene), at about twice
the file size; what remains is the 8-bit sRGB quantisation both encoders share,
which only a lossless proxy format can close.
The header gains an `enc` field (encode version 2) and the folder's look marker
becomes "<look> <enc>": a proxy or marker from before the encode version is
rebuilt/rescanned once, so existing libraries pick the new quality up without
any command. Old files still decode bit-identically (the scale mapping is
untouched, decode ignores `enc`), and a new proxy read by an older build is
left alone by its staleness check.
The fidelity test pins the gain with a frozen copy of the old encoder and a
block-mean dE76 metric (per-pixel dE is dominated by the shared 8-bit floor);
the 0.8 bound is the geometric midpoint between the no-op ratio 1.000 and the
0.632 floor cap the 8-bit quantisation sets on this scene.
MediaCache::enforce_budget never evicts the original of the photo on screen (set_on_screen), but it still
counted it. A 64 MP original (Sony A7R VI: 9984×6656 linear float, 760 MiB) nearly fills the default cache
share (768 MiB) alone, so the loop evicted everything else instead: the photo's preview and thumbnail sources,
the rendered previews and the prefetched neighbours. Every edit at 1:1 (the whole-frame render and the
thumbnail), every zoom out and every step to the next photo then decoded the raw again.
The pinned original is now left out of what is compared with the budget. The rest still fits the share; the
original sits over it, as the first commit of this PR already allowed.
Co-Authored-By: Claude <noreply@anthropic.com>
Each drag tick changed the edited photo's settings, so the filmstrip asked
for a new thumbnail render every frame. For an unedited raw shown with its
embedded preview, the thumbnail-level source had never been decoded, so each
of those renders decoded the whole raw (a 64 MP ARW: over a second), several
side by side, and the loupe's drafts queued behind them.
- The edited photo's thumbnail is not re-rendered during a slider drag while
it has one; it follows once the drag ends.
- A thumbnail of a photo whose preview-level source is decoded (the one in
the loupe) is cut from that source (SourceRef::Fit) instead of decoding the
file again; not when the photo has a denoised picture.
Co-Authored-By: Claude <noreply@anthropic.com>
Sony ARWs of cameras without measured colour matrices were edited on the
relative scale (6500 K / 0 = as shot). Their SR2SubIFD stores the
multipliers of the camera's presets at 2500, 3200, 4500, 6000 and 8500 K;
lightcraft_raw::kelvin_presets reads the as-shot multipliers on the curve
through them and they take the spectral matrices' place in the #730
design (as-shot Kelvin and tint only; look, Bradford adaptation and the
pixel-identical legacy upgrade unchanged).
Covered: ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S.
Kelvin-dial shots read back 3400 -> 3396, 5200 -> 5193, 4700 -> 4708,
5600 -> 5594 K; Shade 7493 K. Off-curve constants (isotherm tilt 0.74,
tint spacing 1) measured from the 14 Sony bodies with both spectral
matrices and presets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add correlated dark holdouts that reject misleading training and accept
matching training. Check finite, monotone curves in both cases. Confirm
the regression fails with the error gate removed and passes when restored.
Qualify the improvement claim when no original curve can be fitted, and
explain the source-look process policy and offline smart preview refresh.
Co-Authored-By: Codex <noreply@openai.com>
Use separate luminance pairs for tone fitting so the camera look follows
its JPEG into deep shadows. Keep colour pairs and holdouts unchanged.
Require enough correlated shadow samples and lower held-out error, with
noise and compression floors and the existing median curve fit.
Add synthetic regressions, bump look and render cache versions, and
record the public multi-make and private numeric validation in the docs.
Fixes#1307
Co-Authored-By: Codex <noreply@openai.com>
Resize::apply rounds the size, then the render fit the frame into that box
again: on a frame a hair under 3:2 the refit gave 1333 x 1.4999999 = 1999.49
and lost a pixel of the long edge. Frame::fit now keeps a box that already has
the frame's aspect up to rounding, and output_size rounds the native size to
whole pixels first so float noise can't decide a half-pixel tie.
An uncompressed Leaf Aptus 22 MOS keeps its 16-bit mosaic in IFD0
(compression 1, BlackIsZero, one sample per pixel) with no Make tag and
no CFA photometric, so `probe` returned None and the file imported as a
greyscale image of the mosaic. Leaf's data block, tag 0x8606 (LeafData
in the ExifTool EXIF tag-name documentation), now marks such an IFD0 as
a raw: OtherTiff, preview only with the reason, as the lossless-JPEG MOS
already is. A rendered RGB image that kept the block stays an image.
The Hasselblad Lunar is a Sony NEX-7 that writes an ARW under Make
HASSELBLAD; only SONY files reached the ARW reader, so it opened as its
preview. A HASSELBLAD file with Sony's raw coding (an IFD with
compression 32767 or the tone-curve tag 0x7010, which no 3FR / FFF has)
now goes there and decodes like a NEX-7: black 512, the SR2SubIFD white
balance. The Tag2010 white-balance fallback, the one table arw.rs keys
by model, takes the body from the maker note's SonyModelID (0xb001)
when the file is not Sony-branded.
Docs: the Lunar is verified in docs/raw/evidence.tsv (236 verified),
raw-parity.md, parity-checklist.md, gaps.md and whats-new.md.
RENDER_CACHE_VERSION 61 -> 62: a Lunar's thumbnails were rendered from
its preview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
library.up / library.down (Up / Down) go to the cell above / below in the
grids (nearest by position in justified rows; the top / bottom row stays
put), and step one photo in the filmstrip.
Sources: own design.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Go to History Step (and new Previous/Next History Step commands) loads the
step's settings and moves a per-photo cursor (`Photo.history_cursor`,
`Op::SetHistoryCursor`, catalog format v8; older catalogs open at the latest
step) instead of appending "History: …". Later steps stay listed, greyed in
the History panel, and can be returned to; a new edit made behind the end
drops them first. Each move is one undo step; undoing an edit restores the
dropped steps and the cursor. `history.list` returns `cursor`.
Also: spots.rs uses as_chunks (clippy chunks_exact_to_as_chunks).
Sources: own design (Lightroom Classic / Photoshop History panel behaviour, observed).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A decoded 64 MP original (Sony A7R VI: 9984×6656 linear float, 797 MB) is bigger than the
default cache share (half of 1.5 GiB). MediaCache::enforce_budget evicted it as soon as the
render handed it over, so every pan of a 1:1 view and every slider move while zoomed decoded
the raw again (and lost the shared GPU copy, keyed by the CPU Arc).
The frontend now tells the cache which photo Detail shows (MediaCache::set_on_screen, set per
frame in panels::compare::prepare); that photo's original is not evicted for the budget and is
let go as soon as another photo or view is shown. Keying it on "no newer preview of another
photo" instead does not work: the neighbour prefetch stores previews after the original.
The decoders keep the alpha plane, but to_working converted only the
colour, so transparent pixels kept the black most encoders store under
them: renders, previews, exports and contact sheets showed it. The
working image is now flattened onto white, in linear light like the
rest of the pipeline.
- Files with no capture date in their metadata or sidecar (e.g. exports saved without
metadata) take the file's modification time, as Lightroom does, instead of all landing
under "Unknown Date" (the loupe overlay showed the import date for them). The import
review and its folder/rename templates use the same date.
- The import review counted files with the same bytes as another file in the source as
"already in the library" (8 on a brand-new, empty library); they are now shown apart as
"identical copies".
- View > Grid Info captions are drawn on Square Grid only; choosing one from Photo Grid now
switches to Square Grid instead of seeming to do nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With XMP auto-write on, photo.readMetadataFromFile committed what it read
and the command handler then wrote the sidecar back, adding lc:settings to
a sidecar that had only another app's crs: settings. Later reads prefer
lc:settings, so LRTimelapse's next crs: edits were ignored. The read now
sets the existing one-command skip_auto_write switch (as photo.removeRegion
does); edits made afterwards are still auto-written.
Tests: a read leaves the sidecar byte for byte and a later develop edit is
still written; the issue's crs:Exposure2012 0.5 -> 1.2 round trip is read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A panic inside RenderJob::run or QuickJob::run (a raw decoder or pipeline bug) unwound the
job pool's worker without sending a result. The slot stayed pending and its key was never
asked for again, so the loupe or the thumbnail rendered forever; and the pool never
replaces a dead worker, so after as many such panics as it has workers (2–6) no photo
rendered until LightCraft was restarted.
Both now catch the panic (guard::catch) and return it as the job's failure, answering the
request it came from, so the renderer records it like any failed render: no retry until
the catalog changes, and the workers live on. run() is the one place every render goes
through (the pool, export, Build Previews, contact sheets, face and AI-mask workers);
callers that had their own guard keep working, and the watermark preview's, now
redundant, is gone. The web build aborts on panic, so nothing changes there.
Co-Authored-By: Claude <noreply@anthropic.com>