Commit Graph
96 Commits
Author SHA1 Message Date
ronibalthazar c8390afbd2 Fail closed on malformed PSD metadata during original export 2026-10-22 14:21:32 +01:00
aiongg 29565bbad0 Merge PR #1861: LR-EXP-DIM: Width & Height limits each edge to its own number (#1620)
Conflicts:
- docs/parity-checklist.md: header keeps the newer date; revision rows are the union
2026-10-22 02:39:44 +00:00
aiongg 087a82e88a Merge PR #1966: LR-EXP-BITDEPTH: app.export refuses a bitDepth the format doesn't have 2026-10-21 14:49:23 +00:00
aiongg 893fdffc75 Merge PR #2145: Stop export lanes when a panic unwinds the batch thread 2026-10-21 14:48:49 +00:00
wstockandClaude Fable 5.1 780476de1c LR-EXP-DIM: Width & Height limits each edge to its own number (#1620)
Lightroom's Resize to Fit has two boxes: Dimensions (the larger number to
the long edge, whichever way round the photo is) and Width & Height (the
width limited to W and the height to H as the photo stands). LightCraft
had only the first, labelled "Width × Height". Add `ResizeMode::WidthHeight`,
`fit: widthHeight` beside `width` + `height` in app.export, the mode in the
Export dialog (the old one is now called Dimensions), and the docs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
2026-10-18 15:22:09 +00:00
Marco Arnulfo eabcae0fdc LR-EXP-BITDEPTH: judge a preset's bitDepth against the format the export writes
A preset without `format` lost its `bitDepth` before the call's `path` was merged in,
because it was judged against JPEG: `app.export {preset, path: a.png}` with a 16-bit
preset wrote an 8-bit PNG. Session::export_params now merges first and drops a preset's
depth only if the format written (with the caller's fallback) lacks it; a depth the call
passes is still validated. export.savePreset judges the depth at once only when the preset
has a `format`.
2026-10-18 13:45:08 +02:00
Marco Arnulfo 302faf547b LR-EXP-BITDEPTH: check bitDepth against the format a path without a known extension gets
app.export with a path like `output` or `output.xyz` and no `format` skipped the depth
check: the desktop app then wrote JPEG and the headless backend PNG, both at 8 bits.
ExportOptions::export_format resolves the format written (with the caller's fallback for
such a path), from_params_or validates against it, and both callers use it instead of
their own override.
2026-10-18 13:27:07 +02:00
NetterSilverandClaude Haiku 5.5 414764ef77 Stop export lanes when a panic unwinds the batch thread
Fixes #2132

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 11:25:12 +00:00
Marco Arnulfo e14db89462 LR-EXP-BITDEPTH: app.export refuses a bitDepth the format doesn't have (part of #1952) 2026-10-16 02:21:07 +02:00
FredApps 9bec9db541 LR-1670: refresh opaque RGB export against main 2026-10-15 22:24:56 +02:00
Brandon Thomas 9ca113defa Merge PR #1135 (rebased)
# Conflicts:
#	xtask/src/main.rs
2026-10-15 00:53:45 -04:00
Brandon Thomas 9ca3cd0926 Merge PR #1135
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	README.md
#	crates/raw/Cargo.toml
#	crates/ui-egui/src/theme.rs
#	xtask/src/main.rs
2026-10-14 23:23:18 -04:00
Brandon ThomasandClaude Opus 5.5 b8c5913afe Merge PR #1705
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:11:45 -04:00
Brandon Thomas 327e67af9e Merge PR #1798
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-14 22:31:53 -04:00
Brandon Thomas e2f6555220 Merge PR #1750 2026-10-14 22:29:42 -04:00
Brandon Thomas ff66d188e8 Merge PR #996
# 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
#	docs/parity-checklist.md
2026-10-14 21:58:01 -04:00
Brandon Thomas 58dc540cc0 Merge PR #1010
# Conflicts:
#	docs/gaps.md
2026-10-14 21:57:58 -04:00
BuonBredaandClaude Opus 5.5 4551790eab LRC-DEV-LOUPEOVERLAY: a layout image over the loupe (View ▸ Loupe Overlay)
Lightroom Classic's Loupe Overlay ▸ Layout Image: a PNG with transparency (a
cover's masthead and cover lines, a banner's safe areas) drawn over the photo
in the loupe, fitted and centred, following zoom and pan. Opacity and matte
(the photo darkened outside the graphic's box) in steps, ⌥⌘O shows / hides it,
the choice is saved with the UI state. The file is read off the UI thread; an
unreadable one is reported on the canvas. engine::export::read_graphic is the
watermark logo's reader, now shared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mj2w3LwYjFhMHALZuFp2rU
2026-10-14 19:32:06 +02:00
Marco Arnulfo 17e05759b4 DNG export and Convert / Copy as DNG write the colour a raw without matrices opens with (fixes #1745) 2026-10-14 18:08:53 +02:00
Jason Baldwin f776d98167 Merge slideshow-composer into slideshow-overlays (origin/main: batch 35)
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 14:35:02 +02:00
Sergio PereaandClaude Opus 5.5 3c907e0c9d fix(export): a metadata/sharpen/conflict value that passes validation is the one exported
`ExportOptions::validate` accepted `metadata`, `sharpen`, `sharpenAmount` and
`conflict` case-insensitively, but `from_json` read them with serde's
exact-case names and silently fell back to the default when that failed.
`{"format": "jpeg", "metadata": "NONE"}` therefore passed validation and
exported with `MetadataPolicy::All`: GPS, location and camera data went out
in a file the caller asked to carry none. MCP, the control channel and the
CLI all go through `from_params`, so all were affected.

Each of the four enums gets a case-insensitive `parse` (the pattern
`HdrCurve::parse` and `JxlEffort::parse` already use), and both `validate`
and `from_json` call it, so validation and conversion can no longer
disagree. Accepted inputs, error messages, defaults and `to_json` output are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:08:36 +02:00
FredAppsandCodex 2b86ca0537 LR-EXP-TYPE: omit redundant alpha from opaque 8-bit exports (#1670)
Preserve real transparency, pixels and embedded metadata. Cover RGB PNG,
TIFF compression modes and WebP, plus real-alpha round trips.

Co-authored-by: Codex <codex@openai.com>
2026-10-14 10:23:40 +02:00
Andrew FrahnandClaude Opus 5.5 e5efaea6a8 Merge upstream/main into pr/file-writes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:23:00 +11:00
Brandon Thomas 418f6255be Merge PR #1543
# Conflicts:
#	crates/engine/src/export.rs
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-14 01:40:03 -04:00
Fabio Lussi d5b79bf9a1 Merge remote-tracking branch 'origin/main' into lc-956
# 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
#	crates/ui-egui/src/panels/dialogs.rs
#	crates/ui-egui/src/tests_dialog_fit.rs
#	docs/control-protocol.md
#	docs/parity-checklist.md
2026-10-13 23:53:00 +02:00
Andrew FrahnandClaude Opus 5.5 363a1e2a67 Merge upstream/main into pr/file-writes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 08:08:53 +11:00
Michael Anderson 27d1e9b04d Never crash: Cap enlarged exports at 512 MP and check memory first
A huge enlargement failed to allocate and took the app down.
2026-10-13 10:45:39 -05:00
Michael Anderson 337f02b4dc Never crash: Fix export size panic for very long strips
Apply the 1 px and 65535 px bounds in turn instead of clamping between bounds that can cross.
2026-10-13 10:45:34 -05:00
Jason Baldwin ba9262a4a0 Merge slideshow-composer into slideshow-overlays (origin/main: batches 32–33) 2026-10-13 16:57:38 +02:00
bflatastic 32253d862a Merge PR #1017
# Conflicts:
#	crates/pipeline/src/spots.rs
#	docs/gaps.md
#	docs/parity-checklist.md
2026-10-13 11:05:54 +07:00
bflatastic d3c024e4df Merge PR #1047
# Conflicts:
#	crates/pipeline/src/spots.rs
#	docs/gaps.md
2026-10-13 11:03:10 +07:00
Marco Arnulfo 07d7d5d498 M-export: a resized export keeps the edge it was asked for (fixes #1312)
Resize::apply rounds the size, then the render fit the frame into that box
again: on a frame a hair under 3:2 the refit gave 1333 x 1.4999999 = 1999.49
and lost a pixel of the long edge. Frame::fit now keeps a box that already has
the frame's aspect up to rounding, and output_size rounds the native size to
whole pixels first so float noise can't decide a half-pixel tie.
2026-10-12 21:05:42 +02:00
Chad ElliottandClaude Opus 5.5 3e42f2e05c feat(testkit): #1037 tests record why they skip
libtest has no skipped status and hides the output of passing tests, so a
test that returns early without a GPU, a corpus file, a craft-fonts face or
a model passed silently. `lightcraft-testkit` (the testkit slot the layering
table already reserves, dev-dependency only, std only) adds
`skip!(Need, "reason")`: it prints the note as before and, when
LIGHTCRAFT_SKIP_LOG names a file, appends one JSON line (need, reason, test,
binary, file:line). Plain `cargo test` is unchanged. The 61 existing skip
sites use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:33:54 -05: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 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
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
Jason Baldwin b2036ba50d Merge slideshow-composer into slideshow-overlays (origin/main: batch 31; whats-new keeps main's Reliability entry) 2026-10-11 17:12:42 +02:00
Jason Baldwin 56d78344c1 Merge slideshow-composer into slideshow-overlays (origin/main: watermark draws through the shared TextLayout with main's quarter-turn rotation, separate insets and preview floor scale; overlay watermark sanitizes insetX/insetY/rotation and maps legacy inset) 2026-10-11 16:26:40 +02:00
Claude 9978dc739d Merge PR #643
# Conflicts:
#	crates/engine/src/export.rs
#	crates/engine/src/tests_export.rs
#	crates/mcp/src/headless.rs
#	crates/mcp/src/tools.rs
#	crates/ui-egui/locales/de-formats.json
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es-formats.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/fr-formats.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/ja-formats.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br-formats.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru-formats.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/uk-formats.json
#	crates/ui-egui/locales/uk.json
#	crates/ui-egui/locales/zh-hans-formats.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant-formats.json
#	crates/ui-egui/locales/zh-hant.json
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-11 12:43:20 +00:00
Claude 4a0d04b994 Merge PR #780
JPEG XL export via the pure-Rust jixel crate (robmdunn; fixes #247).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4
2026-10-11 12:40:55 +00:00
Claude e1016acc0f Merge PR #953
# Conflicts:
#	crates/codecs/src/lib.rs
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-11 12:03:58 +00:00
Claude 62ece65ac9 Merge PR #664
# Conflicts:
#	crates/engine/src/tests_export.rs
#	crates/mcp/src/headless.rs
#	crates/mcp/src/tools.rs
2026-10-11 11:50:59 +00:00
Claude b6ad706aa6 Merge PR #712
# Conflicts:
#	crates/engine/src/export.rs
2026-10-11 11:50:06 +00:00
Claude c97b2790d3 Merge PR #843
# Conflicts:
#	crates/engine/src/export.rs
#	docs/parity-checklist.md
2026-10-11 11:46:15 +00:00
Claude 36cd72bb33 Merge PR #925 2026-10-11 11:43:08 +00:00
cookingwithsimonandClaude Opus 5.5 44b12efea3 LR-EXP-PSD: Edit in PhotoCraft (right-click Edit In, flat PSD hand-off)
Photo > Edit In > Edit in PhotoCraft (Cmd+Shift+E, also on the photo
context menu) renders a copy with the edits as a flat 16-bit PSD, which
PhotoCraft saves back to in place, stacks it on the original and opens
it in PhotoCraft, found where its installers put it. Edit in Other
Editor (Cmd+Alt+E) keeps the configured program. Per-editor format,
colour space and bit depth in Settings > External Editing. Export gains
a flat PSD format.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4kqvR9KVCPia3SSXyB8WG
2026-10-11 09:35:43 +00:00
Andrew FrahnandClaude Opus 5.5 db7e37843e Export: Remove location info also covers Original and DNG formats
No location in the XMP sidecar or the DNG's packet; exporting an original
that embeds GPS with the option on is refused instead of leaking it.

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 19:30:23 +11:00
Alex ChambersandClaude Opus 5.5 484c160c13 LR-EXP-PHOTOS: Apple Photos imports in the activity stack (#345)
Rebased onto the activity stack (#601). An export adding its files to
Photos keeps its row through that step, with "Adding to Apple Photos…" as
the detail. The worker turns the row's ✕ off (TaskHandle::set_cancellable)
before its last look at the cancel flag and reports what it saw, so a
cancel either stops the export before Photos is asked or is refused, never
both "cancelled" and imported. activity: a per-task gate orders a cancel's
decision against a step switching cancelling off, so a cancel can't be
accepted between the switch and the step's look at the flag and then be
ignored. An import of its own (export.addToPhotos, or
the Photos step left running after a foreground app.export) holds a row of
kind applePhotos while Photos works: no ✕, no count, the album as detail.
The desktop app refuses export.addToPhotos {wait: true}: its window's
thread never waits for Photos. The label is in every catalog; tests assert
the rows, the refused cancel and the refused wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:19:00 +02:00
Alex ChambersandClaude Opus 5.5 a6431c152d LR-EXP-PHOTOS: add exported files to Apple Photos (macOS, #236)
The Export dialog gets "Add to Apple Photos" with an optional album (macOS
only). app.export takes addToPhotos / photosAlbum (dialog, Export with
Previous, presets, MCP, lightcraft-cli run and render --opt); the new
export.addToPhotos {paths, album?, wait?} adds files already on disk and
export.photosImports {job?} reports how imports went.

A constant AppleScript run by /usr/bin/osascript asks Photos' import
command (Apple Events, not synthetic input) to import the files into a
top-level album of exactly that name (made when missing; albums in folders
are never used; several of that name: refused), skip check duplicates
false, and returns the media item ids. Album names and paths are passed
only as separate arguments (on run argv), never spliced into the script.
The runner is injectable (Session::apple_photos): with_fs installs the real
one on macOS only, and tests use fakes, so no test starts osascript or
Photos.

Imports are jobs (Session::apple_photos_imports), one at a time. The
desktop app (Session::apple_photos_background, set in open_session) never
waits for Photos on its UI thread: the background export adds on its
worker, app.export without background and export.addToPhotos answer with
the running job, and the app announces the outcome. Without a window (MCP,
lightcraft-cli) imports wait by default, and both headless front ends (the
MCP/CLI backend and the snapshot UI) wait for a running one before the
process ends, on every exit path, through one engine helper
(Session::wait_for_photos_imports) whose deadline follows the job: a
writing allowance while an export still writes, then the import's own
time limit from when Photos was asked, under an overall cap. A runner that panics, waiting or on a
worker, ends its import as failed and frees the slot (panic guard plus a
completion guard). Session::after_export reserves
the import slot before an export writes anything and frees it on every
exit path (handed the files, cancelled, failed, dropped), so a competing
import is refused with the export's job number instead of the export
failing its Photos step after writing. Permission (-1743), missing Photos,
cancel, timeouts, ambiguous albums, busy imports, other osascript failures
and partial imports produce actionable messages; the export's files are
kept either way. Hidden on other platforms; the web build compiles it out.

Packaging: the hardened-runtime entitlements gain
com.apple.security.automation.apple-events (app and CLI) and Info.plist
gains NSAppleEventsUsageDescription, without which macOS refuses the
events without asking.

Not yet tested end to end against a real Photos library: manual steps,
with the packaged app, in docs/apple-photos.md. Parity row LR-EXP-PHOTOS is
now partial.

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