Soft proofing (S in the loupe; S in grids stays Expand/Collapse Stack):
- pipeline Proof {space, destWarning, displayWarning}: gamut-map into the
proof space, show it in the output space; red/blue gamut warnings
(neutral clipping excluded). CPU only; the GPU path declines proofs.
- RenderJob::with_proof; paper-white surround and Proof Preview label;
Soft Proofing strip under the histogram (profile, warnings, Create
Proof Copy = a named virtual copy). view.softProof for control/MCP.
- photo.virtualCopy takes an optional name.
Mask components: MaskComponent.name; mask.component {component, action:
invert|duplicate|delete|rename|op}; right-click / … menu per component,
double-click rename, Intersect button.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- develop.quickAdjust {control, delta}: adds a step to every target photo's own value (one undo
step); the Edit panel shows Quick Develop rows (exposure ⅓ / 1 stop, contrast, highlights,
shadows, whites, blacks, clarity, vibrance, temperature) in the grid with several selected.
- library.findSimilar {id?, similarity?}: look-alike photos by their culling signature (cached
per content), most similar first, as a view filter (Filter.only); photo menu entry.
- develop.matchExposure: selected photos' Exposure set so shutter × ISO ÷ aperture² + slider
matches the active photo; photos without exposure data are skipped.
- Impromptu slideshow row marked (View ▸ Slideshow).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A reference photo (photo menu ▸ Set as Reference Photo, or the active photo) shown fixed on
the left beside the active photo on the right, which the Edit panel keeps working on — for
matching a look by eye. View ▸ Reference View (⇧R); reference pane menu ▸ Clear Reference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mask.refine (0–100, mask.refine; Masking panel ▸ Refine Edges): after a mask's components are
combined, a guided filter steered by the photo's luminance (window up to ~4 % of the long
edge) snaps soft mask edges to the photo's edges. Rendered on the CPU (the GPU path has no
kernel for it yet).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lightcraft-pipeline dust.rs: sensor-dust detection on a rendered image — pixels darker than
their blurred surroundings by more than 3× the local texture, grouped into small (0.15–2 % of
the long edge), roughly round blobs. spot.findDust {sensitivity?, add?} renders the uncropped
photo, detects and adds a heal spot on each (auto source; one undo step). Remove panel ▸
Find Dust Spots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lightcraft-pipeline cull.rs: focus (Laplacian energy of the sharpest 16-px blocks at 512 px,
0–100, calibrated on the CC0 corpus), clipping share and an 8×8 look-alike signature.
photo.analyze scores the selection (else the view), groups look-alike shots taken within 10 s
into bursts and marks each burst's sharpest; optionally rejects photos below a focus score and
picks each burst's best (one undo step). Scores live on the photo (Analysis, Op::SetAnalysis):
rule fields Focus and Best of Similar Shots, Info panel line. Photo ▸ Assisted Culling….
Co-Authored-By: Claude Opus 5.5 <[email protected]>
folder.rename {path, name} and folder.move {path, into}: the folder is renamed / moved on disk
in one step (its files and sidecars go along) and the photos inside are relinked (and a
browsed folder follows); like the file manager it is not an undo step. Refuses clashes, moves
into itself and the library's own folders. Local folder tree: Rename Folder…, Move Folder To…,
Show in Finder.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When only vibrance / saturation (and colour grading) are active, chroma is scaled on OkLab
a/b directly — the same result without sin/cos (and atan2 unless vibrance needs the hue).
Bench (loaded machine): finish 1920×1280 + vibrance/saturation 71.8 → 47.7 ms wall on the CPU.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lightcraft-pipeline gains lut.rs: a .cube parser (LUT_3D_SIZE, DOMAIN_MIN/MAX, TITLE), a
trilinear lookup and an in-process registry; the finishing stage applies a `lut:` profile to
the display-encoded colour, blended by the profile amount (0–200 %); LUT profiles render on the
CPU. profile.import {paths} reads files, folders and zips, copies each LUT into the library's
Profiles/ folder, lists them in prefs and registers them when the library opens; the profile
browser shows them under their folder / zip name; profile.deleteImported. File ▸ Import
Profiles & Presets…, the file picker and drag & drop take .cube files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Keyword painter (tool.keywordPainter {keyword}): Keywords panel ▸ Paint; clicking a photo in
the grid toggles the keyword on it (no selection change), Esc / Stop ends.
- View ▸ Grid Info (view.gridInfo): the square grid's caption shows the file name, the
exposure (1/250 · f/2.8 · ISO 100 · 18 mm) or the capture date.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ExportOptions.dng_compression (`dngCompression`: lossless (default) | deflate | uncompressed),
in the export dialog when the format is DNG. Corpus-gated test: all three decode, uncompressed
is the biggest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The justified layout (and the aspect ratio of every photo it needs) was rebuilt every frame.
It is now kept in LightcraftApp::caches, keyed by catalog revision, the photos in view, the
width, the thumbnail size, the mode and the date groups. Headless measurement (ignored test
grid_frame_100k, machine under load): 43.1 → 5.3 ms on the UI thread.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both walked the whole library every frame (the filter bar even cloned every photo to list
cameras, lenses and keywords). They now come from LightcraftApp::caches, recomputed when the
catalog revision changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A 100k-photo scaling check (ignored test `scale_100k_library_queries`) showed the left
panel rebuilding the keyword tree every frame (323 ms), the grid recomputing date headers
(57 ms) and the Keywords panel its suggestions (44 ms) every frame.
- keyword_tree builds with hash maps instead of linear sibling scans: 323 → 57 ms at 100k.
- LightcraftApp::caches keeps the keyword tree, date runs and keyword suggestions per catalog
revision (and inputs), so each is computed once per change instead of every frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
photo.setMeta {gps: "lat, lon" | [lat, lon] | null} — decimal, or degrees / minutes / seconds
with N/S/E/W ("48°51'30"N 2°17'40"E"); applied to all target photos, saved in XMP / exports.
Info: the GPS line is an editable field, and Show on Map opens the place in OpenStreetMap.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/whats-new.md (release highlights, built into the app) shown by Help ▸ What's New;
Help ▸ Send Feedback… opens a new GitHub issue; the Help menu lists What's New, Keyboard
Shortcuts and System Info together.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AccessKit (already on in the desktop build) now sees what the painted widgets are: sliders
announce their control and value, text / icon buttons and dropdowns their label or tooltip and
state, section headers and flyouts open / closed, left-panel sources their name and count, grid
thumbnails file name, rating, flag and label. Test reads the AccessKit tree.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The active photo, fitted, in its own native window (an egui immediate viewport) with its own
render slot sized for that window; closing the window turns it off. Without native windows
(web, headless) egui shows it as a floating panel.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Smart previews (smart.rs): a ~1 MB proxy per photo in the library's "Smart Previews" folder
(the decoded preview-size source, scaled, sRGB-encoded JPEG behind a one-line header). When
a photo's original is missing (an unplugged drive), its source loads from the proxy: the
photo renders, edits and exports at proxy size. library.smartPreviews {discard?},
photo.smartPreview status; File ▸ Previews ▸ Build / Discard Smart Previews; Info shows
"Original offline · editing the smart preview".
- Photo ▸ Duplicate (photo.duplicate): a real "-copy" file with the same settings, metadata,
rating, flag, label and albums; one undo step.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
library.autoImport {folder, copy, album} (in prefs) and library.autoImportScan: files in the
watched folder the library doesn't have are added once their size held between two scans
(still-copying files wait), in place or copied into Originals, into the named album; each file
is tried once; arrivals don't take the selection. The desktop app scans every 3 s with a toast;
Settings ▸ Import ▸ Auto Import picks the folder.
Fix: library.import into an album no longer fails when nothing was selected.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Op::SetContent records a photo's file facts (size, dimensions, content hash; undoable).
photo.reload re-probes the photos' files and, for those that changed on disk, updates them and
drops their cached sources; render keys now include the file's content key, so the grid and
loupe re-render. The app reloads this session's external-edit copies whenever its window
regains focus (with a toast).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
photo.editExternal renders the active photo with its edits as a 16-bit TIFF "<name>-Edit.tif"
(Adobe RGB by default; ProPhoto / P3 / sRGB) next to the original, never overwriting, adds it
stacked on top of the original and selects it. Photo ▸ Edit in External Editor (⇧⌘E, as
observed; the export dialog stays on ⇧E) then opens it in the application set in Settings ▸
General ▸ External Editor, or the system default (desktop: open -a / start / xdg-open,
spawned, never waited for).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Auto versions: leaving an edited photo keeps its settings as an Auto version (when they
differ from its latest version; 20 kept); saved with the library, not an undo step.
- View ▸ Slideshow (⌥⌘↩, `interval`): the photos in view full screen, wrapping; Space
pauses, ←/→ step, Esc ends.
- Photo ▸ Convert to DNG (photo.convertToDng): lossless DNG next to the raw with the settings
embedded; the photo and its virtual copies are relinked (Relink gains an optional format),
originals kept, undoable. Import: Copy as DNG (`dng`) for copy imports.
- A fine grid over the photo while a geometry slider is dragged.
- View ▸ Show Photo Counts; Help ▸ System Info… (app.systemInfo, JSON for agents).
- Native menus: modified Enter accelerators.
- Parity rows refreshed (target / quick collection, auto sync, location rules, bring to front).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each place in Local (Pictures, Desktop, Downloads, Home, the browsed folder) has a disclosure
triangle listing its subfolders (hidden ones skipped; listed lazily, at most every 2 s), to any
depth; folders on the way to the one in view open by themselves; clicking a folder browses it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Albums gain a `quick` flag: the Quick Collection is created the first time B adds to it.
album.toggleTarget adds the selection to the target album (the Quick Collection unless
album.setTarget names another regular album) or removes it when all of it is already there;
album.clearQuick empties it. B in the photo / square grids does this (with a toast); in the
loupe and edit views B stays the masking brush. Album menu: Set as Target Album / Stop Using
as Target Album / Clear Quick Collection; the target is marked + in the list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lightcraft-meta gains tags.rs: every tag of IFD0 and the EXIF, GPS and interoperability IFDs
as (group, name, value) rows, named per the public TIFF / EXIF / DNG specifications, with
common values spelled out (exposure as 1/250 s, f/2.8, 35 mm, orientation, metering, flash,
compression…) and blobs summarised. photo.allMetadata adds the XMP fields (sidecar, else the
file's packet). Info ▸ All Metadata… opens a searchable dialog grouped by TIFF / EXIF / GPS /
XMP.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- While masking, ⇧O cycles the overlay colour as observed in the reference (the mode stays;
View ▸ Cycle Mask Overlay Mode cycles modes).
- macOS: File ends with the system's Close Window (⌘W). Hide / Hide Others / Show All,
Minimize (⌘M) and Zoom were already the system's own items; parity rows verified.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Color mask tile adds an empty colour range and arms the picker: click the photo to sample
(⇧-click adds, up to five); Pick in the Masking panel re-arms it. mask.sampleColor {x, y, add?}
samples via lightcraft_pipeline::color_range_sample, which reads the exposed scene colour at
that point in the same OkLab space the mask compares in (so a click selects what was clicked).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Masking panel lists each component with its controls: luminance ranges get a dark → light
bar with two handles (the nearer one follows the pointer; one undo step per drag), a Smoothness
slider (both falloffs) and Show Luminance Map (the photo in black & white with the selected
range tinted; turning it off restores the previous overlay); colour ranges get Refine.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
As observed in the reference: G switches between Photo Grid and Square Grid (from other
views: the photo grid); ⇧G opens Crop & Geometry with Guided Upright on and the guide tool
active (tool.guidedUpright). Square Grid no longer takes ⇧G.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
library.buildPreviews {size: standard (Settings → Performance preview size) | full, ids?,
wait?} renders each photo's grid thumbnail and loupe view into the memory + disk preview cache
(selected photos, else all in view) on a background thread; library.previewProgress /
cancelPreviews. File ▸ Previews: Build Standard-Sized / 1:1, Stop, Discard Preview Cache;
toasts when a build starts and finishes. Also silences the wasm-only unused `threads` warning.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filter.labels: a photo matches when its label is any of them. The filter bar's label toggles
add / remove labels from the set instead of replacing the single label; the summary reads
"label red or yellow". The filter already persists across albums (locked, as in the desktop
app).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lightcraft-catalog gains rules.rs: RuleSet {match: all|any|none, rules: [{field, op, value} |
{group}]} with text, keyword, number, date (is / before / after / between / in the last N
hours…years) and choice operators over 26 fields (rating, flag, label, type, edits, keywords,
any text, filename, title, camera, lens, location, dates, ISO, aperture, focal length,
megapixels, GPS, virtual copy, album…). Filter.ruleSet: smart albums and library.filter use it;
"in the last" counts from the session clock. album.ruleFields lists fields and operators for
agents; rules are validated.
UI: File ▸ New Smart Album… / album context menu ▸ Edit Smart Album… open the rule editor
(field / operator / value rows with per-kind editors, + / − / ⌥+ for groups, live match count).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Keyword sets: Recent Keywords (the nine most recently added, newest first) plus user sets in
prefs (keyword.sets, useSet, saveSet, deleteSet). keyword.toggleFromSet {index}: adds keyword N
to the selection, or removes it when every photo has it; applying from Recent Keywords keeps
the numbering. Keywords panel: set picker (save current as a set, delete) and a 3×3 grid of
toggles; ⌥1–⌥9.
- ⌘1–⌘5 expand/collapse Light, Color, Effects, Detail, Optics as observed in the reference;
Zoom 100% moves to ⌥⌘0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
library.import gains destination (copies go there instead of the library's Originals/; no
library needed), organize (date | month | flat), rename (file-name template with the rename
tokens, numbered from renameStart; copies are catalogued under their new names) and
metadataPreset. The Add Photos dialog: Copy → Copy to (Choose… / Library), Folders, Rename with
a live example, Metadata preset; batches keep the numbering going.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DNG compression 34892: each tile / strip is a baseline JPEG of 8-bit samples (decoded with
zune-jpeg, pure Rust), then the usual linearization table, levels and crop apply. Per the DNG
specification. Lossy DNGs (Smart Previews, "lossy compression" DNG exports, many phone DNGs)
now open as raw files instead of falling back to their embedded preview. Unit test with
synthetic JPEG tiles (edge tile cut short); the corpus sample decodes (5760×3840 in 144 ms).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- develop.autoSync (Edit ▸ Auto Sync, ⌥⇧⌘A): an edit of the active photo carries only the
settings that changed to the other selected photos, in one undo step; slider drags sync on
release; spots / red eye and history restores stay per photo. Edit ▸ Sync Settings in the
menu; Edit panel banner while it's on with several photos selected.
- Colour-label sets: built-in Colors / Review sets plus user sets in prefs (label.sets,
applySet, saveSet, deleteSet); listed in Photo ▸ Set Color Label; the names dialog can start
from a set and save one. XMP sidecars and exported originals write the label's name, and
reading resolves custom names back to their colour.
- Crop: ⌘-drag draws a straighten line and keeps the crop tool active.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Local corrections stored as crs: structures (MaskGroupBasedCorrections and the older
gradient / circular gradient / paint containers) become our masks: linear and radial
gradients, brushes (dab runs → strokes), Subject / Sky, luminance and depth ranges, with
add / subtract / intersect and invert, and all local sliders. lightcraft-meta's XmpData gains
`values`, a structured view of every property (arrays of structs survive); .lrtemplate Lua
tables convert into the same shape.
Applying a preset adds its masks to the photo's own (once; Amount scales them); sidecar masks
replace them and radial masks are fitted to the photo's aspect. Unsupported components
(colour range, other AI selections) are reported as "Mask: <kind>".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
preset.import (File ▸ Import Presets…, Presets panel ⋯, drag & drop, CLI, MCP) now reads
everything people actually download or carry over from other editors:
- classic .lrtemplate files (Lua table literal parser; value.settings uses crs: field names),
including old process-version fields (Exposure, Contrast, FillLight, HighlightRecovery,
Shadows, Brightness, Clarity, ToneCurve) approximated with today's sliders;
- photos carrying their edits in XMP ("DNG presets" from mobile apps, edited JPEG/TIFF): the
look only, without the photo's crop, geometry or custom white balance;
- .zip bundles (stored + deflate) of any of these, skipping __MACOSX / hidden files;
- folders: the pack's folder name becomes the group, skipping app/format container folders
("Lightroom Classic", "XMP", "Mobile (DNG)"…).
Each imported preset reports the settings that had no counterpart (`unmapped`: profiles,
looks, local masks); the app's toast names them. dryRun previews, group overrides.
Dropped .lcpreset/.lrtemplate/.xmp/.zip files import as presets instead of photos.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- develop.autoBwMix (B&W Mixer → Auto): each hue band's colourful pixels are pushed away from
the image's mean lightness (light bands brighter, dark ones darker; bands with almost no
colour untouched), clamped to ±60; switches to B&W; one undo step.
- library.filter {merged: hdr | panorama | hdrPanorama | any}: Photo Merge results recognised
by the "-HDR" / "-Pano" / "-HDR-Pano" names merges write; filter bar type picker lists HDR,
Panoramas, HDR Panoramas.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- filter.savePreset / applyPreset / presets / deletePreset: the filter bar's settings saved
under a name with the library; filter bar → Presets (apply, save current, delete).
- Return (tool.done) closes Crop / Remove / Red Eye / Masking back to the Edit panel.
- Parity: zoom while cropping verified (works), hover previews now include versions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Export watermark can be a graphic (watermark {image, imageWidth}): a logo with transparency,
scaled premultiplied to a share of the photo's width, converted into the output colour
space, placed with the same anchor / inset / opacity as text; decoded once per file.
Export dialog: Text / Graphic, Choose…, width; size and shadow apply to text only.
- Recently Added: everything imported in the 30 days up to the latest import, newest import
first, grouped by import day (was the latest import day only).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- metadata.savePreset / applyPreset / presets / deletePreset: named sets of copyright,
creator, title, caption, place and keywords (text replaces, keywords add; one undo step),
saved with the library; Photo → Metadata Preset (apply, Save Metadata Preset… from the active
photo's copyright, creator and place); Settings → Import applies one to every import
(library.preferences import.metadataPreset).
- stack.split (this photo and those after it become their own stack; single leftovers are
unstacked), stack.moveUp / moveDown; in the Stack menus.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- ↑/↓ over any slider nudge it by ≈ 1/200 of its range (exposure 0.05, most sliders 1,
temperature 50 K; ⇧ ×5), one undo step each; the key doesn't also move the selection.
- ⇧Z picks and moves to the next photo (with ⇧X / ⇧U).
- library.source {kind: missing}: photos whose file can't be found; a "Missing Photos" row
appears in the left panel when there are any (checked every 5 s).
- Loupe context menu: Zoom submenu. Parity: stale rows fixed (compare/survey, per-camera
defaults, paste from previous, capture-time edit).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LightCraft's web app is built by xtask into <target>/web; the packaging script
ported from PhotoCraft assumed trunk and dist/web. The release job now installs
wasm-bindgen-cli 0.2.129 to match Cargo.lock.
Co-Authored-By: Claude Opus 5.5 <[email protected]>