Commit Graph
63 Commits
Author SHA1 Message Date
Claude 4ebb934c56 Merge PR #677
On merge: the HDR histogram is applied in develop_reporting (#712 split develop), is skipped for
draft renders (a second render per drag tick), and HdrBins::bin clamps a hand-built split so it
can't underflow (test hdr_bins_with_any_split_stay_in_range).
2026-10-11 13:09:34 +00:00
Claude 8c4898ec04 Merge PR #645
# Conflicts:
#	crates/engine/src/media.rs
#	crates/pipeline/src/finish.rs
#	crates/pipeline/src/local.rs
2026-10-11 12:03:53 +00:00
Claude a0e422ce4a Merge PR #752
# Conflicts:
#	crates/engine/src/cmd/develop.rs
#	crates/engine/src/files.rs
#	docs/mcp.md
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-11 12:02:46 +00:00
Claude 05939cbc54 Merge PR #928
# Conflicts:
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/uk.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
2026-10-11 11:53:13 +00:00
Claude 4e6cd5d7b9 Merge PR #704
# Conflicts:
#	docs/parity-checklist.md
2026-10-11 11:49:37 +00:00
Andrew FrahnandClaude Opus 5.5 bac0f75551 Soft proofing against printer ICC profiles: intent, Simulate Paper & Ink, device gamut warning
Soft Proofing's Profile menu takes a printer / paper ICC profile (Other…;
`view.softProof {profile, intent, paperInk}`), RGB or CMYK, matrix or LUT.
The CMS (moxcms) builds a 33^3 proof table over the working space: forward with
the chosen intent (Perceptual / Relative), back relative colorimetric; the
destination gamut warning comes from the colorimetric round trip. Simulate
Paper & Ink renders absolute colorimetric: the profile's media white replaces
screen white and the ink black is unscaled. The pipeline gets the table by id
(`Proof::profile`, `register_proof_profile`), so Proof stays Copy and render
keys include it. Proof copies are named after the profile.

Sources: ICC.1:2022 (rendering intents, media white point, absolute
colorimetric); own design for the proof table.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 19:30:46 +11:00
Andrew FrahnandClaude Opus 5.5 6873aab6eb Drafts keep the wide planes across a noise-reduction change (CPU NR drag)
A noise-reduction change invalidated the scene-linear stage and with it every spatial
plane (base, clarity, dehaze, chromaticity), so each tick of an NR drag on the CPU rebuilt
all of them. The plan now also carries the linear key without the NR settings; a draft
render whose image differs from the cached one only in NR keeps those wide blurs and redoes
only log luminance and the fine texture band. Full-quality renders never take them.

Draft NR drag, single thread: prepare 18.7 -> 7 ms (render ~135 -> ~125 ms CPU).

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 19:30:28 +11:00
JordyUSAandClaude Opus 5.5 fb678b9e35 LR-VIEW-HISTOGRAM: HDR histogram for HDR edits
An HDR edit's preview shows its SDR rendition, so its histogram said nothing
about the highlights above SDR white. Now it shows the HDR rendition:
`pipeline::hdr_histogram` renders the edit small (384 px, float HDR, same
framing) and bins it with SDR on the left half and the HDR range on the right
(`Histogram::of_hdr`, `HdrBins`), on a fixed scale of Hdr::MAX_EV_LIMIT (5)
stops above SDR white so it doesn't rescale when the headroom limit changes.
The engine's `develop()` (CPU and GPU renders) and the web worker swap it in
for HDR edits; high-bit-depth, windowed and proofed requests keep the image's
histogram, and a display profile's histogram doesn't replace it. Web workers
carry the HDR bins across their wire (`Histogram::to_words` / `from_words`).

