mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 20:48:33 -06:00
Merge main into fix/issue-107-web-safety
# Conflicts: # apps/lightcraft-web/src/web.rs # docs/parity.md # docs/whats-new.md
This commit is contained in:
@@ -9,7 +9,16 @@
|
||||
← {"id": 2, "ok": false, "error": "unknown command `nope`"}
|
||||
```
|
||||
|
||||
Requests are answered on the UI thread between frames (timeout 60 s). The MCP server's connect
|
||||
Requests are answered on the UI thread between frames (timeout 60 s).
|
||||
|
||||
**Only requests are read.** Every line must be a JSON object with a string `method` (`id` and `params` are
|
||||
optional; blank lines are skipped). Anything else — text that isn't JSON, a JSON array or number, an object without
|
||||
`method`, invalid UTF-8, or a line longer than 4 MiB — gets one error reply
|
||||
(`{"ok": false, "error": "… closing the connection"}`) and the server **closes the connection**; nothing sent after
|
||||
it on that connection runs. This keeps an HTTP request (for example a web page's cross-origin `fetch` to
|
||||
`127.0.0.1:<port>`) from smuggling a command in its body: its request line is rejected first. Junk never reaches the
|
||||
UI thread, and at most 16 connections are served at once (further ones get an error line and are closed). Clients
|
||||
that hit an error reply should reconnect. The port has no authentication, so only enable it when you need it. The MCP server's connect
|
||||
mode ([mcp.md](mcp.md)) is a thin layer over this channel. Implementation:
|
||||
`crates/ui-egui/src/control.rs` (methods) and `apps/lightcraft/src/control_server.rs` (transport).
|
||||
|
||||
@@ -21,7 +30,7 @@ mode ([mcp.md](mcp.md)) is a thin layer over this channel. Implementation:
|
||||
| `engine.commands` | — | Engine + UI commands: id, label, menu, shortcut, params doc, enabled |
|
||||
| `ui.menu.list` | — | Menu entries (flat: id, label, menu path, shortcut, enabled) |
|
||||
| `ui.menu.tree` | — | The menu bar as shown (File … Help): items `{id, params?, label, shortcut?, enabled, checked?}`, separators, submenus — the model behind the native macOS menu bar and the in-window menus |
|
||||
| `ui.inspect` | — | UI state, window, canvas/image rects, `scroll: {grid, filmstrip}` (scroll offsets in points, `null` until drawn), active photo, selection, perf (`frameMs` = layout, `logicMs` = per-frame logic before it, `updateMs` = both, `maxUpdateMs`, `fps`, render queue …, `gpu` = adapter in use, `gpuReason` = why renders don't use the GPU, `gpuFallback` = latest render redone on the CPU and why — see `docs/gpu-pipeline.md`), status, memory (bytes per cache, see `library.memory`; plus stage caches and textures), `export: {running: {total, done, current} \| null, last}` |
|
||||
| `ui.inspect` | — | UI state, window, canvas/image rects, `scroll: {grid, filmstrip}` (scroll offsets in points, `null` until drawn), active photo, selection, perf (`frameMs` = layout, `logicMs` = per-frame logic before it, `updateMs` = both, `maxUpdateMs`, `fps`, render queue …, `gpu` = adapter in use, `gpuReason` = why renders don't use the GPU, `gpuFallback` = latest render redone on the CPU and why — see `docs/gpu-pipeline.md`), status, memory (bytes per cache, see `library.memory`; plus stage caches and textures), `export: {running: {total, done, current} \| null, last}`, `notices` (warnings waiting to be shown, e.g. a damaged settings file; OK = `button:noticeOk`), `quitPrompt` (why quitting was stopped: unsaved changes; `button:quitRetry` / `button:quitAnyway` / `button:quitCancel`) |
|
||||
| `ui.widgets` | `{filter?}` | On-screen widgets `{id, rect: [x, y, w, h]}` (screen points) |
|
||||
| `ui.clickWidget` / `ui.dragWidget` / `ui.hoverWidget` | `{id, count?, fx?, fy?}` / `{id, toX?, toY?, dx?, dy?, steps?}` / `{id, fx?, fy?}` | Real egui input on a widget (hover: the pointer rests on it, e.g. for preset/profile previews) |
|
||||
| `ui.move` / `ui.click` / `ui.drag` | `{x, y, count?, button?}` / `{x, y, toX, toY, steps?}` | Raw pointer input, screen points |
|
||||
@@ -49,6 +58,15 @@ shows a warning (widget `indicator:unsaved`). Queries and commands that change n
|
||||
compaction (snapshot) is not a failed command — the log is kept whole — and only shows in `library.info` →
|
||||
`lastError`.
|
||||
|
||||
### When the library can't be opened
|
||||
|
||||
If the desktop app can't open its library at launch (another program has it open, an unreadable or newer-format
|
||||
catalog, a missing drive), the session starts empty and in memory — never with demo photos — and a window asks what
|
||||
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.
|
||||
|
||||
## Headless rendering (no window, no GPU)
|
||||
|
||||
The egui UI can be rasterized on the CPU (`crates/ui-egui/src/softpaint.rs`, driven by
|
||||
|
||||
+4
-1
@@ -15,7 +15,10 @@ It runs in one of two modes:
|
||||
|
||||
Options: `--library DIR` opens (or creates) a persistent LightCraft library — the same crash-safe
|
||||
format the desktop app uses (`~/Pictures/LightCraft Library` by default there) — so ratings, edits and albums
|
||||
survive between sessions (with `--demo`, a new library is seeded with the demo photos);
|
||||
survive between sessions (with `--demo`, a new library is seeded with the demo photos). A library
|
||||
is open in one program at a time (`catalog.lock` in the library folder): while the desktop app has
|
||||
it open, `--library` on the same folder fails with "This library is already open in LightCraft
|
||||
(process N …)" — use connect mode to work with the running app instead;
|
||||
`--demo` starts the headless session with the procedurally generated demo library;
|
||||
`--compact` lists only the helper tools (see below). In connect mode the server starts even when
|
||||
the app is not running yet and connects on the first call (and reconnects if the app restarts).
|
||||
|
||||
+16
-15
@@ -47,10 +47,10 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table
|
||||
| X. Cross-cutting behaviours (BEHAV) | 15 | 3 | 2 | 1 | 8/8 (100%) | 6/8 (75%) |
|
||||
| Y. Menus | 81 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) |
|
||||
| Z. Keyboard shortcuts (desktop) | 74 | 3 | 3 | 1 | 49/52 (94%) | 22/23 (96%) |
|
||||
| Lightroom Classic extras | 30 | 9 | 40 | 9 | — | 20/21 (95%) |
|
||||
| **Total** | 386 | 29 | 91 | 37 | 194/200 (97%) | 138/148 (93%) |
|
||||
| Lightroom Classic extras | 31 | 9 | 40 | 9 | — | 21/22 (95%) |
|
||||
| **Total** | 387 | 29 | 91 | 37 | 194/200 (97%) | 139/149 (93%) |
|
||||
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **79.2%** of 506 in-scope rows — P0 98.5% of 200 · P1 95.6% of 148 · P2 39.2% of 158.
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **79.2%** of 507 in-scope rows — P0 98.5% of 200 · P1 95.6% of 149 · P2 39.2% of 158.
|
||||
<!-- /parity:summary -->
|
||||
|
||||
## Top gaps
|
||||
@@ -116,7 +116,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-IMP-ADD-DIALOG | Add photos/folders | P0 | ✅ | `cmd:file.addPhotos`, `cmd:library.importPreview`, `cmd:library.import` (`mode`, `album`, `albumName`, `preset`, `keywords`), `crates/ui-egui/src/import.rs` | review dialog (Import Photos): the scanned source (a scanned folder is not added to Local), candidate grid with thumbnails and checkboxes (duplicates by path/content marked and unchecked), transfer mode (add in place / copy / move, each explained), album (existing/new), preset, keywords; the folder scan runs on a background thread with a progress window and Cancel (a NAS folder no longer freezes the window); batched import with a progress window; one undo step. Drag-and-drop still imports directly |
|
||||
| LR-IMP-DRAGDROP | Drop files/folders to import | P0 | ✅ | `crates/ui-egui/src/lib.rs` (dropped files → `cmd:library.import`) | dropping onto a specific album not supported |
|
||||
| LR-IMP-DUPES | Skip duplicates by content | P1 | ✅ | `crates/engine/src/import.rs`, `crates/engine/src/tests_import.rs` | |
|
||||
| LR-IMP-DEVICE | Import from camera/card | P1 | ✅ | `cmd:library.devices`, `cmd:file.addFromDevice`, `crates/engine/src/devices.rs` | mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Import from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); no PTP/MTP (cameras that don't mount as a disk) |
|
||||
| LR-IMP-DEVICE | Import from camera/card | P1 | ✅ | `cmd:library.devices`, `cmd:file.addFromDevice`, `crates/engine/src/devices.rs` | mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Import from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); copies are verified like Move's (a new file, synced, compared byte for byte; a bad copy is removed and reported as failed, never counted as imported; taken names get -1, -2…, never replaced — issue #96, `crates/engine/src/import_move.rs`); no PTP/MTP (cameras that don't mount as a disk) |
|
||||
| LR-IMP-AUTO | Watched-folder auto import | P2 | ✅ | `cmd:library.autoImport`, `cmd:library.autoImportScan`, `crates/ui-egui/src/panels/settings.rs` | Settings ▸ Import ▸ Auto Import: a watched folder whose new photos are added (in place or copied) once complete, into an optional album; scanned every 3 s |
|
||||
| LR-IMP-PRESET | Preset on import | P2 | ✅ | `cmd:library.import` (`preset`) | chosen in the import review; one History entry |
|
||||
| LR-IMP-RAWDEFAULT | Raw defaults | P1 | ✅ | `cmd:library.preferences`, `crates/engine/src/import.rs`, `crates/ui-egui/src/panels/settings.rs` | LightCraft default / a preset / per camera (make + model); non-raw default too; the preset look counts as unedited and Reset returns to it |
|
||||
@@ -128,7 +128,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-IMP-CAMERA-COVERAGE | Camera coverage, verified per model | P0 | 🟡 | `crates/raw/tests/corpus.rs`, `xtask/src/main.rs` | a supported container is not the same as every camera that writes it decoding correctly: ~40 CC0 corpus files (plus ~60 NEFs checked by hand) against the >1,000 models Lightroom lists; per-model differences still surface (e.g. the CR2 colour-filter layout differed by model until it was read from the file's own tag, issue #85, checked on 35 CR2 bodies). Grow the CC0 corpus per model and test each sample decodes with plausible colour |
|
||||
| LR-IMP-CULL-AT-IMPORT | Culling analysis at import | P2 | 🟡 | `cmd:photo.analyze` | run Assisted Culling on the imported photos (they're selected after an import); not automatic |
|
||||
| LR-IMP-MOVE | Move on import [Classic] | P1 | ✅ | `cmd:library.import` (`mode` move), `crates/engine/src/import_move.rs`, `crates/engine/src/tests_import_move.rs`, `crates/ui-egui/src/import.rs` | Transfer ▸ Move with Copy's destination, folders (day / month / one folder / custom template), rename template and example destination; XMP sidecars (both namings) move along; each source is removed only after its destination is written (hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate, unchecked files keep their sources, taken names get -1, -2…, never overwritten; files already in the destination/library are added in place; sources that can't be removed (read-only card) are kept and reported (`kept`). Undo removes the photos from the library but leaves the moved files at the destination. No Copy as DNG while moving |
|
||||
| LR-IMP-DNG-CONVERT | Convert to DNG on import [Classic] | P2 | ✅ | `cmd:library.import` (`dng`), `crates/ui-egui/src/import.rs` | copy imports: Raw files ▸ Copy as DNG (lossless; the card is untouched) |
|
||||
| LR-IMP-DNG-CONVERT | Convert to DNG on import [Classic] | P2 | ✅ | `cmd:library.import` (`dng`), `crates/ui-egui/src/import.rs` | copy imports: Raw files ▸ Copy as DNG (lossless; the card is untouched); the raw copy is removed only once its DNG is verified and on disk (issue #106) |
|
||||
|
||||
## B. Library management (LIB)
|
||||
|
||||
@@ -153,7 +153,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-LIB-DELETE | Delete / Recently Deleted | P0 | ✅ | `cmd:photo.delete`, `cmd:photo.restore`, `cmd:photo.deletePermanently` | no confirmation dialog, no auto-purge after N days, no "Empty" |
|
||||
| LR-LIB-REMOVE-ALBUM | Remove from album | P0 | ✅ | `cmd:album.removePhotos` | |
|
||||
| LR-LIB-DUPLICATE | Duplicate a photo | P2 | ✅ | `cmd:photo.duplicate` | Photo ▸ Duplicate: a real `-copy` file with the same settings, metadata and albums (virtual copies share the file) |
|
||||
| LR-LIB-RENAME | Batch rename | P1 | ✅ | `cmd:photo.rename`, `cmd:photo.renamePreview`, `cmd:photo.renameTokens`, `cmd:dialog.rename`, `crates/engine/src/rename.rs` (`TOKENS`) | template tokens {name} {num} {seq:N} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext} (shared with export naming and import rename); a Tags picker beside every template field (Rename Photos, import Rename, export File name) lists each tag with meaning + live example, the date directives and the rules, and inserts the clicked tag at the text cursor; unknown tags are flagged; preview; renames files on disk with their XMP sidecars, never overwriting (-1, -2… suffixes), rolls back on failure; undo/redo move the files; virtual copies follow |
|
||||
| LR-LIB-RENAME | Batch rename | P1 | ✅ | `cmd:photo.rename`, `cmd:photo.renamePreview`, `cmd:photo.renameTokens`, `cmd:dialog.rename`, `crates/engine/src/rename.rs` (`TOKENS`) | template tokens {name} {num} {seq:N} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext} (shared with export naming and import rename); a Tags picker beside every template field (Rename Photos, import Rename, export File name) lists each tag with meaning + live example, the date directives and the rules, and inserts the clicked tag at the text cursor; unknown tags are flagged; preview; renames files on disk with their XMP sidecars (a stem sidecar another file still shares, e.g. raw + JPEG, is copied rather than taken), never overwriting (-1, -2… suffixes; a change of letter case only is checked by file identity, so on case-sensitive volumes `img_1.jpg` next to `IMG_1.JPG` is a collision), rolls back on failure (a file that can't be moved back is reported old → new and the library follows it as an undoable partial rename; the same for undo/redo); undo/redo move the files; virtual copies follow |
|
||||
| LR-LIB-CAPTURETIME | Edit capture time | P1 | ✅ | `cmd:photo.setCaptureTime`, `cmd:dialog.captureTime`, `crates/catalog/src/dates.rs` | set (the other selected photos shift by the same amount, or `each`), shift by days/hours/minutes, time-zone shift; one undo step, journaled; Info panel button; no “revert to original capture time” |
|
||||
| LR-LIB-SHOWFINDER | Reveal original in file manager | P0 | ✅ | `cmd:app.showInFinder` | ⌘R (see MENU-FILE-SHOWFINDER) |
|
||||
| LR-LIB-COVER | Album cover | P2 | ✅ | `cmd:album.setCover` | |
|
||||
@@ -210,7 +210,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-META-COPYRIGHT-DEFAULT | Default copyright on import | P1 | ✅ | `cmd:library.preferences` (`import.copyright`, `import.creator`), `crates/ui-egui/src/panels/settings.rs` | Settings → Import → Metadata; fills only photos without their own |
|
||||
| LR-META-LOCATION | Location editing | P2 | 🟡 | `cmd:photo.setMeta` (`location`, `city`, `state`, `country`, `gps`) | place fields and GPS (decimal or degrees / minutes / seconds) edited in Info, read/written as IPTC Core / Photoshop / EXIF XMP; Show on Map opens OpenStreetMap; no embedded map, no geocoding |
|
||||
| LR-META-COPYPASTE | Copy / paste metadata | P2 | ✅ | `cmd:photo.copyMetadata`, `cmd:photo.pasteMetadata` | title, caption, alt text, extended description, copyright (notice, status, usage terms, info URL), creator, place fields, keywords; `fields` picks a subset |
|
||||
| LR-META-XMP | XMP read/write | P0 | ✅ | `cmd:photo.saveMetadataToFile`, `cmd:photo.readMetadataFromFile`, `cmd:library.xmpPreferences`, `crates/engine/src/sidecar.rs`, `docs/xmp-interop.md` | sidecar wins for metadata; a sidecar capture time (`exif:DateTimeOriginal` / `photoshop:DateCreated` / `xmp:CreateDate`) fills in only when the file has none |
|
||||
| LR-META-XMP | XMP read/write | P0 | ✅ | `cmd:photo.saveMetadataToFile`, `cmd:photo.readMetadataFromFile`, `cmd:library.xmpPreferences`, `crates/engine/src/sidecar.rs`, `docs/xmp-interop.md` | sidecar wins for metadata; a sidecar capture time (`exif:DateTimeOriginal` / `photoshop:DateCreated` / `xmp:CreateDate`) fills in only when the file has none; saving merges into an existing sidecar (`crates/meta/src/xmp_merge.rs`): another app's `crs:`, `xmpMM:History` and unknown namespaces are kept byte for byte, an unreadable sidecar is backed up first; with stem naming, files sharing a stem (raw + JPEG) get separate sidecars (`<file>.xmp` for all but the raw) |
|
||||
| LR-META-EXIF-FULL | Full EXIF/IPTC [Classic] | P1 | ✅ | `cmd:photo.allMetadata`, `cmd:dialog.allMetadata`, `crates/meta/src/tags.rs`, `crates/meta/src/exif.rs`, `crates/meta/src/iptc.rs` | read and written on export; Info ▸ All Metadata…: every TIFF / EXIF / GPS / interop tag (named, common values spelled out) and the XMP fields, searchable; metadata presets |
|
||||
|
||||
## F. Edit panel — global adjustments (EDIT)
|
||||
@@ -436,10 +436,10 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-EXP-METADATA | Metadata policy | P1 | ✅ | `cmd:app.export` (`metadata`, `removeLocation`) | |
|
||||
| LR-EXP-WATERMARK | Watermark | P1 | ✅ | `cmd:app.export` (`watermark`: text or `image` + `imageWidth`), `crates/engine/src/export.rs` (`Watermark`) | text (size, colour, shadow) or a graphic with transparency (width as % of the photo, converted to the output colour space); position, inset, opacity |
|
||||
| LR-EXP-NAMING | File naming | P1 | ✅ | `cmd:app.export` (`naming`, `startNumber`), `crates/engine/src/rename.rs` (`expand_tokens`) | one template language for export, Rename Photos and import renaming: `{name}`, `{num}` (number at the end of the name), `{seq}` / `{seq:N}`, `{date}` / `{date:%Y-%m-%d}`, `{folder}`, `{camera}`, `{lens}`, `{iso}`, `{rating}`, `{title}`, `{creator}`, `{ext}`; free-form template rather than a list of named schemes |
|
||||
| LR-EXP-LOCATION | Destination folder | P0 | ✅ | `cmd:app.export` (`dir`, `subfolder`, `conflict`: unique / overwrite / skip), `crates/ui-egui/src/panels/dialogs.rs` (Choose… folder picker) | folder picker on the desktop; existing files get `-2`, `-3`… by default |
|
||||
| LR-EXP-LOCATION | Destination folder | P0 | ✅ | `cmd:app.export` (`dir`, `subfolder`, `conflict`: unique / overwrite / skip), `cmd:export.checkTarget`, `crates/ui-egui/src/panels/dialogs.rs` (Choose… folder picker), `crates/engine/src/originals.rs`, `crates/catalog/src/safe_file.rs` | folder picker on the desktop; existing files get `-2`, `-3`… by default; the policy covers a file and its sidecars together; a catalogued original (or its sidecar) is never written over, whatever the policy or an exact `path` (export, `ui.render`, MCP `render_photo`, merge `previewPath`, `lightcraft-cli render`); files are written to a temp file, synced and renamed into place (issue #93) |
|
||||
| LR-EXP-PREVIOUS | Export with previous settings | P0 | ✅ | `cmd:app.exportPrevious`, `cmd:dialog.export` | last options persist in prefs.json; dialog prefilled; no named export presets yet |
|
||||
| LR-EXP-DNGOPT | DNG options | P2 | 🟡 | `cmd:app.export` (`dngCompression`), `crates/engine/src/export.rs` | compression: lossless JPEG (default), ZIP or none; no embedded JPEG preview size, no lossy DNG output |
|
||||
| LR-EXP-ORIGINAL | Original + XMP | P1 | ✅ | `cmd:app.export` (`format: original`) | file copied byte for byte, sidecar named after the output |
|
||||
| LR-EXP-ORIGINAL | Original + XMP | P1 | ✅ | `cmd:app.export` (`format: original`), `crates/engine/src/tests_export.rs` | file copied byte for byte, sidecar named after the output and subject to the conflict policy |
|
||||
| LR-EXP-PHOTOS | Export to the system photo library | P2 | ⬜ | | |
|
||||
| LR-EXP-PSD | Round trip to an external editor | P2 | ✅ | `cmd:photo.editExternal`, `cmd:photo.editInExternal`, `crates/engine/src/cmd/convert.rs` | a 16-bit TIFF `-Edit` copy with the edits (Adobe RGB / ProPhoto / P3 / sRGB) next to the original, added stacked on top of it and opened in the editor set in Settings ▸ General (or the system default) |
|
||||
|
||||
@@ -463,7 +463,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-PREF-GENERAL | General settings | P0 | ✅ | `cmd:app.settings`, `cmd:app.openLibrary`, `crates/ui-egui/src/panels/settings.rs` | General / Import / Performance / Interface tabs; app settings in ui.json, library settings in prefs.json |
|
||||
| LR-PREF-GENERAL | General settings | P0 | ✅ | `cmd:app.settings`, `cmd:app.openLibrary`, `crates/ui-egui/src/panels/settings.rs`, `crates/ui-egui/src/panels/notices.rs` | General / Import / Performance / Interface tabs; app settings in ui.json (written atomically, saved when the library changes and every few seconds), library settings in prefs.json; a damaged settings file is kept as `.corrupt-<time>` and reported, an unreadable one is not overwritten that session (`library.info` → `settingsWarnings`); quitting with unsaved changes retries, then asks |
|
||||
| LR-PREF-LOCALSTORAGE | Storage & cache | P1 | ✅ | `cmd:library.preferences`, `cmd:library.clearPreviews`, `cmd:library.compact` | thumbnail cache size + clear in Settings → Performance; library location + Open Library… in General |
|
||||
| LR-PREF-ACCOUNT | Account | OOS | 🚫 | | |
|
||||
| LR-PREF-INTERFACE | Interface options | P1 | ✅ | `cmd:app.settings` | filmstrip names/badges, grid badges (auto/always/never), square-grid names, navigator, info overlay |
|
||||
@@ -480,7 +480,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-CLOUD-SYNC | Cloud sync | OOS | 🚫 | | |
|
||||
| LR-CLOUD-SMARTPREVIEW | Editable proxies | P2 | ✅ | `cmd:library.smartPreviews`, `cmd:library.smartPreviewsLocation`, `cmd:photo.smartPreview`, `crates/engine/src/smart.rs`, `crates/ui-egui/src/panels/settings.rs` | File ▸ Previews ▸ Build / Discard Smart Previews: ~1 MB proxies in the library; with the original offline the photo renders, edits and exports (at proxy size) from its proxy; Info shows the status; the proxy folder is chosen per library (Settings → Performance → Smart previews, saved in the library): the effective path, count and size are shown, changing it needs a choice for the proxies already built (move / leave / delete), and an unavailable or unwritable folder is an error, never a fallback to the library drive. The thumbnail cache stays in the library |
|
||||
| LR-CLOUD-SMARTPREVIEW | Editable proxies | P2 | ✅ | `cmd:library.smartPreviews`, `cmd:library.smartPreviewsLocation`, `cmd:photo.smartPreview`, `crates/engine/src/smart.rs`, `crates/ui-egui/src/panels/settings.rs` | File ▸ Previews ▸ Build / Discard Smart Previews: ~1 MB proxies in the library; with the original offline the photo renders, edits and exports (at proxy size) from its proxy; Info shows the status; the proxy folder is chosen per library (Settings → Performance → Smart previews, saved in the library): the effective path, count and size are shown, changing it needs a choice for the proxies already built (move / leave / delete), and an unavailable or unwritable folder is an error, never a fallback to the library drive; proxies are written atomically and a damaged one (cut short by a crash or a full drive) doesn't count as built, is rebuilt (`repaired`) and is never moved over a good one (issue #106). The thumbnail cache stays in the library |
|
||||
| LR-AI-UPDATE-INDICATOR | AI-settings update indicator | P2 | ⬜ | | |
|
||||
| LR-AI-CREDITS | Generative credits | OOS | 🚫 | | |
|
||||
|
||||
@@ -488,7 +488,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-BEHAV-AUTOSAVE | Instant autosave | P0 | ✅ | `crates/catalog/src/journal.rs`, `crates/engine/src/library.rs` | every change fsynced before the command returns; a failed write fails the command ("saved in memory but not written to disk"), keeps the change queued and retries; top-bar warning until saved |
|
||||
| LR-BEHAV-AUTOSAVE | Instant autosave | P0 | ✅ | `crates/catalog/src/journal.rs`, `crates/engine/src/library.rs` | every change fsynced before the command returns; a failed write fails the command ("saved in memory but not written to disk"), keeps the change queued and retries; top-bar warning until saved; a write that fails part-way is cut back to the last whole record so the retry never reads back as damage (logs from older builds with such a fragment load in full); quitting saves queued changes in the closing snapshot even when the log can't be appended to; versioned catalog format (`journal::VERSION`): older libraries are upgraded on open, a library from a newer LightCraft is refused untouched ("written by a newer version") |
|
||||
| LR-BEHAV-UNDO | Global undo | P0 | ✅ | `cmd:edit.undo`, `crates/engine/src/tests.rs` (`rating_flag_undo_redo`) | covers ratings, albums, deletes, edits |
|
||||
| LR-BEHAV-MULTISELECT | Multi-selection | P0 | ✅ | `cmd:library.select` (`replace`/`add`/`toggle`/`range`), `cmd:library.selectAll` | |
|
||||
| LR-BEHAV-BATCH | Batch apply to selection | P0 | ✅ | `cmd:photo.rate`, `cmd:develop.paste`, `cmd:preset.apply`, `cmd:app.export` | |
|
||||
@@ -714,7 +714,8 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
|
||||
| LRC-LIB-SURVEY | Survey view | P2 | ✅ | `cmd:view.survey` | N |
|
||||
| LRC-LIB-REFVIEW | Reference view | P2 | ✅ | `cmd:view.reference`, `cmd:photo.setReference`, `crates/ui-egui/src/panels/compare.rs` (`show_reference`) | View ▸ Reference View (⇧R): the reference photo beside the active one (edited with the Edit panel); photo menu ▸ Set as Reference Photo |
|
||||
| LRC-LIB-CATALOG-PANEL | Catalog sets | P1 | ✅ | `cmd:library.source` (`missing`) | all, recently added, picks, recently deleted, missing photos (when any) |
|
||||
| LRC-LIB-FOLDERS | Disk folder tree | P1 | ✅ | `cmd:library.browse`, `cmd:folder.rename`, `cmd:folder.move`, `crates/ui-egui/src/panels/left.rs` (`local_section`, `folder_tree`) | standard places + the browsed folder + Browse Folder…, each an expandable tree of subfolders, breadcrumb navigation; context menu: Rename Folder…, Move Folder To…, Show in Finder (on disk, photos relinked; like the file manager, not an undo step) |
|
||||
| LRC-LIB-CATALOG-LOCK | Catalog open in one program at a time | P1 | ✅ | `cmd:app.openLibrary`, `crates/catalog/src/lock.rs`, `crates/engine/src/library.rs` (`open_library`) | exclusive OS lock on `catalog.lock` for the session (app and `lightcraft-cli --library`); a second opener is refused with who has it (`catalog.lock.owner`: program, process, computer); released by the OS on a crash, so never stale; file systems without locks open unprotected with a warning; no read-only mode; the browser build (OPFS) has no guard across tabs yet. A library that can't be opened at launch (locked, unreadable, newer format, drive missing) shows a window — Try Again, Choose Another Library…, Continue Without Saving (then a banner; nothing written), Quit — instead of a silent demo session (`crates/ui-egui/src/panels/library_problem.rs`) |
|
||||
| LRC-LIB-FOLDERS | Disk folder tree | P1 | ✅ | `cmd:library.browse`, `cmd:folder.rename`, `cmd:folder.move`, `crates/ui-egui/src/panels/left.rs` (`local_section`, `folder_tree`) | standard places + the browsed folder + Browse Folder…, each an expandable tree of subfolders, breadcrumb navigation; context menu: Rename Folder…, Move Folder To…, Show in Finder (on disk, photos relinked; one undo step each: undo / redo move the folder back and forth, refused when the destination is taken) |
|
||||
| LRC-LIB-COLLECTIONS | Collections & sets | P1 | ✅ | `cmd:album.create`, `cmd:album.createSmart`, `cmd:album.toggleTarget`, `cmd:album.setTarget`, `cmd:album.clearQuick` | albums + folders (sets), smart albums (rule editor), Quick Collection and target album (B in the grids adds / removes the selection; album menu ▸ Set as Target Album; the target is marked +) |
|
||||
| LRC-LIB-SMARTCOLL | Smart-collection rules | P1 | ✅ | `cmd:album.createSmart`, `cmd:album.setRules`, `cmd:album.ruleFields`, `cmd:dialog.smartAlbum`, `crates/catalog/src/rules.rs`, `crates/ui-egui/src/panels/rules_editor.rs` | rule editor: match all / any / none, nested groups (⌥+ or + Group), 26 fields (rating, flag, label, type, edits, keywords, any text, filename, format, title, caption, camera, lens, location, creator, copyright, copyright status, capture / import / edit date, ISO, aperture, focal length, megapixels, GPS, virtual copy, album) with text / number / date / in-the-last operators; live match count; also usable as a library filter (`ruleSet`) |
|
||||
| LRC-LIB-PUBLISH | Publish services | P2 | ⬜ | | |
|
||||
@@ -728,8 +729,8 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
|
||||
| LRC-LIB-PEOPLE | People view | P2 | ⬜ | | |
|
||||
| LRC-LIB-COMMENTS | Comments panel | P2 | ⬜ | | |
|
||||
| LRC-LIB-VISUALSEARCH | Find similar photos | P2 | ✅ | `cmd:library.findSimilar`, `crates/pipeline/src/cull.rs` (`signature`) | photo menu / Photo ▸ Find Similar Photos: look-alike photos (composition and tones), most similar first, as a filter (`only`) |
|
||||
| LRC-LIB-MISSING | Missing files & relink | P1 | ✅ | `cmd:library.missing`, `cmd:photo.relink`, `cmd:library.findMissing`, `cmd:file.findMissing`, `cmd:photo.locate`, `crates/engine/src/cmd/missing.rs` (`checked_path`) | File → Find Missing Photos… (same name and size anywhere in a folder, one undo step), photo menu → Locate Missing File…; unreadable files show "!" in the grid and a reason in the loupe; undo never moves files. Missing Photos in the sidebar (counted on a worker thread); it covers library photos only — Local browse records are never checked, for the count, the view, `library.missing` and Find Missing alike |
|
||||
| LRC-LIB-CONVERT | Convert to DNG | P2 | ✅ | `cmd:photo.convertToDng`, `crates/engine/src/cmd/convert.rs` | Photo ▸ Convert to DNG: lossless DNG next to the raw with the settings embedded, photo (and its virtual copies) relinked; originals kept; undoable |
|
||||
| LRC-LIB-MISSING | Missing files & relink | P1 | ✅ | `cmd:library.missing`, `cmd:photo.relink`, `cmd:library.findMissing`, `cmd:file.findMissing`, `cmd:photo.locate`, `crates/engine/src/cmd/missing.rs` (`checked_path`) | File → Find Missing Photos… (anywhere in a folder: same name, size and content hash; renamed files by size + content hash; same name and size alone only when unambiguous — look-alikes are skipped and reported; one undo step), photo menu → Locate Missing File…; unreadable files show "!" in the grid and a reason in the loupe; undo never moves files. Missing Photos in the sidebar (counted on a worker thread); it covers library photos only — Local browse records are never checked, for the count, the view, `library.missing` and Find Missing alike |
|
||||
| LRC-LIB-CONVERT | Convert to DNG | P2 | ✅ | `cmd:photo.convertToDng`, `crates/engine/src/cmd/convert.rs` | Photo ▸ Convert to DNG: lossless DNG next to the raw with the settings embedded, photo (and its virtual copies) relinked; originals kept; undoable; the DNG is decoded and compared with the raw data, written to a temp file, synced, read back and only then given a free name (never replacing a file) — a failed write leaves no DNG and the photo on its raw (issue #106) |
|
||||
| LRC-LIB-PREVIEWS | Build / discard previews | P1 | ✅ | `cmd:library.buildPreviews`, `cmd:library.previewProgress`, `cmd:library.cancelPreviews`, `cmd:library.clearPreviews`, `crates/engine/src/cmd/previews.rs`, `crates/preview/src/lib.rs` | File ▸ Previews: build standard-sized (Settings → Performance size) or 1:1 previews of the selected / visible photos in the background (progress toasts, stop), discard the cache; disk thumbnail + view cache; no smart previews (offline proxies) |
|
||||
| LRC-LIB-SLIDESHOW-IMPROMPTU | Impromptu slideshow | P2 | ✅ | `cmd:view.slideshow` | see LR-VIEW-SLIDESHOW |
|
||||
| LRC-DEV-SNAPSHOTS | Named snapshots | P1 | ✅ | `cmd:version.create`, `cmd:version.restore` | = versions |
|
||||
|
||||
@@ -83,6 +83,10 @@ are the same in every version, so serve them with `Cache-Control: no-cache` (not
|
||||
the background within a frame or two, each file replaced atomically, in modification order
|
||||
(`apps/lightcraft-web/src/files.rs`). View state and UI prefs are saved every second when they
|
||||
change (a tab can close without notice).
|
||||
- Known limitation: the browser storage has no file locks, so two tabs of the same origin open
|
||||
the same library and the last one to write a snapshot wins (the desktop app and the CLI lock
|
||||
a library folder: `catalog.lock`, issue #99). Use one tab at a time; a guard through the Web
|
||||
Locks API (`navigator.locks`) is still to do.
|
||||
- `originals/<content hash>`: the bytes of every imported photo. The catalog refers to them as
|
||||
`web/<hash>/<file name>`; the main thread keeps recently used originals in memory (≤ 768 MB)
|
||||
and loads the rest on demand (the active photo is prefetched).
|
||||
|
||||
@@ -10,6 +10,38 @@
|
||||
- 23 new built-in presets: Portrait, Landscape, Urban, Food, Seasons, Vintage and B&W toners.
|
||||
|
||||
### Reliability
|
||||
- Exports and renders never write over a photo's original (issue #93): exporting into the photo's own folder with
|
||||
the same name and "Overwrite" (or Export with Previous repeating it), an exact output path from the control
|
||||
channel or MCP, a merge preview path or `lightcraft-cli render IMG.jpg -o IMG.jpg` is refused with a clear
|
||||
message, and the original is left byte for byte. Ordinary earlier exports are still overwritten when asked.
|
||||
Exported files are written to a temp file, synced and then renamed into place, so a full disk or an unplugged
|
||||
drive never leaves a truncated file; the XMP sidecar of an "Original" export follows the "If file exists"
|
||||
choice too.
|
||||
- Convert to DNG, Copy as DNG, Photo Merge and smart previews no longer write straight to the final file (issue
|
||||
#106): a DNG is checked against the raw data, written to a temp file, synced and read back before it gets its
|
||||
name (never replacing a file), and only then is the photo relinked or the raw copy removed — a failed write
|
||||
leaves no DNG and keeps the raw. Smart previews are written the same way; a damaged one (cut short by a crash or
|
||||
a full drive) no longer counts as built and Build Smart Previews replaces it.
|
||||
- Import ▸ Copy verifies every copy, like Move (issue #96): each file is written as a new file, synced to disk and
|
||||
compared byte for byte with the card. A copy that fails or differs is removed and reported as a failed import —
|
||||
so "import complete" means the copies are good before you format the card — and a name that is taken gets -1,
|
||||
-2… instead of being replaced.
|
||||
- Saving metadata to an XMP sidecar another application wrote no longer replaces it (issue #92): LightCraft merges its
|
||||
fields in and keeps the rest — e.g. that application's develop settings and edit history — byte for byte. A
|
||||
sidecar that isn't valid XMP is copied to `<name>.xmp.bak-<time>` first. With the default stem naming, a raw and a
|
||||
JPEG with the same name (`IMG_0001.CR3` + `IMG_0001.JPG`) no longer share one sidecar: the raw keeps `IMG_0001.xmp`,
|
||||
the JPEG uses `IMG_0001.JPG.xmp`.
|
||||
- A save that fails part-way (a full disk, a network share that drops) no longer looks like a damaged catalog
|
||||
afterwards (issue #101): the partial write is cut off before LightCraft retries, so the next launch replays every
|
||||
change. Catalogs already holding such a fragment load in full. Quitting while the catalog log can't be written
|
||||
still saves your queued changes in the closing snapshot.
|
||||
- The catalog has a format version (issue #102). Opening a library from an older LightCraft upgrades it; a library
|
||||
written by a newer LightCraft is refused with "this library was written by a newer version of LightCraft" and left
|
||||
untouched — older versions no longer read part of it as a damaged log. Once this version has opened a library,
|
||||
LightCraft 0.2.0 and older refuse it ("unsupported format … v2").
|
||||
- The control port (`--control`) closes a connection as soon as it receives anything that isn't a JSON request
|
||||
(issue #94): an HTTP request from a web page can no longer carry a command in its body. Lines are capped at
|
||||
4 MiB and connections at 16.
|
||||
- Browser version (experimental), keeping a library safe (issue #107): File ▸ Back Up Library… downloads the catalog
|
||||
and every imported photo as one zip, and File ▸ Restore Library from Backup… brings it back (the current library
|
||||
is kept). A failed save (storage full) now shows the unsaved warning and is retried, a photo that can't be stored
|
||||
@@ -25,8 +57,26 @@
|
||||
in short pieces so slow integrated GPUs aren't reset by their watchdog. `ui.inspect` → `perf`
|
||||
(`gpuReason`, `gpuFallback`), Help ▸ System Info and Settings ▸ Performance say why the GPU isn't
|
||||
used (e.g. a skipped software adapter such as llvmpipe) and why the last render fell back.
|
||||
- The thumbnail cache only ever counts and deletes its own files (issue #98): a library opened on a folder that
|
||||
already has a `thumbs/` folder of other pictures no longer loses them when the cache is trimmed or cleared.
|
||||
- Settings files are never quietly reset (issue #103): a damaged `prefs.json`, `presets.json` or `view.json` is kept
|
||||
as `<name>.corrupt-<time>` and you're told; one that can't be read (e.g. locked by another program) is left alone
|
||||
for the session instead of being overwritten with defaults. The app settings (`ui.json`, which remembers your
|
||||
library) are written atomically and saved as soon as you open another library, not only at quit. Quitting while
|
||||
changes couldn't be saved tries once more, then asks: Try Saving Again, Quit Anyway or Cancel.
|
||||
|
||||
### Library
|
||||
- Rename Photos never overwrites another photo when only the letter case changes (issue #95): on case-sensitive
|
||||
volumes (Linux, case-sensitive APFS) `img_1.JPG` next to `IMG_1.JPG` is a different photo and the renamed one gets
|
||||
`img_1-1.JPG`; on case-insensitive volumes the case change still goes through.
|
||||
- Rename Photos reports files it could not move back after a failure (issue #105), e.g. when a network share drops
|
||||
mid-batch: the error lists them (old → new) and the library points at their new names (an undoable partial rename),
|
||||
so none shows as missing. Renaming one of a raw + JPEG pair copies their shared `IMG_0001.xmp` instead of taking
|
||||
it away from the other (issue #92). Find Missing Photos also finds renamed files by their content, prefers a content match
|
||||
over a same-name same-size look-alike, and skips (and reports) photos it can't tell apart instead of guessing.
|
||||
- Rename Folder… and Move Folder To… (Local) can be undone and redone (issue #97): the folder moves back on disk with
|
||||
its photos and XMP sidecars, and the photos point at it again. If something now occupies the old place, the undo
|
||||
is refused and nothing is overwritten.
|
||||
- Smart albums with a rule editor: match all / any / none, nested groups, 26 fields.
|
||||
- Quick Collection and target album (B in the grid), keyword sets (⌥1–⌥9), colour-label sets.
|
||||
- Colour-label filter with several labels at once; expandable folder tree in Local.
|
||||
@@ -44,6 +94,14 @@
|
||||
- Smaller, faster catalogs: photos you only looked at in Local (never added, rated or edited) are forgotten once
|
||||
their folder has not been browsed for 30 days — your files and sidecars stay, and browsing the folder shows them
|
||||
again. Change the period (or turn it off) in Settings → Performance.
|
||||
- A library is open in one program at a time (issue #99): opening a library that LightCraft or `lightcraft-cli` already
|
||||
has open — on this computer or another one sharing the folder — says who has it ("already open in LightCraft (process
|
||||
123 on studio-mac)") instead of letting both write and silently drop each other's edits. A crash never leaves the
|
||||
library locked: the lock is the operating system's and goes away with the program.
|
||||
- If your library can't be opened at launch (open in another program, unreadable, on a drive that isn't connected,
|
||||
written by a newer LightCraft), LightCraft says so and why, and offers Try Again, Choose Another Library…, Continue
|
||||
Without Saving and Quit (issue #100). It no longer quietly starts a demo session that looked like a reset library and
|
||||
lost everything at quit; a temporary session shows a banner the whole time and never writes to your library.
|
||||
|
||||
### Editing
|
||||
- Auto Sync: edits apply to every selected photo. Auto B&W mix. Automatic versions.
|
||||
|
||||
+35
-2
@@ -8,8 +8,8 @@ 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`. |
|
||||
| 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. Written atomically (temp file + rename). |
|
||||
| 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. |
|
||||
| Preferences | `library.xmpPreferences {autoWrite?, naming?: stem\|full}`, stored in the library's `prefs.json`. |
|
||||
|
||||
@@ -37,6 +37,39 @@ used — on import (it then also files a copied photo in its date folder) and by
|
||||
`xmp:Rating="-1"` (the XMP convention for rejected) sets the reject flag. Develop settings come from
|
||||
`lc:settings` when present (exact); otherwise from the `crs:` fields below (approximate).
|
||||
|
||||
## Saving into an existing sidecar
|
||||
|
||||
A sidecar may already hold another application's data — e.g. its `crs:` develop settings and `xmpMM:History` — often
|
||||
the only copy of those edits outside that application's catalog. Saving never drops it:
|
||||
|
||||
- LightCraft **owns** the properties in the table above: `xmp:Rating`, `xmp:Label`, `dc:title`, `dc:description`,
|
||||
`dc:rights`, `dc:creator`, `dc:subject`, `Iptc4xmpCore:Location`, `Iptc4xmpCore:AltTextAccessibility`,
|
||||
`Iptc4xmpCore:ExtDescrAccessibility`, `photoshop:City`/`State`/`Country`, `xmpRights:Marked`/`UsageTerms`/
|
||||
`WebStatement` and everything in `lc:`. They are replaced on every save, and removed when LightCraft has no value
|
||||
(clearing a title in LightCraft clears it in the file). All of them are read back on import, so the library starts
|
||||
from what the sidecar said.
|
||||
- The **capture time** (`exif:DateTimeOriginal`, `photoshop:DateCreated`) and **GPS** (`exif:GPSLatitude`,
|
||||
`exif:GPSLongitude`) are replaced only when LightCraft has a value; otherwise the file's stay.
|
||||
- **Everything else is kept byte for byte**: other namespaces (`crs:`, `xmpMM:`, `lr:hierarchicalSubject`, unknown
|
||||
ones), `xmp:CreatorTool`, comments, the packet wrapper and padding. LightCraft's properties go into one
|
||||
`rdf:Description` of their own; owned properties written by another application (in attribute or element form) are
|
||||
removed from its description, which otherwise stays as it was.
|
||||
- A sidecar that can't be read as XMP (not well-formed, not UTF-8, no `rdf:RDF`) is first copied to
|
||||
`<name>.xmp.bak-<date><time>` (`-1`, `-2`… if taken — a backup is never overwritten), then replaced. A sidecar that
|
||||
can't be read at all (permissions) is left alone and the save fails.
|
||||
|
||||
Limits: LightCraft writes keywords to `dc:subject` only, so another application's `lr:hierarchicalSubject` is kept as
|
||||
it was and may still list keywords removed in LightCraft. LightCraft doesn't write `crs:`: the other application's
|
||||
develop settings stay as that application left them, next to LightCraft's own (`lc:settings`, which LightCraft reads
|
||||
first).
|
||||
|
||||
## Shared names
|
||||
|
||||
With stem naming, `IMG_0001.CR3` and `IMG_0001.JPG` map to the same `IMG_0001.xmp`. When two or more files in the
|
||||
library share a stem, the stem sidecar belongs to one of them — a raw first, otherwise the first by file name — and
|
||||
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`).
|
||||
|
||||
## Reading `crs:` develop fields
|
||||
|
||||
Many raw developers store edits as `crs:` properties (`http://ns.adobe.com/camera-raw-settings/1.0/`) in sidecars, in
|
||||
|
||||
Reference in New Issue
Block a user