Merge main into #2800

main replaced "Show Clipping" with "Clipping": the PR's Beschneidung term moves to the new key.
Also takes CodeRabbit's two wording suggestions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-24 10:08:20 -04:00
co-authored by Claude Opus 5.5
178 changed files with 13105 additions and 1104 deletions
+56 -3
View File
@@ -90,6 +90,7 @@ filmstrip is visible.
| `ui.set` | partial UI state, e.g. `{"view": "detail"}` | Resulting UI state |
| `ui.dialog.confirm` / `ui.dialog.cancel` | — | Close the open dialog (confirm runs its action; Synchronize Folder on a folder that is up to date answers `{"upToDate": true}` and starts nothing) |
| `ui.resize` | `{width, height}` | Resize the window |
| `ui.dropFiles` | `{paths: [..], hover?}` | Files dragged from the file manager. `hover: true` makes every following frame report `paths` as hovered over the window (the drop card appears, `ui.widgets` lists `dropOverlay`) until another `ui.dropFiles` replaces them; `{paths: [], hover: true}` ends the hover. Without `hover` the files are dropped: the hover ends and the next frame receives them as dropped files, exactly as an OS drop arrives (photos and folders open the import review, or import at once with Settings ▸ Import ▸ Import dropped files without review). An empty path stands for a file whose path the platform doesn't tell |
| `ui.zoomFactor` | `{factor}` | Set the interface scale (0.5–3; egui's zoom keys are off, Cmd+= / Cmd+- zoom the photo) |
| `ui.screenshot` | `{path?, headless?}` | `{path, width, height}` once the frame (with finished renders) is captured. `headless: true` draws the UI on the CPU (no compositor needed); a windowed capture that gets no frame within 2 s falls back to headless automatically. While the loupe still waits for a render, `ui.widgets {filter: "loupe:rendering"}` lists its "Rendering…" pill; `loupe:renderFailed` is the pill saying a render failed behind a stand-in; Compare and Reference panes have their own (`compare:rendering:<pane>`, `compare:renderFailed:<pane>`) |
| `engine.execute {command: "app.export", params}` | export params (see `docs/mcp.md`), plus `preset`, `dir` / `path`, `ids`, `background`; `destinationMode: "sameAsOriginal"` writes each photo into its original's folder (+ `subfolder`) instead of `dir` | Writes the files and returns `{files}` (one `{path, width, height, bytes, sidecars}` per file, plus `gpuFallback` — why that render fell back from the GPU to the CPU — when it did; see `docs/gpu-pipeline.md`); with `background: true` (what the Export dialog and menus use) it returns `{background: true, total}` at once and the batch runs on a worker thread — poll `ui.inspect` → `export` |
@@ -165,7 +166,42 @@ catalog, a missing drive), the session starts empty and in memory — never with
to do: `ui.inspect` → `libraryProblem` is `{path, error, temporarySession, pendingImport}` (else `null`); its buttons
are `button:libraryRetry`, `button:libraryChoose`, `button:libraryTemporary` (Continue Without Saving) and
`button:libraryQuit`. A temporary session shows a banner (`indicator:temporarySession`, `button:libraryReopen`) and
writes nothing. `app.openLibrary` opening a library ends it.
writes nothing. `app.openLibrary` opening a library ends it. `pendingImport` counts the files waiting for a
library: those given on the command line, and photos dropped on the window while it (or the launch list below) is
up (desktop app); they are imported into the library that opens, or into the temporary session. Presets dropped
then are refused with a toast.
### Choosing the library at launch
With Settings ▸ General ▸ At launch set to Ask me (`ui.set {"settings": {"atLaunch": "ask"}}`; `"last"`, the
default, opens the last library), or Alt/Option held during the first 2 s after the window appears, the desktop app
shows a window listing the recent libraries (issue #1290). `--library` and `LIGHTCRAFT_LIBRARY` skip it, and so does
`--memory`; `lightcraft-cli` never shows it. `ui.inspect` → `libraryProblem.chooser` is `{libraries: [{path,
lastOpened, available, checking, reason}], selected, libraryOpen}` (else `null`; `libraryOpen: true` when it was opened
over the last library with Alt). Each library is checked read-only on a worker thread of its own, never on the UI
thread (the folder and its catalog file are looked at, never locked or written, so a library another program has
open shows as available and Open reports who has it in `libraryProblem.error`). `checking: true` until its check
answers (the row says *Checking…*, then that the drive isn't answering after 5 s); `available` is `false` only
once a check found a `reason`, so a row still checking can be opened. The selection starts on the last library and
moves to the first one that can be opened once every check has answered, unless it was moved meanwhile. Each row is the widget `libraryRecent:<index>`; the buttons are `button:libraryOpen`,
`button:libraryOpenAnother` (the folder dialog of `app.openLibrary`), `button:libraryNew` (`app.newLibrary`),
`button:libraryForget` (Remove from List) and `button:libraryQuit`; `ui.key` ArrowUp / ArrowDown move the selection,
Enter opens it and Escape quits (over an open library: closes the window and keeps it). `app.openLibrary {path}`
opening a library ends it. While it shows, the settings are saved as usual (a removal is kept).
`app.recentLibraries` returns `[{path, name, exists, checking, open, lastOpened}]`. `exists` is what the last
background check found (`false`: moved, on a disconnected drive, not a folder or no longer a library; `null`: not
checked yet, or the check couldn't tell, e.g. a folder that can't be listed, an unreadable catalog or a lookup that
failed) and `checking` whether one
is running: the command never looks at a folder itself. A failed open never marks a library missing (another program
may have it). Recent libraries are checked in the
background when the app starts (the first 8) and when the launch window opens; one not found is looked for again every
30 s (a drive plugged back in), the others only when the window comes back to the front (a timer would wake external
drives and shares). File ▸ Open Recent Library greys out only an entry a check found missing (not there, not a folder,
no library in it); an unreadable catalog, a folder that refuses to be listed, a failed open or a lookup that failed
(a share that timed out) doesn't grey it out. `lastOpened`: ISO 8601, `null` when not known; kept in `ui.json` →
`settings.recentLibrariesOpened`. `{remove: path}` forgets one entry (an error if the list
doesn't hold it) and `{clear: true}` (or `app.clearRecentLibraries`) all of them; nothing on disk is touched.
### Open Library on a Lightroom catalog
@@ -173,12 +209,29 @@ writes nothing. `app.openLibrary` opening a library ends it.
folder (it holds a `*.lrcat`), one of its `*.lrdata` / `*.lrcat-data` folders or `Backups/`, the `.lrcat` itself or
its `-wal` / `-shm` / `.lock` files, it keeps the library open now and returns `{opened: false, lightroomCatalog:
<the .lrcat found, or null>, path}`; the window shows a dialog whose `button:dialogOk` (Import Lightroom Catalog…, or
`ui.dialog.confirm`) runs `file.importLightroom` with that catalog (no catalog found: it asks for one) and
`button:dialogCancel` (`ui.dialog.cancel`) does nothing. A folder that is a LightCraft library (`catalog.snap` / `catalog.log`) always
`ui.dialog.confirm`) runs `file.importLightroom` with that catalog (no catalog found: it asks for one), which then
asks where the catalog goes when `settings.lightroomImport` is `"ask"` (next section), and `button:dialogCancel` (`ui.dialog.cancel`) does nothing. A folder that is a LightCraft library (`catalog.snap` / `catalog.log`) always
opens as one. With no library open (the library problem window) it is an error instead. The engine refuses the same
places (`EngineError::LightroomCatalog`, so `lightcraft-cli --library` and `mcp --library` too). `library.importLightroom
{status: true}` reports the catalog's `path` after the task ends as well as while it runs.
### Import Lightroom Catalog: where the catalog goes
`file.importLightroom {path?, into?, name?, location?}` (File ▸ Import Lightroom Catalog…, issue #2672). With `into`
it acts without asking: `"open"` imports into the library open now (`library.importLightroom`); `"new"` creates the
library `name` (default: the catalog's file name without `.lrcat`, or `Travel 2`… when taken) in the folder
`location` (a full path; default: the open library's parent folder) under `app.newLibrary`'s rules, opens it and
imports there, returning the import's start with `library: {path, name, photos}`. A catalog file that isn't there is
refused before anything is made; so is a new library while an import runs. Without `into`, a library on disk with
`settings.lightroomImport` `"ask"` (the default) opens a window and returns `{asking: true, path, into}` (`into`: the
choice it opens with, `"open"` when the library is empty, else `"new"`). Its widgets:
`radio:lightroomDestination-open`, `radio:lightroomDestination-new`, `field:lightroomLibraryName`,
`field:lightroomLibraryLocation`, `button:lightroomLibraryChoose` (where there is a folder dialog), `check:lightroomDontAsk` (with Merge only; sets
`"alwaysMerge"`), `button:dialogOk` / `ui.dialog.confirm` (which stays open on an error such as a folder that isn't
empty) and `button:dialogCancel`. With `"alwaysMerge"` (`ui.set {"settings": {"lightroomImport": "alwaysMerge"}}`,
or Settings ▸ General `button:settingsLightroomImport-0|1`), or in a session in memory, it imports into the open
library without asking.
## External controllers
External apps can use this channel to send commands and poll the running desktop app for feedback
+1 -1
View File
@@ -6,7 +6,7 @@ zooms to it, so checking whether the subject is sharp takes one key while cullin
## Using it
- **View ▸ Show Focus Point** (⌥F; `view.focusPoint {on?}`) draws the camera's AF frame as green corner brackets in
- **View ▸ Focus Point** (⌥F; `view.focusPoint {on?}`) draws the camera's AF frame as green corner brackets in
Detail, Compare and Survey. It follows Rotate, flips and the crop; a point cropped away is not drawn. Off by
default; the choice is saved with the app's settings.
- **View ▸ Zoom to Focus Point** (`view.zoomToFocus {on?}`, on by default): Toggle Zoom (Z or Space) zooms in
+11 -5
View File
@@ -1,6 +1,6 @@
# Gaps: where LightCraft falls short of Lightroom
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (gap 28: ⌘L Enable Filters switches the filter off and on; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF)
Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and
how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is
@@ -23,11 +23,11 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 6 B | **Compressed ORF** (every Olympus / OM body since ~2008) | measured: 66 of 78 Olympus / OM models preview-only; `orf.rs` refuses; OM bucket 3% of photographers, 3% of them fully work | +2.6 points; best gain per hour of any raw gap (~0.2) | 8–15 | [raw-parity](raw-parity.md#compressed-orf-a-clean-room-starting-point) (the two Olympus patents that describe the method) |
| 7 | **Nikon High Efficiency NEF** (HE / HE★ on Z 8, Z 9, Z 6III, Z f, Z50II, Z5II when set to HE) | named and preview only (#193); needs a pure-Rust JPEG XS decoder | +1.1 points (only shots taken in HE; lossless works on the same bodies); low gain per hour (~0.04), so not a beta blocker | 20–40 | [raw-parity](raw-parity.md) |
| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion, Sony ILCE-7RM4A, full-frame ILCE-7CR distortion (Auto only) and ILCE-7RM6 distortion (`docs/sony-ilce7rm6-lens-corrections.md`) and Nikon Z 6II / Z 8 / Z f distortion (`docs/nikon-lens-corrections.md`) | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) |
| 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) |
| 9 B | **Tests don't run in CI on macOS or Windows**: Linux x64 runs `cargo xtask ci` on every PR and push to main (`.github/workflows/ci.yml`) and FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (ci, freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU; macOS / Windows jobs: #1148 | regressions in `cfg(target_os)` code reach macOS and Windows users | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) |
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 551 of 1,446 Adobe models verified (38%), ~85% of photographers on a verified body; Nikon 5 bodies verified, Canon 34, Leica 14 (the Q3 by a recorded check of 38 private DNGs). Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 regression (#702); F828 SRF now discovered and imported as preview-only, sensor decoding still missing, DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
| 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) |
| 12 | **Content-aware / generative remove, detect distractions, reflections removal** | REM rows 75%; patch synthesis missing | cleanup work goes back to Photoshop | 25–45 (+ generative model) | [target-app-parity](target-app-parity.md#ai) |
| 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data; main-file pages read on demand up to 16 GiB, with dense-table and WAL bounds retained (#798); offline record counts and destination are now shown in a lasting import result | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) |
| 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data; main-file pages read on demand up to 16 GiB, with dense-table and WAL bounds retained (#798); offline record counts and destination are now shown in a lasting import result; before an import the app asks whether the catalog merges into the open library or goes into a new library (opt-out in Settings, #2672) | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) |
| 14 | **HDR display** (EDR / Windows HDR); the headroom limit and SDR rendition from `crs` XMP (the HDR switch, `HDREditMode`, is read). HLG AVIF export done; the display's SDR/HDR decision is tested, blocked on eframe's 8-bit surface | HDR rows 70%; LR-VIEW-HDR-DISPLAY ⬜ | HDR editing works blind on HDR monitors | 12–25 | [hardware-parity](hardware-parity.md) |
| 15 | **Print module**: identity plate / watermark / photo info overlays, Cut Guides for the packages, Custom Package Auto Layout, printer-driver Page Setup and direct printing, JPEG file output (the Print dialog writes a colour-managed PDF) | LRC-PRINT rows | Classic users who print | 15–25 | [target-app-parity](target-app-parity.md#features) |
| 16 | **Map view, Book, Slideshow module, publish services** | LRC-MAP / BOOK / SS rows ⬜ | Classic users; publish services matter for Flickr / SmugMug workflows | 60–100 | [target-app-parity](target-app-parity.md#features) |
@@ -35,7 +35,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 18 | **Video**: playback, trim, global edits, export | VID rows 0% | users who mix stills and clips | 25–45 | [target-app-parity](target-app-parity.md#features) |
| 19 | **Plug-in ecosystem**: no Lightroom SDK equivalent (export / publish plug-ins, metadata plug-ins) | ⬜ | pro workflows built on plug-ins (LR/Enfuse, Jeffrey's exporters, Negative Lab Pro) | 40–80 + owner decision | [target-app-parity](target-app-parity.md#by-dimension) |
| 20 | **Long-tail raw formats**: compressed SRW, ARQ pixel shift, IIQ, Hasselblad compression, X3F, CRW, ERF, KDC, DCR, MEF, GPR, old FinePix (MRW decodes since 2026-10-18) | raw-parity.md | small user share each | 40–80 | [raw-parity](raw-parity.md) |
| 21 | **Localization**: 10 of 16 shipped languages partial (de, es, fr, ja, uk now 97–99% of catalog messages and id every message but What's New, awaiting native review; pt-BR, ru, zh-hans, zh-hant 74–92%), lower-layer errors English; Hindi, Arabic, Korean, Vietnamese absent; no RTL or complex shaping | measured from catalogs | non-English users see mixed English | 40–70 (shipped to full); 135–239 (four new) | [localization-parity](localization-parity.md) |
| 21 | **Localization**: 10 of 16 shipped languages partial (de, es, fr, ja, uk now 97–99% of catalog messages and id every message but What's New, awaiting native review; pt-BR, ru, zh-hans, zh-hant 74–92%), lower-layer errors English; Hindi, Arabic, Korean, Vietnamese absent; no RTL or complex shaping; exports add Thai glyphs only (#2052), with horizontal mark positioning still incomplete and Arabic fallback deferred until joining and bidi exist | measured from catalogs | non-English users see mixed English | 40–70 (shipped to full); 135–239 (four new) | [localization-parity](localization-parity.md) |
| 23 | **Accessibility**: AccessKit tree audited by a test; canvas handles, spots and mask pins by keyboard; left: brush and red-eye by keyboard, a session with a real screen reader | LR-BEHAV-ACCESS 🟡 | blind and low-vision users | 2–4 | [ui-parity](ui-parity.md) |
| 22 | **Pen pressure on macOS / Linux tablets** (brush size and flow follow pressure where winit reports a pen force), OS monitor profiles on Windows (Linux reads colord's, macOS the screen's), follow-window colour on Linux / Windows | hardware-parity.md | brush users with tablets; multi-monitor setups | 11–22 | [hardware-parity](hardware-parity.md) |
| 24 | **Brush live preview and masking feel** (#517, #518); Feather / Edge mask refinement (15.5) | issues | masking feels slower than Lightroom | 8–12 | [ui-parity](ui-parity.md) |
@@ -43,7 +43,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 26 | **Performance at scale**: 85k-photo library opens in 1.7–4.7 s; 100k grid test pending; colour NR at half resolution; at 100k photos stack and album lookups are indexed (an "In a Stack" or "Album" smart album counts in ~14 ms, was 0.6–2.7 s) | ROADMAP log, gpu-pipeline.md | large-catalog users | 15–30 | [target-app-parity](target-app-parity.md#by-dimension) |
| 27 | **Windows / Linux runtime coverage**: the Windows x64 installer is verified on Windows 11 (0.9.1: install and upgrade end on the completion page, a custom folder is kept, Launch works, removal is clean), but uninstalling from Settings → Apps ends with no confirmation (#892, #931); GPU path proven on Apple and on one AMD GPU on Linux (Radeon 8060S, Vulkan); NVIDIA, Intel and DX12 unverified; the Linux riscv64 tarball (new in 0.6.0, #844) is cross-compiled and never run beyond a QEMU smoke test | ROADMAP, issues, `release.yml` | non-Mac users find bugs first | 13–27 | [hardware-parity](hardware-parity.md) |
| 29 | **HDR merge leftovers** (LR-MERGE-HDR): AI Denoise doesn't run on merged (linear) DNGs; JPEG brackets are treated as linear (no camera response curve); every frame is held as 32-bit float, ~0.86 GB per 67 MP frame (half-float frames would halve it) | tested on Sony A7R VI brackets 2026-10-14 ([merge.md](merge.md)); Lightroom's Denoise supports HDR DNGs | HDR shooters who denoise their merges, merge JPEGs, or merge 7+ frames of high-resolution raws on 16 GB machines | 10–20 (+ denoise linear-RGB contract, gap 11) | [merge](merge.md) |
| 28 | **Small Classic shortcuts and keys**: Tab / ⇧Tab (hide the side panels / all panels; Tab moves the keyboard focus here) and F5. Rating `[` `]`, flag cycling (`` ` ``), `\` (Filter Bar), ⌘L (Enable Filters), F1 help, F6 / F7 / F8 (filmstrip, left panel, right panel) are bound | [ui-parity](ui-parity.md#shortcuts) | Classic muscle memory | 2–4 | [ui-parity](ui-parity.md) |
| 28 | **Small Classic shortcuts and keys**: Tab / ⇧Tab (hide the side panels / all panels; Tab moves the keyboard focus here) and F5. Rating `[` `]`, flag cycling (`` ` ``), `\` (Filter Bar), ⌘L (Enable Filters), F1 help, F6 / F7 / F8 (filmstrip, left panel, right panel) are bound; the grid-cycle key is shown beside both grid rows in View (#2500) | [ui-parity](ui-parity.md#shortcuts) | Classic muscle memory | 2–4 | [ui-parity](ui-parity.md) |
| 30 | **Multi-select metadata editing in the UI**: `photo.setMeta` already iterates all selected photos, so the engine applies changes to every target, but the Info panel and All Metadata dialog always show one photo's values with no "mixed" indication when selected photos differ — users cannot tell whether a field is uniform or mixed across the selection before editing | standard in Lightroom Classic's multi-select Info panel | anyone editing metadata on a batch (copyright, keywords, custom fields) | 4–8 | [ui-parity](ui-parity.md) |
Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform work they need around them,
@@ -84,15 +84,21 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| Date | Change | Summary |
|---|---|---|
| 2026-10-23 | minor | Gap 13: before a Lightroom catalog import the app asks whether it merges into the open library or goes into a new one (opt-out in Settings, #2672); hours unchanged |
| 2026-10-22 | patch | Gap 28: View shows the current grid-cycle key beside Photo Grid and Square Grid (#2500); no new binding and no change to the remaining shortcut gap or hours |
| 2026-10-22 | minor | Gap 15: the Print page setup and PDF gain the Japanese photo and postcard paper sizes (L判 89×127, 2L判 127×178, KG判 102×152, はがき 100×148, 往復はがき 148×200, 六切 203×254, 四切 254×305, 名刺 55×91 mm), by their customary names; gap not closed |
| 2026-10-22 | minor | #2052: Thai glyphs added to shared export text; gap 21 retains Thai mark positioning, Arabic joining and bidi limits. No ranked gap closes; estimates unchanged |
| 2026-10-22 | patch | XMP namespace-prefix collisions no longer overwrite foreign metadata; ranked gaps and estimates unchanged |
| 2026-10-22 | patch | Issue #2519: PSD/PSB EXIF resource 1058 and XMP resource 1060 now use the common metadata extractor, and `.psb` files are accepted by import; no ranked gap or readiness count changes |
| 2026-10-22 | minor | Gap 30 added: multi-select metadata editing — Info panel and All Metadata dialog show one photo's values with no mixed-value indication |
| 2026-10-21 | minor | Gap 28 shrinks: F8 shows / hides the right panel (`view.rightPanel`); rating `[` `]`, flag cycling and F1 help were already bound, so the entry lists only filter-bar keys, Tab / ⇧Tab and F5; 2–4 h (was 4–8) |
| 2026-10-20 | minor | #2362: import destination albums use the shared hierarchy/search picker instead of a flat name list; existing import workflows retained; ranked gaps/readiness counts unchanged |
| 2026-10-19 | minor | Gap 9 shrinks: `.github/workflows/ci.yml` runs `cargo xtask ci` on Linux x64 for every PR and push to main; macOS and Windows remain |
| 2026-10-19 | minor | Gap 21: Indonesian (`id`) shipped with every catalog message but What's New; four new languages left (Hindi, Arabic, Korean, Vietnamese), 135–239 h |
| 2026-10-19 | minor | Gap 27: the released 0.9.1 x64 MSI is tested on Windows 11 Pro 24H2 (a fresh install and an upgrade from 0.8.0 end on the completion page, so #790 is fixed; the upgrade keeps a custom folder; Launch starts the app; removal leaves nothing behind); uninstalling from Settings → Apps still ends with no confirmation (#892, #931); hours unchanged |
| 2026-10-18 | patch | Gap 1: six X-Trans CMOS 4 Fujifilm bodies (X-T30 …) take the X-T3's spectral matrices instead of the neutral matrix |
| 2026-10-15 | minor | Gap 15 shrinks: the Page panel's Page Options (Page Numbers, Page Info, Crop Marks) print on Single Image and Contact Sheet pages; identity plate, watermark, photo info, Cut Guides, direct printing and JPEG output remain |
| 2026-10-14 | minor | Gap 4 follow-up: the diagnostic overlays (Visualize Spots, Point Color range, HDR range) render zoom windows too; soft proofing runs on the GPU |
| 2026-10-12 | minor | Gap 10: SR2/SRF folder discovery fixed; F828 imports as preview-only. Sensor decoding remains a gap; readiness scores unchanged |
| 2026-10-23 | minor | Gap 28: ⌘L / Ctrl+L (Library ▸ Enable Filters) puts the filter aside and brings it back as it was; the entry drops ⌘L; hours unchanged |
| 2026-10-15 | minor | Gap 28: `\` shows / hides the Filter Bar in the grids; the entry drops `[` `]` (#509), flag cycling (#1490) and F1 (Help), already on main; ⌘L is the filter-bar key left; hours unchanged |
+15 -1
View File
@@ -4,6 +4,10 @@ The CPU pipeline (`crates/pipeline`) is the reference ("oracle"). `lightcraft-gp
evaluates the same stages with wgpu compute shaders (WGSL) on Metal / Vulkan / DX12. Everything is
pure Rust (wgpu, naga); the drivers are the system's. No GL backend is compiled in.
CPU fast guided-filter allocation measurements and reproduction commands are documented in
[guided-filter-memory.md](guided-filter-memory.md). That optimization retains the full-size base
and does not change the GPU's coefficient buffers.
Warped geometry uses a CPU-computed coverage bit mask for the source boundary.
The CPU reference evaluates that boundary in double precision; GPU float
rounding otherwise can turn a blank edge pixel into a photo pixel. The mask
@@ -101,7 +105,8 @@ or worked around with `LIGHTCRAFT_GPU_BACKEND`.
**The window's renderer and its fallback (Windows).** Before the window opens, each renderer is probed in
order: DX12, Vulkan, GL, then eframe's OpenGL renderer (glow). A probe creates a device and builds egui's
pipeline inside wgpu error scopes, so a driver that cannot compile egui's shaders ("The parameter is
incorrect. (0x80070057)" in `egui_pipeline`) is logged and skipped instead of killing the app. The renderer
incorrect. (0x80070057)" in `egui_pipeline`) is logged and skipped instead of killing the app; no pop-up
unless that renderer was forced (#1624). The renderer
that worked is remembered in the settings folder (`window-renderer`); a start that dies before the window is up
leaves `window-init.marker`, and the next start (relaunched automatically, at most 4 times) tries that
renderer last. `LIGHTCRAFT_GPU_BACKEND=dx12 | vulkan | gl | glow` (or `WGPU_BACKEND`) forces one; DX12 stays
@@ -161,6 +166,8 @@ walks the drive.
(log luminance, dark channel, guided-filter steps, white balance, luminance / colour NR, airlight
sampling), `mask` (linear / radial / luminance / colour range / brush shapes, combine, finalize),
`finish` (the whole per-pixel stage) — each mirrors a named CPU function.
- Soft proofing against an output space (`RenderRequest::proof`, with its destination and display gamut
warnings) runs in `finish`; a proof against an output-device profile (its 3D table) renders on the CPU.
- Per-stage hybrid: defringe and spot removal (rare, CPU-only for now) download the resampled image,
run on the CPU and upload; heuristic mask shapes (Sky, Subject, Background, …) and brushes of more
than 4096 dabs are evaluated on the CPU and uploaded; sources over the buffer limit are resampled
@@ -260,6 +267,13 @@ Remaining differences come from f32 vs f64 coordinate math, fast-math transcende
Metal and summation order in the box filters — all far below one 8-bit step.
## Measurements
**In the app:** Help ▸ Debug ▸ Frame Rate (`view.frameRate`, `{show?}`; `LIGHTCRAFT_FPS=1`
starts with it on, never saved) draws frames per second, each frame's UI CPU time against its
time spent waiting (for vsync, or for the GPU), the latest whole-frame and zoom-window render
times, the render queue, renders thrown away as superseded, the GPU adapter and its memory, and a
graph of the last frames with a tick where a render arrived. Once a second it logs the same
numbers (`fps-hud:`), so a scripted drag (`ui.dragWidget`) can be measured from the log.
`render_bench` on `corpus/raw/arw-sony-a7m3-compressed.arw` (24 MP; loupe = 2560 px preview →
1920×1280), min of 7 runs, wall clock, on a heavily shared machine (load average 60–110 on 14 cores,
so CPU numbers are pessimistic; the GPU numbers are less affected):
+53
View File
@@ -0,0 +1,53 @@
# CPU fast guided-filter memory
`local::guided_fast` computes blurred coefficients on the same reduced grid as before, then
samples them while applying `a * p + b`. It keeps the raster resizer's weights, boundary
normalization and horizontal-then-vertical accumulation order. Odd dimensions therefore use
their actual reduced/full dimension ratios. Four coefficient row pairs and one bias row are cached
per active 32-row band; adjacent output rows reuse their horizontal samples.
The two full-resolution coefficient planes and their resize intermediates are gone. The returned
full-resolution base remains: downstream CPU tone mapping and the spatial-plane cache need it.
The step-1 filter, Box reduction, Gaussian coefficients, V2's median input, process selection and
Draft sigma cap are unchanged. The GPU implementation is unchanged; this reduces CPU memory only.
## Reproduce
The dev-only example includes the previous materialized algorithm and uses the workspace's
existing `dhat` allocator. No allocator or profiling dependency is added to product builds.
```sh
cargo build --release -p lightcraft-pipeline --example guided_memory
RAYON_NUM_THREADS=1 target/release/examples/guided_memory old 6000 4000 120 15
RAYON_NUM_THREADS=1 target/release/examples/guided_memory new 6000 4000 120 15
RAYON_NUM_THREADS=1 target/release/examples/guided_memory old 6000 4000 120 1 --heap
RAYON_NUM_THREADS=1 target/release/examples/guided_memory new 6000 4000 120 1 --heap
```
If `CARGO_TARGET_DIR` is set, use that directory instead of `target` for the executable.
Arguments are mode, width, height, integer sigma and timing iterations. The example warms up
before measurement; `--heap` counts one filter call with the source allocated beforehand.
Time the runs without `--heap`: allocation profiling significantly changes timings. It reports
the minimum wall time; on a shared machine repeat when other builds/renders have stopped.
Use `LIGHTCRAFT_PROFILE=1` and `cargo xtask bench` for broader pipeline measurements.
## Observed allocation and time
Measured 2026-10-16 UTC, macOS aarch64, Rust 1.96.0, release build, one Rayon thread. Bytes are
requested heap bytes counted by dhat, not process RSS. Timings are minimums of 15 unprofiled
calls on a shared machine with concurrent builds; they are indicative, not a speed guarantee.
| Dimensions / sigma / step | Old peak bytes | New peak bytes | Old total allocated bytes | New total allocated bytes | Old / new wall ms |
|---|---:|---:|---:|---:|---:|
| 6000 × 4000 / 120 / 16 | 289,125,000 | 97,780,856 | 313,083,488 | 135,643,856 | 42.135 / 45.131 |
| 1601 × 1067 / 24 / 8 | 20,822,412 | 7,331,172 | 25,102,604 | 11,562,560 | 3.359 / 3.121 |
The 24 MP result saves about 191 MB of peak allocation (66%). Its **96,000,000-byte output is
included**; its **96,000,000-byte input is excluded**. Application RGB images, log luminance,
V2 median input and its construction temporaries, other spatial planes and caches also remain
outside this isolated measurement. More Rayon threads can hold more band scratch concurrently.
This is not a bound on total pipeline memory.
Regression tests compare the sampled result exactly against materialized bilinear resizes for
empty/singleton/one-dimensional, odd/even and multiple-band images; flat fields, signed texture,
edges and impulses; small/full and reduced filters; and median tone bases.
+37 -3
View File
@@ -6,7 +6,41 @@ and a C SQLite runtime are not required. The source database is read-only, inclu
File → Open Library… given a Lightroom catalog folder (or an `.lrdata` / `.lrcat-data` folder, or the `.lrcat`)
doesn't make a LightCraft library there: it offers this import instead, with the catalog found
(`crates/engine/src/lightroom_folder.rs`, issue #891). `lightcraft-cli --library` refuses those places too.
(`crates/engine/src/lightroom_folder.rs`, issue #891), and then asks where the catalog goes as described next
(unless the question is turned off, below).
`lightcraft-cli --library` refuses those places too.
**Where the catalog goes.** Before the import starts, the desktop app asks where the catalog goes (issue #2672),
unless Settings says to always merge (*Turning the question off*, below):
- **Merge into the open library "<name>" (N photos)**: the catalog's photos, collections and edits join the ones
already in the library open now (the library's name is its folder's name).
- **Create a new library**: named after the catalog file (`Travel.lrcat` → "Travel", or "Travel 2" when a
"Travel" folder is already there) and placed next to the open library (in its parent folder); both can be
changed (Choose… picks the folder). LightCraft creates and opens it as File → New Library… does (the folder
must be new or empty; it is refused while another import runs), then imports the catalog into it. The library
left behind stays in File → Open Recent Library.
- **Cancel** does nothing.
The window opens on *Merge* when the open library has no photos (there is nothing to merge with) and on *Create a
new library* when it has some.
**Turning the question off.** Settings → General → Library → *When importing a Lightroom catalog*: *Ask where to
import it* (the default) or *Always merge into the open library* (how imports always worked before). The window's
*Don't ask again (always merge into the open library)* checkbox, offered with *Merge*, picks the second; the window
says where to turn the question back on. The choice is an app setting (`ui.json`, `settings.lightroomImport`:
`"ask"` or `"alwaysMerge"`); a settings file without it asks.
A session with no library on disk (in memory, e.g. after Continue Without Saving) has nothing to put a new library
next to: the catalog merges into it without asking, as before.
**Scripts and agents.** The app command `file.importLightroom {path?, into?, name?, location?}` (control channel,
`lightcraft-cli mcp --connect`) chooses without the window: `into: "open"` merges, `into: "new"` creates the library
`name` (default: the catalog's name) in the folder `location` (default: the open library's parent folder; a full
path), opens it and imports. Without `into` it shows the window (and returns `{asking: true, path, into}`, `into`
being the choice the window opens with) or merges when the setting says so. A catalog path that doesn't exist is
refused before a new library is made. The engine command `library.importLightroom` always imports into the library
open now.
Equivalent commands:
@@ -15,8 +49,8 @@ lightcraft-cli run --library "LightCraft Library" library.inspectLightroom path=
lightcraft-cli run --library "LightCraft Library" library.importLightroom path="Lightroom Catalog.lrcat"
```
Import adds records to the currently open LightCraft library; it does not create a separate
folder named after the Lightroom catalog. The desktop result stays open until dismissed and
The import itself (`library.importLightroom`, `lightcraft-cli run`) adds records to the currently open LightCraft
library; only the desktop window above can make a new library for it first. The desktop result stays open until dismissed and
shows the destination library, the number of records added, missing originals and warnings.
**Show in Library** opens All Photos, clears filters and selects an imported record. Errors stay
readable in the same result window. If another dialog is open, the result waits until that dialog closes. The result
+7 -5
View File
@@ -6,7 +6,7 @@ How far each interface language is from `full`. How to add or maintain a languag
[localization.md](localization.md); per-language notes are in `localization-<code>.md`
([zh-hans](localization-zh-hans.md), [zh-hant](localization-zh-hant.md), [ja](localization-ja.md),
[pt-br](localization-pt-br.md), [es](localization-es.md), [de](localization-de.md),
[ru](localization-ru.md), [fi](localization-fi.md), [ka](localization-ka.md), [id](localization-id.md), [vi](localization-vi.md)).
[ru](localization-ru.md), [fi](localization-fi.md), [ka](localization-ka.md), [id](localization-id.md), [vi](localization-vi.md), [pl](localization-pl.md)).
## The target
@@ -62,7 +62,7 @@ string follows that.
| 11 | Korean | `ko` | 0 (0%) | none | **no Korean (Hangul) face**: needs a non-Adobe OFL face in craft-fonts (Noto CJK is allowed only for Chinese) | no | none | 14–22 h |
| 12 | Vietnamese | `vi` | 2,471 / 2,471 (100% key coverage of current union; intentional English like `ISO`/`OK` kept) | menus, panels, dialogs, presets, profiles, dates, formats; lower-layer errors English | Latin with stacked diacritics (Inter) | no (community contribution) | partial (catalog complete; lower-layer errors English) | 1–3 h |
## Other languages LightCraft ships (4)
## Other languages LightCraft ships (7)
| Language | Code | UI strings translated | Script | Native review | Status | To `full` |
|---|---|---:|---|---|---|---:|
@@ -72,8 +72,9 @@ string follows that.
| Italian | `it` | 2,628 / 3,278 (80%, measured 2026-10-15; every interface string is translated, the other 650 are What's New lines, left in English) | Latin (Inter) | partly (written with an agent; corrections and the 2026-10-15 additions by a native speaker) | partial (lower-layer errors English) | 3–5 h |
| Georgian | `ka` | 2,398 / 2,398 (100%: every plain and formats message any catalog has on 2026-10-12, plus the 27 display labels and 397 What's New lines no other catalog translates) | Geor: no craft-fonts face yet (boxes until one is available) | yes (native speaker) | partial (lower-layer errors English) | 2–4 h (+ a Georgian face) |
| Finnish | `fi` | 2,610 / 3,308 (79%, measured 2026-10-17: 2,313 plain + 297 formats; the rest is What's New) | Latin (Inter) | yes (written with an agent; native-speaker review by @roopeg) | partial (lower-layer errors English) | 3–5 h |
| Polish | `pl` | 2,159 messages + 291 formats; every format message translated (2026-10-15) | Latin (Inter) | no (AI-drafted; follow-up by a native speaker) | partial (lower-layer errors English) | 2–4 h (full native review) |
Target languages LightCraft lacks: Korean, Dutch, Polish, Swedish, Norwegian Bokmål, Thai
Target languages LightCraft lacks: Korean, Dutch, Norwegian Bokmål, Thai
(Thai needs shaping and line breaking without spaces). Each Latin-script one is ~10–16 h; Thai
~20–35 h on top of the shaping work.
@@ -90,9 +91,9 @@ Target languages LightCraft lacks: Korean, Dutch, Polish, Swedish, Norwegian Bok
## Totals
- Bring the 10 shipped non-English catalogs to `full`: ~55–90 h (incl. the shared work).
- Bring the shipped non-English catalogs to `full` (17 on 2026-10-23; the estimate was made for 10): ~55–90 h (incl. the shared work).
- Add the 5 missing key languages: ~145–255 h, most of it RTL and complex shaping in egui.
- Reach the target's 16 languages: + ~50–85 h (Dutch, Polish, Swedish, Norwegian, Thai).
- Reach the target's 16 languages: + ~30–55 h (Dutch, Norwegian, Thai; Polish and Swedish ship).
## Revision history
@@ -100,6 +101,7 @@ Target languages LightCraft lacks: Korean, Dutch, Polish, Swedish, Norwegian Bok
|---|---|---|
| 2026-10-19 | minor | Indonesian interface catalog (`id`): every plain and formats message any catalog has, plus 69 command labels; What's New left in English |
| 2026-10-17 | minor | Finnish interface catalog (`fi`): 2,610 of 3,308 messages (2,313 plain + 297 formats), What's New left in English |
| 2026-10-15 | minor | Polish row added: the format messages still in English translated, counts made right for every number, dates day.month.year ([localization-pl.md](localization-pl.md)) |
| 2026-10-15 | trivial | Italian (`it`): 339 interface messages and 60 format strings that were missing or still in English translated (2,628 of 3,278; only What's New left in English); native-speaker corrections: Exposure → *Esposizione*, Sharpening → *Nitidezza*, Preset → *Preset*, blurry → *sfocate* |
| 2026-10-13 | minor | Remeasured denominator (2,222 plain + 249 formats = 2,471); Vietnamese catalog complete at 2,471/2,471 (status partial: lower-layer errors still English) |
| 2026-10-12 | minor | Italian interface catalog (`it`): 2,201 of 2,398 messages, What's New left in English |
+56
View File
@@ -0,0 +1,56 @@
# Polski
Język polski (`pl`, English name **Polish**, nazwa natywna **Polski**) włącza się przez **Edytuj → Język →
Polski** albo **Ustawienia → Ogólne → Język**. Zmiana działa od razu i jest zapamiętywana przy kolejnym
uruchomieniu. Polecenie to `app.language.polish`; kanał sterowania przyjmuje `ui.set {"language":"pl"}`
oraz znaczniki `pl-PL` i `pl_PL.UTF-8`. Bez okna można ustawić `LIGHTCRAFT_LANGUAGE=pl`.
Katalogi `crates/ui-egui/locales/pl.json` i `pl-formats.json` dodał #1100 (tłumaczenie przygotowane
z pomocą AI). Uzupełnienie opisane niżej również przygotowano z pomocą AI; przygotował je rodzimy
użytkownik języka polskiego (@lewandowskit). Profesjonalny tłumacz nie robił jeszcze korekty.
## Konwencje
- Menu: Plik, Edytuj, Widok, Zdjęcie, Okno, Pomoc.
- Library = Biblioteka, Develop = Wywoływanie, Preset = Ustawienie wstępne, Stack = Stos,
Masking = Maskowanie, Virtual Copy = Kopia wirtualna, Filmstrip = Pasek zdjęć.
- Clarity = Przejrzystość, Dehaze = Usuń zamglenie, Vibrance = Dynamika, Saturation = Nasycenie.
- Highlights = Światła, Shadows = Cienie, Whites = Biele, Blacks = Czernie.
- Cudzysłowy polskie: „…”. Ścieżki menu w tekstach: `Edytuj → Cofnij`.
- Data ma postać `20.09.2026` (dzień.miesiąc.rok), nagłówek dnia `Niedziela, 20.09.2026`,
miesiąca `09.2026`; czas 24-godzinny (`16:04:05`).
- **Liczby i rzeczowniki.** Polski ma trzy formy liczby mnogiej (1 zdjęcie, 2 zdjęcia, 5 zdjęć),
a mechanizm katalogów rozróżnia tylko „jeden” i „wiele”. Dlatego liczba stoi po rzeczowniku:
„Zaimportowane zdjęcia: 22”, „Usuń zdjęcia (3)”, „Oczekujące zdjęcia: 2”. Taka forma jest
poprawna dla każdej liczby. Angielska końcówka liczby mnogiej jest pochłaniana przez `{:.0}`
(jak w [localization.md](localization.md)). Po przyimku z dopełniaczem („z 10 zdjęć”, „dla 3
zaznaczonych zdjęć”) liczba może stać przed rzeczownikiem, bo forma jest wtedy zawsze ta sama.
- Komunikaty `{n} {noun}` (np. w panelu Odszumianie AI) mają postać `{noun}: {n}`, a obie formy
rzeczownika (`photo waiting` / `photos waiting`) tłumaczą się tak samo.
- Font Inter dołączony do aplikacji zawiera wszystkie polskie litery i cudzysłowy; inny font
nie jest potrzebny.
## Uzupełnienie (2026-10-15)
- Przetłumaczono 40 komunikatów `pl-formats.json`, które były jeszcze po angielsku (ustawienia
efektów masek, szablony pokazu slajdów, import katalogu Lightroom, słowa kluczowe, osoby,
scalanie panoram, komunikat o nieoczekiwanym zakończeniu programu) i 2, w których zmieniono
tylko strzałkę.
- Poprawiono 25 komunikatów, w których liczba stała przed rzeczownikiem w dopełniaczu i dla
2–4, 22–24… dawała błędną formę („Usuń 2 zdjęć”), oraz `{} failed`, gdzie `{}` jest nazwą
zadania, a nie liczbą.
- Daty: `{month} {year}` i `{weekday}, {day} {month} {year}` mają postać dzień.miesiąc.rok
zamiast „20 9 2026”.
Katalog ma 2 159 zwykłych komunikatów i 291 szablonów formatów. Komunikaty, których nie ma w
katalogu, oraz techniczne błędy niższych warstw wyświetlają się po angielsku.
## Weryfikacja
Test `i18n::tests::polish_switches_formats_dates_and_reads_every_count` sprawdza przełączanie
z menu i kanału sterowania, zapamiętanie języka, znaczniki `pl-*`, daty, czas, liczby 0–111
(w tym 2–4 i 22–25) w liczonych komunikatach, nazwy użytkownika pozostawione bez zmian oraz
polskie znaki w fontach interfejsu. Zgodność placeholderów sprawdza
`catalogs_agree_on_placeholders_and_report_gaps`.
Postęp dodawania języków opisuje [localization.md](localization.md).
+2 -2
View File
@@ -2,11 +2,11 @@
The interface ships in the language it is written in (English) plus every language in the table in
`crates/ui-egui/src/i18n.rs`. Today that is English, Simplified Chinese (`zh-hans`), Traditional
Chinese (`zh-hant`, Taiwan), Japanese (`ja`), Brazilian Portuguese (`pt-br`), Spanish (`es`), German (`de`), Russian (`ru`), French (`fr`), Ukrainian (`uk`), Czech (`cs`), Georgian (`ka`), Swedish (`sv`), Finnish (`fi`), Indonesian (`id`) and Vietnamese (`vi`). This file is the reference for **adding or maintaining a language**; the per-language notes
Chinese (`zh-hant`, Taiwan), Japanese (`ja`), Brazilian Portuguese (`pt-br`), Spanish (`es`), German (`de`), Russian (`ru`), French (`fr`), Ukrainian (`uk`), Czech (`cs`), Italian (`it`), Georgian (`ka`), Swedish (`sv`), Finnish (`fi`), Indonesian (`id`), Vietnamese (`vi`) and Polish (`pl`). This file is the reference for **adding or maintaining a language**; the per-language notes
are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-hant.md`](localization-zh-hant.md),
[`localization-ja.md`](localization-ja.md), [`localization-pt-br.md`](localization-pt-br.md), [`localization-es.md`](localization-es.md),
[`localization-de.md`](localization-de.md), [`localization-ru.md`](localization-ru.md),
[`localization-cs.md`](localization-cs.md), [`localization-fi.md`](localization-fi.md), [`localization-ka.md`](localization-ka.md), [`localization-id.md`](localization-id.md) and [`localization-vi.md`](localization-vi.md).
[`localization-cs.md`](localization-cs.md), [`localization-fi.md`](localization-fi.md), [`localization-ka.md`](localization-ka.md), [`localization-id.md`](localization-id.md), [`localization-vi.md`](localization-vi.md) and [`localization-pl.md`](localization-pl.md).
## Adding a language
+1 -1
View File
@@ -170,7 +170,7 @@ per-request `_meta` receive `resultType: complete` and list/read cache hints; re
|---|---|
| `list_commands {filter?}` | Every command: id, label, menu, shortcut, parameter doc, enabled now |
| `run_command {command, params?}` | Run any command by id |
| `import {paths, album?, mode?, destination?, organize?, rename?}` | Import files/folders (folders are scanned recursively); the first new photo becomes active. `mode: "copy"` copies into `destination` (default: the library's Originals/), `mode: "move"` moves there (each original and its XMP sidecars are removed from the source only after the copy is verified and catalogued; duplicates and failures keep their sources; the result's `moved` / `kept` list what moved and what stayed, and why; undo leaves the moved files at the destination), filed by `organize`: `date` (YYYY/YYYY-MM-DD), `month`, `flat` or a folder template such as `{date:%Y}/{date:%Y%m%d}` (→ `2026/20260114`; capture date, else the import date; always inside the destination), and named by the `rename` template (`run_command photo.renameTokens` lists the tags) |
| `import {paths, album?, mode?, destination?, organize?, rename?}` | Import files/folders (folders are scanned recursively); the first new photo becomes active. `mode: "copy"` copies into `destination` (default: the library's Originals/), `mode: "move"` moves there (each original and its XMP sidecars are removed from the source only after the copy is verified and catalogued; duplicates and failures keep their sources; the result's `moved` / `kept` list what moved and what stayed, and why; undo leaves the moved files at the destination), filed by `organize`: `date` (YYYY/YYYY-MM-DD), `month`, `flat` or a folder template such as `{date:%Y}/{date:%Y%m%d}` (→ `2026/20260114`) or `{date:%Y}/{date:%B}` (→ `2026/January`; capture date, else the import date; always inside the destination), and named by the `rename` template (`run_command photo.renameTokens` lists the tags) |
| `query_photos {filter?, sort?, offset?, limit?}` | Photos in the current view (or matching a catalog `Filter`) |
| `select_photos {ids, active?, mode?}` | Set the selection / active photo. Every id (and `active`) must be in the library: an unknown id is a tool error (`no such photo 9999`) and the selection and active photo are left as they were |
| `list_controls {section?}` | Every develop slider: id (`light.exposure`…), range, default, current value |
+2 -1
View File
@@ -8,7 +8,8 @@ All algorithms are our own implementations from published papers (cited in the m
- **App:** select the photos, then *Photo Merge ▸ HDR…* (`Ctrl+H`), *Panorama…* (`Ctrl+M`) or
*HDR Panorama…* from the photo context menu (or the `dialog.merge*` commands). The dialog previews
the merge at ≤ 1024 px and re-runs the preview when an option changes; *Merge* runs the full merge
the merge at ≤ 1024 px and re-runs the preview when an option changes; as in Lightroom, a click on the preview
zooms in on that point (drag to pan, click again to fit), and it stays there while options change; *Merge* runs the full merge
in the background (a row in the activity stack shows its progress, with ✕ to stop it). The result is written next to the first photo as
`<name>-HDR.dng`, `-Pano.dng` or `-HDR-Pano.dng`, imported and selected. *Photo Merge ▸ HDR for Each Bracket Set*
merges many brackets in one go with the last HDR options: one HDR per stack among the selected photos, then per
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -158,7 +158,7 @@ essentials replaces its guessed 45% raw look with 84% × 0.75 (+1 point).
| Discount | Mainstream | Essentials | Evidence |
|---|---:|---:|---|
| Interaction fidelity / UI clarity | ×0.95 | ×0.95 | brush live preview gaps and feathering (#517, #518); crop artefacts and limits (#731, #742, #770, #771); Compare white-balance selector (#743–#745); low-res slider previews (#330); dialogs taller than the window (#781); mouse-wheel scroll (#714) |
| Stability on real machines | ×0.92 | ×0.92 | launch and quit crashes or silent startup failures (#250 macOS 12 Intel, #315 Windows Intel GPU, #431 Intel Mac, #260, #462, #783 Linux / Nouveau, #790 Windows installer, fixed in 0.9.1); tests run in CI on FreeBSD only (gap 9) |
| Stability on real machines | ×0.92 | ×0.92 | launch and quit crashes or silent startup failures (#250 macOS 12 Intel, #315 Windows Intel GPU, #431 Intel Mac, #260, #462, #783 Linux / Nouveau, #790 Windows installer, fixed in 0.9.1); tests run in CI on Linux x64 and FreeBSD only (gap 9) |
| File exchange with Lightroom users (mainstream) / opening files people send (essentials) | ×0.92 | ×0.95 | Adobe XMP sidecars change colours (#290), XMP CameraProfile and Upright ignored (#204, #205), catalog import fails (#599), same Light values render differently (#523, #209), no write-back to `.lrcat`; for casual users: CR3 beyond verified bodies, compressed ORF, HE NEF open as previews (#473, #485, #556), HEIC trouble (#693) |
| **Product** | **×0.804** | **×0.830** | |
@@ -300,6 +300,7 @@ P2 items 12–25 h; look tuning, performance, packaging, hardening 25–45 h.
| Date | Change | Summary |
|---|---|---|
| 2026-10-19 | patch | Stability note: tests run in CI on Linux x64 (every PR) as well as FreeBSD; macOS and Windows remain (gap 9) |
| 2026-10-19 | minor | Localization ~55% → ~64% (mean of the 12 key languages): Indonesian (`id`) catalog, every message but What's New; full target 61.2% → 61.5% (still ~61%) |
| 2026-10-19 | patch | Platforms: the Windows installer is no longer listed as unverified (the 0.9.1 x64 MSI is tested on Windows 11; a confirmation after uninstalling from Settings → Apps, #892, remains); #790 noted as fixed in 0.9.1; percentages and hours unchanged |
| 2026-10-14 | minor | Localization ~52% → ~55% (mean of the 12 key languages): de, es, fr, ja and uk catalogs filled |
+2 -1
View File
@@ -27,7 +27,7 @@ assessment and the notes that don't fit a row. Visual reference for agents: `pla
| Crop, Upright, transform handles | handles, aspect lock, angle, overlays (O / ⇧O), guided Upright | ✅ all of them; overlay cycle on ⇧O | 85% | crop opacity of cut-out area (15.5) |
| Masking tools | brush (A / B, flow, density, auto mask, pressure), linear / radial, range masks, AI masks, pins | brush, linear, radial, colour / luminance range, add / subtract / intersect / invert, masks panel, pins | 60% | brush live preview (#517 / #518), no pen pressure, no depth range, AI masks, Feather / Edge controls (15.5) |
| Remove / heal | content-aware remove, heal, clone, visualize spots | heal, clone, auto source, visualize spots, red / pet eye | 65% | content-aware (patch synthesis) remove, generative remove, detect distractions |
| Zoom and navigation | Fit / Fill / 1:1 / 2:1 … 11:1, Navigator, spacebar pan | Fit to 800%, pinch zoom, pan, Navigator-like | 60% | diagnostic overlays (Visualize Spots, Point Color range, HDR range) render the whole frame instead of a zoom window; everything else, mask overlays and soft proofing included, is sharp at 1:1–8:1 |
| Zoom and navigation | Fit / Fill / 1:1 / 2:1 … 11:1, Navigator, spacebar pan | Fit to 800%, pinch zoom, pan, Navigator-like; every view, mask and diagnostic overlays (Visualize Spots, Point Color range, HDR range) and soft proofing included, is sharp at 1:1–8:1 | 60% | zoom stops at 800 % (Adobe's reference goes to 11:1) |
| Library culling and metadata | ratings, flags, labels with keys, painter, keywording, metadata panel, filter bar | ratings / flags / labels and advance, keyword list and sets, keywording box, metadata fields, smart albums, filter bar | 85% | painter tool, quick collection, some Classic keys |
| Shortcuts | Lightroom desktop and Classic keymaps | Lightroom-desktop keymap with Classic additions, editable | 85% | deliberate differences below; no Classic keymap layer |
| Context menus | everywhere | 31 sites (grid, filmstrip, albums, folders, keywords, masks, presets, text fields…), all through `widgets::context_menu` (`crates/ui-egui/src/widgets.rs`): a right-click elsewhere while a menu is open closes it and opens the menu there, also on the same widget (issue #1877: that one flashed at the new spot and closed, as egui counts the opening click as a click while open) | 70% | unaudited against Lightroom's menus item by item |
@@ -151,6 +151,7 @@ establish OS keyboard delivery or cross-platform acceptance.
| 2026-10-15 | patch | Context menus: a right-click elsewhere on the same widget while its menu is open moves the menu to the pointer; before it flashed at the new spot and closed (issue #1877); every site uses `widgets::context_menu`; 70% unchanged |
| 2026-10-23 | patch | KEYC-RATING: ⌘L / Ctrl+L (Library ▸ Enable Filters) switches the filter off and on, restoring it as it was |
| 2026-10-15 | patch | KEYC-RATING: `\` shows / hides the Filter Bar in Photo Grid and Square Grid (Show Original in Detail); ⌘L is the filter-bar key left |
| 2026-10-14 | minor | Zoom row: diagnostic overlays render zoom windows too |
| 2026-10-14 | patch | Shortcuts: Visualize HDR loses its default ⇧H, which cycles the Tool Overlay (issue #224); both PRs had claimed it |
| 2026-10-12 | minor | Accessibility: crop handles, spots, gradient handles and mask pins operable by keyboard with AccessKit names and values; Tab order test for the right panels; hand-drawn icons and rows named; 55% → 75%; UI remaining 34–58 h → 32–54 h |
| 2026-10-13 | minor | Shortcuts: Tool Overlay modes for spot and mask pins on ⇧H (H stays Remove; issue #224), listed with the deliberate differences; dropped from the missing list |
+3 -3
View File
@@ -9,12 +9,12 @@ Double-click a photo in the grid, then press `E` to open the **Edit** panel. Dra
## Steps
1. In **Photo Grid** or **Square Grid**, double-click the photo. It opens in the **Detail** view.
2. Press `E`, or choose **Window** > **Edit**, to open the **Edit** panel.
1. In **Photo Grid** or **Square Grid**, double-click the photo. It opens in the **Detail View**.
2. Press `E`, or choose **Window** > **Edit Panel**, to open the **Edit** panel.
3. Click **Auto** for a first pass, or skip this step and start with the sliders.
4. Open the **Light** section and set **Exposure**, **Contrast**, **Highlights**, **Shadows**, **Whites** and **Blacks**.
5. Open the **Color** section to change **Vibrance** and **Saturation**. Open **Effects** for **Texture**, **Clarity** and **Dehaze**. The panel also has **Detail** and **Optics** sections.
6. Check your work against the original. Hold `\` to peek at the original. Press `Y` (**View** > **Compare Before and After**) to see before and after side by side.
6. Check your work against the original. Hold `\` to peek at the original. Press `Y` (**View** > **Before/After Side by Side**) to see before and after side by side.
7. To go back a step, choose **Edit** > **Undo**. To drop all edits on the photo, click **Reset** at the top of the **Edit** panel.
## Black and white
+8 -1
View File
@@ -158,7 +158,14 @@ are the same in every version, so serve them with `Cache-Control: no-cache` (not
`write` service hands each file to the browser as a download instead of writing it to disk.
- **Automation.** `await lightcraft.command("library.info", "{}")` runs any engine or UI command
by id on the next frame and resolves to the JSON result (`web.stats` reports storage, workers
and the render queue). This is how the headless-Chrome checks drive the page.
and the render queue). The params are a JSON object; empty text and JSON `null` mean `{}`
(no params: a command that takes photo ids then acts on the selection). Any other text that
isn't an object rejects the promise and runs nothing, as the control channel and MCP do, so a
typo can't run a command on the selection. This is how the headless-Chrome checks drive the page.
- **The page title names the open photo**: `<file name> — LightCraft`, with ` •` after the name while
changes are not yet written to browser storage (`LightcraftApp::page_title`). eframe's web runner
ignores the window title the desktop app sets, and a host that embeds the page reads this title to
tell which photo a drop opened and whether it is saved (issue #2397).
## Not yet
+120 -2
View File
@@ -31,6 +31,38 @@
stay available in All Metadata; PSD edit copies keep their capture date. Original exports with Remove Location Info enabled
are refused when a PSD or PSB embeds GPS, since LightCraft cannot strip metadata from a byte-for-byte original copy.
### Choose where a Lightroom catalog goes
- File ▸ Import Lightroom Catalog… now asks first: merge the catalog's photos, collections and edits into the
library open now, or create a new library for them, named after the catalog and placed next to the open library
(both can be changed). LightCraft opens the new library, then imports the catalog into it. Cancel does nothing.
Open Library… on a Lightroom catalog goes through the same choice.
- To import as before without the question, choose Settings ▸ General ▸ When importing a Lightroom catalog ▸
Always merge into the open library, or tick Don't ask again in the window. Ask where to import it brings the
question back.
### Choose which library opens at launch
- Settings ▸ General ▸ **At launch**: *Open the last library* (as before) or *Ask me*. With *Ask me*, LightCraft
starts with a window listing your recent libraries: each one's name, the folder it is in and when you last opened
it. One that is gone (moved, renamed, on a disconnected drive), can't be read or no longer holds a library is
greyed out with the reason. Each one is checked in the background, showing *Checking…* until it answers (and that
the drive isn't answering after 5 s), so the list never waits on a network share that is offline or a disk that
has to wake up; File ▸ Open Recent Library uses the same answers (it greys out only libraries that are gone). Making the list only looks: it never locks or writes anything in your libraries, so one
that another program has open is reported, with who has it, when you press **Open**, and the window stays. The
last library is selected; **Open** (or Return, or a double-click) opens it,
**Open Another…** asks for a folder, **New Library…** makes one, **Quit** (or Esc) quits. ↑ and ↓ move through
the list.
- Holding Alt (Option on a Mac) as the window appears shows the same list once, over the library that opened: open
another one from it, or press Esc to keep working where you were. On a Mac, an Option key held from before
LightCraft starts is only noticed once it changes, so press it as the window appears.
- `--library` and `LIGHTCRAFT_LIBRARY` still decide the library when given, and `lightcraft-cli` never asks.
- File ▸ Open Recent Library ▸ **Remove from List** forgets one library, and the launch window's **Remove from
List** does the same. Only the entry goes: the library and its photos stay where they are. Agents use
`app.recentLibraries {remove: path}`; its list now also says when each library was last opened (`lastOpened`).
- Photos dropped on the window while the list (or the window of a library that couldn't open) is up wait for the
library you open and are imported into it, as files given on the command line are; presets dropped then are
refused with a note, since they belong to a library.
## 2026-10-22
### Reliability
@@ -46,8 +78,66 @@
key could still run the menu command instead of reaching the field or the editor. The keys come back on the menu
items when the field loses focus or recording ends; the menu's order, checkmarks and enabled state are unchanged.
### Find the grid shortcut in View
- Photo Grid and Square Grid now show the grid-cycle shortcut (G by default) in the View menu. The hints follow
shortcut changes; clicking a menu row still selects that grid (issue #2500).
## 2026-10-21
### Settings window
- Settings is one size on every tab and stays where it is as you go through them; before, each tab made its own
size and the tabs moved under the pointer. The tabs stay in place while a long page scrolls (in a very short
LightCraft window, where the page has too little room to scroll by itself, the tabs and the page scroll together), a
page that goes on below shows a scroll handle without the pointer on it, and each page remembers where it was
scrolled to, also when Settings is closed and opened again.
- Nothing in Settings runs off the side any more. A row with more choices than fit (the languages on General) wraps
onto further lines and a long label wraps in its column, where both used to widen the window: in a small
LightCraft window the labels and the Close button of General ended up off screen.
- The Slideshow tab is one column like the other tabs, with the slide preview first, instead of a column beside the
preview that made Settings much wider on that tab. The preview stays above the page while it scrolls, so pointing
at a template further down still shows it there (in a very short window it scrolls with the page instead). Pointing
at a template no longer pushes its rows out from under the pointer, and the Export buttons wrap.
- A long folder (General ▸ Library ▸ Location, the folder Auto Import watches, the smart previews folder, a monitor
profile's file) is shortened to its last folders ("…/Pictures/Library") on one line; rest the pointer on it for
the whole path.
### One star rating control
- The stars in the bottom bar, the Info panel and the filter bar are now one shared control, and the badges on grid
cells, filmstrip thumbnails and the Compare caption use the same drawing. Hovering a star in the filter bar now
previews the rating like the others do, and with a star focused (Tab), ← and → lower and raise the rating.
### Progress looks the same everywhere
- Download, scan, denoise and activity progress now use one bar that fills the width it is given, with its text on
it, and a gentle sweep while the amount of work isn't known yet. Waits that only said "Selecting…", "Waiting to
start…" or "Installing…" now show a small turning arc beside the text, and a thumbnail still being made shows a
quiet placeholder that fits the theme in the grid, the filmstrip and Compare. Screen readers hear a progress
indicator with its percentage.
### Menu names that say what they are
- Switches in the menus are named for what they show, with the check mark saying whether it is on: Filmstrip ✓,
Focus Point ✓, Clipping ✓, Photo Counts ✓, Mask Overlay ✓ instead of Show Focus Point ✓. No two commands share a
name any more: View ▸ Detail is now Detail View (the Edit section Detail keeps its name), Window ▸ Edit is Edit
Panel, and Compare Before and After is Before/After Side by Side, next to Split and Top/Bottom.
- The preview cache command has one name, Discard Preview Cache, in File ▸ Previews and in the toast that confirms
it. Keyboard shortcuts are unchanged.
### Dropping files shows what will happen
- While files are dragged over the window, it dims and a card says what dropping will do: review the photos and
folders for import, import the presets, or open the face model. If a dialog is open, the card asks you to close it
first, and a drop is refused with a message instead of replacing the dialog. The same goes while an import is
running, and in the web version.
- The import review of a finished scan (a drop, or File ▸ Import) no longer replaces a dialog you opened in the
meantime; it opens as soon as that dialog is closed.
- Photos and folders dropped on the window now open the same import review as File ▸ Import, so you pick add, copy or
move, the destination and a preset before anything is imported. Settings ▸ Import ▸ "Import dropped files without
review" brings back the old behaviour of importing at once.
- In the web version, a dropped file the browser can't read, or that can't be imported, now shows a message instead of
failing silently.
- For scripts: `ui.dropFiles` hovers files over the window or drops them, as the operating system does.
### Counting many smart albums is faster
- Counting every smart album at once (the numbers in the Albums list, and the `albums.list` command) now works out an
album that other smart albums test (“album is …”) once for the whole library, instead of again for every album
that tests it. On a synthetic library of 312,000 photos with 77 smart albums, 51 of them testing one shared album,
the full count took about 14 s instead of about 23 s on a heavily loaded machine; the rest is the text rules of
the albums themselves. The counts are the same as before, including for albums that loop through each other.
### Sharper resampling in HDR and panorama merges
- Merged photos (HDR and panorama) are resampled with a sharper filter (Lanczos-3 instead of bilinear). Moving a
photo by half a pixel used to lose a third of its finest detail; now it keeps most of it, without halos around
@@ -85,6 +175,20 @@
- `photo.setReference` refuses an `id` that is not a photo id (a string, a negative or fractional
number) instead of pinning the active photo; only an absent or null `id` means the active photo.
### Edit Capture Time leaves a capture time it can't read as it was
- A capture time that is cut short, has seconds that aren't digits or has text after it (a Lightroom catalog's capture
time is copied as it is) is no longer shifted into a clean-looking wrong time: Edit Capture Time leaves the photo as
it was and says so, and its preview says the photo's time can't be read instead of previewing a wrong one.
`photo.setCaptureTime` counts those photos as `unreadable`. Set date & time still repairs such a photo selected on
its own; with other photos selected it says why it can't move them by the same amount. When the dialog can't make a
change (a time that isn't a date, a shift past year 9999), it now says why and stays open, instead of closing.
### Month names in folder and file name templates
- Date templates take the month by name: `%B` gives January, `%b` gives Jan. Import ▸ Folders ▸ Custom template…
`{date:%Y}/{date:%B}` files photos into 2026/January, 2026/February…, the layout many existing libraries already
use. The names are English in every interface language, so folders don't change when the language does. Rename
Photos and export file names take them too.
## 2026-10-20
### Smart albums check File Path rules faster
@@ -93,7 +197,6 @@
or "doesn't contain" rule took about 17 ms per pass instead of about 62 ms, around 3.5 times faster, with the same
photos matched; about 6 ms of that is only reading the paths. Paths and rule text with letters beyond A–Z are
compared as before.
## 2026-10-19
### AI Denoise on DirectX 12 with NVIDIA GTX 10-series cards
@@ -116,6 +219,13 @@
- With several locations removed from Local, "Show N hidden locations" now lists them, so each comes back on its own
(Restore All is there too). The row used to bring every hidden location back at once (issue #2114).
### Rating in a filtered view keeps your place
- When a filter shows only one-star photos and you take the star off a photo, it leaves the view and the photo after
it is now selected, so you carry on from where you were (issue #2312). The same goes for flags and colour labels,
in Picks and in smart albums. Before, the photo stayed selected although the grid no longer showed it, and → or
Auto Advance jumped back to the first photo of the view. With Auto Advance on, or with Shift+0–5, you land on the
photo after it: no photo is skipped.
## 2026-10-18
### Thumbnails of Lightroom HDR and panorama merges in a second
@@ -187,7 +297,6 @@
one (issue #1507). LightCraft used to open the library without it, showing an incomplete catalog that the next save
would write over the stored copy. It now leaves the library unopened, saves nothing and says which file couldn't be
read; reload the page to try again.
## 2026-10-14
### Model downloads time out on a name that doesn't resolve
@@ -433,6 +542,15 @@
screen and nearly as tall counts as zoomed.
- For scripts: a size set with `ui.resize` is remembered like any other, so a script that needs a size sets it.
### Keyword List: a keyword shows that one inside it is applied
- In the Keyword List (right panel ▸ Keywords), a keyword whose box is empty now carries a dot in it when the
selected photos have a keyword inside it, at any depth: with `Places › Italy › Rome` on a photo, `Places` and
`Italy` show the dot and `Rome` the tick. A folded keyword no longer hides that something in it is applied.
Resting on the box names what is inside.
- The tick and the dash are as they were, and are about the keyword itself: all of the selected photos have it, or
some do. A keyword the photos have themselves shows that, not the dot. A click on the box still gives the
keyword itself to the selected photos.
### HDR merges keep the lens correction
- An HDR made with Photo ▸ Photo Merge ▸ HDR now keeps the lens correction the photos carry (Sony's, Panasonic's
and Leica's, and the one some DNGs carry, e.g. from drones): Optics ▸ Enable Lens Corrections straightens it as it