LR-IMP-ADD-DIALOG: reconcile album picker with integrated main (#2425)

Retain both sets of parity revision notes after the upstream integration.
Keep the hierarchical picker and deleted-destination guard unchanged.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
tylerphotos
2026-10-21 17:34:40 -05:00
co-authored by Codex
138 changed files with 5681 additions and 767 deletions
+14 -3
View File
@@ -2,7 +2,8 @@
LightCraft selects objects with **SAM 3** (Segment Anything with Concepts, Meta 2025), run in
pure Rust by `crates/segment` on [candle](https://github.com/huggingface/candle): on the GPU
through Metal on macOS, on the CPU elsewhere (for now).
through Metal on macOS, on the CPU elsewhere (for now). If the Mac's GPU can't run it, it runs on
the CPU (see the worker below).
**The model is optional.** It is not part of LightCraft, and nothing requires it: without it,
Object and Describe offer to download it, every other feature (and every existing mask, see
@@ -16,7 +17,10 @@ In the Masking panel (M):
to leave it out. Clicks show as green (include) and red (leave out) dots.
- **Describe**: type what to select — `sky`, `trees`, `the red car` — and press Return. Every
instance the model finds (score above 0.5) joins the mask; if nothing matches, nothing is
created and you are told so.
created and you are told so. Around what it found, the mask follows the model's semantic mask
for the phrase, which is sure inside and runs on where two instances touch (the instances'
own masks alone left a sky or a net about 70–90 % selected and a thin line between two people
side by side).
- Several things at once: `car, road` (comma-separated) selects each and merges them.
- **+ / −** under the selected mask (and the Add / Subtract / Intersect menus) combine
selections: Describe `car`, click the mask, + ▸ Describe… `road`; − ▸ Describe… `people` takes
@@ -214,7 +218,7 @@ wait and return the result (when built with the `sam` feature; the default CLI h
`NOTICE` — the ported files say so and that they were modified): the 32-layer ViT backbone
with 2-D RoPE and windowed attention, the feature pyramids, the SAM 2-style prompt encoder and
two-way mask decoder for clicks, and the CLIP text encoder, DETR encoder/decoder (box
relative-position bias, presence token) and pixel decoder for text. The CLIP tokenizer is a
relative-position bias, presence token), pixel decoder and semantic head for text. The CLIP tokenizer is a
small BPE in `tokenizer.rs`. The downloader (original code) is the `lightcraft-fetch` crate, shared by any model that is downloaded; `fetch/` here says what SAM 3 needs.
- candle is pinned at 0.9.2: later releases make `candle-core` depend on `tokenizers` with the
Oniguruma C library, and the product is pure Rust. Weights are read with positional reads (no
@@ -225,6 +229,13 @@ wait and return the result (when built with the `sam` feature; the default CLI h
encoded photo; the session sends it jobs and receives results over channels, never holding a
lock the model holds. Each job runs under `catch_unwind`: a panic inside candle becomes an
error and the model is reloaded on the next request; busy counters are reset by drop guards.
- GPU fallback (issue #1974): a job that fails on the GPU (a panic or a candle error, e.g. an
AMD GPU in an Intel Mac whose Metal compiler crashes on a matmul kernel) frees the model and runs
once more on the CPU. If that works, the model stays on the CPU until LightCraft restarts and
the user is told once ("The GPU couldn't run the AI mask model…"); if the CPU fails too, the
error is reported and the next job tries the GPU again. Missing or damaged model files and
remote-worker errors are not retried. `LIGHTCRAFT_GPU=0` or `LIGHTCRAFT_GPU_BACKEND=off` keep
the model on the CPU from the start.
- Hostile documents: a selection keeps at most 4 detail patches (when read and when rendered),
each at most 1024² logits; more than 64 clicks are never sent to the model.
- Accuracy: `crates/segment/tests/reference.rs` compares every stage against the Python
+1
View File
@@ -60,6 +60,7 @@ guard.detail(&file_name);
| Lightroom catalog import | `crates/ui-egui/src/lightroom_import.rs` | stop being cancellable for a phase that must finish |
| Face scan, AI Denoise | `crates/engine/src/cmd/face_recognize.rs`, `crates/engine/src/denoise.rs` | a continuous job with no ✕, kept in session state |
| Find Missing Photos | `crates/ui-egui/src/tasks.rs` | a generic background task with a row (`kind: Some(..)`) |
| Reload from Disk | `crates/ui-egui/src/reload.rs`, `crates/engine/src/cmd/reload.rs` | a command split into plan / probe / apply so the app reads the files on a worker thread (`tasks::spawn_tracked`: progress and `Cancel::Flag`); the result is committed through `Session::execute_fn` (undo step, save, XMP auto-write), and dropped for a photo a later-started reload has already updated; `wait: true` runs it in place |
| Reading focus points | `crates/engine/src/focus_scan.rs` | a worker paced by the UI's per-frame pump (`focus.pump {pace}`); `Cancel::Flag`; commits in batches without undo steps |
## Testing
+12
View File
@@ -127,6 +127,18 @@ tiles. All ten sensor comparisons pass. R8 HEVC preview tracks are distinguished
the primary Bayer RAW; the HEVC preview itself is not decoded. Missing corpus
files are reported as skipped by the tests.
In a tile with a neighbour on its right, the vertical high-pass bands of the level-1 and level-2
low-pass columns (bands 8 and 5) are entropy coded at an odd width: an even band carries one extra
column that synthesis does not use. No other band is extended, including the last level's (band 2).
The decoder takes each band's coded width from this rule. It used to try the synthesis width and up to
two more columns and keep the first that decoded to the exact end of the stream, but two widths can
both do that and give different coefficients (#899). The rule was measured on every C-RAW plane in
`cr3_corpus.rs`: two-tile files at tile widths 2816, 3144, 3192, 3444 and 3564 (G5 X Mark II, M50,
R100, 250D, RP, R, 90D, M6 Mark II), whose left tiles extend bands 5 and 8 exactly where the rule says,
and single-tile `0x100` and `0x200` files, which never extend a band. In every plane, each band decodes
at its coded width and at no other width up to two columns wider, so all sensor hashes are unchanged.
No public file has three or more C-RAW tiles; a middle tile follows the same rule, which is unverified.
The version `0x200` QP predictor and entropy framing were checked independently.
Controlled input mutations establish floor averaging of paired QP rows before
the nonlinear integer table, then multiplication, division by eight, addition
+1 -1
View File
@@ -27,7 +27,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 545 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 / F828 regression (#702), 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; 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 | 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) |
+2
View File
@@ -43,6 +43,8 @@ Overrides (read once at launch):
- else `WGPU_BACKEND` (wgpu's own variable, same names) — before #136 only the window honoured it,
the compute device always used Vulkan + DX12 (+ Metal).
- `LIGHTCRAFT_GPU=0`: GPU rendering off for the process (the window is unaffected).
- Both `LIGHTCRAFT_GPU=0` and `LIGHTCRAFT_GPU_BACKEND=off` also keep AI Denoise, the face models
and the AI mask model (SAM 3 on Metal) on the CPU.
**DX12 shader compiler (issue #471).** Every instance (window and compute) compiles DX12 shaders with
FXC (`d3dcompiler_47.dll`, part of Windows; `lightcraft_gpu::backend::backend_options`). wgpu's
+19 -5
View File
@@ -28,6 +28,11 @@ settings are imported. Missing originals remain catalogued for relinking, count
separately from failures reading files that are present. Known-missing originals are not probed
again. Previously browsed Local records join All Photos even when their originals are offline;
their promotion is counted once and undone together with the catalog import.
An image whose file, folder or root folder record is gone from the catalog, or whose original path
is empty (Lightroom leaves these behind and ignores them), is skipped together with its virtual
copies instead of failing the whole import. The result lists them as data (`skipped`: each
image id with its reason, `missingFile`, `missingFolder`, `missingRoot`, `incompletePath` or
`masterSkipped`), also in `library.inspectLightroom`, and the desktop result window says how many in the UI language.
Virtual copies count as added records too; reimport reports zero additions when all
records already exist. Existing LightCraft
edits are preserved by default; `updateExisting=true` explicitly replaces them. Persistent import
@@ -44,11 +49,16 @@ retained. Oldest managed archives are retired; legacy `.json` archives remain un
`Interop/lightroom-index.json` records imported identities. Source catalogs and originals are
never overwritten.
Each native `.lrcat` database and WAL file is independently limited to 1 GiB
(1,073,741,824 bytes). This bounds the current whole-file reader; referenced photo sizes and
photo counts are not part of that limit. XMP packets are capped at 16 MiB and each table at one million rows.
The in-tree, read-only Rust SQLite B-tree reader follows SQLite’s public file-format
specification; it introduces no dependency or C runtime. Table payloads are capped at 256 MiB.
Native import reads referenced database pages on demand (at most 64 KiB per page), rather than
loading the whole `.lrcat`. Database files are capped at 16 GiB and 16,777,216 pages; the WAL
remains capped at 1 GiB. Tables still have a 256 MiB encoded-payload budget and one million rows,
individual records are capped at 64 MiB, and XMP packets at 16 MiB. These limits can still reject
large catalogs with dense tables; file size alone no longer determines the memory allocation.
The open file and source path are checked again after all tables are read. Close Lightroom before
importing: metadata checks detect ordinary concurrent changes, but are not SQLite snapshot isolation.
The in-tree, read-only Rust SQLite B-tree reader follows SQLite's public file-format
specification; it introduces no dependency or C runtime.
Optional history, keyword and collection tables that exceed limits are skipped with warnings;
required photo tables still report errors. Unrelated unsupported schemas are ignored. A needed
WITHOUT ROWID or virtual table produces an explicit error, handled as a warning when optional.
@@ -106,3 +116,7 @@ an absolute path is not looked at: the import lists it as missing with a warning
photo and its virtual copies out of the library. To import originals on a share, open the catalog
from that share. A path that is not a path on this computer (a Windows drive path read on macOS or
Linux) is not looked at either and stays catalogued as missing.
## Historical local validation
Validated in a local Linux environment after upstream reconciliation: 47 catalog-focused tests, nine pipeline spot tests, the exact sparse-catalog regression, and all eight `cargo xtask ci` stages passed. The full gate recorded 3,166 passing test executions, zero failures, and 28 ignored tests. An original synthetic sparse catalog with a required image-table page beyond 1 GiB also passed headless application import and selection; its expected missing-original placeholder retained its five-star rating. This snapshot check does not establish native-window coverage or dense-catalog performance.
+6 -4
View File
@@ -27,11 +27,13 @@ revizí rozsahu a terminologie. Rodilý mluvčí ani profesionální překladate
plurálové pravidlo se nepřidává.
- Písmo Inter dodávané s aplikací obsahuje české znaky i uvozovky „…“. Další font není potřeba.
Katalog obsahuje 2 056 běžných položek a 246 formátových šablon. Každý běžný klíč existuje
v jiném katalogu nebo je používán kódem. Oproti německému katalogu má 108 dalších klíčů,
Katalog obsahuje 2 394 běžných položek a 297 formátových šablon. Každý běžný klíč existuje
v jiném katalogu nebo je používán kódem. Oproti německému katalogu má 215 dalších klíčů,
všechny s doloženým použitím: popisky příkazů, menu, ovládacích prvků a nastavení,
texty HDR/exportu a obličejů a kontextové hodnoty Ano/Ne. Z toho 41 klíčů již existuje
v dalších katalozích a 67 doplňuje současné překladové lookupy.
texty HDR/exportu a obličejů a kontextové hodnoty Ano/Ne. Z toho 66 klíčů již existuje
v dalších katalozích a 149 doplňuje současné překladové lookupy. Doplnění z 18. 10. 2026
přidalo texty novějších funkcí (prezentace, tisk, přednastavení efektů masek, zaostřovací
body) a prošlo stejnou kontrolou.
Řádkové fragmenty poznámek k vydáním byly odstraněny, včetně 163 fragmentů převzatých
ze sdílených katalogů; text poznámek v dialogu Co je nového používá anglický fallback.
+4 -2
View File
@@ -23,8 +23,10 @@ interface never uses Mtavruli (a test checks the whole catalog). Commands are ve
are count-neutral, noun first (`იმპორტირებულია ფოტო: 3`); English plural endings are dropped with
`{:.0}`. Dates read `კვირა, 20.09.2026` (`{weekday}, {day:02}.{month:02}.{year}`), months `09.2026`.
Inter has no Georgian letters and craft-fonts has no Georgian face yet, so Georgian text shows boxes
until a Georgian face is available to the UI.
Inter has no Georgian letters, so the UI draws Georgian with craft-fonts' Noto Sans Georgian (`Geor`),
like the Thai and Arabic faces: desktop release builds embed it, and a web build made with
`CRAFT_FONTS_DIR` keeps it in the WASM (`crates/engine/build.rs`). The release web build doesn't use
craft-fonts yet, and a build without `CRAFT_FONTS_DIR` has no Georgian face and shows boxes.
## Terminology
+3 -2
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (de, es, fr, ja, uk catalogs filled; re-measured; Italian and Georgian interface catalogs added) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** trivial (Italian: every interface string translated, native-speaker corrections; previously minor: de, es, fr, ja, uk catalogs filled; re-measured; Italian and Georgian interface catalogs added) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
How far each interface language is from `full`. How to add or maintain a language is in
[localization.md](localization.md); per-language notes are in `localization-<code>.md`
@@ -65,7 +65,7 @@ that is promising but is not recorded as review, so the column says `no (communi
| Traditional Chinese (Taiwan) | `zh-hant` | 1,993 / 2,304 (87%) | Hant; uses the Simplified Chinese face until craft-fonts has a TC face | no (community edits) | partial | 8–14 h (incl. the font) |
| Russian | `ru` | 1,704 / 2,304 (74%) | Cyrillic (Inter) | no (community edits) | partial | 8–12 h |
| Ukrainian | `uk` | 2,383 / 2,398 (99%; 2026-10-12) | Cyrillic (Inter) | no (community edits) | partial (lower-layer errors English) | 2–4 h |
| Italian | `it` | 2,201 / 2,398 (92%, measured 2026-10-12; the rest is What's New) | Latin (Inter) | no (written with an agent; native-speaker pass pending) | partial (lower-layer errors English) | 3–5 h |
| 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) |
Target languages LightCraft lacks: Korean, Dutch, Polish, Swedish, Norwegian Bokmål, Thai
@@ -93,6 +93,7 @@ Target languages LightCraft lacks: Korean, Dutch, Polish, Swedish, Norwegian Bok
| Date | Change | Summary |
|---|---|---|
| 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-12 | minor | Italian interface catalog (`it`): 2,201 of 2,398 messages, What's New left in English |
| 2026-10-11 | minor | Shared work: the interface decides on error kinds, not English error text (#1046) |
| 2026-10-12 | minor | Georgian (`ka`) row: every catalog message |
+5 -2
View File
@@ -146,8 +146,11 @@ Each edit in a batch has its own undo step. Connect mode also lists `ui_inspect`
Ports and connect mode are unchanged.
Every listed tool has a title and all four MCP hints. Generated command tools are conservatively
marked as edits. Helpers with optional output paths are marked as writers; `export` is not
idempotent because the default conflict policy chooses a new filename on repeated calls.
marked as edits. `export`, `render_photo` and `screenshot` have `readOnlyHint: false` and
`destructiveHint: true`: their caller-supplied output paths can replace existing files, even
when a path is optional. The image-only `render_preview` and `ui_screenshot` remain read-only
and non-destructive. These annotations are client hints, not a file-access sandbox.
`export` is not idempotent because the default conflict policy chooses a new filename on repeated calls.
Unknown helper argument keys return JSON-RPC `-32602` naming the key and accepted arguments.
Generated `cmd_*` tools and nested command params remain free-form: the registry has prose
parameter docs, not machine-readable schemas. Existing command validation (including strict
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -1,6 +1,6 @@
# RAW parity: cameras, formats, demosaic, colour and lens profiles
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** patch (X-Trans CMOS 4 bodies take the X-T3's spectral matrices) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (lossy JPEG XL DNG tiles with a non-linear declared transfer curve decode; X-Trans CMOS 4 bodies take the X-T3's spectral matrices) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
Raw processing is the deepest single dimension of Lightroom parity: a photographer whose camera
does not decode, or decodes with the wrong colour, cannot switch. This document measures it model by
@@ -155,7 +155,7 @@ checked (the per-model CC0 sweeps of October 2026); compressed ORF (OM / Olympus
| SRW | uncompressed, 12-bit packed | ✅ | EX1 corpus; NX10, NX20 |
| SRW | compressed 32770 / 32772 / 32773 | ⬜ | refused |
| DNG | uncompressed, LJ92, lossy JPEG / Smart Preview, Deflate incl. float, LinearRaw, tiles / strips, row / column interleave | ✅ | 6 corpus DNGs; 4 ILCE-6400 JXL DNGs; 38 Leica Q3 DNGs (lossless JPEG, 14-bit, private check) |
| DNG | JPEG XL (DNG 1.7, lossless and lossy) | ✅ | synthetic bit-exact; real lossless and lossy files |
| DNG | JPEG XL (DNG 1.7, lossless and lossy) | ✅ | synthetic bit-exact; real lossless and lossy files; lossy tiles keep their declared transfer curve (Lightroom Classic 15.1 LinearRaw DNGs label linear data with the sRGB curve); PQ / HLG tiles preview-only |
| DNG | Apple ProRAW (incl. semantic mattes) | ✅ | iPhone 12 Pro corpus; ProfileGainTableMap read but off (ΔE00 19 against Lightroom with it on) |
| DNG | phone DNGs (Pixel, Galaxy, Xiaomi…) | 🟡 | Pixel 2 XL corpus; 13 further Apple, Google and Samsung models in the pinned sweep; white-level pile-up fix #548 |
| DNG | GoPro GPR (VC-5) | ⬜ | refused |
@@ -288,6 +288,8 @@ Beyond decoding:
| Date | Change | Summary |
|---|---|---|
| 2026-10-18 | patch | X-T30, X-T30 II, X-Pro3, X100V, X-S10 and X-E4 take the X-T3's spectral matrices (same 26.1 MP X-Trans CMOS 4 sensor): an X-T30 raw with a black-and-white (Acros) camera JPEG no longer opens in the muted neutral fallback |
| 2026-10-15 | minor | DNGs with a look table and no tone curve (Lightroom-written, Adobe Standard) fit their tone to the embedded preview, also Lightroom's 1024 px one: two lossy ILCE-7CM2 DNGs no longer render 0.8 EV dark and flat |
| 2026-10-15 | minor | Lossy JPEG XL DNG tiles that declare a non-linear SDR transfer curve (sRGB, BT.709, DCI, gamma) decode with that curve restored instead of falling back to the embedded preview: Lightroom Classic 15.1 LinearRaw DNGs (Rec. 2100 primaries, sRGB curve; 2 private ILCE-7CM2 files) |
| 2026-10-15 | minor | Hasselblad 3FR / FFF embedded previews: the uncompressed 8-bit RGB image, handed on as a baseline TIFF (checked on the CC0 H4D-40 FFF, 1217 × 913, and CFV 3FR, 218 × 218, upright through the file's orientation); counts unchanged |
| 2026-10-15 | minor | Hasselblad 3FR / FFF get their own reader (`crates/raw/src/vendor/hasselblad.rs`): an FFF takes its colour matrix from the original 3FR header in tag `0xC51B` instead of developing with the neutral matrix; RGGB checked against the camera preview on the CC0 H4D-40 FFF and CFV 3FR; counts unchanged |
| 2026-10-14 | minor | Camera-coverage sweep (#1327): 30 models verified on 40 CC0 files (`coverage_sweep_corpus.rs`, `coverage-sweep.tsv`; `docs/raw/coverage.py` 515 → 545 verified rows); the counts above are not re-measured here. Sensor verification does not establish colour parity (#1277) |
+8 -1
View File
@@ -111,7 +111,9 @@ are the same in every version, so serve them with `Cache-Control: no-cache` (not
grant it, a notice says the library may be evicted under storage pressure.
- **File ▸ Back Up Library…** downloads a zip of the library files and every stored original
(`originals/<hash>/<file name>`, so an unzipped backup is browsable; entries are stored, not
compressed; at most 4 GB). **File ▸ Restore Library from Backup…** reads such a zip into a
compressed; at most 4 GB). While a save is failing it refuses to start and says how many
changes are unsaved and why: the zip copies the stored library files, so it would miss them
(issue #1508). **File ▸ Restore Library from Backup…** reads such a zip into a
new library folder (`library-restored-<time>/`; originals already stored are skipped, every
entry's checksum is verified, and each restored original must match its content hash) and
only then switches to it (`active-library` names the folder in use) and reloads: the previous
@@ -121,6 +123,11 @@ are the same in every version, so serve them with `Cache-Control: no-cache` (not
writes, so commands report `saved in memory but not written`, the top bar shows the unsaved
warning (as on the desktop, see `docs/control-protocol.md`), and saving is retried every
2 s until it works (`web.stats` → `saveError`).
- A failed read at start-up is not taken for a missing file: only `NotFoundError` means absent.
When a stored library file (or the `active-library` pointer) exists but can't be read, the
library isn't opened and nothing is saved, so the stored copy stays as it was; the window says
which file failed (issue #1507). A thumbnail index that can't be read keeps the stored
thumbnails instead of deleting them as unindexed.
- A picked or dropped photo whose bytes can't be stored is not added (it would be gone after
a reload); a notice says why.
- One tab at a time: the page holds a Web Lock (`navigator.locks`, `lightcraft-library`); a
+45
View File
@@ -1,5 +1,50 @@
# What's new in LightCraft
## 2026-10-19
### Hidden Local locations come back one at a time
- 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).
## 2026-10-18
### Fill Location from GPS: no more neighbouring town in City
- The offline place names list towns of 15,000 people or more, so a photo taken in a smaller one got the nearest larger
town in City, sometimes another municipality (issue #2242). The Fill Location tooltip and the command help now say so,
and `metadata.reverseGeocode` takes `maxDistance` (metres): past it, City stays empty (State and Country still fill)
and the reply names the town it found as `nearestCity`.
### A preset that couldn't be saved is no longer lost silently
- When presets.json can't be written (a full disk, or the browser's storage quota), the new or changed preset now
counts as unsaved, like other changes: the top bar warns, saving is retried, and quitting asks first (issue #2266).
Before, the failure was only logged, and the preset could be lost without any warning.
### Portrait crop ratios stay portrait
- A crop ratio no longer flips to the photo's orientation (issue #2253): after X turns the crop portrait, picking
another ratio (4 × 5, 2 × 3…) keeps it portrait, as in Lightroom, and a Custom ratio taller than wide gives a
portrait crop.
## 2026-10-15
### Constrain Crop tightens the crop you see
- Geometry ▸ Constrain Crop now changes the crop itself, as in Lightroom (issue #1927). Ticking it, or moving a
Geometry slider, Upright or a lens correction while it is ticked, shrinks the crop about its centre until the blank
corners left by the correction are gone, and the Crop tool shows the tighter crop. Before, it only applied when the
photo was shown outside the Crop tool, so in the Crop panel, where the checkbox is, nothing seemed to change. A crop
you draw by hand is kept as drawn, and unticking keeps the tighter crop.
### Web: no backup that misses unsaved changes
- In the browser version, File ▸ Back Up Library… no longer reports success while a save is failing (storage full or
cleared): the backup copies the stored library, so it left out the changes that couldn't be saved (issue #1508). It
now refuses to start and says how many changes are unsaved and why. LightCraft retries the save with every change;
back up once nothing is left unsaved.
### Web: an unreadable library is not opened
- In the browser version, a library file that exists but can't be read at start-up is no longer taken for a missing
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
+5 -1
View File
@@ -10,7 +10,7 @@ other tools), and let LightCraft pick up edits made elsewhere.
|---|---|
| Name | `<stem>.xmp` by default (`IMG_0001.CR3` → `IMG_0001.xmp`); `<file>.xmp` (`IMG_0001.CR3.xmp`) with `naming: "full"`. Reading accepts either (preferred first) and `.XMP`. With stem naming, files sharing a stem don't share a sidecar: see *Shared names* below. |
| Write | `photo.saveMetadataToFile {ids?}` (Photo ▸ Save Metadata to File, ⌘S), or automatically after every change with `library.xmpPreferences {autoWrite: true}` (File ▸ Automatically Write Changes into XMP). Slider drags are written once, when the drag ends; undo/redo rewrite the sidecar. An existing sidecar is **merged into, never replaced** (see *Saving into an existing sidecar*). Written atomically (temp file, fsync, rename). The result lists `written`, `merged` and `backups`. |
| Read | On import (`library.import`, the report counts `sidecars`), and `photo.readMetadataFromFile {ids?}` (one undo step). For raw/DNG files without a sidecar, the XMP embedded in the file is used. `photo.readMetadataFromFile` never writes the sidecar, even with auto-write on: writing back what it has just read would add `lc:settings`, which later reads prefer over another app's `crs:` edits (issue #934). |
| Read | On import (`library.import`, the report counts `sidecars`), and `photo.readMetadataFromFile {ids?}` (one undo step). For raw/DNG files without a sidecar, the XMP embedded in the file is used. `photo.readMetadataFromFile` never writes the sidecar, even with auto-write on: writing back what it has just read would add `lc:settings`, which later reads prefer over another app's `crs:` edits (issue #934). A file arriving in an import is ranked as if it were catalogued already: it reads its own `<file>.xmp` when another file of its stem (a raw first) owns the shared stem sidecar (issue #1905). |
| Preferences | `library.xmpPreferences {autoWrite?, naming?: stem\|full}`, stored in the library's `prefs.json`. |
What we write (standard namespaces, so other tools can read the metadata):
@@ -74,6 +74,10 @@ library share a stem, the stem sidecar belongs to one of them — a raw first, o
the others write and read `<file>.xmp` (`IMG_0001.JPG.xmp`), falling back to the stem sidecar for reading when they
have none. So saving one photo never overwrites the other's metadata (`Session::sidecar_naming`).
An import ranks the files it is adding the same way, before they are catalogued: a TIFF or JPEG arriving beside a
raw of its stem reads its own `<file>.xmp` and takes nothing from the raw's `IMG_0001.xmp`, whether it arrives
with the raw or after it (issue #1905).
## Face regions (MWG)
LightCraft reads face, pet, focus and barcode regions in the Metadata Working Group's region schema