The SDR shoulder in ToneMap::display_hdr ran at neutral Contrast, Whites
and Blacks too, so an unedited JPEG or PNG exported neutral 255, 254 and
253 one level darker and a white background never came out as 255. The
shoulder now shapes edited curves only; neutral sliders are the identity
up to white and clip above it. Edited curves render as before (V1).
Tests: the identity test runs to 1.0, a new pipeline test renders the
issue's neutral greys back exactly, the sRGB ramp round trip is exact,
and a guard keeps the shoulder on edited curves.
Constrain Crop only applied at render time, and the Crop tool shows the
uncropped view with the stored crop, so in the Crop panel (where the
checkbox is) ticking it or moving a Geometry slider changed nothing.
Session::set_develop now shrinks the stored crop about its centre until it
fits the warped photo when Constrain Crop is turned on, or when an edit
changes the warp (Geometry, Upright, lens corrections, section eyes) while
it is on. A crop edit itself is kept as drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Describe merged the detector's per-instance masks. Those stay near 0 even well inside what they
found: a sky came out about 87 % selected, a volleyball net about 70 %, a person about 94 %.
Two instances that touch both fade out where they meet, so a line showed between people standing
side by side.
- The detector's semantic head is now ported: semantic_projection over the pixel decoder, the
transformers implementation's semantic_seg. It matches the reference to about 1e-3, like the
instance masks. tools/sam3_reference.py writes it and the reference test checks it; older
reference files still pass.
- Each instance found (score above the threshold, as before) takes the semantic logits where it
is the strongest instance and near (above -4). The semantic mask is sure inside and runs on
across the boundary between two instances. Far from every instance nothing changes, so
Describe selects nothing it didn't find before.
On 16 prompts over three photos (a mountain landscape, two gym photos), the median coverage
inside each selection rises, e.g.:
- sky 0.87 -> 1.00
- mountains 0.81 -> 0.99
- net 0.71 -> 0.99
- floor 0.92 -> 1.00
- person 0.94 -> 0.99
Each selection gains 0 to 1.9 % of the photo at its soft rim and loses none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OPFS lookups turned any exception into "absent", and start-up only logged
other read errors, so a library with an unreadable file opened incomplete
and the next save could overwrite the stored copy. Only NotFoundError now
means absent; when a stored library file or the active-library pointer
can't be read, the library isn't opened, nothing is saved and the window
names the file. An unreadable thumbnail index no longer deletes every
stored thumbnail as unindexed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
The browser backup copies the stored library files, so while a save to
browser storage is failing it reported success but left out the queued
changes. It now refuses with the number of unsaved changes and why
(Session::unsaved), and is allowed again once they are written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Both sides added code at the same places: main's Prepared filter struct next to
search_tokens()/unquote() in query.rs, its touched_since test next to the
people-search test in tests.rs, and its 2026-10-14 What's New sections next to
this one. All kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
These files carry no JPEG: their preview is an uncompressed 8-bit RGB
IFD (1217 x 913 in the H4D-40 FFF, 218 x 218 in the CFV 3FR). The
largest such image is handed on by embedded_preview as a small baseline
TIFF, which lightcraft_codecs::decode reads, so 3FR / FFF show a preview
in the grid and loupe before they are developed. No Orientation tag is
written: the raw file's applies, as for JPEG previews without one. The
FFF's 16-bit copy (with a TransferFunction of unknown meaning) is not
used.
decode_raw_preview applied a container-declared colour space by forcing
the JPEG decoder on any owned preview; it now checks for JPEG bytes, so
a wrapped TIFF (or a JPEG XL preview) goes to its own decoder.
Checked on the CC0 H4D-40 FFF and CFV 3FR through
embedded_preview_srgb: right colours, the 3FR upright through its
Rotate90. Tests cover the wrap, the choice among RGB images, a short
strip and truncated files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2NDVXA8jQQWeJb4YVTb6b
With stem naming, `IMG_0001.xmp` is the sidecar of every file of that stem and, in a
folder Lightroom has worked on, it is the raw's. On import the packet was looked up
with the library naming alone, so a TIFF or JPEG arriving beside a raw of the same
name read the raw's packet and took its rating, label, keywords and `crs:` develop
settings (the case radialmonster reported in #92; the read side of a catalogued photo
is #1523).
`ImportStems` ranks the files an import is adding as if they were catalogued already,
in the order `StemOwners` uses for writing: the library's files and the arriving ones
together, a raw first, then the file name in lower case. A file that does not own the
stem sidecar reads only its own `<file>.xmp`, never the sibling's packet. A file that
is not catalogued yet has no id, and the id it draws when the import commits is above
every id in the library, so it loses a tie against a catalogued file; between
arriving files the order they arrive in stands for it.
Folder synchronize and the Lightroom catalog import go through the same function, so
a file arriving there is ranked the same way.
An FFF (a 3FR converted on the desktop) has no colour matrix of its own:
the camera's stays in the original 3FR header the FFF carries as a
complete TIFF in private IFD0 tag 0xC51B. The new vendor::hasselblad
reader decodes 3FR / FFF through dng::decode_as and, when the file has
no matrix, takes the matrices from that header, so an FFF no longer
develops with the "camera RGB = sRGB" fallback (H4D-40 as-shot white
moves from xy 0.279, 0.356 to 0.325, 0.336).
RGGB checked against the camera preview on the CC0 H4D-40 FFF and CFV
3FR (raw.pixls.us 931 and 709). Synthetic tests cover the embedded
matrix, an own matrix winning, a missing or damaged header, and
truncated files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2NDVXA8jQQWeJb4YVTb6b
In Detail, dragging with the middle button (the mouse wheel pressed) pans the
zoomed photo with a hand cursor, in every tool. Remove / Heal / Clone, the
mask brushes and Crop no longer have to be left to look around a zoomed photo,
and they don't paint or move during that drag: it is handled with the pinch and
scroll gestures, before the tool overlays.
Right-click keeps the photo's context menu and Space keeps the zoom toggle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decoder tried each high-frequency band at its synthesis width and up to
two more columns, keeping the first width whose entropy stream ended exactly.
Two widths can both do that with different coefficients (#899).
Measured on every C-RAW plane in cr3_corpus.rs (CC0 raw.pixls.us files):
each band decodes at exactly one width. A tile with a neighbour on its right
codes bands 5 and 8 (the vertical high-pass of the level-2 and level-1
low-pass columns) at an odd width, so an even band gains one unused column;
no other band is extended. The width now comes from that rule, and a stream
that does not fit it is corrupt (the CR3 falls back to its preview).
All corpus sensor hashes are unchanged. The property-test encoder now codes
bands 5 and 8 the same way. Middle tiles (three or more C-RAW tiles) follow
the left-tile rule; no public file has one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A right-click on a photo (or any widget) while its own context menu was open
flashed the menu at the new spot and closed it again on the same frame. egui's
`Popup::show` judges "was open last frame" by the popup's id, so the click that
reopened the menu at the pointer also counted as a click while open and closed
it under `CloseOnClick`. A right-click on a different widget was fine.
`widgets::context_menu` wraps `Popup::context_menu` and ignores clicks on the
frame of the right-click that opens or moves the menu; every context menu
(30 sites) goes through it, and `TextField` applies the same rule to its
close-on-click-outside menu (`widgets::context_menu_close`).
Tests: `right_clicking_elsewhere_moves_the_context_menu` (headless grid; failed
at the second frame before the fix) and `right_clicking_again_moves_the_menu`
(text field). docs/ui-parity.md: Context menus row and revision history.
Fixes#1877
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Settings → Dialogs → "Close a dialog by clicking outside it" (off by default,
so a stray click never throws away a dialog's choices). When on, a click
pressed and released on the dimmed backdrop closes the dialog like Esc and
doesn't reach the panels below. A drag that ends outside (a slider pulled past
the edge), a click that only closes a dropdown, and the click that opened the
dialog keep it open. The import review never closes this way.
The press and release are read from the raw pointer events: egui clears the
press origin on release, and a scripted click is a single frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
X is bound to both photo.reject and crop.rotateAspect. In the crop tool
both bindings fired in the same frame and each ran crop.rotateAspect, so
the crop turned portrait and straight back to landscape: X looked like it
did nothing. Swap once when crop.rotateAspect fires itself.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the shared graphic reader and apply EXIF orientation before sizing. Preserve upstream export safeguards and JPEG improvements. All eight local CI gates pass, including the three export regressions.
The Print dialog's Page section gets Lightroom's Page Options for Single Image
and Contact Sheet: the page number at the bottom right, the print settings line
(sharpening, profile, resolution) at the bottom left, and crop marks at the
corners of each photo. They are `print.set {page: {pageNumbers, pageInfo,
cropMarks}}`, saved with templates, drawn in the preview and composed into the
PDF; marks and text turn white on a dark page.
photo.reload decoded every targeted file on the UI thread, so a selection
of raws froze the window until all were read. The command is now split into
plan / probe / apply (engine `cmd::reload`); the app intercepts photo.reload
and probes the files on a worker thread with a cancellable activity row
(kind `reload`, counted progress), applying the result as one undo step.
`wait: true` keeps the old in-place behaviour for agents; CLI/MCP unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Like Lightroom's merge preview: a click zooms in on the point clicked (one
preview pixel per screen pixel, at least twice the fitted size), a drag pans,
another click fits again. The zoom stays while options change, so deghost
levels and projections can be compared at the same spot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use luminance pairs after the existing camera looks fail their colour-pair
minimum. Keep the source colour, exclude clipped pixels from training, and
check held-out luminance and each clipped pixel before accepting the curve.
The darkest supplied D610 sample changes from +2.81 to -0.09 stops at the
median. The other 15 renders are byte-identical. Add synthetic coverage for
source-colour precedence, independent holdout and invalid or clipped input.
Bump LOOK_VERSION to 10 and RENDER_CACHE_VERSION to 67 after checking open PRs.
Engine tests pass. Workspace tests pass with only the known #1457 overlay
test excluded. Formatting, Clippy, HEIC, parity, layers, assets and WASM pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the corrected-radius table convention for full-frame Bayer ILCE-7CR files with distortion Auto or forced Auto. Preserve Off files, unvalidated modes and the existing ILCE-7RM4A conversion.
Add mapping, setting, scope, padding, orientation, resizing and DNG checks. Record camera-JPEG geometry and color measurements, including the remaining wide-end edge residual. Refresh stored lens data with LOOK_VERSION 11 and rendered results with RENDER_CACHE_VERSION 68.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both tone fits refused a curve when the brightest 1/32 of the training
pairs was less than 1.5x the darkest 1/32, a hard cliff before any
acceptance gate: a hazy ILCE-7RM4 burst (issue #631) opened with the
neutral fallback on frames just under it (DSC01630: range 1.489, dE76 vs
the camera JPEG 23.8) and with a camera look on the frame just over it.
MIN_TONE_RANGE = 1.2 still refuses nearly uniform pairs.
A curve under the old 1.5 must also keep the bright areas (camera JPEG
luminance 0.85 and up), which the fit and the held-out gates leave out, no
further from the camera JPEG than the neutral fallback (highlights_hold,
full and partial looks): fitted to a narrow range it extrapolates them, and
reviews found synthetic frames whose accepted curve rendered a 0.93 sky at
0.28 to 0.61. The bright pixels come from the proxies before any training
filter, and for DRO photos the curve is checked lowered as it will be
used, inside the search. The tone curve of a black-and-white camera JPEG
(#577) comes from the same fit, so it gets the same check, by luminance as
its own gates compare: a synthetic frame's 0.93 sky rendered at 0.28
without it. DSC01630 (DRO Auto): look accepted, dE76 1.94. Every other
file of a 309-file sweep (135 public Sony) renders pixel-identical, also
with the camera JPEGs made grey. RENDER_CACHE_VERSION 63, LOOK_VERSION 6
(smart previews refit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reuse the engine's import extensions in MCP and CLI discovery. Add
recursive, case-insensitive discovery tests and record that DSC-F828
SRF import remains preview-only.
Co-Authored-By: Codex <noreply@openai.com>
Batches 36's doc conflicts kept main's side of rows both sides had touched; for some, main had not changed the row,
so the PR's update was lost. Restored: LR-VIEW-FILMSTRIP (#1482), KEYC-COMPARE (#1491 + #1555), LR-IMP-LOCAL
(#1547 + #1820), LR-LIB-STACK (#1556), gaps 28 (#1555), 2 (#1029) and 23 (#1038), ROADMAP UI/UX (#1038). The
raw-parity by-maker and by-use tables are re-measured with coverage.py (545 verified, ~94% / ~85%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holding ⌥ (Option) while dragging a slider shows a diagnostic view in the loupe, as in
Lightroom; releasing ⌥ or the slider brings the photo back and nothing is saved:
- Blacks: shadow clipping (white; black where every channel is 0, the colour of the
unclipped channels where only some are).
- Whites / Exposure: highlight clipping (black; white where every channel is 255, the
clipped channels' colour where only some are).
- Sharpening Masking: the weight sharpening gets at each pixel, white on black.
- Sharpening Amount / Radius / Detail, luminance NR: the image in grey.
New `Overlay` variants (ShadowClip, HighlightClip, SharpenMask, Grey) are drawn by the shared
`visualize::apply`, so CPU and GPU agree; the sharpening mask needs the detail planes and
renders on the CPU (like HdrRange). The mask uses the same weight function as the render
(`finish::sharpen_weight`). With a display profile the clipping views are tested in sRGB, like
the histogram, and diagnostic views keep the photo's histogram instead of the view's.
Tracker: LR-VIEW-CLIPPING ✅ → 🟡 (J only frames the photo, it doesn't paint clipped pixels)
and LR-EDIT-DETAIL-SHARPEN ✅ → 🟡 (Radius and Detail don't reach the render).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A debugging toggle (`view.frameRate`, `{show?}`, a checked item in a new Help ▸ Debug
submenu; LIGHTCRAFT_FPS=1 starts with it on; never saved). In the window's top-right
corner it draws frames per second and the worst over the last second, each frame's UI
CPU time against its time spent waiting (vsync, or the GPU), the latest whole-frame
and zoom-window render times, the render queue, renders thrown away as superseded
(Renderer::discarded), the GPU adapter and its memory, and a graph of the last frames
with a tick where a render arrived. It keeps the window repainting while shown and
logs the same numbers once a second (`fps-hud:`), so scripted drags can be measured.
Perf::host_frame_ms (the frame's CPU time as eframe reports it) is added as #1009
adds it, identically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows ARM64 on main failed since batch 36: #1479's scripts pinned 8dcdacd, which predates the Thai face #1311
made required.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Georgian next to Italian in the language table, menus, catalogs list and docs; ka-formats.json lists the format
strings other PRs added, in English until translated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>