* CRW-LIB: decode Canon CRW through the shared craft-crw crate
LightCraft decodes Canon CRW (the CIFF raws of the EOS D30, D60, 10D, 300D and the
PowerShot G1/G6) through craft-libs' `craft-crw`, the clean-room decoder derived and
verified on the `clean-room-crw-format-decode` branch and moved there with its frozen
tables and derivation record. `crates/raw/src/vendor/crw.rs` is now the adapter: it maps
the crate's header and samples onto `RawImage` (CFA, levels, active area, orientation,
as-shot white balance, the make/model/time records a CRW keeps instead of Exif) and
converts the capture time through chrono. The dependency is a local path
(`../craft-libs/crates/crw`) until the craft-libs branch merges; then it becomes a git
revision.
Everything LightCraft-side from that branch comes along unchanged: `probe_bytes` fills
metadata gaps from the decoder's records so CRW imports name their camera; the EOS D60
bundled camera profile and its chart-measured dual-illuminant matrices (`chart_table.rs`,
`spectral::Camera` carries its illuminant pair) give the D60 a Kelvin white-balance scale;
CRW joins the JPEG-fitted starting look, the catalog's raw formats and `lightcraft-cli
calibrate`; `RENDER_CACHE_VERSION` 71; the CC0 CRW corpus entries for `cargo xtask corpus`;
coverage, parity, gaps, raw-parity, target-app-parity and ROADMAP updates. The G5 stays
preview-only (the crate refuses it with the reason) and the robustness test keeps its CRW
sample.
What does not come along: the decoder, its tables and `crw_oracle.rs` (now craft-crw's
digest-pinned sample test) and the derivation record, which lives in craft-libs
`crates/crw/clean-room/`. `clean-room/crw/` here keeps only the EOS D60 chart-calibration
document and its capture list, with a README pointing at the record.
Gates: fmt, clippy -D warnings (raw all targets; engine, catalog, cli), lightcraft-raw tests
(475 + 134, the corpus files decoding through the adapter), catalog tests, engine lib tests
(1,300 pass; the two failures, `tests_import::recursive_discovery_…` and
`originals::tests::write_targets_pin_absent_paths_…`, are in files unchanged from main),
`cargo xtask parity`, `assets`, `layers`, `coverage.py --check`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* no message
* no message
* Update documentation after moving code to craft-libs
* Review coment fixes
* CRW: pin craft-crw f3b62ec, a truncated entropy stream is Corrupt
craft-libs #472 makes craft_crw::decode refuse a stream that ends more than
Limits::max_stream_end_fill (64 samples, one block) before the frame does,
instead of filling the rest with predictor values and returning success.
The adapter already maps craft_crw::Error::Corrupt to RawError::Corrupt;
a regression test proves a truncated CRW now fails at this boundary (it
decoded as a flat frame on the old pin) while its header still reads.
Only craft-crw moves; craft-ui/docpath/update/window stay on c5a1cbf.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review comment fix
* Add some tests
---------
Co-authored-by: Robert Woldberg <314159+rwoldberg@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
unused_port() lets its port go before the CLI connects, and the denoise-serve test
does the same before starting its server: with several test threads the server could
start on a port a refused-connection test had picked, which then reached it instead
of being refused. The three tests now hold one lock for their whole run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `--connect` failure-hint tests connected to 127.0.0.1:18437 and assumed
nothing listened there, but agents give their apps' control servers ports in
18000-19999. With an app on 18437 the tests sent it `ui.inspect` and failed.
They now bind 127.0.0.1:0, drop the listener and use that port, and check the
`--control <port>` hint against it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Render caches: a database lens profile is pinned to the photo. A decode never
overrides the catalog's database profile (media::source::pinned_lens), and
the profile is part of the render, thumbnail and variant cache keys
(media::quick::lens_key; only database profiles with the correction on, so
existing thumbnails keep their keys). When a download replaces the
database's files (lens_profiles::replaced), the refresh also re-reads raws
whose profile came from the database and raws that named no lens; an
ordinary start does not. library.refreshLensProfiles takes {all: true}.
Installation: files are staged beside the database folder, SOURCE.txt is
written there last, and the staged folder replaces the old one as a whole;
a failed swap puts the previous database back (and recover() does at the
next start). The app's own folder is read only with its marker, and
installed() checks every pinned file at its pinned size.
Shared hosts: with_default_lens_database() only selects the existing folder;
the permission to fetch is with_lens_auto_download() (desktop only), and the
first-start decision is one function, lensfun_download::fetch_on_start.
CLI render / merge and the headless MCP server select the folder;
lens.downloadDatabase selects the folder it downloads into.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- EmbeddedLens.source is a serialized field an older build would drop:
catalog format 9 -> 10 (journal table row, NEWEST_FIELDS in the
version guard).
- lightcraft-cli snapshot reads a downloaded lens database but never
starts the download (scripts and tests stay offline).
- The staging folder name and file cap are native-only (unused in the
web build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- lensfun's data/db (CC BY-SA 3.0) is fetched by the desktop app on first
run into <config dir>/lensfun, pinned to commit 43f9e001 by name, size and
SHA-256, staged until every file is verified, with an activity row (never
bundled). LIGHTCRAFT_LENSFUN_URL lists other mirrors; LIGHTCRAFT_LENSFUN_DIR
still wins. lens.downloadDatabase / lens.databaseStatus for agents.
- The database is read per folder and again after a download; when it
changes, library raws without a profile whose lens it knows are probed in
the background and get it (untouched photos switch the correction on as
import would); ticking Enable Lens Corrections looks those photos up again
(library.refreshLensProfiles).
- EmbeddedLens records its source (file / lens database); the Optics panel
says "Profile: <lens> (lens database)", "Profile: from the file" or
"No lens profile found for <lens>." (13 locales).
- Tests: pinned download and refusal of a wrong file, the lookup and its
edited / untouched rules, and presets / Copy-Paste / Sync carrying only the
switch and amounts, each photo keeping its own lens's profile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 773740fbb22eb66e7f62bada9cb32b594b6ff4d7)
`inf`, `NaN` and `1e999` parse as f64 but serialize to JSON null, so the control was
silently skipped and the photo rendered without the requested edit. They are now an error.
Hostile-input check added to the render subcommand test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `app.gpu {wait: false}` with GPU rendering on starts creating the device in the background
(`warm_up`), so a caller that never waits still gets an answer later. `warm_up` starts one
thread at most, and may start again if GPU rendering was turned off before it ran.
- Settings > Performance asks for a repaint every 250 ms while it shows "GPU: starting…": the
device is created on another thread, which doesn't wake the window.
- CLI test: `{enabled: true, wait: false}` answers enabled with available, adapter and reason
null (skips with LIGHTCRAFT_GPU=0, since it starts creating a device).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: with GPU rendering off (preference, environment, earlier failure) the
no-wait reply now reports available=false and the reason at once, without creating the
device. The Performance tab no longer calls gpu::available() while the device is still
being created, which could block the UI thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: dev profile keeps line tables for workspace crates and no debug info for dependencies
* Control server tests: persistent_requests_and_slow_replies_outlive_the_idle_read_timeout no longer races the idle timeout
Each request (and the closing half-shutdown) reaches the socket before the
worker reads, so on a slow machine only the reply waits outlast the 100 ms
idle timeout, as the test intends. The idle test sends its input the same
way, and the no-frames test allows 5 s instead of 2 s to give up.
* CI: one integration-test binary per crate
Each crate's tests/*.rs become modules of tests/it/main.rs (51 test files
into 13 binaries, plus 6 that stay their own). Inner #![cfg] attributes
move to the mod lines. Files that change state for the whole process keep
a binary of their own: codecs jxl_tail_heap and raw sr2_heap (dhat global
allocator), gpu shutdown, shutdown_first and memory, ui-egui shutdown.
Docs, comments and the GPU probe name the new paths and targets
(--test it <module>::); the CI report names an it binary's skips after
the crate.
* CI: tests under nextest, doctests apart, one wasm check
xtask ci runs the tests with cargo nextest run --lib --bins --tests when
cargo-nextest is installed, else cargo test with the same targets and a
one-line notice; doctests run after them with cargo test --workspace --doc.
The heif and hardware GPU steps use the same runner. Examples stay out of
the test build (clippy --all-targets still checks them). Concurrent tests
under nextest follow the RUST_TEST_THREADS cap (NEXTEST_TEST_THREADS).
The wasm check is one cargo check --keep-going over the same crates, and
clippy runs with --keep-going.
* CI: tests run under cargo test by default, nextest with LIGHTCRAFT_NEXTEST=1 (per-process scene cache)
`lightcraft-cli commands` (text and --json) now sorts the registry by id
before printing, so each family's commands sit together. The engine's
registry order, used by engine.commands over MCP and the control channel,
is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A script line for `ui.screenshot` without a `path` had its generated path
assigned with `params["path"] = …`: `serde_json`'s mutable index panics on
an array, a string or a number, and the CLI installs no `catch_unwind`, so
a script handed to the documented headless workflow aborted the process.
Only an object (or null, like an absent `params`) takes the path now;
anything else is a script error naming the line, like the existing handling
of a malformed line.
The regression test runs the real binary on such a script and asserts a
non-zero exit, no "panicked" in stderr and the `params` must be a JSON
object message (it fails before the fix with `cannot access key "path" in
JSON array`).
A library with no photos opened on the Detail view, whose empty state said "Choose a photo in
the grid or filmstrip" while the grid was empty and hidden: the only screen that mentioned
import was off screen, and Import itself was nowhere but the File menu and its shortcut.
- An empty library (no photos, or only deleted or browsed ones) opens on the Photo Grid with the
sidebar (Square Grid stays as it is), in the desktop app and in `lightcraft-cli snapshot`. The
desktop app switches before it notes what its settings file holds, so the switch alone isn't
saved.
- The grid and the Detail view show "Your library is empty" with Import Photos… (primary) and
Import Lightroom Catalog… (not on the web) as buttons, and a line saying photos and folders
can be dropped into the window, with the key Import Photos… is bound to now (`⌘⇧I` on macOS,
`Ctrl+Shift+I` elsewhere, or a remapped one). Not while a folder is browsed under Local or
Recently Deleted is shown: their photos aren't in the library but are on screen.
- The right-hand panel says "No photos yet / Import photos to get started" there instead of
"Select a photo to edit".
- The top bar has an Import button beside Export in every library. The centred search field's
reserve grows by both sides' share (620, and a 190 minimum), so the filter icon stays clear of
the icons on the right at every window width, also with macOS's title bar in the window. The
top bar's icons are named for screen readers, and a refused click says why in a toast.
- `empty_state` is `empty_message` with buttons that do the next step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Wake the serving loop after protocol output fails and preserve the actual writer error. Use one process-owned stdin worker so the CLI can finish normal backend cleanup without waiting for client EOF, and terminate the output worker even when a progress hook is retained.
Closes#1527
Co-Authored-By: OpenAI Codex <noreply@openai.com>