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`.
Found while testing crates/catalog/src/dates.rs:58 (normalize_iso), used by crates/engine/src/cmd/manage.rs:201
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.
An X-T30 RAF shot in Acros embeds a black-and-white camera JPEG, so the
per-file look fits only the tone curve, and the colour came from the
neutral "camera RGB ~ sRGB" fallback: a grey sky and muted colours. The
raw decode (lossless compressed) was correct.
Add a hand-kept SAME_SENSOR alias list next to the generated
rawtoaces-data table: the 26.1 MP X-Trans CMOS 4 bodies (X-T30, X-T30 II,
X-Pro3, X100V, X-S10, X-E4) use the X-T3's measured matrices, and their
white balance reads in Kelvin through them.
Since the web zip moved from trunk to cargo xtask web (aeaf4154), packaging/web/package.sh needs the
wasm-bindgen CLI at Cargo.lock's version, and the shared release workflow installs only trunk, so
0.9.0's Web (wasm) job failed. In CI, install it first (as FilmCraft and EffectCraft do).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+Shift+O (the all-masks overlay) shows each visible mask's painted region, but only the selected mask drew its white outline. Draw the outline for every visible mask while mask_overlay_all is active so the other Radial/Linear masks read like the selected one. Handles and the rotation grip stay on the selected mask, so nothing else becomes draggable.
Extends the all-masks overlay added in #919.
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/catalog/src/dates.rs:52 (shift_iso, tail = iso.trim().get(19..)); used by crates/engine/src/cmd/manage.rs:222
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.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>
Snapshot the requested catalog source and retain stale-result suppression.
Add regressions for relinking and superseded failed renders.
Partial follow-up for #1890.
The failed FreeBSD run (39062750738) failed only this menu-interaction test
(None vs Some(Red) at tests_panels.rs:1773), unrelated to the wasm-only
i18n change; the branch already sits on current main, so this empty commit
exists only to produce a fresh merge run.
default_language() — what the Auto setting resolves to — only read LIGHTCRAFT_LANGUAGE,
which does not exist in a browser, so the web build always started in English. ui-egui
gains an installable default (set_default_language + a pure locale_from_tags that picks
the first shippable language from BCP-47 tags via Locale::parse_tag), and the web app
installs it at startup from navigator.languages (falling back to navigator.language),
read fallibly through js_sys::Reflect so it also works from a worker. Desktop never
installs the override and is unchanged; a language stored in ui.json still wins, which is
exactly Auto semantics.
Tests: locale_from_tags takes the first shippable language (it-IT -> Italiano,
unsupported-first skips ahead, zh-TW -> zh-hant, ja_JP.UTF-8 -> Japanese, pt-BR region
variant), empty/unsupported -> En fallback.
The reviewer audit of the first commit found the DEFLATE arm computed row_bytes in
wrapping usize before the gate, so on wasm32 a hostile chunk header (width 2^29+1,
16-bit) could wrap cw * cpp * bits to a small value and slip a garbage chunk past the
new gate. row_bytes is now computed in u64 (the gate caps at MAX_SAMPLES, so the usize
cast is lossless on 32-bit too), checked_pixel_count takes u64 directly so no call site
can truncate before the check, is_packed12 uses a checked_mul chain instead of comparing
against a possibly-wrapped length, and the arw decode head documents the invariant the
cap provides: with w*h <= 2^30 every downstream usize product stays inside range on
32-bit targets.
Behaviour is unchanged for every input the previous commit accepted; the only shrunk
set is chunks that passed because wrapping made row_bytes small.
On wasm32 usize is 32-bit, so 65536 x 65536 wraps to 0 and products like w * h,
w * h * cpp, fw * fh * nc and row_bytes * ch wrapped past their own validation and let
later true-dimension indexing panic (profile.web sets panic = abort). All the enumerated
sites now validate through a checked u64 gate:
- lightcraft-raw gains checked_pixel_count(width, height) (u64, capped at MAX_SAMPLES),
used by the ARW decode head (the strip-length equality now compares the true sample
count, not a wrapped one) and by RawImage::check's data-length validation;
- the CR2 lossless-JPEG frame size is checked in u64 on both the Full and Header paths
(it used to be clamped AFTER wrapping), and the no-slices width fallback is checked;
- the TIFF DEFLATE chunk's inflate length is checked and capped before the cast;
- lightcraft-codecs gains checked_sample_buf_len for the finish() buffer bound, compared
in u64 via try_from so no cast truncates on 32-bit.
Tests pin the gate arithmetic with values that would wrap 32-bit usize (65536 x 65536,
u32::MAX x 2, 2^30 x 4). cargo check --target wasm32-unknown-unknown passes for both
crates. The issue's suggested wasm32 arithmetic-side-effects clippy pass is a follow-up.
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.
max_ifds (1024) x max_entries (8192) tables that overlap in the file — SubIFD pointers
shifted by one entry reuse the same bytes — could make Tiff::parse allocate ~8M Entry values
(~400 MB) from a ~100 KB input, since count-0 entries cost nothing from the value budget and
only the per-IFD entry cap existed. ParseOptions gains max_entries_total (65 536 default,
generous for real scanned TIFFs), charged per IFD before the entries Vec is allocated, so a
hostile file stops with TiffError::Limit("entry count") instead of amplifying.
Tests: a hostile overlapping-table chain stops after 8 IFDs inside one parse, and a
legitimate multi-IFD file (chain + Exif/GPS children) still parses fully.
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>
craft-libs#169 is merged, so the fork pin is no longer needed. Pinned to
storytold/craft-libs 82a24b006eefa036b43c09a67fb9a3fbd17ffe63 (main after the merge) rather than the older 12281d07,
which predates resolve_absolute and the Windows colon refusal.
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>
Retain the pre-frame size check and allocation budget while avoiding untracked input-sized buffers. Add the reported heap regression and container/progressive compatibility checks.
Co-authored-by: Codex <noreply@openai.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>
Use Taiwan-standard terms instead of Mainland ones (底片, 巢狀, 控制項,
影像, 浮水印, 儲存庫, 覆寫, 相依套件 …), match the in-app zh-hant labels
(照片庫, 算繪, 編輯前/後, 縮圖目錄, 開啟記錄資料夾, 光線/色彩/效果 panel
names, stock preset and profile names), and rewrite literal translations
that read stiffly or changed the meaning.
Use 照片庫 for "library" everywhere (it was split between 照片庫 and
圖庫), and 縮圖目錄 for the contact sheet progress label to match the
File menu item. Only translations change; English keys and format
placeholders are untouched.
Add README.zh-hant.md alongside the existing English and Simplified
Chinese READMEs, translated from the current README.md and aligned
with the in-app zh-hant terminology (crates/ui-egui/locales/zh-hant.json).
Cross-link all three README files in their language switchers.
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>