The HDR / Panorama / HDR Panorama dialogs now open with the options of the
last merge of that kind instead of the defaults, and those options are kept
in the library's prefs.json (Session::last_merge) so they survive a restart.
"With Last Settings" and HDR for Each Bracket Set read the same store.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The thumbnail cache wrote .jpg files into the library's thumbs folder, which is usually inside
Pictures: photo galleries listed every thumbnail as a photo (issues #610, #1066), and every read
brought the file's date forward, so each launch made them all look new.
- Cache files are named <32 hex>.lcthumb (JPEG data as before). A .jpg of before is taken over
under the new name when it is asked for; the rest when the cache is tidied, which the engine does
once per cache folder, on a thread of its own with a row in the activity stack that shows its
progress and can stop it. Nothing is rendered again.
- Reading writes nothing: what was read is remembered for the session (a full cache lets it go
last), and only a file that went unused for a week has its date brought forward.
- A folder that holds nothing but the cache gets a CACHEDIR.TAG, when the cache makes it and when
it is tidied; one that also holds somebody's files is not tagged (issue #98).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Everything derived from the catalog was keyed on its revision, which every
op raises. So each edit (a keyword, a rating, every tick of a slider) had
the grid's list queried and sorted again from every photo, the source
counted by a second full query, and the side panels tally the whole library
again. In a library of 67,000 photos the frame after one keyword took about
156 ms with both side panels open (an idle frame: 2 ms).
The catalog now remembers which photos its latest ops touched and what of
them they changed (`Catalog::touched_since`, `aspects_since`, `Aspects`). It
answers "not known" for an op that changes more than photos of its own (an
album, a stack, the keyword list), for changes too long ago, and for a
revision it did not make itself (a library just loaded).
- The view (`Session::visible`): when the same view is asked again after a
small edit, the photos the edit touched are taken out of the list it had,
asked about again, and put back at their place in the order
(`Catalog::follow`). 37 ms -> 0.6 ms. Worked out whole as before: more
than 256 photos touched, a change that is not photos' own, an album in its
own order, Recently Added, Missing Photos, and a view that asks about "the
last N days" (it changes with the clock too).
- The source's count: taken from the list when no filter is set, else
counted without sorting (`Catalog::count`). 37 ms -> 0.
- The side panels (`Caches`): the folder tree, keyword tree, date groups,
people's names and library counts are kept through an edit that changed
nothing they read. A rating or a slider changes none; a keyword leaves the
folders, dates and counts.
The frame after a keyword: about 156 ms -> 54 ms with both side panels
open, 71 ms -> 2 ms with the grid alone (`tests_view_cost.rs`, a synthetic
library, run with --ignored --nocapture).
Still paid on each edit: the count beside every album and smart album in the
left panel (about 25 ms for a smart album there), and the filter bar's lists.
Tests: 1,440 random edits of every photo-local kind across sources, sorts,
filters and the hold, each compared with the list worked out whole; 600
random edits comparing every kept tally with a fresh one; the clock; the
record itself, with its failure paths. `ui.inspect` -> `perf` reports
`viewFollowed` and `viewWorkedOut`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A view is asked again after every edit, so a photo that an edit makes stop
matching it leaves the grid at once and every photo after it moves up: a
keyword given in a smart album of photos without keywords takes the photo
away from under the cursor, and in a random order, with no landmarks, the
shift reads as a new shuffle (the seed does not change).
View > Hold View Until Refreshed (`library.holdView`, off by default, kept
with the library's preferences): a photo the view shows stays in it when an
edit makes it stop matching a smart album's rules, Picks or the filter bar.
View > Refresh View (`library.refresh`, also in the sort menu while the hold
is on), Reshuffle, another source, filter or sort, or new rules for the
smart album shown, shows what matches now.
- Photos that start matching still appear at once.
- A photo that is deleted, or taken out of the album or folder shown, still
leaves at once: a held photo has to be where the view looks
(`Catalog::query_keeping`, `within`).
- A source with no filter over it holds nothing and is queried as before.
- A refresh keeps the seed.
- The grid's count no longer reads "3 of 2" when a held view shows more than
now match its source.
Each edit still asks the whole library again; that cost is not addressed
here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Print module's state lives in the session (Session::print), so the app,
CLI, control channel and MCP share one print: layout style (Single Image,
Contact Sheet, Picture Package, Custom Package), page setup, grid, image
settings, page background, guides and job. 18 commands, none journaled
(nothing here touches the catalog):
print.get / set / reset set merges a partial patch, validates the whole
result and changes nothing on error; a new
paper or margin pulls cells back inside
print.cell.add|move|resize|delete|stack|photo|page
print.layout.autoLayout|newPage|clear
print.exportPdf {path, ids?} composes and writes the PDF with an activity
row, refuses to replace an original
print.template.list|save|apply|delete
Templates keep layout, image settings, page and job (not the photos assigned
to cells, not the guides) and are saved in the library's prefs.json next to
the export presets. They are stored as JSON and checked when applied, so a
template from another version or a hostile one is an error on use and never
makes the preferences file unreadable.
Tests drive every command through the dispatcher (hostile numbers, unknown
keys, caps, persistence across reopening a library) and a CLI end-to-end test
chains set / add cells / Auto Layout / PDF in one run.
Mask effects gain groups, stars, partial presets (only some sliders, with or
without Amount), optional brush settings and the settings an imported preset
carries that we can't apply (kept for export). New commands: mask.updateEffect,
renameEffect, duplicateEffect, favoriteEffect, setEffectGroup,
renameEffectGroup, toolEffect; applyEffect takes several masks, all masks, or
the same-named masks on the selected photos in one undo step. A new mask starts
with the effect last applied to a mask of its kind (kept with the library).
Import (mask.importEffects and preset.import): Lightroom Classic local
.lrtemplate, Camera Raw local-correction .xmp, .lceffect and zips of these;
folders and LrC divider presets ("10 ----- DETAIL ----- 10") become groups;
grain / Point Color / toningLuminance reported and kept. Malformed files are
errors, never panics.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Named sets of local adjustment sliders, picked from a new Effect menu
above the mask sliders in the Masking panel (apply sets every slider and
keeps Amount; save current; import; right-click delete; the menu names
the matching effect, else Custom). Saved with the library preferences.
Commands: mask.effects, mask.applyEffect, mask.saveEffect,
mask.deleteEffect, mask.importEffects, mask.exportEffects (.lceffect).
Lightroom local adjustment presets (.lrtemplate with type
LocalizedAdjustmentPreset) are read by mask.importEffects and routed
there by preset.import / File > Import Profiles & Presets, which used to
reject them as having no develop settings. Their lower-camel Local* keys
map through the same table as XMP mask fields (crs_masks::local_values);
sliders without a counterpart (toningLuminance, grain) are reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Lightroom collection imported whole (11 113 presets, a 42 MB presets.json)
made every interaction take seconds:
- Session::persist ran after every command and serialized the whole preset
list to compare it with the last write: 609 ms per slider tick (release,
the real collection). Session::presets is now a PresetList whose generation
changes on mutable access; persist serializes only when it (or the profile
lists) changed, and skips the write when the bytes hash the same.
develop.set: 609 ms -> 2.3 ms; preset.apply: 598 ms -> 3.1 ms.
- The Presets panel deep-cloned every preset into a BTreeMap, built a row,
widget id and (with thumbnails) a variant render job for each of them every
frame. Grouping is cached per generation and only rows in view are laid
out: 5041 -> ~20 rows a frame, 2.3 ms with 5000 presets.
- The preset pickers (Import dialog, Settings > Import) build only the rows
in view.
- add_presets and opening a library compared every preset with every other
(quadratic); both use hash indexes now. Opening no longer serializes the
list once just to remember it.
Tests: develop commands don't re-serialize unchanged presets (and a change
is still saved), generation semantics, a 20 000-preset import, and the
panel building only visible rows (fails on the old code: 5041 rows).
An ignored bench (presets_scale_bench, LIGHTCRAFT_BENCH_PRESETS) times a
real presets.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check the target (Lightroom catalog, file, unreadable folder, locked by another
program) with Session::check_library before closing the open library, and if
opening still fails, reopen the previous one (both errors reported if that fails
too). New Library names reject Windows-reserved characters and trailing dots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4
`lightcraft-cli run --library <dir>/catalog.snap` failed with the OS's "File exists"
(creating a folder over the file). Session::open_library now opens the folder that holds
catalog.snap / catalog.log / catalog.lock, and says "is a file; a library is a folder"
for any other file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Open Library given a Lightroom Classic catalog folder, one of its .lrdata /
.lrcat-data folders, Backups/, the .lrcat or its -wal/-shm/.lock files used to
create an empty LightCraft library inside Lightroom's folder (or, for the
.lrcat file, close the current library and then fail). Now:
- engine: lightroom_folder::lightroom_catalog_at (names only, top-level scan
bounded to 10 000 entries, never fails); Session::open_library refuses those
places with EngineError::LightroomCatalog {path, catalog} before creating
anything. A LightCraft library (catalog.snap / catalog.log) always wins.
- app: app.openLibrary checks before closing the current library, keeps it,
returns {opened: false, lightroomCatalog, path} and shows a dialog with
Import Lightroom Catalog… (file.importLightroom with the catalog found) and
Cancel. The library problem window gets the error instead.
- Lightroom import status keeps the catalog path after the task ends.
- CLI --library explains how to import instead.
Intro and ending title screens (Lightroom Classic's Titles panel): each on
or off, a background colour and, centred, the Overlays identity plate or a
line of text, in the plate's colour or an override colour, at a scale, for
its own duration (default: the slide duration).
- Playback timeline: titles are items (Item::Title) of Playback; the intro
plays once before the first photo, the ending after the last photo when
repeat is off; random order never moves them; arrows step across them;
fades, pause and readiness work as for slides; n of N counts photos only.
- Engine: slide_title (TitleSettings in prefs.json, TitleCompose drawing
through the slide composer and overlay text renderer), slideshow.titles
get/set, slideshow.preview {title}.
- UI: titles composed on a render worker (Renderer::request_title), the
control bar shows Intro / End, Settings > Slideshow > Titles section and
a Photo / Intro / Ending preview choice; strings in the nine locales.
- Docs: parity row (partial), playback note, whats-new, control and MCP.
- Engine: slide_overlay (OverlaySettings in prefs.json, the new slideshow.overlays
command): identity plate (text, or a PNG/JPEG file with its transparency; size,
colour, opacity; render behind image), rating stars (own vector stars), up to 16
text overlays with tokens ({caption}, {exposure}, {file}, {date}, {focal}… plus
every rename token; unknown tags show as typed and are reported) and a drop
shadow, each at one of nine anchors of the slide or photo plus an offset, kept on
the slide; the export watermark on the photo. Sizes relative to the slide's
shorter side; validated and clamped.
- Drawn by the engine as a second pass over lightcraft_slideshow::compose_with
(new behind hook for "render behind image"); the watermark's text renderer moved
to engine text.rs (TextLayout, Inter + craft-fonts CJK) and is shared; glyphs
off the image are skipped so huge text stays cheap.
- rename::expand_tokens_with lets overlays add tokens without changing rename.
- Playback composes slides when overlays are on; slideshow.preview returns each
overlay's box. Settings > Slideshow > Overlays section with live preview.
- Docs: parity row (partial), whats-new, control/MCP docs, nine locale catalogs.
- Tests: tokens, nine anchors, star count, plate image opacity and missing file,
text shadow, resolution independence, hostile input, big-slide cost, CJK with
craft-fonts, prefs persistence, preview and headless playback pixel checks.
- New crate lightcraft-slideshow (L1, no UI): SlideSettings (margins linked or
independent, guides, aspect preview, zoom to fill, stroke border, cast shadow,
backdrop colour, colour wash, background image; lengths relative to the slide's
shorter side; validated and clamped) and a CPU compose() used by playback, the
Settings preview and slideshow.preview.
- Engine: slideshow.layout (partial merge, saved in prefs.json) and
slideshow.preview {id?, width?, height?, path?} (PNG to path or base64);
RenderJob::with_slide composes on the render worker; the background photo is
rendered once and shared.
- Playback: with a non-default layout each slide is the composed screen-sized
slide, prepared ahead like before; the default layout plays as before.
- Settings > Slideshow: Layout, Options and Backdrop sections and a debounced
live preview of the active photo's slide (with margin guides).
- Tests: composer geometry/pixels/hostile input, command get/set/persist/preview,
headless playback of a composed slide and of the default layout.
A smart preview keeps the colour and tone curves the camera-look fit gave it, so a
fit change (#499, #528, #583) left existing previews with the old look. They now
record LOOK_VERSION in the header; an older one is rebuilt from its original when the
library is opened or Build Smart Previews runs, and kept as it is while the original
is offline.
One config-dir helper (engine::config::config_dir; camera_profiles re-exports it) with both
the face-models and denoise-models defaults; hosts set up both model folders; Settings has
Faces and AI Denoise tabs; both command sets registered; both model dialogs; locale entries
from both; one [features] table per app manifest; Cargo.lock regenerated by cargo; the
DecodedSource cache cost counts both segmentation mattes and the denoised twin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep current upstream UI behavior and the later detector contract checks. Validate candidate models before replacing working weights and records, with a synthetic regression that failed before the fix. Remove the unrelated GPU equivalence-test relaxation. All seven quality gates pass with the pinned hosted fonts.
Generated with Codex.
Recover the unpublished denoise work against current upstream. Preserve later opt-in model setup, shared Rust HTTPS downloads, pending-photo resume and an Amount-independent switch. Keep original RAWs unchanged and share the current color finish between ordinary and denoised sources.
Bound public tiling inputs, refuse corrupt-cache exports, and retain synthetic/real-model coverage. Include two Windows path spelling fixes in existing tests. No model weights, face code, media or Tract runtime are included.
Validation: cargo xtask ci (all seven gates); installed RawNIND CPU/GPU agreement 1.38e-6; native CPU preview/export on a public 24 MP Sony RAW, unchanged original SHA-256.
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Generated with Codex.
When catalog.lock can't be locked (a file system without locks, such as
some network shares), the library opens unprotected, as documented, but
the promised warning went only to the log. It is now the first of the
library's warnings: library.info lists it under settingsWarnings, the
app shows it once in its notices, and lightcraft-cli run / mcp print the
library's warnings on stderr (stdout stays results / protocol only). A
normally locked library shows none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
- ui: counts stay at the visible edge when the sidebar scrolls sideways
(with the row's colour behind them where a long name runs under); the
smart-album dialog's live count includes the album's folder
- catalog: a folder path is used as given (a name with a space at its edge is
not its neighbour: Remove acted on the wrong folder); a plain POSIX path
is checked against the folder without building a key (about half the cost
on a big library, with a test that it agrees with the careful check)
- engine: editing a smart album's rules keeps the folder the editor cannot
show; a shown folder that loses its last photo gives way to All Photos;
library.state and view.json name the folder only while it is shown; a blank
folder filter is no filter
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- engine: a smart album made from a folder view keeps the folder; only a
folder that shows exactly its own photos can be the source (not the
startup disk, not a folder above disks, not one with no photos); a
folder source and a folder filter never coexist; a folder source with no
folder shows nothing; a saved view whose folder is gone opens on All
Photos; removing a folder above a disk needs `disk: true` even with one
disk mounted; library.folders documents volume/selectable
- ui: deep chains stop indenting after five levels, the grid title gives
way to the photo count (full name on hover), disclosure triangles have
spoken names, the remove dialog says what is included and has a disk
title; test temp folders are cleaned up on failure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- catalog: a row is exactly one folder (no folding of single-child levels);
the UI starts a folder that holds nothing and leads to one folder open
- engine: a library folder is a source (LibrarySource::LibraryFolder with
Session::library_folder, like Local's browse): library.source
{kind: libraryFolder, path}, kept with the library's view, shown by
library.state, followed through folder.rename / folder.move and undo,
dropped back to All Photos when its folder is removed
- ui: clicking a folder replaces whatever the grid showed (Picks, Recently
Deleted, a Local folder…) instead of narrowing it with a filter chip; the
grid is titled after the folder
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changing the disk cache size (library.preferences cacheMb) re-attached a
new PreviewCache object, and the renderer treats a new cache object as a
library reset: every texture, the loupe included, was dropped and
re-rendered. Thumbnails are keyed by content, so a new budget leaves them
valid. The budget now changes in place on the attached cache (same object,
same generation); the next write trims to it, as before. A preview clear,
a generation bump and a new library still invalidate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A catalog loaded from disk counts revisions from 0, like a new empty one, and the sidebar
counts, keyword tree, date groups and grid list are cached by revision alone. Opening an empty
library after one that was never edited this session therefore kept the old library's numbers.
Each catalog loaded in a process now gets its own revision range.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A date, keyword or person left over from the last session silently hid photos
behind a small badge, so view.json no longer stores or restores the filter (the
source, sort and selection still come back). In the loupe a small pill at the
canvas' top left names the active filters (the filmstrip and Next / Previous
follow them) and clears them on a click; it sits in the canvas margin, so the
photo does not move.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>