Commit Graph
1193 Commits
Author SHA1 Message Date
Jason Baldwin 70a12af9a2 LR-IMP-LOCAL: browsing a Local folder keeps or picks the active photo (#1447)
library.browse (engine and the UI's background read) switched the source to the
folder without touching the selection, so the previous view's photo stayed
active: Photo Grid marked nothing, and Detail, the histogram and the info
showed a photo the folder doesn't hold. Browsing now follows library.source's
rule through a shared Session::keep_active_in_view: the active photo stays when
the folder shows it, else the folder's first photo becomes active (also as the
background read's batches arrive).
2026-10-15 00:57:42 +02:00
Jason Baldwin 5c5370fc9b LR-LIB-REMOVE-ALBUM: removing a photo from an album keeps it in its folder (fixes #1805)
In an album the Delete key ran photo.delete, which moved the photo to
Recently Deleted, so it also left its folder views. It now removes the
selection from the album only (album.removePhotos), as in Lightroom; in a
smart album it changes nothing. Photo > Remove from Album shows in normal
albums with the Delete key; Photo > Delete Photo still deletes from the
library. album.removePhotos defaults to the album on screen and returns
{removed, album}.
2026-10-15 00:54:17 +02:00
Jason Baldwin b5169e907c LR-META-XMP: radial masks keep their inside/outside from Lightroom (fixes #1815)
crs:Flipped="true" is how Lightroom Classic writes an ordinary radial mask
(effect inside); it was read as our invert, so every imported radial applied
outside the circle. Established from Lightroom Classic 15.6's exports of
synthetic DNGs:

- MaskGroupBasedCorrections, Version 2: Flipped is the effect-inside flag;
  inverting is the component's MaskInverted.
- MaskGroupBasedCorrections without Version: inside whatever Flipped says
  (Lightroom rewrites both values as Flipped=true, Version=2), and its Feather
  is halved, as Lightroom rescales it on that upgrade.
- Legacy CircularGradientBasedCorrections Radial Filter: outside by default,
  Flipped=true (Invert Mask) is inside.

One mapping serves XMP sidecars, DNG-embedded XMP, presets and .lrcat catalog
import. LightCraft writes no crs: masks, so there is no writer to match.
2026-10-15 00:53:53 +02:00
NetterSilverandClaude Haiku 5.5 3080f71680 Keep crop finite when crop.set gets a huge rect
Found while testing crates/geom/src/crop.rs:59 (CropGeometry::constrained, via crates/engine/src/cmd/develop.rs:524 crop.set)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:44:54 +02:00
NetterSilverandClaude Haiku 5.5 072bcfeaf1 Fix crop.rotateAspect turning a zero-size crop into a NaN rect
Found while testing crates/engine/src/cmd/develop.rs:676 (crop.rotateAspect); also crates/geom/src/lib.rs:180 (Rect::fit_aspect)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:31:45 +02:00
Guillem Hermida 053b7420fe LR-CROP-STRAIGHTEN: turning a crop shrinks it about its centre instead of swapping it for the largest crop (#1794)
CropGeometry::with_angle replaced a crop that no longer fit at the new
angle with the largest centred crop of its aspect. For a crop dragged in
from a corner (which still touches two edges) that happened at the first
fraction of a degree, so the crop jumped back to nearly the whole photo.

A crop that no longer fits now shrinks about its own centre, keeping its
aspect, to the largest size that fits there. The full photo, or a crop
an earlier turn shrank to the largest of its aspect, still follows the
largest crop and grows back towards 0 degrees (#1388). A crop whose
centre is off the turned photo is centred at its own size at most. No
crop the user made ever grows.
2026-10-14 23:09:11 +02:00
midasmatterhorn ea826b11cc engine: refuse negative indices instead of targeting element 0
`version.*`, `pointColor.delete` and `redeye.catchlight`/`redeye.delete`
parsed `index` with `f64_req(..)? as usize`. Rust's float-to-int cast
saturates, so `index: -1` became 0 and the bounds checks never saw a
negative: a caller sending `index: -1` silently deleted, restored or
renamed version 0 (or the first point-colour sample / red-eye correction)
and got `ok` back. `history.restore` already guards this and `spot_index`
uses `as_u64`.

New `index_req` helper in `cmd/mod.rs` (the same finite, non-negative
guard) is used by the five sites. Behaviour for valid indices is
unchanged.

Regression test `negative_indices_are_refused` creates a version, then
asserts `version.delete/restore/rename` with `index: -1` error and version
0 is untouched (before the fix the first one deletes it).
2026-10-14 23:12:32 +03:00
Michael Anderson 9ef40b9886 Commands: Refuse malformed photo ids instead of using the selection (#1506)
A wrongly typed id or ids fell back to the selection, or kept only the valid part of the list. Session::targets now returns an error for those, and callers pass it on.
2026-10-14 13:56:33 -05:00
Michael Anderson 30ec48d9b3 LR-META-XMP: Retry automatic XMP sidecar writes that failed
A sidecar write that failed because the folder was offline or read-only was only logged. Keep those photos waiting, write them again once their files are reachable, and tell the user.
2026-10-14 13:56:23 -05:00
Michael Anderson 4352147910 Faces: Drop faces made from a photo's old file when it is replaced (#1524) 2026-10-14 13:19:22 -05:00
BuonBredaandClaude Opus 5.5 4551790eab LRC-DEV-LOUPEOVERLAY: a layout image over the loupe (View ▸ Loupe Overlay)
Lightroom Classic's Loupe Overlay ▸ Layout Image: a PNG with transparency (a
cover's masthead and cover lines, a banner's safe areas) drawn over the photo
in the loupe, fitted and centred, following zoom and pan. Opacity and matte
(the photo darkened outside the graphic's box) in steps, ⌥⌘O shows / hides it,
the choice is saved with the UI state. The file is read off the UI thread; an
unreadable one is reported on the canvas. engine::export::read_graphic is the
watermark logo's reader, now shared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mj2w3LwYjFhMHALZuFp2rU
2026-10-14 19:32:06 +02:00
LabDennis 1f4aaaec47 Fonts: finish Thai fallback delivery for desktop and web releases
Retain the complete reviewed change, including prior integration resolutions,
on current upstream.

#736: deliver Thai UI fonts in desktop and web releases

Use the merged craft-fonts input containing Noto Sans Thai across release,
FreeBSD and Nix builds. Retain the small Thai face in the wasm subset and
reject required release inputs that lack it. Exercise the reported Thai
folder name through all UI families and include original font licence texts
in Windows installers as well as portable packages.

Refs #736; completes delivery of the fallback added in #750.

Validation: UI tests, UI-only strict Clippy, 28 wasm checks, HEIF checks,
assets, layers, parity and formatting pass. All 21 unrelated workspace
failures reproduce with upstream Rust sources and the old font input;
existing Rust 1.95 metadata/engine Clippy findings remain unchanged.
2026-10-14 18:52:15 +02:00
Claude 05352e18dd Merge storytold/lightcraft main into LR-MERGE-HDR (white balance)
Brings in batch 35 / v0.8.0. Code merged cleanly; conflicts only in docs: the LR-MERGE-HDR row keeps main's
lens-correction text plus this branch's white-balance item, and this branch's What's New entry moves to the
2026-10-14 section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
2026-10-14 16:22:53 +00:00
Marco Arnulfo 17e05759b4 DNG export and Convert / Copy as DNG write the colour a raw without matrices opens with (fixes #1745) 2026-10-14 18:08:53 +02:00
Michael Anderson e8818281d5 LR-IMP-LOCAL: Forget deleted files when browsing from the sidebar too
The sidebar reads the folder on a worker thread and skipped the cleanup library.browse does, so the fix for #1392 didn't reach the app.
2026-10-14 10:40:41 -05:00
Claude c472f52c84 Lightroom import: raise catalog/WAL size limit from 1 GiB to 4 GiB
Reserve the exact file size before reading so a large catalog does not
need twice its size in memory while the buffer grows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TGi6EFawAr2RPfqXsoCrQ
2026-10-14 14:51:26 +00:00
Jason Baldwin 3aa6b3ba31 Merge slideshow-templates into slideshow-export (origin/main: batch 35)
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 14:42:03 +02:00
Jason Baldwin 5f3bdbcb73 Merge slideshow-titles into slideshow-templates (origin/main: batch 35)
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 14:39:47 +02:00
Jason Baldwin 7919c1471d Merge slideshow-overlays into slideshow-titles (origin/main: batch 35)
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 14:37:20 +02:00
Jason Baldwin f776d98167 Merge slideshow-composer into slideshow-overlays (origin/main: batch 35)
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 14:35:02 +02:00
Jason Baldwin 2b766142bc Merge origin/main into focus-point (tracker header/summary/change log, whats-new entry moved under 2026-10-14) 2026-10-14 14:33:49 +02:00
Michael Anderson ead24aded9 LR-PROF-CAMERACOLOR: Add a bundled Nikon D5300 camera profile
Fitted with lightcraft-cli calibrate to 264 of my own D5300 photos.
2026-10-14 07:32:56 -05:00
Michael Anderson 975b08c334 LR-PROF-CAMERACOLOR: Add a bundled Nikon Z 6II camera profile
Fitted with lightcraft-cli calibrate to 296 of my own Z 6II photos.
2026-10-14 07:32:56 -05:00
Michael Anderson 2bc2f60277 LR-PROF-CAMERACOLOR: Add a bundled Nikon Z 8 camera profile
Fitted with lightcraft-cli calibrate to 261 of my own Z 8 photos.
2026-10-14 07:32:56 -05:00
Michael Anderson c854e172e4 LR-PROF-CAMERACOLOR: Match the camera look against lens-corrected JPEGs
The fit compared the uncorrected raw with the corrected JPEG. It now warps the raw with the file's own lens correction when that lines up better.
2026-10-14 07:32:47 -05:00
Michael Anderson 441f46683a LR-IMP-DEVICE: Show the import review when a card is connected
Adds an option under Settings > Import > Cameras and Cards that opens Import from Device when a camera or memory card shows up. It's off by default, and a card that was already in at startup doesn't ask.
2026-10-14 07:32:47 -05:00
Michael Anderson 1acbf88661 LR-META-INFO: Show the lens for Nikon DSLR raws
They don't write a lens name, so read the range from the maker note's Lens tag.
2026-10-14 07:32:46 -05:00
Michael Anderson 02d5d6bc03 LR-MERGE-PANO: Keep panoramas at full resolution when memory allows (#1455)
Drop the 40 MP default and free each photo's pixels once it's projected.
2026-10-14 07:32:45 -05:00
Michael Anderson 697010d520 LR-IMP-LOCAL: Forget untouched records of deleted files when browsing a folder again (#1392)
Rated, flagged or edited photos stay and show as missing.
2026-10-14 07:32:43 -05:00
Jason Baldwin b60c74a6d7 Merge slideshow-playback into slideshow-composer (origin/main: batch 35)
# Conflicts:
#	crates/engine/src/import.rs
#	crates/engine/src/media.rs
#	docs/parity-checklist.md
2026-10-14 14:31:53 +02:00
Jason Baldwin 3aaadd1456 Merge origin/main into slideshow-playback (batch 35: tracker summary refreshed, slideshow rows kept) 2026-10-14 14:29:54 +02:00
Nathaniel LedfordandClaude Opus 5.5 d2523e8544 Preview cache: cached thumbnails are no longer taken for the person's photos
The thumbnail cache wrote .jpg files into the library's thumbs folder, which is usually inside
Pictures: photo galleries listed every thumbnail as a photo (issues #610, #1066), and every read
brought the file's date forward, so each launch made them all look new.

- Cache files are named <32 hex>.lcthumb (JPEG data as before). A .jpg of before is taken over
  under the new name when it is asked for; the rest when the cache is tidied, which the engine does
  once per cache folder, on a thread of its own with a row in the activity stack that shows its
  progress and can stop it. Nothing is rendered again.
- Reading writes nothing: what was read is remembered for the session (a full cache lets it go
  last), and only a file that went unused for a week has its date brought forward.
- A folder that holds nothing but the cache gets a CACHEDIR.TAG, when the cache makes it and when
  it is tidied; one that also holds somebody's files is not tagged (issue #98).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:26:17 -04:00
Nathaniel LedfordandClaude Opus 5.5 43ba500c6e PERF-EDIT: a smart album's count follows an edit, asking only the photos it touched
The left panel shows a count beside every album. Each catalog revision had
every album counted again, and a smart album's count asks every photo of
the library whether its rules hold: one pass over the library for each smart
album after each keyword, rating or slider tick (about 25 ms an album in a
library of 67,000 photos).

`AlbumTally` (`crates/catalog/src/tally.rs`) keeps the counts and, for each
smart album, which photos it holds. After a small edit each smart album asks
only the photos the edit touched (`Catalog::touched_since`); an album of
chosen photos is counted again only when a photo came, went or was deleted.
Counted from every photo as before: new rules, a change to an album, a stack
or the keyword list, an edit to more than 256 photos, another library, and
each new minute while an album asks about "the last N days".

What an album remembers is in proportion to the photos it holds: a bit a
photo id while ids are close together, a set when they are far apart.

The frame after a keyword in that library, both side panels open: about
54 ms -> 27 ms (156 ms before #1660).

Tests: 900 random edits against eight smart albums (keywords, flags,
ratings, labels, sliders, capture dates, file names, an album of chosen
photos) and an ordinary one, the followed counts compared with
`album_count` after each; a delete followed, three hundred photos counted
whole; the members' bits at the edges and for ids far apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:46:42 -04:00
Nathaniel LedfordandClaude Opus 5.5 5f1037a192 PERF-EDIT: an edit is followed from what it touched, not worked out from every photo
Everything derived from the catalog was keyed on its revision, which every
op raises. So each edit (a keyword, a rating, every tick of a slider) had
the grid's list queried and sorted again from every photo, the source
counted by a second full query, and the side panels tally the whole library
again. In a library of 67,000 photos the frame after one keyword took about
156 ms with both side panels open (an idle frame: 2 ms).

The catalog now remembers which photos its latest ops touched and what of
them they changed (`Catalog::touched_since`, `aspects_since`, `Aspects`). It
answers "not known" for an op that changes more than photos of its own (an
album, a stack, the keyword list), for changes too long ago, and for a
revision it did not make itself (a library just loaded).

- The view (`Session::visible`): when the same view is asked again after a
  small edit, the photos the edit touched are taken out of the list it had,
  asked about again, and put back at their place in the order
  (`Catalog::follow`). 37 ms -> 0.6 ms. Worked out whole as before: more
  than 256 photos touched, a change that is not photos' own, an album in its
  own order, Recently Added, Missing Photos, and a view that asks about "the
  last N days" (it changes with the clock too).
- The source's count: taken from the list when no filter is set, else
  counted without sorting (`Catalog::count`). 37 ms -> 0.
- The side panels (`Caches`): the folder tree, keyword tree, date groups,
  people's names and library counts are kept through an edit that changed
  nothing they read. A rating or a slider changes none; a keyword leaves the
  folders, dates and counts.

The frame after a keyword: about 156 ms -> 54 ms with both side panels
open, 71 ms -> 2 ms with the grid alone (`tests_view_cost.rs`, a synthetic
library, run with --ignored --nocapture).

Still paid on each edit: the count beside every album and smart album in the
left panel (about 25 ms for a smart album there), and the filter bar's lists.

Tests: 1,440 random edits of every photo-local kind across sources, sorts,
filters and the hold, each compared with the list worked out whole; 600
random edits comparing every kept tally with a fresh one; the clock; the
record itself, with its failure paths. `ui.inspect` -> `perf` reports
`viewFollowed` and `viewWorkedOut`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:46:30 -04:00
Nathaniel LedfordandClaude Opus 5.5 540a281ccd PERF-SYNC: a Synchronize Folder scan reads off the folder's listing whether a photo and its sidecar are there
The scan asked the disk about every library photo in the folder: is its
file there, and has it a sidecar under any of four names. On a network
share each question is a round trip, and the scan had just listed the
folder. Measured on a hard disk over SMB, for unchanged folders of 1,600
to 2,400 photos: 4.6 to 7.1 s of questions after a listing of 4 to 5 s.

- The walk hands each folder's listing over (`walk::files_in_noting`:
  every entry's name and kind, hidden ones too, and whether that is all of
  them). The scan walks the folder once, then checks the photos beside
  the probing of new files.
- `sync::Listing` answers "is it there" / "is it a file" where that holds
  on any file system: a name the folder lists is there; a plain ASCII name
  is not there when the folder was listed to its end, all its names are
  ASCII and none is that name in another case. Everything else is asked
  of the disk as before (a link, a name Windows reads another way, a
  folder that was not walked, and every name not listed in a folder that
  holds a name outside ASCII).

Now under 0.05 s for those folders. Two things differ on purpose: a file
the folder lists is there though it can't be opened by its path (a folder
that may be listed but not searched, an over-long path), where such
photos were listed as missing; and the answers are the folder as the
walk saw it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:45:43 -04:00
Nathaniel LedfordandClaude Opus 5.5 73b9cc3708 LR-LIB-SMARTALBUM: a view can hold its photos until it is refreshed
A view is asked again after every edit, so a photo that an edit makes stop
matching it leaves the grid at once and every photo after it moves up: a
keyword given in a smart album of photos without keywords takes the photo
away from under the cursor, and in a random order, with no landmarks, the
shift reads as a new shuffle (the seed does not change).

View > Hold View Until Refreshed (`library.holdView`, off by default, kept
with the library's preferences): a photo the view shows stays in it when an
edit makes it stop matching a smart album's rules, Picks or the filter bar.
View > Refresh View (`library.refresh`, also in the sort menu while the hold
is on), Reshuffle, another source, filter or sort, or new rules for the
smart album shown, shows what matches now.

- Photos that start matching still appear at once.
- A photo that is deleted, or taken out of the album or folder shown, still
  leaves at once: a held photo has to be where the view looks
  (`Catalog::query_keeping`, `within`).
- A source with no filter over it holds nothing and is queried as before.
- A refresh keeps the seed.
- The grid's count no longer reads "3 of 2" when a held view shows more than
  now match its source.

Each edit still asks the whole library again; that cost is not addressed
here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:41:44 -04:00
dev-null-undefinedandClaude Opus 5.5 b52762797f LR-MERGE-HEADLESS: HDR for Each Bracket Set
Photo ▸ Photo Merge ▸ HDR for Each Bracket Set (`merge.hdrBatch`) merges many brackets in one go with the last HDR
options, as Lightroom merges a selection of stacks: one HDR per stack among the photos, then per bracket among the
unstacked ones (`Catalog::bracket_sets`: photos taken less than about a second after the previous one's shutter
closed, at exposures at least ½ EV apart; bursts at one exposure and earlier merge results are left out). The app runs
the merges one after the other in the activity stack (cancelling one stops the rest; a failed set doesn't); the
command runs them inline for the CLI and MCP (`lightcraft-cli merge hdr-batch FILES…`) and reports merged and failed
sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:37:13 +02:00
dev-null-undefinedandClaude Opus 5.5 6fb0d22326 LR-MERGE-HDR: decode full-size merge frames one at a time; AHD writes its tiles straight into the picture
A 7-frame bracket of 67 MP raws ran out of memory: the merge decoded the frames four at a time, and the AHD demosaic
collected every tile before copying them into a second full-size picture. Full-size frames now decode one after the
other (the demosaic is parallel inside; previews stay parallel), and AHD fills the picture by rows of tiles. Peak memory
of 67 MP HDR merges: 3 frames 4.8 → 3.8 GB, 4 frames 6.3 → 4.7 GB, about the same time; AHD output is pixel-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:56:59 +02:00
Sergio PereaandClaude Opus 5.5 3c907e0c9d fix(export): a metadata/sharpen/conflict value that passes validation is the one exported
`ExportOptions::validate` accepted `metadata`, `sharpen`, `sharpenAmount` and
`conflict` case-insensitively, but `from_json` read them with serde's
exact-case names and silently fell back to the default when that failed.
`{"format": "jpeg", "metadata": "NONE"}` therefore passed validation and
exported with `MetadataPolicy::All`: GPS, location and camera data went out
in a file the caller asked to carry none. MCP, the control channel and the
CLI all go through `from_params`, so all were affected.

Each of the four enums gets a case-insensitive `parse` (the pattern
`HdrCurve::parse` and `JxlEffort::parse` already use), and both `validate`
and `from_json` call it, so validation and conversion can no longer
disagree. Accepted inputs, error messages, defaults and `to_json` output are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:08:36 +02:00
Andrew FrahnandClaude Opus 5.5 78b014fb79 Merge upstream/main into pr/pen-pressure
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 20:04:19 +11:00
Andrew FrahnandClaude Opus 5.5 f625c74c7a Faces: whole networks on the GPU, batched faces, CPU-only switch, paced GPU use
Activations stay on the device between layers (net/program.rs lowers the graph; every
operator is a WGSL kernel, one submission and read-back per run); faces of a photo go
in batches of up to 16; Settings > Faces shows each model's device and a CPU-only
switch (faces.gpu); the scan's pace limits GPU use; the GPU check is an activity row.
YuNet 58 -> 21 ms, SFace 40 -> 9.6 ms, AuraFace 295 -> 71 ms (heavily loaded machine).

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:43:44 +11:00
FredAppsandCodex 2b86ca0537 LR-EXP-TYPE: omit redundant alpha from opaque 8-bit exports (#1670)
Preserve real transparency, pixels and embedded metadata. Cover RGB PNG,
TIFF compression modes and WebP, plus real-alpha round trips.

Co-authored-by: Codex <codex@openai.com>
2026-10-14 10:23:40 +02:00
Andrew FrahnandClaude Opus 5.5 e5efaea6a8 Merge upstream/main into pr/file-writes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:23:00 +11:00
Andrew FrahnandClaude Opus 5.5 d598c304aa Merge upstream/main into pr/zoom-detail
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:20:53 +11:00
Andrew FrahnandClaude Opus 5.5 befd25f2a3 Merge pr/raw-erf into pr/raw-mef
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:00:57 +11:00
Andrew FrahnandClaude Opus 5.5 0bc2f79bb5 Import: find sidecars whose photo extension or .xmp differs in case
On a case-sensitive file system, `DSC_1.nef.xmp` or `DSC_1.Xmp` beside `DSC_1.NEF` was never
found, so the photo came in with only the file's own metadata (its IPTC and keywords) and lost
the sidecar's rating and crop. The lookup now tries the photo extension in lower and upper case
and `.xmp`/`.XMP`/`.Xmp`; Move takes such a sidecar along.

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:56:21 +11:00
Andrew FrahnandClaude Opus 5.5 50cb797872 Merge upstream/main into pr/raw-erf
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:48:38 +11:00
Val EnfysandClaude Opus 5.5 f29a340fa6 LR-IMP-FORMATS: recheck stale preview-only raws when a library opens (#662)
A raw imported while its format couldn't be decoded kept preview_only until a
manual Reload, so new decoders in a release never reached existing photos.
When a library opens in another app version, its preview-only photos whose
stamp differs are probed again in the background and get exactly what Reload
from Disk gives them (shared reload_ops): a file that decodes now loses the
flag, one that still doesn't gets the current reason and a fresh stamp. The
result is one journaled system batch, not an undo step; develop settings and
history are kept. Unreadable files are left for next time, photos changed
while probing are skipped, and a result for a library closed meanwhile is
dropped. A toast says how many raws now develop from their raw data.

- engine: cmd/recheck.rs (stale / probe / apply) and
  library.recheckPreviewOnly {all?} -> {checked, updated, cleared}; refused
  without file access, where every probe would clear every flag
- desktop: recheck::tick once per library generation, through tasks::spawn
  ("Checking raw files" in the activity stack)
- the preview-only notice (Edit, Info) gains a Reload from Disk button
- strings translated in all nine catalogs; docs/mcp.md and the parity row

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:19:43 +01:00
Val EnfysandClaude Opus 5.5 5773533e23 LR-IMP-FORMATS: preview-only photos record the app version that decided it (#662)
Catalog format v9 adds Photo.preview_checked and Op::SetContent.preview_checked:
the app version (CARGO_PKG_VERSION) whose decoder set preview_only, empty when
it isn't set. Import and content_op (Reload, Relink) stamp it; undo swaps it
back. Catalogs from before v9 read an empty stamp, so their preview-only photos
count as stale (Photo::preview_only_stale) and will be rechecked once.

The version guard test now also accepts a format version that only adds fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:19:17 +01:00
Alex ChambersandClaude Opus 5.5 ac677a2624 LR-EDIT-OPTICS-PROFILE: decode Sony shading, CA and distortion tables with settings and provenance; apply nothing new
Sony ARWs record shading, lateral chromatic aberration and distortion as a camera setting plus a radial
table in the raw image IFD (0x7031-0x7037), with copies in the encrypted SR2SubIFD (0x797c-0x7982).
They are now decoded for every body that records them and carried through RawInfo and RawImage as
`lens_metadata.sony` (types in crates/raw/src/lens_metadata.rs, decoder in
crates/raw/src/vendor/arw_lens.rs):

- Raw-IFD fields are the ones the TIFF reader parsed, which is what the image itself uses. A scan of
  the directory adds diagnostics only: repeated tags, and why an entry the reader skipped was dropped
  (values outside the file, an unknown type, past the reader's limits). SR2SubIFD fields are read
  from their first entry. Only the exact layouts are accepted: SHORT[1] or non-negative SSHORT[1]
  settings, SSHORT[17] headed by 16 for shading and distortion, SSHORT[33] headed by 32 for CA.
  Nothing is allocated from a count in the file.
- The raw IFD's copy wins. The SR2SubIFD copy fills in only for a tag the raw IFD lacks, never for a
  malformed one, and the two copies' agreement is recorded.
- Settings keep their number and their documented meaning, Unknown(value) otherwise (never Off).
- Framing evidence (stored frame, Sony and default crops, reference rectangle, coding, 3:2) and a
  per-component status. Only Applied components become opcodes.

Applied corrections are unchanged: the ILCE-7RM4A distortion conversion receives exactly the table it
received before, and no other body or component gains a correction. The ILCE-7CR distortion misses its
camera JPEG at 24 mm, and shading and CA semantics are not validated on any body; see
docs/sony-lens-corrections.md.

Locating the SR2SubIFD, now done for every ARW that has one, selects the SR2Private offset, length and
key with the TIFF reader's own rules, which lightcraft-tiff now exposes and builds its reader on
(directory_entries, entry_span, ParseOptions::budget_for, decode_value). Only each field's first value
is decoded, so the directory's other entries cost no memory sized by the file: a 16 MiB file whose
SR2Private holds eight fields aliasing it peaks at 37 kB. DirectoryEntries keeps its layout private,
and its positions() iterator uses checked arithmetic.

Tests: synthetic decoder tests; a differential test of the SR2Private selection against the TIFF
reader's in both byte orders (0 to 8,193 filler entries, unreadable and unusual first occurrences of
each field, a used-up value budget); a dhat peak-heap test; public-corpus checks against the values
ExifTool prints, with pinned and anchored SHA-256s (LIGHTCRAFT_CORPUS_REQUIRED=1 makes absent files
fail); and an opt-in local inventory check (crates/raw/tests/sony_lens_inventory.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:19:21 +02:00