Commit Graph

49 Commits

Author SHA1 Message Date
Brandon Thomas 89753f5d67 Merge PR #563
# Conflicts:
#	docs/parity.md
2026-10-09 20:56:57 -04:00
Brandon Thomas 771882ae52 Merge PR #570 2026-10-09 20:53:53 -04:00
Brandon Thomas 835dcf6657 Keep the UI-zoom tests: set the interface scale with ui.zoomFactor now that Cmd+Plus zooms the photo (#566)
The CLI snapshot test and the headless resize test sent Cmd+Plus to change egui's
interface scale; with the keys handed to View > Zoom they no longer did. A
ui.zoomFactor control request (0.5-3, documented) sets the scale explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:49:00 -04:00
Jason Baldwin a677478b92 KEY-ZOOM: Cmd+= / Cmd+- / Cmd+0 zoom the photo, not egui's interface scale (#566)
egui's zoom_with_keyboard was left on, so at the end of every frame it took the
same keys as View ▸ Zoom In / Zoom Out / Zoom to Fit for its browser-style
interface zoom. On macOS the native menu bar got the keys first; on Windows and
Linux the whole UI scaled. theme::apply turns the option off for every context
(desktop, headless, tests).

Headless tests that pressed Cmd+Plus / Cmd+Minus to exercise the interface
zoom's viewport handling set the zoom factor directly; the CLI snapshot test
that did the same is dropped (the unit tests cover the mechanism). New
tests_zoom_keys.rs: the shortcuts step the photo zoom while the zoom factor
stays, and Ctrl+wheel over the photo zooms it in the Edit view.
2026-10-09 20:14:49 +02:00
Vikram Bhaskaran 57b5317722
Export a contact sheet of photos to a paginated PDF.
File → Contact Sheet PDF… writes the current edits to A4 or Letter at 150 dpi, with optional captions, and will not overwrite an original.
2026-10-10 04:43:31 +11:00
MarekSpodBiedry e529e6f8f4 Use the system clock in the CLI and headless MCP sessions (#554) 2026-10-09 19:29:39 +02:00
storybored379 dedc049692 MCP: add export progress and cancellation between photos
Answer ping at photo boundaries, preserve queued requests and EOF completion, and suppress cancelled responses. Keep completed exports and the existing atomic file writer; cover cancellation, output preservation and transport poison recovery.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
2026-10-08 23:02:21 -07:00
Brandon Thomas c65065de2f Merge the faces stack (#172/#173/#174, integrate-batch-14) into AI RAW denoise (#384)
One config-dir helper (engine::config::config_dir; camera_profiles re-exports it) with both
the face-models and denoise-models defaults; hosts set up both model folders; Settings has
Faces and AI Denoise tabs; both command sets registered; both model dialogs; locale entries
from both; one [features] table per app manifest; Cargo.lock regenerated by cargo; the
DecodedSource cache cost counts both segmentation mattes and the denoised twin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:29:59 -04:00
Brandon Thomas 42f966eec2 Merge PR #335 2026-10-08 23:46:17 -04:00
Brandon Thomas ca97329ae1 Merge PR #174 2026-10-08 23:45:49 -04:00
Brandon Thomas cbf9935b6e Merge PR #172
# Conflicts:
#	docs/parity.md
2026-10-08 23:45:49 -04:00
Brandon Thomas 4977c648f6 Merge main into formats/heif-heif-decode; CI gates the heif feature
Resolve the docs/parity.md conflict, keep the HEIC rows. `cargo xtask ci` gets
a heif step (clippy + tests for lightcraft-codecs with --features heif), since
the workspace build leaves the feature off. The Cargo.toml comments no longer
say official builds enable it; the web one notes that wasm32 aborts on panic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:24:28 -04:00
sanyamsingh001 56754e5703 LR-BEHAV-HEADLESS: keep CPU snapshots isolated from saved preferences
Co-authored-by: Codex <codex@openai.com>
2026-10-09 13:19:47 +10:00
Daniel Voshart 24f87a91a0 LR-PREF-PEOPLE: update pure-Rust recognition and preserve valid model replacements
Keep current upstream UI behavior and the later detector contract checks. Validate candidate models before replacing working weights and records, with a synthetic regression that failed before the fix. Remove the unrelated GPU equivalence-test relaxation. All seven quality gates pass with the pinned hosted fonts.

Generated with Codex.
2026-10-08 22:29:31 -04:00
Daniel Voshart fd4019300a LR-EDIT-DETAIL-DENOISE: address installation and model-policy review
Generated with Codex.
2026-10-08 22:07:23 -04:00
Daniel Voshart 7935999d94 LR-EDIT-DETAIL-DENOISE: non-destructive AI RAW denoise with pure-Rust CPU/GPU
Recover the unpublished denoise work against current upstream. Preserve later opt-in model setup, shared Rust HTTPS downloads, pending-photo resume and an Amount-independent switch. Keep original RAWs unchanged and share the current color finish between ordinary and denoised sources.

Bound public tiling inputs, refuse corrupt-cache exports, and retain synthetic/real-model coverage. Include two Windows path spelling fixes in existing tests. No model weights, face code, media or Tract runtime are included.

Validation: cargo xtask ci (all seven gates); installed RawNIND CPU/GPU agreement 1.38e-6; native CPU preview/export on a public 24 MP Sony RAW, unchanged original SHA-256.

Co-authored-by: Codex <noreply@openai.com>

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

Generated with Codex.
2026-10-08 22:07:20 -04:00
Daniel Voshart cb34bb4842 Faces: Settings > Faces tab and the add-a-model licence dialog
An off-by-default "Recognise faces" toggle and the face model list in Settings,
with a one-drop install: pick a .onnx (Add a model file…) or drop one on the
window. The engine inspects it (recognised by hash, or described from its
shape), a dialog shows its licence, whether commercial use is allowed, what it
was trained on and what LightCraft had to assume, and Install stays disabled
until "I accept" is ticked. "Get…" opens a model's own page in the browser:
LightCraft never fetches a model itself. A file it cannot use says why.

The models folder is shared by the app, the CLI and the MCP server
(`engine::config`, `$LIGHTCRAFT_FACE_MODELS` overrides `<config>/models`), so a
model installed from one is there for the others. A session has none until a host
asks (`with_default_face_models`), which keeps tests hermetic.

Nothing runs a model yet, and the tab says so. Headless tests cover the whole
flow (the dialog, refusing until accepted, installing, a file it cannot use).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:59:01 -04:00
Claude 41e67e1edd
Batch fix: the CLI snapshot test settles before its screenshots
Under load (the full workspace suite) the photo grid was captured before
its thumbnails rendered, darker than the export dialog dimmed over them,
and the dimming comparison failed; it passes alone. Each screenshot now
follows ui.settle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 20:56:35 +00:00
Claude 5601d7879a
Fix #171: a library that can't be locked opens with a warning the user sees
When catalog.lock can't be locked (a file system without locks, such as
some network shares), the library opens unprotected, as documented, but
the promised warning went only to the log. It is now the first of the
library's warnings: library.info lists it under settingsWarnings, the
app shows it once in its notices, and lightcraft-cli run / mcp print the
library's warnings on stderr (stdout stays results / protocol only). A
normally locked library shows none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 19:27:32 +00:00
Claude d56d265044
Merge PR #282
# Conflicts:
#	ROADMAP.md
#	assets/ATTRIBUTION.md
#	crates/engine/src/camera_preview.rs
#	crates/raw/tests/corpus.rs
#	docs/camera-preview-colour.md
#	docs/parity.md
2026-10-08 18:55:07 +00:00
Claude b936c1a683
Merge PR #340 (CLI logging; the desktop keeps #286's file logger) 2026-10-08 17:56:31 +00:00
Claude 74b9129a05
Merge PR #287 2026-10-08 17:18:58 +00:00
DWAA1660 a6cedcc9e8 Issue #168: the desktop app and CLI log warnings to stderr; RUST_LOG works
lightcraft-cli installed no `log` backend, so every log::warn!/info! record
(the GPU-to-CPU fallback, an unknown LIGHTCRAFT_GPU_BACKEND, the adapter
chosen) was dropped and RUST_LOG, which docs/gpu-pipeline.md documents for
the fallback warning, did nothing. The desktop app had its own logger that
read only LIGHTCRAFT_LOG.

Both now install lightcraft_engine::logging: warnings and errors on stderr
by default; LIGHTCRAFT_LOG or RUST_LOG (off, error, warn, info, debug,
trace) picks another level, LIGHTCRAFT_LOG winning, and RUST_LOG's
per-target directives (`lightcraft=debug,wgpu=warn`) set LightCraft's
level. Below warnings only LightCraft's own records are shown, as the app's
logger did. The app's --help lists the variable.

Tests: level parsing and target filtering (engine logging::tests); the CLI
prints "LIGHTCRAFT_GPU_BACKEND=bogus names no known backend" with
RUST_LOG=warn and nothing with RUST_LOG=off (cli.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3vVft4VXvvy8SpbbUceTW
2026-10-08 17:14:04 +00:00
Walter Boring 853ada7754 LR-IMP-FORMATS: HEIC/HEIF decode behind the optional `heif` feature
New crate `lightcraft-heif` (a port of PhotoCraft's photocraft-heif): a thin,
panic-guarded wrapper around heic-rs (pure Rust, pinned =0.1.1) - 8/10-bit,
alpha, grid tiles, container irot/imir/clap, ICC/EXIF/XMP. `lightcraft-codecs`
decodes HEIF behind its non-default `heif` feature (HEVC patents are the
distributor's call, the same policy as PhotoCraft); the apps forward it. HEIC
files stay recognised without the feature: opening one is a clear
"isn't included in this build" error. Fuzz-found heic-rs panics arrive as
regression fixtures (inline bytes, no media committed).

Verified end-to-end on real iPhone HEICs (lightcraft-cli render, rotation and
metadata correct). Note for reviewers: measured here, the engine currently
ignores the decoded image's source-space matrix on export for ANY format (a
true Display-P3 PNG round-trips unchanged); the ICC and space are carried for
all codecs, so this inherits - and does not worsen - existing behaviour.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-10-08 12:45:33 -04:00
wstock 88d9b7196d Issues #181, #183: `app.export` refuses unknown params and bad values; watermark keys documented and range-checked
`ExportOptions::from_json` reads what it recognises and defaults the rest, so
`longEdgee=400` exported at 3000 px, `longEdge=banana` at full size, and a
`watermark` of `5` or `{"text":"x","bogus":1}` passed with `ok:true`, while
`size: 3` was silently clamped to 0.5 of the short edge (#181, #183).

`from_json` stays lenient for stored prefs and dialog prefill. New
`ExportOptions::validate(cmd, params)` / `from_params(params)` check every key
against the option and target lists (unknown ones name the nearest known key),
every value's kind and range, and a `watermark` object's keys, kinds and ranges
(`size` 0.005..0.5 and `inset` 0..0.4 of the short edge, `imageWidth` 0.01..1 of
the width, `opacity` 0..1, `color` [r,g,b]), returning
`invalid parameters for \`app.export\`: …` like the other commands. The three
places that run an export use it — the desktop `app.export`, the headless
backend behind `lightcraft-cli run` and the MCP `export` tool, and
`lightcraft-cli render --opt` — and `export.savePreset` validates a preset's
params when it is saved. Built-in presets, the Export dialog's `to_json`,
Export with Previous and `render --opt` all pass unchanged.

The `app.export` param string (`lightcraft-cli commands`, `engine.commands`),
the MCP `export` tool schema and docs/parity.md LR-EXP-WATERMARK now list the
watermark keys, their units and ranges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 11:06:59 +00:00
wstock ee0b0ef67e Issue #166: `snapshot --script` exits non-zero when a scripted request fails
`fn snapshot` wrote every reply but never looked at `ok`, so a script with a
bad `ui.set`, an unknown command or a failed `ui.screenshot` exited 0 while
the PNG showed the wrong view or was never written. The run now counts
replies with `"ok": false`, keeps going (every reply still reaches stdout and
the final `-o` screenshot is still written — continue-and-report, like
`run --keep-going`) and returns `N scripted request(s) failed` afterwards,
which `main` turns into exit 1. Usage text and docs/control-protocol.md
"Headless rendering" state the contract.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 10:46:29 +00:00
sebastian pfluegelmeier cfcc169ff1 M11.4: calibrate Fujifilm colour with bundled X-H2S and X-T4 profiles 2026-10-08 10:49:55 +02:00
Daniel Voshart 9089092969 Tests: pass on Windows, and under load
- paths are built by component, not with a hard-coded '/' (cli, import, rename, find-missing)
- the rename tests' model volume treats both separators as one
- the Local-folder UI tests hide Home when the temp folder lies inside it, and wait for
  the worker-thread listing instead of three frames
- the GPU equivalence test tolerates up to two edge pixels where the CPU and the card
  fall on different sides of the lens-warp sampling boundary

cargo xtask ci: all 7 steps pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:28:33 -04:00
Harald Hoyer 623122c1a9 Merge origin/main into fix/arw-saturated-hues
Conflicts with issue #150 (NEF colour from the camera JPEG): the per-file
look, its hue/saturation/value table and chroma curve, and camera
profiles now apply to every `file_local_look` format (ARW, NEF, NRW);
`calibrate` also collects NEF/NRW. Render cache version 11 (main 9,
branch 10).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:51:08 +02:00
Harald Hoyer 27d6bc7790 LR-PROF-CAMERACOLOR: camera colour profiles pooled from many photos
One photo can show too little of a colour for its own fit: a lime shirt
covering a few dozen proxy pixels next to foliage at the same hue stayed
yellow (hue 68 deg vs the JPEG's 82 deg).

- `lightcraft-cli calibrate [--max N] [--out DIR] FOLDERS...` pools the
  colour pairs of many ARWs per model (each against its embedded JPEG,
  192 px proxies, <= 4000 pairs per photo) and fits one matrix from
  white-balanced camera RGB plus a hue/saturation/value table.
- Profiles live in $LIGHTCRAFT_CAMERA_PROFILES or
  <config>/camera-profiles/<model>.json; validated on load, damaged files
  ignored with a warning, part of the render cache keys.
- Photos of a profiled model take its colour and fit only their own tone
  and chroma curves; the acceptance gates still apply.
- The table fit buckets samples by hue and runs nodes in parallel; the
  app's config_dir moves to the engine.

Profile from 300 Feb-Aug ILCE-7M4 photos (2 min); 29 held-out September
photos: mean dE vs camera JPEG 3.54 -> 3.10, saturated 6.41 -> 5.20;
the shirt's hue 68 -> 82 deg (JPEG 81.5).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:38:46 +02:00
Mohammed Munzir 489f78ba69 CLI: the connect hint names the port it actually tried (#167)
run --connect ADDR could not reach the app and always advised lightcraft --control 7980, whatever port ADDR named, so following the hint left the command connecting elsewhere. The hint now takes the port from ADDR itself, with an end-to-end test on a refused connection.
2026-10-07 10:41:45 +03:00
Brandon Thomas b66cdb23a3 Issue #136: Windows: use DX12 by default, honour WGPU_BACKEND, never let GPU init block startup
wgpu loads the driver of every backend in an instance's set while it
enumerates adapters. The compute device (crates/gpu) hard-coded
Backends::PRIMARY (Vulkan + DX12 + Metal) and ignored WGPU_BACKEND, and
was created from LightcraftApp::new on a background thread at launch, so
a crashing Vulkan driver (Intel UHD 630, igvk64.dll) killed the process
before the window appeared; the window (egui-wgpu) also listed Vulkan.

- lightcraft_gpu::backend: DX12 only on Windows (Metal on macOS, as before
  on Linux) for both the compute device and the window;
  LIGHTCRAFT_GPU_BACKEND=dx12|vulkan|metal|gl|auto|off, else WGPU_BACKEND;
  backends missing from the build fall back to the default.
- The device is created only when GPU rendering is enabled, after the
  first frame; the preference is applied before the window opens.
- Crash sentinel: gpu-init.marker in the settings folder around device
  creation; if left behind, the next launch starts with GPU rendering off
  and a notice saying how to turn it back on.
- Tests: backend parsing/selection, sentinel, window defaults, headless
  snapshot with LIGHTCRAFT_GPU=0 / LIGHTCRAFT_GPU_BACKEND=off.
- Docs: gpu-pipeline.md (backends, env vars, troubleshooting), whats-new,
  parity LR-BEHAV-GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:16:05 -04:00
Brandon Thomas e1d3b56bb6 Merge main into fix/issue-99-library-lock
# Conflicts:
#	crates/catalog/src/lib.rs
2026-10-05 18:54:56 -04:00
Brandon Thomas ee4b78f646 Issue #99: Library: exclusive lock so two processes can't open one library
Two processes on one library each kept their own catalog and seq counter;
the last snapshot silently dropped the other's edits.

- lightcraft_catalog:🔒 LibraryLock holds an exclusive OS lock
  (std File::try_lock: flock / LockFileEx) on <library>/catalog.lock for
  the session; who holds it goes to catalog.lock.owner (separate file: a
  Windows lock also blocks reads). Released on drop and by the OS when the
  process dies, so leftover files from a crash never lock anyone out. File
  systems without lock support open unprotected with a warning.
- Session::open_library takes the lock before reading anything and fails
  with EngineError::LibraryInUse ("this library is already open in
  LightCraft 0.x (process N on host)"); reopening the session's own
  library hands its lock over.
- lightcraft-cli (mcp/run/snapshot --library) adds how to drive the
  running app instead (--control + mcp --connect).
- Tests: in-process, leftover files, a killed child process, two engine
  sessions, CLI e2e (mcp --library holds it, run --library is refused).
  Existing tests that reopened a library while it was open now drop the
  first session.
- docs: parity row LRC-LIB-CATALOG-LOCK, mcp.md, web.md (no guard across
  browser tabs yet), whats-new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:48:34 -04:00
Brandon Thomas 46732cb0fe Issue #93: Export: never overwrite an original; atomic writes
- lightcraft_catalog::safe_file: one tested helper for crash-safe writes
  (temp file in the same folder, sync, rename, folder sync; temp removed on
  failure), write_new/write_new_unique that never replace a file and read
  the result back, same_file, and a per-thread fault injection for tests.
- engine::originals::OriginalGuard: refuses a write whose target is a
  catalogued photo's original or XMP sidecar (by file identity), whatever
  the conflict policy or exact path. Used by run_batch (export, Export with
  Previous, control/MCP exact paths), ui.render/ui.screenshot paths, merge
  previewPath, MCP render_photo/screenshot paths (new query command
  export.checkTarget) and lightcraft-cli render.
- The conflict policy now covers an export's sidecars together with the
  file (Unique gives both the same free name, Skip skips when either is
  taken).
- export::write_file replaces the plain std::fs::write in the desktop app,
  CLI, MCP and engine writers; XMP sidecar writes use the shared helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:03:07 -04:00
Brandon Thomas 1ef74a787d Never crash: move the one unsafe FFI call into crates/sysmem
The macOS malloc_zone_pressure_relief call (returns freed allocator pages
after raw decodes) lived in identical copies inside the desktop app and the
CLI. It now lives in lightcraft-sysmem, an isolated L0 helper crate and the
workspace's only crate allowed unsafe: one #[allow(unsafe_code)] module, a
SAFETY comment, a safe API that returns bytes released (0 off macOS), and
tests (repeat calls, release after large frees, concurrent calls). Both apps'
alloc_release.rs now just install the safe hook, and their crate roots gain
#![forbid(unsafe_code)], so every production crate except sysmem forbids it.
AGENTS.md/CLAUDE.md name the exception, per craftrules standards/never-crash.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:06:08 -04:00
Brandon Thomas bd2e756b24 Never crash: gpu and merge free of panicking calls
gpu (its render already runs under catch_unwind and falls back to the CPU):
- a failed readback (device lost, out of memory) logs, marks the GPU
  broken and returns zeros, so crate::render discards the result and
  renders on the CPU, instead of panicking;
- an unknown kernel name does the same instead of panic!;
- finish-parameter fields are looked up as Option (debug_assert on an
  unknown name) instead of panic!;
- the warp sampler matches (mode, warp) like the CPU pipeline;
- Buf::raw keeps one documented expect (Some from construction to Drop).

merge:
- pyramid building (push_pull, Blender::finish), camera graph growth,
  bundle adjustment, focal median and alignment use let-else / filter_map
  instead of expect;
- synth::bracket_dngs / pano_views return Result (a missing demo scene
  or a DNG write error is an error); lightcraft-cli synth-merge and the
  engine merge tests handle it.

lightcraft-gpu, lightcraft-merge and lightcraft-cli 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 Thomas 967bf3d8fb LR-IMP-DEVICE: import from camera / memory card
library.devices lists mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and
/run/media, Windows drive letters; LIGHTCRAFT_DEVICE_ROOTS overrides; cached for 2 s).
File → Add from Device opens the import review of a card's DCIM folder, set to copy into the
library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:53:49 -04:00
Brandon Thomas 8a83f8c6ce LR-BEHAV-BG-TASKS: background export with progress and cancel
- Engine: export split into prepare_export (session thread, cheap) and PreparedExport::run
  (Send: read/decode/render/encode); run_batch with per-photo progress, cancel and collected
  errors; export_batch keeps the stop-at-first-error behaviour for scripted calls.
- Desktop: the Export dialog, Export with Preset (menu and context menu) and Export with
  Previous run their batch on a worker thread (app.export {background: true}); a progress
  panel shows count / file / Cancel; ui.inspect reports export {running, last}.
- Headless: settle waits for a running export; `lightcraft-cli snapshot` finishes it before
  exiting. Web keeps foreground exports (downloads are main-thread).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:27:06 -04:00
Brandon Thomas 80dfb437c1 M0.9: lightcraft-cli run — one-shot command chains for agents
`lightcraft-cli run [--demo | --library DIR | --connect [ADDR]] [--import PATH]… CMD k=v … CMD …`
runs any engine/UI command or control-protocol method (headless, on a persistent library, or
against the running app) and prints one JSON line per command; non-zero exit on failure,
--keep-going, --script FILE|- for JSON lines. Imported photos are selected (first active) so
the chain acts on them; a library's edits are saved for later invocations. Docs: docs/mcp.md,
README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:20:45 -04:00
Brandon Thomas 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 Thomas 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 Thomas 7266f4153f M5.6c: one memory budget for caches, decode gate and GPU pool; peak RSS 1.9-2.3 GB -> 0.83-0.99 GB
- memory::budget (default min(25% RAM, 1.5 GiB); LIGHTCRAFT_MEMORY_MB, app.memoryBudget):
  half for engine caches (sources + rendered previews, LRU across them via a global
  use tick; the photo on screen stays), a quarter for decodes in flight (WorkGate:
  thumbnails, prefetch and import probes wait; loupe/export loads never wait), an
  eighth for the GPU free pool.
- GPU: best-fit buffer reuse (<= 25% slack), buffers released outside a render are
  pooled at once (no stranded retired lists), pool trimmed after 3 s idle.
- Decodes drop file bytes and raw samples early, colour/crop/orient in place.
- macOS: apps install malloc_zone_pressure_relief as the release hook (after each
  decode, import and idle trim): freed large blocks no longer stay resident.

14 raws + loupe + 12 steps (M4 Pro): peak RSS 1.9-2.3 GB -> 0.83-0.99 GB, footprint
2.5-2.6 GB -> 0.91-0.96 GB, heap peak 930 -> 656 MB; stepping median ~13 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:35:08 -04:00
Brandon Thomas c937ff97ee M5.6b: header-only raw probe (lightcraft_raw::probe_info); import no longer decodes pixels
Decoders take a Mode: Header reads everything but the samples (DNG, CR2, ARW,
RAF, RW2; PEF when it has crop tags). NEF/ORF (and PEF without crop tags)
derive geometry/CFA from the samples and still unpack them. decode() stays
bit-exact (debug-hash of every corpus/raw decode identical before/after); the
corpus test asserts probe_info == decode().info() for all 37 files.

Import of 14 corpus raws: 3.4 s -> 1.4-2.0 s; peak RSS through import
1.41-1.49 GB -> 1.12-1.22 GB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:46:54 -04:00
Brandon Thomas bc2cda97cf M13.2: Photo Merge commands, dialog with live preview, CLI
- engine: `merge.hdr`, `merge.panorama`, `merge.hdrPanorama` (bracket size from
  EXIF); MergeJob (Send) with progress/cancel, preview renders through the develop
  pipeline (deghost overlay, auto-crop frame); finish writes <name>-HDR/-Pano.dng
  next to the first source, imports, selects, applies Auto Settings / auto crop
- ui: Photo Merge dialog (HDR / Panorama / HDR Panorama) with live preview on a
  worker thread, background final merge with progress toast; context menu and
  Ctrl+H / Ctrl+M (Ctrl now means the physical Control key on macOS)
- cli: `merge hdr|panorama|hdr-panorama`, `synth-merge` for synthetic inputs
- merge::synth: procedural brackets (DNG) and panorama views; docs/merge.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:30:03 -04:00
Brandon Thomas 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 Thomas 37ab239b7b M0.10: headless UI rendering — CPU rasterizer for egui, ui.screenshot headless + fallback, lightcraft-cli snapshot
- ui-egui/softpaint: pure-Rust rasterizer for tessellated egui output (gamma-space premultiplied
  blending like egui-wgpu, bilinear texel-centre sampling, scissor clips, tie-break rule so shared
  edges blend once, parallel row bands, deterministic) + CPU texture mirror from TexturesDelta.
- ui-egui/headless: HeadlessView (offscreen context with our fonts/theme) and Headless (windowless
  app session: logic+ui per frame, answers viewport Screenshot/InnerSize/Close, settle loop,
  control requests through the shared handler).
- ui.screenshot {headless: true} in the running app renders from the logic tick (works while the
  window is occluded / display asleep); windowed captures fall back to headless after 2 s.
  Screenshots now wait for in-flight renders, not just queued ones.
- lightcraft-cli snapshot [--demo|--library DIR] [--script f.jsonl] -o out.png --size WxH --scale S.
- Fixes found with it: dialog backdrop was painted over the dialog itself (now an area below the
  window); dialog content padding; crop overlay options use our buttons (registered widget ids).
- Docs: control-protocol.md (headless section), AGENTS.md (prefer headless when unattended).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:48:48 -04:00
Brandon Thomas 55c5c6f0c6 M1.4: persistent libraries for headless MCP (lightcraft-cli mcp --library DIR); bump render cache version
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:17:38 -04:00
Brandon Thomas 50307e59db M0.9: MCP server + lightcraft-cli
crates/mcp (lightcraft-mcp, L5): hand-written MCP over stdio (JSON-RPC 2.0,
protocol 2025-06-18 with 2025-03-26/2024-11-05 negotiation): initialize,
ping, tools/list, tools/call, resources/list/read. Two backends behind one
trait: Headless (in-process engine Session; answers engine.execute,
engine.commands, ui.render, app.export) and Remote (the desktop app's
loopback JSON-lines control channel; lazy connect + reconnect).

Tools: helpers (list_commands, run_command, import, query_photos,
select_photos, list_controls, get/set_develop, apply_preset, crop,
render_photo -> image content, export), UI tools when connected
(screenshot -> image content, inspect_ui, set_ui, list_widgets, click,
press_key, type_text, pointer_gesture), plus one cmd_<id> tool generated
per command in the registry (--compact omits them).

apps/lightcraft-cli: `mcp [--connect [ADDR]] [--demo] [--compact] [FILES]`,
`render <in> -o <out> [--set control=value]... [--settings] [--preset]
[--size] [--quality]`, `commands`, `controls`.

Acceptance e2e: crates/mcp/tests/e2e.rs sets exposure over MCP and renders
(headless and through the TCP transport); apps/lightcraft-cli/tests/cli.rs
does the same through the real binary's stdio. Docs: docs/mcp.md,
docs/control-protocol.md. Also rustfmt of one line in pipeline/finish.rs
(main was not fmt-clean).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:37:08 -04:00