The dialog's "Scanning… n / m" counted only the files probed, so it
could sit at m / m while the library's photos were still being checked.
SyncProgress counts both halves (its `files.cancel` still stops the
whole scan).
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>
Highlight reconstruction rebuilds clipped channels from the chromaticity of
unclipped pixels nearby. Three changes to that colour:
- an unclipped pixel counts by its brightness ((m / 0.8 clip)^4, at most 1),
filled coarse to fine by push-pull, so dark wires across a bright cloud no
longer tint it (the iPhone frame of #548 turned cyan at Highlights -100);
- pixels within 6 px of clipping (partly demosaiced from clipped samples, 25 %
greener than the sky on that frame) give no colour unless nothing else does;
- the rebuilt colour is drawn toward neutral at its brightest channel as more
channels clip ((n - 1 + s) / 2, s rising as the next channel nears its clip).
Binned previews keep each channel the mean of its block and mark blocks with a
clipped sample in a mask (develop_binned_masked, reconstruct_masked), instead
of taking the block maximum: one clipped specular sample no longer becomes a
green speckle at 500 px (HUAWEI PRA-LX1, WAS-LX1A). The denoised twin uses the
same mask. Camera-colour fitting keeps the old binning.
RENDER_CACHE_VERSION 26 (adjust at merge).
The steps to merge were counted from the undo history's length, which
stops growing at its 1000-step limit: with a full history nothing merged,
or only the last part was relabelled "Synchronize Folder" while the
import stayed behind it. Session now counts committed steps
(Session::commits) and Synchronize merges by that count.
Once the import is in, a later part that fails is listed in `failed`
instead of aborting with the import left as a separate step.
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>
scan_with ran the import scan beside the photo checks in a thread scope
on every target; on wasm32 a thread can't be spawned and Scope::spawn
panics, so Synchronize Folder… crashed the web build. There the two run
one after the other, like the other file work in the engine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-click a folder (or a disk other than the startup disk) in the
sidebar's Folders section ▸ Synchronize Folder…: the dialog opens at
once and scans on a worker thread (with progress), then offers Import
new photos / Remove missing photos / Read metadata updates with their
counts (Lightroom Classic's defaults: import only). Synchronize runs
folder.synchronize on exactly the scan shown; closing the dialog
cancels a running scan and lets go of the result. New strings are
translated in every shipped language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan waits on the disk, not the CPU, so its two halves now run at
once: the look for new files (which probes on its own threads) beside
the checks of the library's photos (missing file, newer sidecar), which
run on several threads like the import probes do. A cancelled scan stops
checking. Results keep the photos' order.
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>
Rows below the split row use the existing lossy Huffman path; from the split
row on every pixel is a (bits - 4)-bit word: a complete prefix code for the
difference category followed by the top bits of the T.81 extra bits, dequantised
to the centre of the kept bin, with the usual two-left predictor and an
unclamped running value. The rule was derived by black-box analysis of the 11
CC0 samples (all decode to the same samples as the analysis decoder).
The decoder checks that the fixed-rate section ends within the last byte of the
strip (whole rows plus 0..7 padding bits) and otherwise keeps refusing the file
(preview only), so a file that does not follow the rule is never decoded wrongly.
RENDER_CACHE_VERSION 28 (main is 22, open PRs use 23-27): adjust at merge.
LRC-LIB-LIBFOLDERS lists folder.label and the Set Color Label menu, with
its limits (no filter or sort by folder label; labels don't follow a
folder moved outside LightCraft). What's new describes the feature and
catalog format v4. The roadmap notes folder records as the first step
towards Classic's folder-id model and when the next step would pay off.
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>
heic-rs reconstructs iPhone HEVC tiles bit-exactly (checked against libde265), but
its colour stage was 2-27 code values off libheif on every iPhone photo: it ignores
the HEVC VUI and assumes BT.709 limited range. lightcraft-heif now converts YCbCr
itself (nclx, else VUI, else H.265 defaults; nearest chroma, bilinear for odd clap
offsets, as libheif), inherits tile ICC/nclx on grids, follows libheif's imir axis
and clap rounding, and outputs 16-bit RGB. Measured on 30 real and synthetic
files: within 1 code value of libheif (pillow-heif 1.8 / libheif 1.23).
Also: HEIF header reader for import (no pixel decode), embedded thumbnail for small
decodes, nclx/VUI colour hint (shared CICP mapping with PNG), clear 4:2:2/4:4:4
Unsupported error, Format::not_decodable as the one source of "why not" (import
reports "HEIC/HEIF support isn't included in this build"), `.heif` imports.
Release packaging and Nix build with the heif feature, as whats-new promised.
lightcraft_heif::testdata writes lossless (PCM) synthetic HEICs for the tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-click a folder or disk row in the sidebar's Folders section ▸
Set Color Label ▸ a colour or None (folder.label). A labelled row shows
the label's dot before its count, and its spoken name includes the label.
The colour items are shared with the photo menu (grid::label_items):
each is now a control-channel widget (photoLabel:/folderLabel:<colour>),
and choosing None closes the photo menu like choosing a colour does.
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>
Library folders can now carry what the library knows about them beyond
their photos: FolderRecord (for now a colour label), kept in
Catalog.folder_records under the folder's identity (folder_key) and
changed through Op::SetFolderRecord, one undo step. An empty record is
never kept; a path that names no folder is refused.
folder_records_follow builds the ops that carry a folder's records and
those of its subfolders to its new place on rename or move (dropping
stale records left at the destination), so they can be committed with
the photos' relinks. FolderNode reports the label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C5060WZ, C7070WZ, SP-510UZ, SP-550UZ, SP-565UZ and SP-570UZ files say
uncompressed 12-bit with many strips whose byte counts sum to exactly
width x height x 1.5. main refused them because the 12-bit branch wanted a
single strip, so they fell to "Olympus compressed ORF" and opened
preview-only. The samples are packed MSB-first, the strips hold two fields
(even output rows, then odd ones) with one gap at the seam, and the last row
of the second field sits right after the last strip. Accepted only when all of
that holds exactly. CFA comes from the Exif tag, white balance from maker-note
entries 0x1017/0x1018, black 0. RENDER_CACHE_VERSION 27 (adjust at merge).
A keyword with a line break in its name split the export toast in two.
Names left out now show escaped (Line\nbreak).
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>
What's New now says that an unreadable list names its line or asks for
UTF-8, and that export names any keyword it had to leave out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Se añadieron 1 palabra clave" and "1 Stichwort/Stichwörter" didn't read
right. The counts now follow a label ("Palabras clave añadidas: 1",
"Stichwörter hinzugefügt: 1"), which reads right for any number, as the
Portuguese toasts already do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A list that couldn't be written, for example to a folder that's gone,
used to fail silently from the menu. Its error now shows as a toast, as
Import Keywords…'s already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like the other menu commands, Auto-Tag from Tracklog stamped its toast
with a throwaway context whose clock starts at zero. A while into a
session, the toast had already expired by the time it was drawn. It now
uses the app's context. Lightroom catalog import gets the app's context
too, so its background work repaints the window it belongs to.
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>
Reading a list looked each line up in all the lines before it, and
importing looked each keyword up among every photo's keywords: a
40,000-line list into a 20,000-photo library took nearly two minutes.
Both now use an index built once, and the same import takes about a
second.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Classic Mac line ends (a lone carriage return) are lines: a whole
file used to become one keyword holding them.
- Indenting with spaces is refused at its line: the levels are tabs,
and space-indented files (hand edits, editors that turn tabs into
spaces) used to come in flat without a word.
- Control characters in a name, and "|" in a synonym, are refused.
- A keyword repeated in another case keeps its first spelling for the
keywords inside it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parity row covers File ▸ Import / Export Keywords… and the apps
that share the format, and lists what stays open (IPTC keywords in
exported files, hierarchy from other apps' XMP fields); What's New
explains it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The File menu (beside the presets) imports a keyword list file
(Lightroom Classic's, Capture One's, Photo Supreme's .utf8) and exports
LightCraft's, with native file dialogs off the UI thread like the
other file commands; the desktop app gives the synchronous ones too.
Import says how many keywords it added and how many gained synonyms;
export says how many it wrote and names the keywords Capture One won't
import (; , < or > in a name).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI's menu commands made their toasts with a fresh egui context,
whose clock starts at 0, while the app checks a toast's end against its
own clock: once the app had run a few seconds, every such toast (Auto
Advance On, presets imported or exported, the keyword painter, …) was
over before it was drawn. They use the app's context now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `keyword.export {path?}` writes the keyword list file (Lightroom
Classic's format, which Capture One, Photo Supreme and Bridge read),
or gives its text, and names the keywords Capture One's importer
refuses (none of ; , < > may be in a list it reads).
- `keyword.import {path | text}` reads one (Photo Supreme's Formatted
Vocabulary File too) as one undo step → {added, updated}. A line it
can't read is named and nothing is taken; a file larger than 16 MB or
not UTF-8 is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Photo Supreme's Formatted Vocabulary File (Data ▸ Export to Formatted
Vocabulary File) turns out to be Lightroom Classic's keyword list
format: plain text with Windows line ends, a top-level category in
square brackets, keywords a tab further in at each level. A test with
a file of that shape pins it down; a real export of 118 keywords read
and wrote back line for line. (Photo Supreme reads Lightroom keyword
files too: Data ▸ Import from Lightroom Keywords file.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Lightroom Classic's keyword list files (Metadata ▸ Import / Export
Keywords): a keyword a line, those inside it one tab further in, a
synonym in braces on a line of its own under its keyword, a keyword
left out of export in square brackets.
- `Catalog::keyword_list_text` writes the whole tree that way, by name.
- `keywords::parse_keyword_list` reads one: Windows line ends, a
byte-order mark and blank lines are fine; a line it can't read (two
levels down at once, a synonym of nothing, a "|" in a name, nesting
past 64 levels: a file is input) is an error naming its line.
- `Catalog::import_keywords_ops` adds the keywords the library doesn't
have, with the file's attributes and in the library's spelling of
the levels it has; those it has keep their attributes and gain the
file's synonyms. One undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Set name" (Edit Keyword Set, Save Keyword Set) read 标签集名称 /
標籤集名稱, the colour label sets' term; it is 关键字集名称 /
關鍵字集名稱, as the rest of the dialog says. "Edit Set…" says what it
edits: 编辑关键字集… / 編輯關鍵字集….
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Edit Set… on Recent Keywords saves a new set (`new: true`): a name
another set has is refused, where it used to replace that set.
- The dialog checks names before saving and says why in the user's
language ("There is a keyword set “…” already"; Recent Keywords is
built in), instead of showing the engine's message.
- A set deleted while its dialog was open is saved again, as a new set,
rather than refused with no way out but Cancel.
- Save as a new set (Lightroom Classic's Save as New Preset) keeps the
edited set and saves the edits under the new name.
Russian gets "Recent Keywords", which showed in English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving compared set names by any letter's case while using, deleting
and the current set compared ASCII case only: "été" saved over "ÉTÉ"
but couldn't use or delete it. Every set command (and Recent Keywords)
now compares names one way, as keywords are compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving Recent Keywords as a set under the name of an existing one
(Edit Set… on Recent Keywords) replaced that set's keywords without a
word, while renaming onto a taken name was refused. `keyword.saveSet`
takes `new: true` for a set that must be new, and refuses a name
another set has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`keyword.saveSet {name, replace}` without `keywords` took the current
set's keywords, so renaming a set that wasn't current gave it another
set's nine. The keywords default to the renamed set's own now (the
current nine only for a new set), and an empty `replace` is no rename.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nine fields took their width from what the dialog had while it
found its size: the first two columns came out squeezed ("ceremony"
showed "ceren") and the third wide. They are now equal columns of a
set width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Keyword Set menu gets Edit Set… (Lightroom Classic's Edit Keyword
Set): the set's name and its nine keywords, one field per ⌥ key, typed
in place. Editing Recent Keywords and naming them saves a new set and
makes it current; editing a named set renames it in its place. An
empty field is an empty slot, shown as an idle button so the others
keep their keys. Return in any field saves; a set without a name says
so and the dialog stays open. Russian gets "Set name", and the label
check covers the new labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For Edit Keyword Set (Lightroom Classic's), `keyword.saveSet`:
- keeps each slot in its place: an empty one ("") is an empty slot, so
the keywords after it keep their ⌥ keys (empty ones were dropped and
the rest moved up); trailing empty slots go, nine at most;
- holds each keyword once, whatever its case: a second one leaves its
slot empty;
- with `replace`, renames that set in its place among the sets and
keeps it current; a name another set has is refused.
⌥ on an empty slot does nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A DNG can state a white level the sensor never reaches (an iPhone 15 Pro Max Bayer DNG states 4095 but clips at 3567), so clipped highlights were not recognised and white balance tinted them magenta. On decode, samples that form a plateau clearly below the white level, with nothing above it, are raised to the white level. RENDER_CACHE_VERSION 25 (needs adjusting at merge).
German says "Es werden keine Stichwörter exportiert", the natural word
order; Japanese names the view "書き出されるキーワード" (the keywords
that will be exported) rather than the vaguer "書き出される内容".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>