Conflicts:
- apps/lightcraft/src/native_menu.rs: upstream's item swapping (macOS keys
given up for real, Help menu not swapped, Settings / Quit tracked) with
KEY-GRID's keymap-aware rows. `Item.label` stays the text the row shows
(native_item_presentation), so `Item::native_label` reuses it instead of
formatting it again; Settings / Quit store theirs through
display_item_label, as upstream's native_label did. No early return after
a rebuild (upstream).
- crates/ui-egui/src/i18n.rs: upstream's tests (#2729: one test per check
across languages, gaps reported, never a failure) plus the batch's two new
ones (dropped-photos notice, Polish counts); production code as merged.
- grid.rs, keyword_list.rs, masking.rs: the translation tests #2729 removed
stay removed; the batch's new keyword and segmenter tests stay.
- cmd/library.rs, ui-egui/src/lib.rs: both test modules, each with its
#[cfg(test)].
- xtask/src/main.rs: #2302's test_selected and upstream's translation_gaps.
- docs: tracker / gap rows take both sides' edits, headers list both sides'
changes, history rows of both sides newest-first. What's New: one section
per date, newest first (the batch had 10-22 above 10-23, and second 10-21
and 10-19 sections); no line lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.avif has no AV1 decoder in any build and .heic/.heif only decode when
the codecs' heif feature is enabled, so listing them let web users pick
files import would immediately report as failed. accept_list() now mirrors
lightcraft_codecs::Format::Heif::can_decode() and never offers .avif.
EXTENSIONS itself is unchanged, so files on disk are still recognised and
imported-or-reported as before.
docs/parity-checklist.md: LR-IMP-FORMATS row keeps main's DNG/Hasselblad text and adds #1165's SR2/SRF phrase; revision rows are the union
docs/raw-parity.md, docs/gaps.md: revision rows are the union
crates/ui-egui/locales/{cs,it,pl}-formats.json: kept the translations and added the track's new keys.
docs/parity-checklist.md: newer header line; revision rows from both sides.
docs/whats-new.md: both tracks' 2026-10-15 entries.
The web import picker hard-coded a 13-extension accept list, so raws the engine imports (CR3, NRW, RAF, ORF, RW2, RWL, RAW, PEF, SRW, …) were silently filtered out of the dialog on Windows/Edge, which matches accept case-sensitively. Generate the list from EXTENSIONS, each extension in both cases. Fixes#2212.
At Fit the loupe was softer than a good downscale of the same file. Four
resamples stacked up between the source and the screen:
- the render edge is rounded up to a 64 px step, so the texture was drawn up
to ~6 % smaller than its size and the GPU shrank it with 2-tap bilinear
sampling;
- the fitted texture sat on a fractional pixel position, so even at the right
size every screen pixel was averaged with its neighbour (#1831 fixed the
same for 100 %);
- the render box truncated its short side (2560 x 1706 for a 2560 x 1707
source), a slightly anisotropic resample that blurred one axis only;
- the codec's decode-time shrink at 2:1 and more, and SourceRef::Fit, used a
box filter, which halves the contrast of detail near the output's limit.
RenderJob::display_fit shrinks a full-quality render that is larger than its
drawn size to exactly that size with Lanczos-3 on the worker (after the caches
have the full render; drafts untouched; part of the key). The Detail view sets
it for the After and Before renders at rest, and snap_fit_rect draws a texture
that is its drawn size on whole physical pixels. The render boxes round their
short side, and the 2:1-and-more shrinks use Lanczos-3.
Refs #1472, #1042.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With stem naming, `IMG_0001.xmp` is the sidecar of every file of that stem and, in a
folder Lightroom has worked on, it is the raw's. On import the packet was looked up
with the library naming alone, so a TIFF or JPEG arriving beside a raw of the same
name read the raw's packet and took its rating, label, keywords and `crs:` develop
settings (the case radialmonster reported in #92; the read side of a catalogued photo
is #1523).
`ImportStems` ranks the files an import is adding as if they were catalogued already,
in the order `StemOwners` uses for writing: the library's files and the arriving ones
together, a raw first, then the file name in lower case. A file that does not own the
stem sidecar reads only its own `<file>.xmp`, never the sibling's packet. A file that
is not catalogued yet has no id, and the id it draws when the import commits is above
every id in the library, so it loses a tie against a catalogued file; between
arriving files the order they arrive in stands for it.
Folder synchronize and the Lightroom catalog import go through the same function, so
a file arriving there is ranked the same way.
photo.reload decoded every targeted file on the UI thread, so a selection
of raws froze the window until all were read. The command is now split into
plan / probe / apply (engine `cmd::reload`); the app intercepts photo.reload
and probes the files on a worker thread with a cancellable activity row
(kind `reload`, counted progress), applying the result as one undo step.
`wait: true` keeps the old in-place behaviour for agents; CLI/MCP unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reuse the engine's import extensions in MCP and CLI discovery. Add
recursive, case-insensitive discovery tests and record that DSC-F828
SRF import remains preview-only.
Co-Authored-By: Codex <noreply@openai.com>
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.
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>
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>
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>
Docs recounted with docs/raw/coverage.py: 241 verified, 35 unsupported. The MCP
headless extension list now comes from engine::import::EXTENSIONS upstream, which
already carries mef.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Setting up Synchronize Folder, an import review or an import made a table
of every photo in the library on the session's thread before the first
file was looked at, and naming one photo's sidecar worked out who owns
every shared sidecar name in the library. In a library of 3.5 million
photos (`scan_setup_bench`, release, a folder of 500): 5.8 s before a
Synchronize scan, 2.6 s before an import review, 1.7 s before an import,
2.4 s for each sidecar written.
- A scan keeps the library as it is (photos are shared, not copied) and
looks up the files it finds (`library_files`) and the contents it probes
(`library_hashes`) on its own thread.
- An import looks the library up with its first files
(`ImportJob::look_up_library`), from the library and the options as they
were when it was set up.
- The owners of shared sidecar names are worked out for the photos asked
about (`StemOwners::among`): all five callers name only those photos.
- Synchronize Folder names its photos' sidecars in the scan, after it has
seen that the folder is there.
Now 0.9 s, 0.05 s, 0.05 s and under 1 s. What is found, and how sidecars
are named, is unchanged: the tests hold the owners against the table of
the whole library as it was made before, and the key made without
allocating against the key, for every string of up to six of the
characters that decide it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unedited raw's face tile is cut from its largest embedded preview, read
the planned way (header and JPEG, never the sensor data), when the preview
holds at least 3/4 of the tile's pixels across the face; otherwise, and for
edited photos and other files, it is developed as before. Tiles keep going
to the memory + on-disk thumbnail cache, now at 16 px size steps so the
thumbnail slider reuses them. The planned preview path carries an Adobe RGB
preview colour space and applies it instead of declining to the whole-file
read (Sony alpha 1 previews).
- 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>
Conflicts in crates/ui-egui/src/lightroom_import.rs: main moved the
Lightroom import's progress from its own window to the shared activity
stack, and this branch had added a second bar to that window.
Resolved by keeping main's activity row and feeding it this branch's
step-by-step progress (LightroomProgress): the row counts the step under
way (the catalog, reading the photos, adding them) and names it, so it
moves while the photos are read. The progress window stays removed; the
whole-import fraction is still in the task's status.
The engine half of this branch merged without conflict and is what main
still lacks: reading the photos is counted, and Cancel stops it.
main's two activity tests now drive the task through LightroomProgress.
"Reading photos…" and "Adding photos…" are added to the language
catalogs, cut from the existing "… N of M" translations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Preserve Target-aware sidecar operations, add French/Ukrainian warnings, and ensure omitted diagnostic fields decode in the native UI.
Generated with Codex.
- New crate lightcraft-slideshow (L1, no UI): SlideSettings (margins linked or
independent, guides, aspect preview, zoom to fill, stroke border, cast shadow,
backdrop colour, colour wash, background image; lengths relative to the slide's
shorter side; validated and clamped) and a CPU compose() used by playback, the
Settings preview and slideshow.preview.
- Engine: slideshow.layout (partial merge, saved in prefs.json) and
slideshow.preview {id?, width?, height?, path?} (PNG to path or base64);
RenderJob::with_slide composes on the render worker; the background photo is
rendered once and shared.
- Playback: with a non-default layout each slide is the composed screen-sized
slide, prepared ahead like before; the default layout plays as before.
- Settings > Slideshow: Layout, Options and Backdrop sections and a debounced
live preview of the active photo's slide (with margin guides).
- Tests: composer geometry/pixels/hostile input, command get/set/persist/preview,
headless playback of a composed slide and of the default layout.
Raws without matrices in the file (ARW, NEF, RW2, RAF, CR2, CR3, PEF, SRW,
ORF) were all edited on the relative white-balance scale (6500 K / 0 = as
shot), even when lightcraft_raw::spectral has measured matrices for the
camera. Those 52 bodies (ILCE-7M3, ILCE-7M4, D850, EOS 5D Mark III...) now
edit in Kelvin like a DNG and like Lightroom.
Design:
- One decision on both sides: spectral::find_camera("Make Model") backs
catalog::relative_wb_camera (Photo::relative_wb); files::WbScaleOf decides
the same from the decoded file. A corpus test asserts they agree.
Photo::fitted_look keeps the old predicate for smart previews.
- The matrices only convert: As Shot is the file's multipliers read through
the model's ColorMatrix1/2 (spectral::as_shot_white_through). The look
(JPEG fit / profile / spectral fallback) is unchanged, and the slider
adapts from that white with Bradford. The camera-space rebalance stays
reserved for files with their own matrices.
- Presets resolve to their Kelvin values (WbMode::preset, sources cited).
- The Color panel shows the white balance in effect (effective_wb).
- XMP: crs::Target::for_file takes the camera; matrix cameras read
crs:Temperature as Kelvin (marked "scale": "kelvin"), others keep the
#510 as-shot shift.
Migration (no look changes):
- WhiteBalance.scale: WbScale { Legacy (absent in JSON), Kelvin }. A
white balance without a scale keeps its old meaning. On a spectral-Kelvin
raw (SourceInfo.legacy_relative_wb) the pipeline composes the old
adaptation (6500 -> value) with the new one (as shot -> ...) in Bradford
cone space (local::legacy_white). Von Kries scalings compose exactly, so
the matrix matches to f32 and the pixels are identical
(pipeline legacy_relative_white_balance_renders_as_before_on_a_kelvin_raw,
media old_relative_white_balance_on_a_kelvin_camera_keeps_its_look...).
- Every white-balance writer (develop.set/adjust/quickAdjust/wb/wbPick/
reset) first runs local::normalize_wb: a legacy value becomes the Custom
Kelvin value with the same look and is marked Kelvin. Partials without a
scale (old presets, develop.merge) stay legacy.
- Catalog format 7: Op::SetAsShotWb replaces the 6500/0 as-shot recorded
by older imports when the file is next decoded or reloaded (system op,
not an undo step). Format-version test updated.
- The web wire carries legacy_relative_wb.
Docs: parity (LR-EDIT-COLOR-TEMP), camera-preview-colour, xmp-interop,
lightroom-catalog-import, control-protocol, mcp, whats-new.
Import a photo, delete it, import the same file again: the import skipped
it as a duplicate "already in the library" while the grid showed it in
Recently Deleted, and the user was stuck (issues #298, #706). The default
for a file whose photo is in Recently Deleted is now to restore the photo,
with its edits, rating and albums (`onDeleted: restore`; `skip` keeps the
old behaviour, `fresh` imports a clean copy). A photo matched by content
whose file is gone is relinked to the imported file. The import review
checks such files, badges them "Will be restored", counts them as "in
Recently Deleted" and starts its choice on Restore them; dropped files
say "Restored 1 photo from Recently Deleted".
Browsing a folder in Local, Auto Import and a Lightroom catalog import
keep skipping: only an import the user asked for brings a photo back.
The import review's folder scan (and reading a folder for Local) is a
row in the activity stack: "Looking for photos…" while the folders are
listed, then the files read of the total. Its cross drops the scan at
once, without waiting for the file being read, as the old Cancel did.
The separate Scanning window is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The import reused the review's probes for every file it had seen, even
one deleted (or rewritten) since: such a file was added as a photo of a
file that isn't there. A cached probe is now used only while the file
is there with the same size (checked in prepare_files, on the import's
worker thread; the web's files keep their probes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan waits on the disk, not the CPU, so its two halves now run at
once: the look for new files (which probes on its own threads) beside
the checks of the library's photos (missing file, newer sidecar), which
run on several threads like the import probes do. A cancelled scan stops
checking. Results keep the photos' order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
heic-rs reconstructs iPhone HEVC tiles bit-exactly (checked against libde265), but
its colour stage was 2-27 code values off libheif on every iPhone photo: it ignores
the HEVC VUI and assumes BT.709 limited range. lightcraft-heif now converts YCbCr
itself (nclx, else VUI, else H.265 defaults; nearest chroma, bilinear for odd clap
offsets, as libheif), inherits tile ICC/nclx on grids, follows libheif's imir axis
and clap rounding, and outputs 16-bit RGB. Measured on 30 real and synthetic
files: within 1 code value of libheif (pillow-heif 1.8 / libheif 1.23).
Also: HEIF header reader for import (no pixel decode), embedded thumbnail for small
decodes, nclx/VUI colour hint (shared CICP mapping with PNG), clear 4:2:2/4:4:4
Unsupported error, Format::not_decodable as the one source of "why not" (import
reports "HEIC/HEIF support isn't included in this build"), `.heif` imports.
Release packaging and Nix build with the heif feature, as whats-new promised.
lightcraft_heif::testdata writes lossless (PCM) synthetic HEICs for the tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>