Lightroom's Resize to Fit has two boxes: Dimensions (the larger number to
the long edge, whichever way round the photo is) and Width & Height (the
width limited to W and the height to H as the photo stands). LightCraft
had only the first, labelled "Width × Height". Add `ResizeMode::WidthHeight`,
`fit: widthHeight` beside `width` + `height` in app.export, the mode in the
Export dialog (the old one is now called Dimensions), and the docs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
Go back to one "image N: not imported; …" warning per image, the format the
import's other skip warnings use; the result window already caps the list
(first eight, then "N more warnings"). Note the behaviour in the
LRC-LIB-CATALOG-IMPORT tracker row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhZLBHaSnSko1eMByPibfh
The crop fit ran before the Auto Sync delta, so a Geometry edit carried
the active photo's fitted crop rect into the delta and onto every other
selected photo, replacing (and could enlarge) their own crops. The delta
is now taken from the edit as asked, and each target fits its own crop.
Review on #1990.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
A catalog with many orphaned images produced one warning line each (82 in
the reported catalog). Skipped images are now listed in one warning per
reason, showing the first ten ids. An image whose original path is empty is
skipped the same way instead of failing the whole import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhZLBHaSnSko1eMByPibfh
A catalog can keep Adobe_images rows whose AgLibraryFile, AgLibraryFolder
or AgLibraryRootFolder record is gone; Lightroom ignores them, but the import
stopped at the first one with "image N: missing file record". Such images,
and their virtual copies, are now skipped with a warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhZLBHaSnSko1eMByPibfh
Native already appends every non-CJK craft face via ui_fallback; the wasm build
only embedded BIZ UDPGothic and Noto Sans Thai, so add Noto Sans Georgian to
that filter. Check its glyphs next to Thai and Arabic, and bump craft-fonts-ref
to a71c20e (storytold/craft-fonts#27).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
A preset without `format` lost its `bitDepth` before the call's `path` was merged in,
because it was judged against JPEG: `app.export {preset, path: a.png}` with a 16-bit
preset wrote an 8-bit PNG. Session::export_params now merges first and drops a preset's
depth only if the format written (with the caller's fallback) lacks it; a depth the call
passes is still validated. export.savePreset judges the depth at once only when the preset
has a `format`.
app.export with a path like `output` or `output.xyz` and no `format` skipped the depth
check: the desktop app then wrote JPEG and the headless backend PNG, both at 8 bits.
ExportOptions::export_format resolves the format written (with the caller's fallback for
such a path), from_params_or validates against it, and both callers use it instead of
their own override.
Compare the typed camera-local-tone flag on both sides of the Auto Sync delta so selected peers receive Off while retaining their own profile id, amount, and unrelated settings. Add regressions for propagation, unchanged values, selection boundaries, and history/serialization behavior.
Co-authored-by: Codex <codex@openai.com>
Found while testing crates/engine/src/cmd/cull.rs:17-20 (secs, used by the burst grouping at cull.rs:45)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
A proxy whose stored pixels are an older encoding is stale for every
photo, not only the ones that take the per-file camera look, so DNG and
JPEG proxies migrate to the current encoding like the raws do. And a
source decoded from a smart preview now knows it came from the cache:
Build Smart Previews refuses it as the original, so an offline refresh
can no longer rebuild a stale proxy from itself.
Turning HDR off with Auto Sync active now turns it off for the other selected photos. Enabling HDR preserves existing per-photo HDR tuning.
Co-authored-by: Codex <codex@openai.com>
Unrelated to the #2135 security fix in the commit before this one: both lines
predate this branch and are rustc/clippy 1.96 lint churn. `cargo clippy
--workspace --all-targets` only warns about them, but `cargo xtask ci` runs it
with `-D warnings` (xtask/src/main.rs), so the gate fails before any test runs.
- `clippy::nonminimal_bool` in `slide_overlay::shadow` (reported twice, two
overlapping suggestions on the one expression): `!(A && B) || !C || !D` is
`!(A && B && C && D)` by De Morgan, which is what the line now says. The
operands are pure float comparisons and `is_finite` guards with no side
effects, so this is an exact rewrite — a NaN or infinite `mx0`/`my0` still
makes `shadow` return without drawing, as before. (Clippy's second suggestion
drops the `my0.is_finite()` term and is *not* equivalent; it is not used.)
- `clippy::drop_non_drop` on `drop(write)` in
`same_folder_as_original_refuses_photos_without_a_folder`. The line is not
dead: `write` is a closure holding a mutable borrow of `written`, and the
`drop` ended that borrow so the next assertion could read it. The closure now
lives in a block of its own instead, which ends the borrow at the brace
rather than relying on `drop` of a non-`Drop` type. Every assertion of the
test is unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
Photo ▸ Edit In ▸ Open Original in Other Editor handed a photo's stored path
straight to `open` / `xdg-open` / `explorer.exe`, with `Path::is_file()` as the
only check, and Lightroom catalog import kept original paths exactly as the
catalog had them. A crafted `.lrcat` could therefore put `payload.exe`,
`start.lnk`, `run.bat`, `page.hta`, `Evil.app` or `go.command` into the library
and have it run on one user click.
- `lightcraft_engine::open_path`: one gate for every path that leaves
LightCraft for the OS. The extension must be one LightCraft reads
(`import::EXTENSIONS`, which already covers the Edit In copy formats
`tif`/`tiff`/`psd`), it must be a regular file (so a `.app` bundle and any
other folder are out), what a link resolves to must be a read type too, and
what is handed over is the absolute path. A refusal is the sentence shown to
the user.
- Open Original and Edit In go through it; a refused photo is listed in
`skipped` with the reason, and the menu item is greyed out for a record that
can never be opened (extension only, so no disk or share is touched while a
menu is drawn).
- `lightroom_catalog::refuse_non_media` drops such images where the catalog is
read and again where its photos become records, with a warning each, so a
refused entry never becomes an openable library record. A virtual copy of a
refused original goes with it.
Tests: a catalog naming a program (and a virtual copy of it) imports neither
while the catalog's real photos do; a library record naming a program is never
handed to the stub launcher, which records no invocation, and a program
selected beside a photo leaves only the photo, by absolute path. Both fail
before this change.
Co-Authored-By: Claude <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/cull.rs:115 (find_similar; empty-`only` semantics at crates/catalog/src/query.rs:328)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/slideshow/src/settings.rs:296-300 (parse_color); same pattern in crates/engine/src/slideshow.rs:117-122 (fadeColor)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
On an Intel Mac with an AMD Radeon Pro 5300M the Metal compiler crashes
on candle's mlx_gemm kernel, candle-metal-kernels panics on the compile
error, and the worker reported "the AI model failed unexpectedly" and
loaded the model on Metal again for every next job, so AI masks never
worked there.
The worker now retries a job that fails on the GPU (a panic or a candle
error) once on the CPU, after freeing the model. When that works, the
model stays on the CPU for the session and the job's outcome carries a
notice that segment_poll shows once; when the CPU fails too, the GPU
was not the cause and the next job uses it again. Missing or damaged
model files, render and remote-worker errors are not retried. Only
builds with a GPU path (macOS) retry; LIGHTCRAFT_GPU=0 and
LIGHTCRAFT_GPU_BACKEND=off now keep SAM 3 on the CPU from the start
(lightcraft_gpu::env_off_reason), like AI Denoise and the face models.
lightcraft-segment: Sam3::load_cpu, GPU_BUILD; text_logits returns the
crate's Error so Describe failures are classified like clicks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Manual metadata reread now finds standard embedded JPEG XMP when no sidecar exists. Existing sidecars remain authoritative, metadata updates remain undoable, and rereading does not write a sidecar. The reader retains its bounded header policy; Extended XMP assembly is unsupported.
Co-authored-by: Codex <codex@openai.com>
Found while testing crates/engine/src/rename.rs:919 (start parsed at crates/engine/src/cmd/manage.rs:12)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict only in docs/parity-checklist.md: the generated summary is
refreshed with `cargo xtask parity --write`, and the Last updated line
keeps main's format with this PR's change note (today's date) instead of
the duplicated note the branch carried. ⌥-drag preview still wins over
AllMasks and the selected mask in view_overlay; overlay kinds 6–9 unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1: with no active photo saved (an empty Missing view leaves
none), open_stores worked the view out to pick its first photo, with the
same synchronous check of every original. Skip that on Missing Photos;
the test now opens both an empty and a full Missing view.
Forgetting untouched Local records on open kept the visible photos, and on
Missing Photos working them out stats every original on the UI thread
before the availability worker runs. A 37k-photo library on a sleeping
SMB share froze the window for minutes. Missing Photos lists library
photos only, never Local records, so it can keep nothing: skip it.
Export Keywords wrote with std::fs::write to any path, replacing a library
original or its XMP sidecar. It now goes through check_write_target and
write_file_durable like preset.export and curve.exportPresets.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
Canon's maker-note ColorData lists the levels of each white-balance preset
with Canon's colour temperature for it. For versions -4 and 13-15 the
Daylight, Shade, Cloudy, Tungsten and Flash rows now give a Kelvin curve
(kelvin_presets, generalised from five fixed points to any number, with a
from_unordered constructor that sorts and drops points that would turn the
curve back). Fluorescent (off the locus) and the Kelvin row (the dial's last
setting) are left out. Enabled for the PowerShot G1 X Mark III, which has no
colour matrices: its nine as-shot white balances read within 4 % of the
camera's own ColorTempAsShot.
Sources: own design on the existing kelvin_presets model; ColorData row
positions from the ExifTool Canon tag-name documentation, confirmed with
ExifTool run as a black-box tool on sample files.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A DNG whose profile has a LookTable but no ProfileToneCurve (Adobe Standard in
Lightroom-written DNGs) was not tone-fitted and rendered with the near-linear
default tone: flat and ~0.8 EV darker than Lightroom. A look table defines colour,
not tone, and the fit already applies the tables to its proxy. The preview may now
be 10% of the raw's long edge (and 384 px), so Lightroom's 1024 px previews
qualify. Two lossy ILCE-7CM2 DNGs now match Lightroom's mean linear RGB within 3%.
Render cache 65, look version 10.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows 11, `cargo test --workspace` on main fails 11 tests:
- tests_export::same_folder_as_original_exports_next_to_each_photo built its
expected paths with Path::join (`\`), but export joins the subfolder and
file name with `/` (join_path) on every platform. Expect what export writes.
- tests_folder_drop (8 tests) looked rows up as
`source:libfolder:C:\...`, while the Folders section names them with `/`
(`source:libfolder:C:/...`), so no row was found. Look them up the same way.
- mcp backend::tests::{valid_replies_and_errors_are_preserved,
malformed_replies_are_rejected_without_resending} failed only when the whole
crate ran: the shutdown and one-shot-process tests start a copy of the test
binary, the child can inherit a socket a fake server just accepted, and when
the child exits Windows closes its copy abortively, resetting the test's
connection. A test-only lock keeps socket tests and child-process tests from
overlapping (passes 6 of 6 whole-crate runs, failed 4 of 4 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>