Files
lightcraft/docs/parity-checklist.md
2026-10-13 02:01:30 -04:00

206 KiB
Raw Permalink Blame History

Lightroom parity checklist

Last reviewed: 2026-10-10 · Last updated: 2026-10-11 · Change: minor (LR-MERGE-HDR: merges of raws without colour matrices keep their colour (#965); rapid slider entry; LR-PROF-CAMERACOLOR: tone curve from black-and-white camera JPEGs; LR-VIEW-ZOOM, LR-VIEW-COMPARE: Compare and Reference zoom windows; LR-IMP-FORMATS, LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes; renamed from parity.md; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; filesystem history retry/Undo/Redo evidence; renamed from parity.md; Top gaps moved to gaps.md, shortcut notes to ui-parity.md) · Target: Adobe Lightroom Classic 15.5 / Lightroom 9.6

The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers does the feature exist? (feature breadth). Whether it is good enough to replace Lightroom is assessed in target-app-parity.md, and the work list is gaps.md. Whoever lands a feature updates its row in the same commit.

The rows are maintained by hand; the summary block is generated (cargo xtask parity --write), and cargo xtask parity (part of cargo xtask ci) checks that every command id, control id and path cited here still exists. Formerly docs/parity.md.

  • Ids come from the (local, gitignored) reference in plan/lightroom/: LR-… = feature catalog (03), MENU-… = menu items (04), KEY-… / KEYC-… = keyboard shortcuts (06, desktop / Classic), LRC-… = Classic extras (08). Ids are stable; never renumber.
  • Tier as in the catalog: P0 core, P1 important parity, P2 later / AI / niche, OOS out of scope.
  • Status: ✅ done (usable end to end; polish may be noted) · 🟡 partial (note says what is missing, or the status is unverified) · ⬜ missing · 🚫 out of scope / not applicable.
  • Evidence: cmd:<id> = command id (engine command_specs() or UI UI_COMMANDS), ctl:<id> = develop control id (lightcraft-cli controls), plus source files. A trailing * matches a prefix (ctl:mixer.*).
  • Feature names and notes are our own words. No Adobe text, screenshots or assets belong here.

Summary

Section ✅ 🟡 ⬜ 🚫 P0 done P1 done
A. Import (IMP) 12 3 1 1 4/6 (67%) 5/5 (100%)
B. Library management (LIB) 22 2 1 2 9/9 (100%) 9/9 (100%)
C. Views & navigation (VIEW) 15 1 1 0 8/9 (89%) 4/4 (100%)
D. Search & filter (FILT) 11 1 1 0 4/4 (100%) 4/4 (100%)
E. Metadata (META) 5 1 0 0 2/2 (100%) 2/2 (100%)
F. Edit panel — global adjustments (EDIT) 43 2 3 1 28/28 (100%) 13/14 (93%)
G. Profiles (PROF) 7 1 3 0 3/4 (75%) 3/3 (100%)
H. Crop & rotate (CROP) 9 0 0 1 6/6 (100%) 3/3 (100%)
I. Remove / healing (REM) 8 1 2 2 4/4 (100%) 2/3 (67%)
J. Red eye (EYE) 2 0 0 0 — 1/1 (100%)
K. Masking (MASK) 13 5 5 0 7/8 (88%) 5/5 (100%)
L. Presets (PRE) 6 0 1 1 2/2 (100%) 2/2 (100%)
M. Versions & history (VER) 5 0 0 0 1/1 (100%) 3/3 (100%)
N. Copy / paste / sync (SYNC) 5 0 0 0 3/3 (100%) 1/1 (100%)
O. Merge (MERGE) 4 0 0 0 — —
P. Enhance (ENH) 0 0 2 0 — —
Q. HDR (HDR) 2 3 0 0 — —
R. Video (VID) 0 0 5 1 — 0/2 (0%)
S. Export (EXP) 15 4 0 0 7/7 (100%) 7/7 (100%)
T. Share (SHARE) 0 0 0 4 — —
U. Map & location (MAP) 0 1 1 0 — 0/1 (0%)
V. Preferences (PREF) 6 2 2 2 1/1 (100%) 4/5 (80%)
W. Cloud & AI infrastructure (CLOUD / AI) 1 0 1 2 — —
X. Cross-cutting behaviours (BEHAV) 23 6 1 1 8/8 (100%) 7/10 (70%)
Y. Menus 84 0 4 8 47/47 (100%) 23/23 (100%)
Z. Keyboard shortcuts (desktop) 76 3 3 1 49/52 (94%) 23/24 (96%)
Lightroom Classic extras 34 16 35 8 — 24/26 (92%)
Total 408 52 72 35 193/201 (96%) 145/157 (92%)

Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): 81.6% of 532 in-scope rows — P0 98.0% of 201 · P1 95.2% of 157 · P2 50.0% of 173.

Top gaps and shortcut notes

Moved on 2026-10-10: the ranked gaps are in gaps.md; shortcut conflicts, deliberate differences and missing bindings are in ui-parity.md.

A. Import (IMP)

Id Feature Tier Status Evidence Missing / notes
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; with several file types, a toggle per type checks or unchecks all of its files, e.g. the raws without the JPEGs beside them, issue #344) filling a resizable window (issue #337), transfer mode (add in place / copy / move, each explained), album (existing/new), preset, keywords; the folder scan runs on a background thread, shown in the activity stack with ✕ to cancel (a NAS folder no longer freezes the window); the import itself (probing, sidecars, copying / moving files) runs on a worker thread and its batches join the library between frames, shown in the activity stack with ✕ to cancel; one undo step. Drag-and-drop goes through the same background import. File dialogs list the importable extensions (lightcraft_engine::import::EXTENSIONS) in any letter case on Linux, where GTK and the desktop portal match globs case-sensitively (apps/lightcraft/src/dialog_filter.rs, issue #342; 0.4.0 hid .JPG / .NEF, issue #705). The folder scan, Browse Folder, drag-and-drop and the sidecar lookup match extensions in any case (crates/engine/src/tests_import.rs)
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); copies are verified (a new file, synced, checked against the content hash the scan computed — byte compare with the source when there is none, issue #134; 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
LR-IMP-MIGRATE Migrate other catalogs OOS 🚫
LR-IMP-PROFILES Import profiles & presets P1 ✅ cmd:file.importPresets, cmd:preset.import, cmd:profile.import, crates/engine/src/preset_import.rs, crates/engine/src/preset_luminar.rs presets: .lcpreset, XMP, classic .lrtemplate, photos carrying edits ("DNG presets"), Luminar looks (.lmp, .mplumpack collections; sliders with a counterpart), .zip bundles, folders (folder → group), drag & drop (an .xmp dropped together with its photo is that photo's sidecar and is not imported as a preset, issue #936); masks carried over; unmapped settings reported (fields that only describe the preset or photo, e.g. Cluster, SortName, SupportsAmount2, the as-shot white, empty Point Color slots, are not); profiles: .cube 3D LUTs (one toast reports both halves; a LUT that fails is named with its reason); Adobe profile formats deliberately unsupported
LR-IMP-LOCAL Work on files in place P0 ✅ cmd:library.browse, cmd:photo.addToLibrary, cmd:library.import (mode add), cmd:local.addRoot, cmd:local.hide, cmd:local.restoreHidden, crates/engine/src/cmd/browse.rs, crates/ui-egui/src/panels/left.rs (local_section), crates/ui-egui/src/panels/grid.rs (folder_header) Local: browse Pictures / Desktop / Downloads / Home or any folder without adding it (breadcrumb, Include subfolders, Add N to My Photos); browsed photos stay out of All Photos, albums and counts (album commands skip them and the album menus say to Add to My Photos first, issue #684); folders picked with Browse Folder… (or Keep in Local / Add to Local on a folder row) stay listed across restarts, and browsing a folder inside a listed one highlights it inside that tree (opened down to it, siblings reachable) instead of adding a root; edits go to XMP sidecars; importing promotes them; right-click a top-level location → Remove from Local hides the shortcut only (nothing on disk or in the catalog changes), saved with the UI state, and “Show N hidden locations” (or Browse Folder… on it) brings it back; a folder is one location however its path is spelled (/ or \, trailing separator, ./.., drive-letter case; folder_key in crates/catalog/src/query.rs); browsed photos nobody changed are forgotten once their folder has not been browsed for 30 days (Settings → Performance → Local folders, 0 = never; checked when the library opens, or cmd:library.forgetLocal with dryRun; files and sidecars stay, browsing again brings them back; see crates/catalog/src/local.rs for what counts as a change)
LR-IMP-SIDECAR-SPLIT Separate XMP sidecar variants P2 ⬜ cmd:library.xmpPreferences sidecar naming option exists (stem/full), no split sidecars
LR-IMP-FORMATS Supported formats P0 🟡 crates/codecs/src/lib.rs, crates/raw/src/lib.rs, crates/raw/src/jxl.rs JPEG, PNG, TIFF (1- to 64-bit integer, 16- to 64-bit float, incl. bilevel and CCITT fax), WebP, JXL, PSD, GIF, BMP (lossy wide-gamut JXL decodes to the right colours: jxl-oxide desaturated XYB rendered to P3/Rec. 2020, worked around in crates/codecs/src/jxl.rs); DNG (incl. lossy / Smart Preview DNG and DNG 1.7 JPEG XL tiles/strips: lossless ones decode bit-exactly on synthetic CFA and LinearRaw files, lossy ones are restored to the declared linear primaries without clipping or gamut mapping, checked on one real file with 108 lossy tiles, and any tile that fails to decode makes the file preview-only; DNG 1.7 row/column-interleaved rasters (RowInterleaveFactor / ColumnInterleaveFactor) are undone before the mosaic is read, so a file from Adobe's converter with a 2×2 interleave no longer decodes as four scaled-down copies in the wrong colours; a dimension the factor does not divide decodes with short trailing fields; verified on 4 real ILCE-6400 JPEG XL DNGs rendering bit-identical to their non-interleaved twins, factors above 2 rest on the plain reading of the tag; a single-chunk JPEG XL preview IFD is offered as the embedded preview; a DNG whose samples pile up at one value well below its stated WhiteLevel, as a camera app's 12-bit Bayer DNG that states 4095 and clips at 3567, has those samples raised to the white level on decode, so highlight reconstruction sees the clipped areas instead of white balance tinting them magenta, issue #548, crates/raw/src/saturation.rs), CR2 (colour-filter layout from the file's CR2CFAPattern tag, issue #85), ARW (pre-2017 bodies: white balance and black level from the encrypted SR2SubIFD, issue #148, its WB_RGGBLevels being the white balance the camera applied, issue #535; the DSLR-A900's packed 12-bit strip, two pixels per three bytes, read only when the strip is exactly 1.5 bytes per pixel, with its camera-JPEG window centred, checked on one CC0 file (A850 unmeasured); downsized Sony lossless YCbCr 4:2:0 / 4:2:2 tiles decoded to linear RGB, verified on private ILCE-7M4 3:2 & 4:3 examples including 16-bit full-resolution export; Sony DSC-R1 .SR2 decodes, its big-endian 16-bit words read in the order that fits the sample range, checked on the CC0 3221 sample; its crop and white balance live in the SR2SubIFD and are not read yet; ARW6 Compressed / Compressed (HQ) of the ILCE-7RM6 (crates/raw/src/vendor/arw6.rs, crates/raw/src/llvc.rs; 12-bit codes exact against a black-box oracle on 8 CC0 files, FF over the full frame, APS-C in the interior (LibRaw's own edge rows differ); output through our own companding curve, measured from CC0 lossless / ARW6 pairs; geometry checked against the camera JPEG, docs/arw6-compression.md; the ILCE-7M5 writes the same codec, unverified)), NEF (uncompressed, lossless and lossy compressed 12/14-bit, crates/raw/src/vendor/nefc.rs), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans; crates/raw/src/vendor/rafc.rs, full sensor verification on 13 bodies, docs/raf-compression.md), RW2 / Leica RWL / Panasonic RAW in every raw format (compressed 4 and 6, the prefix-coded strips of format 8 from the GH6 on, packed 2/5/7, the 16-bit words of the 2005–2007 bodies; black levels, in-camera aspect crops and mapped-out defect pixels; 178 CC0 files from 118 bodies checked, crates/raw/src/vendor/rw2.rs), PEF, ORF (uncompressed; colour-filter layout from the file's Exif CFAPattern; 12.8 bit/px 16-byte blocks of ten 12-bit samples + a zero pad byte, E-300 / E-330 / E-500 verified on CC0 files, E-M5 Mark II / PEN-F high-res files decode smooth but CFA and black unverified), Kodak DCS520C TIFF (a two-component 12-bit lossless JPEG strip, the two components the even and odd columns; the file's GrayResponseCurve table linearises the companded samples to 12 bits, black measured per CFA position, white balance from the KodakIFD; RawFormat::KodakDcs, crates/raw/src/vendor/kodak.rs, checked on the one CC0 sample against its IFD0 thumbnail), Hasselblad 3FR and FFF (a TIFF without a DNG version tag that describes its CFA IFD, levels, crop and white balance with DNG's tags; RawFormat::CfaTiff, read by dng::decode_as, RGGB when the file states no CFAPattern): uncompressed (X1D, X1D II, X2D, CFV 100C) and Hasselblad's lossless JPEG with scan selection value 8 (every FFF; H3D, H4D and CFV-50c 3FR), ljpeg::decode_hasselblad, its rule measured on 14 CC0 files with the ljpeg-explorer tool, Samsung SRW without compression (NX5, NX10, NX11 and the NX20-generation bodies NX20, NX200, NX210, NX1000 and NX1100 as 12-bit packed samples in either bit order, EX1 and WB2000 as 16-bit words; layout, camera-JPEG framing, levels and white balance from the maker note, crates/raw/src/vendor/srw.rs; median ΔE vs the camera JPEG 2.2–7.6 on 11 of the 12 files from 10 bodies checked, 10.7 (5.7 registered) on the backlit WB2000 sample). 12-bit NEF black level: maker note 0x003d is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). Lossy type 2 NEF curves of version 0x44 0x40 (D780, D6, Z 5, Z 6, Z 7, Z 7_2) span a quarter of the sample range, read from the table's own point spacing. Import probes (probe_info) of compressed NEFs decode only the first 128 rows, the ones the masked-trailing-column test reads in both the probe and the full decode (the D5100 and D7000 write 8 masked columns at 0 with no maker-note tag for them, compressed and uncompressed alike), instead of the whole Huffman stream: ~3 ms instead of the full decode time per file (386 ms on a 24 MP D750 file), issue #708. CR3 lossless CRX Bayer and version 0x100/0x200 C-RAW decodes to sensor pixels with timed white balance, black/white levels and file crop (EOS R6 Mark III ColorData version 66: as-shot white balance and black level from the file, white level from the data, docs/cr3.md) (the shot's aspect ratio from the maker note's AspectInfo); full sensor samples match the reference on M50/R100/R8 (crates/raw/tests/cr3_corpus.rs, docs/cr3.md). EOS R5 Mark II C-RAW and RAW decode too (the QP map's first-symbol literal and 23-zero escape, odd-height Bayer planes): 4 CC0 files pinned, every QP map of 33,665 private C-RAW files decodes exactly, and full-frame images match the camera JPEG. Unsupported coding variants retain metadata and use the embedded JPEG when available; HEVC preview decoding remains unsupported. Missing: CR3 unverified CRX variants, compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini: preview only with the reason), NEFs labelled compressed (34713) but stored uncompressed without a 0x0096 table (Z 6 packed 14-bit, D850 12-bit uncompressed), Nikon High Efficiency NEF (HE / HE★ from the Z 8, Z 9, Z 6III and Z f: compression 34713 with a JPEG XS codestream and no 0x0096 table; no public specification, so preview only with the reason "Nikon High Efficiency NEF", issue #193; Lossless compressed NEFs from the same bodies decode), NEF "lossy after split" (preview only: imported with preview_only = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, previewOnly in catalog.query; Reload clears it once the file decodes), TIFF-based raws whose raw data sits in a private block (Phase One / Leaf IIQ, Canon EOS-1D / 1Ds and the Kodak DCS Pro / DCS760C TIFFs (compression 65000): IFD0 is only a reduced RGB image next to a much larger declared picture) are recognised from that structure and import as preview_only with the reason, instead of opening as a thumbnail-sized ordinary image (thumbnail_shell in crates/raw/src/lib.rs), Recognised raws LightCraft cannot decode open as preview_only with any usable embedded JPEG, also Canon CRW, Minolta MRW and Sigma X3F (containers recognised by magic, preview found by a structural JPEG scan), Kodak KDC (IFD0 without ImageWidth), Leaf MOS and Epson ERF (scan; ones with no JPEG still fail with "without an embedded preview", AVIF decode (HEIC decodes only behind the non-default heif feature, which the release packages enable: crates/heif, heic-rs for the container and HEVC (bit-exact against libde265 on iPhone tiles) plus our own colour conversion, matching libheif within 1 code value on iPhone-style grids (VUI full range, Display P3 ICC, nclx, 10-bit, irot/imir/clap, gain map/depth ignored); 4:2:0/monochrome only, 4:2:2/4:4:4 Canon/Sony HIF refused; same policy as PhotoCraft), palette (indexed-colour) TIFF (refused by the tiff crate); transparency is flattened onto white, in linear light, where the working image is built (to_working in crates/codecs/src/lib.rs): renders, previews, exports and contact sheets no longer turn transparent areas black, issue #1212; median ΔE vs the camera JPEG 2.2–7.6 on 11 of the 12 files from 10 bodies checked, 10.7 (5.7 registered) on the backlit WB2000 sample). 12-bit NEF black level: maker note 0x003d is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). Lossy type 2 NEF curves of version 0x44 0x40 (D780, D6, Z 5, Z 6, Z 7, Z 7_2) span a quarter of the sample range, read from the table's own point spacing. Import probes (probe_info) of compressed NEFs decode only the first 128 rows, the ones the masked-trailing-column test reads in both the probe and the full decode (the D5100 and D7000 write 8 masked columns at 0 with no maker-note tag for them, compressed and uncompressed alike), instead of the whole Huffman stream: ~3 ms instead of the full decode time per file (386 ms on a 24 MP D750 file), issue #708. CR3 lossless CRX Bayer and version 0x100/0x200 C-RAW decodes to sensor pixels with timed white balance, black/white levels and file crop (the shot's aspect ratio from the maker note's AspectInfo); the Hasselblad Lunar, a Sony NEX-7 under Make HASSELBLAD, decodes as an ARW: a HASSELBLAD file with Sony's raw coding (compression 32767 or tag 0x7010) goes to the ARW reader, and the body for its Tag2010 layout comes from the maker note's SonyModelID, checked on the CC0 4051 file, issue #1275; an uncompressed MOS, whose IFD0 is the 16-bit mosaic with no Make tag, is recognised by Leaf's data block 0x8606 instead of opening as a greyscale image of its mosaic, issue #1275), and any decode failure of a recognised raw, not only an unsupported variant
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: ~55 CC0 corpus files plus 6 exact-sensor CR3 cases and 4 pinned EOS R5 Mark II files (plus ~60 NEFs checked by hand and 178 Panasonic / Leica RW2, RWL and RAW files from 118 bodies, checked against each file's own JPEG) 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; CR2 black levels were read from border columns light already reaches, left of SensorInfo's border, so CR2s rendered dark with crushed shadows (EOS 6D: 2591 instead of 2047), now measured from the masked columns only, checked on 8 corpus bodies; pre-2017 Sony ARWs opened green until their scrambled white balance / black level were read, issue #148, checked on 16 ARWs from 15 Sony bodies; their white balance now comes from the encrypted SR2SubIFD's WB_RGGBLevels, the gains the camera applied, which 14 bodies (DSLR-A450 … A700, NEX-3/5/C3, SLT-A33/A35/A55, ILCE-6001, UMC-R10C) had no other source for and which the maker-note estimate read before missed by up to 100 % on preset shots, issue #535, checked against ExifTool on 100 decoded ARWs from 87 Sony bodies; the encrypted SR2 black level is now read from its directory entry (the DSLR-A450/A500/A550/A700 read 354–975 instead of 512), checked on 126 ARWs from 97 Sony bodies, and downsized lossless M/S ARWs use their measured black level 1024 instead of 512, checked on 12 files from 4 bodies, issue #535, and on a fifth, the ILCE-7CM2 (issue #697: 0.5.0 rendered its lossless M file with that 512 pedestal, the darkest half of a dusk scene +11 L* and blue, the lamplight white; its darkest 0.1 % per channel sits at 1017/1020/1024, and with 1024 the render is within +3 L* of the camera JPEG; the body's compressed and uncompressed "M-size" files are Bayer at 4736×3132 with black 512, two public ones in the corpus); pre-2017 Sony bodies shot in the camera's 16:9 mode (ILCE-7SM2, DSLR-A580) now get the 16:9 crop of their camera JPEG instead of the whole 3:2 frame with padding columns, issue #535; Canon PowerShot and EOS M CR2s whose ColorData is stored as bytes opened green until the bytes were read as 16-bit words, 25 files from 25 bodies checked; private Z 8 crop verified; the Nikon D4, which records no CropArea, gets its image area and white level measured on 148 NEFs against the camera JPEG (optical-black stripes and magenta clipped highlights gone); the D7100, whose data holds picture beyond the camera's frame, develops at Lightroom's frame (the camera JPEG's size) in full frame and 1.3x crop, so Lightroom crops line up (#1248).; CR2 white levels came from masked rows holding samples at 16383, so on the EOS 50D, 60D, 450D (Rebel XSi), 500D (Rebel T1i), 550D (Rebel T2i), 1200D (Rebel T5) and 100D (Rebel SL1) clipped highlights sat at 81–98% of the range and were not treated as clipped, now taken from the active area only, issue #1272). Compressed RAF sensor arrays verified on 13 Fujifilm bodies (docs/raf-compression.md). ILCE-7RM6 ARW6 12-bit codes verified on 8 CC0 files (FF and APS-C, both compression modes). Sony ILCE-7CR (61 MP): lossless compressed L and M and compressed ARW2 samples of one scene in the corpus (black level, default crop, as-shot gains, lossless vs compressed mosaic agreement, fitted camera look), and all nine public codings (also uncompressed, small and APS-C crop) decode and fit. Grow the CC0 corpus per model and test each sample decodes with plausible colour. Nikon CropArea and untagged-preview colour-space regression tests; the Nikon D4, which records no CropArea, gets its image area and white level measured on 148 NEFs against the camera JPEG (optical-black stripes and magenta clipped highlights gone). Pentax PEFs without area tags (KP, KF, K-3 Mark III, K-70, K-1, K-1 Mark II, K-S1, K-S2, 645Z) no longer keep their masked border: black, empty and saturated lines are trimmed against the maker-note BlackPoint, issue #1271, measured on 38 CC0 PEFs from 30 bodies.; Canon PowerShot and EOS M CR2s whose ColorData is stored as bytes opened green until the bytes were read as 16-bit words, 25 files from 25 bodies checked). Compressed RAF sensor arrays verified on 13 Fujifilm bodies (docs/raf-compression.md). ILCE-7RM6 ARW6 12-bit codes verified on 8 CC0 files (FF and APS-C, both compression modes). Sony ILCE-7CR (61 MP): lossless compressed L and M and compressed ARW2 samples of one scene in the corpus (black level, default crop, as-shot gains, lossless vs compressed mosaic agreement, fitted camera look), and all nine public codings (also uncompressed, small and APS-C crop) decode and fit. Grow the CC0 corpus per model and test each sample decodes with plausible colour. Nikon CropArea and untagged-preview colour-space regression tests; the Nikon D4, which records no CropArea, gets its image area and white level measured on 148 NEFs against the camera JPEG (optical-black stripes and magenta clipped highlights gone). The Canon PowerShot G9, which writes no ColorData, takes its as-shot white balance from the WBInfo row (0x0029) of the preset ShotInfo names, issue #1273, checked on 1 CC0 file against neutral areas of the camera JPEG (within 0.1 %).; the Nikon D4, which records no CropArea, gets its image area and white level measured on 148 NEFs against the camera JPEG (optical-black stripes and magenta clipped highlights gone). The Olympus E-10 (no ImageProcessing directory) takes its as-shot gains and its black level (0x1012 in 0x102c ValidBits units: 128 at 12 bits) from the main maker note, issue #1273: its render against the camera thumbnail goes from ΔE76 18.5 with a rejected fit to 11.5 with the fit accepted (measured by AlexChambers).; the Nikon D4, which records no CropArea, gets its image area and white level measured on 148 NEFs against the camera JPEG (optical-black stripes and magenta clipped highlights gone), and the Nikon D90 likewise on 10 of 550 NEFs (42 filler columns on the right that developed as a pink band; green clips below red and blue).; the Nikon D4, which records no CropArea, gets its image area and white level measured on 148 NEFs against the camera JPEG (optical-black stripes and magenta clipped highlights gone).
LR-IMP-PREVIEW-READ Preview reads the header, not the raw P0 ✅ crates/raw/src/preview.rs (bounded_preview, embedded_preview_from_range), crates/engine/src/files.rs (preview_window), docs/raw-preview-reads.md, crates/engine/src/files.rs (preview_window, planned_preview), crates/raw/src/sparse.rs (preview_plan) a grid thumbnail and a loupe preview of an unedited raw show the camera's embedded JPEG, which sits near the start of the file (a 24 MP Fuji RAF keeps its 5.6 MB JPEG in a 56 MB file), so the preview loader reads the header and that JPEG instead of the whole file: 56.5 MB → 5.9 MB and ~10 s → 0.96 s measured on one real RAF over a GVFS/SMB Wi-Fi share (byte-identical preview pixels; LIGHTCRAFT_PROFILE=1 reports the bytes); the RAF pointer is read from its fixed header fields, a CR3's boxes and a TIFF raw whose preview tag sits past the prefix are read through lightcraft_raw::sparse::preview_plan, which names the ranges embedded_preview reads and nothing else; a cached render as stand-in, an edited photo and a raw without an embedded JPEG still render; the formats it cannot describe (CRW/MRW/X3F are found by scanning, NEF/ORF/PEF probe from samples) keep the whole-file path, and a lying pointer, a short read or a file that changes underneath us falls back to it rather than losing the preview. An unedited raw's grid thumbnail is that camera JPEG itself, so no sensor decode is scheduled behind it (Session::embedded_thumb_is_final, grid::skips_the_real_render): drawing a 512 px thumbnail used to decode the whole raw — 45 s a photo at 1.2 MB/s, 8.3 GB for a 149-raw folder — and the queue it built delayed the render the user was waiting for
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); the raw copy is removed only once its DNG is verified and on disk (issue #106)

B. Library management (LIB)

Id Feature Tier Status Evidence Missing / notes
LR-LIB-ALLPHOTOS All photos P0 ✅ cmd:library.source, crates/ui-egui/src/panels/left.rs
LR-LIB-RECENT-ADDED Recently added P1 ✅ cmd:library.source (recentlyAdded), crates/engine/src/view.rs (RECENT_DAYS) photos imported in the 30 days up to the latest import, newest import first, grouped by import day
LR-LIB-BYDATE Browse by date P1 ✅ cmd:library.filter (date), crates/ui-egui/src/panels/left.rs (date_row), crates/catalog/src/query.rs (date_groups) year → month → day tree with library-wide counts; a row filters the current source by that date. Active filters show as removable chips under the grid header (crates/ui-egui/src/panels/chips.rs, with “+N more” and Clear all), the header counts “N of M photos” (matching of the source's total), a badge on the top-bar filter icon counts them, and an empty result says no photos match the active filters
LR-LIB-ALBUM Albums P0 ✅ cmd:album.create, cmd:album.rename, cmd:album.delete, cmd:album.addPhotos, cmd:album.removePhotos, cmd:dialog.newAlbum, crates/engine/src/tests_albums_local.rs, crates/ui-egui/src/tests_panels.rs photos dragged from the grid onto an album row are added to it (LR-BEHAV-DRAGDROP); albums hold library photos (issue #684): album.create {addSelected}, album.addPhotos and album.toggleTarget (B) skip browsed Local photos and return skipped (a toast says so), a Local-only selection is an error and leaves Photo ▸ Add to Album, the grid's right-click Add to Album, a folder row's Add Selected Photos and a drop onto an album row off with the hint to Add to My Photos first (button:contextAddToAlbum, button:contextAlbum:<id>); right-click a photo ▸ Add to Album lists the albums and ends with New Album…, which creates one holding the selection — also when the library has no album yet (issue #676; button:contextNewAlbum); New Album's "Include selected photos" (check:albumIncludeSelected, on by default; dialog.newAlbum {includeSelected}) leaves the selection out when unticked, and, as in Lightroom, is only shown when something is selected (never for a folder)
LR-LIB-FOLDER Folders of albums P0 ✅ cmd:album.create (folder, parent), cmd:album.move, cmd:album.reorder, cmd:album.sort, cmd:dialog.newAlbum / cmd:dialog.newFolder / cmd:dialog.smartAlbum / cmd:dialog.newSmartAlbum (parent), crates/ui-egui/src/panels/left.rs (albums_tree, folder_menu, disclosure_triangle), crates/ui-egui/src/tests_panels.rs Delete removes a folder and its nested albums as one undoable edit, keeps photos in the library and returns a deleted album view to All Photos (issue #501); folders nest and fold with the same disclosure triangle as Local / By Date / Folders / Keywords (albumToggle:<id>; the row folds them too; open by default); right-click a folder ▸ New ▸ Create Album… / Create Smart Album… / Create Smart Album from Filter… / Create Folder… creates inside it (the dialog.new* commands take parent), opening the folders down to the new row; creating is only allowed inside folders; moving: drag an album or folder row onto a folder (outlined when allowed; a closed folder opens after 0.6 s under the pointer; the "Albums" header takes an item that sits in a folder back to the top level while it is dragged), the Move to menu or album.move — one undo step each; not onto a plain album, itself or something inside it; drop on the top / bottom half of an album or the edge of a folder to place it there by hand (cmd:album.reorder, a line shows the place; ordered per folder, folders stay first, new albums go after the ordered ones; right-click ▸ Sort Contents A–Z or + ▸ Sort Albums A–Z = cmd:album.sort)
LR-LIB-SMARTALBUM Smart albums P1 ✅ cmd:album.createSmart, cmd:album.setRules, crates/catalog/src/query.rs, crates/ui-egui/src/panels/filterbar.rs, crates/ui-egui/src/lib.rs (album_counts) saved filters (rating/flag/label/kind/edited/keyword/camera/lens/date range/text/album) or a rule set from the rule editor (match all / any / none, nested groups; see LRC-LIB-SMARTCOLL), live; sidebar counts are cached per catalog revision (and per minute while an “in the last…” rule is used), counted without building id lists
LR-LIB-SHARED-ALBUM Shared albums P2 ⬜ needs a sharing service
LR-LIB-OFFLINE Keep album offline P2 🚫 not applicable: local-first library
LR-LIB-TARGET Target album P2 ✅ cmd:album.setTarget, cmd:album.toggleTarget see LRC-LIB-COLLECTIONS
LR-LIB-RATING Star ratings P0 ✅ cmd:photo.rate (advance), cmd:photo.decreaseRating, cmd:photo.increaseRating, crates/ui-egui/src/shortcuts.rs 0 clears; 1–5 set; Shift+0–5 applies and advances once, including with Auto Advance. Default grid brackets adjust each photo by one, clamped to 0–5; one batch undo and AutoWrite. Batch grid and undo/redo regressions; macOS native ownership corrected (PR #261)
LR-LIB-FLAG Pick / reject flags P0 ✅ cmd:photo.pick, cmd:photo.reject, cmd:photo.unflag, cmd:photo.flag P pick, X reject, U clear; Shift+P picks and advances in both grids (Presets elsewhere); Shift+X/U and legacy Shift+Z advance; no flag cycle; rejection does not delete the file
LR-LIB-LABEL Colour labels P1 ✅ cmd:photo.label, cmd:label.setNames, cmd:label.names, cmd:dialog.labelNames, keys 6–9 in crates/ui-egui/src/shortcuts.rs Photo menu and grid context menu (coloured, named), Info-panel swatches, label dot plus translucent colour on square-grid/filmstrip surrounds and photo-grid footers (photo pixels unchanged); colour-label toasts have pale matching backgrounds; setting/clearing through keys (including Shift), menus or Info swatches shows the same bottom toast as ratings, with the colour/custom name; editable label names (undoable, journaled); no purple key (as in Lightroom), no custom extra labels
LR-LIB-KEYWORD Keywords P0 ✅ cmd:panel.keywords, cmd:photo.setMeta (addKeywords/removeKeywords), cmd:keyword.list, cmd:keyword.rename, cmd:keyword.delete, cmd:keyword.merge, cmd:keyword.suggest, crates/catalog/src/keywords.rs, crates/ui-egui/src/panels/left.rs left-panel keyword tree with counts (click filters, children included; context menu rename / merge / delete / add / remove); library-wide ops are one undo step and replay from the op log; suggestions (co-occurring / most used / completions) in the Keywords panel; keywords reorganize by drag and drop in the Keyword List (LRC-LIB-KEYWORDS)
LR-LIB-PEOPLE People / faces P2 🟡 cmd:faces.detect, cmd:view.people, cmd:view.faceBoxes, cmd:photo.removeRegion, cmd:photo.setRegion, cmd:library.filter, crates/ui-egui/src/panels/people.rs, crates/catalog/src/query.rs (people), crates/engine/src/media.rs (face_job), crates/meta/src/xmp.rs MVP: named face regions read from XMP (MWG-RS, read-only, see docs/xmp-interop.md; clipped to the photo; Lightroom's Rotation of ±π/2 and π handled, mirrored orientations taken as written; boxes follow Rotate Left/Right and flips; a sidecar stating mwg-rs:Regions, even empty, replaces the photo's on re-read, one without leaves them) become cards in the People view (View ▸ People, or the People button in the toolbar): a close-up of the person's largest face, name and photo count; a click filters the grid (person in library.filter, person: search token, filter chip). Face boxes in the loupe can be switched off (View ▸ Face Boxes, toolbar) resized with eight drag handles and removed with the × shown on hover (catalog-only and undoable, one undo step per drag; the sidecar is never rewritten for either); no moving a box by dragging it yet. Photo ▸ Detect Faces (cmd:faces.detect) finds faces with the YuNet detector (pure Rust, about 200 ms a photo, matches OpenCV's own YuNet on a 45-photo test set; the 232 KB model is a one-time download offered in the same step, never part of LightCraft) and adds them as unnamed boxes; names are suggested from the faces already named (recognition models are opt-in downloads, run on the CPU in a background thread; a face is named only when you confirm it); a person's page shows their cropped faces and the unnamed faces that look like them (LRC-LIB-PEOPLE); with recognition on, a background scan finds faces in photos that have none and embeds every face (one to three worker threads, raws through their embedded preview); an Unnamed faces section orders faces by likeness and names a selected group at once; no separated clusters, no manual tagging, no writing regions back
LR-LIB-STACK Stacks P1 ✅ cmd:stack.group, cmd:stack.ungroup, cmd:stack.toggle, cmd:stack.setTop, cmd:stack.remove, cmd:stack.auto, crates/catalog/src/stacks.rs grid/filmstrip count badges, expand/collapse, auto-stack by capture time; no visual-similarity auto-stack
LR-LIB-VERSIONS Versions P1 ✅ cmd:version.create see section M
LR-LIB-DELETE Delete / Recently Deleted P0 ✅ cmd:photo.delete, cmd:photo.restore, cmd:photo.deletePermanently Delete and Delete Permanently retain surviving selected photos; if none remain, select the next visible survivor (previous at the end) in the current source/filter/sort; permanent deletion removes catalog records, not files on disk; Restore and Delete Permanently in the Photo menu and the photo context menus for photos in Recently Deleted (before, the UI had no way to restore); importing a file that is in Recently Deleted again restores the photo with its edits, rating and albums (issues #298, #706: before, the import skipped it as "already in the library" while the grid showed it deleted) — the review dialog checks it, badges it "Will be restored", counts it as "in Recently Deleted" rather than "already in the library", and its choice Restore them / Import as new / Leave them starts on Restore; with Leave them such a file starts unchecked and Check All and the file-type toggles leave it out, but a click still checks it and it is restored (issue #1285: before, the click did nothing and the "In Recently Deleted" badge was drawn over the checkbox, which now sits above it); dropped files restore too ("Restored 1 photo from Recently Deleted"); a deleted photo matched by content whose file is gone is relinked to the imported file; Photo ▸ Empty Recently Deleted (cmd:library.emptyRecentlyDeleted, one undo step, shown while the trash is the source); import takes onDeleted restore (default) / skip / fresh for files in the trash (cmd:library.import; the report lists restored, and with skip flags the duplicate existingDeleted); browsing a folder in Local (cmd:library.browse), Auto Import and a Lightroom catalog import never restore a deleted photo; no confirmation dialog, no auto-purge after N days (needs a deleted-at date in the catalog); Delete asks first ("Move this photo to Recently Deleted?") from the key, the Photo menu and the photo context menu, on by default and switched off in Settings ▸ General (#1239: before, it was off by default and the context menu never asked); no auto-purge after N days (needs a deleted-at date in the catalog)
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 (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”. Impossible calendar dates cannot anchor relative shifts; rejection preserves metadata, selection, Undo and Redo.
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
LR-LIB-CULL Assisted culling P2 🟡 cmd:photo.analyze, cmd:dialog.cull, crates/pipeline/src/cull.rs Photo ▸ Assisted Culling…: focus score (0–100), clipping, burst grouping (look-alike shots within 10 s) with the sharpest marked; reject below a focus score, pick each burst's best; classical measures — no eyes-closed / expression detection
LR-LIB-ACTIVITY Comments & likes OOS 🚫
LR-LIB-QUICKCOLL Quick collection [Classic] P2 ✅ cmd:album.toggleTarget, cmd:album.clearQuick B in the grids
LR-LIB-VIRTUALCOPY Virtual copies [Classic] P1 ✅ cmd:photo.virtualCopy, crates/engine/src/cmd/organize.rs “Copy N” badge, stacked with the original, same albums, no XMP writes; no “Set Copy as Master”

C. Views & navigation (VIEW)

Id Feature Tier Status Evidence Missing / notes
LR-VIEW-PHOTOGRID Justified photo grid P0 ✅ cmd:view.photoGrid, cmd:library.groups, cmd:library.sort (group), crates/ui-egui/src/panels/grid.rs, crates/catalog/src/dates.rs grouped by capture / import / edit date (day headers “Wednesday, 30 September 2026 · 12 photos”, months or years when zoomed out, or a fixed day/month/year/none choice in the sort menu); sticky header; clicking a header selects its photos; stacks never split; scrolls to the active photo only when it changes (or the grid comes back on screen), never on its own
LR-VIEW-SQUAREGRID Square grid P0 ✅ cmd:view.squareGrid same date headers as the photo grid
LR-VIEW-DETAIL Single-photo view P0 ✅ cmd:view.detail, crates/ui-egui/src/panels/detail.rs a “Rendering…” pill at the bottom of the photo (widget loupe:rendering; above the Navigator when that is in the way) once a render has been on its way for 0.5 s with nothing new on screen: over a stand-in (the raw's embedded JPEG, a thumbnail), the previous render after an edit, or, zoomed in, the magnified frame waiting for its window; Before views watch their own renders. Always on (Lightroom Classic makes it a View Options choice). When the render fails behind a stand-in, a caution pill says “This photo can't be opened” (“Original offline · no smart preview” when that is why) with the reason on hover and for screen readers (widget loupe:renderFailed; ui.inspect → loupe.failed)
LR-VIEW-EDIT Edit view P0 ✅ cmd:panel.edit
LR-VIEW-FULLSCREEN Full-screen preview P1 ✅ cmd:view.fullScreenPreview, cmd:view.enterFullScreen photo on black, arrows step, Esc exits; ⇧⌘F window full screen
LR-VIEW-FILMSTRIP Filmstrip P0 ✅ cmd:view.filmstrip, crates/ui-egui/src/panels/detail.rs the photo context menu on right-click; the mouse wheel scrolls it sideways; it follows the active photo only when that changes (centred if off screen), so a scrolled strip stays put; every selected photo is highlighted, the active one framed in white, as in the grid
LR-VIEW-ZOOM Zoom & pan P0 🟡 cmd:view.zoomFit, cmd:view.zoom100, cmd:view.zoomIn, cmd:view.zoomOut, cmd:view.zoomToggle, cmd:view.clickZoom, cmd:view.navigate, crates/ui-egui/src/panels/detail.rs, crates/ui-egui/src/headless.rs continuous native pinch / modifier-wheel zoom (Fit–800%, anchored under the pointer), two-finger scroll / touch pan with edge limits; Detail (including tools, Before/After and full-screen preview), Compare and Reference; fractional percentages preserve small gestures; image, picker and targeted-adjustment cursors stay steady between gesture events; keyboard steps 25–800 % (not 6–1600 %); Fill in the bottom bar; click / Z / Space eases to the chosen ratio (1:1 default, 2:1, 3:1, 4:1, 8:1). The loupe's cost follows the canvas, not the zoom (issue #323): the whole frame is rendered at about canvas size (at most the 2560 px preview source level unless a larger Preview size is chosen, and never above the GPU's texture side), and once the view is zoomed well past that (more than 25 %) the window on screen is rendered too, cut from a frame of at most the photo's own pixels, so beyond 100 % the GPU magnifies it (region::plan, RenderRequest::window, Session::region_job, crates/ui-egui/src/region.rs); 1:1 and 2:1–8:1 are sharp at any photo size. A slider tick takes 7–12 ms on a 1400 × 900 canvas and 11–25 ms on a 2800 × 1700 one at 100 % and 400 % of a 24 MP photo (M1 Max, GPU, profile_slider_drag); about 20–25 ms at 100 % on the big canvas is over a 16 ms frame. A continuous pinch holds the render sizes (and the windows there are) until it is quiet. A fit view on a canvas up to about 3200 px (a Retina Mac) never decodes the original; larger displays get a window at fit, which does. Modes that draw another picture over the loupe (mask overlays, soft proofing) and photos whose spots or Auto Mask strokes read too far for one window (only photos longer than 8192 px) render the whole frame at the size it is drawn, as before windows; while a preset or profile is hovered, or over a picture that is not the frame (an unsupported raw's embedded JPEG), the whole frame stays canvas-sized. Before/After (side by side, stacked, \\ and both wipes) gets the same windows on both sides, clipped to their pane or side of the line. Windows render on the GPU like whole frames (equivalence-tested against the CPU), in the browser build too (windows no wider than its GPU's texture side, at most 3072 px), and are not made while hovering a look, soft-proofing or showing an overlay, nor over a picture that is not the frame (an unsupported raw's embedded JPEG with another crop). No texture is larger than the GPU allows. Spots and Auto Mask brushes work while what a window reads fits one render (8192 px along an axis, 4096 in the browser): the window grows to hold the spot and its source patch, or the dabs near it, and if that would take more (a clone from far away at 8:1) the loupe keeps the whole-frame render instead, and spots with an automatic source pick it from a fixed-size render of the whole frame (so a window's choice can differ from a whole-frame render's, which picks at its own resolution). The wide stages (dehaze, clarity, highlights/shadows base) and Refine Edges on masks read a window margin capped at 768 px; measured on hard-edged blocks that costs at most a few levels at 8:1. the histogram and Navigator come from the whole-frame render. Compare and Reference get the same windows, one per pane (Slot::CompareRegion, panels/compare.rs), over each pane's whole-frame render (at most 2560 px); while either view is open two originals stay decoded (MediaCache::set_full_capacity), so one pane's window does not evict the other's source; a pinch, or a slider drag on the active photo in Reference, keeps the windows there are until it ends. Still 🟡: mask overlays and soft proofing have no window (a full-size whole-frame render at 1:1)
LR-VIEW-NAVIGATOR Navigator mini map P1 ✅ cmd:view.navigator, crates/ui-egui/src/panels/detail.rs shown while zoomed (bottom right); click/drag pans
LR-VIEW-BEFOREAFTER Before / after P0 ✅ cmd:view.showOriginal, cmd:view.beforeAfter, cmd:view.beforeAfterSplit, cmd:view.beforeAfterTopBottom, cmd:view.beforeAfterSplitTopBottom, cmd:beforeAfter.setBefore, cmd:beforeAfter.copyAfterToBefore, cmd:beforeAfter.copyBeforeToAfter, cmd:beforeAfter.swap, cmd:beforeAfter.resetBefore all four layouts; before = the import state (defaults + import preset) or a chosen history step / version / the current settings; copy and swap (View → Before/After Settings, History row menu). The chosen before lasts for the session; in side by side and top / bottom, clicks with the white-balance selector, colour samplers and targeted adjustment work in the Before view too (crates/ui-egui/src/tests_before_after.rs). Zoomed in, the side by side and stacked views each stay in their pane, with their labels under it
LR-VIEW-COMPARE Compare two photos P1 ✅ cmd:view.compare, cmd:compare.swap, cmd:compare.makeSelect, cmd:compare.set, crates/ui-egui/src/panels/compare.rs, crates/ui-egui/src/tests_compare.rs select / candidate, synced zoom + pan, arrows move the candidate; a filmstrip click puts the photo into the active side (a photo on the other side swaps), so the active photo is always one of the two shown; 1:1 to 8:1 sharp on both sides (zoom windows, see LR-VIEW-ZOOM); no zoom-link toggle; each pane says “Rendering…” (widget compare:rendering:<pane>) or “This photo can't be opened” (compare:renderFailed:<pane>) as the loupe does
LR-VIEW-SURVEY Survey view [Classic] P2 ✅ cmd:view.survey, crates/ui-egui/src/panels/compare.rs selection tiled (≤ 48), keys act on the active photo, hover × removes
LR-VIEW-INFOOVERLAY Info overlay on the photo P1 ✅ cmd:view.infoOverlay off / file + date + size / exposure + camera; ⌘I cycles (I in full screen; elsewhere I stays the Info panel)
LR-VIEW-SLIDESHOW Slideshow P2 ✅ cmd:view.slideshow View ▸ Slideshow (⌥⌘↩): the photos in view full screen, every 4 s (interval), wrapping; Space pauses, ←/→ step, Esc ends
LR-VIEW-SECONDWINDOW Second display window [Classic] P2 ✅ cmd:view.secondWindow, crates/ui-egui/src/panels/second.rs Window ▸ Second Window (⌘F11): the active photo fitted in its own native window with its own render (a floating panel where there are no native windows); loupe view only (no grid / compare / survey there)
LR-VIEW-CLIPPING Clipping indicators P0 ✅ cmd:view.clipping
LR-VIEW-HISTOGRAM Histogram P0 ✅ cmd:view.histogram, crates/ui-egui/src/panels/edit.rs, crates/pipeline/src/lib.rs (hdr_histogram) HDR edits: SDR on the left half, the HDR range (0 to 5 stops above SDR white, a fixed scale) on the right with the headroom limit marked; colour bar (white, or Visualize HDR range's colours when that is on) and SDR / HDR labels; no drag-to-adjust on the histogram; it follows slider drags live (a small render beside the drafts, Slot::HdrHistogram) and never drops to the SDR layout mid-drag
LR-VIEW-HDR-DISPLAY HDR display output P2 ⬜ , apps/lightcraft/src/hdr_display.rs (choose, preferred_hdr_format)

D. Search & filter (FILT)

Id Feature Tier Status Evidence Missing / notes
LR-FILT-SEARCH-META Text search P0 ✅ cmd:library.filter (text), crates/ui-egui/src/panels/topbar.rs, crates/catalog/src/query.rs fielded tokens (rating:3, iso:>800, camera:…); no suggestions dropdown
LR-FILT-SEARCH-AI Natural-language search P2 ⬜
LR-FILT-RATING Rating filter P0 ✅ cmd:library.filter (rating, ratingOp), crates/ui-egui/src/panels/filterbar.rs ≥ / = / ≤ stars in the filter bar (cmd:view.filterBar)
LR-FILT-FLAG Flag filter P0 ✅ cmd:library.filter (flag), crates/ui-egui/src/panels/filterbar.rs picked / rejected / unflagged (one at a time)
LR-FILT-LABEL Colour-label filter P1 ✅ cmd:library.filter (label), crates/ui-egui/src/panels/filterbar.rs one label at a time; no “no label” choice
LR-FILT-TYPE Type / edited filter P1 ✅ cmd:library.filter (kind, merged, edited), crates/ui-egui/src/panels/filterbar.rs, crates/catalog/src/query.rs (merged_kind) photos / raw / videos / HDR / panoramas / HDR panoramas (merge results by name), edited / unedited; no depth kind
LR-FILT-KEYWORD Keyword filter P1 ✅ cmd:library.filter (keyword), crates/ui-egui/src/panels/filterbar.rs keyword picker
LR-FILT-CAMERA Camera / lens filter P1 ✅ cmd:library.filter (camera, lens), crates/ui-egui/src/panels/filterbar.rs camera and lens pickers
LR-FILT-LOCATION Location filter P2 ✅ cmd:library.filter (text, ruleSet field location) free text, or a rule on location / city / state / country (smart albums, library.filter)
LR-FILT-PEOPLE People filter P2 🟡 cmd:library.filter, crates/catalog/src/query.rs filter by a person's name (case-insensitive, named Face regions only) from the People view, a filter chip or person:; no unnamed / suggested people
LR-FILT-CULL Culling-score filters P2 ✅ cmd:library.filter (ruleSet fields sharpness, bestOfGroup) Focus and Best of Similar Shots in the rule editor / smart albums
LR-FILT-SORT Sort P0 ✅ cmd:library.sort, cmd:library.shuffle, crates/ui-egui/src/panels/bottombar.rs Random sort (seeded, stable under edits; choosing Random or Reshuffle picks a new seed); the shuffle is part of the saved view state; no effect in Recently Added (always newest import first); no colour-label or custom (manual) order
LR-FILT-SAVED Filter presets [Classic] P2 ✅ cmd:filter.savePreset, cmd:filter.applyPreset, cmd:filter.presets, cmd:filter.deletePreset, crates/ui-egui/src/panels/filterbar.rs filter bar → Presets: apply, save current filter, delete (right-click); saved with the library

E. Metadata (META)

Id Feature Tier Status Evidence Missing / notes
LR-META-INFO Info panel P0 ✅ cmd:panel.info, cmd:photo.setMeta, crates/ui-egui/src/panels/right.rs (info, camera_card) camera card (camera, lens, size, format, focal length / shutter / aperture / ISO); title, caption, alt text, extended description, copyright, copyright status, rights usage terms, copyright info URL, creator; file name (rename), file path (reveal), capture time (edit); location, city, state, country; GPS. No flash, map snippet or people
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), cmd:metadata.reverseGeocode 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; Fill Location fills empty City / State / Country from GPS offline (GeoNames, downloaded on request: cmd:metadata.downloadPlaceNames); no embedded map
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; 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); a crs: custom white balance on a relative-WB raw is shifted from crs:AsShotTemperature rather than read as Kelvin, or kept As Shot and reported when the packet has no as-shot white (issue #510); heal and clone spots (crs:RetouchAreas, legacy crs:RetouchInfo) are read into our spots (crates/engine/src/crs_masks.rs, see docs/xmp-interop.md → Spot removal); Read Metadata from File never writes the sidecar back, even with auto-write on, so another app's later crs: edits are still read (issue #934)
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)

Id Feature Tier Status Evidence Missing / notes
LR-EDIT-AUTO Auto settings P0 ✅ cmd:develop.auto, crates/pipeline/src/auto.rs a Lightroom-like recipe calibrated black-box on Lightroom's Auto results for 24 Sony raws: Exposure from the scene's scene-linear log-average luminance (Lightroom's observed slope, -0.41 EV per EV), Highlights -66 and Shadows +52 (Lightroom's average Auto values), Contrast +68 and Vibrance +30 fitted on our scales. Held out one photo at a time: on the 16 photos whose start Sony's DRO doesn't brighten, a median dE76 of 6.4 from Lightroom's Auto results against 8.3 unedited; on all 24, 7.7 against 7.9 (issues #318, #209). Calibrated on Sony files only; Whites and Blacks stay 0
LR-EDIT-BW Black & white P0 ✅ cmd:develop.treatment
LR-EDIT-HDR-MODE HDR editing P2 ✅ cmd:develop.hdr, ctl:hdr.maxEv, crates/ui-egui/src/panels/edit.rs (hdr_row) HDR button next to B&W in the Edit panel header, as Lightroom (Visualize HDR and the headroom limit in the Light panel while it's on); see Q. HDR
LR-EDIT-LIGHT-EXPOSURE Exposure P0 ✅ ctl:light.exposure
LR-EDIT-LIGHT-CONTRAST Contrast P0 ✅ ctl:light.contrast
LR-EDIT-LIGHT-HIGHLIGHTS Highlights P0 ✅ ctl:light.highlights, crates/raw/src/highlight.rs sensor-clipped highlights stay neutral when Highlights (or Exposure, Whites) darkens them: fully clipped content is rebuilt to the neutral of the colour model applied after it, partly clipped content takes its colour from reliable unclipped content nearby (not from dark objects, lens fringes or another surface beside it) and white where there is none, and pale partly clipped content (a white sky) turns white on its way to clipping (#523); binned previews and thumbnails decide clipping per sample before averaging (develop_binned_masked, highlight::reconstruct_masked), so a block with one clipped specular sample keeps its mean instead of jumping to the clip level, which reconstruction turned into green speckles at 500 px (#548) From process V2 the Highlights/Shadows base is the edge-aware filter of the 0.15 %-of-long-edge median of log luminance: twigs and fine texture take their surroundings' shift, so bright areas keep their local contrast as with Exposure (V1's base kept ~0.6 of it at −100, #632); region edges stay sharp
LR-EDIT-LIGHT-SHADOWS Shadows P0 ✅ ctl:light.shadows From process V2, the same base as Highlights: dark areas keep their local contrast when lifted (#632)
LR-EDIT-LIGHT-WHITES Whites P0 ✅ ctl:light.whites
LR-EDIT-LIGHT-BLACKS Blacks P0 ✅ ctl:light.blacks
LR-EDIT-LIGHT-CURVE-PARAM Parametric curve P0 ✅ ctl:curve.highlights, ctl:curve.lights, ctl:curve.darks, ctl:curve.shadows, ctl:curve.split*
LR-EDIT-LIGHT-CURVE-POINT Point curve P0 ✅ cmd:develop.curve, cmd:curve.reset, cmd:curve.presets, cmd:curve.applyPreset, cmd:curve.savePreset, cmd:curve.deletePreset, cmd:curve.importPresets, cmd:curve.exportPresets, cmd:file.importCurvePresets, cmd:file.exportCurvePresets, crates/engine/src/cmd/curves.rs, crates/ui-egui/src/panels/edit.rs click to add a point; drag a point in both axes (between its neighbours, input / output readout, one undo step per drag; ⌥-drag moves it a tenth as far for fine adjustment, issue #1363); drag empty space to add and drag; double-click removes; reset: double-click a channel selector (that channel), the Reset button under the graph (every curve incl. parametric) or right-click the graph (channel / all); Point Curve preset dropdown: own Linear / Medium Contrast / Strong Contrast, user presets (save the current point curves, delete by right-click; saved with the library), import / export as .lccurve JSON
LR-EDIT-LIGHT-CURVE-RGB Per-channel curves P0 ✅ cmd:develop.curve (channel)
LR-EDIT-LIGHT-CURVE-REFINESAT Curve saturation compensation P1 ✅ ctl:curve.refineSaturation
LR-EDIT-LIGHT-CURVE-TAT Drag-on-image curve adjust P1 ✅ cmd:develop.targeted (target: curve)
LR-EDIT-COLOR-WB-PRESET White-balance presets P0 ✅ cmd:develop.wb
LR-EDIT-COLOR-WB-PICKER White-balance eyedropper P0 ✅ cmd:tool.wbPicker, cmd:develop.wbPick, crates/ui-egui/src/tests_wb_selector.rs the Edit panel's selector button does what W does (from Compare, Survey or the grid it opens the photo in Detail); no magnified loupe while picking
LR-EDIT-COLOR-TEMP Temperature P0 ✅ ctl:wb.temp, crates/pipeline/src/local.rs (wb_matrix_for, legacy_white), crates/raw/src/color.rs (rebalance), crates/raw/src/spectral.rs (find_camera), crates/catalog/src/model.rs (relative_wb_camera), crates/develop/src/controls.rs (ControlSpec::position), crates/raw/src/kelvin_presets.rs Kelvin (2000–50000 K, As Shot read from the file) on raws with colour matrices: in the file (DNG: re-developed for the chosen white through the camera's own matrices, camera-space white balance as Lightroom; the eyedropper / Auto invert the same model) or measured for the camera model (lightcraft_raw::spectral, 52 bodies, issue #730: the matrices turn the as-shot multipliers into Kelvin and the slider back, the look stays the file's fit, and the picture is adapted from the as-shot white, Bradford), or read from the camera's own white-balance presets (Sony ARW SR2SubIFD, Sony ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S: Sony's Kelvin dial reads back within 0.3 % on 5 settings of 4 bodies); raws without matrices anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). The Kelvin slider's track is Lightroom's: 7000 K in the middle, 2000–7000 K evenly on the left half, logarithmic up to 50000 K on the right (within 2 % of positions measured in Lightroom Classic), for drawing, clicks and drags; wheel / arrow-key nudges and Quick Develop still step a fixed 50 / 100 / 500 K; the relative slider stays linear. Settings written on the relative scale before a camera's matrices reached it keep their look (WbScale). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open; raws without matrices or presets anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). Settings written on the relative scale before a camera's matrices reached it keep their look (WbScale). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open
LR-EDIT-COLOR-TINT Tint P0 ✅ ctl:wb.tint, docs/tint-direction.md DNG / Lightroom sign: negative (left) adds green, positive (right) magenta; XMP crs:Tint values render as in Lightroom (issue #188); CPU/GPU regression tests
LR-EDIT-COLOR-VIBRANCE Vibrance P0 ✅ ctl:color.vibrance
LR-EDIT-COLOR-SATURATION Saturation P0 ✅ ctl:color.saturation
LR-EDIT-COLOR-MIXER-HSL 8-band colour mixer P0 ✅ ctl:mixer.* no targeted (drag-on-image) mode
LR-EDIT-COLOR-MIXER-BW B&W mix P1 ✅ ctl:bw.*, cmd:develop.autoBwMix eight bands; Auto pushes each hue band's colourful pixels away from the mean lightness (own rule)
LR-EDIT-COLOR-POINTCOLOR Point colour P1 ✅ cmd:pointColor.pick, cmd:pointColor.delete
LR-EDIT-COLOR-GRADING Colour grading wheels P0 ✅ ctl:grading.*, crates/ui-egui/src/panels/edit.rs (grading) 3-Way / Shadows / Midtones / Highlights tabs; a range tab shows one large wheel with Hue, Saturation and Luminance sliders; Blending and Balance in every tab; 3-Way shows the hovered wheel's hue and saturation; every wheel marks its neutral centre; wheel modifiers as in Lightroom: Shift-drag keeps the puck's hue and changes saturation only (the spoke is the guide; a puck at the centre has no hue, so it takes the angle where the pointer pressed), Cmd/Ctrl-drag keeps the saturation and changes hue only (the circle is the guide), Alt/Option-drag fine-adjusts at 0.1x and combines with either lock, and a hover tooltip lists them (issues #559, #1160; tests shift_dragging_across_the_tangent_keeps_the_hue_and_moves_saturation, shift_drag_keeps_the_pucks_hue_wherever_the_pointer_grabs, cmd_drag_keeps_the_saturation_and_changes_only_the_hue, alt_drag_fine_adjusts_to_a_tenth_of_the_pointers_travel, alt_works_with_shift_and_cmd_constraints in crates/ui-egui/src/tests_grading.rs); the large wheel adds 25/50/75 % saturation rings (issue #559); each wheel's eye hides its effect for viewing only (RenderJob::with_hidden_grading, not saved, export unaffected) (issue #737). No Global wheel tab yet
LR-EDIT-EFFECTS-TEXTURE Texture P0 ✅ ctl:effects.texture
LR-EDIT-EFFECTS-CLARITY Clarity P0 ✅ ctl:effects.clarity
LR-EDIT-EFFECTS-DEHAZE Dehaze P0 ✅ ctl:effects.dehaze
LR-EDIT-EFFECTS-VIGNETTE Post-crop vignette P0 ✅ ctl:vignette.*, crates/pipeline/src/finish.rs, crates/ui-egui/src/panels/edit.rs style picker (Highlight / Color / Paint) in the Effects section
LR-EDIT-EFFECTS-GRAIN Grain P1 ✅ ctl:grain.*
LR-EDIT-DETAIL-SHARPEN Sharpening P0 ✅ ctl:detail.sharpenAmount, ctl:detail.sharpenRadius, ctl:detail.sharpenDetail, ctl:detail.sharpenMasking no Alt-drag mask preview
LR-EDIT-DETAIL-NR Luminance noise reduction P0 ✅ ctl:detail.nrLuminance, ctl:detail.nrDetail, ctl:detail.nrContrast
LR-EDIT-DETAIL-CNR Colour noise reduction P0 ✅ ctl:detail.nrColor, ctl:detail.nrColorDetail, ctl:detail.nrColorSmoothness
LR-EDIT-DETAIL-DENOISE AI denoise P2 🟡 cmd:denoise.toggle, cmd:denoise.models.install, cmd:denoise.queue, ctl:enhance.denoise, crates/denoise/, docs/denoise.md non-destructive Bayer RAW, opt-in weights, pure-Rust CPU/GPU; GPU convolution uses independently written shared scalars (Metal equivalence regression, issue #479); tiles can also run on another computer's graphics card (lightcraft-cli denoise-serve, crates/denoise/src/remote.rs); X-Trans/linear RGB, model policy and Lightroom fidelity remain; closing the model setup without installing turns the switch back off (#922)
LR-EDIT-DETAIL-RAWDETAILS Improved demosaic toggle P2 ⬜
LR-EDIT-DETAIL-SUPERRES Super resolution P2 ⬜
LR-EDIT-DETAIL-AISHARPEN AI sharpen OOS 🚫
LR-EDIT-OPTICS-CA Remove chromatic aberration P1 ✅ crates/ui-egui/src/panels/edit.rs (checkbox), ctl:optics.caRed, ctl:optics.caBlue
LR-EDIT-OPTICS-PROFILE Lens profile corrections P1 🟡 ctl:optics.profileDistortion, ctl:optics.profileVignetting, crates/pipeline/src/optics.rs, crates/raw/src/vendor/rw2.rs, crates/raw/src/vendor/raf_lens.rs, crates/engine/src/crs.rs uses corrections embedded in the file: DNG WarpRectilinear / WarpRectilinear2 (opcode 14, DNG 1.6: degree-14 radial polynomial, valid-radius clamp, reciprocal option, applied in list order with the spec's skip rule for a WarpRectilinear after an optional one, so Pixel DNGs' two-stage warp is corrected; up to 3 chained stages, sampled on the CPU because the polynomials need f64; issue #635) / FixVignetteRadial, and the camera's own distortion correction in Panasonic / Leica RW2 / RWL files (tag 0x0119, read as the equivalent WarpRectilinear; on 79 photos from 13 bodies and 11 lenses the corrected render lines up with the camera JPEG within 0.75 px at 1440 px at the corners, from up to 13 % of the half diagonal uncorrected; issue #256); an imported preset's LensProfileEnable=0 leaves them on, as Lightroom applies built-in corrections regardless (inferred); Sony ILCE-7RM4A Bayer ARW raw-IFD distortion tables (0x7037, 16-sample layout, native 3:2 framing) use the same warp path, enabled only for the independently validated ILCE-7RM4A with FE 24–105 and FE 200–600 lenses; other Sony models remain uncorrected (see docs/sony-lens-corrections.md); Fujifilm RAF raw-IFD tables (0xf00b distortion, 0xf010 vignetting; crates/raw/src/vendor/raf_lens.rs, docs/fuji-lens-corrections.md) become a WarpRectilinear with the camera's frame-filling zoom and a FixVignetteRadial gain for 59 body models with sample files checked against the camera JPEG (the table indexed by the source radius with last knot at the corner; on 67 body/lens combinations the corrected render's radial shape error against the camera JPEG falls from a median of 0.72 % to 0.05 % of the half diagonal, largest 0.42 %, and the vignetting gain of the JPEG tracks the table at a median correlation of 0.987); files whose table scale is not the recorded image's half diagonal (X-S20, X-T30 III, X-M5, X-T5 crops) and six unmatched bodies stay uncorrected; Fujifilm's lateral CA table (0xf00f) is real but is not applied, because our demosaic already registers the planes and the stored shift over-corrects; vignetting and lateral CA data of Olympus and Sony raws not read yet; Reload from Disk reads the file's lens data for photos catalogued without it and turns the correction on for those still at import settings (issue #314); lens-profile database: a lensfun-format XML database in LIGHTCRAFT_LENSFUN_DIR (none is bundled) supplies distortion (poly3 / poly5 / PTLens), TCA and vignetting for lenses it lists (crates/lensdb, crates/engine/src/lens_profiles.rs, docs/lens-profiles.md); no settings switch or bundled database yet
)
LR-EDIT-OPTICS-DEFRINGE Defringe P1 ✅ ctl:optics.defringe* no fringe eyedropper
LR-EDIT-OPTICS-MANUAL Manual distortion / vignetting P1 ✅ ctl:optics.distortion, ctl:optics.vignetting, ctl:optics.vignettingMidpoint
LR-EDIT-GEOM-UPRIGHT Upright P1 ✅ cmd:geometry.upright, cmd:geometry.guides
LR-EDIT-GEOM-MANUAL Manual transform P1 ✅ ctl:geometry.*
LR-EDIT-GEOM-CONSTRAIN Constrain crop P1 ✅ crates/ui-egui/src/panels/right.rs (checkbox → cmd:develop.merge)
LR-EDIT-GEOM-GRID Grid while transforming P2 ✅ crates/ui-egui/src/panels/detail.rs a fine grid over the photo while a geometry slider is dragged
LR-EDIT-LENSBLUR Lens blur P2 ⬜ settings field reserved, not rendered
LR-EDIT-CALIB Calibration [Classic] P1 ✅ ctl:calibration.* shadows tint, red/green/blue primary hue and saturation; read/written in XMP
LR-EDIT-SECTION-TOGGLE Section on/off P1 ✅ cmd:develop.sectionEnabled, crates/develop/src/lib.rs (effective) the eye on a section header hides its adjustments: Light (with the curve), Color (white balance, presence, mixers, grading, point colour), Effects, Detail, Optics, Geometry, Calibration; masks stay on (issue #316 fixed Light, Color and Detail, which had no effect)
LR-EDIT-RESET Reset all / section / slider P0 ✅ cmd:develop.reset, cmd:develop.resetSection, cmd:develop.resetControl, crates/ui-egui/src/widgets.rs (double-click) no "reset to open"
LR-EDIT-SHOWORIG Show original P0 ✅ cmd:view.showOriginal

G. Profiles (PROF)

Id Feature Tier Status Evidence Missing / notes
LR-PROF-DROPDOWN Profile menu P0 ✅ cmd:develop.profile, cmd:profiles.list, cmd:profiles.menu, cmd:profile.favorite, crates/ui-egui/src/panels/edit.rs Favorites, Recent (last 5), group submenus, favourite toggle; favourites/recent persist with the library; Amount slider under the menu for non-default profiles; Browse… opens the profile browser; resting on an entry previews it in the loupe
LR-PROF-BROWSER Profile browser P1 ✅ cmd:panel.profiles, crates/ui-egui/src/panels/profiles.rs, crates/engine/src/media.rs grid of live variant thumbnails of the current photo per group (Favorites first), star toggles favourites, amount slider, hover = loupe preview (no history), click applies
LR-PROF-ADOBE Standard raw looks (own equivalents) P0 ✅ crates/engine/src/presets.rs (PROFILES), crates/pipeline/src/profiles.rs six own looks: Color, Neutral, Vivid, Landscape, Portrait, Monochrome
LR-PROF-ADAPTIVE Adaptive profiles P2 ⬜
LR-PROF-CAMERA Camera-matching looks P2 ⬜
LR-PROF-CAMERACOLOR Camera colour calibration (own) P0 🟡 crates/raw/src/color.rs, crates/raw/src/profile.rs, crates/raw/src/gaintable.rs, crates/engine/src/camera_preview.rs DNG files use the colour matrices and the camera-profile look they carry (ProfileHueSatMap, ProfileLookTable, ProfileToneCurve, read from the file at run time per DNG spec ch. 6 — Lightroom-converted DNGs rendered flat and muted without them, issue #138). A DNG's ProfileGainTableMap / ProfileGainTableMap2 (Apple ProRAW's local tone map; both versions, DNG 1.7 precedence, the file's byte order with a big-endian fallback) is read and written back by DNG export and not rendered by default, as Lightroom Classic renders ProRAW without it (measured on one CC0 iPhone 12 Pro ProRAW); the Profile option "Camera local tone mapping" (profile.camera_local_tone, per photo, off by default, crates/pipeline/src/local_tone.rs) renders it as the camera does, per output pixel in the scene-linear stage (CPU, also under the GPU path); Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3 and CR2, Pentax PEF, Samsung SRW and Olympus ORF get a guarded file-local fit to their own embedded JPEG — chromaticity matrix + hue/saturation/value table + tone and chroma curves, relative WB — or, with a bundled or local per-model profile pooled from many photos (lightcraft-cli calibrate, ARW, NEF and RAF so far, crates/engine/src/camera_profiles.rs), the profile's colour and only their own tone/chroma curves (on 29 held-out ILCE-7M4 photos mean ΔE vs the camera JPEG 3.54 → 3.10; bundled ILCE-7CR from 125 photos: 26 held-out 4.07 → 3.29, and every one of the 131 photos accepted, 8 of which fell back to neutral without it); the tone fit also uses dark JPEG samples excluded from colour training, so deep shadows follow the camera toe (#1307); Sony ARWs shot with DRO on (maker note 0xb025, on by default) keep that colour but their tone curve is lowered to Sony's curve without DRO, whose lifted shadows are in the camera JPEG only (issue #244: 8 ILCE-7CR photos +6.4 → +0.8 L* from Lightroom's default, median ΔE 7.5 → 3.3); 52 models without a profile whose JPEG fit fails (or that have no usable JPEG) start from DNG-style matrices fitted to their measured spectral sensitivities instead of the neutral fallback (crates/raw/src/spectral.rs, rawtoaces-data v1.1.0, Apache-2.0, aliases included; not yet measured against the camera JPEG); ARW: on 10 public raw.pixls.us samples from 8 bodies 7 fits were accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples (D750, D780, D850, D7500, Z 50 and local D7500 shots) accepted, ΔE 13–46 → 3–8 (issue #150); RW2 / RWL: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0 with matching lightness (13 of the 14 corpus files with a preview: ΔE 9.6–27 → 2.8–8.9; most rejections are compacts and kit zooms whose camera JPEG is distortion-corrected); a fit that fails the gates on all pixels is retried away from edges, where a lens-corrected camera JPEG still lines up (issue #232: the six distortion-corrected public ILCE-7RM2 samples ΔE 16.6–16.9 → 3.7–3.8, the corpus GX80 19.1 → 7.9); RAF: bundled X-H2S/X-T4 profiles from 201/545 photos; on 150 withheld files mean ΔE improves slightly over per-file fitting (X-H2S 1.70 → 1.62, X-T4 2.43 → 2.34); 23 of 24 references from 13 bodies accepted, including the dark X-H2S samples (8.74 → 2.25); ORF: the 3 decodable corpus files (E-1, E-400, XZ-2) accepted (docs/camera-preview-colour.md); a raw whose camera JPEG is black and white (a monochrome picture style) takes only the JPEG's tone curve, on the camera profile's, spectral or neutral colour (issue #577); this is not measured camera calibration; other raws and rejected fits use a neutral fallback (camera RGB ≈ linear sRGB, matrix_is_fallback) with as-shot white balance, so colours are muted and not accurate. Needs our own per-camera calibration: matrices the files carry themselves (e.g. Olympus ColorMatrix), fitting each camera to its own embedded JPEG, then chart shots. Adobe matrices / DCPs are never used. Biggest image-quality gap today; ILCE-7RM6: per-file fit now runs on previews above 64 MP, a pooled profile (46 photos) was tried and not bundled (withheld ΔE 5.71 → 6.40, 11 of 15 regressed), kept with smart previews (see docs/camera-preview-colour.md)
LR-PROF-CREATIVE Creative profiles (own) P2 ✅ cmd:develop.profile, crates/pipeline/src/profiles.rs 16 own looks in Film / Cinematic / Muted / B&W (tone + point-curve fades, colour grading, mixer / B&W mix); scale with ctl:profile.amount; sliders untouched
LR-PROF-LEGACY Legacy profiles P2 ⬜
LR-PROF-NONRAW Profiles for non-raw files P0 ✅ cmd:develop.profile same looks apply to JPEG/TIFF
LR-PROF-AMOUNT Profile amount P1 ✅ ctl:profile.amount
LR-PROF-IMPORT Import profiles P1 ✅ cmd:profile.import, cmd:profile.deleteImported, crates/pipeline/src/lut.rs, crates/engine/src/cmd/lut_profiles.rs, crates/ui-egui/src/panels/edit.rs, crates/ui-egui/src/panels/profiles.rs .cube 3D LUTs (files, folders, zips) become creative profiles with Amount, grouped in the profile browser, kept with the library; rendered on the CPU; Adobe profile formats deliberately unsupported

H. Crop & rotate (CROP)

Id Feature Tier Status Evidence Missing / notes
LR-CROP-RECT Crop rectangle P0 ✅ cmd:crop.set, cmd:crop.drag, crates/geom/src/crop_drag.rs, crates/ui-egui/src/panels/detail.rs double-click inside the box applies the crop (like Return)
LR-CROP-ASPECT Aspect ratios P0 ✅ cmd:crop.aspect, cmd:crop.rotateAspect, crates/ui-egui/src/panels/right.rs menu with presets + custom W × H, visible Lock toggle (also A), button names the active ratio; every handle honours the lock, holding Shift while dragging a handle keeps the frame's proportions for that drag (issue #614), and the drag stops at the image edge instead of shifting the frame (issue #295); "Original" is the photo's own shape (no separate As Shot); the last chosen aspect is saved as a crop tool default, resolving Original per photo and preserving existing crops; opening Crop or browsing never edits a photo: the default applies only when a crop gesture starts, within its single undo step
LR-CROP-STRAIGHTEN Straighten tool P0 ✅ cmd:crop.straighten, cmd:crop.autoStraighten, crates/ui-egui/src/panels/detail.rs (rotate_cursor, angle_readout), crates/ui-egui/src/panels/right.rs (Angle field), crates/ui-egui/src/tests_crop_rotate.rs Straighten Tool button: drag along a horizon/vertical; double-click or Auto levels automatically. Dragging outside the crop box rotates it: there the pointer is a curved double arrow that bulges away from the side of the box it is on, diagonally off a corner and following a rotated box (rotate_cursor_turn, tested by rotate_cursor_faces_away_from_the_crop_box) (with ⌘ held, the straighten crosshair), and while rotating the angle shows next to it, kept on the canvas (issue #534); an exact angle goes in the Angle field under Straighten (cropAngleField: applies on Return or leaving it, Esc keeps the old angle, takes a decimal comma; a drag on it is one undo step) or by clicking the Straighten value (its hover says so). Every way of setting the angle (rotate handle, Straighten slider, Angle field, crop.straighten, crop.set {angle}, and the generic develop.set / develop.adjust / develop.quickAdjust / develop.resetControl on crop.angle, as MCP, the control channel and lightcraft-cli render --set use them) keeps the crop if it still fits, else shrinks it to the largest crop of its aspect inside the photo (CropGeometry::with_angle, crates/engine/src/tests_crop_angle.rs; before issue #742 the generic path left the box, and the export, past the photo with smeared edge pixels in the corners). The dimming outside the box is the photo minus the (rotated) box, drawn as one mesh (outside_crop_mesh), so rotating never dims wedges inside the box or doubles them outside (issue #731)
LR-CROP-AUTO Auto straighten P1 ✅ cmd:crop.autoStraighten crop-angle leveling from detected horizon/plumb lines (consensus required)
LR-CROP-ANGLE Angle slider P0 ✅ ctl:crop.angle, crates/ui-egui/src/panels/right.rs the Straighten slider; an exact angle can also be typed in the Angle field under it (see LR-CROP-STRAIGHTEN); setting crop.angle through develop.set fits the crop inside the photo as Straighten does (issue #742)
LR-CROP-ROTATE90 Rotate 90° P0 ✅ cmd:photo.rotateLeft, cmd:photo.rotateRight
LR-CROP-FLIP Flip P0 ✅ cmd:photo.flipHorizontal, cmd:photo.flipVertical, crates/pipeline/src/geometry.rs the crop tool's uncropped view shows the flips too, with the crop box and its dimming mirrored to match
LR-CROP-OVERLAY Crop overlays P1 ✅ cmd:view.cropOverlay, cmd:view.cropOverlayOrientation, crates/ui-egui/src/panels/crop_overlay.rs thirds, grid, golden ratio, diagonal, triangle, golden spiral (mirrored with ⇧O while cropping); no aspect-ratio overlays
LR-CROP-ZOOM Zoom while cropping P1 ✅ cmd:view.zoom100, cmd:view.zoomIn, crates/ui-egui/src/panels/detail.rs zoom levels apply with the crop tool open (verified: 100% shows the photo at native size with the crop frame)
LR-CROP-GENEXPAND Generative expand OOS 🚫

I. Remove / healing (REM)

Id Feature Tier Status Evidence Missing / notes
LR-REM-CONTENTAWARE Content-aware remove P1 🟡 cmd:spot.add (mode: remove / fill), crates/pipeline/src/spots.rs, crates/inpaint/src/lib.rs Remove heals with an automatic source; Fill synthesises the area from its surroundings (PatchMatch completion over a membrane-blended start, crates/inpaint, ported from PhotoCraft; bounded by the spot size limit); no generative fill (M8.4)
LR-REM-HEAL Heal P0 ✅ cmd:spot.add (mode: heal)
LR-REM-CLONE Clone P0 ✅ cmd:spot.add (mode: clone)
LR-REM-GEN Generative remove OOS 🚫
LR-REM-DETECT Object detection for remove P2 ⬜
LR-REM-BRUSH-PARAMS Brush size / feather / opacity P0 ✅ cmd:spot.add (size, feather, opacity), cmd:brush.smaller, cmd:brush.larger, cmd:brush.featherLess, cmd:brush.featherMore, crates/ui-egui/src/panels/right.rs Size / Feather / Opacity sliders (for new spots and the selected one); [ ] size, ⇧[ ⇧] feather (also the Masking brush)
LR-REM-SPOT-EDIT Edit existing spots P0 ✅ cmd:spot.select, cmd:spot.update, cmd:spot.refreshSource, cmd:spot.delete click a pin to select, drag target or source, ⌫ deletes the selected spot, / picks another source; automatic sources are resolved when the spot is added
LR-REM-VISUALIZE Visualize spots P1 ✅ cmd:view.visualizeSpots
LR-REM-PEOPLE Remove people (generative) OOS 🚫
LR-REM-REFLECT Remove reflections P2 ⬜
LR-REM-DUST Dust detection P2 ✅ cmd:spot.findDust, crates/pipeline/src/dust.rs Remove panel ▸ Find Dust Spots: small, soft, round dark spots on smooth areas become heal spots (sensitivity; one undo step); Visualize Spots for checking by eye
LR-REM-BLEMISH Find blemishes on faces (beyond Lightroom) P2 ✅ cmd:spot.findBlemishes, crates/pipeline/src/blemish.rs, crates/engine/src/cmd/face_detect.rs Remove panel ▸ Find Blemishes: with the YuNet detector installed, small darker-or-redder round spots on each face's skin (eyes, brows, nostrils and mouth cut out around the landmarks) become heal spots (one undo step); faces under 160 px and faces with more than 12 candidates (stubble, freckles) get none, at most 6 per face; sensitivity 0–100. Classical, no learned model beyond the detector; not a Lightroom feature
LR-REM-SYNC Sync spots P1 ✅ cmd:develop.copy (groups), crates/develop/src/presets.rs

J. Red eye (EYE)

Id Feature Tier Status Evidence Missing / notes
LR-EYE-RED Red-eye correction P1 ✅ cmd:panel.redeye, cmd:redeye.add, cmd:redeye.delete, cmd:redeye.catchlight red eye and pet eye (with catchlight)
LR-EYE-PET Pet eye P2 ✅ cmd:redeye.add (pet), cmd:redeye.catchlight

K. Masking (MASK)

Id Feature Tier Status Evidence Missing / notes
LR-MASK-PANEL Masks panel P0 ✅ cmd:panel.masking, cmd:mask.add, cmd:mask.select, cmd:mask.rename, cmd:mask.duplicate (invert), cmd:mask.move, cmd:mask.visible, cmd:mask.delete, crates/ui-egui/src/panels/masking.rs list with per-mask show/hide eye, double-click rename, right-click menu (duplicate, duplicate and invert, invert, hide, move up/down, rename, delete); fits any panel width (create tiles go to three columns, mask actions wrap, long component names are cut short); no drag-to-reorder
LR-MASK-SUBJECT Select subject P2 🟡 cmd:mask.add (subject), crates/pipeline/src/masks.rs saliency heuristic, no segmentation model; a photo that carries person mattes (DNG semantic masks: iPhone ProRAW portrait / skin / hair / teeth / glasses mattes) uses them instead (synthetic files only: no corpus file carries person mattes)
LR-MASK-SKY Select sky P2 🟡 cmd:mask.add (sky), crates/pipeline/src/masks.rs (Mattes), crates/raw/src/semantic.rs heuristic; a photo that carries a sky matte (DNG semantic mask, e.g. iPhone ProRAW) uses it instead, through orientation / crop / lens corrections, preview = export (checked on one CC0 iPhone 12 Pro ProRAW and synthetic files)
LR-MASK-BACKGROUND Select background P2 🟡 cmd:mask.add (background) inverse of the subject mask (heuristic, or the photo's person mattes)
LR-MASK-OBJECTS Object selection P2 🟡 cmd:mask.add (object, prompt), cmd:mask.objectPoint, cmd:mask.refineDetail, cmd:segment.prepare, cmd:segment.model.status, cmd:segment.model.download, cmd:segment.model.cancel, crates/segment, crates/engine/src/segment/mod.rs SAM 3 in pure Rust (candle; Metal on macOS, CPU elsewhere): Object tile → click to include, ⌥-click to leave out; Describe tile → a text prompt selects every instance ("sky", "the red car"); both also as Add/Subtract/Intersect components; + / − under the selected mask; comma lists (car, road); a zoomed-in detail pass for 5–10× finer edges on small objects; per-selection Edge (hard ↔ soft). The model runs on its own worker thread (the UI never waits; panics become errors), is unloaded after 10 min idle, and is optional: the segmentation is stored with the mask (288² logits), so renders and exports never need it. Weights are never bundled (SAM License): without them the app offers a consented background download (mirrors, resume, timeouts, SHA-256) — but no default download location is configured yet (users set LIGHTCRAFT_SAM3_MIRRORS or install by hand: the dialog names the three files, shows the model folder and links docs/ai-masks.md). Not verified against Lightroom's Select Object; no brush/box object mode; the first click on a photo waits for its analysis (~4 s on an M4 Pro, much longer on CPU)
LR-MASK-PEOPLE People parts P2 ⬜ shape exists (no UI): uses the photo's person / skin / hair / teeth / glasses mattes when it carries them (DNG semantic masks), else the subject heuristic
LR-MASK-LANDSCAPE Landscape classes P2 ⬜ shape exists, evaluates empty
LR-MASK-BRUSH Brush mask P0 🟡 cmd:tool.brush, cmd:mask.brushStroke (autoMask), crates/pipeline/src/masks.rs, crates/ui-egui/src/brush_live.rs size/feather/flow/density/erase; Create New Mask starts a separate brush in paint mode; the stroke shows while the button is held with the committed stroke's dab spacing, feather and flow, so fast strokes have no gaps (#517); coverage builds up by passes, not hold time (holding the brush still adds nothing, live or committed, #518); Auto Mask: dabs weighted by similarity to the colour under the dab centre, refined by a guided filter on luminance (CPU + GPU); no A/B brushes, no pressure
LR-MASK-LINEAR Linear gradient P0 ✅ cmd:tool.linear, cmd:mask.update
LR-MASK-RADIAL Radial gradient P0 ✅ cmd:tool.radial, cmd:mask.update, crates/ui-egui/src/panels/detail.rs drag the selected ellipse's body or centre pin to move; edge grips resize, outer grip rotates; rotated and compound radial components, hidden pins, outside hit rejection and one-step undo tested
LR-MASK-COLORRANGE Colour range P1 ✅ cmd:mask.add (colorRange), cmd:mask.sampleColor, cmd:mask.update, crates/pipeline/src/lib.rs (color_range_sample) Color tile → click the photo to sample (⇧-click adds, up to 5; samples taken in the space the mask compares in), Pick button, Refine slider
LR-MASK-LUMRANGE Luminance range P1 ✅ cmd:mask.add (luminanceRange), cmd:mask.update, crates/ui-egui/src/panels/masking.rs (range_controls) range bar with two handles (one undo step per drag), Smoothness, Show Luminance Map (B&W photo with the range tinted)
LR-MASK-DEPTHRANGE Depth range P2 ⬜ shape exists, needs depth data
LR-MASK-COMBINE Add / subtract / intersect P0 ✅ cmd:mask.addComponent, cmd:tool.brush Add/Subtract > Brush creates a component with the chosen operation; Erase removes paint within that brush component
LR-MASK-INVERT Invert P0 ✅ cmd:mask.invert
LR-MASK-AMOUNT Mask amount P1 ✅ cmd:mask.adjust (amount), crates/ui-egui/src/panels/masking.rs
LR-MASK-FEATHER-EDGE Refine mask edges P2 ✅ cmd:mask.refine, crates/pipeline/src/masks.rs (evaluate_one) Refine Edges slider per mask: guided filter on the photo's luminance snaps soft mask edges to the photo's edges (rendered on the CPU)
LR-MASK-SLIDERS Local adjustment sliders P0 ✅ cmd:mask.adjust, crates/pipeline/src/finish.rs every slider renders (CPU + GPU), incl. Noise, Moiré, Defringe (negative Defringe has no effect); no local curve or effect presets
LR-MASK-OVERLAY Mask overlay P0 ✅ cmd:view.maskOverlay, cmd:view.maskOverlayMode, cmd:view.maskOverlayColor, crates/pipeline/src/visualize.rs rendered alpha of the selected mask (CPU + GPU): colour, colour on B&W, image on black/white, white on black; ⇧O cycles while masking; successful local adjustments (including Amount) hide the overlay until manually shown again with O / the eye; shape/edge edits retain it; zoomed in, the overlay is rendered on the same sharp window as the plain view (zoom and pan unchanged; crates/ui-egui/src/tests_preview_limit.rs, crates/pipeline/src/tests_window.rs); no auto-show on hover
LR-MASK-OVERLAYALL Show all mask overlays P1 ✅ cmd:view.maskOverlayAll, crates/ui-egui/src/panels/masking.rs, crates/pipeline/src/visualize.rs one diagnostic tint over every visible mask at once (CPU + GPU): the masks still stack their adjustments as before — this only paints each mask's region together, so you can see all masked areas at a glance (⌘⇧O / Ctrl+Shift+O)
LR-MASK-PINS Pins P1 ✅ cmd:view.maskPins, crates/ui-egui/src/panels/detail.rs a pin per radial/linear/brush component: click selects its mask, drag moves it; no pins for range/AI components, no Auto mode
LR-MASK-UPDATE Recompute AI masks P2 ⬜
LR-MASK-SYNC Copy masks to other photos P1 ✅ cmd:develop.copy (groups)
LR-MASK-ADAPTIVE Adaptive presets P2 ⬜

L. Presets (PRE)

Id Feature Tier Status Evidence Missing / notes
LR-PRE-PANEL Presets panel P0 ✅ cmd:panel.presets, cmd:preset.apply, crates/ui-egui/src/panels/presets.rs grouped list with amount; resting on a preset previews it in the loupe (no history entry); optional live thumbnails (⋯ → Show Thumbnails)
LR-PRE-CREATE Create preset P0 ✅ cmd:dialog.createPreset, cmd:preset.create (groups) name, group and a checklist of settings groups (crop, masks, remove, red eye off by default; All / None)
LR-PRE-MANAGE Manage presets P1 ✅ cmd:preset.delete, cmd:preset.favorite, cmd:preset.import, cmd:preset.export, cmd:preset.rename, cmd:preset.update, cmd:preset.move rename, update with current settings, move to a group (existing or new); no hiding of groups
LR-PRE-AMOUNT Preset amount P1 ✅ cmd:preset.apply (amount 0–200)
LR-PRE-ADAPTIVE Adaptive presets P2 ⬜
LR-PRE-PREMIUM Built-in presets (own) P2 ✅ crates/engine/src/presets.rs 41 own-authored presets in 10 groups (Color, Film, B&W incl. toners, Portrait, Landscape, Urban, Food, Seasons, Vintage, Style)
LR-PRE-RECOMMENDED Community recommendations OOS 🚫
LR-PRE-ONIMPORT Apply during import P2 ✅ cmd:library.import (preset) chosen in the import review; raw / per-camera defaults in Settings

M. Versions & history (VER)

Id Feature Tier Status Evidence Missing / notes
LR-VER-CREATE Create version P1 ✅ cmd:version.create
LR-VER-PANEL Versions panel P1 ✅ cmd:panel.versions, cmd:version.create, cmd:version.restore, cmd:version.delete, cmd:version.rename, cmd:version.update, crates/ui-egui/src/panels/right.rs (versions) Named / Auto tabs, a thumbnail of each version's look, name and date, current marker; resting on a version previews it in the loupe; click restores, double-click renames; row menu (update, rename, set as before, delete). No automatic versions are created yet (LR-VER-AUTO)
LR-VER-AUTO Automatic versions P2 ✅ crates/engine/src/lib.rs (auto_version) leaving an edited photo keeps its settings as an Auto version (when they differ from the latest version; 20 kept; not an undo step); Versions panel ▸ Auto
LR-VER-HISTORY Edit history P1 ✅ cmd:panel.activity, cmd:history.list, cmd:history.restore, cmd:history.previous, cmd:history.next going to a step moves a per-photo cursor without adding a step; later steps stay listed (greyed) until a new edit drops them; history.list returns cursor
LR-VER-UNDO Undo / redo P0 ✅ cmd:edit.undo, cmd:edit.redo

N. Copy / paste / sync (SYNC)

Id Feature Tier Status Evidence Missing / notes
LR-SYNC-COPY Copy edit settings P0 ✅ cmd:develop.copy
LR-SYNC-CHOOSE Choose settings to copy P0 ✅ cmd:dialog.copySettings groups are coarser than per-slider
LR-SYNC-PASTE Paste to selection P0 ✅ cmd:develop.paste no separate "paste selected" (choose at copy time instead)
LR-SYNC-SYNCBTN Sync active → selected P1 ✅ cmd:develop.sync
LR-SYNC-PREVIOUS Paste from previous / auto sync [Classic] P2 ✅ cmd:develop.pastePrevious, cmd:develop.autoSync ⌥⌘V pastes the previously active photo's settings (copy groups); Auto Sync (⌥⇧⌘A)

O. Merge (MERGE)

Id Feature Tier Status Evidence Missing / notes
LR-MERGE-HDR HDR merge P2 ✅ cmd:merge.hdr, cmd:dialog.mergeHdr auto align, deghost None–High + overlay, auto settings, float DNG, Create Stack; JPEG brackets treated as linear; raws without colour matrices of their own (CR2, NEF, ARW…) keep the colour they open with, written as DNG tags (starting_colour_tags; the look's chroma curve has no tag; panoramas too), issue #965
LR-MERGE-PANO Panorama P2 ✅ cmd:merge.panorama, cmd:dialog.mergePanorama spherical/cylindrical/perspective + auto, boundary warp, auto crop, fill edges (diffusion), DNG; no lens model / 360° wrap
LR-MERGE-HDRPANO HDR panorama P2 ✅ cmd:merge.hdrPanorama, cmd:dialog.mergeHdrPanorama brackets grouped by EXIF
LR-MERGE-HEADLESS Merge with last settings P2 ✅ cmd:merge.hdrLast, cmd:merge.panoramaLast, cmd:merge.hdrPanoramaLast no dialog; the options of the last merge of that kind (defaults the first time)

P. Enhance (ENH)

Id Feature Tier Status Evidence Missing / notes
LR-ENH-DIALOG Enhance dialog P2 ⬜
LR-ENH-INPLACE In-place enhance P2 ⬜

Q. HDR (HDR)

Id Feature Tier Status Evidence Missing / notes
LR-HDR-EDIT HDR editing P2 🟡 cmd:develop.hdr, crates/pipeline/src/tone.rs (ToneMap::hdr), crates/pipeline/src/tests_hdr.rs HDR tone map (SDR curve through the midtones, highlights up to the headroom limit) and float HDR render, CPU only; no on-screen HDR display yet (LR-VIEW-HDR-DISPLAY); crs:HDREditMode from XMP / Lightroom catalogs turns HDR editing on or off (the headroom limit and SDR rendition aren't read from crs yet)
LR-HDR-SDRPREVIEW SDR preview of HDR P2 🟡 ctl:hdr.sdr* SDR rendition sliders (brightness, contrast, highlights, shadows, whites, clarity) used by every SDR view and export; no separate SDR-preview toggle until HDR display lands
LR-HDR-VISUALIZE Visualize HDR range P2 ✅ cmd:view.visualizeHdr, crates/pipeline/src/visualize.rs (hdr_range) grey below SDR white, four colour bands by stops above it
LR-HDR-LIMIT HDR headroom limit P2 ✅ ctl:hdr.maxEv, cmd:develop.hdr 0 to 5 stops; marked on the HDR histogram (whose scale stays at 5 stops)
LR-HDR-EXPORT HDR export P2 🟡 cmd:app.export, crates/codecs/src/gainmap.rs, crates/engine/src/export.rs (encode_gain_map_jpeg) hdr export option: ISO 21496-1 gain map JPEG (plus Adobe hdrgm and Apple XMP), 10-bit Rec. 2020 AVIF with PQ or HLG (hdrTransfer, BT.2100 / BT.2408, SDR white at signal 0.75), 16-bit Rec. 2020 PQ PNG (cICP + cLLI; crates/codecs/src/encode.rs encode_png_pq), 16-bit Rec. 2020 PQ JPEG XL, 32-bit float TIFF; no HEIC (no permissive HEVC encoder)

R. Video (VID)

Id Feature Tier Status Evidence Missing / notes
LR-VID-PLAY Video playback P1 ⬜ crates/catalog/src/model.rs videos can be catalogued (kind, duration); no player
LR-VID-TRIM Trim P1 ⬜
LR-VID-EDIT Edits on video P2 ⬜
LR-VID-COVER Cover frame P2 ⬜
LR-VID-EXPORT Video export P2 ⬜
LR-VID-PHOTO2VIDEO Photo to video (generative) OOS 🚫

S. Export (EXP)

Id Feature Tier Status Evidence Missing / notes
LR-EXP-DIALOG Export dialog P0 ✅ cmd:dialog.export, cmd:app.export (preset), cmd:export.presets, cmd:export.savePreset, cmd:export.deletePreset, crates/ui-egui/src/panels/dialogs.rs, crates/engine/src/export.rs batch export; built-in presets (JPEG Small 2048 px / Large full size, Original + Settings, DNG) and saved user presets (library prefs) load into the dialog; deleting a user preset is command-only; on a short window the options scroll and Cancel / OK stay visible (LR-BEHAV-DIALOGS, issue #781)
LR-EXP-TYPE File types P0 ✅ cmd:app.export (format), crates/engine/src/export.rs (ExportFormat), crates/engine/src/tests_export.rs JPEG, PNG, TIFF, PSD (flat, 8/16-bit, no layers: crates/codecs/src/psd.rs), WebP, AVIF, JPEG XL (issue #247: jixel, native only; lossy with quality or lossless, 8/16-bit, effort fast/normal (jixel's slowest search left out: jxl-oxide misdecodes its files), progressive, EXIF/XMP boxes; lossy files in Adobe RGB / ProPhoto are written as Rec. 2020, lossless keep any space via ICC; crates/codecs/src/jxl_encode.rs), DNG (raw photos: lossless re-encode with the edits in the embedded XMP), Original (+ XMP sidecar). Non-raw → DNG not supported; no JPEG XL export in the web build
LR-EXP-DIM Output size P0 ✅ cmd:app.export (longEdge, shortEdge, width, height, megapixels, percent, dontEnlarge, ppi), crates/engine/src/export.rs (Resize), crates/ui-egui/src/panels/dialogs.rs full size = the cropped native size (no longer upscaled); W × H fits either orientation; ppi written to JFIF / pHYs / TIFF tags
LR-EXP-QUALITY JPEG quality P0 ✅ cmd:app.export (quality, limitKb)
LR-EXP-BITDEPTH Bit depth P1 ✅ cmd:app.export (bitDepth), crates/pipeline/src/output.rs (OutputDepth), crates/engine/src/export.rs 16-bit PNG/TIFF/JPEG XL rendered at 16 bits (TIFF defaults to 16), 32-bit float linear TIFF with a linear profile, 10-bit AVIF; high-bit-depth renders run on the CPU
LR-EXP-COMPRESSION TIFF compression P1 ✅ cmd:app.export (tiffCompression: none / lzw / zip)
LR-EXP-COLORSPACE Output colour space P0 ✅ cmd:app.export (colorSpace), crates/pipeline/src/output.rs, crates/engine/src/export.rs sRGB, Display P3, Adobe RGB (1998) compatible, ProPhoto RGB, Rec. 2020: rendered from the working space with gamut mapping into the target gamut (CPU + GPU), own ICC profile embedded; AVIF stays sRGB (muxer has no ICC); lossy JPEG XL declares sRGB / P3 / Rec. 2020 (Adobe RGB and ProPhoto written as Rec. 2020), lossless JPEG XL embeds the profile
LR-EXP-HDR HDR output P2 🟡 cmd:app.export, crates/ui-egui/src/panels/dialogs.rs HDR output checkbox in the export dialog: gain map JPEG, PQ or HLG AVIF, PQ PNG, PQ JPEG XL, float TIFF; no HDR presets yet
LR-EXP-SHARPEN Output sharpening P1 ✅ cmd:app.export (sharpen, sharpenAmount)
LR-EXP-METADATA Metadata policy P1 ✅ cmd:app.export (metadata, removeLocation)
LR-EXP-WATERMARK Watermark P1 ✅ cmd:app.export (watermark: text, or {text, vertical, size, opacity, anchor, insetX, insetY, inset, rotation, color, shadow, image, imageWidth}), crates/engine/src/export.rs (Watermark, WATERMARK_PARAMS) text (size = height as a fraction of the short edge, 0.005..0.5, default 0.035; color sRGB [r,g,b]; shadow; Japanese vertical glyph forms and origins, tested by biz_ud_vertical_watermarks_place_punctuation_at_the_top_right; extended grapheme cells keep decomposed dakuten/handakuten and Latin accents together, tested by vertical_watermarks_keep_combining_marks_in_one_cell and upright_watermark_cell_retains_multiple_glyphs (see docs/vertical-watermarks.md)) or a graphic with transparency (image path, imageWidth = fraction of the photo's width, 0.01..1, converted to the output colour space); anchor (nine positions), insetX / insetY (margins from the side and from the top/bottom edges, each a fraction of the short edge, 0..0.4; the Export dialog's Horizontal and Vertical inset sliders, as in Lightroom Classic; the older single inset still reads as both; tested by watermark_insets_are_horizontal_and_vertical), rotation (0, 90, 180 or 270 degrees clockwise, the Export dialog's Rotate row, as Lightroom Classic's rotate buttons; sized before it is turned, placed and inset by the turned box; tested by watermark_rotation_turns_the_text_in_its_corner and graphic_watermark_is_placed_scaled_and_blended), opacity 0..1. Unknown keys and out-of-range values are errors (issue #183). The Export dialog previews it on the first exported photo's thumbnail (draw_watermark_preview scales the 6 px minimum text size to the preview so it matches the file), tested by watermark_preview_scales_the_minimum_text_size and export_dialog_previews_the_watermark
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), 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 and renamed into place (issue #93; not synced file by file — exports can be made again, issue #134)
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), crates/engine/src/tests_export.rs file copied byte for byte, sidecar named after the output and subject to the conflict policy
LR-EXP-RENDERDNG Render to DNG (Classic 15.5) P2 🟡 cmd:app.export (format: rendereddng), crates/raw/src/dngwrite.rs (write_rendered_dng), crates/raw/tests/rendered_dng.rs export format rendereddng (dialog: Format DNG, "DNG of: Rendered photo"): the rendered photo (edit, crop, size, sharpening, watermark baked in) as a LinearRaw DNG of 16-bit float Deflate tiles in the export colour space, with ColorMatrix1 / ForwardMatrix1 for that space, a neutral AsShotNeutral and an identity ProfileToneCurve so raw converters show it as rendered; HDR edits keep highlights above white. Not a menu command on selected photos (Lightroom adds the DNG to the catalog next to the original); export only
LR-EXP-PHOTOS Export to the system photo library P2 🟡 cmd:app.export (addToPhotos, photosAlbum), cmd:export.addToPhotos, cmd:export.photosImports, crates/engine/src/apple_photos.rs, crates/ui-egui/src/panels/dialogs.rs, packaging/macos/entitlements.plist, docs/apple-photos.md macOS: the exported files go to Apple Photos through Apple Events to its import command (a constant AppleScript run by /usr/bin/osascript; album names and paths are separate arguments), into a top-level album of exactly that name, made when missing (several of that name: refused); the Export dialog's Add to Apple Photos, Export with Previous, presets, MCP and lightcraft-cli all reach it; imports are jobs, off the UI thread in the desktop app and awaited in one-shot headless runs, one at a time, reserved by an export before it writes; permission, missing Photos, timeouts and partial imports are reported; the packaged app carries the Apple Events entitlement and usage text. Tested end to end with the packaged app on macOS 26.6.2 / Photos 11.0 (manual steps in docs/apple-photos.md). Other platforms' photo libraries are not covered
LR-EXP-PSD Round trip to an external editor P2 ✅ cmd:photo.editExternal, cmd:photo.editInPhotoCraft, cmd:photo.editInExternal, cmd:photo.openOriginalInExternal, crates/engine/src/cmd/convert.rs, apps/lightcraft/src/editors.rs an -Edit copy of each selected photo (up to 50 at once, issue #920) with the edits (PSD or TIFF, 16- or 8-bit, Adobe RGB / ProPhoto / P3 / sRGB, chosen per editor in Settings ▸ General ▸ External Editing) next to the original, added stacked on top of it and opened in PhotoCraft (found where its installers put it; a PSD by default, which PhotoCraft saves back to in place) or the other editor set there (or the system default); also on the photo's right-click menu (Edit In). Re-read when LightCraft is focused again. Open Original in Other Editor (RAW, no copy) hands that editor each selected photo's original file itself instead (a raw stays a raw, e.g. for DxO PhotoLab: no render, no copy, nothing added; several photos go to one start of the editor, which DxO PhotoLab 8 needs to get them all; demo photos and missing files are skipped with the reason). No "Edit a copy / Edit original" choice for non-raw files yet

T. Share (SHARE)

Id Feature Tier Status Evidence Missing / notes
LR-SHARE-LINK Web share link OOS 🚫
LR-SHARE-INVITE Invite collaborators OOS 🚫
LR-SHARE-WEBGALLERY Web galleries OOS 🚫
LR-SHARE-COMMUNITY Community edits OOS 🚫

U. Map & location (MAP)

Id Feature Tier Status Evidence Missing / notes
LR-MAP-INFO Location in the info panel P1 🟡 cmd:photo.setMeta (location, city, state, country, gps), crates/ui-egui/src/panels/right.rs location, city, state/province, country and GPS editable; Show on Map (OpenStreetMap in the browser); no map in the panel
LR-MAP-MODULE Map module [Classic] P2 ⬜

V. Preferences (PREF)

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, crates/ui-egui/src/panels/notices.rs General / Import / Performance / Display / 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, cmd:view.appearance, cmd:view.theme Appearance Mode (Sync with System / Dark / Light; the top bar button cycles Auto, Light, Dark) with separate dark (Charcoal, Midnight) and light (Silver, Paper) themes previewed in cards; hover stars persist favorite themes, the appearance button context menu opens Interface settings and lists light favorites before dark favorites; filmstrip names/badges, grid badges (auto/always/never), square-grid names, navigator, info overlay
LR-PREF-DISPLAY Monitor (display) colour management P1 🟡 cmd:app.displayProfile, crates/codecs/src/display.rs, crates/engine/src/display.rs, crates/ui-egui/src/panels/settings.rs, apps/lightcraft/src/screen_profile.rs, apps/lightcraft/src/colord.rs, docs/display-profiles.md Settings ▸ Display: a monitor ICC profile (matrix/TRC exactly, LUT-based through the CMS) chosen by hand, or on macOS the profile of the screen showing the window (followed across screens, #787), on Linux the profile the desktop assigns in colord (read off the UI thread every 3 s; used with one display, or several sharing one profile); the loupe, Before, Compare, hover previews and the second window render into the display's primaries (its full gamut, CPU and GPU), thumbnails and stand-ins are converted from sRGB; histogram, preview caches and exports stay sRGB; soft proofing's display gamut warning uses the display's gamut. Missing: Windows' WCS profile (needs FFI, so unsafe), X11 _ICC_PROFILE, per-monitor profiles on Linux (eframe doesn't say which monitor the window is on)
LR-PREF-PERFORMANCE GPU / performance P1 ✅ cmd:app.gpu, cmd:app.memoryBudget, cmd:app.settings GPU on/off (a crash while the GPU device or its kernels are built, issue #250, turns it off for the next launch), preview size (Automatic = the whole-frame loupe render at canvas size, up to 2560 px; or a 1600–5120 px size for it; zoomed views add a window at up to 100 %, issue #323), memory budget (a quarter of it is the idle per-view stage caches' share; the open photo's views are counted and reported, and cut only past four times that, the before side's and the zoom window's first — a Before/After at 1:1 on a 2800 px canvas holds about 1 GB of GPU stages, plus the one 288 MB copy of the original, which ui.inspect reports as sharedSourceBytes), thumbnail cache size in Settings
LR-PREF-PEOPLE Face recognition P2 🟡 cmd:faces.enable, cmd:faces.models.list, cmd:faces.models.inspect, cmd:faces.models.install, cmd:faces.models.download, cmd:faces.models.downloads, cmd:faces.models.downloadCancel, cmd:faces.models.remove, cmd:faces.models.select, cmd:dialog.faceModel, crates/faces/src/lib.rs, crates/ui-egui/src/panels/faces.rs Settings ▸ Faces: an off-by-default toggle and the face model list. Models are opt-in and never bundled, the 232 KB YuNet detector included. Press Download (terms first; then a pinned address, size and SHA-256 checked, fetched over pure-Rust https with lightcraft-fetch), or add any .onnx by picking or dropping it: it is recognised by hash or described from its shape, shows its licence terms and training-data note, runs a self-test, and installs only after an "I accept" box; an installed model is chosen and recognition switched on at once (the newest wins, each model keeps its own embedding cache). Non-commercial models are add-by-file only, never offered for download. Suggestions of names run on the shared pure-Rust CPU interpreter (crates/faces/src/net.rs, crates/faces/src/runtime.rs; bounded im2col/GEMM, checked ONNX shapes/operators; no external inference runtime; 40 public crops agree within 2.1e-6, one-thread SFace 34–36 ms / AuraFace 233–252 ms, see docs/faces.md) in a background worker and are never applied automatically. Thresholds are starting points calibrated on busts, not yet on live faces; the Recognise faces switch is off until a recognition model is chosen and installed (faces.enable {enabled: true} refuses otherwise; removing the last recogniser switches it off), issue #678
LR-PREF-WATERMARK Watermark settings P1 ✅ cmd:export.savePreset, crates/ui-egui/src/panels/dialogs.rs set in the Export dialog; kept with Export with Previous and in saved export presets
LR-PREF-SHORTCUTS Shortcut customisation — ✅ cmd:app.setShortcut, cmd:app.resetShortcuts, crates/ui-egui/src/panels/keymap.rs an extra (the reference app has no keymap editor): Help ▸ Keyboard Shortcuts (⌘/) lists every command; click a shortcut and press the new keys (Esc cancels), × removes, ↺ restores, Reset All; a key another command had moves over; saved in ui.json; native menu accelerators follow. Rating/label digits and secondary keys are listed read-only and yield to a key given to a command
LR-PREF-TECHPREVIEW Early-access toggles P2 ⬜
LR-PREF-NOTIFICATIONS Notifications OOS 🚫
LR-PREF-DEVICE Device settings P2 ⬜

W. Cloud & AI infrastructure (CLOUD / AI)

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 (on a worker thread, progress and Stop like Build 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). Each proxy records the version of the camera-look fit that wrote it (look_version, LOOK_VERSION in crates/engine/src/camera_preview.rs, bumped whenever the fit changes); only raws with the per-file camera look go stale (JPEG / PNG / DNG proxies are never rebuilt for it); one from an older fit is rebuilt from its original by a background scan the first time the library is opened at a new version (a look-version marker in the proxy folder keeps it to once per version) and by Build Smart Previews, and kept as it is while the original is offline (docs/camera-preview-colour.md). The thumbnail cache stays in the library
LR-AI-UPDATE-INDICATOR AI-settings update indicator P2 ⬜
LR-AI-CREDITS Generative credits OOS 🚫

X. Cross-cutting behaviours (BEHAV)

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; 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), crates/engine/src/tests_filesystem_history.rs covers ratings, albums, deletes, edits; partial file/XMP moves keep unfinished repairs across retries and discarded history; merged renames move directly to their final catalog path
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
LR-BEHAV-PREVIEW-HOVER Hover previews P1 ✅ crates/ui-egui/src/panels/presets.rs, crates/ui-egui/src/panels/profiles.rs, crates/ui-egui/src/panels/right.rs (versions), crates/ui-egui/src/panels/detail.rs presets, profile menu, profile browser and versions preview in the loupe
LR-BEHAV-PROGRESSIVE Progressive rendering P0 ✅ crates/engine/src/media.rs, crates/preview/src/lib.rs
LR-BEHAV-BG-TASKS Background tasks P0 ✅ cmd:activity.list, cmd:activity.cancel, crates/engine/src/activity.rs, crates/ui-egui/src/panels/activity.rs, docs/background-tasks.md, crates/preview/src/lib.rs (JobPool) renders off the UI thread; exports run several photos of a batch side by side (export_parallelism, memory-gated; files, names and order as one at a time, #496); one activity stack in the top-left corner under the top bar, as in Lightroom Classic (issue #345): imports, folder scans, exports (and the contact sheet PDF), Synchronize Folder (its scan and its run), Build / Smart Previews, Lightroom catalog inspect / import, Photo Merge, model downloads (SAM 3, face, AI Denoise), the face scan, AI Denoise and Find Missing Photos each show a row (name, progress bar or a sweep while the amount isn't known, count and current item) once 0.5 s old, three rows then "+N more"; ✕ cancels the jobs that can stop ("Stopping…" until they do); quitting asks while a cancellable task runs; agents and ui.inspect (activity) see the same list. No pause (Lightroom can pause some tasks)
LR-BEHAV-OFFLINE Offline editing P1 ✅ local-first: everything works offline
LR-BEHAV-WEB-SAFETY Library safety in the browser build P1 🟡 cmd:file.backupLibrary, cmd:file.restoreLibrary, apps/lightcraft-web/src/safety.rs, apps/lightcraft-web/src/backup.rs, apps/lightcraft-web/src/files.rs web only (experimental): Back Up Library (zip of the catalog and every stored original) and a non-destructive Restore; failed saves show as unsaved and are retried; a photo that can't be stored isn't added; one tab per library (Web Locks); notices for storage that isn't persistent and for a library that can't open; ?reset asks first; a panic shows a message. Missing: ?safe start, deleting originals of removed photos, zip64 (backups > 4 GB)
LR-BEHAV-GPU GPU acceleration P0 ✅ crates/gpu/src/render.rs, cmd:app.gpu, docs/gpu-pipeline.md, crates/ui-egui/src/panels/notices.rs CPU fallback per render on device limits, out of memory, device errors / loss, incomplete or black results (perf.gpuReason, perf.gpuFallback; each render reports its own reason — lightcraft_gpu::try_render, gpuFallback per exported file — issue #675); short submissions for slow iGPUs; backend choice (DX12 only on Windows, LIGHTCRAFT_GPU_BACKEND / WGPU_BACKEND), device created after the window opens, crash sentinel (issue #136); quitting waits (up to 2 s) for renders in flight and starts no new GPU work, so none is inside the driver when the process exits (crates/gpu/src/exit.rs, issue #620; seen crashing only on Linux + NVIDIA, other drivers unverified); with GPU rendering on and no adapter found at all (e.g. no Vulkan loader on NixOS outside nix develop), the desktop app says once per session that it renders on the CPU and points to Help ▸ System Info (gpu_notice; not when the GPU is turned off or only a software adapter exists); no tiling beyond the buffer limit (CPU)
LR-BEHAV-RENDER-FIDELITY Rendering matches Lightroom P1 🟡 crates/pipeline/src/lib.rs, crates/pipeline/src/tone.rs every slider exists and works, but the character of the result (tone curve shape, highlight recovery, texture / clarity / dehaze, noise reduction, sharpening, default look) is tuned by eye; there is no systematic side-by-side comparison against Lightroom on the same CC0 raws. Needs a fidelity suite: Lightroom reference renders kept only in the local plan/ (never committed), compared per slider and preset with a perceptual metric. Tuning that changes how existing settings render ships as a new process version (LRC-DEV-PROCESS, docs/process-versions.md)
LR-BEHAV-DRAGDROP Drag and drop P1 ✅ crates/ui-egui/src/lib.rs, crates/ui-egui/src/panels/grid.rs (drag_feedback), crates/ui-egui/src/panels/left.rs (drop_target, album_drag_over, top_level_drop_target, drag_auto_scroll) files → app (import); grid photos → an album row (adds the selection, with a count badge while dragging); sidebar album / folder rows → a folder (moves it; its name follows the pointer); a drag of albums or grid photos near the top or bottom edge of the sidebar scrolls it (faster nearer the edge, only while the pointer is over the sidebar)
LR-BEHAV-TOAST Toast notifications P1 ✅ crates/ui-egui/src/panels/mod.rs
LR-BEHAV-DIALOGS Dialogs fit the window P1 ✅ crates/ui-egui/src/panels/dialogs.rs (dialog_body, list_height), crates/ui-egui/src/tests_dialog_fit.rs a dialog taller than the window (Export on a 1366×768 screen, issue #781) never runs off it: its options scroll while the title bar and the Cancel / OK row stay on screen; lists that scroll on their own (What's New, All Metadata, shortcuts, smart-album rules, credits) get shorter first; dialogs that fit look as before; Import keeps its own resizable, scrolling window
LR-BEHAV-SIDEBAR-COLLAPSE Collapsible left-sidebar sections P2 ✅ crates/ui-egui/src/panels/left.rs (section, section_header), crates/ui-egui/src/sidebar.rs (SidebarLayout), crates/ui-egui/src/tests_panels.rs click the Albums, Local, By Date, Folders or Keywords header (or drive ui.clickWidget sidebarSection:<albums|local|byDate|folders|keywords>) to fold or unfold the section; a chevron before the title shows the state; the choice is kept across restarts (sidebar in the UI state; the collapsedSidebar list of older settings files is still read). the click itself is UI-only, like the By Date / Keywords disclosure triangles, and cmd:view.sidebarSection {section, collapsed} folds and unfolds for agents; photos dropped on a folded Albums section have no target
LR-BEHAV-SIDEBAR-SECTIONS Left-sidebar sections share one frame and one arrangement P2 ✅ crates/ui-egui/src/sidebar.rs (SidebarSection, SidebarLayout), crates/ui-egui/src/panels/left.rs (section, section_header, tree_toggle), crates/ui-egui/src/tests_panels.rs Albums, Local, By Date, Folders and Keywords are drawn by one section frame (the same gap above, header, fold and accessibility name) from one arrangement, sidebar in the UI state: a list of {id, hidden, collapsed} top to bottom, also settable with ui.set. Whatever is read, every section is in it exactly once (unknown and repeated entries are dropped, the rest follow in the usual order), so a damaged or newer settings file never loses the other settings. Rows that open (album folders, Local folders, dates, library folders, keywords) share tree_toggle: the same triangle, the same remembered open state, announced as Expand / Collapse. A section with nothing to list (Folders before anything is imported, Keywords before any is added) stays, open, with a one-line note in place of rows (sidebarEmpty:<id>) instead of disappearing. ui.set reads sidebar strictly (SidebarLayout::parse): an unknown or repeated section, or a flag that is not true / false, is refused and changes nothing; sections left out follow in the usual order, shown and open; the older collapsedSidebar list still folds and unfolds. A section may have one button at the end of its header, declared in one place (header_button: an icon, a tooltip, and either an action or a menu): the frame draws it at the visible edge in the header and its pinned copy, keeps its click from folding the section and leaves it room beside a folded section's count. Albums has + (icon:albumNew: Create Album / Smart Album / Folder…), Keywords has + (icon:keywordNew: Create Keyword Tag, for a keyword inside the default parent, not inside what the Keyword List has picked; once made, Keywords is shown and opened down to it). My Photos (All Photos, Recently Added, Picks, Missing Photos, Recently Deleted) is not a section: it stays on top
LR-BEHAV-SIDEBAR-HIDE Show and hide left-sidebar sections P2 ✅ cmd:view.sidebarSection, cmd:view.sidebarReset, crates/ui-egui/src/sidebar.rs, crates/ui-egui/src/panels/left.rs (sections_menu), crates/ui-egui/src/menubar.rs, crates/ui-egui/src/tests_panels.rs View ▸ Sidebar Sections lists Albums, Local, By Date, Folders and Keywords in the sidebar's order, each checked while shown; unchecking one takes its header and rows off the sidebar, checking it puts it back where and how it was (still folded if it was). The same list opens with a right-click on any section header (with Hide This Section first; widgets sidebarMenu:hide, sidebarMenu:section:<id>, sidebarMenu:reset) and on the My Photos title (sidebarTitle), which is always there, so hidden sections can always come back. Reset Sidebar Sections (enabled once something changed) shows and opens every section in the usual order. view.sidebarSection {section, show?, collapsed?} flips shown / hidden when neither flag is given and refuses an unknown section or a flag that is not true / false. Hiding the section of what the grid shows (an album, a keyword filter) leaves the grid as it is; an album made while Albums is hidden or folded brings Albums back, open. Kept with the UI state (sidebar). My Photos itself can't be hidden
LR-BEHAV-SIDEBAR-REORDER Reorder left-sidebar sections P2 ✅ cmd:view.sidebarMoveSection, crates/ui-egui/src/sidebar.rs (move_before, move_by), crates/ui-egui/src/panels/left.rs (section_drag, section_drop_before, section_drag_feedback), crates/ui-egui/src/tests_panels.rs drag a section by its header: its name follows the pointer, a line (sidebarSectionDrop) shows the gap it would go to (above the section whose upper half the pointer is over, or below the last), releasing over the sidebar moves it there, Esc or releasing elsewhere leaves it; the sidebar scrolls when the drag nears its top or bottom edge, also over a pinned header (a section picked up by the pinned header scrolls once the pointer has left it). A press on a header's button is never a section drag. A click on the header still folds the section, and an album dragged onto the Albums header is still an album drag. The header's menu has Move Up / Move Down (sidebarMenu:moveUp / moveDown), one shown section at a time, stepping over hidden ones, disabled at the ends. view.sidebarMoveSection {section, direction: up|down} or {section, before: <section>|null} (null: last) answers {moved, order}. The order is kept with the UI state (sidebar). Recently Deleted is listed with My Photos (below Picks / Missing Photos), not after the sections. Not here: dragging My Photos, or rows from one section into another
LR-BEHAV-SIDEBAR-STICKY Pinned section header and section look P2 ✅ crates/ui-egui/src/panels/left.rs (pinned_header, section_header, empty_hint), crates/ui-egui/src/tests_panels.rs while the rows of an open section are in view but its header has scrolled away, a copy of the header (sidebarSectionPinned:<id>, with the section's buttons and menu) stays at the top of the sidebar; the next section's header pushes it out; folding from it brings the section's own header into view. Rows scrolled beneath the pinned header are covered by it: an album or photos dropped on the pinned Albums header go to the top level / nowhere, never to the row under it; folding from it keeps the sideways scroll. Sections are told apart by a hairline in the gap above each; the fold chevron sits in the gutter, in the column of the rows' triangles (sidebarSectionChevron:<id>); a header highlights under the pointer; a folded section shows how many top-level rows it holds (sidebarSectionCount:<id>, with Show Photo Counts); a count of 0 is fainter. Not here: a height of its own per section (one scroll area serves the whole sidebar), a count for Recently Added, Local's Browse Folder… as a header button
LR-BEHAV-PANEL-RESIZE Resizable side panels P1 ✅ crates/ui-egui/src/panels/mod.rs (resizable_side), crates/ui-egui/src/state.rs (LEFT_WIDTH, RIGHT_WIDTH), crates/ui-egui/src/tests_panels.rs drag the left sidebar's right edge (200–480 pt) or the right panel's left edge (250–520 pt); the photo area keeps ≥ 360 pt; widths are kept across panels, views and restarts (leftWidth / rightWidth in the UI state); the Presets column stays fixed
LR-BEHAV-EMPTY-STATES Empty states P1 ✅ crates/ui-egui/src/panels/mod.rs (empty_message)
LR-BEHAV-TOOLTIPS Tooltips with shortcuts P0 ✅ crates/ui-egui/src/panels/bottombar.rs
LR-BEHAV-TEXTFIELD Text fields behave alike P1 🟡 crates/ui-egui/src/text_field.rs, crates/ui-egui/src/panels/topbar.rs, crates/ui-egui/src/widgets.rs, crates/ui-egui/src/panels/right.rs (meta_field), crates/ui-egui/src/panels/dialogs.rs, crates/ui-egui/src/panels/unnamed.rs one shared field: right-click Cut / Copy / Paste / Select All (no Paste in the browser), Esc gives back the text from before the edit, Return / leaving / Esc reported to the caller, optional select-on-focus; agents use ui.clipboard (MCP clipboard) and ui.inspect → copied. Used by Search Photos, slider values, the Info panel's metadata fields (multi-line captions), the one-field dialogs (New / Rename Album, New Smart Album, Rename / Merge Keywords, text prompts) and the unnamed faces' name field so far; the other text fields (most dialogs, import, settings, masking, keywords …) still use egui's own and have no menu
LR-BEHAV-HEADLESS Headless UI snapshots P2 ✅ apps/lightcraft-cli/src/main.rs, crates/ui-egui/src/headless.rs, docs/control-protocol.md snapshot rasterizes the UI and defaults photo development to the CPU before the first frame, without adapter discovery; scaled dimensions are validated before allocation, and UI zoom preserves the requested physical viewport; JSON-lines control scripts are supported
LR-BEHAV-ACCESS Accessibility P2 🟡 crates/ui-egui/src/widgets.rs, crates/ui-egui/src/text_field.rs, crates/ui-egui/src/panels/grid.rs, crates/ui-egui/tests/accessibility.rs AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control, value, range and step and take Increment / Decrement, buttons / icon buttons / dropdowns / section headers / sources their labels and state, text fields their caption, grid thumbnails file name, rating, flag and label. tests/accessibility.rs fails on an unnamed control, a slider without range or an untitled dialog in every view, Develop tool and 13 dialogs, and checks that Crop and Remove sliders are reached with Tab. Canvas handles, spot and mask placement are pointer-only; not tried with a real screen reader (audit)
LR-BEHAV-LOCALE Language options P2 ✅ cmd:app.language.english, cmd:app.language.simplifiedChinese, cmd:app.language.traditionalChinese, cmd:app.language.japanese, cmd:app.language.portuguese, cmd:app.language.french, cmd:app.language.spanish, cmd:app.language.german, cmd:app.language.russian, cmd:app.language.ukrainian Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md)
LR-BEHAV-LOCALIZE Localisation P2 🟡 crates/ui-egui/src/i18n.rs, crates/ui-egui/locales/, docs/localization.md, docs/localization-zh-hans.md, docs/localization-zh-hant.md, docs/localization-ja.md, docs/localization-pt-br.md, docs/localization-es.md, docs/localization-de.md, docs/localization-ru.md One table entry per language (code, endonym, ISO 15924 script, catalog): English, Simplified Chinese, Traditional Chinese (Taiwan), Japanese, Brazilian Portuguese, Spanish, German, Russian and Ukrainian UI with a persisted language preference and craft-fonts regular/bold faces per script (a variable face, as for Arabic, is drawn heavier for bold from its one file); menus, edit, crop/masks, settings, import/export and primary progress messages. Adding a language registers the locale and its language command, plus two catalogs. Stock presets/profiles, history steps, library headings, date headings and capture times are localised (user-named items stay verbatim); the Ukrainian catalog covers every existing catalog key, command/control label and rule-editor label; the German catalog reports newly added gaps; the other catalogs fall back to English for messages they lack. Technical errors remain English; release notes are translated in German and Ukrainian; craft-fonts faces for other scripts (Thai, Arabic, …) are UI fallbacks in every language, for file, folder and album names (#736); Traditional Chinese borrows the Simplified Chinese face (no TC face in craft-fonts yet), and the web build embeds the Japanese face in WASM while loading the Simplified Chinese face from a separate font file.
LR-BEHAV-LEARN Tutorials OOS 🚫
LR-BEHAV-WHATSNEW What's new P2 ✅ cmd:app.whatsNew, docs/whats-new.md Help ▸ What's New: release highlights
LR-BEHAV-AI-EA Early-access badges P2 ⬜
LR-BEHAV-TITLEBAR Title bar drag and double-click zoom (macOS) P2 🟡 crates/ui-egui/src/titlebar.rs, crates/ui-egui/src/panels/topbar.rs, crates/ui-egui/src/tests_titlebar.rs the native title bar is hidden on macOS, so the top bar's empty space (including the traffic-light margin) moves the window and a double-click zooms or restores it; buttons and the search field keep their clicks; ignored in full screen. Zoom is winit's Maximized: it does not follow System Settings › “Double-click a window's title bar to” (Fill / Minimize). a press that starts on a button or the search field never moves the window. Known gap: egui-winit reads the zoomed state only at start-up and after our own Maximized command, so a window zoomed another way (green button, Window ▸ Zoom, snapping) makes the first double-click a no-op before the toggle catches up. Checked by hand on a real macOS window (drag, zoom, restore)

Y. Menus

From 04-menu-tree.md. ✅ = a command exists and is reachable from the UI (button, panel or shortcut). There is no visible menu bar yet: the menu model is only exposed through the control channel (ui.menu.list).

Id Feature Tier Status Evidence Missing / notes
MENU-BAR Menu bar rendering P1 ✅ crates/ui-egui/src/menubar.rs, apps/lightcraft/src/native_menu.rs native macOS menu bar (muda) with live labels/enabled/checked; in-window menus on web/Windows/Linux (with one menu open, the pointer on another title switches to it, as in native menu bars); ⌫ and X stay egui-handled (contextual), so they show no key in the native menu; a scrolled menu draws no scrollbar over its rows: the ▲ / ▼ strips, the wheel and dragging scroll it (issue #691: egui's floating bar hid the shortcut text's last character; crates/ui-egui/src/tests_menubar.rs)
MENU-APP-ABOUT About P2 ✅ cmd:app.about
MENU-APP-SETTINGS Settings… P0 ✅ cmd:app.settings app menu on macOS, Edit menu elsewhere
MENU-APP-UPDATES Check for updates P2 ⬜
MENU-APP-SYNC Sync status / pause OOS 🚫
MENU-APP-SIGNOUT Sign out OOS 🚫
MENU-APP-HIDE Hide / hide others / show all P1 ✅ apps/lightcraft/src/native_menu.rs the system's own items in the app menu (⌘H, ⌥⌘H)
MENU-APP-QUIT Quit P0 ✅ cmd:app.quit, apps/lightcraft/src/native_menu.rs macOS: app menu (native); elsewhere: File → Quit LightCraft, always the last item in its own group (after commands the File layout does not list)
MENU-FILE-ADDPHOTOS Import Photos… (was Add Photos…) P0 ✅ cmd:file.addPhotos, crates/ui-egui/src/menus.rs first item of File, ⇧⌘I; file picker → the import review
MENU-FILE-ADDFOLDER Import from Folder… (was Add Folder…) P0 ✅ cmd:file.addFolder, cmd:library.importPreview folder picker (desktop) → the import review, subfolders included; path param for agents. Worded as importing: it doesn't save a Local location (that is Local → Browse Folder…)
MENU-FILE-MIGRATE Migrate photos OOS 🚫
MENU-FILE-NEWALBUM New Album… P0 ✅ cmd:dialog.newAlbum
MENU-FILE-NEWFOLDER New Folder… P0 ✅ cmd:dialog.newFolder ⇧⌘N
MENU-FILE-NEWSMART New Smart Album… P1 ✅ cmd:dialog.newSmartAlbum saves the current view (source + filter)
MENU-FILE-IMPORTPROFILES Import Profiles & Presets… P1 ✅ cmd:file.importPresets presets in every common format and .cube LUT profiles (see LR-IMP-PROFILES)
MENU-FILE-EXPORT Export… P0 ✅ cmd:dialog.export
MENU-FILE-EXPORTPREV Export with Previous P0 ✅ cmd:app.exportPrevious ⌥⇧⌘E
MENU-FILE-EXPORTPRESETS Export preset submenu P0 ✅ cmd:app.export (preset), crates/ui-egui/src/menubar.rs (Export with Preset) built-ins, then user presets, then Custom… (the dialog); exports to the last folder
MENU-FILE-SHARE Share / get link / invite OOS 🚫
MENU-FILE-PHOTOSHOP Edit in external editor P2 ✅ cmd:photo.editInPhotoCraft, cmd:photo.editInExternal, cmd:photo.openOriginalInExternal Photo ▸ Edit In ▸ Edit in PhotoCraft / Edit in Other Editor / Open Original in Other Editor (RAW, no copy), and Edit In on a photo's right-click menu
MENU-FILE-SHOWFINDER Show in Finder P0 ✅ cmd:app.showInFinder ⌘R; Explorer on Windows, the folder on Linux; disabled for demo scenes and on the web
MENU-FILE-OFFLINE Store album locally P2 🚫 not applicable: local-first
MENU-FILE-CLOSE Close Window P1 ✅ apps/lightcraft/src/native_menu.rs the system's Close Window item at the end of File (⌘W)
MENU-EDIT-UNDO Undo P0 ✅ cmd:edit.undo label does not name the step
MENU-EDIT-REDO Redo P0 ✅ cmd:edit.redo
MENU-EDIT-COPYPASTE Copy / paste (edit settings) P0 ✅ cmd:develop.copy, cmd:develop.paste
MENU-EDIT-CHOOSECOPY Choose Edit Settings to Copy… P0 ✅ cmd:dialog.copySettings Return copies, Esc cancels
MENU-EDIT-PASTESELECTED Paste Selected Settings P0 ✅ cmd:dialog.pasteSettings, cmd:develop.paste (groups) checklist prefilled with the copied groups; only copied groups can be pasted; Return pastes, Esc cancels
MENU-EDIT-SELECTALL Select All P0 ✅ cmd:library.selectAll
MENU-EDIT-SELECTNONE Select None P0 ✅ cmd:library.selectNone
MENU-EDIT-SELECTBY Select by flag / rating P1 ✅ cmd:library.selectBy Edit → Select by: picks, rejects, unflagged, ★…★★★★★ and higher, unrated, colour labels; add extends the selection
MENU-EDIT-FIND Find… P0 ✅ cmd:view.focusSearch, crates/ui-egui/src/panels/topbar.rs ⌘F focuses the search field
MENU-VIEW-PHOTOGRID Photo Grid P0 ✅ cmd:view.photoGrid
MENU-VIEW-SQUAREGRID Square Grid P0 ✅ cmd:view.squareGrid
MENU-VIEW-DETAIL Detail P0 ✅ cmd:view.detail
MENU-VIEW-EDIT Edit P0 ✅ cmd:panel.edit
MENU-VIEW-FULLSCREENPREVIEW Full Screen Preview P1 ✅ cmd:view.fullScreenPreview
MENU-VIEW-LIGHTSOUT Lights Out P2 ✅ cmd:view.lightsOut everything but the photo veiled; panels stay put
MENU-VIEW-ENTERFULLSCREEN Enter Full Screen P1 ✅ cmd:view.enterFullScreen
MENU-VIEW-PHOTOSPANEL Show/Hide photos panel P0 ✅ cmd:view.leftPanel
MENU-VIEW-FILMSTRIP Show/Hide filmstrip P0 ✅ cmd:view.filmstrip
MENU-VIEW-INFO Show/Hide info P0 ✅ cmd:panel.info
MENU-VIEW-KEYWORDS Show/Hide keywords P0 ✅ cmd:panel.keywords
MENU-VIEW-ACTIVITY Show/Hide activity (comments) OOS 🚫 our History panel is panel.activity
MENU-VIEW-VERSIONS Show/Hide versions P1 ✅ cmd:panel.versions
MENU-VIEW-HISTOGRAM Show/Hide histogram P0 ✅ cmd:view.histogram
MENU-VIEW-INFOOVERLAY Show info overlay P1 ✅ cmd:view.infoOverlay cycles
MENU-VIEW-SHOWORIGINAL Show Original P0 ✅ cmd:view.showOriginal
MENU-VIEW-BEFOREAFTER Before/After submenu P0 ✅ cmd:view.beforeAfter, cmd:view.beforeAfterSplit, cmd:view.beforeAfterTopBottom, cmd:view.beforeAfterSplitTopBottom
MENU-VIEW-ZOOM Zoom in / out / toggle / fit / 1:1 P0 ✅ cmd:view.zoomIn, cmd:view.zoomOut, cmd:view.zoomToggle, cmd:view.zoomFit, cmd:view.zoom100
MENU-VIEW-CLIPPING Show Clipping P0 ✅ cmd:view.clipping
MENU-VIEW-MASKOVERLAY Mask overlay / cycle colour P0 ✅ cmd:view.maskOverlay, cmd:view.maskOverlayMode, cmd:view.maskOverlayColor colour cycles through the panel's swatches (no params) or takes color / opacity
MENU-VIEW-INCLUDESUBFOLDERS Include subfolders P1 ✅ cmd:library.browse (subfolders) toggle in the folder header; imports of folders are recursive
MENU-VIEW-SORT Sort submenu P0 ✅ cmd:library.sort, cmd:library.shuffle Random + Reshuffle; no colour-label key
MENU-VIEW-STACKS Expand/collapse stacks P1 ✅ cmd:stack.expandAll, cmd:stack.collapseAll
MENU-VIEW-PHOTOCOUNT Show photo counts P2 ✅ cmd:view.photoCounts View ▸ Show Photo Counts toggles the left panel's counts
MENU-VIEW-SIDEBARSECTIONS Sidebar sections P2 ✅ cmd:view.sidebarSection, cmd:view.sidebarReset View ▸ Sidebar Sections ▸ a checkable item per left-sidebar section, then Reset Sidebar Sections (see LR-BEHAV-SIDEBAR-HIDE)
MENU-VIEW-HDR HDR display options P2 ⬜
MENU-PHOTO-ADDTOALBUM Add to album P0 ✅ cmd:album.addPhotos off for a Local-only selection, with an Add to My Photos First hint item (issue #684)
MENU-PHOTO-REMOVEFROMALBUM Remove from album P0 ✅ cmd:album.removePhotos
MENU-PHOTO-RATE Rate submenu P0 ✅ cmd:photo.rate
MENU-PHOTO-FLAG Flag submenu P0 ✅ cmd:photo.flag
MENU-PHOTO-LABEL Colour label submenu P1 ✅ cmd:photo.label, cmd:dialog.labelNames shows custom names; Edit Label Names…
MENU-PHOTO-ROTATE Rotate left / right P0 ✅ cmd:photo.rotateLeft, cmd:photo.rotateRight
MENU-PHOTO-FLIP Flip horizontal / vertical P0 ✅ cmd:photo.flipHorizontal, cmd:photo.flipVertical
MENU-PHOTO-CREATEVERSION Create Version… P1 ✅ cmd:version.create no name prompt
MENU-PHOTO-STACK Stack submenu P1 ✅ cmd:stack.group, cmd:stack.ungroup, cmd:dialog.autoStack
MENU-PHOTO-MERGE Photo merge submenu P2 ✅ cmd:dialog.mergeHdr, cmd:dialog.mergePanorama, cmd:dialog.mergeHdrPanorama, cmd:merge.hdrLast
MENU-PHOTO-ENHANCE Enhance… P2 ⬜
MENU-PHOTO-AUTO Auto settings P0 ✅ cmd:develop.auto
MENU-PHOTO-BW Convert to B&W P0 ✅ cmd:develop.treatment
MENU-PHOTO-RESET Reset edits / crop P0 ✅ cmd:develop.reset, cmd:crop.reset
MENU-PHOTO-UPDATEAI Update AI settings P2 ⬜
MENU-PHOTO-RENAME Rename N photos… P1 ✅ cmd:dialog.rename live “Rename N Photos…” label
MENU-PHOTO-CAPTURETIME Edit capture time… P1 ✅ cmd:dialog.captureTime
MENU-PHOTO-COVER Set as album cover P2 ✅ cmd:album.setCover
MENU-PHOTO-DELETE Delete N photos… P0 ✅ cmd:photo.delete asks first (Settings ▸ General, on by default, #1239); static label
MENU-PHOTO-MOVETOCLOUD Move/copy to cloud OOS 🚫
MENU-WINDOW-MINIMIZE Minimize / zoom P1 ✅ apps/lightcraft/src/native_menu.rs the system's Minimize (⌘M) and Zoom items at the top of Window
MENU-WINDOW-PANELS Panel switches P0 ✅ cmd:panel.edit, cmd:panel.crop, cmd:panel.remove, cmd:panel.masking, cmd:panel.presets, cmd:panel.versions
MENU-WINDOW-BRINGFRONT Bring all to front P2 ✅ apps/lightcraft/src/native_menu.rs the system's item at the end of Window
MENU-HELP-HELP Help P2 ✅ cmd:app.help opens the documentation
MENU-HELP-TUTORIALS Tutorials OOS 🚫
MENU-HELP-WHATSNEW What's new P2 ✅ cmd:app.whatsNew
MENU-HELP-SHORTCUTS Keyboard shortcuts P1 ✅ cmd:app.shortcuts
MENU-HELP-FEEDBACK Send feedback P2 ✅ cmd:app.feedback opens a new issue on the project's GitHub
MENU-HELP-LOGFOLDER Open log folder P2 ✅ cmd:app.openLogFolder an extra (the reference app has no such item): Help ▸ Open Log Folder reveals logs/lightcraft.log in the system file manager (Finder, Explorer, or the folder on Linux); off on the web and in --memory sessions, which keep no log (issue #260)
MENU-HELP-SYSINFO System info P2 ✅ cmd:app.systemInfo, cmd:library.info Help ▸ System Info…: version, OS, CPU threads, GPU, memory budget, preview size, library, timings; Copy to Clipboard; one Close button, like About (#782); JSON for agents (open: false)
MENU-CTX-GRID Photo context menu P0 ✅ crates/ui-egui/src/panels/grid.rs (context_menu) rate, flag, label, add to / remove from album, rename, virtual copy, version, stack, copy / paste / paste selected, reset, merge, rotate, show in Finder, export / export with preset, set as album cover (in an album), delete (restore / delete permanently in Recently Deleted)
MENU-CTX-DETAIL Loupe context menu P1 ✅ crates/ui-egui/src/panels/detail.rs Zoom submenu (fit, 100%, in, out), then the photo menu
MENU-CTX-ALBUM Album / folder row menu P0 ✅ crates/ui-egui/src/panels/left.rs (folder_menu), cmd:album.move, cmd:dialog.export, cmd:file.addPhotos, cmd:file.addFolder, cmd:file.addFromDevice add selected, Import ▸ Import Photos… / Import from Folder… / Import from Device ▸ (one entry per camera or card, as in File ▸ Import from Device; the import review opens with that album chosen: {album} on those commands; button:albumImport), export album (dialog / preset), New ▸ Create Album / Smart Album / Smart Album from Filter / Folder (on a folder: made inside it), move to a folder or the top level, rename, delete; smart albums: update rules
MENU-CTX-MASK Mask / component menu P0 ✅ crates/ui-egui/src/panels/masking.rs (mask_menu, component_row_menu), cmd:mask.component mask rows: duplicate (and invert), invert, show/hide, move, rename, delete; component rows (right-click or "…"): invert, duplicate, mode (add / subtract / intersect), intersect with / subtract a new component, rename (also double-click), delete (the last one deletes the mask)
MENU-CTX-PRESET Preset menu P1 ✅ crates/ui-egui/src/panels/presets.rs favourite, update with current settings, rename, move to group, delete; group: export
MENU-CTX-PROFILE Profile favourites P2 ✅ cmd:profile.favorite, crates/ui-egui/src/panels/profiles.rs star in the profile browser; Favorites group first
MENU-CTX-VERSION Version menu P1 ✅ crates/ui-egui/src/panels/right.rs (versions) restore, update with current settings, rename, set as before, delete
MENU-CTX-KEYWORD Keyword chip menu P1 ✅ crates/ui-egui/src/panels/keywording.rs (menu) acts on the selected photos: add to all selected photos (a keyword only some have), remove from selected photos, show photos with keyword, Edit Keyword Tag…; deleting a keyword from the library is the Keyword List's

Z. Keyboard shortcuts (desktop)

From 06-shortcuts.md part 1. Evidence is our binding; conflicts are explained in UI parity → Shortcuts.

Id Feature Tier Status Evidence Missing / notes
KEY-CROP Crop & rotate — C P0 ✅ cmd:panel.crop
KEY-DETAIL Detail — D P0 ✅ cmd:view.detail
KEY-EDIT Edit — E P0 ✅ cmd:panel.edit
KEY-FULLSCREEN Full-screen preview — F P1 ✅ cmd:view.fullScreenPreview
KEY-LIGHTSOUT Lights Out — Shift+L (Classic's L is Linear Gradient here) P2 ✅ cmd:view.lightsOut On → Dim (80 %) → Off; Esc turns the lights on
KEY-GRID Grid — G P0 ✅ cmd:view.gridToggle, cmd:view.photoGrid G toggles Photo Grid ↔ Square Grid
KEY-INFO Info — I P0 ✅ cmd:panel.info
KEY-KEYWORDS Keywords — K P0 ✅ cmd:panel.keywords
KEY-CLIPBOARD Copy / paste edit settings — ⌘C / ⌘V P0 ✅ cmd:develop.copy, cmd:develop.paste, crates/ui-egui/src/shortcuts.rs (matches) ⌘X has nothing to cut outside text fields; on Windows and Linux, where the windowing layer sends Ctrl+C / Ctrl+V as clipboard events rather than key presses, those run the shortcuts too (⇧Insert pastes plainly); there Ctrl+V only arrives while the system clipboard holds text (egui-winit drops it otherwise), so with an empty clipboard Paste Settings needs the menu
KEY-UNDOREDO Undo / redo — ⌘Z / ⇧⌘Z P0 ✅ cmd:edit.undo, cmd:edit.redo
KEY-MINIMIZE Minimize — ⌘M P1 ✅ apps/lightcraft/src/native_menu.rs native Window ▸ Minimize
KEY-AUTO Auto — ⇧A P0 ✅ cmd:develop.auto
KEY-PHOTOSHOP External editor — ⇧⌘E P2 ✅ cmd:photo.editInPhotoCraft, cmd:photo.editInExternal ⇧⌘E as observed (Edit in PhotoCraft, LightCraft's Photoshop); ⌥⌘E the other editor; the export dialog is ⇧E
KEY-ROTATE Rotate — ⌘[ / ⌘] P0 ✅ cmd:photo.rotateLeft, cmd:photo.rotateRight
KEY-ZOOM Zoom in / out — ⌘= / ⌘− P0 ✅ cmd:view.zoomIn, cmd:view.zoomOut, crates/ui-egui/src/tests_zoom_keys.rs the keys zoom the photo only: egui's own interface zoom on ⌘= / ⌘− / ⌘0 (Ctrl on Windows and Linux, where no native menu bar took the keys first) is off (issue #566)
KEY-SELECTALL Select all — ⌘A P0 ✅ cmd:library.selectAll
KEY-SELECTNONE Select none — ⌘D P0 ✅ cmd:library.selectNone secondary binding (primary ⌘⇧A)
KEY-PASTESELECTED Paste selected — ⇧⌘V P0 ✅ cmd:dialog.pasteSettings
KEY-PREFS Settings — ⌘, P0 ✅ cmd:app.settings
KEY-SEARCH Search — ⌘F P0 ✅ cmd:view.focusSearch
KEY-VISUALIZESPOTS Visualize spots — A P1 ✅ cmd:view.visualizeSpots
KEY-CYCLEOVERLAY Cycle overlay — O P0 ✅ cmd:view.maskOverlay, cmd:view.maskOverlayMode, cmd:view.cropOverlay O toggles the mask overlay; ⇧O cycles mask overlay modes while masking, crop overlays elsewhere
KEY-PHOTOSPANEL Photos panel — P P0 🟡 cmd:view.leftPanel bound to ⌘⇧L; P = pick
KEY-LINEAR Linear gradient — L P0 ✅ cmd:tool.linear
KEY-RADIAL Radial gradient — R P0 ✅ cmd:tool.radial
KEY-CLIPPING Clipping — J P0 ✅ cmd:view.clipping
KEY-WB White-balance selector — W P0 ✅ cmd:tool.wbPicker
KEY-FILMSTRIP Filmstrip — / P0 ✅ cmd:view.filmstrip
KEY-SHOWORIGINAL Show original — \ P0 ✅ cmd:view.showOriginal
KEY-TOGGLEZOOM Toggle zoom — Space P0 ✅ cmd:view.zoomToggle Space is a secondary binding (primary Z)
KEY-MASKCOLOR Cycle mask colour — ⇧O P1 ✅ cmd:view.maskOverlayColor, crates/ui-egui/src/shortcuts.rs ⇧O while masking cycles the overlay colour (while cropping: the guides' orientation); the overlay mode is in View ▸ Cycle Mask Overlay Mode
KEY-EXPORTPREV Export with previous — ⌘E P0 ✅ cmd:app.exportPrevious ⌘E (alias) and ⌥⇧⌘E (Classic)
KEY-EXPORTDIALOG Export dialog — ⇧E P0 ✅ cmd:dialog.export secondary binding (primary ⌘⇧E)
KEY-ENTERFULLSCREEN Window full screen — ⇧⌘F P1 ✅ cmd:view.enterFullScreen
KEY-STACK Group / ungroup stack — ⌘G / ⇧⌘G P1 ✅ cmd:stack.group, cmd:stack.ungroup also S expand/collapse, ⇧S top of stack
KEY-GUIDEDUPRIGHT Guided Upright — ⇧G P1 ✅ cmd:tool.guidedUpright, cmd:geometry.upright opens Crop & Geometry with Guided Upright on and the guide tool active; G toggles Photo Grid ↔ Square Grid (cmd:view.gridToggle) as observed
KEY-HIDE Hide / hide others — ⌘H / ⌥⌘H P1 ✅ apps/lightcraft/src/native_menu.rs native app-menu items
KEY-QUIT Quit — ⌘Q P0 ✅ cmd:app.quit ⌘Q (Ctrl+Q off macOS)
KEY-CREATEVERSION Create version — ⇧M P1 ✅ cmd:version.create secondary binding (primary ⌘⇧S)
KEY-CLOSEWINDOW Close window — ⌘W P1 ✅ apps/lightcraft/src/native_menu.rs native File ▸ Close Window
KEY-DELETE Delete photo — ⌫ P0 ✅ cmd:photo.delete
KEY-ADDPHOTOS Import photos — ⇧⌘I P0 ✅ cmd:file.addPhotos
KEY-VERSIONS Versions panel — ⇧V P1 ✅ cmd:panel.versions
KEY-SECTIONS Expand/collapse edit sections — ⌘1…⌘6 P1 ✅ cmd:section.light, cmd:section.color, cmd:section.effects, cmd:section.detail, cmd:section.optics ⌘1–⌘5 (Light, Color, Effects, Detail, Optics) as observed; our Edit panel has no Lens Blur / Geometry section for ⌘6 / ⌘7; Zoom 100% moved to ⌥⌘0
KEY-PRESETS Presets panel — ⇧P P0 ✅ cmd:panel.presets Shift+P opens Presets outside the two library grids; in grids it picks and advances (Classic convention). Menu hides the Presets key there; shortcut help follows the view
KEY-HISTOGRAM Histogram — ⌘0 P0 🟡 cmd:view.histogram bound to ⌘⇧H; ⌘0 = zoom to fit
KEY-BRUSHSIZE Brush size — [ / ] P0 ✅ cmd:brush.smaller, cmd:brush.larger Masking brush and Remove tool (and its selected spot)
KEY-BRUSHFEATHER Brush feather — ⇧[ / ⇧] P0 ✅ cmd:brush.featherLess, cmd:brush.featherMore
KEY-BRUSH Brush — B P0 ✅ cmd:tool.brush
KEY-HEAL Remove / heal — H P0 ✅ cmd:panel.remove
KEY-MERGE HDR / panorama merges — ⌃H ⇧⌃H ⌃M ⇧⌃M P2 ✅ cmd:dialog.mergeHdr, cmd:dialog.mergePanorama, cmd:merge.hdrLast, cmd:merge.panoramaLast
KEY-PICK Pick — Z P0 🟡 cmd:photo.pick bound to P; Z toggles zoom
KEY-UNFLAG Unflag — U P0 ✅ cmd:photo.unflag
KEY-REJECT Reject — X P0 ✅ cmd:photo.reject swaps crop aspect while cropping
KEY-RATING Ratings — 0…5 P0 ✅ crates/ui-egui/src/shortcuts.rs → cmd:photo.rate 0 clears, 1–5 set; Shift applies and advances; native macOS plain keys stay with egui; grid multi-selection and undo/redo tested
KEY-LABELS Labels — 6…9 P1 ✅ crates/ui-egui/src/shortcuts.rs → cmd:photo.label 6 red, 7 yellow, 8 green, 9 blue; Shift+6–9 labels and advances; purple/none available from menus
KEY-MASKING Masking — M P0 ✅ cmd:panel.masking
KEY-ERASE Erase while held — ⌥ P0 ✅ crates/ui-egui/src/panels/detail.rs, crates/ui-egui/src/panels/masking.rs ⌥ while painting flips Add ↔ Erase for that stroke; Add/Erase buttons too
KEY-RATEADVANCE Rate and advance — ⇧0…5 P1 ✅ crates/ui-egui/src/shortcuts.rs → cmd:photo.rate (advance)
KEY-FLAGADVANCE Flag and advance — ⇧Z / ⇧X / ⇧U P1 ✅ cmd:photo.flag (advance), crates/ui-egui/src/shortcuts.rs Shift+Z pick, Shift+X reject, Shift+U clear; Shift+P also picks and advances in the library grids; exactly one step with Auto Advance enabled
KEY-NEXTPREV Next / previous — → / ←; above / below — ↑ / ↓ P0 ✅ cmd:library.next, cmd:library.previous, cmd:library.up, cmd:library.down ↑ / ↓ go to the cell above / below in the grids (nearest by position in justified rows; the top / bottom row stays put), one photo in the filmstrip (issue #1171); no vim keys by default (H, J, K, L are taken), but they can be assigned in the keyboard shortcuts editor
KEY-BA-CYCLE Before/after — Y P0 ✅ cmd:view.beforeAfter toggles side by side (no cycling)
KEY-BA-TOPBOTTOM Before/after top/bottom — ⌥Y P1 ✅ cmd:view.beforeAfterTopBottom, cmd:view.beforeAfterSplitTopBottom ⌥Y, ⇧⌥Y
KEY-BA-SPLIT Split before/after — ⇧Y P0 ✅ cmd:view.beforeAfterSplit
KEY-CHOOSECOPY Choose settings to copy — ⇧⌘C P0 ✅ cmd:dialog.copySettings
KEY-RESETALL Reset all — ⇧⌘R P0 ✅ cmd:develop.reset
KEY-GENAI Generative toggles / variations — ⌥⇧G, ⌥←/→ OOS 🚫
KEY-DETECT Detect objects — ⌥⇧O P2 ⬜
KEY-CROP-CONSTRAIN Lock crop aspect — A P1 ✅ cmd:crop.aspect (toggle, current) A while cropping locks the current shape / unlocks
KEY-CROP-SWAP Swap crop orientation — X P0 ✅ cmd:crop.rotateAspect
KEY-CROP-OVERLAYORIENT Crop overlay orientation — ⇧O P1 ✅ cmd:view.cropOverlayOrientation while cropping: O cycles the overlay, ⇧O its orientation
KEY-CROP-RESET Reset crop — ⌥⌘R P0 ✅ cmd:crop.reset
KEY-STRAIGHTEN Straighten while held — ⌘ drag P1 ✅ cmd:crop.straighten, crates/ui-egui/src/panels/detail.rs (straighten_overlay) ⌘-drag in Crop draws a level line; the crop tool stays active
KEY-SLIDER-RESET Reset slider — double-click P0 ✅ crates/ui-egui/src/widgets.rs
KEY-SLIDER-NUDGE Nudge slider — ↑/↓ P1 ✅ crates/ui-egui/src/widgets.rs (nudged), cmd:develop.adjust ↑/↓ over any slider: ≈ 1/200 of its range (exposure 0.05, most sliders 1, temperature 50 K); ⇧ ×5; one undo step each
KEY-SLIDER-TYPE Type a slider value P1 ✅ crates/ui-egui/src/widgets.rs (typed_value), crates/ui-egui/src/text_field.rs, crates/ui-egui/src/widgets/tests_numeric_entry.rs (opening_release_honors_return_and_escape_immediately), crates/ui-egui/src/tests_keymap.rs (opening_numeric_release_keeps_keys_in_the_editor) click the value next to any slider's name and type one (1.5, +0,25, -20, 5600 K): Return or clicking away applies it on the control's steps and range as one undo step, Esc keeps the old value; issue #322; Return/Escape take effect on the opening release, retaining the stored value on Escape; typing stays in the editor without triggering global shortcuts
KEY-SHORTCUTS Shortcut list — ⌘/ P1 ✅ cmd:app.shortcuts
KEY-HELP Help — F1 P2 ✅ cmd:app.help
KEY-VIDEO-PLAY Play/pause video — Space P1 ⬜
KEY-ESC Leave tool / view — Esc P0 ✅ cmd:view.back, crates/ui-egui/src/icons.rs (navigation_arrowheads_have_both_halves), crates/ui-egui/src/tests_crop_tool.rs top-bar navigation arrowheads render both halves at 1× and 2× scale; in the crop tool Esc closes the tool first (the crop stays), the next Esc goes back to the grid; Esc that ends an edit in a text field does nothing else
KEY-COMMIT Commit tool — Return P1 ✅ cmd:tool.done Return closes Crop / Remove / Red Eye / Masking back to Edit (edits apply live, as in Lightroom)
KEY-DELETE-PIN Delete selected pin — ⌫ P0 ✅ cmd:mask.delete, cmd:spot.delete ⌫ deletes the active mask (Masking) or the selected spot (Remove), never the photo while retouching
KEY-HIDEPINS Hide pins — H P2 ⬜ H = Remove panel

Lightroom Classic extras

From 08-lightroom-classic-extras.md (Classic-only features) and part 2 of 06-shortcuts.md (Classic keys, grouped).

Id Feature Tier Status Evidence Missing / notes
LRC-LIB-IMPORT Full import dialog P1 ✅ cmd:library.import (mode, destination, organize, rename, metadataPreset, preset, keywords, album), crates/ui-egui/src/import.rs review grid with duplicates skipped; add in place or copy (library Originals or any folder; by day / by month / one folder / custom folder template such as {date:%Y}/{date:%Y%m%d} → 2026/20260114, kept inside the destination; example destination shown; rename template with live example, Tags picker (insert at cursor) and unknown-tag warning, numbered across the import); develop preset, metadata preset, keywords, album on import
LRC-LIB-AUTOIMPORT Watched-folder import P2 ✅ cmd:library.autoImport see LR-IMP-AUTO
LRC-LIB-TETHER Tethered capture P2 ⬜
LRC-LIB-VIEWS Grid / loupe / compare / survey / people P1 🟡 cmd:view.photoGrid, cmd:view.squareGrid, cmd:view.detail, cmd:view.compare, cmd:view.survey, cmd:view.gridInfo grid, square grid, loupe, compare, survey, second window; square-grid captions: file name, exposure or capture date (View ▸ Grid Info; no people view; picking one in Photo Grid switches to Square Grid)
LRC-LIB-COMPARE Compare view P1 ✅ cmd:view.compare ⇧C (C is Crop here); swap, make select; a filmstrip click replaces the active side; zoom always linked; each pane says “Rendering…” or that the photo can't be opened, as the loupe does (see LR-VIEW-COMPARE)
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; each pane says “Rendering…” (widget compare:rendering:<pane>) or “This photo can't be opened” (compare:renderFailed:<pane>) as the loupe does
LRC-LIB-CATALOG-PANEL Catalog sets P1 ✅ cmd:library.source (missing) all, recently added, picks, recently deleted, missing photos (when any)
LRC-LIB-CATALOG-IMPORT Native Lightroom Classic catalog import P1 🟡 cmd:library.inspectLightroom, cmd:library.importLightroom, crates/engine/src/lightroom_catalog.rs, docs/lightroom-catalog-import.md read-only .lrcat plus committed WAL pages, in-tree read-only Rust SQLite B-tree reader; background inspection/import with progress/cancellation, per-open library identity checks; originals in place, ratings/flags/labels, XMP, hierarchical keywords, collection sets/regular collections, virtual copies, mapped develop settings; stable reimport identities, undo, missing-file placeholders, a root folder not found at its recorded path looked up relative to the catalog (Lightroom's relativePathFromCatalog, so a project folder copied from another computer stays linked; reported as a warning), existing edits preserved. Best-effort deduplicated recovery archives retain settings/history/snapshots/collection rules (32 MiB each, eight archives/128 MiB aggregate); archive limits/write failures and optional-table limits warn without blocking photo import; unrelated unsupported tables and opaque SQL BLOBs skipped. Custom white balance on relative-WB raws (RAW in Lightroom's terms) stays As Shot and is reported, since the catalog keeps no as-shot white to shift from (issue #510). Adobe rendering, AI data and native history replay remain incomplete; no writes to Lightroom catalogs. Open Library given a Lightroom catalog folder, an .lrdata / .lrcat-data folder or the .lrcat makes no library there and offers this import with the catalog found (cmd:app.openLibrary, crates/engine/src/lightroom_folder.rs; issue #891)
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 (the app's notices, library.info → settingsWarnings, and the CLI's stderr; issue #171); 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); the lock is released explicitly when the library closes, not by letting its file handle close, so a program another thread is starting at that moment (which holds a copy of every file handle until it is launched) cannot keep the library "already open" for a moment (issue #680)
LRC-LIB-CATALOG-SWITCH New / Open Recent catalog, catalog name shown P1 ✅ cmd:app.newLibrary, cmd:app.openLibrary, cmd:app.recentLibraries, cmd:app.clearRecentLibraries, crates/ui-egui/src/panels/settings.rs, crates/ui-egui/src/panels/left.rs File ▸ New Library… (name, then location), File ▸ Open Recent Library (last 8; missing ones greyed out, dropped when opened; Clear), the open library's name under My Photos and in the window title (issue #902); a library that can't be opened leaves the current one open
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-LIBFOLDERS Library folders P1 ✅ cmd:library.folders, cmd:library.source (libraryFolder), cmd:library.filter (libraryFolder), cmd:library.removeFolder, cmd:folder.rename, cmd:folder.move, cmd:folder.label, cmd:folder.create, cmd:photo.moveToFolder, cmd:folder.scanChanges, cmd:folder.synchronize, crates/catalog/src/folders.rs, crates/engine/src/sync.rs, crates/ui-egui/src/sync.rs, crates/ui-egui/src/panels/left.rs (folders_section, folder_menu_for_library, row_menu), crates/ui-egui/src/tests_panels.rs sidebar Folders: the folders the library's photos were imported from, with photo counts (subfolders included), as another way to explore the library next to By Date and Keywords; Local stays the way to browse any folder on disk. Each disk is a row of its own and starts open: a drive letter, a Windows share (\\server\share, \\?\UNC\…, \\wsl$\…), a macOS share or disk (/Volumes/tokyo), a Linux mount (/media, /run/media, /mnt, gvfs smb-share:…, /net/<host>), NFS (/Network/Servers/<host>), else “This Computer” — told apart from the stored paths alone, so an offline share lists like a mounted one and the sidebar never waits on a network. Only folders that hold imported photos are listed (browsed, deleted and demo photos don't count); every folder is a row of its own (Users → me → Pictures), and a folder that holds nothing itself and leads to one folder starts open; every level indents one step more than its parent however deep it goes: the whole sidebar (Albums, Local, By Date, Folders, Keywords) scrolls sideways when its widest row is wider than the panel (rows are measured one frame behind; Create Album and the counts stay at the visible edge); a name that does not fit ends in an ellipsis at the count (the full name on hover; a row asks for room for at most 140 pt of its name, so depth, not one long name, makes the sidebar scroll), as does the grid title; selection and hover bars end at the panel's edge; click makes the folder the source, like an album or a Local folder (its photos and those of the folders inside it fill the grid, titled “photos/travel”; the filter bar narrows it further; library.source libraryFolder + path; kept with the library's view), and its row is always opened into view; the shown folder follows a rename or move and their undo (engine-tested; the sidebar test covers rename), is dropped back to All Photos when it is removed or no longer holds a photo, and a smart album made from the view keeps it, also when its rules are edited (the editor has no folder field). Only a folder that shows exactly its own photos can be shown (not the startup disk, not /Volumes-like folders above disks, not an empty one). A row that would cover other disks' photos too (the startup disk, /Volumes, /mnt…) only opens. Right-click (row or triangle): Rename Folder…, Move Folder To…, New Folder on Disk… (folder.create; one undo step, undo removes it while empty), Move Selected Photos Here (photo.moveToFolder, also Move to Folder… in the photo context menu: files and XMP sidecars move, virtual copies follow, across volumes copied and verified before the source goes, a name taken in the destination refuses the whole move, one undo step), Show in Finder (on disk, photos relinked, as in Local), Set Color Label ▸ (the five colours with their custom names, None, Edit Label Names…; folder.label, one undo step; the row shows the label's dot before its count and says it with its name; kept in the catalog as a folder record under the folder's identity, so it follows Rename / Move and their undo and redo, the folders inside going along), Synchronize Folder… (the dialog opens at once and scans the folder and the folders inside it on a worker thread — the look for new files beside parallel checks of the library's photos — then offers Import new photos (checked; files only browsed in Local count as new), Relink photos whose file was renamed or moved within the folder (checked; matched by content — a Duplicate's too — or, for a photo with no hash on record such as one from a Lightroom catalog, by file size and dimensions when exactly one file agrees), Remove missing photos from the library (to Recently Deleted) and Read metadata updates from XMP sidecars (saved after the photo came in or was last edited here, and saying something the library doesn't; the sidecar wins), with counts and skipped duplicates / unreadable files; folder.scanChanges / folder.synchronize, which acts on exactly the scan shown — a scan whose folder's photos changed meanwhile is refused and made again in the background — and is one undo step, also with a full history; a folder that isn't there (an unplugged disk) is reported as such, not as every photo missing; a whole disk or a folder holding disks needs disk: true; Esc stops the scan; Synchronize hands everything to a worker thread — the import, the checks that moved and missing files are still as found, the sidecar reads — and commits its steps between frames, shown in the activity stack with ✕ to stop, so no file is touched on the UI thread) and Remove from Library… (asks; moves the folder's photos to Recently Deleted, one undo step, no file touched); a disk row (not the startup disk) offers Set Color Label ▸, Synchronize Folder… and Remove Disk from Library… (the command needs disk: true for a whole disk or a folder spanning several). Limits: names are compared as written (macOS /Volumes/tokyo vs /Volumes/TOKYO are two disks; /Volumes/Macintosh HD/… and /… two rows); no disk capacity / online status; no “include subfolders” switch (a folder always includes the ones inside it); photos can't yet be dragged onto a folder row (use the menus); a name clash on moving photos is refused rather than renamed -1, -2; folders can't be filtered or sorted by label, and a label stays with the old path when the folder is moved outside LightCraft (Find Missing relinks photos, not folder records); Synchronize has no “show import dialog first” or “show missing photos” option, and only knows when a photo came in or was last developed here: a rating, label or metadata change made here (auto-write off) after another app saved the sidecar is listed as an update, and reading it restores the sidecar's older value (reading is never the default); the walk for new files isn't cancelled midway and its depth/entry limits are only logged
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; rules incl. File Path — the whole string, either separator, case-insensitive), 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), 47 fields. The field menu shows Rating, Pick Flag, Color Label and Any Searchable Text, then one submenu per group (FIELD_GROUPS; album.ruleFields reports each field's group): Source (album — a plain or a smart album, so one smart album can include or exclude another; a rule that would make an album include itself, directly or through others, is refused, and a loop saved anyway can't recurse: inside it, the album being evaluated counts as holding no photos, so an "isn't" rule on it matches every photo, and the sidebar marks it ⚠ —, virtual copy, copy name, in a stack), File (filename, extension, file path, type, format, video duration), Date (capture / import / edit), Keywords & People (keywords, keyword count, people on named faces, people count), Description (title, caption, alt text, creator, copyright, copyright status), Camera Info (camera, lens, focal length, aperture, shutter speed as 1/250, ISO), Location (any, city, state / province, country, GPS), Size (long / short edge, aspect ratio and megapixels, all of the photo as shown: oriented and cropped), Develop (has edits, cropped, treatment), Assisted Culling (focus, best of similar shots); text / number / date / in-the-last operators; live match count; also usable as a library filter (ruleSet). Missing vs Classic: develop preset, flash, exposure bias, camera serial, colour profile / bit depth, IPTC Job, smart-preview status. Choice values read as words in every language (Public Domain, Black & White; album.ruleFields gives choiceLabels), while rules store the ids; yes/no fields read Yes / No and take true/false, "yes"/"no", "true"/"false" or 1/0. Every value is checked against its field (RuleSet::check): numbers (rating 0–5), shutter speeds, dates (2026, 2026-08, 2026-08-14, then T10, T10:00 or T10:00:00, a space working as the T, and after whole seconds a fraction and a time zone as imported capture times carry them (.250+02:00); no 30 February; rules::rule_date is the one reading for the check and the matcher; each date field has a calendar picker, crates/ui-egui/src/date_picker.rs, that picks a year, a month or a day and keeps a "between" in order), between = two bounds in order, in the last = more than 0 of a known unit, choices (ids, the flag's picked / rejected, a colour label's custom name), text with something to look for, an existing album (plain or smart, not a folder), no empty groups; each problem names its rule (rule 2.1: no rating 0–5 is 9), and album.createSmart / setRules / library.filter refuse them. The editor marks each bad rule under its row (inside groups too), keeps OK disabled until they are fixed (and its confirm refuses them before renaming anything), picks an Album rule's album with an album picker (crates/ui-egui/src/album_picker.rs: the sidebar's tree with folders, or type part of a name or folder; ↑ / ↓ / Enter; albums that can't be picked shown greyed with the reason) (is / isn't; a folder can't be tested), and drawing it never changes a rule: a rating of 9, "in the last 0 days" or 20,000 days, a "between" that isn't a pair all stay as stored (and are marked where they can't match) until edited. Album rules saved with an old operator (≥, between) are read as the "isn't" they always meant (RuleSet::upgrade). Saved smart albums whose rules no longer check are marked ⚠ in the sidebar (tooltip: what to fix; albums.list problems, Catalog::smart_album_problems); the name and rules of an album are saved as one undo step (album.setRules name). Problems read as the field's name and the issue in the editor's language (“Titel: braucht einen Suchbegriff”; Problem::issue / field, Issue::text, translated in every locale), while commands keep the English rule 2.1: … message for agents
LRC-LIB-PUBLISH Publish services P2 ⬜
LRC-LIB-FILTERBAR Library filter bar P1 ✅ cmd:view.filterBar, cmd:filter.applyPreset, cmd:library.filter (labels, ruleSet), crates/ui-egui/src/panels/filterbar.rs rating/flag/label (several labels at once = any of them)/kind/edited/camera/lens/keyword, clear, save as smart album, filter presets; the filter stays as you change albums (always locked, as in the desktop app); arbitrary conditions via the smart-album rule editor
LRC-LIB-STACKS Stacks (full) P1 ✅ crates/catalog/src/stacks.rs, cmd:stack.group, cmd:stack.split, cmd:stack.moveUp, cmd:stack.moveDown group / ungroup / toggle / set top / remove / auto by time / split / move up and down
LRC-LIB-VC Virtual copies P1 ✅ cmd:photo.virtualCopy ⌘'
LRC-LIB-LABELS Colour-label sets P1 ✅ cmd:label.sets, cmd:label.applySet, cmd:label.saveSet, cmd:label.deleteSet, cmd:label.setNames, crates/engine/src/cmd/manage.rs built-in Colors / Review sets + user sets (Photo ▸ Set Color Label, Edit Label Names… dialog); names written to and read from xmp:Label
LRC-LIB-KEYWORDS Hierarchical keywords, sets, painter, Keyword List P1 ✅ crates/catalog/src/keywords.rs, crates/ui-egui/src/panels/keyword_list.rs, cmd:keyword.list, cmd:keyword.create, cmd:keyword.edit, cmd:keyword.move, cmd:keyword.info, cmd:keyword.purgeUnused, cmd:keyword.setDefaultParent, cmd:keyword.sets, cmd:keyword.toggleFromSet, cmd:keyword.saveSet, cmd:tool.keywordPainter hierarchical a|b|c keywords (tree, parent filters include children, rename moves children); keyword sets + Recent Keywords (⌥1–⌥9); keyword painter (Keywords panel ▸ Paint: click photos in the grid to toggle a keyword, Esc stops). Keyword List (Keywords panel): every keyword, those without photos too (a keyword list in the catalog, format version 4), with counts, a filter box, a tri-state tick box for the selection and an arrow to show its photos; + / Create Keyword Tag (name, synonyms, put inside, add to selected photos), double-click / Edit Keyword Tag, − / Delete with confirmation, context menu (create inside, Put New Keywords Inside This Keyword, purge unused keywords); drag a keyword onto another to nest it or onto the list's title for the top level (a merge asks first), drag photos onto a keyword to tag them. The list is there without a selection too; Put New Keywords Inside This Keyword (kept with the keyword in the library, undoable) also places new keywords typed in the Keywords panel. Keyword tag options include on export, export containing keywords and export synonyms shape exported files (names in dc:subject, paths in lr:hierarchicalSubject), and files' lr:hierarchicalSubject paths come back as keywords on import and from sidecars; Person is stored only. Keywording box (crates/ui-egui/src/panels/keywording.rs): the selection's keywords as chips (one only some selected photos have marked with an asterisk; × only removes, from every selected photo; with no selection, the active photo's), a Keywords / & Containing / Will Export switch (Lightroom Classic's Keyword Tags views: the keywords to edit, them with their parents, what exported files carry), keyword-set buttons and suggestions that follow the selection, shared text fields for typing and painting. Edit Keyword Set (Keyword Set ▸ Edit Set…): a set's name and nine slots typed in place, empty slots kept so each keyword stays on its ⌥ key, a set renamed in place (cmd:keyword.saveSet replace) or saved as a new set (new, a taken name refused). File ▸ Import Keywords… / Export Keywords… (cmd:keyword.import, cmd:keyword.export): keyword list files in Lightroom Classic's format (tab-indented, {synonym}, [left out of export]), which Capture One, darktable, Bridge and Photo Supreme (its Formatted Vocabulary File) read and write too; import adds what is missing in one undo step, export names the keywords Capture One refuses (; , < >). Not yet: synonyms in search, person keywords tied to faces, Keyword Shortcut, IPTC keywords in exported files (Piwigo, RawTherapee), hierarchy from digiKam / ACDSee / Microsoft XMP fields
LRC-LIB-METADATA Metadata panel & presets P1 ✅ cmd:photo.setMeta, cmd:metadata.savePreset, cmd:metadata.applyPreset, cmd:metadata.presets, cmd:metadata.deletePreset, cmd:dialog.captureTime IPTC core, accessibility, place, capture-time edit; copyright status (unknown / copyrighted / public domain), rights usage terms and copyright info URL (photo.setMeta copyrightStatus, usageTerms, copyrightUrl; XMP Rights Management fields, kept by "copyright only" exports, a smart-album rule); metadata presets (Photo → Metadata Preset, Save Metadata Preset… from the active photo; applied on import from Settings)
LRC-LIB-QUICKDEV Quick develop P2 ✅ cmd:develop.quickAdjust, crates/ui-egui/src/panels/edit.rs (quick_develop) grid with several photos selected: Edit panel ▸ Quick Develop steps (exposure ⅓ / 1 stop, contrast, highlights, shadows, whites, blacks, clarity, vibrance, temperature) added to each photo's own value, one undo step
LRC-LIB-PEOPLE People view P2 🟡 cmd:view.people, cmd:view.person, cmd:faces.person, cmd:faces.unnamed, cmd:faces.nameFaces, crates/ui-egui/src/panels/people.rs, crates/ui-egui/src/panels/person.rs, crates/ui-egui/src/panels/unnamed.rs Named People cards (face close-up, the photo count on the picture, name) for the photos the active filters let through, with the filter chips shown above them, sized by the thumbnail slider. Below them the Unnamed faces: every face nobody has named as a cropped picture, look-alikes next to each other once recognition has looked at them, a suggested name on a face the named ones recognise; click or Shift-click to select, type a name, Enter names the whole group in one undo step. A click on a person opens their page: only cropped faces, then More (unnamed faces that look like them, best first; click to confirm, × to hide). Every face is shown by the detector's box where the scan has one, so faces are equally close. Until recognition is set up a line at the top offers the next step (Settings ▸ Faces, or Turn on), as does the loupe's name box. No clusters with separators between groups, no persistent reject, no merge of two people
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), crates/ui-egui/src/panels/left.rs (locate_missing_item) 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; searched on a worker thread, one undo step), photo menu → Locate Missing File…; a folder or disk row's Locate Missing Files… chooses its new location and scopes the same recursive background finder to that folder and its subfolders. 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; the view, the Info panel, the grid's context menu and thumbnails read file availability from a cache a worker thread fills, so an offline NAS never stalls a frame); 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 (a row in the activity stack, ✕ to 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
LRC-DEV-HISTORY Full history panel P1 ✅ cmd:history.list, cmd:history.restore, cmd:history.previous, cmd:history.next, cmd:history.clear row menu: copy step to before, create version from step, clear history; clicking a step loads it without adding one (current step highlighted, later steps greyed and returnable until a new edit truncates them; one undo step; cursor saved in the catalog, old catalogs open at the latest step)
LRC-DEV-SOFTPROOF Soft proofing P2 🟡 cmd:view.softProof, crates/pipeline/src/output.rs (Proof), crates/ui-egui/src/panels/edit.rs (soft_proofing) S in the loupe (in grids S stays Expand/Collapse Stack): paper-white surround and "Proof Preview", proof profile (sRGB / Display P3 / Adobe RGB / ProPhoto / Rec. 2020), destination (red) and display (blue) gamut warnings (the display's gamut is the monitor profile's, cmd:app.displayProfile, else sRGB), Create Proof Copy (a named virtual copy). printer / paper ICC profiles can be loaded as the proof profile (size-capped, the last few kept), with rendering intent (Perceptual / Relative Colorimetric), Simulate Paper & Ink and a device-gamut warning (crates/codecs/src/proof.rs, crates/engine/src/proof.rs)
LRC-DEV-AUTOSYNC Sync / auto sync / paste previous P1 ✅ cmd:develop.sync, cmd:develop.autoSync, cmd:develop.pastePrevious Edit ▸ Sync Settings / Auto Sync (⌥⇧⌘A): only the changed settings carry over, one undo step, slider drags sync on release, spots / red eye stay per photo; Edit panel banner
LRC-DEV-MATCHEXP Match total exposures P2 ✅ cmd:develop.matchExposure Photo ▸ Match Total Exposures: the selected photos' Exposure set so shutter × ISO ÷ aperture² plus the slider matches the active photo's
LRC-DEV-CALIB Calibration panel P1 ✅ ctl:calibration.*
LRC-DEV-TAT Targeted adjustment tools P1 ✅ cmd:develop.targeted (target: curve / hue / sat / lum)
LRC-DEV-DEFAULTS Per-camera raw defaults P1 ✅ cmd:library.preferences (camera, import.perCamera), crates/ui-egui/src/panels/settings.rs Settings → Import: raw default and per-camera presets
LRC-DEV-PROCESS Process versions, Update to Current Process P1 ✅ cmd:develop.updateProcess, cmd:develop.reset, cmd:develop.get, crates/develop/src/settings.rs, crates/pipeline/src/finish.rs, docs/process-versions.md each photo records the rendering process its settings are interpreted by: settings saved before this existed load as V1, new photos and Reset get the latest, virtual copies inherit, copy / paste / sync / presets keep each photo's own; Photo ▸ Update to Current Process (one undo step, enabled while a selected photo is on an older process); a number from a newer LightCraft is kept and renders with the newest known process. Only V1 exists so far: this is what lets a retuned default tone (#146) or stronger Light sliders (#196) ship without changing existing edits
LRC-DEV-VIEWOPTIONS Develop view options P2 ⬜
LRC-DEV-VIDEO Video frame capture P2 ⬜
LRC-MAP-VIEW Map view P2 ⬜
LRC-MAP-GEOTAG Drag photos onto the map P2 ⬜
LRC-MAP-LOCATIONS Saved locations P2 ⬜
LRC-MAP-TRACKLOG GPS track logs P2 ✅ cmd:photo.autoTagTracklog, cmd:photo.tagFromTracklog, crates/meta/src/gpx.rs Photo ▸ Auto-Tag from Tracklog…: a GPX 1.0 / 1.1 track log sets the GPS of the selected photos by capture time — interpolated between the points of a track segment, else the nearest point within maxGap (10 min); never across segment breaks. The camera's time zone comes from the photo (Exif offset) or is asked for; photos that already have a location keep it unless replace; dryRun previews; one undo step. No track drawn on a map (no Map module)
LRC-MAP-FILTER Location filter bar P2 ⬜
LRC-MAP-REVGEO Reverse geocoding P2 🟡 cmd:metadata.reverseGeocode offline from GeoNames (City / State / Country, no sublocation); no online provider
LRC-BOOK-SETTINGS Book settings P2 ⬜
LRC-BOOK-AUTOLAYOUT Book auto layout P2 ⬜
LRC-BOOK-PAGE Book pages & templates P2 ⬜
LRC-BOOK-GUIDES Book guides P2 ⬜
LRC-BOOK-CELL Book cell padding P2 ⬜
LRC-BOOK-TEXT Book photo/page text P2 ⬜
LRC-BOOK-TYPE Book typography P2 ⬜
LRC-BOOK-BG Book backgrounds P2 ⬜
LRC-BOOK-VIEWS Book views P2 ⬜
LRC-BOOK-EXPORT Book export (PDF/JPEG) P2 ⬜
LRC-SS-TEMPLATES Slideshow templates P2 ⬜
LRC-SS-OPTIONS Slideshow options P2 ⬜
LRC-SS-LAYOUT Slideshow layout P2 ⬜
LRC-SS-OVERLAYS Slideshow overlays P2 ⬜
LRC-SS-BACKDROP Slideshow backdrop P2 ⬜
LRC-SS-TITLES Slideshow titles P2 ⬜
LRC-SS-MUSIC Slideshow music P2 ⬜
LRC-SS-PLAYBACK Slideshow playback P2 ⬜
LRC-SS-EXPORT Slideshow export P2 ⬜
LRC-PRINT-LAYOUTSTYLE Print layout styles P2 🟡 cmd:dialog.contactSheet, cmd:export.contactSheet, crates/engine/src/contact_sheet.rs paginated contact sheets; single-image/custom package layouts missing
LRC-PRINT-IMAGESETTINGS Print image settings P2 ⬜
LRC-PRINT-LAYOUT Print layout P2 🟡 cmd:export.contactSheet, crates/ui-egui/src/panels/dialogs.rs A4/Letter, portrait/landscape, 1–8 columns and 1–10 rows; fixed margins and spacing
LRC-PRINT-GUIDES Print guides P2 ⬜
LRC-PRINT-CELLS Picture-package cells P2 ⬜
LRC-PRINT-PAGE Print page options P2 🟡 cmd:export.contactSheet, crates/engine/src/contact_sheet.rs optional filename captions; no page backgrounds/identity plates
LRC-PRINT-JOB Print job & colour management P2 🟡 cmd:export.contactSheet, cmd:app.contactSheet, crates/ui-egui/src/export_task.rs PDF export at fixed 150 dpi in sRGB, current edits, background progress/cancel and original protection; no native printer integration, ICC soft proof/print profiles or selectable resolution
LRC-PRINT-TEMPLATES Print templates P2 ⬜
LRC-WEB-LAYOUT Web gallery layouts OOS 🚫
LRC-WEB-SITEINFO Web gallery site info OOS 🚫
LRC-WEB-COLOR Web gallery colours OOS 🚫
LRC-WEB-APPEARANCE Web gallery appearance OOS 🚫
LRC-WEB-IMAGEINFO Web gallery image info OOS 🚫
LRC-WEB-OUTPUT Web gallery output OOS 🚫
LRC-WEB-UPLOAD Web gallery upload OOS 🚫
KEYC-PANELS Classic panel keys (Tab, ⇧Tab, T, F5–F8, solo) P2 ⬜
KEYC-MODULES Classic module switching (⌘⌥1–7) P2 🚫 no modules in LightCraft
KEYC-VIEWS Classic view keys (E, G, C, N, L, F, I, ⇧R, ⌘⌥0) P2 🟡 cmd:view.photoGrid, cmd:view.zoom100 G works; E opens Edit (not loupe); no compare/survey/lights-out/screen modes
KEYC-SECONDWINDOW Classic secondary-window keys P2 ⬜
KEYC-CATALOG Classic photo/catalog keys (⇧⌘I, ⌘', ⌘R, F2, ⌫, ⇧⌘E…) P2 ✅ cmd:library.import, cmd:photo.delete, cmd:photo.virtualCopy, cmd:app.showInFinder, cmd:dialog.rename, cmd:photo.editInExternal ⇧⌘I add, ⌘' virtual copy, ⌘R show in Finder, F2 rename, ⌫ delete, ⇧⌘E external editor
KEYC-COMPARE Classic grid/compare keys (Z, Home/End, =/−, ⌘⇧D, S…) P2 🟡 cmd:view.zoomToggle Z toggles zoom; no compare, stacks, thumbnail-size keys
KEYC-RATING Classic rating/flag keys (1–5, ⇧1–5, 6–9, P, X, U, ⇧X, ⇧U, [ ], `) P2 🟡 cmd:photo.rate, cmd:photo.pick, cmd:photo.flag 0–5 / Shift+0–5 and 6–9 / Shift+6–9; P/X/U, Shift+X/U and grid Shift+P work; tests cover batch/undo, active Compare candidate and text focus. Missing: Shift+P outside grids (Presets), rating [ / ], flag cycle and filter-bar keys
KEYC-COLLECTIONS Classic collection keys (⌘N, B…) P2 🟡 cmd:dialog.newAlbum ⌘N new album; no quick collection
KEYC-METADATA Classic keyword/metadata keys (⌘K, ⌘S, ⌘⌥⇧C/V…) P2 🟡 cmd:photo.saveMetadataToFile ⌘S saves metadata; no keyword sets, metadata copy/paste
KEYC-DEVELOP Classic develop keys (V, ⌘U, ⇧⌘U, R, Q, K, M, ⇧M, ⇧W, ⇧J, ⇧Q…) P2 🟡 cmd:develop.treatment, cmd:develop.reset, cmd:crop.reset V, ⇧⌘R, ⌥⌘R, W, J, Y, ⇧Y, \ match; R/K/M/⇧M differ; no Classic keymap layer
KEYC-MODULE-OUTPUT Book / slideshow / print / map / web keys P2 ⬜ modules not implemented
KEYC-HELP Classic help keys (⌘/, F1) P2 🟡 cmd:app.shortcuts ⌘/ only

Japanese text watermarks

app.export watermark objects accept vertical (boolean, defaults to false). The export dialog offers Horizontal/Vertical and multiline text. Japanese glyphs fall back to the craft-fonts Mincho faces (BIZ UDMincho; on the web build, BIZ UDPGothic) when built with CRAFT_FONTS_DIR. Vertical lettering uses upright em cells and right-to-left newline columns; Latin stays upright. Advanced Japanese composition remains open. Regression coverage: export::tests::japanese_watermarks_support_vertical_columns, japanese_watermark_options_and_legacy_defaults, watermarks_work_without_craft_fonts.

Revision history

Date Change Summary
2026-10-11 minor LR-MERGE-HDR: HDR and panorama merges of raws without colour matrices (CR2, NEF, ARW…) write the colour they open with as DNG tags (issue #965); status unchanged
2026-10-12 minor LR-META-XMP: Read Metadata from File no longer auto-writes the sidecar it has just read, which added lc:settings and hid another app's later crs: edits (#934); status unchanged (✅)
2026-10-12 patch LR-IMP-CAMERA-COVERAGE: CR2 white level from the active area, not the saturated masked rows above it (issue #1272); status unchanged (🟡)
2026-10-12 minor LR-IMP-CAMERA-COVERAGE: Pentax PEFs without area tags lose their masked border lines (issue #1271); status unchanged (🟡)
2026-10-12 minor Camera-look deep shadows follow the JPEG toe (#1307); camera calibration remains partial
2026-10-11 minor LR-IMP-PREVIEW-READ: a preview reads the raw's header and its embedded JPEG instead of the whole file (a 56 MB RAF: 56.5 MB → 5.9 MB, ~10 s → 0.96 s over SMB, byte-identical pixels), and an unedited raw's grid thumbnail is that camera JPEG itself, so no sensor decode is queued behind it. A CR3's JPEG track and a TIFF raw whose preview tag sits past the prefix are read through lightcraft_raw::sparse::preview_plan, which names the ranges embedded_preview reads; the formats it cannot describe (CRW/MRW/X3F, NEF/ORF/PEF) keep the whole-file path
2026-10-12 minor LR-BEHAV-ACCESS: AccessKit tree audit test; text fields named, sliders expose range and Increment / Decrement; status stays 🟡
2026-10-12 minor LR-PREF-DISPLAY: on Linux, previews follow the display profile the desktop assigns in colord
2026-10-12 minor LR-IMP-FORMATS: transparent areas are flattened onto white in linear light instead of rendering black (issue #1212); status unchanged (🟡)
2026-10-12 minor LRC-LIB-MISSING: folder and disk context menus locate a moved tree recursively without relinking missing photos outside it (issue #1194)
2026-10-12 minor LR-IMP-CAMERA-COVERAGE: Canon PowerShot G9 as-shot white balance from WBInfo (issue #1273); status unchanged (🟡)
2026-10-12 minor LR-IMP-CAMERA-COVERAGE: Olympus E-10 as-shot white balance and black level from the main maker note (issue #1273); status unchanged (🟡)
2026-10-13 minor LR-IMP-CAMERA-COVERAGE: Nikon D90 NEFs drop their 42 filler columns (pink band on the right) and clip at one measured white level; status unchanged (🟡)
2026-10-13 minor LR-VER-HISTORY, LRC-DEV-HISTORY: History cursor (go to a step without adding one; Previous/Next History Step)
2026-10-12 minor LR-IMP-FORMATS: the Hasselblad Lunar (a Sony NEX-7 ARW under Make HASSELBLAD) decodes from sensor data instead of opening as its preview; an uncompressed Leaf MOS imports as a preview-only raw instead of a greyscale image of its mosaic (#1275); status unchanged (🟡)
2026-10-12 minor LR-HDR-EXPORT, LR-EXP-HDR: HLG AVIF export (hdrTransfer); LR-VIEW-HDR-DISPLAY: tested SDR/HDR display decision
2026-10-12 minor LR-EXP-PSD, MENU-FILE-PHOTOSHOP: Photo ▸ Edit In ▸ Open Original in Other Editor (RAW, no copy) hands the original files (the raws themselves) to the other editor in one start; statuses unchanged
2026-10-11 minor KEY-SLIDER-TYPE: honor immediate Return/Escape and keep typed keys in the value editor; add regression references
2026-10-11 minor LR-PROF-CAMERACOLOR: raws with a black-and-white camera JPEG take its tone curve (issue #577)
2026-10-11 minor LR-VIEW-ZOOM and LR-VIEW-COMPARE: Compare and Reference are sharp at 1:1 (zoom windows per pane, two originals kept decoded while they are open); statuses unchanged
2026-10-11 minor LR-IMP-FORMATS: EOS R6 Mark III CR3 opens with its as-shot white balance and black level (ColorData version 66); status unchanged (🟡)
2026-10-11 minor LR-IMP-FORMATS and LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes from sensor data (57% of its C-RAW files opened as a preview: QP-map escape); status unchanged (🟡)
2026-10-10 patch LR-BEHAV-UNDO: file/XMP repair retries and merged rename Undo/Redo evidence; status and summary counts unchanged.
2026-10-10 minor Renamed from parity.md (progress-docs standard); Top gaps moved to gaps.md (its duplicated items 1–2 from a bad merge resolved there), shortcut notes moved to ui-parity.md. Rows unchanged; a recount of the rows gives 81.1% of 517 in-scope rows (P0 98.0%, P1 95.1%, P2 46.9%), so the generated summary above is due a cargo xtask parity --write
2026-09-30 – 2026-10-11 — Rows added and updated with each feature (see git log -- docs/parity.md docs/parity-checklist.md)