Commit Graph
78 Commits
Author SHA1 Message Date
Jason Baldwin 6ec7044ebf Merge slideshow-templates into slideshow-export (origin/main: batch 31; whats-new keeps main's Reliability entry, parity summary refreshed) 2026-10-11 17:18:12 +02:00
Jason Baldwin 54c1048b57 Merge slideshow-titles into slideshow-templates (origin/main: batch 31; open_session keeps both the slide templates dir and main's apple_photos_background, formats locales merged, whats-new keeps main's Reliability entry, parity summary refreshed) 2026-10-11 17:16:36 +02:00
Jason Baldwin d485dc9011 Merge slideshow-overlays into slideshow-titles (origin/main: batch 31; whats-new keeps main's Reliability entry) 2026-10-11 17:14:04 +02:00
Jason Baldwin b2036ba50d Merge slideshow-composer into slideshow-overlays (origin/main: batch 31; whats-new keeps main's Reliability entry) 2026-10-11 17:12:42 +02:00
Jason Baldwin 2138175826 Merge slideshow-playback into slideshow-composer (origin/main: batch 31; locales merged keeping main's values, whats-new keeps main's Reliability entry, parity summary refreshed) 2026-10-11 17:11:12 +02:00
Jason Baldwin d14a44630d Merge origin/main into slideshow-playback (whats-new: batch 31's Reliability entry kept, Slideshow playback after it under 2026-10-11; parity summary refreshed) 2026-10-11 17:09:28 +02:00
Jason Baldwin ccfff38548 Merge slideshow-templates into slideshow-export (origin/main: sidebar and slideshow_export modules side by side; whats-new and tracker summary) 2026-10-11 16:33:00 +02:00
Jason Baldwin b061563383 Merge slideshow-titles into slideshow-templates (origin/main: headless sessions load main's SAM3 model and the slide templates; format-locale keys from both; whats-new and tracker summary) 2026-10-11 16:30:20 +02:00
Jason Baldwin 41c9cd3817 Merge slideshow-overlays into slideshow-titles (origin/main: title-screen RenderResult gains main's gpu_fallback field; tracker summary refreshed) 2026-10-11 16:27:59 +02:00
Jason Baldwin 56d78344c1 Merge slideshow-composer into slideshow-overlays (origin/main: watermark draws through the shared TextLayout with main's quarter-turn rotation, separate insets and preview floor scale; overlay watermark sanitizes insetX/insetY/rotation and maps legacy inset) 2026-10-11 16:26:40 +02:00
Jason Baldwin efaa39b8b5 Merge slideshow-playback into slideshow-composer (origin/main: geocode + slideshow crates side by side in the manifests, locale keys from both, tracker summary refreshed) 2026-10-11 16:22:50 +02:00
Jason Baldwin 7b0fc19ebb Merge origin/main into slideshow-playback (icons: Play/Pause beside grading and appearance icons; settings: Slideshow tab beside appearance cards; render: Slide and CompareRegion slots; tracker summary and whats-new) 2026-10-11 16:19:50 +02:00
Jason Baldwin b0972e4488 Merge slideshow-templates into slideshow-export (origin/main: integration batch 26, #975; no conflicts) 2026-10-11 15:05:18 +02:00
Jason Baldwin 7e4ff5fddf Merge slideshow-titles into slideshow-templates (origin/main: integration batch 26, #975; no conflicts) 2026-10-11 15:05:04 +02:00
Jason Baldwin 857c15f3b3 Merge slideshow-overlays into slideshow-titles (origin/main: integration batch 26, #975; no conflicts) 2026-10-11 15:05:04 +02:00
Jason Baldwin 29203d6eec Merge slideshow-composer into slideshow-overlays (origin/main: integration batch 26, #975; no conflicts) 2026-10-11 15:05:04 +02:00
Jason Baldwin ee8b56f1db Merge slideshow-playback into slideshow-composer (origin/main: integration batch 26, #975; Cargo.lock: lightcraft-slideshow 0.6.0) 2026-10-11 15:05:04 +02:00
Jason Baldwin 8d692ed0a9 Merge origin/main into slideshow-playback (menus.rs ui_enabled: keep main's compare.set and this branch's slideshow.* arm) 2026-10-11 14:51:59 +02:00
Jason Baldwin b391307009 LRC-SS-EXPORT: slideshow export to PDF, JPEG sequence and AV1 video
View > Export Slideshow... (and Export PDF / JPEG / Video in Settings >
Slideshow) writes the photos in view as their composed slides, between
the intro and ending screens that are on:

- PDF: a page per slide (1024/1920/3840 px long side, JPEG quality),
  /Dur and /Trans fades per page, optional /PageMode /FullScreen. The
  contact sheet's writer moves to engine/src/pdf.rs and streams to disk.
- JPEG: <name>-001.jpg... in a folder.
- Video: AV1 (rav1e, speed 10, low latency) in MP4 or WebM written by
  our own muxers (codecs/src/video.rs); fades rendered as blended frames,
  held slides one key-frame sample with a long duration; 720p/1080p,
  24/30 fps; no audio; capped at 2 hours.

slideshow.export runs in the background with a row in the activity
stack (activity.cancel stops it); outputs are written as .part and
renamed when complete, so cancel or failure leaves nothing behind. A
photo that can't be rendered is skipped with a warning.
slideshow.exportStatus reports the result.
2026-10-11 10:35:09 +02:00
Jason Baldwin 6bfe2235af LRC-SS-TEMPLATES: slideshow templates: built-ins of our own, user templates, browser with hover preview
A slideshow template is the Playback (not "prepare previews"), Layout /
Options / Backdrop, Overlays and Titles settings as one versioned JSON
document (format lightcraft.slideshowTemplate, version 1).

- Engine: slide_template (SlideLook, Template, built-ins, user template
  files in <settings>/slideshow-templates read through each section's own
  merged() validation; size and count caps; newer versions refused; bad
  files reported and skipped; file names sanitised so ids never leave the
  folder). Applying replaces the four settings in one step and keeps the
  replaced ones as "previous" for the session (prefs have no undo).
- Five built-ins of our own: Plain Black, Gallery Wall, Edge to Edge,
  Cinema Wide, Shooting Data (no Lightroom names or designs, no images).
- Commands slideshow.templates.list/apply/save/update/rename/delete/
  import/export; slideshow.preview {template} previews without applying.
- UI: Template Browser at the top of Settings > Slideshow (built-in, user,
  previous settings; click applies, hover previews on the selected photo,
  right-click update / rename / export / delete; save and import), file
  dialogs on the desktop; strings in the nine locales.
- Docs: parity row (partial), whats-new, control protocol and MCP.
2026-10-11 09:22:11 +02:00
Jason Baldwin 43f31532a7 LRC-LIB-CATALOG-IMPORT: Open Library recognises a Lightroom catalog and offers to import it (#891)
Open Library given a Lightroom Classic catalog folder, one of its .lrdata /
.lrcat-data folders, Backups/, the .lrcat or its -wal/-shm/.lock files used to
create an empty LightCraft library inside Lightroom's folder (or, for the
.lrcat file, close the current library and then fail). Now:

- engine: lightroom_folder::lightroom_catalog_at (names only, top-level scan
  bounded to 10 000 entries, never fails); Session::open_library refuses those
  places with EngineError::LightroomCatalog {path, catalog} before creating
  anything. A LightCraft library (catalog.snap / catalog.log) always wins.
- app: app.openLibrary checks before closing the current library, keeps it,
  returns {opened: false, lightroomCatalog, path} and shows a dialog with
  Import Lightroom Catalog… (file.importLightroom with the catalog found) and
  Cancel. The library problem window gets the error instead.
- Lightroom import status keeps the catalog path after the task ends.
- CLI --library explains how to import instead.
2026-10-11 08:40:18 +02:00
Jason Baldwin f98078415e LRC-SS-TITLES: slideshow intro and ending screens
Intro and ending title screens (Lightroom Classic's Titles panel): each on
or off, a background colour and, centred, the Overlays identity plate or a
line of text, in the plate's colour or an override colour, at a scale, for
its own duration (default: the slide duration).

- Playback timeline: titles are items (Item::Title) of Playback; the intro
  plays once before the first photo, the ending after the last photo when
  repeat is off; random order never moves them; arrows step across them;
  fades, pause and readiness work as for slides; n of N counts photos only.
- Engine: slide_title (TitleSettings in prefs.json, TitleCompose drawing
  through the slide composer and overlay text renderer), slideshow.titles
  get/set, slideshow.preview {title}.
- UI: titles composed on a render worker (Renderer::request_title), the
  control bar shows Intro / End, Settings > Slideshow > Titles section and
  a Photo / Intro / Ending preview choice; strings in the nine locales.
- Docs: parity row (partial), playback note, whats-new, control and MCP.
2026-10-11 08:24:54 +02:00
Jason Baldwin 0a09450809 Merge slideshow-composer (with main) into slideshow-overlays 2026-10-11 06:42:02 +02:00
Jason Baldwin 1b8cb436a8 Merge slideshow-playback (with main) into slideshow-composer
# Conflicts:
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/uk.json
2026-10-11 06:40:14 +02:00
Jason Baldwin 46e431a200 Merge origin/main into slideshow-playback (0.6.0 and shared docking) 2026-10-11 06:40:02 +02:00
Jason Baldwin c52ff0f014 Merge origin/main into albums-library-photos-only (0.6.0 and shared docking; locale catalogs: kept both sides' keys) 2026-10-11 06:40:00 +02:00
Jason Baldwin 13dd73eb49 Merge branch 'slideshow-composer' into slideshow-overlays 2026-10-10 23:00:04 +02:00
Jason Baldwin 213047b3ff Merge branch 'slideshow-playback' into slideshow-composer 2026-10-10 23:00:01 +02:00
Jason Baldwin 570b2a10c5 LRC-SS-PLAYBACK: the render-ahead test waits for the slide up too (renders finish in any order) 2026-10-10 22:59:58 +02:00
Jason Baldwin 12dc533396 LRC-SS-OVERLAYS: slideshow overlays: identity plate, rating stars, text with tokens, watermark
- Engine: slide_overlay (OverlaySettings in prefs.json, the new slideshow.overlays
  command): identity plate (text, or a PNG/JPEG file with its transparency; size,
  colour, opacity; render behind image), rating stars (own vector stars), up to 16
  text overlays with tokens ({caption}, {exposure}, {file}, {date}, {focal}… plus
  every rename token; unknown tags show as typed and are reported) and a drop
  shadow, each at one of nine anchors of the slide or photo plus an offset, kept on
  the slide; the export watermark on the photo. Sizes relative to the slide's
  shorter side; validated and clamped.
- Drawn by the engine as a second pass over lightcraft_slideshow::compose_with
  (new behind hook for "render behind image"); the watermark's text renderer moved
  to engine text.rs (TextLayout, Inter + craft-fonts CJK) and is shared; glyphs
  off the image are skipped so huge text stays cheap.
- rename::expand_tokens_with lets overlays add tokens without changing rename.
- Playback composes slides when overlays are on; slideshow.preview returns each
  overlay's box. Settings > Slideshow > Overlays section with live preview.
- Docs: parity row (partial), whats-new, control/MCP docs, nine locale catalogs.
- Tests: tokens, nine anchors, star count, plate image opacity and missing file,
  text shadow, resolution independence, hostile input, big-slide cost, CJK with
  craft-fonts, prefs persistence, preview and headless playback pixel checks.
2026-10-10 22:54:40 +02:00
Jason Baldwin a3c33658bb Merge origin/main into brush-hold-hardens (clean merge; tracker edit followed the parity-checklist.md rename) 2026-10-10 21:49:33 +02:00
Jason Baldwin ae033aaf32 LRC-SS-LAYOUT: slideshow layout, options and backdrop: slides composed with margins, stroke, shadow and backdrop
- New crate lightcraft-slideshow (L1, no UI): SlideSettings (margins linked or
  independent, guides, aspect preview, zoom to fill, stroke border, cast shadow,
  backdrop colour, colour wash, background image; lengths relative to the slide's
  shorter side; validated and clamped) and a CPU compose() used by playback, the
  Settings preview and slideshow.preview.
- Engine: slideshow.layout (partial merge, saved in prefs.json) and
  slideshow.preview {id?, width?, height?, path?} (PNG to path or base64);
  RenderJob::with_slide composes on the render worker; the background photo is
  rendered once and shared.
- Playback: with a non-default layout each slide is the composed screen-sized
  slide, prepared ahead like before; the default layout plays as before.
- Settings > Slideshow: Layout, Options and Backdrop sections and a debounced
  live preview of the active photo's slide (with margin guides).
- Tests: composer geometry/pixels/hostile input, command get/set/persist/preview,
  headless playback of a composed slide and of the default layout.
2026-10-10 21:47:42 +02:00
Jason Baldwin 7216c8589a LR-EDIT-COLOR-TEMP: raw-parity.md describes Kelvin WB for matrix cameras (#730) 2026-10-10 21:47:36 +02:00
Jason Baldwin 45227de921 Merge origin/main into kelvin-wb-with-matrices (no conflicts; docs/parity.md rename followed cleanly) 2026-10-10 21:46:44 +02:00
Jason Baldwin 0fdf6c5785 Merge remote-tracking branch 'origin/main' into brush-live-preview-gaps 2026-10-10 21:44:40 +02:00
Jason Baldwin 78c5b4d099 Merge origin/main into crop-angle-constrained 2026-10-10 21:39:04 +02:00
Jason Baldwin 62dacf2791 Merge origin/main into crop-rotation-artefact 2026-10-10 21:37:05 +02:00
Jason Baldwin 7a232ada8b Merge remote-tracking branch 'origin/main' into nef-probe-header-only 2026-10-10 21:37:02 +02:00
Jason Baldwin 7765255ec6 Merge follow-up: whats-new entry under 2026-10-10 2026-10-10 21:36:32 +02:00
Jason Baldwin de6b33c662 Merge remote-tracking branch 'origin/main' into reimport-after-delete 2026-10-10 21:33:45 +02:00
Jason Baldwin b4d7cd1a03 Merge remote-tracking branch 'origin/main' into gpu-fallback-per-render 2026-10-10 21:33:40 +02:00
Jason Baldwin 89516d2e3e Merge origin/main into lock-unlock-on-drop (whats-new entry moved under 2026-10-10) 2026-10-10 21:32:55 +02:00
Jason Baldwin e7076762c0 Merge remote-tracking branch 'origin/main' into sony-arw-washed-out 2026-10-10 21:31:53 +02:00
Jason Baldwin bf4a271a71 Merge remote-tracking branch 'origin/main' into lock-unlock-on-drop 2026-10-10 21:31:51 +02:00
Jason Baldwin be4cbebbd3 LR-PREF-PEOPLE: restore 'faces' typo in the tracker row after the merge 2026-10-10 21:27:27 +02:00
Jason Baldwin 2c2f28952f Merge origin/main into menu-scroll-clips-shortcuts (docs restructure; no conflicts) 2026-10-10 21:18:32 +02:00
Jason Baldwin 85b6fdaedd Merge remote-tracking branch 'origin/main' into faces-toggle-needs-a-model 2026-10-10 21:18:31 +02:00
Jason Baldwin 6ae35b0a5b Merge remote-tracking branch 'origin/main' into albums-library-photos-only 2026-10-10 21:18:30 +02:00
Jason Baldwin 46ff7bcf4e Merge remote-tracking branch 'origin/main' into grid-add-to-album-new-album 2026-10-10 21:18:25 +02:00
Jason Baldwin 4dd6fb5089 Merge origin/main into uppercase-extensions (whats-new: keep main's entries, re-add the Formats bullet under 2026-10-10) 2026-10-10 21:16:49 +02:00
Jason Baldwin dd73753723 Merge origin/main into info-dialogs-close (no conflicts; MENU-HELP-SYSINFO note now in docs/parity-checklist.md) 2026-10-10 21:16:30 +02:00
Jason Baldwin 179941f4ff Merge origin/main into dialogs-fit-the-window (re-applied LR-BEHAV-DIALOGS row and LR-EXP-DIALOG note onto docs/parity-checklist.md, regenerated summary) 2026-10-10 21:15:37 +02:00
Jason Baldwin 1064e9847d Merge origin/main into enter-applies-copy-paste (parity rows applied to docs/parity-checklist.md) 2026-10-10 21:07:39 +02:00
Jason Baldwin 52875ec3f9 Merge origin/main into slideshow-playback (docs restructure #788: parity.md is now parity-checklist.md; slideshow rows re-applied, summary regenerated) 2026-10-10 20:54:29 +02:00
Jason Baldwin 92077d0807 LRC-SS-PLAYBACK: slideshow playback: fades, random order, repeat, previews prepared in advance, playback controls 2026-10-10 20:31:38 +02:00
Jason Baldwin bf49935fe7 LR-BEHAV-DIALOGS: dialogs fit the window; options scroll, title and buttons stay visible (#781)
Every dialog but Import draws its options in a vertical scroll area capped at the
window height minus the title bar, frame and button row (measured on the previous
frame), so on a 1366x768 screen the Export dialog's title and Cancel / OK stay on
screen. Dialogs that fit keep their size. Lists that scroll on their own (What's
New, All Metadata, shortcuts, smart-album rules, credits) get shorter first rather
than scrolling inside a scrolling dialog. Import keeps its resizable window.
2026-10-10 18:57:37 +02:00
Jason Baldwin 19833e56e3 MENU-HELP-SYSINFO: System Info, All Metadata and What's New end with one Close button (#782)
Their confirm does nothing, so they join About, Shortcuts and Settings in the
footer's informational list: one Close, no Cancel / OK. System Info keeps Copy
to Clipboard; Esc still closes them.
2026-10-10 18:50:12 +02:00
Jason Baldwin 61df5d304f LR-MASK-BRUSH: holding the brush still adds no coverage; flow builds up by passes (#518)
A stationary hold recorded one pointer sample per frame and each sample laid
another dab at the same spot, so flow accumulated with hold time and the
feathered edge hardened. brush_dabs now lays no dab for a sample that hasn't
moved from the last one, and the canvas no longer records repeated samples at
the same position. Moving strokes and passes back over an area are unchanged.
2026-10-10 17:21:23 +02:00
Jason Baldwin 923b1cde5e LR-MASK-BRUSH: the live brush stroke places dabs like the committed one, no gaps on fast strokes (#517)
While the button was held the Detail view drew one hard tinted circle per
pointer sample, so a fast stroke showed separate dabs; the committed stroke
interpolates dabs a quarter radius apart with feather and flow. The live
stroke now uses the pipeline's dab placement (push_dabs, shared with
brush_dabs), dab falloff and flow accumulation in an incremental screen
buffer (crates/ui-egui/src/brush_live.rs): each frame stamps only the new
samples' dabs and uploads only the rectangle they touched. push_dabs also
caps the dabs of a hostile segment instead of looping without bound.
2026-10-10 17:21:22 +02:00
Jason Baldwin bbcbbebf8e LR-EDIT-COPY-PASTE: Enter applies the Copy and Paste Settings dialogs (#735)
Return presses OK in Choose Edit Settings to Copy and Paste Selected
Settings when no widget has the keyboard focus (a Tab-focused button or
checkbox takes Return itself, as egui does). Esc already cancelled them;
tests now cover both keys.
2026-10-10 16:49:39 +02:00
Jason Baldwin 76e633c9e1 LR-EDIT-COLOR-TEMP: Kelvin white balance for raws whose camera has colour matrices (#730)
Raws without matrices in the file (ARW, NEF, RW2, RAF, CR2, CR3, PEF, SRW,
ORF) were all edited on the relative white-balance scale (6500 K / 0 = as
shot), even when lightcraft_raw::spectral has measured matrices for the
camera. Those 52 bodies (ILCE-7M3, ILCE-7M4, D850, EOS 5D Mark III...) now
edit in Kelvin like a DNG and like Lightroom.

Design:
- One decision on both sides: spectral::find_camera("Make Model") backs
  catalog::relative_wb_camera (Photo::relative_wb); files::WbScaleOf decides
  the same from the decoded file. A corpus test asserts they agree.
  Photo::fitted_look keeps the old predicate for smart previews.
- The matrices only convert: As Shot is the file's multipliers read through
  the model's ColorMatrix1/2 (spectral::as_shot_white_through). The look
  (JPEG fit / profile / spectral fallback) is unchanged, and the slider
  adapts from that white with Bradford. The camera-space rebalance stays
  reserved for files with their own matrices.
- Presets resolve to their Kelvin values (WbMode::preset, sources cited).
- The Color panel shows the white balance in effect (effective_wb).
- XMP: crs::Target::for_file takes the camera; matrix cameras read
  crs:Temperature as Kelvin (marked "scale": "kelvin"), others keep the
  #510 as-shot shift.

Migration (no look changes):
- WhiteBalance.scale: WbScale { Legacy (absent in JSON), Kelvin }. A
  white balance without a scale keeps its old meaning. On a spectral-Kelvin
  raw (SourceInfo.legacy_relative_wb) the pipeline composes the old
  adaptation (6500 -> value) with the new one (as shot -> ...) in Bradford
  cone space (local::legacy_white). Von Kries scalings compose exactly, so
  the matrix matches to f32 and the pixels are identical
  (pipeline legacy_relative_white_balance_renders_as_before_on_a_kelvin_raw,
  media old_relative_white_balance_on_a_kelvin_camera_keeps_its_look...).
- Every white-balance writer (develop.set/adjust/quickAdjust/wb/wbPick/
  reset) first runs local::normalize_wb: a legacy value becomes the Custom
  Kelvin value with the same look and is marked Kelvin. Partials without a
  scale (old presets, develop.merge) stay legacy.
- Catalog format 7: Op::SetAsShotWb replaces the 6500/0 as-shot recorded
  by older imports when the file is next decoded or reloaded (system op,
  not an undo step). Format-version test updated.
- The web wire carries legacy_relative_wb.

Docs: parity (LR-EDIT-COLOR-TEMP), camera-preview-colour, xmp-interop,
lightroom-catalog-import, control-protocol, mcp, whats-new.
2026-10-10 16:09:31 +02:00
Jason Baldwin b8f868830b LR-CROP-STRAIGHTEN: every way of setting the crop angle keeps the crop inside the photo (#742)
develop.set / develop.adjust / develop.quickAdjust / develop.resetControl on crop.angle
(MCP set_develop, the control channel, lightcraft-cli render --set) and crop.set {angle}
now turn the crop as crop.straighten does, through the new CropGeometry::with_angle:
kept if it still fits, else the largest crop of its aspect inside the rotated photo.
Before, they left the full rect, and the export took in smeared edge pixels in the corners.
2026-10-10 16:01:25 +02:00
Jason Baldwin 60844cf162 LR-EDIT-CROP: no artefact inside the crop overlay when the crop is rotated (#731)
The dimming outside the crop box was four quads, each joining a side of the
photo to the matching side of the box. Aligned, that is the photo minus the
box; rotated, the quads cross themselves and each other, so wedges inside the
box were dimmed and wedges outside doubly dark, growing with the angle (the
reporter's -45° shows it best). The export was never affected: the overlay
alone was wrong.

The dimming is now `outside_crop_mesh`: the photo's rectangle minus the box,
as one mesh of four disjoint half-strips (outside edge i, inside edge i+1,
clipped to the photo). A flat or non-finite box dims nothing.

Tests: a coverage test over several box sizes and angles (every photo point
outside the box in exactly one triangle, none inside the box or outside the
photo), a degenerate-box test, and a headless pixel test at -45° (inside the
box the frame matches the 0° frame; the photo's corner is dimmed), which
failed on the old mesh.
2026-10-10 15:12:03 +02:00
Jason Baldwin 1b178ca2e5 LR-EDIT-DENOISE: a running job's picture is Ready only when the job reports (#719)
The denoise worker saves the picture, then tears down its rayon pool and
returns before it writes its outcome. A `denoise.pump` in that window saw the
file on disk (`denoise_check_photo` for the selected photo, or a reindex),
marked the photo Ready and still held the job as running: `PhotoState::Ready`
and `denoise_busy()` at once, the activity row up for a photo that said it was
done, and the flaky `a_photo_waits_for_its_turn…` assertion on a loaded
machine.

The disk checks now leave the running photo to its job: `denoise_poll` makes
it Ready in the same pump that clears `running`. A test blocks the worker in
the model loader, writes the picture in its place and shows the photo stays
Running until the worker reports, then Ready and idle together.
2026-10-10 14:05:37 +02:00
Jason Baldwin 90875cf8f2 LR-IMP-FORMATS: probe compressed NEFs from their headers, not the Huffman stream (#708)
`probe_info` (every file an import adds) ran the whole Nikon Huffman
decode on a compressed NEF because `nef::decode` had no header mode and
the masked-trailing-column test wanted the samples: 386 ms per 24 MP
file, the full decode time, where every other format probes in < 1 ms.

The header has no source for those columns (the D5100 and D7000 write 8
masked columns at value 0, compressed and uncompressed alike, with no
CropArea or other maker-note tag), so the probe cannot skip the test.
Instead both modes now judge the active width from the first 128 rows
(`nef::MASK_ROWS`) against the nominal white level: the header probe
decodes only those rows (`nefc::decode_rows`, the stream is sequential;
uncompressed files read only the strips holding them) and the full
decode uses the same rule, so `probe_info == RawImage::info` still
holds. On every corpus NEF/NRW the answer is the same for 8 to 512 rows
as for the whole frame. The whole-image size and truncation checks still
apply in both modes; the "lossy after split" end-of-strip rule is only
checked when the whole strip is decoded.

Release, same files: probe 66 -> 3.4 ms (D5100 lossless), 72 -> 3.2 ms
(D7000 lossy 12), 88 -> 3.1 ms (D7500 lossless 12), 92 -> 3.7 ms (Z f
lossless), 23 -> 1.5 ms (D5100 uncompressed), 54 -> 2.7 ms (B700 NRW).

Tests: a synthetic lossless NEF whose stream is cut off after two
thirds of the rows probes to the same info as the whole file while the
full decode reports the truncation; synthetic uncompressed NEFs with
masked columns in the top rows, lower rows, all or none give the same
active area in both modes. Both failed before the fix. Docs: the
`probe_info` doc comment, docs/parity.md (LR-IMP-FORMATS),
docs/gpu-pipeline.md.
2026-10-10 13:58:20 +02:00
Jason Baldwin 3285e0282d LR-LIB-DELETE: importing a deleted photo's file again brings it back (#706)
Import a photo, delete it, import the same file again: the import skipped
it as a duplicate "already in the library" while the grid showed it in
Recently Deleted, and the user was stuck (issues #298, #706). The default
for a file whose photo is in Recently Deleted is now to restore the photo,
with its edits, rating and albums (`onDeleted: restore`; `skip` keeps the
old behaviour, `fresh` imports a clean copy). A photo matched by content
whose file is gone is relinked to the imported file. The import review
checks such files, badges them "Will be restored", counts them as "in
Recently Deleted" and starts its choice on Restore them; dropped files
say "Restored 1 photo from Recently Deleted".

Browsing a folder in Local, Auto Import and a Lightroom catalog import
keep skipping: only an import the user asked for brings a photo back.
2026-10-10 13:51:10 +02:00
Jason Baldwin 0de789bac9 LR-LIB-LOCK: parity row notes the explicit release (#680) 2026-10-10 13:48:13 +02:00
Jason Baldwin 58f7e74f2a LR-IMP-FORMATS: file extensions match whatever their case (#705)
Issue #705 (Linux, Nikon `.JPG` hidden by the Add Photos dialog) is the
symptom of #342, fixed by e2d35de8 after the 0.4.0 release and shipped in
0.5.0: 0.4.0 handed GTK / the desktop portal `*.jpg`-style globs, which
they match case-sensitively. Every dialog filter now goes through
`add_filter_nocase` (`*.jpg`, `*.JPG`, `*.[jJ][pP][gG]`).

An audit of every other extension match (folder scan, Browse Folder,
drag-and-drop, the CLI, `codecs` sniffing, the XMP sidecar lookup,
raw+JPEG stems, export format by extension) found them all folding case
already. This adds the regression test that proves it: a folder with
`a.JPG`, `b.Jpg`, `c.JPEG`, `d.PNG`, `e.NEF`, an `a.XMP` sidecar and a
`b.Jpg.xmp` sidecar imports all five (both sidecars read) and
`library.browse` shows all five. Docs note what was case-sensitive and
when it was fixed.
2026-10-10 13:33:32 +02:00
Jason Baldwin 47e9181f69 LR-PREF-PERFORMANCE: GPU fallback reason per render; the export fallback test no longer reads process-wide state (#675)
`export_falls_back_to_the_cpu_when_gpu_work_is_lost` asserted on
`lightcraft_gpu::last_fallback()`, one value for the whole process that
every other engine test's GPU render overwrites between the export and
the assertion (out of device memory under parallel load, once "a GPU
render panicked"). It also switched the GPU off for the whole process
through the injected fault and cleared everyone's state with
`reset_failures()`.

A render now reports its own fallback: `lightcraft_gpu::try_render`
returns `Result<Rendered, Fallback>` (`NotTried` when the GPU was not
used, `Failed(reason)` when it was tried and the CPU must render), and
`render` is a thin `.ok()` over it. `media::develop_reporting` passes
the reason on as `RenderResult::gpu_fallback`, an export carries it as
`Exported::gpu_fallback`, and each file in an `app.export` batch result
has `gpuFallback` when its render fell back. `last_fallback()` stays as
the process-wide diagnostic for `ui.inspect` → `perf.gpuFallback`.

A simulated failure (`inject_fault`) is thread-local, and so are its
consequences now: a fatal injected fault stops GPU use on the injecting
thread only (`available()` false, `unavailable_reason()` set there);
`reset_failures()` clears that and nothing else, so neither reaches the
other tests in the process. Real failures still stop the whole process.

The engine test asserts on its own export's reason and checks that
another thread still has the GPU; `crates/gpu/tests/fallback.rs` uses
`try_render` and checks the same; a new batch test checks `gpuFallback`
lands on the file whose render fell back. Docs: gpu-pipeline.md,
control-protocol.md (app.export result), parity.md.
2026-10-10 12:45:42 +02:00
Jason Baldwin 8166185a0f LR-LIB-LOCK: release the library lock explicitly on drop (#680)
LibraryLock freed the library by letting its File close. On Unix the
flock lock belongs to the open file description and lasts while any
descriptor on it is open. When another thread spawns a process,
posix_spawn copies the whole descriptor table into the child and
O_CLOEXEC only closes the copies at execve, so a LibraryLock dropped in
that window stayed locked until the child exec'd. Re-acquiring the
library then failed with LockError::InUse(None) ("already open in
another LightCraft program") although nobody had it open. That is the
flake in tests_lock::second_opener_is_refused_until_the_first_lets_go,
whose neighbour lock_held_by_another_process_is_released_when_it_dies
spawns the test binary on a parallel libtest thread.

Drop now removes the owner note while still holding the lock (so it
can't delete the next holder's), then calls File::unlock(): LOCK_UN
through our descriptor frees the lock whatever copies a child still
holds. An unlock error is logged, never a panic; the handle still closes
right after, which releases the lock once every copy is gone, as before.

Tests: the_library_is_free_once_dropped_even_if_a_child_holds_a_copy
holds a try_clone of the lock's descriptor across the drop (the same
open file description a child gets between fork and exec) and
re-acquires; dropping_the_lock_frees_the_library_while_another_thread_
spawns_processes runs a thread spawning /bin/true against two seconds of
acquire/drop cycles. Both fail every run without the fix (the first
deterministically) and pass with it; the flaky pair went from 3/100 to
0/100 failed runs here. Both are cfg(unix): Windows handles aren't
inherited by CreateProcess.
2026-10-10 12:36:16 +02:00
Jason Baldwin c739f12e45 Merge origin/main into #690 (both sides added locale catalog entries: kept both) 2026-10-10 11:53:09 +02:00
Jason Baldwin 8a60e3cd30 LR-PREF-PEOPLE: Recognise faces stays off until a recognition model is chosen (#678)
Settings ▸ Faces let the switch be turned on with no recognition model
chosen or installed: the box ticked, Settings closed without a word, and
recognition was on with nothing to run. Now the box is enabled only with a
chosen, installed recogniser (and a build that can run models), says
"Choose a model below (Use) to start." on hover, and that hint stays under
it on or off; a box left on by an older build can still be switched off.
faces.enable {enabled: true} refuses without such a model, so the control
channel and MCP cannot reach that state either, and removing the last
recogniser switches recognition off instead of leaving it on with nothing
chosen. Installing a model still chooses it and switches recognition on.

Tests: tests_face_models::recognition_cannot_be_turned_on_without_a_chosen_model
and tests_panels::recognise_faces_stays_off_until_a_model_is_chosen (fails
on main at the first click). Docs: faces.md, parity LR-PREF-PEOPLE.
2026-10-10 11:51:56 +02:00
Jason Baldwin 94fed226df MENU-BAR: a scrolled menu draws no scrollbar over its rows (#691)
A menu taller than the room under the menu bar scrolls inside an egui
ScrollArea whose floating scrollbar is drawn over the rows' right edge,
where the shortcut text ends. At 1.0x the 2-point handle grazes the last
glyph; at 1.75x the bar is in its wide hovered state with a semi-opaque
background and hides the last character of every shortcut, which reads as
clipping. The scroll content, its clip and the rows are the level's width
in both modes (instrumented), so nothing else needs to move: the scroll
area hides its bar. The up/down strips, the wheel and dragging were already
the designed ways to scroll a menu, like native menus; the native macOS
menu bar does not go through this code.

Test: tests_menubar::a_scrolled_menu_draws_no_scrollbar_over_its_shortcuts
(a 700 pt and a 1400 pt window: no tall narrow rectangle along the level's
right edge, equal widths), via a new HeadlessView::painted_shapes; fails on
main with the bar's track [512, 59]-[514, 660] and handle.
2026-10-10 11:39:28 +02:00
Jason Baldwin bec920f67d LR-LIB-ALBUM: albums hold library photos; Local photos are skipped, a Local-only selection is refused (#684)
Album views list library photos only, but album.create {addSelected},
album.addPhotos and album.toggleTarget (B) took the selection as is, so a
browsed Local photo became an album member that never shows: count 1, an
empty album, a cover that can't render.

Engine: the three commands drop Local photos from their targets and report
`skipped`; a selection of nothing but Local photos is an error that says to
Add to My Photos first; album.addPhotos and album.toggleTarget use a new
has_library_selection enabler. album.create still makes the album, minus
the Local photos.

UI: for a Local-only selection the grid's right-click Add to Album is off
with a hover hint, Photo ▸ Add to Album starts with a disabled "Add to My
Photos First" item and its entries and New Album… are off, a folder row's
Add Selected Photos is off, and a drag over an album row shows not-allowed
and toasts the hint on release. A mixed selection runs and toasts how many
Local photos were skipped (drop, context menu, folder row, New Album
dialog). Context-menu album entries are button:contextAlbum:<id>.

Messages added to every locale catalog. Tests: tests_albums_local.rs
(engine) and tests_panels::local_photos_cannot_be_added_to_albums_until_
they_join_my_photos (UI, fails on main). Docs: parity rows LR-LIB-ALBUM,
LR-IMP-LOCAL, MENU-PHOTO-ADDTOALBUM.
2026-10-10 10:54:07 +02:00
Jason Baldwin 4721627d31 LR-LIB-ALBUM: right-click ▸ Add to Album offers New Album… (#676)
The grid context menu's Add to Album submenu listed only the existing plain
albums, so in a library without one it came up empty. The Photo menu's
submenu already ends with New Album…, and album.create adds the selection
by default; the context menu now has the same entry (after a separator
when albums exist). The submenu and the entry are registered as
button:contextAddToAlbum / button:contextNewAlbum for the control channel.

Test: right_click_add_to_album_offers_a_new_album_when_there_is_none
deletes every plain album, right-clicks a thumbnail, hovers Add to Album,
clicks New Album…, names and confirms it; the album exists and holds the
photo. Fails on main with "no widget button:contextAddToAlbum".
2026-10-10 10:29:08 +02:00
Jason Baldwin a677478b92 KEY-ZOOM: Cmd+= / Cmd+- / Cmd+0 zoom the photo, not egui's interface scale (#566)
egui's zoom_with_keyboard was left on, so at the end of every frame it took the
same keys as View ▸ Zoom In / Zoom Out / Zoom to Fit for its browser-style
interface zoom. On macOS the native menu bar got the keys first; on Windows and
Linux the whole UI scaled. theme::apply turns the option off for every context
(desktop, headless, tests).

Headless tests that pressed Cmd+Plus / Cmd+Minus to exercise the interface
zoom's viewport handling set the zoom factor directly; the CLI snapshot test
that did the same is dropped (the unit tests cover the mechanism). New
tests_zoom_keys.rs: the shortcuts step the photo zoom while the zoom factor
stays, and Ctrl+wheel over the photo zooms it in the Edit view.
2026-10-09 20:14:49 +02:00
Jason Baldwin 13e3406829 Merge origin/main into #540 (relative-WB format list: SRW from main joins the shared helper) 2026-10-09 18:12:44 +02:00
Jason Baldwin 29b3f35136 LRC-LIB-CATALOG-IMPORT: read Lightroom's Custom white balance by its as-shot shift on relative-WB raws (#510)
A Custom white balance from a Lightroom catalog or XMP sidecar was taken as absolute Kelvin for every
raw. Raws LightCraft develops relative to their as-shot look (ARW, NEF, RW2, RAF, CR3, CR2, PEF; 6500 K
means as shot) then read a small tweak as a large blue shift: ΔE 22 against Lightroom's export on the
photos of issue #510.

The crs mapper now takes a `Target` (preset, rendered, raw with a measured illuminant, relative-WB raw)
instead of a bool. For relative-WB raws, and rendered files with only `Temperature`, a Kelvin value is
converted to the same mired shift from the packet's `AsShotTemperature` / `AsShotTint`. Without that
reference (Lightroom catalogs keep none for Custom photos) the photo stays As Shot and the import
report lists `Temperature, Tint` as not carried over. Import, sidecar read and catalog import decide
the target the same way through `lightcraft_catalog::relative_wb_format`.

Tests: shift from the issue's numbers (3650 → 3578 K lands at ~6275 K), the As Shot fallback with its
report entry, target selection per file, and the catalog mapper's Custom-WB case.
2026-10-09 17:48:05 +02:00