docs/parity-checklist.md: the PR's edit to the LRC-LIB-CATALOG-SWITCH row applied to main's row.
docs/whats-new.md: the PR's 2026-10-29 section added above main's sections.
docs/parity-checklist.md: header keeps main's newer date plus the PR's item; the PR's edit to the LRC-LIB-FOLDERS row applied to main's row; revision rows union, newest first.
docs/whats-new.md: the PR's 2026-10-28 section added above 2026-10-27.
crates/engine/src/import/job.rs: union of the imports (main's Seen, reusable_probes, sidecars_of plus the PR's FileFacts); the sidecar read uses main's batched found_sidecar and the PR's skip for duplicates with a known capture time.
crates/engine/src/import.rs, cmd/library.rs: main split these files; kept main's side here and ported the PR's hunks to import/job.rs, cmd/library/import.rs and cmd/library/specs.rs (module doc stays in import.rs).
docs: headers keep main's newer date plus the PR's item; the PR's row additions applied to main's rows; revision rows and What's New entries kept, one heading per date.
docs/parity-checklist.md: header keeps main's date plus the PR's item; the LR-IMP-ADD-DIALOG row keeps main's text and gets the PR's sentence; revision rows union.
docs/whats-new.md: both entries kept.
docs/gaps.md, docs/parity-checklist.md: header keeps the newer date plus both sides' items; revision row added newest first.
locales/ru.json: union of keys.
docs/gaps.md, docs/parity-checklist.md: kept main's header and rows, added the PR's video text and revision row.
crates/engine/src/lightroom_catalog.rs: the PR's two tests moved to lightroom_catalog/tests.rs (main split the module).
Dropped the PR's unrelated hunks (slide_overlay.rs, tests_export.rs, raw corpus tests) per the review.
Since a content duplicate carries its file's capture time, the duplicate
check ran after the sidecar read, so a duplicate whose file already has
a capture time read and parsed a sidecar it doesn't use (more I/O on a
network share). The check now happens first and the sidecar is read for
a duplicate only when the file has no capture time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#f2a516, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).
craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/engine/src/cmd/export.rs
- crates/ui-egui/src/dialogs/rules_editor/rules_move.rs
- crates/ui-egui/src/panels/detail.rs
A content duplicate whose catalog row has no capture time took the
capture time of the library photo with the same bytes, which Edit
Capture Time may have changed. The import job now resolves the file's
capture time (the file's, else its XMP sidecar's, else its modification
time) before it sets the file aside as a duplicate, and hands it over
with the file's metadata (`Duplicate::file`, a `FileFacts`). The
Lightroom import uses it; the catalog's capture time still applies on
top when the row has one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A catalog photo whose bytes match another library photo became a new record that took only the
camera description from that photo's record, so the copyright, rights, creator and other metadata
its file carries (EXIF, IPTC, embedded XMP), which a file import keeps, was lost whenever the
catalog's XMP didn't restate it. Copying them from the other record would have carried that
photo's edits instead.
The file import already reads each content duplicate before it skips it; the probed metadata now
rides on the duplicate (`Duplicate::file_meta`, not serialised) and the new record starts from it,
before the import defaults and the catalog's metadata go on top, as for a file import. Without a
probe (a path match, which this branch never sees) it keeps the exposure description only, no
longer the user-editable GPS.
Test: a content duplicate of a trashed photo, imported through the Lightroom job with a file
carrying copyright, status, usage terms and creator, keeps them and takes nothing from the other
record's edited notice, creator or GPS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A catalog photo whose bytes match another library photo got the generic develop defaults, so a raw
with no white balance or lens setting in the catalog lost its as-shot white balance and embedded
lens corrections and rendered unlike a file import. It now gets the import defaults (camera
defaults plus the default preset), as a file import does, before the catalog's settings go on top.
Show in Library selected the lowest mapped photo id, which can be a master left in Recently Deleted
whose new virtual copy the import added; All Photos doesn't show it. It now selects the lowest
mapped photo not in Recently Deleted, and the button is left out when there is none.
Tests: a content-duplicate raw with no catalog settings equals its camera defaults (as-shot WB,
lens profile on); Show in Library skips a trashed mapped photo and offers nothing when only
trashed or vanished ids are mapped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new record for a catalog photo whose bytes match another library photo copied that whole
record and reset a few fields, so the other photo's edits, history, versions, ratings and written
metadata (title, keywords, face regions) carried over, from a trashed photo too. It now starts as a
new photo and takes only what the bytes determine: format, size, kind, capture time, camera EXIF,
GPS, focus point, content hash, embedded lens data and preview-only state. The catalog's own
settings and metadata apply on top as before.
Tests: the duplicate test gives the other photo edits, history, a version, a title, keywords and a
Local baseline and checks none reach the new record; a reimport after a master and its virtual copy
were trashed counts each once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A catalog photo whose file matched a photo in Recently Deleted by content was cloned with the trashed
record's `deleted` flag (and its virtual-copy fields), so it landed in Recently Deleted while the report
counted it as imported. A new virtual copy of a trashed master did the same. Both are now live records,
as Create Virtual Copy already does.
Catalog photos matched by path to a trashed photo stay there (the Lightroom import's OnDeleted::Skip,
#706); the report counts them as `inRecentlyDeleted` and the result window says so in one line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reasons Try Again and the launch check give for a missing remembered library are now in the
interface language (all 17 catalogs). A folder that can't be looked into (no permission, a share
that timed out) is no longer called gone: lightcraft_engine::library::library_state tells a missing
catalog from a failed lookup, and the window shows the system's reason.
Require a covering embedded image only for renderable import candidates. Keep the available camera JPEG for preview-only raws and failed or unavailable render fallbacks; ordinary quick views and grid thumbnails retain their immediate stand-in behavior. Also release import preview state when canceled through the control channel.
Co-Authored-By: Codex <noreply@openai.com>
Each argument's check had its own 2 s, so several paths on a dead mount could hold the window back
2 s each. `launch_imports_in` now takes one `launch_deadline()` for the whole batch and checks each
argument with `launch_path_until`; once it has passed, an argument is imported without the check.
Test: a past deadline imports a library folder at once, without probing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Unix the folder is compared by device and inode with the home folder and its places, so a
spelling through a macOS firmlink or a bind mount is refused too; a name that is not a link is
read in full, which turns a Windows short name into the long one.
A relative path is made absolute against the working folder (as the commands act on it), a
path through a linked parent is compared by where the parent really is, and case is ignored
only on macOS and Windows.
The engine now refuses these two commands for the home folder and its Pictures, Desktop and
Downloads, whoever asks (the app, a script, an agent). The check is a pure function of the path
and the home folder; the commands read the real home once.
Lightroom Classic's HDR and panorama merges of monochrome raws store a
one-plane LinearRaw image as lossy (XYB) JPEG XL tiles whose codestream
declares a grey colour encoding (16-bit float with a linear curve, or
16-bit integer with the sRGB curve). Those were refused as "lossy JPEG XL
tile with a single colour plane", so the files opened preview-only.
A lossy grey tile is still coded as three XYB channels. It is now
restored like the RGB case: jxl-oxide renders linear sRGB (never the
declared encoding, which would clip), the single plane is the luminance
(CIE Y, D65) of that, then the declared transfer curve is applied.
One plane must be declared grey and three RGB; HDR curves, other white
points and intensity targets above 255 stay unsupported.
RENDER_CACHE_VERSION 70 -> 71: files that showed their embedded preview
before render from raw data now, under the same content key.
Refs #275
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM
Where the folder listing can't vouch for a name (non-ASCII names, a listing that is gone), each photo's sidecar names are asked of the disk, nine per photo, one photo after another. The batch's lookups now run 16 at a time; the loop that plans the photos takes each one's answer in order.
They use the same `at_once` as the probe checks, so the latency stand-in measured with the previous commit applies to them as well (16 at a time against one at a time); the lookups themselves were not timed on a share for this version.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reusing a scan's probes costs one stat per file to see that it is as it was. They ran one after another, so on a network share every file waited for a round trip. They now run 16 at a time, in the import's batches and in the scan's check of an earlier scan's probes.
Measured on this version with each stat made to take 2 ms (a stand-in for a share's round trip): 5,990 stats took 15.1 s one at a time and 0.95 s through `stat_all`, with the answers in the same order. An earlier version of this change, tried on a real network share, spent most of the import's time in these stats; that figure is not repeated here because it was not taken on this code.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
library_dir_of applies library::is_library (catalog.snap or catalog.log) to a
snapshot's folder and to a named folder, so a CATALOG.SNAP on a case-sensitive
file system no longer opens an empty library. The launch path runs the check on
its own thread and waits at most LAUNCH_PROBE_LIMIT (2 s); a probe blocked on a
dead mount falls back to importing the argument in the background.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The probe asked for a file's size, read it (an open and its own size question), and then asked for its modification time again by path: three round trips to a network share's server for what one open can answer. It now opens the file once and takes the size and the modification time from the open file. What is read, the work-gate weight, and the text of the error for a file that can't be read are unchanged.
- finish_8bit/finish_deep hand back the render itself when there is no
output sharpening and no watermark. Before, every export cloned the
image (~100 MB at 24 MP) and output_sharpen built a ~290 MB float copy
of it even for SharpenFor::None, all on one thread.
- Output sharpening's box blurs run row-parallel (par_rows); each pixel
is computed as before.
- The parallel JPEG encoder converts each band's rows to YCbCr inside the
band's task instead of first filling whole-image planes (~290 MB for
4:4:4 at 24 MP, written once and read back).
- fdct_quant rounds with plain arithmetic (round_i16) instead of
f32::round, a libm call per coefficient without SSE4.1; it equals
`x.round() as i16` for all 2^32 inputs.
Output is byte-identical (pinned by encoded_bytes_are_pinned and
sharpened_pixels_are_pinned, whose hashes come from main).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
examples/export_bench imports the files given and runs export_photo
(decode, full-size render, encode) N times, forgetting the decoded
original between runs like a batch export does, and prints the median
and minimum wall time. OPT takes export options as app.export JSON,
PHASES=1 times render and encode separately, OUT=dir writes the files
for byte comparisons between builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>