Auto Mask strokes weight each dab by similarity (log luminance + chromaticity) to the pixel under
its centre and are refined by a cross guided filter on log luminance (max-combined, never removing
paint). Local Noise smooths small-amplitude luminance detail (negative boosts it) and calms colour
noise; Moiré blends chromaticity towards a blurred plane (appended to the GPU mask buffer);
Defringe desaturates purple/green casts on edges. Pixel tests per slider and for the edge
constraint, CPU/GPU equivalence cases, Auto Mask checkbox and Moiré/Defringe sliders in the panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderRequest gains a sample format (OutputDepth: U8 for previews, U16, F32Linear); deep renders
run the per-pixel stage with the exact sRGB curve and return Rendered::deep alongside the 8-bit
image (GPU stays 8-bit: deep exports render on the CPU). Export encodes deep renders: 16-bit PNG and
TIFF, 32-bit float linear TIFF with a linear ICC profile we generate, 10-bit AVIF (YCbCr planes into
ravif). Output sharpening and watermarks work on deep images. ExportOptions.bitDepth with per-format
defaults (TIFF 16-bit, others 8-bit), export dialog bit depth choice (remembered with the last
export), MCP export schema. Tests: a smooth gradient has 1024 levels in a 16-bit TIFF vs 13 in
8-bit, float TIFF decodes to the same light as 16-bit, per-format depth rules, AVIF 10-bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The profile encoder packs tags back to back, so the odd-length description of the
"Adobe RGB (1998) compatible" profile misaligned the copyright tag and macOS ImageIO ignored the
profile in PNG iCCP (sips showed none). Tags are re-laid at aligned offsets; already aligned
profiles (sRGB, P3, ProPhoto, Rec. 2020) are byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings (Cmd+, / app menu on macOS, Edit elsewhere) with General, Import,
Performance and Interface tabs. App settings (startup view, delete
confirmation, GPU, preview size, filmstrip/grid badges, last library) live in
UiState.settings (ui.json); library settings (raw / non-raw / per-camera
import presets, XMP, thumbnail cache size) go through library.preferences into
prefs.json. Import applies the matching default preset; the photo remembers it
as its import look (unedited; Reset returns to it). Open Library… switches
libraries at runtime.
View modes: F full-screen preview (photo on black, arrows step, I cycles the
info overlay, Esc exits), Cmd+Shift+F window full screen, Cmd+I cycles the
loupe info overlay (off / file + date / exposure), Navigator mini map while
zoomed (click/drag pans).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog keyword ops (one SetMeta batch: undoable, replayed from the op log), hierarchical
a|b|c keywords (tree with counts, parent filters include children), keyword.* commands,
rename/merge dialogs, Keywords-panel suggestions; catalog, engine and headless tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The selected mask's evaluated alpha is drawn by the renderer as Overlay::Mask (CPU and GPU share the
alpha the render used; hidden masks are evaluated on demand), kept out of the view-preview cache.
Pins for radial/linear/brush components select their mask on click and move the component on drag;
view.maskPins hides them. CPU/GPU equivalence and headless keyboard/pointer tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog date runs (stacks never split), Sort.group (auto/day/month/year/none) in the sort
menus, library.groups query, sticky headers, header click selects the group; layout unit
tests and a headless test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderRequest gains an output space (default sRGB; previews stay sRGB). The per-pixel stage converts
scene-linear Rec.2020 to the target primaries, gamut-maps into the target gamut and encodes with the
target curve, on CPU and GPU (WGSL finish kernel reads the matrix, luma weights and curve from its
parameter block). sRGB output is byte-identical to before on the CPU. Export embeds our own
generated ICC profile for the space; AVIF stays sRGB. ExportOptions.colorSpace, export dialog and
MCP export schema. Tests: P3 colour survives a P3 export (decoded through the embedded profile),
every space/format round-trips its profile, GPU equivalence per space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- memory::budget (default min(25% RAM, 1.5 GiB); LIGHTCRAFT_MEMORY_MB, app.memoryBudget):
half for engine caches (sources + rendered previews, LRU across them via a global
use tick; the photo on screen stays), a quarter for decodes in flight (WorkGate:
thumbnails, prefetch and import probes wait; loupe/export loads never wait), an
eighth for the GPU free pool.
- GPU: best-fit buffer reuse (<= 25% slack), buffers released outside a render are
pooled at once (no stranded retired lists), pool trimmed after 3 s idle.
- Decodes drop file bytes and raw samples early, colour/crop/orient in place.
- macOS: apps install malloc_zone_pressure_relief as the release hook (after each
decode, import and idle trim): freed large blocks no longer stay resident.
14 raws + loupe + 12 steps (M4 Pro): peak RSS 1.9-2.3 GB -> 0.83-0.99 GB, footprint
2.5-2.6 GB -> 0.91-0.96 GB, heap peak 930 -> 656 MB; stepping median ~13 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix-ups: diagnostic overlays never populate the view-preview cache; Photo Merge's RenderRequest gets
the new overlay field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Photo Merge's Create Stack option now puts the merged DNG on top of a
collapsed stack of its sources (Catalog::stack_with_ops), so the grid
shows the result; test in tests_merge (and no stack without the option).
Photo Merge dialogs live in a Photo > Photo Merge submenu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also: GPU retired-buffer list leaks its buffers at thread exit instead of
dropping them (wgpu's thread-locals may already be gone; the drop aborted
the test process intermittently), and headless UI tests wait up to 120 s
for renders (they timed out on a loaded machine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Overlay::Spots(threshold): luminance minus its Gaussian blur (sigma relative to the long edge,
so the same specks show at any preview size), thresholded to white/black; the threshold slider
(0..100) raises sensitivity. Runs after either renderer like the other overlays (equivalence:
share of differing pixels, since the view is binary). Remove panel: Visualize Spots checkbox +
Threshold slider; UI command view.visualizeSpots (A) opens the Remove tool with the view on or
toggles it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decoders take a Mode: Header reads everything but the samples (DNG, CR2, ARW,
RAF, RW2; PEF when it has crop tags). NEF/ORF (and PEF without crop tags)
derive geometry/CFA from the samples and still unpack them. decode() stays
bit-exact (debug-hash of every corpus/raw decode identical before/after); the
corpus test asserts probe_info == decode().info() for all 37 files.
Import of 14 corpus raws: 3.4 s -> 1.4-2.0 s; peak RSS through import
1.41-1.49 GB -> 1.12-1.22 GB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pet eyes are found by brightness (the tapetum glows in any colour) and the disc is replaced by
a dark neutral pupil (Darken scales it); redeye.catchlight {index, on, offset} adds a soft
white catchlight at an offset from the pupil centre in pupil radii. Red Eye panel: Red Eye /
Pet Eye mode for new corrections and an Add Catchlight checkbox. CPU/GPU twins updated
together (equivalence covers both modes and the catchlight).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pipeline::redeye: inside the user's ellipse the pupil is found on the source image (a fixed
grid in normalized coordinates, so previews and exports agree): weighted centroid and area of
red-dominant pixels (or, for pet eyes, of the brightest ones). Pupil Size scales the detected
disc; rendering in scene-linear light after white balance neutralizes the red where it is red
(iris and catchlights stay) and darkens it. CPU (lin stage) and GPU kernel redeye_k with an
equivalence test (both the GPU and the CPU scene-linear paths). Indexed controls
redEye.<i>.pupilSize/darken, redeye.add/delete commands, the Red Eye panel (selected eye's
sliders, delete) and the loupe tool (drag an ellipse, click to select).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filter bar above the grid (View > Filter Bar, ⇧F, or the top-bar filter
icon): rating with ≥ / = / ≤, picked / rejected / unflagged, colour labels,
kind, edited/unedited, camera / lens / keyword pickers, Clear and "Save as
Smart Album…" — every control drives library.filter. Two rows when the
grid is narrow. Empty grid under a filter says so. Headless test clicks
the bar's widgets and saves a smart album.
docs/parity.md: rows for smart albums, stacks, virtual copies, compare,
survey, filter bar, menu bar updated; finished top gaps removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- engine: `merge.hdr`, `merge.panorama`, `merge.hdrPanorama` (bracket size from
EXIF); MergeJob (Send) with progress/cancel, preview renders through the develop
pipeline (deghost overlay, auto-crop frame); finish writes <name>-HDR/-Pano.dng
next to the first source, imports, selects, applies Auto Settings / auto crop
- ui: Photo Merge dialog (HDR / Panorama / HDR Panorama) with live preview on a
worker thread, background final merge with progress toast; context menu and
Ctrl+H / Ctrl+M (Ctrl now means the physical Control key on macOS)
- cli: `merge hdr|panorama|hdr-panorama`, `synth-merge` for synthetic inputs
- merge::synth: procedural brackets (DNG) and panorama views; docs/merge.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auto uses Upright's consensus level angle as the crop angle; sign verified on a tilted test image
and pinned by a test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolved: album.create (main's one-liner + smart: None), menus (panels in
the Window menu, main's dialog.newFolder ⇧⌘N, app.showInFinder and
app.exportPrevious placed in the menu layout), shortcuts (native-menu skip
+ main's aliases and UI-over-engine rule; Shift+X/Shift+U aliases go
through the culling path; my Shift+P/X/U flag-and-advance dropped in
favour of the aliases). X and Delete stay egui-handled (contextual keys).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
develop.targeted {target: curve|hue|sat|lum, x, y, delta, channel?}: samples the photo at the
point on a proxy render with the target stage and everything after it neutralized, then
nudges the parametric curve region (by the sampled tone and the splits), moves/adds a point
on the master/RGB point curve, or adjusts the colour-mixer (or B&W mix) bands weighted by
their share of the sampled OkLCh hue. UI: target buttons in the curve editor and the colour
mixer; a vertical drag on the loupe sends one step per unit inside one interaction (one
undo step). New original vector icon (Target) in icons.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Up to 8 OkLCh samples (pointColor.pick samples the colour after the mixer on a proxy render);
per sample hue/saturation/luminance shift, variance (compress/expand the spread around the
sample) and an overall + per-axis range with soft edges, applied after the colour mixer in
ColorOps and its WGSL twin (equivalence case). Indexed control specs
(pointColor.<i>.hueShift …) so develop.set/controls/MCP address each sample. Visualize range
is a render overlay (RenderRequest::overlay) drawn after either renderer: the sample's own
edit is left out and colours outside the range turn grey. Point Color flyout in the Color
section with swatches, eyedropper, sliders, visualize and delete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
menubar.rs builds File/Edit/View/Photo/Window/Help from engine + UI
commands (layout table for order and separators, parameterized submenus
for rating, flag, label, sort, add to album), with live labels ("Undo
Exposure"), enabled and checked state; ui.menu.tree exposes it. The
desktop app installs it natively on macOS with muda (pure Rust over
AppKit; no GTK — muda is a macOS-only dependency): app menu (About,
Services, Hide, Quit), Window menu (Minimize/Zoom/Bring All to Front),
Help menu; per-frame state sync; accelerators without Cmd and the text
editing ones step aside while a text field has focus; natively handled
shortcuts are skipped by the egui handler. Other hosts (web, Windows,
Linux) get the same menus in the top bar. Panels/sections/tools move to
the Window menu. Verified with System Events: menus listed with live
enabled state, View > Survey dispatches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Delete" parsed as egui's forward-Delete key, so the Mac ⌫ key (Backspace) didn't delete. In the
Masking panel ⌫ now deletes the active mask; while retouching it does nothing. ⇧⌘N no longer
fires album.create (which needs a name).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
curve.refineSaturation (0..100, default 100 = the curve's own saturation): after the tone
curves, the chroma around luma is rescaled so saturation moves from the curve's towards the
pre-curve value, keeping the curve's tone. CPU (finish::refine_saturation) and GPU twin,
equivalence case, slider under the curve editor, crs:CurveRefineSaturation import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compare view (Shift+C): select | candidate side by side with synced zoom and
pan; arrows move the candidate, rating/flag/label keys act on the active
(candidate) photo, swap / make-select. Survey view (N): the selection tiled,
keys act on the active photo, hover x removes from the survey. Auto Advance
setting (Photo menu, bottom bar toggle) and Shift+P/X/U flag-and-advance.
Escape returns to Detail. Headless keyboard test.
Fix: render-job keys include the photo id, so a view slot never shows one
photo's result as current for another with identical settings/size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>