Commit Graph
1561 Commits
Author SHA1 Message Date
Atmik Shetty 3b32f82e9e LRC-PRINT-LAYOUTSTYLE: print.* commands for the layout styles, cells, templates and PDF
The Print module's state lives in the session (Session::print), so the app,
CLI, control channel and MCP share one print: layout style (Single Image,
Contact Sheet, Picture Package, Custom Package), page setup, grid, image
settings, page background, guides and job. 18 commands, none journaled
(nothing here touches the catalog):

  print.get / set / reset      set merges a partial patch, validates the whole
                               result and changes nothing on error; a new
                               paper or margin pulls cells back inside
  print.cell.add|move|resize|delete|stack|photo|page
  print.layout.autoLayout|newPage|clear
  print.exportPdf {path, ids?} composes and writes the PDF with an activity
                               row, refuses to replace an original
  print.template.list|save|apply|delete

Templates keep layout, image settings, page and job (not the photos assigned
to cells, not the guides) and are saved in the library's prefs.json next to
the export presets. They are stored as JSON and checked when applied, so a
template from another version or a hostile one is an error on use and never
makes the preferences file unreadable.

Tests drive every command through the dispatcher (hostile numbers, unknown
keys, caps, persistence across reopening a library) and a CLI end-to-end test
chains set / add cells / Auto Layout / PDF in one run.
2026-10-12 14:35:03 +05:30
Atmik Shetty 8336f06d28 LRC-PRINT-IMAGESETTINGS: compose print pages (fit / Zoom to Fill, Rotate to Fit, border, stroke, background)
print/compose.rs turns a layout and its photos into PageRasters: 16-bit
interleaved RGB encoded in Rec. 2020 at the job's pixel density. Each cell's
photo is rendered with the export pipeline at exactly the size the cell needs,
then fitted (whole photo, centred) or zoomed to fill (centre crop), turned a
quarter clockwise when Rotate to Fit makes it print larger (or crop less),
framed by the Photo Border (package styles only) and finished with the Inner
Stroke drawn inside the photo edge. The page background, border and stroke
colours are given in sRGB and converted to the page encoding.

Photos go to cells by layout style: Single Image and Contact Sheet paginate
the selection, Picture Package repeats the first photo, Custom Package prints
each cell's own photo. The job is prepared on the session and run off it with
progress and cancellation, like the contact sheet; page and job pixel counts
and page counts are capped so a hostile resolution or paper cannot exhaust
memory.

print/output.rs holds the interim PDF writer (Rec. 2020 to 8-bit sRGB JPEG
pages sized to the paper in points) through the contact sheet's Pdf, now
pub(crate). PrintJob carries the print resolution (72 to 720 ppi, 240 default).

Tests draw on synthetic images for exact pixels (borders, strokes, crop,
rotation direction, resampling) and on the demo library for whole pages.
2026-10-12 14:28:40 +05:30
Atmik Shetty 4f1ed6df65 LRC-PRINT-LAYOUT: the print layout model (paper, margins, grid, package cells, packer, snapping)
crates/engine/src/print/layout.rs is the pure page model the Print module
builds on: ten named papers plus custom, orientation, margins and the
printable area; Single Image, Contact Sheet (grid with spacing and square
cells), Picture Package and Custom Package styles; package cells with a
stacking order (send to back / backward / forward / bring to front); a
deterministic MaxRects packer behind Auto Layout (largest first, earlier
pages filled before a new one opens, a cell turned a quarter only when it
fits nowhere as given); snap-to-cells and snap-to-grid helpers; ruler units
and guide options.

All lengths are inches in page space, after orientation. Every number from
outside is checked (NaN, infinity, negative, oversized, cell and page caps)
and Layout::validate guards saved layouts.

