crates/engine/src/export.rs: main moved the code to export/; the 16-bit ZIP size test ported to export/tests.rs
crates/ui-egui/src/panels/dialogs.rs: main moved the dialog to dialogs.rs and dialogs/export.rs; the LZW note helpers, preset hover hint and tests ported there
crates/ui-egui/src/panels/grid.rs: calls crate::dialogs::preset_lzw_note
The doc comment of export_dialog_params stays directly above that function (review fix)
Found while testing crates/mcp/src/base64.rs:20 (base64_decode)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A request sent just as the app expires an idle control connection crosses
the app's id-less idle-close notice: the client's pre-send check saw an
open connection, wrote the request, and read the notice as its reply, so
`--connect` and the MCP server failed a command the app never read.
The app reads nothing after its idle timeout, so such a request never ran.
The app now marks the notice `"closed": "idle"`, and `Remote::call` sends
a request whose reply is that notice once more on a new connection (it
also matches the notice's exact text, for apps from before the marker).
At most one resend per call, whatever the reason; a reply with an id or
any other error is returned as before.
`idle_close_reconnects_without_replaying_a_command` now forces both
orders (notice before the next request, and a request crossing it)
instead of racing the notice; with main's client it fails every run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16-bit LZW TIFFs come out larger than uncompressed ones and slower than
ZIP. The export keeps LZW exactly as picked (some software reads only
LZW), and says what it costs where it is chosen:
- Export dialog: a note under Compression when LZW is selected, worded
more strongly at 16 bit, a milder one at 8 bit and 32-bit float.
- Export presets that write LZW TIFFs carry the same hint on hover, in
the dialog's Preset list and the grid's Export with Preset menu.
- tiffCompression docs: MCP schema and headless params, the user export
guide and the parity checklist.
Tests: the note shows for LZW only and under the Compression choice
(headless), presets get the same hint, the dialog keeps LZW; 16-bit ZIP
is never larger than uncompressed on a noisy synthetic render (noise
0 to 0.1) and on the demo photo's export.
Fixes#2882
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM
The render_photo, render_preview and screenshot tools had the backend write their PNG to lightcraft-mcp-<pid>-<n>-<tag>.png right in the temp dir. On Linux and FreeBSD, where that is the shared /tmp, other users could read the rendered photos and screenshots, and a name someone else created first made the render fail. Each image now goes in a new lightcraft-mcp-<pid>-<random> folder, made 0700 on Unix and never reused when the name already exists, and the folder is removed with anything left in it once the image is read or the call fails. On Windows, and on macOS with its usual per-user TMPDIR, the temp dir was already private; there the PNG just sits one folder deeper.
`recursive_discovery_includes_sony_sr2_and_srf_case_insensitively` (engine) and
`folder_discovery_matches_engine_extensions` (mcp) joined "trip/day2/..." as one string, so
on Windows the expected paths kept `/` while discovery returns `\`, and both failed. They now
join one component at a time. Test-only change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/parity-checklist.md: LR-IMP-FORMATS row keeps main's DNG/Hasselblad text and adds #1165's SR2/SRF phrase; revision rows are the union
docs/raw-parity.md, docs/gaps.md: revision rows are the union
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
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`.
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.
On Windows 11, `cargo test --workspace` on main fails 11 tests:
- tests_export::same_folder_as_original_exports_next_to_each_photo built its
expected paths with Path::join (`\`), but export joins the subfolder and
file name with `/` (join_path) on every platform. Expect what export writes.
- tests_folder_drop (8 tests) looked rows up as
`source:libfolder:C:\...`, while the Folders section names them with `/`
(`source:libfolder:C:/...`), so no row was found. Look them up the same way.
- mcp backend::tests::{valid_replies_and_errors_are_preserved,
malformed_replies_are_rejected_without_resending} failed only when the whole
crate ran: the shutdown and one-shot-process tests start a copy of the test
binary, the child can inherit a socket a fake server just accepted, and when
the child exits Windows closes its copy abortively, resetting the test's
connection. A test-only lock keeps socket tests and child-process tests from
overlapping (passes 6 of 6 whole-crate runs, failed 4 of 4 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reuse the engine's import extensions in MCP and CLI discovery. Add
recursive, case-insensitive discovery tests and record that DSC-F828
SRF import remains preview-only.
Co-Authored-By: Codex <noreply@openai.com>
The connect-mode MCP client accepted any newline-less or wrong-id reply as
the answer to its request. Require a complete newline-terminated reply of
at most 64 MiB, the matching integer id, boolean ok and a result or string
error; otherwise close the connection and report an unknown outcome without
resending. Idless pre-dispatch refusals from the control server still work.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Wake the serving loop after protocol output fails and preserve the actual writer error. Use one process-owned stdin worker so the CLI can finish normal backend cleanup without waiting for client EOF, and terminate the output worker even when a progress hook is retained.
Closes#1527
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Document polling of selection/settings/control ranges and shared edit
interactions. Clarify explicit-photo reads, empty selection and navigation,
and correct photo-tool ID descriptions and MCP initialization guidance.
Partial documentation work for issue 251;
hardware feedback remains outside this change.
Co-authored-by: Codex <codex@openai.com>
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>
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>
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>
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>
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>