View > Export Slideshow... (and Export PDF / JPEG / Video in Settings >
Slideshow) writes the photos in view as their composed slides, between
the intro and ending screens that are on:
- PDF: a page per slide (1024/1920/3840 px long side, JPEG quality),
/Dur and /Trans fades per page, optional /PageMode /FullScreen. The
contact sheet's writer moves to engine/src/pdf.rs and streams to disk.
- JPEG: <name>-001.jpg... in a folder.
- Video: AV1 (rav1e, speed 10, low latency) in MP4 or WebM written by
our own muxers (codecs/src/video.rs); fades rendered as blended frames,
held slides one key-frame sample with a long duration; 720p/1080p,
24/30 fps; no audio; capped at 2 hours.
slideshow.export runs in the background with a row in the activity
stack (activity.cancel stops it); outputs are written as .part and
renamed when complete, so cancel or failure leaves nothing behind. A
photo that can't be rendered is skipped with a warning.
slideshow.exportStatus reports the result.
A slideshow template is the Playback (not "prepare previews"), Layout /
Options / Backdrop, Overlays and Titles settings as one versioned JSON
document (format lightcraft.slideshowTemplate, version 1).
- Engine: slide_template (SlideLook, Template, built-ins, user template
files in <settings>/slideshow-templates read through each section's own
merged() validation; size and count caps; newer versions refused; bad
files reported and skipped; file names sanitised so ids never leave the
folder). Applying replaces the four settings in one step and keeps the
replaced ones as "previous" for the session (prefs have no undo).
- Five built-ins of our own: Plain Black, Gallery Wall, Edge to Edge,
Cinema Wide, Shooting Data (no Lightroom names or designs, no images).
- Commands slideshow.templates.list/apply/save/update/rename/delete/
import/export; slideshow.preview {template} previews without applying.
- UI: Template Browser at the top of Settings > Slideshow (built-in, user,
previous settings; click applies, hover previews on the selected photo,
right-click update / rename / export / delete; save and import), file
dialogs on the desktop; strings in the nine locales.
- Docs: parity row (partial), whats-new, control protocol and MCP.
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.
Reset Edits rebuilt a photo without a default-preset look from DevelopSettings::default(),
so a raw imported with capture sharpening 40 and colour noise reduction 25
(DevelopSettings::for_raw) came back with both at 0, and with its embedded lens
corrections off. It now starts from the photo's own built-in defaults
(Photo::camera_defaults, the same as import) and keeps the white balance of its
current as-shot reference, as before. Rendered files are unchanged (their defaults
are DevelopSettings::default()). Reported in #763's notes.
Only proxies of raws that take the per-file camera look (Photo::relative_wb) go stale: JPEG/PNG/DNG proxies are never rebuilt for the look. The opening scan runs once per LOOK_VERSION (a look-version marker in the smart previews folder), not at every library open; Build Smart Previews still refreshes stale proxies. Each proxy is checked and written under one lock, so the background refresh and library.smartPreviews never write the same file at once. LOOK_VERSION 2: main's DRO / ILCE-7CR camera-look changes (#528, #583, #568, #616).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a photo's AI Denoise picture is being made or waits its turn, the
stack shows "AI Denoise" with the running photo's tiles as a percentage
and its file name (no cross: what is made follows the Denoise amounts and
Settings). The row goes when the queue is idle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While photos are left to search for faces the stack shows "Finding
faces" with how many are done (no cross: the scan is switched off in
Settings). The engine keeps the scan's peak, so Settings and the stack
agree. Also translates the "Downloading model" label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Downloading the SAM 3, face and AI Denoise models shows a row with the
model's name and the megabytes so far; its cross stops the download like
the Settings button. The denoise download tests get their own scratch
folders (they shared the face tests' names and could delete each
other's files).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build Previews, Build Smart Previews and Discard Smart Previews show a row
with the photo count and stop from its cross; the start toast goes, the
finish toast stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The registry is reachable as commands, so the UI, the control channel,
the CLI and MCP list and cancel background tasks the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `ExportOptions.hdr` (`hdr` param): photos edited in HDR export as ISO
21496-1 gain map JPEGs (the SDR rendition as the base, the gain map measured
against it as written) or 32-bit float TIFF with highlights above 1.
Other formats and SDR edits export as before.
- Output sharpening and watermarks apply to both renditions.
- `develop.hdr {enabled?, maxEv?}` turns HDR editing on/off and sets the
headroom limit.
Checked on macOS: ImageIO reports both the ISO and the Apple gain map, and
CoreImage's HDR decode of a 3 EV edit reaches 2.95 EV.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scan went stale on any catalog change anywhere (a background import,
a rating elsewhere), and folder.synchronize then scanned again inline:
on the UI thread, and acting on results the person never saw. A scan
now stays current while the folder's own photos are the same records
(Session::take_folder_changes compares them by identity). The dialog
asks with `scanned: true`, which refuses a stale scan instead of
redoing it; the dialog then stays open and scans again in the
background. Agents that just call folder.synchronize still get a fresh
scan.
The app dropped session.folder_changes every frame no dialog was open,
so an agent's folder.scanChanges never reached its folder.synchronize
in the running app; it now only lets go of a scan its dialog made.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file only browsed in Local was counted as already in the library and
silently dropped; it is now new, and synchronizing brings it into the
library, as an import does.
A photo whose file was renamed or moved within the folder showed as
missing while its file counted as a skipped duplicate, so "Remove
missing" sent the photo and its edits to Recently Deleted and imported
nothing. The scan now matches a missing photo's content hash among the
folder's files and lists it as moved; synchronizing relinks it (on by
default, "Relink photos whose file was renamed or moved"), in the same
undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folder that isn't there (moved whole, or on a disk that isn't
connected) used to scan as "every photo missing", one tick away from
moving a whole disk to Recently Deleted. The scan (on its worker, so an
asleep share never stalls the UI) now reports it as offline and both
commands refuse it; the dialog says so. A whole disk or a folder holding
disks needs `disk: true`, as Remove Disk from Library does; the disk rows
pass it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
folder.scanChanges keeps its result in the session; folder.synchronize
uses it (once) while it is of the same folder and the catalog has not
changed since, so a scan made on a worker thread is not repeated on the
UI thread and files that arrive after the dialog was shown are left for
the next synchronize. Otherwise it scans again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scan compares a library folder (and the folders inside it) with the
disk without changing anything: new files (the import scan's duplicate
rules), missing photos (the Missing Photos rule) and metadata updates
(XMP sidecars saved after the photo came in or was last edited here
that say something the library doesn't). It runs without the session
(SyncInput + scan_with) so the app can scan on a worker thread.
folder.synchronize imports the new files in place and, when asked,
moves missing photos to Recently Deleted and reads the updated
sidecars, all as one "Synchronize Folder" undo step. Only folders the
library holds photos in are accepted, never the startup disk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes. folder_records_follow now drops records left at the
destination even when the moved folder carries none, so an unlabelled
folder renamed onto an old path no longer inherits that path's label.
folder.label {label: none} works once the folder's photos are gone (a
label can no longer get stuck); setting a label still needs photos.
Adds tests for both, undo/redo of a labelled move, and labels replayed
from the journal without a snapshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
folder.label {path, label} gives a library folder a colour label (or
takes it off) as one undo step; only folders the library holds photos in
are accepted. library.folders reports each row's label. folder.rename and
folder.move commit the folder records with the photos' relinks, so the
labels of the folder and the folders inside it go along and undo/redo
carry them back and forth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export now leaves out only what it must, and its counts are exact:
- A name starting with a byte-order mark is left out. On the first
line, the reader would take the mark for the file's own.
- A synonym in braces is written, since it reads back. An empty
synonym, or one that repeats another in a different case, is
skipped quietly, as the reader would drop it anyway.
- keyword.export counts and checks for Capture One the keywords it
actually wrote, now returned with the text. It used to guess from
the lines, so a name starting with a brace was missed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A list saved as UTF-16, as Windows' Notepad can, now says to save it as
UTF-8 instead of only "not UTF-8 text". Text passed to keyword.import is
capped at 16 MB, like a file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list format can't hold some names. A name in brackets or braces
would read back as a left-out keyword or a synonym. A line break or a
leading space would split it or break the indentation. A keyword nested
deeper than 64 levels won't read back either. These keywords, and those
inside them, are now left out of the file. keyword.export names them as
`unwritable`, and the toast says which.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>