The panel draws it in Lightroom's layout: a solid line at SDR white, dashed
lines between the stops, the headroom limit as an orange line with the range
past it dimmed, a bar under the plot (white; one band per stop in Visualize
HDR range's colours while that is on), SDR / HDR labels and an info tooltip.
Follow-up to #646, ported from #466.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:46:27 +07:00
radialmonster 70103a7e55 LR-EDIT-LIGHT-HIGHLIGHTS: keep local contrast under Highlights/Shadows, as process V2 (#632)
The highlights/shadows base (fast guided filter of log luminance, eps 0.35)
followed thin high-contrast detail, so the per-pixel shift flattened twigs,
lines and texture in bright areas (and dark ones under Shadows); Exposure
kept them.

This changes how every existing edit with Highlights or Shadows renders, so
it ships as a new rendering process (docs/process-versions.md):

- Process::V2 (version 2) is added and made LATEST. New photos and Reset get
  it; existing edits stay on V1 until Update to Current Process. Settings
  saved without the field still load as V1.
- From V2 the base is the same guided filter of a separable median of log
  luminance (radius 0.15 % of the long edge, <= 24 px): fine detail takes its
  surroundings' shift and keeps its contrast, region steps stay put (no new
  halos). V1's base is untouched: local::tone_base_detail is 0 under V1 and
  tone_base then is exactly guided_fast(l, sigma, BASE_EPS), as before.
- CPU (local::prepare) and GPU prepare (median_h / median_v kernels, bit for
  bit with the CPU median) both branch on it via PlaneSigmas::base_detail.
- The base plane's cache key is now (sigma, process, median radius)
  (local::BaseKey), so a warm cache never serves one process the other's
  base. plane_at is shared with the GPU crate.
- finish::base_tone: V2 uses V1's tone map.

Tests: V2 keeps twig/local contrast (>= 0.9 of Exposure's) with no halo; V1
plane sigmas have no median and its base equals guided_fast bit for bit; V1
and V2 render alike without Highlights/Shadows; warm-cache renders across
V1/V2 switches equal fresh ones (fails if the key drops the process); GPU
matches CPU on both processes at 720 and 2400 px, cached and not.

Pentax 645D 2401 at 4000 px: main and V1 renders bit-identical (plain,
Highlights -100, Shadows +100). Highlights -100 bright rel. detail
0.095 (V1) -> 0.147 (V2), Exposure -1: 0.124; Shadows +100 dark rel. detail
0.126 -> 0.148.

RENDER_CACHE_VERSION 54 (adjust at merge).
2026-10-10 13:46:14 -04:00
Jason Baldwin 76e633c9e1 LR-EDIT-COLOR-TEMP: Kelvin white balance for raws whose camera has colour matrices (#730)
Raws without matrices in the file (ARW, NEF, RW2, RAF, CR2, CR3, PEF, SRW,
ORF) were all edited on the relative white-balance scale (6500 K / 0 = as
shot), even when lightcraft_raw::spectral has measured matrices for the
camera. Those 52 bodies (ILCE-7M3, ILCE-7M4, D850, EOS 5D Mark III...) now
edit in Kelvin like a DNG and like Lightroom.

Design:
- One decision on both sides: spectral::find_camera("Make Model") backs
  catalog::relative_wb_camera (Photo::relative_wb); files::WbScaleOf decides
  the same from the decoded file. A corpus test asserts they agree.
  Photo::fitted_look keeps the old predicate for smart previews.
- The matrices only convert: As Shot is the file's multipliers read through
  the model's ColorMatrix1/2 (spectral::as_shot_white_through). The look
  (JPEG fit / profile / spectral fallback) is unchanged, and the slider
  adapts from that white with Bradford. The camera-space rebalance stays
  reserved for files with their own matrices.
- Presets resolve to their Kelvin values (WbMode::preset, sources cited).
- The Color panel shows the white balance in effect (effective_wb).
- XMP: crs::Target::for_file takes the camera; matrix cameras read
  crs:Temperature as Kelvin (marked "scale": "kelvin"), others keep the
  #510 as-shot shift.

Migration (no look changes):
- WhiteBalance.scale: WbScale { Legacy (absent in JSON), Kelvin }. A
  white balance without a scale keeps its old meaning. On a spectral-Kelvin
  raw (SourceInfo.legacy_relative_wb) the pipeline composes the old
  adaptation (6500 -> value) with the new one (as shot -> ...) in Bradford
  cone space (local::legacy_white). Von Kries scalings compose exactly, so
  the matrix matches to f32 and the pixels are identical
  (pipeline legacy_relative_white_balance_renders_as_before_on_a_kelvin_raw,
  media old_relative_white_balance_on_a_kelvin_camera_keeps_its_look...).
- Every white-balance writer (develop.set/adjust/quickAdjust/wb/wbPick/
  reset) first runs local::normalize_wb: a legacy value becomes the Custom
  Kelvin value with the same look and is marked Kelvin. Partials without a
  scale (old presets, develop.merge) stay legacy.
- Catalog format 7: Op::SetAsShotWb replaces the 6500/0 as-shot recorded
  by older imports when the file is next decoded or reloaded (system op,
  not an undo step). Format-version test updated.
- The web wire carries legacy_relative_wb.

Docs: parity (LR-EDIT-COLOR-TEMP), camera-preview-colour, xmp-interop,
lightroom-catalog-import, control-protocol, mcp, whats-new.
2026-10-10 16:09:31 +02:00
aayush113002andClaude Fable 5.1 ec1e26545e M8: spot.findBlemishes — heal small blemishes on detected faces
A sibling of spot.findDust for portraits: with the YuNet detector installed, small
round spots on each face's skin that are darker or redder than the surrounding skin
become heal spots (one undo step). The detector's box and landmarks define the skin
(ellipse, skin-colour gate, eye band / brows / nostrils / mouth cut out); a candidate
must stand out from a blurred copy beyond the sensitivity threshold and 2.5x the local
texture, then grow into a small roundish blob. Conservative: faces under 160 px get
nothing, faces with more than 12 candidates (stubble, freckles, pores) get nothing, at
most 6 per face (maxPerFace). Classical, no model beyond the detector.

Tests (synthetic, no model): cheek spots found at their positions, clean face empty,
spots on features ignored, small face ignored, speckled face treated as texture,
hostile boxes and an empty image do not panic. New LR-REM-BLEMISH parity row, Find
Blemishes section in docs/faces.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 15:48:31 +05:30
Brandon ThomasandClaude Opus 5.5 e46e7ab265 Merge PR #646 (HDR peak threaded through #533's base_tone; V1 at peak 1 unchanged)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:26:27 -04:00
akvaithiandClaude Opus 5.5 af06f2dbc1 LR-HDR-VISUALIZE: HDR controls in the Light panel, Visualize HDR, export option
- Light panel: HDR switch, headroom limit and an SDR Rendition flyout
  (shown with HDR on); Visualize HDR beside the switch.
- `Overlay::HdrRange` / `view.visualizeHdr`: the SDR render dimmed to grey,
  with pixels the HDR rendition puts above SDR white painted in four bands by
  stops above it. Rendered on the CPU from the float HDR render.
- Export dialog: "HDR output" for JPEG (gain map) and 32-bit TIFF.
- Parity (Q. HDR 2 ✅ 3 🟡, LR-EDIT-HDR-MODE ✅, LR-EXP-HDR 🟡), README, ROADMAP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:40:37 -05:00
akvaithiandClaude Opus 5.5 791526aa8c LR-HDR-EDIT: HDR settings, HDR tone map and the float HDR render
- `DevelopSettings.hdr`: enabled, headroom limit (`max_ev`) and SDR-rendition
  offsets (brightness, contrast, highlights, shadows, whites, clarity), with
  control specs (`hdr.maxEv`, `hdr.sdr*`) and copied with the Light group.
- `ToneMap::hdr` / `display_hdr`: the SDR curve through the midtones, with
  highlights lifted up to `2^max_ev`; identical to the SDR tables at peak 1.
- `OutputDepth::F32Hdr`: unclamped linear output up to the peak. The finish
  stage carries the peak through highlight desaturation, gamut mapping,
  encoding, curves, grain and LUT profiles; with HDR off every branch is the
  SDR code path, bit for bit.
- `settings_for`: every non-HDR render (CPU and GPU) uses the SDR rendition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:00:54 -05:00
Brandon Thomas 987a572512 Merge PR #612
# Conflicts:
#	crates/pipeline/src/finish.rs
#	docs/parity.md
#	docs/whats-new.md
2026-10-09 20:56:58 -04:00
Brandon Thomas 6f323e01d9 Merge PR #533
# Conflicts:
#	docs/parity.md
2026-10-09 20:50:41 -04:00
kelannaandClaude Opus 5.5 3bcc265bf6 LR-PREF-DISPLAY: monitor ICC profiles (display colour management)
Previews were always sRGB, which wide-gamut monitors show oversaturated.
Settings > Display > Choose Profile... (and the UI command
app.displayProfile) now shows photos through the monitor's ICC profile,
matrix/TRC or LUT-based.

- codecs: DisplayProfile (moxcms): the display's primaries (colorant tags,
  or measured through the A2B tables of a LUT-only profile), the proxy ->
  device correction (per-channel curve tables for matrix/TRC profiles, a
  CMS transform for LUT profiles) and sRGB <-> display transforms.
- pipeline: DisplaySpace / RenderRequest::display: the per-pixel stage
  renders into the display's primaries and gamut maps into its gamut
  (CPU and GPU: only to_out / out_luma change). Soft proofing's display
  gamut warning uses the display's gamut.
- engine: RenderJob::with_display; display::present converts sRGB
  results (thumbnails, cached previews, embedded JPEGs). Histograms and
  the preview caches stay sRGB; exports are unchanged.
- ui: the loupe, Before, Compare, hover and second window render for the
  display; everything else is converted on the render workers. Settings >
  Display tab, ui.json setting, file picker opening in the system's ICC
  folder.

Tests: profile parsing and transforms (sRGB, P3, gamma 2.2, a synthetic
LUT-only profile, corrupted files), display renders equal 8-bit renders
into the matching output space, sampled histograms, and a headless UI test
of the whole flow. docs/display-profiles.md describes the design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEuxxPyG8t6KcADbZSEAUj
2026-10-09 11:21:09 -07:00
Alex ChambersandClaude Opus 5.5 2e817a45f4 LRC-DEV-PROCESS: process versions, so a look change can ship without changing existing edits (#146)
Each photo's develop settings record the rendering process they are
interpreted by (`DevelopSettings::process`). There is one process, V1, the
pipeline as it is; nothing renders differently (180 CPU 8/16-bit and GPU
renders of procedural scenes, and the saved settings JSON and hash64, are
byte-identical to main).

Settings saved without the field load as V1 (a field-level serde default),
new settings get the latest (`Default`), and V1 is left out when saving, so
files and preview-cache keys stay as they were. A damaged value (anything
but a whole number up to 2^32 - 1) reads as V1 too, so it never costs the
rest of the settings (an unparsable `lc:settings` sidecar is ignored). Reset moves a photo to the
latest process, section and slider resets don't; virtual copies inherit
it; copy / paste / sync / Auto Sync / presets don't carry it; a number from
a newer LightCraft is kept and renders with the newest known process. The
process is not an edit (Edited badges, import-look checks). The base tone
map is chosen per process in one place (`finish::base_tone`, shared by CPU
and GPU). Photo > Update to Current Process (`develop.updateProcess`) moves
selected photos on an older process to the latest as one undo step. Its
menu enablement follows the selection; a call that names photos (`ids` /
`id`) is validated on those instead, through a new opt-in
`CommandSpec::explicit_targets` (execute checks enablement before the
handler sees `ids`). `develop.get` reports the process. The rule and how to add V2:
docs/process-versions.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:03:45 +02:00
BuonBredaandClaude Opus 5.5 fa1565d855 LR-PROF-CAMERACOLOR: camera local tone mapping (ProfileGainTableMap) as a per-photo opt-in
Renders a raw's DNG ProfileGainTableMap (Apple ProRAW's local tone mapping)
when the photo's Profile option "Camera local tone mapping" is on
(`profile.camera_local_tone`, off by default: Lightroom Classic renders
ProRAW without it). Built on #427's parser; supersedes the decode-time
applier of the first version of #273.

- raw/gaintable: GainTableMap::gain / evaluator (GainEval) per DNG 1.7.1:
  bilinear between tables with edges replicated, input clamp(weights .
  (R,G,B,min,max))^gamma in linear ProPhoto after the baseline exposure,
  index input x MapPointsN clamped to the last point. Saturating cell
  math; maps whose sizes don't match their gains aren't evaluated.
  SourcePlacement: where a developed source sits in the active area.
- pipeline/local_tone: the gain per output pixel in the scene-linear
  stage, before the white balance, mapped back through the frame (crop,
  user orientation, warp) and the EXIF orientation. lin_needs_cpu routes
  the stage to the CPU when it's on (CPU and GPU match); lin_key includes
  it. SourceInfo::local_tone carries the map from the decoder.
- develop: Profile::camera_local_tone (omitted from JSON when off, so
  existing settings and their hashes are unchanged); Profile row checkbox
  when the source has a map; translations.
- RENDER_CACHE_VERSION 17. Docs: parity row and What's New as an opt-in.

Off renders pixel-identical to main on two iPhone 15 Pro ProRAWs (12 and
48 MP). On adds ~60-75 ms to the scene-linear stage at 1536x2048 on a
2-core VM, the same for both files (cost follows output pixels).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFqow2P1EtDxt6nRjjQoTA
2026-10-09 11:11:52 +02:00
Exalted5373andClaude Opus 5.5 0ac5cf4430 LR-EDIT-COLOR-TEMP: camera-space white balance for raws with a colour matrix
A custom white balance on a raw with a colour matrix (DNGs, and other raws
without a file-local look) re-develops the pixels for the chosen white
through the file's own colour model: lightcraft_raw::color::rebalance
re-evaluates the camera transform at the new white and maps the as-shot
development onto it, so a neutral lit by that white renders neutral and
saturated colours move as the camera's matrices say (DNG spec ch. 6, as
Lightroom does). The decoder passes the colour tags and the white the
pixels were developed for in SourceInfo::camera_color; other photos, and
relative white balances, keep the Bradford adaptation. The eyedropper and
Auto invert the same model (color::neutral_white), so a picked grey card
renders neutral.

The Tint sign is main's (#349): unchanged here. Render cache version 15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:54 -07:00
Exalted5373andClaude Opus 5.5 ed59d78e83 LR-MASK-SKY: AI masks use DNG semantic mattes (iPhone ProRAW sky matte)
Read DNG 1.6 semantic masks (SemanticName, SemanticInstanceID,
MaskSubArea; any coding the tile/strip reader decodes, e.g. ProRAW's
lossy-JPEG matte) with lightcraft_raw::semantic_masks, map them onto the
developed image, and carry Apple's sky / portrait / skin / hair / teeth /
glasses mattes in SourceInfo::mattes (SourceInfo is no longer Copy).
Sky, Subject, Background and People components sample them through
orientation, warp (Warp::frame, as geometry.rs) and crop like the image,
so preview = export, instead of the heuristics; files without mattes
render as before. Malformed mask IFDs are skipped, and only the first
MAX_MASKS mask IFDs are decoded, so a file full of them costs bounded
work. The render cache version is bumped once, in the white-balance commit.

The corpus test reads corpus/raw/dng-apple-iphone12pro-proraw.dng, the
name `cargo xtask corpus` downloads it under.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:54 -07:00
Nathaniel LedfordandClaude Sonnet 5.5 05618b2ad4 #323: windows on the GPU; Auto Mask strokes grow the window; dab references at f32
The GPU renderer renders windows (no more CPU fallback): the vignette gets the
window's offset and the whole output's size (VIG_VIEW), the airlight and noise
reduction scale come from the whole frame, and a window grown for spots or
strokes is cut back after the readback. equivalence.rs renders every settings
case as a window plus a corner vignette and a spot reading from outside, within
the existing 1 LSB. Auto Mask strokes join spots in growing the window (they read
the pixel under each dab). An Auto Mask dab's reference pixel is chosen from its
position at f32 precision, like the GPU kernel: an f64 rounding error that
depended on the window's start could put a boundary dab on either pixel.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 eebea486d5 #323: windows: spots render in a window; deep-zoom regression tests for the wide stages
Spot removal reads outside the spot and its automatic source depends on the
pixels at hand. A windowed render now fixes automatic sources from a fixed-size
render of the whole frame and grows the window it works on to hold every spot
that touches the requested one and the patch it reads (bounded at 8192 px),
cutting the result back to the request, so photos with spots get sharp windows
too. The wide stages (dehaze, clarity, highlights/shadows) are checked at depth
on hard-edged blocks: the cut kernels cost at most a few levels, so they are not
computed from a whole-frame proxy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 244088498d #323: windows: noise reduction and mask edge refinement follow the whole frame's size
An adversarial review found local::denoise and Refine Edges scaled with the
window's own size, so their strength changed with pan and zoom. Frame::output_long
gives the whole output's long edge to both; tests on a noisy source cover them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 b358022f14 #323: pipeline: render a window of the output (RenderRequest::window)
A zoomed view needs the pixels it shows at the zoom scale, not a whole-frame
texture stretched over them. RenderRequest gains an optional PixelWindow of the
virtual full output that max_w x max_h describes; only the window is
materialized. It is Frame:🪟 the crop rectangle narrows to the window
(flips as drawn), so sampling, masks, spots, red eye and the lens warp need no
change. The post-crop vignette keeps the whole frame's geometry (Frame::view)
and dehaze uses the whole frame's airlight (estimated on a 384 px render), so a
window matches the same pixels of the full render. The GPU renderer declines
windowed requests (CPU for now).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Claude 9b4ba1aa80 Fix #316: the section eye switches off Light, Color and Detail too
The pipeline honoured a section's eye only where it applies Effects,
Optics, Geometry and Calibration, so turning the eye off on Light, Color
or Detail changed nothing. DevelopSettings::effective() resets the
switched-off sections to their defaults (Light with the tone curve; Color
with white balance, mixers, grading and point colour; Detail), and both the
CPU and GPU renderers render those. Borrowed, so free, when every section
is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 18:29:29 +00:00
arata1972andCodex 43f4e24355 M11: separate Sony ARW colour and tone and preserve decoded source facts
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 07:16:15 +09:00
Brandon ThomasandClaude Opus 5.5 fc13807f91 Never crash: color, geom, raster, develop, scenes, preview, pipeline
- color: RgbSpace::to_xyz/from_xyz and bradford() no longer expect a
  matrix inverse. Degenerate (collinear) primaries, which readers reject
  before building a space from a file, leave the primaries unscaled /
  give the identity; the constant Bradford and OKLab matrices fall back
  to the identity, which can't happen (round trips are tested).
- pipeline geometry: sample() matches (mode, warp) instead of
  expect("warp"); a warp plan without a warp (sample_plan never makes
  one) samples through the equivalent affine.
- color, geom, raster, develop, scenes, preview and pipeline now #![deny]
  unwrap/expect/panic/unimplemented/todo/unreachable; clippy.toml as in
  the steering PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:34:53 -04:00
Brandon ThomasandClaude Opus 5.5 439cbd6ca0 LRC-DEV-SOFTPROOF, MENU-CTX-MASK: soft proofing with gamut warnings; mask component menu
Soft proofing (S in the loupe; S in grids stays Expand/Collapse Stack):
- pipeline Proof {space, destWarning, displayWarning}: gamut-map into the
  proof space, show it in the output space; red/blue gamut warnings
  (neutral clipping excluded). CPU only; the GPU path declines proofs.
- RenderJob::with_proof; paper-white surround and Proof Preview label;
  Soft Proofing strip under the histogram (profile, warnings, Create
  Proof Copy = a named virtual copy). view.softProof for control/MCP.
- photo.virtualCopy takes an optional name.

Mask components: MaskComponent.name; mask.component {component, action:
invert|duplicate|delete|rename|op}; right-click / … menu per component,
double-click rename, Intersect button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 04:01:01 -04:00
Brandon ThomasandClaude Opus 5.5 8d40b3e372 LR-REM-DUST: Find Dust Spots
lightcraft-pipeline dust.rs: sensor-dust detection on a rendered image — pixels darker than
their blurred surroundings by more than 3× the local texture, grouped into small (0.15–2 % of
the long edge), roughly round blobs. spot.findDust {sensitivity?, add?} renders the uncropped
photo, detects and adds a heal spot on each (auto source; one undo step). Remove panel ▸
Find Dust Spots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 03:07:49 -04:00
Brandon ThomasandClaude Opus 5.5 26b00014a8 LR-LIB-CULL: assisted culling — focus scores, bursts, best-of-burst
lightcraft-pipeline cull.rs: focus (Laplacian energy of the sharpest 16-px blocks at 512 px,
0–100, calibrated on the CC0 corpus), clipping share and an 8×8 look-alike signature.
photo.analyze scores the selection (else the view), groups look-alike shots taken within 10 s
into bursts and marks each burst's sharpest; optionally rejects photos below a focus score and
picks each burst's best (one undo step). Scores live on the photo (Analysis, Op::SetAnalysis):
rule fields Focus and Best of Similar Shots, Info panel line. Photo ▸ Assisted Culling….

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:58:52 -04:00
Brandon ThomasandClaude Opus 5.5 4a490db261 LR-PROF-IMPORT: .cube 3D LUTs as creative profiles
lightcraft-pipeline gains lut.rs: a .cube parser (LUT_3D_SIZE, DOMAIN_MIN/MAX, TITLE), a
trilinear lookup and an in-process registry; the finishing stage applies a `lut:` profile to
the display-encoded colour, blended by the profile amount (0–200 %); LUT profiles render on the
CPU. profile.import {paths} reads files, folders and zips, copies each LUT into the library's
Profiles/ folder, lists them in prefs and registers them when the library opens; the profile
browser shows them under their folder / zip name; profile.deleteImported. File ▸ Import
Profiles & Presets…, the file picker and drag & drop take .cube files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:20:39 -04:00
Brandon ThomasandClaude Opus 5.5 f65c2310f2 LR-MASK-COLORRANGE: pick colour-range samples on the photo
The Color mask tile adds an empty colour range and arms the picker: click the photo to sample
(⇧-click adds, up to five); Pick in the Masking panel re-arms it. mask.sampleColor {x, y, add?}
samples via lightcraft_pipeline::color_range_sample, which reads the exposed scene colour at
that point in the same OkLab space the mask compares in (so a click selects what was clicked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:03:14 -04:00
Brandon ThomasandClaude Opus 5.5 3ce86032ff LR-EXP-TYPE + LR-EXP-DIM: export size modes, ppi, Original + XMP and DNG export
- Resize modes: long edge, short edge, width, height, W x H (either orientation), megapixels,
  percent; "don't enlarge" (default on); ppi written to JFIF density / PNG pHYs / TIFF tags.
- Full-size export of a cropped photo now renders the crop at native resolution (it was
  upscaled to the uncropped long edge).
- Formats: Original (file copied byte for byte + XMP sidecar with the edits, named after the
  output) and DNG (raw photos re-encoded losslessly, edits in the embedded XMP).
- Export dialog: Resize-to dropdown, values, Don't enlarge, Resolution; DNG/Original hide
  pixel options.
- lightcraft-cli render goes through the shared exporter (colour space, metadata, ppi) and
  takes any app.export option via --opt KEY=VALUE; MCP export schema lists the new params.
- xtask resolves the workspace root at run time (a moved checkout broke ci).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:12:43 -04:00
Brandon ThomasandClaude Opus 5.5 4075b42d85 Merge main into the masking branch (output space / deep finish, presets, parity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:23:58 -04:00
Brandon Thomas a8ce1d8f13 Merge branch 'worktree-agent-a7cd35d1e288340c6'
# Conflicts:
#	docs/parity.md
2026-10-01 13:18:10 -04:00
Brandon ThomasandClaude Opus 5.5 ff3df3d2c4 Merge main into LR-EXP-COLORSPACE / LR-EXP-BITDEPTH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:11:12 -04:00
Brandon ThomasandClaude Opus 5.5 8c1c7282b2 LR-MASK-BRUSH + LR-MASK-SLIDERS: Auto Mask brush and local Noise / Moiré / Defringe on CPU and GPU
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>
2026-10-01 13:09:14 -04:00
Brandon ThomasandClaude Opus 5.5 03291d6f83 LR-EXP-BITDEPTH: 16-bit PNG/TIFF, 32-bit float linear TIFF, 10-bit AVIF
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>
2026-10-01 13:07:02 -04:00
Brandon Thomas 3c644f7faa Merge branch 'main' into worktree-agent-aecd35c3421bad4ae
# Conflicts:
#	crates/ui-egui/src/render.rs
2026-10-01 13:01:41 -04:00
Brandon ThomasandClaude Opus 5.5 ca8db6e028 LR-MASK-OVERLAY + LR-MASK-PINS: rendered mask overlay (5 views, colour/opacity, O / Shift+O) and draggable pins per component
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>
2026-10-01 12:58:27 -04:00
Brandon ThomasandClaude Opus 5.5 7f40a7a7d9 LR-EXP-COLORSPACE: export in sRGB, Display P3, Adobe RGB (1998) compatible, ProPhoto RGB or Rec. 2020
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>
2026-10-01 12:49:13 -04:00
Brandon Thomas e1b2055897 Merge branch 'main' into worktree-agent-aecd35c3421bad4ae
# Conflicts:
#	crates/ui-egui/src/panels/edit.rs
#	docs/parity.md
2026-10-01 11:47:29 -04:00
Brandon ThomasandClaude Opus 5.5 d6431d0b8a Merge main into M5.6 (memory) branch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:36:32 -04:00
Brandon ThomasandClaude Opus 5.5 9c4526129e LR-PROF-CREATIVE: 16 own creative profiles (Film, Cinematic, Muted, B&W) as amount-scaled settings deltas
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:16:19 -04:00
Brandon ThomasandClaude Opus 5.5 64dfa8adbd Merge edit-panel parity (Point Color, Calibration, curve refine saturation + targeted adjustment, red/pet eye, visualize spots)
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>
2026-10-01 11:02:11 -04:00
Brandon ThomasandClaude Opus 5.5 ebcc41825b LR-EYE-RED: red eye correction with automatic pupil detection
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>
2026-10-01 10:37:02 -04:00
Brandon ThomasandClaude Opus 5.5 78e50b5fc5 M5.6a: memory report (library.memory, ui.inspect memory) and opt-in DHAT heap profile
Per-cache bytes: decoded thumbnail/preview/full sources, rendered previews,
GPU device buffers (allocated/pooled/retired), loupe stage caches (CPU and
GPU) and textures. lightcraft-cli --features dhat-heap adds live/peak heap and
writes dhat-heap.json.

Baseline (14 raws, loupe, 12 steps): peak RSS 1.9-2.3 GB, footprint 2.5-2.6 GB;
heap peak 634 MB after import (probes decode every raw), 930 MB during grid
thumbnails (parallel decodes); GPU free pool grows to 2 GB (exact-size reuse).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:28:13 -04:00
Brandon ThomasandClaude Opus 5.5 490fe943cd LR-EDIT-COLOR-POINTCOLOR: Point Color — sampled colours with shift, variance, range and visualize
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>
2026-10-01 10:18:28 -04:00
Brandon ThomasandClaude Opus 5.5 ec6637f238 M5.4b: faster raw path — bin previews/thumbnails straight from the mosaic
- lightcraft-raw: RawImage::develop_binned(k) — k×k superpixel binning of the CFA
  (Bayer k even, X-Trans k multiple of 3), black/white normalised row by row, default
  crop only, clipping preserved for highlight reconstruction. 2560 px previews of a
  24 MP raw bin 2×, thumbnails 4–8×; exports / 1:1 still demosaic (AHD) at full size.
- highlight::reconstruct and its pyramid fill are rayon-parallel (bit-exact).
- X-Trans directional demosaic: precomputed per-phase neighbour offsets and no
  reflection in the interior, no per-pixel allocation (bit-exact, tested against the
  reference formulation): 2.9 s → 0.75 s CPU at 24 MP.
- files::load_bytes runs on a rayon worker so its short parallel loops don't each
  wait for the pool to wake (dominant cost on a loaded machine); LIGHTCRAFT_PROFILE
  prints per-stage source timings; files::bin_factor picks the block size.
- render_bench ONLY=source: per-file decode / normalise / demosaic / binning / load.
- Fix: u64 overflow on hostile IFD sizes in DNG/ARW/NEF raw-IFD selection (proptest).

load_bytes(2560) CPU time, ARW a7 III 24 MP: 2347 → 240 ms; X-T20: 3120 → 1220 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:01:53 -04:00