No command or UI reaches it yet, so the parity rows keep their status.
2026-10-12 14:21:42 +05:30
GabrielePecchioliandClaude Opus 5.5 aec225b6ac Add Italian interface translation (#1192)
Italian (`it`) catalogs: 1,992 interface messages and all 246 format strings
(2,201 of 2,398 messages; What's New left in English like most languages).
410 labels reuse the PhotoCraft, VectorCraft, PdfCraft and WordCraft Italian
catalogs where the meaning fits a photo app; 46 were retranslated.

Counts follow a label ("Foto importate: 3") because Italian adjectives agree
with the number; the English plural suffix is consumed with {:.0}.

Language table entry, menu command, three tests mirroring Ukrainian's, the
button-fit test extended to Italian, and the Settings language buttons now
wrap so an eleventh language doesn't widen the dialog past a short window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:45:26 +02:00
Alex ChambersandClaude Opus 5.5 f1c023ed3d LR-META-XMP: Read Metadata from File doesn't auto-write the sidecar it just read (#934)
With XMP auto-write on, photo.readMetadataFromFile committed what it read
and the command handler then wrote the sidecar back, adding lc:settings to
a sidecar that had only another app's crs: settings. Later reads prefer
lc:settings, so LRTimelapse's next crs: edits were ignored. The read now
sets the existing one-command skip_auto_write switch (as photo.removeRegion
does); edits made afterwards are still auto-written.

Tests: a read leaves the sidecar byte for byte and a later develop edit is
still written; the issue's crs:Exposure2012 0.5 -> 1.2 round trip is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:24:29 +02:00
LosminekandClaude Opus 5.5 358a19c755 i18n: add Czech (cs) interface language
Add the Czech catalog (2,056 UI messages, 246 format templates), the
language-menu entry, locale detection (cs, cs-CZ, cs_CZ.UTF-8) and
regression tests for switching, persistence, dates, placeholders and
glyphs. Translate a few face/model, person and HDR/PDF captions through
the existing tr() interface. Settings > Language now wraps its buttons,
since twelve language names no longer fit a narrow window.

What's New prose stays English, like most catalogs. AI-assisted, with
an independent review pass; not yet proofread by a native speaker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:32:45 +02:00
Fabio LussiandClaude Opus 5.5 29ed703794 docs: drop the docs workflows until the checker is on artcraft-services main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:09:17 +02:00
Fabio LussiandClaude Opus 5.5 7fe040f66a docs: pin the docs checker to a commit and its SHA-256
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:13:41 +02:00
Atmik Shetty 2d8fc776b2 LR-EDIT-COLOR-GRADING: Cmd/Ctrl-drag keeps the saturation, Alt/Option fine-adjusts the colour wheels (#1160)
The wheels had Shift (keep the hue, change saturation). Lightroom's other
two modifiers are now there too:

- Cmd/Ctrl-drag keeps the puck's saturation and changes only the hue, so it
  moves round its circle; the circle is drawn as the guide, as Shift draws
  the spoke. Shift wins when both are down (keeping both would freeze the
  puck).
- Alt/Option-drag fine-adjusts: the puck moves a tenth as far as the pointer
  from where it was grabbed (the slider's fine-adjust factor), counted from
  the press so no travel is lost to egui's drag threshold. It combines with
  Shift and Cmd/Ctrl.
- Pressing or releasing a modifier mid-drag re-grabs the puck as it is at
  that moment (WheelGrab); wheel_pick takes a WheelLock (Hue / Sat) and
  returns finite values for any input.
- A puck at the centre has no hue, so a Shift-drag from there now takes the
  angle where the pointer pressed, not where egui first reported a drag.
- The wheel has a hover tooltip naming the modifiers.

Shift-drag already kept the puck's own hue whenever its saturation was 1 or
more; shift_drag_keeps_the_pucks_hue_wherever_the_pointer_grabs pins that.

Tests (tests_grading.rs) drive the wheel through ui.dragWidget with each
modifier and check the develop values and one undo step per drag, plus unit
tests of wheel_pick (lock, clamp, centre, NaN/inf) and WheelGrab.
2026-10-12 10:31:26 +05:30
树洋 陈 537789cf1d Merge remote-tracking branch 'origin/main' into feature/show-all-mask-overlays
# Conflicts:
#	docs/parity-checklist.md
2026-10-12 12:20:21 +08:00
树洋 陈 6fd740ac2d Address maintainer review: free shortcut, derived tooltip, fmt, locales
- view.maskOverlayAll: Cmd+O clashed with library.open; rebind to Cmd+Shift+O
- Masking panel tooltip now derives the key label from the binding (platform
  aware: Cmd vs Ctrl) instead of hardcoding Ctrl+O; base string localized
- gpu/render.rs: rewrite the AllMasks alpha gather as a for-loop so it passes
  cargo fmt --check (max_width 150)
- docs/parity-checklist.md: add LR-MASK-OVERLAYALL row
- locales: add 'Show All Mask Overlays' and 'Show all overlays' to de, es, fr,
  ja, pt-br, ru, uk, zh-hans, zh-hant
2026-10-12 11:44:03 +08:00
Fabio LussiandClaude Opus 5.5 d3690e76c4 docs(user): apply final review fixes
Part 1: verified = latest release tag policy in docs/user/AGENTS.md; released-pages sentence in root AGENTS.md. Part 6: blank line in install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:06:01 +02:00
Fabio LussiandClaude Opus 5.5 675e661710 docs(user): apply final review fixes
Items B-LightCraft 1-5 and consistency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:49:21 +02:00
ChippedWandClaude Opus 5.5 17aafcd5bf LR-EXP-PSD: Photo ▸ Edit In ▸ Open Original in Other Editor (RAW, no copy)
Hands each selected photo's original file itself (a raw stays a raw: no
render, no copy, nothing added to the library) to the editor set in
Settings ▸ External Editing ▸ Other editor, e.g. DxO PhotoLab. Also on a
photo's right-click menu (Edit In).

- `photo.openOriginalInExternal {ids?, app?}` (UI command, menus.rs): up to
  50 photos; demo photos and missing files are skipped with the reason; when
  none has a file it fails saying why (toast + error, no panic). Enabled when
  the active photo has a file and the host can start programs (not the web).
- Several photos go to one start of the editor (`Services::open_all_with`,
  editors.rs `open_all_with`): a single-window editor such as DxO PhotoLab
  drops the files that arrive while it is still starting when started once
  per file. The system default ("" editor) still opens them one by one.
- Settings hint, translations of the two new toasts, parity rows
  LR-EXP-PSD / MENU-FILE-PHOTOSHOP.

Tested by hand on Windows with DxO PhotoLab 8 (one and several raws).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:31:34 +02:00
Fabio LussiandClaude Opus 5.5 436c56a7ad docs: add user docs for LightCraft
User docs in docs/user/ (published at getartcraft.com/docs/lightcraft),
the docs-check and docs-publish workflows, and a "User docs" rule in AGENTS.md.
Modelled on the ArtCraft Launcher setup.

Sources per page:
- index.md: README.md, crafting-apps.ts (lightcraft entry), Cargo.toml (0.6.0)
- getting-started/install.md: README Downloads section, `gh release view` v0.6.0 asset list, craft-launcher catalog.rs and install docs
- guides/import-photos.md: crates/ui-egui/src/import.rs, panels/dialogs.rs, menubar.rs, menus.rs
- guides/cull-photos.md: docs/library-shortcuts.md, menubar.rs, panels/compare.rs, README
- guides/edit-a-photo.md: panels/edit.rs, develop/src/controls.rs, menus.rs, README
- guides/export-photos.md: panels/dialogs.rs (Export dialog), menus.rs
- guides/import-lightroom-catalog.md: docs/lightroom-catalog-import.md, menus.rs, issue #832
- troubleshooting/preview-only-raw.md: issue #662, widgets.rs, engine cmd/convert.rs, README
- troubleshooting/raw-photos-look-dull.md: issues #860, #778, #697
- troubleshooting/get-help.md: README (logs), menus.rs Help menu, issue #860
- faq/free-and-private.md: README (local-first, license), ROADMAP link
- faq/supported-files.md: README, menus.rs, export dialog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:06:24 +02:00
dev-null-undefinedandClaude 536d6b88ac LR-VIEW-COMPARE: Compare and Reference panes say "Rendering…" as the loupe does
Compare and Reference View are where focus is checked at 1:1, and their panes show the same
stand-ins as the loupe: a thumbnail while the photo renders, the last render after an
edit, the magnified frame under a window not in yet. Each pane now shows the loupe's
"Rendering…" pill once that has lasted half a second with nothing new (widget
`compare:rendering:<pane>`), and its failed pill ("This photo can't be opened", or
"Original offline · no smart preview", the reason on hover) when the render failed behind
a thumbnail (`compare:renderFailed:<pane>`). Survey's small fit tiles don't.

Compare panes have no draft renders, so a slider drag on the photo would leave its pane
saying "Rendering…" for the whole drag: the photo being dragged doesn't count until the
drag ends. Each pane's wait belongs to the photo in it, so one that moves in (swap, Make
Select) starts its own. A retry of a failed render keeps the failed pill, as in the loupe.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-12 00:01:37 +02:00
dev-null-undefinedandClaude 31c31b726b LR-VIEW-DETAIL: the loupe says when the render behind a stand-in fails
A photo whose render failed while the loupe showed its embedded JPEG or a thumbnail kept
that stand-in up as if it were the photo; "This photo can't be opened" only showed when
nothing at all could be drawn. Now a caution pill at the "Rendering…" spot says so:
"Original offline · no smart preview" when that is why, else "This photo can't be opened",
with the reason on hover and for screen readers (widget `loupe:renderFailed`). Agents see
it as `ui.inspect` → `loupe.failed`, so one waiting for a finished loupe knows when there
won't be one.

A library change retries failed renders; while the retry of the same key runs, the pill
stays and "Rendering…" doesn't take over. The renderer remembers which photo a failure
belongs to (`failure_of`): the centre message no longer shows the previous photo's error
for a moment after a step. Tests share one render offload (`headless::RenderGate`) that
holds renders and can fail chosen slots.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-12 00:01:22 +02:00
Chad ElliottandClaude Opus 5.5 d79dad9ab1 docs(gaps): #1136 gap 26 notes indexed stack and album lookups at 100k
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:48:16 -05:00
Chad ElliottandClaude Opus 5.5 dbf2dcbfbd docs: #1037 visible skips and the ci report
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:33:54 -05:00
Chad ElliottandClaude Opus 5.5 929202b95f docs: #1046 document the cancelled flags and that the UI decides on error kinds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:26:04 -05:00
dev-null-undefinedandClaude 2df78c9e58 LR-BEHAV-BG-TASKS: a render job that panics fails instead of never answering
A panic inside RenderJob::run or QuickJob::run (a raw decoder or pipeline bug) unwound the
job pool's worker without sending a result. The slot stayed pending and its key was never
asked for again, so the loupe or the thumbnail rendered forever; and the pool never
replaces a dead worker, so after as many such panics as it has workers (2–6) no photo
rendered until LightCraft was restarted.

Both now catch the panic (guard::catch) and return it as the job's failure, answering the
request it came from, so the renderer records it like any failed render: no retry until
the catalog changes, and the workers live on. run() is the one place every render goes
through (the pool, export, Build Previews, contact sheets, face and AI-mask workers);
callers that had their own guard keep working, and the watermark preview's, now
redundant, is gone. The web build aborts on panic, so nothing changes there.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-11 23:23:50 +02:00
lloydandClaude Opus 5.5 090553d14d LR-MERGE-HDR: merges of raws without colour matrices keep their colour (#965)
Photo Merge decoded each raw itself and wrote the reference frame's own colour
tags into the merged DNG. A CR2 (and NEF, ARW and the other raws whose colour is
fitted from the camera JPEG) has none, so an HDR merge of CR2s rendered with no
camera colour: green and about 16 L* dark.

`lightcraft_merge::load_frame_with` takes the raw's colour tags from the caller,
and the engine passes `starting_colour_tags`: the colour the raw opens with
(`starting_colour`) as DNG tags. That is one forward matrix with a colour matrix
and AsShotNeutral that agree with it at the as-shot white, the look's hue/sat
table as ProfileHueSatMap and its tone curve as ProfileToneCurve. Raws with
their own matrices (DNG) keep their tags. The look's chroma curve has no DNG
tag and is left out.

On two CC0 CR2s (EOS 550D, 40D) an HDR merge of three copies against the photo
alone: mean CIE76 dE 26.15 -> 1.07 and 22.89 -> 0.47.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:42:18 +02:00
Fabio LussiandClaude Opus 5.5 12498b3acf CLI --import <folder> uses the engine's import extensions (HEIC, HEIF, NRW)
expand_paths kept its own PHOTO_EXTENSIONS list, which had fallen behind
lightcraft_engine::import::EXTENSIONS. It now filters with
import::is_supported, so folder walks and library.import take the same files.

Fixes #1080

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:13:57 +02:00
Andrew FrahnandClaude Opus 5.5 8fbc53584f L10N: fill the de, es, fr, ja and uk catalogs (gap 21)
Every message another catalog knows is now in the German, Spanish, French,
Japanese and Ukrainian catalogs (plain and formats): 927 plain and 34 format
messages added or translated, following each catalog's existing
terminology, register and punctuation; placeholders unchanged. Menu paths
inside translated text use → (the UI font has no ▸). Values identical to
English are kept only where correct (ISO, Display P3, words spelled the same,
language self-names, placeholder-only formats).

Measured: de 97%, es 98%, fr 97%, ja 99%, uk 99% of 2,398 catalog messages.
pt-BR, ru, zh-hans and zh-hant are left to their open PRs. Agent-made
translations: native-speaker review welcome.

Also fixes the Rust 1.99 clippy lint chunks_exact_to_as_chunks in
pipeline/spots.rs that fails `cargo xtask ci` on main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 06:45:46 +11:00
Andrew FrahnandClaude Opus 5.5 b7d77f16b8 LR-IMP-CAMERA-COVERAGE: verify 17 uncompressed Olympus and Samsung models on CC0 files
One CC0 raw.pixls.us file per uncompressed Olympus ORF and Samsung SRW
body not yet pinned (23 files, 17 Adobe models) is added to RAW_SAMPLES
and pinned in crates/raw/tests/olympus_samsung_corpus.rs: own sensor
hash plus a black-box check against the camera's embedded JPEG (the
E-10 pinned only, inspected by eye).

Also: as_chunks in spots.rs for the chunks_exact_to_as_chunks lint.

Docs: evidence.tsv + coverage re-measure (252 verified), gap 10,
checklist, raw-parity, ROADMAP. Headline unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 06:38:36 +11:00
Andrew FrahnandClaude Opus 5.5 9181aa0b6c LR-IMP-FORMATS: import and decode Mamiya MEF (uncompressed 12-bit; WB from the RGB preview)
Sources: TIFF 6.0 and own black-box analysis of the CC0 ZD sample.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 06:27:05 +11:00
Andrew FrahnandClaude Opus 5.5 f63522a234 LR-IMP-CAMERA-COVERAGE: verify 21 Sony and Leica models on CC0 files
One CC0 raw.pixls.us file per Sony and Leica body that no earlier bulk
check covered (22 files, 21 Adobe models) is added to RAW_SAMPLES and
pinned in crates/raw/tests/sony_leica_corpus.rs: own sensor hash plus a
black-box check against the camera's embedded JPEG where it has a colour
one (five files pinned only, inspected by eye).

Also: as_chunks in spots.rs for the chunks_exact_to_as_chunks lint.

Docs: evidence.tsv + coverage re-measure (256 verified), gap 10,
checklist, raw-parity, ROADMAP. Headline unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 06:05:11 +11:00
Andrew FrahnandClaude Opus 5.5 b538b23d40 Gap 27: CPU-GPU equivalence verified on an AMD Radeon 8060S (Vulkan, Linux)
First non-Apple GPU verification: 16 tests / 135 cases pass, 124 at
max 1 LSB, 10 bit-identical, one at max 2 LSB (0.0002% of pixels), worst
mean 0.0003 LSB. Recorded in gpu-pipeline.md, hardware-parity.md and
gaps.md. Also as_chunks in spots.rs for the newer clippy lint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 05:52:43 +11:00
Andrew FrahnandClaude Opus 5.5 a6d048be8d LR-IMP-FORMATS: decode Epson ERF (12-bit 16-byte blocks; WB from the preview's neutrals)
Sources: TIFF 6.0 and own black-box analysis of the three CC0 samples;
white-balance estimate from the embedded preview: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 05:43:30 +11:00
Andrew FrahnandClaude Opus 5.5 bbd2e8cbd4 LR-IMP-CAMERA-COVERAGE: verify 97 Canon CR2, Pentax and Ricoh models on CC0 files
One CC0 raw.pixls.us file per Canon CR2, Pentax and Ricoh body that
decodes (100 files, 97 Adobe models) is added to RAW_SAMPLES and pinned
in crates/raw/tests/canon_pentax_corpus.rs: own sensor hash plus a
black-box check that the decoded green follows the camera's embedded
JPEG (r 0.69-1.00; four files pinned only, inspected by eye).

Also: as_chunks in spots.rs for the chunks_exact_to_as_chunks lint.

Docs: evidence.tsv + coverage re-measure (332 verified, 50% on verified
bodies), gap 10, checklist, raw-parity, ROADMAP. Headline unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 05:27:26 +11:00
Bobby JoseandClaude 73098bf5a3 LR-IMP-FORMATS: EOS R6 Mark III as-shot white balance (ColorData version 66)
Version 66 keeps version 64's as-shot white balance and black level
offsets; its word 661 is below where the sensor clips, so the white
level stays measured from the data. Checked on 33 R6 Mark III files
from 12 shoots: all now open with their as-shot white balance, black
levels match the masked columns.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JKdR4oZ3jRSPPwMLLTa2U
2026-10-11 14:13:06 -04:00
Andrew FrahnandClaude Opus 5.5 2c3d7a743f Control channel: give up a request the app draws no frames for, never run it late (#718)
On Wayland a window on a hidden workspace or minimized gets no frame
callbacks, so queued control requests waited 60 s and then ran when the
window came back, doubling retried work. The server now abandons a request
the app hasn't started after 5 s without a frame, says why, and the app
skips abandoned requests. Running requests keep the full 60 s. Also
as_chunks in spots.rs for the newer clippy lint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 05:13:02 +11:00
OneSlipsandClaude Opus 5.5 bb925f6d2e LR-MASK-EFFECTS: Effect menu, New Effect Preset dialog and Manage window
The Effect menu above the mask sliders is grouped (★ Favorites first, then
groups in menu order), searchable, scrolls, works with ↑ ↓ Return, marks the
matching effect (else Custom) and previews an effect on the photo while the
pointer rests on it. Right-click: apply to all masks or to the same-named mask
on the selected photos, update, rename, duplicate, star, move to group, delete
(asks first). Reset Sliders; Update "<last effect>" with Current Settings.

New Effect Preset dialog: name, group, which sliders (All / None / Changed
Only), Amount, brush size/feather/flow. Manage Effect Presets window: search,
inline rename, update with mask, duplicate, stars, groups, export, delete with
confirmation. Names padded with runs of spaces (as LrC users do) are shown
collapsed. Import toasts name the groups and the settings kept but not applied.
New messages translated in every catalog. Docs: parity row and xmp-interop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:57:07 +10:00
robmdunnandClaude Opus 5.5 6178021107 LR-IMP-CAMERA-COVERAGE: measured image area for Nikon bodies without CropArea
Nikon bodies from before ~2016 record no CropArea, so the decoder kept
their whole sensor data, less trailing dark columns. Where that data
holds masked lines above black or optical black, they were developed as
a coloured or dark bar on that edge: the D7000 and D5100's 36 columns at
~600 (#976), the D800's 14 (#914, the top in portrait), and others.

MEASURED_BODIES (crates/raw/src/vendor/nef.rs) now gives 36 model /
data size / depth groups the frame of their camera JPEG inside the data:
D7000, D5100, D800, D800E, Df, the D4's 12-bit mode, D3, D3S, D700,
D300, D300S, D600, D610, D810, D4S, D5600, Coolpix A, D3000, D40X, D60,
D80 and D2Xs. An entry matches model, data size and depth exactly; the
measured white level becomes optional (only the D4's 14-bit mode has
one). Bodies whose data holds only picture beyond the camera frame get
no entry and render as before.

`cargo xtask raw-frame FILE...` (crates/engine/examples/raw_frame.rs)
measures a body: it aligns each raw's embedded camera JPEG with the
sensor data (half-size luminance, gradient correlation, sub-pixel peak;
weak, ambiguous or between-pixel peaks are flagged), counts the edge
lines that hold no picture and reports what the decoder's crop keeps
beyond the camera frame. It prints entries only with evidence: at least
3 clean files agreeing exactly, of the model (12- and 14-bit pooled) or,
with at least one of its own, with sibling models of the same readout
(data and JPEG size) that agree on the same frame. On the D7500, which
records its CropArea, it finds that area exactly; it declines the Z f,
whose JPEG is distortion-corrected.
It works for any make whose raw embeds a full-size camera JPEG: on
Sony, Canon CR2 / CR3, Pentax and Samsung files it finds the decoder's
own crop (relative to the active area).

docs/nikon-bodies.md surveys every Nikon raw on raw.pixls.us (98
models, 291 files; 138 CC0 samples measured): 22 models record
CropArea; of the 88 groups without it, 36 have entries, 13 are fine, 25
could not be aligned and 7 do not decode. The D3200, D3100, D200, D50,
D750 and Nikon 1 V1 / V2 are pending, each listed with the files it
needs; the doc also describes how to add a body. raw-parity.md no
longer claims every NEF coding but HE decodes.
measured_bodies_are_consistent checks every entry lies inside its data
and is listed once.

The evidence is committed in docs/nikon-bodies/: headers.json (every
file's raw size, depth and maker-note crop tags), samples.json (the 138
measured CC0 files with URL and sha256) and frames.jsonl (the tool's
per-file results). `cargo xtask corpus --download nikon` fetches the
samples (sha256 checked) so `cargo xtask raw-frame` reproduces every
count.

Fixes #976
Fixes #914

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:56:42 -04:00
Andrew FrahnandClaude Opus 5.5 8695c3391f LR-PREF-DISPLAY: on Linux, previews use the display profile from colord
While no monitor profile is chosen, the Linux app reads the profile the
desktop assigns to the display in colord (org.freedesktop.ColorManager,
system bus, via zbus) on a background thread every 3 s, and shows
previews through it. With several displays it is used only when they all
share one profile; without colord the view stays sRGB. Windows' WCS
needs FFI and stays manual. Also satisfies clippy 1.99's
chunks_exact_to_as_chunks in spots.rs.

Sources: the colord D-Bus API documentation; own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:42:12 +11:00
Andrew FrahnandClaude Opus 5.5 a1d7c488f8 LR-EXP-RENDERDNG: Render to DNG export format (linear float DNG of the rendered photo)
New export format `rendereddng` (export dialog: Format DNG, "DNG of: Rendered
photo"; `app.export`; MCP export tool): the rendered photo, with the edit,
crop, size, output sharpening and watermark baked in, written by the new
`lightcraft_raw::write_rendered_dng` as a LinearRaw DNG of 16-bit float Deflate
tiles in the export colour space. ColorMatrix1 (XYZ -> space), ForwardMatrix1
(space -> XYZ D50), a neutral AsShotNeutral and an identity ProfileToneCurve
make raw converters show it as rendered. HDR edits keep highlights above white.
Bad input (wrong sample count, zero or huge sizes, NaN/inf, out of half-float
range) gives an error or is clamped, never a panic.

Also fixes the Rust 1.99 clippy lint chunks_exact_to_as_chunks in
pipeline/spots.rs that fails `cargo xtask ci` on main.

Sources: Adobe DNG Specification 1.4+ (LinearRaw, ColorMatrix/ForwardMatrix,
ProfileToneCurve); own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:33:12 +11:00
Andrew FrahnandClaude Opus 5.5 a44a4baa31 LR-IMP-CAMERA-COVERAGE: verify 117 Nikon and Fujifilm bodies on CC0 files
One CC0 raw.pixls.us file per Nikon and Fujifilm body that decodes is
added to RAW_SAMPLES and pinned in crates/raw/tests/body_corpus.rs: own
sensor hash plus a black-box check that the decoded green follows the
camera's embedded JPEG (r 0.77-1.00).

The sweep found COOLPIX 5700 / 8400 / 8800 decoding wrongly (two stacked
half-height fields, CMYG read as RGB): recorded as unsupported. #702 is
fixed, so the A100 evidence row is preview, not unsupported.

Also: as_chunks in spots.rs for the chunks_exact_to_as_chunks lint.

Docs: evidence.tsv + coverage re-measure (352 verified, 86% use-weighted,
58% on verified bodies), gap 10, checklist, raw-parity, target-app-parity,
ROADMAP. Headline numbers unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:28:43 +11:00
Andrew FrahnandClaude Opus 5.5 c3be1ceead LR-BEHAV-ACCESS: canvas tools by keyboard; name hand-drawn widgets; Tab order test
canvas_keys.rs gives every handle on the photo a focusable stand-in with
an AccessKit name and position: crop corners, edges and box (crop.drag),
spot targets and sources plus a New spot point (Return adds), radial and
linear gradient handles and mask pins. Arrows move by 0.5% (Shift 5%),
one undo step per press; the stand-in keeps arrows/Return/Space from the
shortcuts. The audit now counts any focusable or clickable node; ~50
hand-drawn icons, tiles and rows are announced as buttons. A test walks
Tab through the Edit/Crop/Remove/Masking panels top to bottom.

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:13:49 +11:00
Andrew FrahnandClaude Opus 5.5 32819d4900 LR-MASK-BRUSH: pen pressure scales brush size and flow
Brush strokes record a pen pressure (0..1) per point from egui touch
force while Brush > Pen Pressure is on (default). Each dab's radius goes
from a tenth to all of the brush, and its flow is proportional to the
pressure, in the committed stroke and the live preview alike. A mouse
stroke stores no pressure, so documents and renders are unchanged. The
GPU mask kernel leaves pressure strokes to the CPU. mask.brushStroke takes
`pressure`; bad lists are refused. Also satisfies clippy 1.99's
chunks_exact_to_as_chunks in spots.rs.

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:04:01 +11:00
goujichengandDeepSeek V3.2 3c7f450137 LR-IMP-PREVIEW-READ: a CR3 or a TIFF raw's preview is read from the plan's ranges
The header-only path added in #755 answers for a Fuji RAF, whose preview pointer sits in the first
bytes. A Canon CR3 keeps its preview in a JPEG track, and a TIFF raw (CR2, ARW, DNG, RW2, 3FR …) can
keep its preview tag in an IFD the prefix cuts short: both fell back to `std::fs::read` of the whole
file — tens of megabytes for a JPEG of a few, on the format of the largest bucket in gaps.md (Canon,
33 % of photographers).

`preview_window` now asks `lightcraft_raw::sparse::preview_plan` — the module #961 added for the
import path, which had no production caller — which ranges `embedded_preview` reads, and reads
exactly those: `planned_preview` fills a buffer of the file's length, reads the ranges the plan names
and asks again until it says `Ready`. The buffer is the file's length, which the memory gate counted
already, and its untouched pages stay zero, so what this path costs is the bytes read — on macOS a
freed buffer of that size may sit in the allocator's cache rather than go back to the OS (measured on
a 38k-photo import, #832). The loop gives up past 60 % of the file, the bound step 2's partial read
uses, and a plan that does not fit, a file past 1 GiB or a window that does not decode all end in the
whole-file fallback.

The quick path hands the JPEG to the decoder alone, so two container facts the whole-file path applies
have to be visible in the bytes it is handed, or the file declines and the whole file keeps the
preview:

- a preview colour space of Adobe RGB (`decode_raw_preview` applies it; a JPEG-only decode has the
  JPEG's own space, else sRGB). A container that says sRGB does not decline — that is what the decode
  assumes anyway, and declining it took the fast path away from a real Sony ARW;
- the file's own EXIF orientation, when the preview JPEG carries none of its own: the plan's header is
  a bounded 64 KiB (the header route grows its prefix to the preview's declared extent instead), and
  an orientation tag it cannot see would show the preview unrotated.

Measured over the whole CC0 corpus (111 raws): 52 files take this route, reading a median 7.8 % of
their bytes (0.9-27.7 %) — RW2 3.3 %, Leica RWL 4.1 %, ARW 7.5 %, CR3 8.4 %, DNG 9.7 %, CR2 14.3 % —
while the 21 RAFs stay on the header route (median 4.5 %). The other 38 read whole: NEF, NRW, ORF, PEF
and SRW because the planner does not describe them (an SRW plans to the whole file, which the 60 %
budget then declines), plus the ARWs, CR2s, CR3s and DNGs that declare Adobe RGB or whose plan wants
more than that budget — the bytes they read are the ones the plan would have read anyway. Across all
111 files the plan handed on the same preview as the whole file, with the skipped bytes zeroed and
again 0xFF. `LIGHTCRAFT_PROFILE=1` prints

    [profile] preview …/arw-sony-a7rm4a-compressed.arw: read 688128 of 61832192 bytes (plan, window 483441)

Tests: a synthetic TIFF/DNG whose preview only its tags locate (identical pixels to the whole-file
path, the CFA strip unread), the same structure without the tag that makes it a raw, a file whose
orientation only the whole file shows, and a file that states Adobe RGB against one that states sRGB;
the opt-in real-file test reports which path answered.

Co-Authored-By: DeepSeek V3.2 <noreply@deepseek.com>
2026-10-12 00:57:30 +08:00
dev-null-undefinedandClaude 6fc33e1ee1 LR-VIEW-DETAIL: the loupe says "Rendering…" while what it shows is not the render yet
A raw's embedded JPEG, a thumbnail, the previous render after an edit or the magnified
frame under a 1:1 window all look like the finished photo. Once a render has been on its
way for 0.5 s with nothing new on screen, a pill at the bottom of the photo (widget
`loupe:rendering`) says so; above the Navigator when that is in the way.

The wait starts when the render starts coming (an idle app draws no frames), so a quick
render never shows it, nor does a drag whose drafts keep up; once up it stays until
nothing is coming, so slow drafts don't make it blink. It watches what is on screen: the
After or the Before, and the windows in use. Not an activity row: it says the pixels
aren't final (docs/background-tasks.md).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-11 18:50:44 +02:00
Andrew FrahnandClaude Opus 5.5 1477f892b5 LR-IMP-FORMATS: verify 22 more CR3 bodies; EOS R6 Mark III white balance
All 110 CC0 CR3s on raw.pixls.us decode from sensor data; none uses a
refused CRX variant. One CC0 file per further body (1D X III, 90D, 250D,
850D, M200, M50 II, M6 II, R, R3, R5, R6, R6 II, R6 III, R7, R10, RP, R50,
R50 V, G5 X II, G7 X III, SX70, V1) is added to RAW_SAMPLES and pinned in
cr3_corpus.rs, checked against the camera's embedded JPEG.

The R6 Mark III writes ColorData version 66, laid out as 64/65; it lost
its as-shot WB and white level. Render cache 61 -> 62.

Also: as_chunks in spots.rs for the chunks_exact_to_as_chunks lint.

Docs: evidence.tsv + coverage re-measure (257 verified, 92% use-weighted),
gaps 2/10, checklist rows, raw-parity, target-app-parity, ROADMAP, README.

Sources: own black-box analysis of CC0 files; ExifTool Canon tag tables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 03:45:14 +11:00
Andrew FrahnandClaude Opus 5.5 c78b457c0d LR-VIEW-ZOOM: soft proofing at 1:1 renders a zoom window; 1:1 pixels tested equal to export
Soft proof is per-pixel, so the After window carries the proof like the
whole-frame render under it (the proof key is part of the tile's look).
New tests: a 1:1 window of a 24 MP photo equals the full-size export to
within one level; soft proofing at 1:1 is a canvas-sized frame plus a
native window. profile_slider_drag gains a soft-proof case. Gap 4 closed,
checklist row to done with measured Linux latencies.

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 03:37:35 +11:00
OneSlipsandClaude Opus 5.5 b7222213c9 PERF-PRESETS: thousands of presets no longer stall every command and frame
A Lightroom collection imported whole (11 113 presets, a 42 MB presets.json)
made every interaction take seconds:

- Session::persist ran after every command and serialized the whole preset
  list to compare it with the last write: 609 ms per slider tick (release,
  the real collection). Session::presets is now a PresetList whose generation
  changes on mutable access; persist serializes only when it (or the profile
  lists) changed, and skips the write when the bytes hash the same.
  develop.set: 609 ms -> 2.3 ms; preset.apply: 598 ms -> 3.1 ms.
- The Presets panel deep-cloned every preset into a BTreeMap, built a row,
  widget id and (with thumbnails) a variant render job for each of them every
  frame. Grouping is cached per generation and only rows in view are laid
  out: 5041 -> ~20 rows a frame, 2.3 ms with 5000 presets.
- The preset pickers (Import dialog, Settings > Import) build only the rows
  in view.
- add_presets and opening a library compared every preset with every other
  (quadratic); both use hash indexes now. Opening no longer serializes the
  list once just to remember it.

Tests: develop commands don't re-serialize unchanged presets (and a change
is still saved), generation semantics, a 20 000-preset import, and the
panel building only visible rows (fails on the old code: 5041 rows).
An ignored bench (presets_scale_bench, LIGHTCRAFT_BENCH_PRESETS) times a
real presets.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:32:29 +10:00
Andrew FrahnandClaude Opus 5.5 530320a6bd LR-BEHAV-ACCESS: AccessKit tree audit test; name fields, give sliders range
tests/accessibility.rs walks the AccessKit tree headless in every view,
Develop tool and 13 dialogs and fails on unnamed controls, sliders without
value/range and untitled dialogs. Fixes what it found: text fields are
named by caption or hint, develop sliders expose min/max/step/value text
and take Increment/Decrement, Tab-focused sliders take the arrow keys,
Auto Stack's slider is named. Also as_chunks in spots.rs for the newer
clippy lint. Docs: ui-parity audit section, checklist, gaps, ROADMAP.

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 03:18:48 +11:00
THEtheChadandClaude Opus 5.5 a61a52ed8b perf(gpu): LR-BEHAV-GPU: horizontal box blur reads adjacent pixels
The horizontal box pass gave each thread 16+ consecutive pixels to slide
a running sum over, so a workgroup's threads read pixels 16 apart. On a
Radeon 780M (DX12) that pass took 9-15x as long as the vertical one, and
it sits under every GPU Gaussian and guided filter (NR, texture, dehaze,
clarity/base). Each thread now sums one pixel's 2r+1 window: adjacent
threads read adjacent pixels. Same 64x4 workgroups, parameters and
bindings; box_v unchanged.

Measured on the Radeon 780M (6000x4000): guided filter at sigma 1.75
828 -> 146 ms, colour-NR Gaussian 495 -> 104 ms. Full-size heavy-edit
export of the CC0 Sony ARW: GPU 3.6-4.0 s -> 1.9-2.0 s (CPU 1.9-2.1 s);
GPU-vs-CPU output distance unchanged.

Adds gaussian_matches_cpu (GPU blur vs lightcraft_raster::blur::gaussian,
1-3 channels, partial workgroups, windows wider than the image) and
per-pass rows to bench_kernels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:04:27 +00:00
Andrew FrahnandClaude Opus 5.5 0a6f8d3db8 LR-HDR-EXPORT: HLG HDR AVIF export (hdrTransfer) and a tested SDR/HDR display decision
AVIF HDR export takes hdrTransfer = pq | hlg (export dialog: HDR curve;
MCP export schema lists it). HLG writes CICP transfer 18 and no clli box.
The window logs whether it shows HDR; today always SDR (eframe configures
an 8-bit surface), with the platform / surface format / headroom / opt-out
rules unit-tested. Also satisfies clippy 1.99's chunks_exact_to_as_chunks
in spots.rs.

Part of #868.

Sources: ITU-R BT.2100 (HLG OETF, inverse OOTF with system gamma 1.2),
ITU-R BT.2408 (203 cd/m2 reference white at 75 % HLG); own design for
the display decision.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 03:02:02 +11:00
FredApps 44d5cf7590 #798: reconcile page-backed catalog import with upstream
Preserve page-backed large-catalog import while integrating upstream catalog and documentation changes. Adapt only the sparse test fixture to the shared upstream fixture helper.

Use the equivalent complete RGB-triple iterator required by Rust 1.99 Clippy; the same lint failure was reproduced on the exact upstream baseline.

Validation: 47 catalog tests, nine spot tests, exact sparse regression, all eight CI gates, and an original synthetic headless import/selection check passed.

Generated with Codex.
2026-10-11 17:52:32 +02:00
Andrew FrahnandClaude Opus 5.5 cc3eb1c846 LR-EXP-DNGOPT: lossy DNG export (DNG 1.4 compression 34892)
DNG export gains a "Lossy" compression: the raw data is demosaiced to linear
camera RGB (OpcodeList3 and the default crop applied), scaled to the resize long
edge when one is set, and stored as 8-bit baseline JPEG tiles at the export
quality. The codes follow a 1/2.2 power curve that a LinearizationTable maps
back to linear 16-bit values; colour matrices, white balance and the profile
look are kept. Available in the export dialog, `app.export`
(`dngCompression: "lossy"`) and the MCP export tool.

Also fixes a new clippy lint (chunks_exact_to_as_chunks) in pipeline/spots.rs
that fails `cargo xtask ci` on Rust 1.99.

Sources: Adobe DNG Specification 1.4+ (Compression 34892, LinearizationTable);
own design for the code curve.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 02:40:10 +11:00
Fabio LussiandClaude Opus 5.5 dafa00483a Export: "Same folder as original photo" destination mode
app.export takes destinationMode (specificFolder | sameAsOriginal); the
Export dialog shows it as "Export to". In sameAsOriginal mode each photo
is written into its original's folder plus the optional subfolder, with
the usual conflict policy and original guard per folder. A photo with no
folder on disk or whose folder is missing gets its own error. Saved in
presets and Export with Previous like the other options.

Fixes #956

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:28:40 +02:00