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>
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>
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>
Photo Merge decoded each raw itself and wrote the reference frame's own colour
tags into the merged DNG. A CR2 (and NEF, ARW and the other raws whose colour is
fitted from the camera JPEG) has none, so an HDR merge of CR2s rendered with no
camera colour: green and about 16 L* dark.
`lightcraft_merge::load_frame_with` takes the raw's colour tags from the caller,
and the engine passes `starting_colour_tags`: the colour the raw opens with
(`starting_colour`) as DNG tags. That is one forward matrix with a colour matrix
and AsShotNeutral that agree with it at the as-shot white, the look's hue/sat
table as ProfileHueSatMap and its tone curve as ProfileToneCurve. Raws with
their own matrices (DNG) keep their tags. The look's chroma curve has no DNG
tag and is left out.
On two CC0 CR2s (EOS 550D, 40D) an HDR merge of three copies against the photo
alone: mean CIE76 dE 26.15 -> 1.07 and 22.89 -> 0.47.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
20 engine tests failed on Windows on clean main. Three real bugs on
case-insensitive volumes (Windows, default macOS APFS) caused most of them:
- Renaming a photo probed `a.XMP` before `a.xmp`. On a case-insensitive
volume the upper-case spelling "exists" when only `a.xmp` is there, so the
sidecar was moved as `Trip.XMP` (and its retained-packet record was taken
under the wrong spelling). Upper-case candidates are now used only when the
folder lists them exactly (new `MoveFs::listed`).
- `RealFs::same_file` on Windows (no file id in std) depended on argument
order: `same_file("Trip.XMP", "Trip.xmp")` was false when `Trip.xmp` is the
real entry, so saving after a partial rename reported a phantom collision.
It is now symmetric, and also requires the names to differ only by case
(two different files with equal size and mtime were "the same").
- Saving metadata picked the sidecar spelling by `is_file()`, lower case
first, so the atomic write renamed an existing `Trip.XMP` to `Trip.xmp`.
The spelling the folder lists now wins.
The rest were fixtures: FakeFs fault paths, catalog paths and expected
paths spelled with `/` while the code joins with the host separator.
Regression tests: case_insensitive_renames_keep_each_sidecars_spelling
(FakeFs, every platform; fails without the fix) and
same_file_spellings_are_symmetric_on_windows. Linux behaviour unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests were written for a case-sensitive disk. rename.rs sidecars() offers both
.XMP and .xmp per naming and keeps those that exist; on case-insensitive APFS
a.XMP "exists" for a file stored as a.xmp, so the code renames a.XMP while the
fault injectors (Faults twice, PartialRename) compared exact paths with the
lowercase one: the injected failure never fired and the rename the tests expect
to be refused succeeded.
Fault injectors now match by same_path_or_file (equal path, or the same file by
identity); two assertions that compared an exact spelling use same_path_for_volume
(equal, or equal ignoring ASCII case on a case-insensitive volume), and one that
required the lowercase packet to be absent is skipped there, where both spellings
are one file. Helpers live in tests_xmp.rs. Production code is not touched.
Run with rustc 1.99.0 on macOS: case-insensitive APFS, the ten failed before and
pass now; case-sensitive APFS (a disk image), the original tests pass and so do
these. All 13 sidecar::tests_repair tests pass on both. cargo fmt is clean and
clippy over lightcraft-engine with --all-targets is clean. Not touched: the
.xmp -> .XMP spelling change on rename, and the Lightroom folder test (its own PR).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
APFS refuses names that are not valid UTF-8 (EILSEQ, os error 92), so the test's
create_dir_all(...).unwrap() panicked on macOS. The test now probes once with a
\xff folder; where that fails, the non-UTF-8 folder case and the non-UTF-8 entry
are skipped and the catalog-folder and symlink cases still run on a plain-named
folder. Where the probe succeeds (Linux, FreeBSD) the test is unchanged.
Run on macOS (case-insensitive APFS, rustc 1.99.0): the five tests in
tests_lightroom_folder pass, cargo fmt is clean, and clippy over lightcraft-engine
with --all-targets is clean. The ten sidecar tests of item 3 are not touched.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
On merge: the HDR histogram is applied in develop_reporting (#712 split develop), is skipped for
draft renders (a second render per drag tick), and HdrBins::bin clamps a hand-built split so it
can't underflow (test hdr_bins_with_any_split_stay_in_range).
photo.editExternal takes the selection (or ids, at most 50): each photo gets its own -Edit copy
stacked on its original, the copies are selected and returned in copies (path / id / original
still name the first). Photo ▸ Edit In opens each copy in the editor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4
On merge: Lightroom's default profiles (Adobe Standard, Adobe Color), named by nearly every sidecar,
give no camera-profile warning; other profiles still do (test default_lightroom_profiles_are_not_reported).
The import help string keeps main's onDeleted wording and gains warnings.
On merge: the quick preview path orients like the whole-file path (preview_orientation with the
raw's header prefix, so a portrait raw whose embedded JPEG carries no orientation is not sideways),
and its doc links point at preview_window.
move_across_volumes read both whole files into memory to compare them; it now
compares lengths first, then streams 1 MiB chunks. The grid's Move to Folder…
asks through pick::ask (background dialog where the host has one) like the
other folder pickers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4
Check the target (Lightroom catalog, file, unreadable folder, locked by another
program) with Session::check_library before closing the open library, and if
opening still fails, reopen the previous one (both errors reported if that fails
too). New Library names reject Windows-reserved characters and trailing dots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4