mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-13 17:31:09 -06:00
c31f91c8fec7843cda6078754363b7de34fea02b
5121
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c31f91c8fe | Updates: take craft-update from craft-libs main at 1b34eba41 | ||
|
|
2f4d095903 |
Merge pull request #3995 from dotCooCoo/fix/updater-starts-at-exit
Updates: the updater reports installing, ready, installed and failed, and installs at exit |
||
|
|
e62efc0af2 |
Updates: craft-update comes from storytold/craft-libs
The pin stays at the head of storytold/craft-libs#696. The commit is reachable from storytold, so the dependency no longer names the fork. |
||
|
|
9e8a7b7c88 |
Updates: the updater reports installing, ready, installed and failed, and installs at exit
craft-update moves from a231e517 to the head of storytold/craft-libs#696. The check result window now shows for every status but idle, so the first start after an update reports that it installed or why it failed. Restart to Update on a ready update asks the app to close. The install helper now starts when the app exits. on_exit calls Updater::start_pending_update through LightcraftApp on macOS, because the process ends there without dropping the app. |
||
|
|
7e2b8a6b27 |
Merge pull request #3833 from ZIDAN44/fix/3776-one-selected-count
Issue #3776: one selected photo shows "1 selected" in the header |
||
|
|
b3b49987d4 |
Release: refresh contributor credits for each release (#2254) (#3898)
About > Contributors still showed an old snapshot, because nothing regenerated contributors/contributors.json before a release. Opt in to the shared refresh from craft-common-actions: a read-only credits job regenerates the snapshot for the release commit, and the shared release workflow installs it before every source build. The snapshot is not committed back. |
||
|
|
de096566ff |
Merge pull request #3887 from eliets2/fix/issue-2028-kelvin-white-balance-missing-for-dxo-dem
Cover DxO demosaiced Fujifilm DNGs for Kelvin white balance |
||
|
|
10db03b091 |
LR-IMP-FORMATS: withdraw compressed ORF decoding pending provenance review (#3916)
* LR-IMP-FORMATS: withdraw compressed ORF decoding Remove the decoder introduced in #3775 pending provenance review while preserving uncompressed ORF layouts and embedded previews. Add unsupported probe/decode regressions, remove the prior provenance narrative, and restore camera coverage records to preview-only. * LR-IMP-FORMATS: refresh ORF support status and render caches * LR-IMP-FORMATS: document ORF withdrawal regressions |
||
|
|
daa83c3a43 |
Merge pull request #3862 from eliets2/fix/issue-2475-lightroom-import-mask-issue
Honor inverted Lightroom masks when importing catalog edits |
||
|
|
3b209a40b7 |
Cover DxO demosaiced Fujifilm DNGs for Kelvin white balance
DxO demosaiced DNGs from Fujifilm RAF files should offer the same Kelvin white-balance editing as the original RAF and Adobe demosaiced DNGs, instead of only a relative slider. Treat a DxO make on a DNG as transparent when checking Fujifilm (and other listed) models in covers, so the Kelvin scale is offered for those files. Test: dxo_dng_fujifilm_model_is_covered_for_kelvin failed before the change and passes after. |
||
|
|
8a0b91a95c |
GPU float output for the HDR render (F32Hdr) (#3856)
* Render HDR rendition on GPU for HDR display drafts HDR edits on HDR displays currently pay for a second CPU float render on every full render and show only SDR during drags, making slider drags several times slower. Allow F32Hdr requests through the GPU path with a float output target matching the CPU HDR rendition, and let draft renders carry HDR again instead of being excluded. Test: gpu_handles_f32hdr_renders_for_hdr_display failed before the change and passes after. * Issue #2297: float output from the GPU's finish kernel for the HDR render The kernel wrote 8 bits only, so `handles` taking F32Hdr left no float samples. It now keeps values up to the edit's peak (highlight desaturation, vignette, gamut map, curves above SDR white, grain, LUT profiles, as finish_with does), encodes with the exact sRGB curve as a float render does on the CPU, and writes three linear floats per pixel into `out`. Written words carry the sign bit, so work that never ran still fails the render instead of reading as black. hdr_histogram keeps its Quality::Draft exclusion: it renders on the CPU, and the UI already runs a small render beside the drafts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1b5b766513 |
WEB-WORKER-PARITY: compare serialized jobs with the CPU reference (#3782)
Fix #3778: native thumbnail jobs can use an already initialized GPU, making exact worker serialization assertions depend on test order. Keep exact image and histogram assertions against the engine CPU pipeline. The separate native window equivalence test is unchanged. Validation: reproduced on clean main |
||
|
|
097dac4566 |
Stabilize sidebar month-name test fixture (#3859)
Inject an empty local folder list so asynchronous host discovery cannot move rows between queued clicks. Preserve both clicks and all accessibility assertions. Fixes #3858 Co-authored-by: FredApps <FredApps@users.noreply.github.com> |
||
|
|
92adb04782 |
Issue #3768: dialog_role tests draw glyphs only craft-fonts has as boxes when built without it (#3769)
Settings lists each language by its own name, and Georgian has glyphs only in the craft-fonts faces, so egui_kittest's missing-glyph panic failed settings_is_a_dialog_too in every build without CRAFT_FONTS_DIR. Allow missing glyphs there, as tests/it/accessibility.rs does: these tests check roles and titles, not glyphs. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> |
||
|
|
e354de502b |
Use adapter texture limit for probed window device (#3853)
* Use adapter texture limit for probed window device Mixed-DPI dragging can grow the window past 8192 px; the probed device should allow the adapter's full texture size so the window keeps rendering. Change probe to request the adapter's max_texture_dimension_2d and add a GPU test asserting the probed device matches the adapter limit. Test: probed_device_uses_adapter_max_texture_size failed before the change and passes after. * Window device: the adapter's texture limit on GL too, which the conservative defaults would cap at 2048 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
45ba859f88 |
Merge pull request #3857 from storytold/house-rules
Copy the shared house rules into AGENTS.md |
||
|
|
8f638b33bd |
Treat a first intersect mask component as its base selection (#3854)
* Treat a first intersect mask component as its base selection A mask created with Intersect as its first component currently renders as empty, so the overlay and local adjustments never appear. Using that component as the base matches how Add and Subtract first components already behave. Change evaluate_one so the first component always sets the base alpha regardless of its combine op. Add a regression test with a single Intersect linear-gradient component asserting the start side is selected. Test: intersect_first_component_selects_its_shape failed before the change and passes after. * Masks: the GPU treats a first Intersect as the base too, and the process doc says so Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: what changes for masks that start with Intersect Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
081190c92f |
Match renamed missing files by content when size is unknown (#3852)
Libraries created on another OS may have no recorded file size for some photos. After moving OS and renaming the file, Find Missing should still relink it by its content hash instead of leaving it missing. Allow plan_find_missing to match renamed files by content hash when size is unknown by hashing unused candidate files, and add a regression test with a Windows-style path, unknown size, known hash and a renamed file in the searched folder. Test: find_missing_matches_renamed_file_by_content_without_size failed before the change and passes after. Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> |
||
|
|
ba8caf8fa6 |
Honor inverted Lightroom masks when importing catalog edits
Users importing a Lightroom catalog see masks that darken or lighten the wrong part of the photo, even though the mask exposure value itself looks correct. Carry the inverted flag through catalog mask orientation handling for develop and XMP masks, and add a regression test with an inverted radial mask. Test: inverted_radial_mask_keeps_its_inversion_on_import failed before the change and passes after. |
||
|
|
52134ffc95 | docs: preserve local guidance in house-rules rollout | ||
|
|
a941d706c3 |
Picture Package and Custom Package pages can print cut guides at each cell's corners (#3843)
* Picture Package and Custom Package pages can print cut guides at each cell's corners Picture Package and Custom Package pages can print cut guides at each cell's corners Picture Package and Custom Package pages can print cut guides at each cell's corners Test: picture_package_and_custom_package_pages_can_print_cut_guide failed before the change and passes after. * Print: Cut Guides in the package preview, page panel in its own function, tests follow the guides Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Print: Cut Guides sit at each cell's corners, outside any border Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Print: shorten the scroll position comment to what the code does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Print: back to one scroll position for the settings column Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0b23cc6384 |
Copy the shared house rules into AGENTS.md.
The blocks come from craft-contrib at dde09ea31e2e457f5b4653a6e76f7cdf55944bdf. CLAUDE.md imports AGENTS.md. The house-rules workflow checks the copy. |
||
|
|
d2bf0810bd |
Fix mismatched source/destination rects in before/after zoom window renders (#3842)
* Fix mismatched source/destination rects in before/after zoom window renders Both panes of Before/After cut the zoom window from one rect but paste it over another, which is exactly the inconsistent-offset and artifact behavior reported in issue #1035; a headless test exercising click-zoom in both panes with a fixed camera raw would catch the regression. Detail view: compute `visible` relative to `v.img` instead of `v.target` so the window's source and destination share the animated rect. Compare/Reference view: compute `visible` relative to `display` (the rect actually drawn) instead of `img` so texture-aspect fits land in the right place. Test: compare_zoom_window_tracks_the_zoomed_pane failed before the change and passes after. * Compare: keep before_target, return both rects so Survey's remove button stays on the photo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Format Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Compare: the early return gives both rects Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a856df5f42 |
Write camera make and lens make to EXIF on export (#3828)
* Write camera make and lens make to EXIF on export PTGui and similar stitching tools read EXIF Make/LensMake to identify camera and lens; without them, exported JPEGs from CR2 files are not recognized the same way as Adobe's export. Add make and lens_make fields to catalog Meta, populate them from the raw's EXIF on import in meta_of, and write them as EXIF Make and LensMake tags in export_metadata. Test: export_writes_camera_make_and_lens_make_to_exif failed before the change and passes after. * Export: read Make and LensMake on import and write them, with Model on its own Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Export: strip the maker from Model only as a separate word Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9c07f427f0 |
Arm the window renderer marker before probing the driver (#3817)
* Arm the window renderer marker before probing the driver The issue's own table says the window renderer is created without a crash marker, so a launch that dies in the driver writes nothing and repeats itself. The window does have a marker, but it is armed only after the risky probe has already run, which is exactly the case the reporter's Windows launch hit. Arm the window-init.marker with the renderer name before each probe in pick_probed, so the marker on disk names the renderer being tried while the driver is touched, and re-arm it for the next renderer when a probe fails. The next launch then reads that renderer via take_failed and reports it (the existing 'the window could not start with its ... renderer last time' notice) instead of failing silently again. Test: the_window_marker_is_written_while_the_renderer_is_probed failed before the change and passes after. * Window marker: fix the test's closing brace, skip it under forced glow, sync the marker to disk Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Format Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
09f0550751 |
Face boxes: drag a box's body to move it (#3846)
* Add test that dragging a face box body reports a move edit The test exercises region_overlay through the existing RegionEdit::Resize enumeration and CanvasMap/Photo types, asserting that a drag inside a box body reports a move. It fails on current main because region_overlay returns None for such drags. Add a #[cfg(test)] mod move_tests with a single test that builds a CanvasMap and Photo with one face region, simulates a pointer drag from the box body centre to a new position via egui RawInput, calls region_overlay, and asserts it returns Some(RegionEdit::Resize(_, moved)) with size preserved and rect clamped into [0,1]. Test: a_moved_face_box_keeps_its_size_and_stays_inside_the_photo failed before the change and passes after. * Face boxes: body drag in its own function, no edit for a box that ends where it began, tools keep the drag Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b2007e06a9 |
Remove the dead Cloud button from the top bar (#3832)
* Remove the dead Cloud button from the top bar A clickable top-bar button that does nothing confuses users and contradicts LightCraft's no-cloud design; removing it ends the confusion while save status stays visible through toasts (lib.rs save_status) and library.info. Drop the cloud entry from the top bar's right-hand icon loop in topbar.rs, which also removes its tied warning badge; nothing else in the repo references it. Test: top_bar_has_no_cloud_widget failed before the change and passes after. * Top bar: the cloud icon only shows as the warning that saving is failing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2a35db6bcf |
Add flash, exposure bias and serial rules to smart albums (#3821)
* Add flash, exposure bias and serial rules to smart albums Smart albums cannot filter by flash fired, exposure bias or camera serial even though the file metadata is already parsed. Add flash, exposure_bias and camera_serial to Meta, copy them in meta_of and on reload, and add flash, exposureBias and cameraSerial rule fields evaluated in Rule::matches. Test: flash_exposure_bias_and_serial_rules failed before the change and passes after. * Rules editor: Exposure Bias takes negative values Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3943279105 |
Colour noise reduction: joint linear chroma filter with speck brake, CPU and GPU (#815) (#1348)
* WIP: colour NR v2 prototype (env LIGHTCRAFT_NR_V2, CPU only) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 on by default (LIGHTCRAFT_NR_V2=0 turns it off), GPU renders read back for it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: cache renders with the new colour NR apart from older ones Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 on the GPU (fused down / apply kernels), coefficient grid sigma / 2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 on the CPU without full-size intermediates (row-streamed passes) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: report peak device-buffer use per GPU stage with LIGHTCRAFT_PROFILE * WIP: colour NR v2 linear mode (NRV2_LINEAR=1: c - Y, additive, noise model), Jensen term, mean-drift probe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 noise model with equal-count luminance bands and a non-negative fit; probe asserts the mean over 3 noise realisations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 linear mode by default (per channel, K 64, coarse pass smooths only the coarse part); CPU on GPU renders; real-colour probe Known: tests_window::noise_reduction_does_not_depend_on_the_window fails (the noise is measured in the rendered window; the square-root mode failed it too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 joint guide (NRV2_JOINT=1, opt-in), luminance guide knobs, leaf-next-to-black probe, camera-look guard from 858 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 reads its env knobs once (per-pixel env reads made the linear mode 11x slower); joint stage timings with LIGHTCRAFT_PROFILE Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: joint colour NR without redundant passes (fused paths + next weights, none after the last scale, delta-1 weights from the path), two-component buffers, parallel reconstruction Bit-identical to |
||
|
|
472560b309 |
Tests: clipping warnings crush the shadows with Blacks (#3839)
Main fails this test: Exposure -5 alone no longer leaves a channel at 0 in the demo photo. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
68b464011f |
Import review: an optional batch-edit recipe for newly imported photos (#2940)
* Import: an optional batch-edit recipe for newly imported photos library.import takes a recipe, a list of develop control ids and values. Each control is set, clamped to its range like a slider's, on every photo the import actually adds, after the XMP sidecar, the camera default and the chosen preset, as one more History step named Import adjustments. Photos that are skipped as duplicates, restored from Recently Deleted or joined from a browsed folder are not touched, as the preset and keywords never touch them. A recipe names plain adjustments only. White balance is refused, because a temperature means Kelvin on a raw with colour matrices and a shift from the as-shot white on a rendered photo, so one number cannot name the same look for a whole batch, and the as-shot white balance of a raw is never touched. Geometry and crop controls are refused, because the slider refits the crop around a new angle and a batch write cannot. Per-photo controls such as pointColor.0.hueShift are refused, because a write to a photo without that instance is dropped. The rule is on the control's spec (a control of the shared list, outside the Geometry section, not white balance, and one that set accepts), so new controls follow it. An id named twice counts once, the last value winning, and a list longer than the control list is refused. ImportJob::new checks a recipe built in code the same way, before any file is read or placed. preset.saveRecipe saves a recipe as a user preset that stores only the settings the recipe names, including a control the recipe sets to its default, and refuses a recipe that stores nothing. The preset gets its id the way imported presets do (user.<group>-<name>, then -2, -3 ...), so no two presets share one: an id from the list length repeats once a preset is deleted, and the first preset with an id is the one that gets applied. A name or group over 100 characters is refused, and saving the same recipe under the same name and group again returns the preset that exists. * Import recipe: tests Cover a recipe on import (the values, the Import adjustments step, a render, one undo step), the refusals (unknown ids, malformed items, per-photo controls, geometry and crop, white balance, too many entries, by the command and by ImportJob::new), clamping and a repeated id, a raw's as-shot white balance staying as imported, a saved recipe preset giving the same settings as the recipe (including a reset to a default), the photos the recipe leaves alone (duplicates by path and by content, a restored photo, a browsed photo that joins; a photo imported afresh gets it), preset ids staying distinct after a delete and surviving a reopened library, and both commands being listed with their parameters. * Import review: a Batch edits section The import review gets a collapsible Batch edits section: sliders for the everyday adjustments (exposure, contrast, highlights, shadows, whites, blacks, vibrance, saturation, clarity, texture, dehaze, sharpening, grain, vignette). The controls moved off their default go to library.import as its recipe, only when there are any, and a summary beside the section counts them. The recipe stays in the dialog until the import runs, so cancelling changes neither the catalog nor the presets. Save as Preset, which needs a name and a recipe, saves the recipe with preset.saveRecipe. It leaves the review's own preset choice as it is, so a preset picked for the import is never replaced by the saved one. The name goes in a TextField, as new text boxes do. The summary line is a new tr_format message, added with a translation to every formats catalog; the other new messages are plain and show in English until a catalog translates them. * Import review recipe: tests Unit tests hold the dialog to the engine: every control it offers is one a recipe may name, a slider moved back to its default leaves the recipe, and start passes a recipe to library.import only when the dialog has one. Driving the real review, Save as Preset saves nothing without a name or without a recipe and with both saves exactly the recipe (leaving the review's preset choice and the catalog alone), cancelling the review discards the recipe, and confirming imports the photos with it as one undo step, or without an Import adjustments step when there is none. * Import recipe: tests for the disabled Save button and MCP reachability The disabled state of Save as Preset was not pinned: the engine refuses a nameless or empty recipe anyway, so the preset count stayed the same even with the button always on. The test now also expects no error toast, which only an enabled button that asked the engine would raise. A new MCP end-to-end test drives both commands through the public tools: list_commands documents them, run_command takes library.import with a recipe (a recipe naming white balance is a tool error that imports nothing), get_develop shows the values on the imported photo, and the generated cmd_preset_saveRecipe tool saves a recipe. * Import recipe: follow main's register(ui) and Id::unique after the rebase Upstream main changed widgets::register to take &egui::Ui (every other call site passes ui) and deprecated egui::Id::new in favour of Id::unique (same hash, so the stored open/closed state is unchanged). Adapt the batch-edit recipe section so lightcraft-ui-egui compiles again. * docs: parity row LR-IMP-RECIPE for batch edits during import * Keep HDR resets when a recipe is saved as a preset A recipe that resets an HDR control to its default was dropped when it was saved as a preset, because the HDR subtree is left out of the settings JSON while it is the default. The recipe's settings are now compared with the HDR subtree always present, so the reset is stored alone and next to other adjustments. Round-trip tests apply the saved preset to settings with a non-default headroom. * Address review findings * Address review findings * Import review: the imports the merge left in body.rs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
826875470a |
Paint clipped pixels red/blue when clipping warnings (J) are on (#3638)
Fixes #3620 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0429844cd9 |
never-crash: deny the panic lints in every crate root; GPU asserts become fail paths (#2137) (#2170)
* never-crash: deny the panic lints in every crate root; GPU asserts become fail paths (#2137) The never-crash policy was documented but not machine-checked for the two largest crates: engine and meta carried only #![forbid(unsafe_code)], so a future unwrap/expect/panic would pass cargo xtask ci. Both crate roots now also carry as does xtask (previously un-annotated); the guarded expects in meta/src/xmp.rs and xtask's parity.rs / root() now use the documented #[allow(clippy::expect_used)] + invariant form. crates/gpu: the three production asserts become fail(FailKind::Fatal, ..) + return, so a binding-count mismatch, an over-long readback, or a wrong-length curve fails the render over to the CPU instead of crashing (finish_and_read also early-returns on len == 0, which would have issued a zero-size copy_buffer_to_buffer that wgpu rejects). xtask: new roots check (in ci) verifies every workspace lib root and every binary main.rs carries both attributes; crates/sysmem is the documented exception for forbid(unsafe_code) only. A panic in placement or readback is a fail, not a crash. * audit fixes: roots covers binary and cdylib/rlib roots; render gates on failed() after finish_block (#2137) Two reviewers audited the first commit; both highs it found are fixed here: - roots scanner: the by_ref().any()/any() pair consumed the kind iterator hunting for "lib", so every bin-only root (both app mains, the sam3-worker bin, xtask itself) and the cdylib/rlib web root was silently unchecked — the check covered 29 of the 33 real roots. Kind detection now collects the kinds once and matches lib/rlib/dylib/cdylib/staticlib, with a root_paths regression test over bin, lib and cdylib/rlib targets; unreadable root files now fail with the I/O error instead of a fake policy violation. - render.rs: the caller gates on failed() right after finish_block, so the malformed-curve fail path can no longer reach the band loop's p[y0_at] index on the empty block. - finish_and_read: the len == 0 path now submits the encoder before returning zeros, so a failure staged in the same render stays detectable (the old zero-size copy failed loudly via the error scopes; a bare return would have hidden it). - docs: finish_block and finish_and_read state their failure contracts; USAGE and AGENTS.md name the new roots step in the ci enumeration. * xtask: check a library root at the source path that cargo metadata reports The roots check read src/lib.rs for every library target, so a package that sets its library path was checked on the wrong file. Libraries now use the reported source path, with the same default as binaries when none is given. * xtask: match the crate-root deny lints by whole name, ignoring a trailing comment A lint named only in a trailing comment, or a lint whose name contains another (clippy::panic inside clippy::panic_in_result_fn), satisfied the deny check. Lint names are now compared whole and a trailing comment is ignored. * xtask: read the crate-root attributes from the header, not a fixed 30 lines The roots check read only the first 30 lines of each crate root, so a root whose attributes came after a longer doc header was reported as missing them. The check now reads the inner attributes at the top of the file: it skips blank lines and comments, reads each attribute whole, and stops at the first item. A multi-line list is read in full and a commented-out attribute does not count. * xtask: reword two comments in the roots check Comments only: the lint-name match and the source-path default are described without naming a lint or a build tool. No code changes. * xtask: report a root attribute that overrides the required denies * xtask: accept a runtime manifest dir only when it has a parent * xtask: render the overridden deny attribute without literal braces The write! escaped the braces around the substituted attribute and lint, so the violation printed `#![{allow}({clippy::panic})]`; assert the rendered message in the allow-override test. * Tests: new test helpers meet the crate-root panic lints Main added test helpers after this branch; the deny at the crate root now applies to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * roots: crates/wincolor is the second crate allowed unsafe Main added it for the Windows colour APIs after this branch; its module docs already say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
96f3fd2d61 |
Tests: the next folder drop gets 20 seconds to land (#3834)
It checked the file on the frame after the drop, which failed on busy Windows runners. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dcc8a62910 |
ui: first-run onboarding tour and What's New after an update (#2206)
* ui: first-run onboarding tour and What's New after an update The very first launch now opens a short Welcome tour (six pages over the real views: import, the Photo Grid, Detail, Assisted Culling, export and print, and driving the app with agents over MCP), and after the app has been updated the existing What's New dialog opens once on its own. Both are skipped once the running version has been seen. - UiState.seen_version: the last version whose startup dialog was shown (serde default/skip, so older ui.json files read as None = first run). - new startup module: parse_version, startup_dialog (FirstRun only when nothing was ever seen; WhatsNew when the version changed, comparing through parse_version and falling back to string difference when either side is unparseable), and open_at_startup — headless hosts (snapshots, scripted runs, tests: session-local state) never gain a modal. - UiState::record_startup_dialog_seen records on show, so a crash mid-dialog does not re-trigger it on every launch; ui.json saves every few seconds and at exit, as before. - Dialog::Welcome renders the tour with Back / Skip / Next (Get Started on the last page); every string goes through tr(), every menu path names a real command (file.addPhotos, view.photoGrid, view.detail, dialog.cull, dialog.export, dialog.print). - the decision runs once on the first frame (ui-egui, so desktop and web both get it); headless hosts and already-seen versions open nothing. * ui: the integration harnesses are headless hosts The accessibility and docking tests build the app directly and drive it through logic() and ui(). On a fresh UI state the first frame therefore opened the Welcome tour: its modal took the Tab focus the accessibility tests walk (canvas_tool_sliders_are_reachable_with_tab, tab_walks_the_right_panels_top_to_bottom) and the pointer the docking drag aims at (real_drag_float_close_reopen_and_state_reload_keep_photo_tool_mode). Mark both harnesses as headless hosts, as the Headless harness already does: a headless host never gains a first-run modal. * ui: translate the first-run tour into the 13 catalogs The 21 strings of the first-run tour (its six pages, its window title and its Next and Get Started labels) were in no catalog, so every language showed the tour in English. They are now in each of the 13 catalogs under crates/ui-egui/locales/, with the placeholders and punctuation of the English kept: the ellipses, the menu separators, the middle dots, the parentheses around shortcuts and the final full stop. ka.json stays key-sorted; the other catalogs get the keys appended. WELCOME_TITLE, WELCOME_NEXT and WELCOME_START name the three labels that are not part of WELCOME_PAGES, so one test can check all 21 keys: the_welcome_tour_is_translated_in_every_catalog. It fails with the catalogs as they were. The translations were written without a native speaker's review and need one for each language before release. * ui: translate the first-run tour into the four new catalogs main added sv, fi, id and vi after this branch was cut, and the_welcome_tour_is_translated_in_every_catalog iterates Locale::ALL, so the tour's 21 keys were missing there. Same block, same place (after "Visualize HDR"), in each catalog's own terms. * ui: the lifecycle gesture test is a headless host main's egui-037 lifecycle tests (added after this branch was cut) build the app directly and drive it through logic() and ui(), so the first frame opened the Welcome tour there: its modal took the dialog the delivered gesture under test opens (a_delivered_gesture_may_change_the_state_it_acted_on saw the tour where it expected View Options). Mark that one test's app as a headless host, as the accessibility and docking harnesses already are: a headless host never gains a first-run modal. The flag stays scoped to this test because the file's screenshot tests exercise the headless-capture fallback, which a headless host short-circuits. * ui: build the tour's shortcut chords from the platform bindings WELCOME_PAGES hard-coded Cmd+Shift+I, D and Cmd+P in its prose, and the renderer translated those sentences verbatim, so Windows, Linux and FreeBSD users were shown keys the app does not bind. The three pages that name a shortcut now take the command's live binding at render time (shortcuts::shortcut_of) and paint it the way the menus paint theirs (menubar::shortcut_text: a symbol chord on macOS, Ctrl+... elsewhere); no key is written in the source strings any more. A command whose binding was removed shows the menu path alone. The chorded sentences move from the message catalogs into tr_format! messages (placeholders are format!-checked in every language), translated in all 17 catalogs' -formats.json; the catalog keys stay, chordless, as the no-binding fallback, and the_tour_is_translated_in_every_catalog pins both per language. * ui: the tour's Develop page names the Before/After command, not the Y key The page's prose still hard-coded Y as the before/after key: remap or remove that binding and the tour sends users to a command that no longer runs. The body now names View › Before/After Side by Side the way the other pages name their commands, and every catalog translates the new sentence with its own menu label. * ui: record the startup dialog once its input is not blocked * Keep headless UI tests independent of onboarding Assisted-by: OpenAI Codex * Tests: integration harnesses mark the app as a headless host Services has no headless field; the app does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: Find Similar waits for the worker to set the row total It lost that race twice on the Windows gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c97fee7b2f |
Grid: the library header's selection count is cut, not clipped, when it is wider than the header (#3776)
In the library view the count was painted right-aligned at any width, so at the narrowest pane a long count started outside the header and lost its beginning. It now truncates with "..." and shows the full text on hover, as the folder header's count already did; the title still gives way first. The header moved into its own function so a test can render it at a set width, and the folder view's one-photo count is tested too. |
||
|
|
fe632e05f9 |
Keep a stitched panorama at the photos' own resolution (#3806)
* Keep a stitched panorama at the photos' own resolution The merged panorama of three 4032x3024 photos came out at 4924x2461, i.e. the same pixel count as a single source photo, so every frame was downsampled into the canvas. The megapixel and edge caps cannot cause this: the output scale in `stitch` is capped at 1.0, so the canvas is always at most the surface bounds; those bounds are simply too small because the recovered focal length (and therefore the projected extent) is too small for wide-angle frames. Pin the focal length to the one `register` derives (EXIF 35 mm equivalent, else the homography estimate) instead of letting bundle adjustment drift it freely when only a few frames constrain it, and make `stitch` size the canvas from the frames' own pixel density so no frame is resampled below its resolution. The new test stitches three wide-angle frames with a known focal and asserts the recovered focal and the output size stay at the photos' resolution. Test: a_wide_angle_trio_keeps_the_photos_resolution failed before the change and passes after. * Address review for pull request 3806 * Panorama: the lens sets the native scale, not average density The previous scale upsampled the frames centres, where a rectilinear frame already has f pixels per radian. What can go wrong is the fitted focal coming out below the lens, so EXIF focal length now raises the cap on the output scale, and nothing is upsampled past it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
547fd29f3c |
Extend CR3 white level to actual sensor saturation (#3805)
* Extend CR3 white level to actual sensor saturation The white level comes from Canon's stated value, not from the sensor data, so highlights between the stated level and true saturation are misclassified as clipped. Extending the white level to the real pile-up and folding the delta into baseline_exposure leaves everything below the old white unchanged while recovering the lost highlight range. Add RawImage::extend_white_to_saturation() to saturation.rs that scans for a saturation pile-up above the stated white (coarse 1/128 bins, requires >= 1 sample in 100,000 and >= 64 samples above), sets white to the lowest colour's pile-up edge or the bit-depth top, and returns the EV delta. Call it from cr3.rs decode() on Mode::Full and add the delta to ColorData::baseline_exposure. Bump RENDER_CACHE_VERSION in media.rs. Test: extend_white_to_saturation_above_stated_white failed before the change and passes after. * Preserve CR3 highlight headroom and exposure Assisted-by: OpenAI Codex * Tests: a probe does not know the white headroom a CR3 decode finds The header probe stays a header probe, so the comparison leaves out baseline_exposure, which a full decode adjusts from the samples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ba282d3637 |
LR-IMP-FORMATS: pure-Rust clean-room compressed Olympus ORF bitstream decoder (#3775)
* LR-IMP-FORMATS: pure-Rust compressed Olympus ORF bitstream decoder * LR-IMP-FORMATS: address CodeRabbit review feedback on PR #3747 * orf: bound predictor arithmetic, bit widths, and stream consumption * orf: use unsigned magnitude abs_diff for predictor differences * docs: correct Olympus verified-body count in parity header * orf: saturating wbits derivation, widen pixel shift to i64 before clamp * docs: sync raw parity headline figures with coverage tables * docs: record clean-room provenance and report 58 newly unlocked bodies for compressed ORF * docs: note LibRaw derivation and pending clean-room reimplementation for compressed ORF * LR-IMP-FORMATS: pure-Rust clean-room compressed Olympus ORF bitstream decoder * LR-IMP-FORMATS: sync orfc path in parity docs and correct mainstream weighted depth sum * LR-IMP-FORMATS: address CodeRabbit review feedback on ORF gap and evidence sync * Convert Negative 1/4: lightcraft-film, the negative conversion maths from Film Lab (#2490) The film model, levels in density space, film presets, colour and tone of Film Lab's core (same author, MIT), without the PhotoCraft-only sharpening and JSON scanning, plus a whole-scan measurement (base, black and white points, neutral balance) a host stores with a photo's settings. A library with no caller yet. Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> * LR-IMP-FORMATS: address CodeRabbit PR #3775 review (round 2) - orf.rs: scope sample-based bit-depth inference to Word16 branch only (from_word16 flag prevents heuristic from running on compressed ORF data) - orfc.rs: reject truncated compressed streams before allocating the frame (minimum 1 bit per pixel check) - orfc.rs: remove feedback_shift_644 == 15 special case (ordinary exponent) - ROADMAP/README/target-app-parity: fix '78 bodies' to '58 more bodies (2 verified)' — 78 is total Olympus+OM count, 58 were newly unlocked - target-app-parity: RAW readiness 80% -> 87%, recompute weighted sums - parity-checklist: refresh summary via cargo xtask parity --write Generated with Antigravity * LR-IMP-FORMATS: address CodeRabbit PR #3775 review (round 3) - target-app-parity: RAW camera decoding 85% -> 89% (559+721 of 1446) - target-app-parity: mainstream practitioner row lists ORF as per-model verification, not as unfinished decoder alongside CR3/HE NEF - log_writer: check_regular_directory_entry before truncate open (prevents symlink-following truncation of files outside the library) Generated with Antigravity * LR-IMP-FORMATS: regenerate parity summary after film crate merge cargo xtask parity --write: 83.2% of 543 in-scope rows (was 83.5%/532); sync target-app-parity.md checklist references to match. Generated with Antigravity --------- Co-authored-by: Yorgos S. <smyrgeorge@gmail.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
aa52251d2e |
Apply Auto settings to every selected photo (#3808)
* Apply Auto settings to every selected photo Users expect Auto and preset application to affect the whole selection, as Quick Develop already does. `develop.auto` is the only edit command in that file that hard-codes the active photo instead of the selection. Make `develop.auto` accept the same target rule as the other selection commands (`ids` when given, else the selection), analyse each target photo's own source with `auto_tone`, and commit the results as one batched undo step labelled "Auto". Test: auto_applies_to_every_selected_photo failed before the change and passes after. * Run multi-photo Auto settings off the UI thread Assisted-by: OpenAI Codex * Auto Settings: skip unreadable photos and refuse stale results One offline photo stopped Auto for the whole selection. The background result is checked against each photo's settings, because an undo changes them without a commit, and it commits through the command wrapper so sidecars and the durable save follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Auto Settings: a wrong photo id is refused, not skipped Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7b99ed3903 |
Presets change only the settings they name (fixes #2159) (#2207)
* Presets compose on the photo's original state, not the visible edits (fixes #2159) Applying a preset deep-merged it into the photo's current settings, so a second preset inherited whatever an earlier one (or a manual edit) had put in the groups it styles, and its amount interpolated against the visible values: a B&W preset followed by a color one stayed black and white. Each group a preset covers is now computed on the photo's original state (its import settings) and replaces the photo's own, at any amount; groups the preset doesn't name, and the photo's pixel repairs (masks, spots, red eye), are kept. An unresolved profile reference still means "the photo keeps its own profile", and the built-in color presets now name their treatment, so applying one after a B&W preset brings the colors back. The UI's hover previews and thumbnails show the same composed result. * Presets change only the settings they name (#2159) The commit before this one computed each group a preset names on the photo's import state and replaced the whole group. A preset therefore also reset the settings it leaves out: Blacks under Bright & Airy, the mixer colours it does not name, and imported exposure, whites and blacks under Crisp Landscape. Now each setting a preset names is computed alone, from the photo's import look when a default preset recorded one, and otherwise from its current value, because sidecar and catalog imports record no import look and the camera defaults are then not their import state. Every other setting keeps the photo's value. Pixel repairs (masks, spots, red eye) compose on the photo's own values as before, and masks are still added by the preset. A color preset applied after a black and white one still restores the treatment and the settings it names. Amount 0 applies nothing to a photo without an import look. An unresolved profile reference leaves the photo's profile as it is. Each case has a test. * Document that presets change only the settings they name The presets row and the preset amount row of the parity checklist now describe the composition: each named setting is measured from the import look when one is recorded, otherwise from the photo's current value, and the settings a preset does not name keep their values. The revision history records the change; statuses are unchanged. * Test amount 0 on a setting the preset names The amount-0 test used a preset that does not name exposure, so it passed even when the original was the camera defaults. Bright & Airy names exposure, and the test now checks that the photo keeps its value at amount 0 for that setting. * Keep a preset's null in an option field, and never take one from the original A preset that writes null into an option setting (a crop aspect, where null means free) was dropped below full amount: the scaled patch removed every null, including the ones the preset wrote. At 100 % the same preset cleared the aspect, at 50 to 99 % the photo kept it. Now a null the preset writes applies from half amount up, as its other values do. A null in the original never changes the photo: below half amount such a setting is left out, so the photo keeps its own value. Numbers and other values behave as before. The null-removal step is gone, since the scaled patch no longer writes nulls for settings the amount does not reach. Tests: a preset null clears an option field from half amount up and leaves it below; an option that is unset in the original does not overwrite the photo's value. * Build a preset's scaling base only below full amount The presets panel asks for every visible row at 100 % on every frame, and on hover. Each of those calls serialized the photo's current settings and its original although, at 100 %, the preset's own values apply as they are and the original is not read. Compose now builds the scaling base only below 100 %. The engine also builds the import state, or copies the photo's settings, only below 100 %. Results are unchanged; the existing preset tests cover both paths. * Preserve preset baselines and allocate safe mask IDs Assisted-by: OpenAI Codex --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
84a6f5674c |
Give the pipeline one exact output size instead of re-fitting (#3749)
* Give the pipeline one exact output size instead of re-fitting Export and the pipeline each compute the output size independently from two descriptions of the photo's shape, so an already-chosen export size can come back a pixel short when the pipeline re-fits against a reduced source whose aspect rounds differently. Add an exact output size to RenderRequest, make plan take it unchanged via an exact Frame::sized mode, drop the aspect-tolerance guard, and have export resolve one canonical size before calling the pipeline; add a unit test asserting plan returns the requested size unchanged when it matches the frame aspect. Test: plan_preserves_matching_box_size failed before the change and passes after. * Honor exact output dimensions for exports Assisted-by: OpenAI Codex * Export: an exact size does not stretch a source of another shape A raw whose decode failed can fall back to an embedded preview with a different aspect. The exact box now stands only against a proxy that differs by rounding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
476f9ee86a |
catalog: history steps and versions share segmentation grids (#2154) (#2203)
* catalog: history steps and versions share segmentation grids (#2154) Every history step stored its own copy of each segmentation grid, so a photo with AI masks rewrote every unchanged grid (about 220 KB each) with every slider change: on a real library, 183.6 MB of a 187.8 MB catalog.snap was duplicated grids (830 copies of 126 distinct ones). Snapshots now keep each distinct grid once, in Catalog.grids (a new serialization detail, format v10), and the masks of the photo's settings, versions and history steps refer to it by index (SegMask.grid, with the data omitted). The table is built while the snapshot is written and is never held in the live catalog: Journal::open resolves the references back to inline data (Catalog::repair), so rendering, hash64, presets, XMP and log records are unchanged, and catalogs written before v10 load and are upgraded as before. A reference past the table (a damaged snapshot) is cleared and reported, rendering nothing. Measured on 90 history steps over one 1024x1024 grid: the snapshot drops every step's copy of the grid (1.29 MB -> 0.30 MB in the fixture; on the reporter's library, the grids shrink from 181.8 MB to the 25.9 MB of distinct grids). * catalog: the snapshot's shared grids cover the import look too (#2154) A photo's import look (Photo.import_look, the settings a user default preset gave the photo on import) can hold segmentation masks, and its grids can be the same grids that the develop settings, versions or history steps hold. The snapshot writer referred to such a grid by its table index, but the table was built from the other settings holders only and the load resolved only those, so the look's mask came back empty with a dangling reference. Photo::settings_holders lists every settings holder (develop settings, versions, history steps, import look). The writer builds the table from it and the load resolves references through the same list, so a holder added there is covered on both sides. New test import_look_masks_share_the_table_and_resolve_on_load: a look whose mask shares a grid with the develop settings and a history step, plus a grid only the look holds. It reloads exactly, and the table holds both grids once each. Before this change the look's mask came back as data "" with grid Some(0). * catalog: state what a snapshot copy shares (#2154) The snapshot writer copies the catalog to serialize it. Its photos are shared (Catalog.photos holds Arc<Photo>), so the copy is a pointer per photo, not a deep copy; the comment said so without saying which tables are copied. It now lists them, and notes that the derived indexes start empty on the copy. * develop: a segmentation grid reference is written only by a snapshot (#2154) The inline serialization of a mask wrote its `grid` whenever one was set. A `grid` can be set in memory from a document: a mask given to a command (mask.update, develop.merge) or read from a preset goes through the derived Deserialize, and such a value stayed in the live catalog. The next snapshot wrote it inline, and on load the index resolved to whatever grid sat at that position in the new table, so a mask could take another mask's grid without a repair being reported. The same stray value also changed hash64 for identical settings. The inline form no longer writes `grid`, so it is the pre-v10 form again. Only the snapshot interner writes a reference, into the table it builds, so a snapshot cannot hold an index that points elsewhere. New tests: stray_grid_is_never_written_outside_the_interner (develop) and stray_grid_from_a_document_is_not_written_as_a_reference (catalog: a stray reference is not resolved to another photo's grid after a reload). Both failed before this change: the first wrote "grid":3, the second loaded photo one's grid into photo two. * Address review for pull request 2203 * Format the files this pull request changes * catalog: don't clone a grid that is already in the snapshot's table The snapshot's grid interning indexed with `entry(m.data.clone())`: the whole grid was copied before the map said whether it was already there, so every history step that repeats a mask's grid paid a needless allocation and copy on the snapshot path. The lookup is borrowed now (`contains_key`), and a grid is cloned (into the pool and the table) only when it is new, in the same first-appearance order. The background snapshot also held two catalogs: `snapshot_in_background` clones the catalog for the worker, and `write_snapshot` cloned it again to attach the grid pool. The writer now takes the catalog by value and the background path hands over its copy, so a snapshot in flight holds one catalog; the synchronous path keeps its own single clone. Not observable in behaviour: the grid tests and the catalog suite pass unchanged. * Tests: the hostile-mask test names the new grid field Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e50db33453 |
Cut edited thumbnails from the decoded source, not the file (#3810)
* Cut edited thumbnails from the decoded source, not the file In Detail view the decoded source of the photo on screen is held at full size, but the thumbnail branch of `source_ref_plain` only looks for a preview-level source, so each edit re-queued a thumbnail that decoded the original again —1.4 GB of gate usage for one 489 MB file, matching the reported stall on the SMB share. When building a thumbnail job, reuse the photo's decoded source at whatever level it is cached (preview or full), fitting it to the thumbnail's edge, so an edited photo's thumbnail no longer re-reads the original file while its source is already in memory. Test: a_thumbnail_is_cut_from_the_decoded_full_source failed before the change and passes after. * Reuse decoded sources for thumbnail rendering Assisted-by: OpenAI Codex * Web worker: box the fitted source, as SourceRef::Fit holds it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1b6e722292 |
Import review: say what the folder scan left out (#2944)
* LR-IMP-ADD-DIALOG: a folder scan counts the folders it could not read and the files it skipped
The bounded folder walk used to drop a folder it could not list, and every file its
caller refused, without a trace. It now counts both (Walked::unreadable and
Walked::skipped); which files are returned, their order, the limits and the
handling of links are unchanged, and the Limits struct is untouched.
import::expand_report keeps those counts with the walk's truncation flag
(Expansion); expand_within delegates to it. ScanOutput carries truncated,
unreadable and unsupported, filled by scan_with, and scan_report gives the
session-backed scan the same output while storing the probes for the import
that follows exactly as scan does.
library.importPreview returns truncated and skipped {unreadable, unsupported}
next to the keys it already had, and says so in its description. A walk that
stops at a bound is still logged once, in expand_report, as before.
* LR-IMP-ADD-DIALOG: tests for the scan's skipped, unreadable and truncated counts
The walk counts files its caller refuses (and not hidden files or the skipped
folder) and a folder that vanishes before it is entered. expand_report, scan_with
and library.importPreview report unsupported files, a walk cut at the depth
limit and an unlistable folder, keep the probes for the import, and leave a
file named on its own uncounted. The unlistable folder uses no permissions on
Unix and a deny entry on Windows, and does nothing where the process can list it
anyway.
* LR-IMP-ADD-DIALOG: the import review says when its scan left something out
When the folder scan stopped at the walk limit, could not read some folders, or
skipped files that are not a supported photo type, the review now shows one line
under the count (for example "Stopped at the scan limit: there may be more
photos · 2 folders could not be read · 3 files skipped (not a supported photo
type)"). It shows nothing when the scan left nothing out. The counts come from
the scan's output and are kept on the dialog.
The two messages with a count go in every formats catalog, as the build requires,
and the plain message in every plain catalog.
* LR-IMP-ADD-DIALOG: tests for the import review's scan note
Opening the review for a folder with a stray text file, for a tree deeper than
the walk goes, and for a folder of photos only shows the matching line, or none.
The line follows the counters while the review is open, is written in the
language in use, and names singular and plural counts correctly.
* LR-IMP-ADD-DIALOG: tracker note for the import review's scan note
The row says what the review reports when its scan left something out, what
library.importPreview returns for it, and which other folder walks do not report
these counts. Revision history and last-updated line added; the status stays
done and the generated summary is not touched.
* LR-IMP-ADD-DIALOG: XMP sidecars are not counted as unsupported files
The import reads a photo's XMP sidecar along with the photo, so a sidecar is not a
file the scan skipped. expand_report now leaves .xmp files (any letter case) out of
Expansion::unsupported, which feeds ScanOutput, the "skipped.unsupported" count of
library.importPreview and the review's note; every other file that is not a
supported photo type still counts. The walk itself stays generic: Walked::skipped
still counts every file its caller refused, and which files are returned is
unchanged.
* LR-IMP-ADD-DIALOG: tests for leaving XMP sidecars out of the unsupported count
A folder with a photo, sidecars in lower, upper and mixed case (one in a subfolder)
and a text file reports one unsupported file through the expansion, the scan and
library.importPreview; a folder of photos and sidecars only reports none. The review
shows no note for sidecars alone and the text file's line once one is added.
* LR-IMP-ADD-DIALOG: tracker wording for the sidecar exclusion
The row and the revision-history entry say that XMP sidecars are not counted among
the files the review reports as skipped.
* rename importPreview's walk-truncation flag to scanLimited
- avoids one reply using truncated for two meanings once #2705/#2873 land (maintainer request)
* ui: translate the import scan notes into cs, fi, id, it, ka, pl, sv and vi
* fix: register the scan note against the Ui, not the Context (main's register takes &Ui)
* Report diagnostics even when the scan finds no photos
When a scan finds no photos, the message now says what it left out: folders it could not read, files that are not photos, or a walk stopped at its limit. A complete scan with no photos keeps the plain message.
* Format the scan results the merge touched
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
098c9294aa |
shortcuts: H toggles mask pins while masking (#1458)
* shortcuts: H toggles the mask pins while masking In the Masking panel an unmodified H press runs view.maskPins (the same switch as View > Show Mask Pins) instead of opening Remove; a Remove key reassigned to anything else keeps opening Remove there, and outside Masking H still opens Remove. * shortcuts: test the H mask-pin toggle H toggles the mask pins while the Masking panel is open and does not open Remove there; from the other panels it still opens Remove and leaves the pins alone. * shortcuts: keep the H docs truthful In the Masking panel an unmodified H toggles the mask pins and does not open Remove, a Remove key reassigned to anything else still opens Remove there, and outside Masking H still opens Remove, so the Hide pins row moves from done to partial: Remove's spot pins have no hide toggle. The H comparison row, the revision histories and the parity summary are updated to match. --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
ef7f99d12f |
Add GFX100S II to the spectral camera matrix table (#3139)
GFX100S II is a current Fujifilm body with no measured-matrix path, so any photo its JPEG fit does not accept falls to the neutral fallback instead of step 4's matrices. A unit test pins spectral::find to GFX 100's matrix for this model, following the existing GFX100S → GFX 100 same-sensor pattern.
Add ("Fujifilm", "GFX100S II", "GFX 100") to SAME_SENSOR next to the other Fujifilm same-sensor rows, plus a unit test asserting the lookup returns GFX 100's matrices (fails today, passes after the edit).
Test: gfx100s_ii_uses_gfx_100_matrices failed before the change and passes after.
Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
|
||
|
|
58da12bc5c |
Export: built-in presets for Instagram Feed, Story / Reel, Web / Portfolio, Full Resolution (#2104) (#2209)
* The named-export-preset system itself (export.presets / export.savePreset / export.deletePreset, user presets saved with the library, the Export dialog dropdown and File > Export with Preset) already existed; this adds the four named built-ins the issue asks for, locks them in with an expand / round-trip / apply test, and updates the LR-EXP-DIALOG and LR-EXP-PREVIOUS tracker rows (the stale 'no named export presets yet' note goes away). * #2104: test all four built-in export presets, not only Instagram Feed The named built-in preset test expanded, validated and rendered Instagram Feed alone. It now runs the same checks for each of the four names: the expanded settings, validation as app.export params, the round trip through a user preset, and a render at the size the preset sets. Full Resolution Master sets no long edge, so its render must be the photo's own size, with no resize. The rendered size is also compared with output_size, the same calculation the exporter uses. * The preset sets a 1920 px long edge and nothing about the shape, so a 3:2 photo exports at 1920 x 1280 and no 9:16 frame is made. The export options have no crop-to-aspect setting, and none is added here. The preset's doc comment and the LR-EXP-DIALOG row of the parity checklist say so, and the checklist has a revision line for this and for the translations. * The Export dialog shows built-in preset names through builtin_label, which looks them up in the language catalogs. Instagram Feed, Instagram Story / Reel, Web / Portfolio and Full Resolution Master were in none of the catalogs, so every language showed them in English. Each catalog now carries the four names next to the existing JPEG (Small) and JPEG (Large) entries. The translations are drafts and need review by a native speaker of each language. A new test checks that every catalog has an entry for each of the four names. The catalog comparison only reports gaps, so without this test a missing name would pass silently. * Address review for pull request 2209 * #2104: the four built-in export preset names in the catalogs main added after this branch was cut (Finnish, Indonesian, Swedish, Vietnamese). The i18n test that every catalog carries the four names failed on them after the rebase; each of the four now carries them next to the existing JPEG (Large) entry, in the same shape as the other thirteen catalogs. The translations are drafts and need review by a native speaker of each language. * Say the Web / Portfolio preset keeps copyright and creator metadata The copyright export metadata policy embeds the copyright notice, status, usage terms and info URL together with the creator (export/encoding.rs fills the artist field before the policy match returns), so the LR-EXP-DIALOG row and the 2026-10-17 revision line understated the preset as keeping copyright-only metadata. Both now say copyright and creator metadata. * Docs: finish the parity-checklist merge The merge left conflict markers in the revision history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
baf079f659 |
LR-BEHAV-RENDER-FIDELITY: process V5 rolls raw tone curves off into white in stops, so raising Exposure keeps highlights (#1632) (#3783)
* LR-BEHAV-RENDER-FIDELITY: V3 camera tone curve rolls off in stops past its last knot (#1632) A raw's camera tone curve is fitted only to camera JPEG tones below 0.85, so its last knot sits 1 to 3 stops below the sensor's white. Past it, CameraTone::apply closes on white exponentially in linear light: the sensor's white renders at 8-bit 246-254 at Exposure 0 and 254-255 from +1, and raising Exposure turns the top stop the sensor recorded white. Process V3 extends the curve with CameraTone::apply_rolloff: same value and slope at the knot, then gap / (1 + c (2^t - 1)) at t stops past it, the gap halving every stop in the end. On 11 CC0 corpus files with a fitted look, unclipped pixels rendering white drop from up to 22.5% to 0 at +1 EV and from up to 35% to 0 at +2 EV; Exposure 0 renders change by at most 1 code on average. V1 and V2, the fit itself, JPEGs and raws without a fitted look are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: V3 generic raw curve rolls off in stops too; DRO-off, DNG and GPU checks (#1632) Raws without a camera curve take ToneMap::hdr, which reached white about 1/4 stop past the sensor's white and clipped there (o.min(1.0)). Process V3 now uses ToneMap::rolloff_hdr: from half a stop below the white point (tone::GENERIC_KNEE_STOPS) the extended Reinhard curve hands over to the same stop-based shoulder as the camera curve (tone::rolloff_past, shared with CameraTone::apply_rolloff), with the same value and slope at the join. The sensor's white renders at about 235 / 248 / 252 at Exposure 0 / +1 / +2 where V1 and V2 render 239 / 255 / 255. On 6 CC0 DNGs on the generic curve, unclipped pixels rendering white drop from up to 8.4% to 0 at +1 EV and from up to 17.2% to 0 at +2 EV. The DRO-off gate (look_fit::at_most) also checks the V3 shoulder; the rate bound already implies it, so no fit changes and LOOK_VERSION stays. Tests: the generic shoulder (join, slope, monotone, below white, no kink), a regression test that fails with the clipping curve, a DNG ProfileToneCurve under V3, the DRO-off property tests on the V3 shoulder, and a per-process camera-curve GPU check. Docs: process-versions, parity checklist (LR-BEHAV-RENDER-FIDELITY, LRC-DEV-PROCESS), gap 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: what's new: process 3 keeps raw highlights when Exposure is raised (#1632) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: a flat last camera-curve segment joins the V3 shoulder with its own slope (#1632) The V3 camera shoulder took the last segment's slope with a floor of 0.1, so a flat or shallow segment (valid for a fitted, DNG or saved curve) joined with a kink: slope 0 on the curve, about 0.1 just past it. The shoulder now keeps the segment's own slope at the knot. Below 0.1 a quadratic term in 2^t - 1 joins the denominator: it adds no slope at the knot and makes up the 0.1 pace further up, so a flat curve still closes on white. Segments at 0.1 or steeper are byte-identical, and the generic curve passes no floor. Comments now say the shoulder approaches white in real numbers and can round to it in f32. Found in an independent review of the branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: V4's GPU test pins V4; the corpus DRO photos keep their knots under the V5 bound (#1632) The GPU light-operator test assumed the default process is V4; it now sets V4, since V5 is the default. The corpus DRO test checks that dro_off's V5 bound leaves the second-last knot where the rate bound puts it on the Sony DRO samples, so their V1 to V4 renders don't change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: the DRO-off curve is main's again, so V1 to V4 keep it (#1632) CodeRabbit found that the V5 bound in `dro_off` could move the second-last knot of a DRO photo whose fitted last segment is flat, and V1 to V4 read that curve too. `dro_off` and `at_most` are back to main's, so the camera-look fit is unchanged and LOOK_VERSION stays. The DRO tests check the V5 shoulder where the lowered segment's slope over its squared gap is at most the fitted one's (then it stays below), and on the Sony DRO corpus samples. Also: `rolloff_hdr`'s doc names the HDR lift, and the release note names DNGs whose curve already reaches white. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5981ecadbb |
Issue #2165: a Lightroom collection that can't nest goes under the nearest set instead of failing the import (#3145)
* Issue #2165: place a Lightroom collection that can't nest where it is under the nearest set Lightroom Classic keeps a saved print, book, slideshow or web gallery inside a plain collection, but a LightCraft album nests only in a folder. The importer passed that collection as the album's parent, AddAlbum rejected it with "parent must be an existing folder", and the atomic batch rolled back every album and every catalog-only photo (#2165: photos but no collections; #2532: nothing at all). An album whose Lightroom parent isn't a collection set (plain, smart, a saved creation, system-only or missing) now joins the nearest set above it, or the top level, and the import report names it under warnings. Before each AddAlbum the parent is checked against the catalog's rule (an existing folder, or one added earlier in the batch), so no album can fail the batch. Builds on #2168 by @endthestart. Fixes #2165, fixes #2532. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Issue #2165: a Lightroom collection never silently loses its album; warnings name the set as the library does - Two collections never share an album. An album id the counter hands out that is already taken (the counter saturates, so a damaged one repeats) stops the import with an error naming the collection, as AddAlbum's "album exists" did before; it no longer drops the second collection and its photos while the report counts it. A collection listed twice imports once when both rows are the same and stops the import when they differ. An index that names one existing album for two collections gives the second an album of its own. - A relocation warning names the destination set as this library calls it (a set renamed after an earlier import), and a set whose earlier album isn't a folder is described as such, not as "not a collection set". - The staged-album set is no longer called `added`, which #3142 uses for its photo count in this function. - Tests: relocated re-import, a user-moved album on re-import, a renamed destination set, a set album that isn't a folder, sets nested under a plain collection, an exhausted album counter, duplicate collection rows and an index naming one album twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lightroom): name the Lightroom collection id in relocation warnings (#2165) A collection imported somewhere other than where Lightroom keeps it now ends its warning with "(Lightroom collection N)", so two collections that share a name can be told apart, as #2165 asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lightroom): a collection listed twice costs only the duplicate (#2165) A different collection under the id of an earlier one stopped the whole import. Like any bad Lightroom record it now costs only itself (#1588): the first row of each id is kept, in catalog order, and a different one is left out with a warning naming both; the photos listed under that id go to the first. A row repeated as it is still drops quietly. The warning reads as the one #3152 gives, so the import behaves the same whichever of the two lands first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(lightroom): the nearest-set search ends on a parent loop or a deep chain (#2165) `collection_parent` follows a collection's parents to the nearest set with a seen-set and a 64-level cap. `validate` refuses loops before it runs, so no import test reached that guard: a unit test now gives it A in B in A, a collection in itself, a child of that loop (with and without a set in it) and a 100-deep chain of plain collections. The duplicate-collection test also checks that an exact repeat gives no warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |