Commit Graph
61 Commits
Author SHA1 Message Date
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
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
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
JordyUSAandClaude Opus 5.5 c9c0268f8b LR-HDR-EXPORT: say how viewers without cICP show a PQ PNG
Readers that don't read the PNG cICP chunk show the PQ samples as if they
were sRGB (flat, washed out): expected for PQ PNG, but it surprises people.
Noted in What's new (with the gain map JPEG as the look-right-everywhere
choice), as the export dialog's HDR PNG hint, and in the option and encoder
docs. Review suggestion on #664.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:46:27 +07:00
JordyUSAandClaude Opus 5.5 cbc0e2d7db LR-HDR-EXPORT: HDR PNG export as 16-bit Rec. 2020 PQ (cICP, cLLI)
With HDR output on, a photo edited in HDR exports as a 16-bit PNG with the PQ
curve in Rec. 2020 (SDR white at HDR_REFERENCE_WHITE_NITS), described by a
`cICP` chunk (PNG third edition: BT.2020 primaries, PQ, RGB, full range) and
a `cLLI` content light level chunk (the levels the HDR AVIF records). EXIF,
XMP and print resolution are kept; no ICC profile next to cICP. Photos without
an HDR edit, and HDR edits with HDR output off, export as before.

`codecs::encode_png_pq` sits next to `encode_avif_pq` and takes the same
input. Follow-up to #646, ported from #466.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:46:27 +07:00
john01px2022andClaude Opus 5.5 6f44b33ca7 LR-EXP-WATERMARK: rotate the watermark in quarter turns (#827)
A watermark (text, horizontal or vertical, or a graphic) can be turned 0,
90, 180 or 270 degrees clockwise, like Lightroom Classic's rotate buttons.
It is sized before it is turned and placed and inset by the turned box; the
shadow still falls down and to the right. New watermark key `rotation`
(other values are errors); settings without it draw unturned. The Export
dialog gets a Rotate row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:18:57 -07:00
john01px2022andClaude Opus 5.5 ffd737ceab LR-EXP-WATERMARK: separate horizontal and vertical watermark insets (#827)
Watermark.inset becomes inset_x (from the left or right edge) and inset_y
(from the top or bottom edge), like Lightroom Classic's watermark editor.
The watermark keys are insetX and insetY; the old inset key still reads as
both, so saved presets and existing MCP calls keep working.

The Export dialog's Inset slider becomes Horizontal inset and Vertical
inset, each greyed out when the position is centred along its axis.
cargo fmt reflows WM_POSITIONS and its test from the previous commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:03 -07:00
john01px2022andClaude Opus 5.5 aa6054cd1a LR-EXP-WATERMARK: preview the watermark in the Export dialog (#827)
With Watermark on, the Export dialog shows the first photo it will export
(its cached thumbnail) with the watermark drawn as the file will have it.
The watermark is redrawn on a copy only when it or the output size changes.

draw_watermark_preview scales the 6 px minimum text size and the 1 px
minimum shadow offset by the preview size over the export size, so a small
watermark doesn't look bigger in the preview than in the file. Exports draw
with a scale of 1 and are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:58:53 -07:00
robmdunnandClaude Opus 5.5 afbc0f9744 LR-EXP-TYPE: JPEG XL export (#247)
Export as JPEG XL from the dialog, CLI, MCP and control channel (format jxl): lossy with
quality or lossless, 8/16-bit, effort fast/normal, progressive, EXIF/XMP boxes; HDR edits
as 16-bit Rec. 2020 PQ. Encoder: jixel (pure Rust, BSD-3-Clause OR Apache-2.0, native
only: it spawns its own threads), wrapped in catch_unwind. Lossy files declare an enum
encoding (sRGB / P3 / Rec. 2020; Adobe RGB and ProPhoto written as Rec. 2020); lossless
files embed the ICC profile.

Decoder fix: jxl-oxide desaturated colours outside sRGB when rendering XYB images to
wider primaries; lossy wide-gamut JXL now renders to linear sRGB primaries and is
converted to Rec. 2020 here. jixel's slowest effort is left out: jxl-oxide misdecodes
its (valid) files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:19:30 -04:00
Jason Baldwin 47e9181f69 LR-PREF-PERFORMANCE: GPU fallback reason per render; the export fallback test no longer reads process-wide state (#675)
`export_falls_back_to_the_cpu_when_gpu_work_is_lost` asserted on
`lightcraft_gpu::last_fallback()`, one value for the whole process that
every other engine test's GPU render overwrites between the export and
the assertion (out of device memory under parallel load, once "a GPU
render panicked"). It also switched the GPU off for the whole process
through the injected fault and cleared everyone's state with
`reset_failures()`.

A render now reports its own fallback: `lightcraft_gpu::try_render`
returns `Result<Rendered, Fallback>` (`NotTried` when the GPU was not
used, `Failed(reason)` when it was tried and the CPU must render), and
`render` is a thin `.ok()` over it. `media::develop_reporting` passes
the reason on as `RenderResult::gpu_fallback`, an export carries it as
`Exported::gpu_fallback`, and each file in an `app.export` batch result
has `gpuFallback` when its render fell back. `last_fallback()` stays as
the process-wide diagnostic for `ui.inspect` → `perf.gpuFallback`.

A simulated failure (`inject_fault`) is thread-local, and so are its
consequences now: a fatal injected fault stops GPU use on the injecting
thread only (`available()` false, `unavailable_reason()` set there);
`reset_failures()` clears that and nothing else, so neither reaches the
other tests in the process. Real failures still stop the whole process.

The engine test asserts on its own export's reason and checks that
another thread still has the GPU; `crates/gpu/tests/fallback.rs` uses
`try_render` and checks the same; a new batch test checks `gpuFallback`
lands on the file whose render fell back. Docs: gpu-pipeline.md,
control-protocol.md (app.export result), parity.md.
2026-10-10 12:45:42 +02:00
Brandon ThomasandClaude Opus 5.5 e46e7ab265 Merge PR #646 (HDR peak threaded through #533's base_tone; V1 at peak 1 unchanged)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:26:27 -04:00
akvaithiandClaude Opus 5.5 7828f93c32 LR-HDR-EXPORT: HDR AVIF (10-bit Rec. 2020 PQ)
`encode_avif_pq`: linear Rec. 2020 with SDR white at 1.0 → PQ (SDR white at
203 cd/m², BT.2408), BT.2020 NCL YCbCr 4:4:4, 10-bit, with CICP nclx
(9/16/9, full range) and a clli box. ravif fixes the sRGB curve, so this
drives the rav1e + avif-serialize it already uses (no new crates).

Export: AVIF with `hdr` renders the HDR edit in Rec. 2020 and writes PQ.
Photos without an HDR edit export as before, in every format. CoreImage
decodes the file as HDR (same 3.1 % of the frame above SDR white as the gain
map JPEG of the same edit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:40:37 -05:00
akvaithiandClaude Opus 5.5 34d9c8f2d2 LR-EXP-HDR: HDR export (gain map JPEG, float TIFF) and develop.hdr
- `ExportOptions.hdr` (`hdr` param): photos edited in HDR export as ISO
  21496-1 gain map JPEGs (the SDR rendition as the base, the gain map measured
  against it as written) or 32-bit float TIFF with highlights above 1.
  Other formats and SDR edits export as before.
- Output sharpening and watermarks apply to both renditions.
- `develop.hdr {enabled?, maxEv?}` turns HDR editing on/off and sets the
  headroom limit.

Checked on macOS: ImageIO reports both the ISO and the Apple gain map, and
CoreImage's HDR decode of a 3 EV edit reaches 2.95 EV.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:40:37 -05:00
Brandon Thomas d0f042c269 Merge PR #578
# Conflicts:
#	crates/catalog/src/journal.rs
#	crates/catalog/src/tests_format_version.rs
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
#	docs/whats-new.md
2026-10-09 20:55:40 -04:00
Brandon Thomas 244d279091 Merge PR #536
# Conflicts:
#	crates/catalog/src/tests.rs
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
#	crates/ui-egui/src/lib.rs
#	docs/parity.md
2026-10-09 20:54:53 -04:00
Nathaniel LedfordandClaude Opus 5.5 296fc17376 LRC-LIB-KEYWORDS: exported files carry keywords as their options say
Export wrote each keyword's `a|b` path into `dc:subject` and nothing
into `lr:hierarchicalSubject`. It now writes what
`Catalog::export_keywords` gives: names flat in `dc:subject` (with the
keywords containing them and synonyms, as their options say), full
paths in `lr:hierarchicalSubject`, and nothing of a keyword left out
of export. `export_metadata` takes the catalog for the keyword list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:13:14 -04:00
Nathaniel LedfordandClaude Opus 5.5 9101757bfb LRC-LIB-SMARTCOLL: Shutter Speed rule
Camera Info gains Shutter Speed, compared in seconds and written as the
camera shows it: "≤ 1/60" finds faster exposures, "between 1/1000 and
1/125" works, and a value that isn't a time matches nothing. The editor
takes it as text (1/250, 0.5, 2").

The shutter parser moves from engine export into the catalog
(parse_shutter_seconds) so export and the rule share it. A new
rules-editor test checks that every field × operator default value
makes a valid rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:54:56 -04:00
hxwandClaude Opus 5.5 5630734832 LR-BEHAV-BG-TASKS: render export batches side by side (issue #496)
A batch ran its photos strictly one after another: each render is
row-parallel inside, but a photo's decode, its PNG/TIFF/WebP encode and
the wait on a GPU render are mostly serial, so a 32-core machine averaged
~9.5 busy cores on the CPU path and 1-2 with the GPU.

run_batch now keeps export_parallelism() photos in flight (cores / 4,
1..=4; one on wasm32) on scoped lane threads plus the calling thread.
Each holds an estimate of its working memory from a new export_gate
(twice the memory budget, separate from the decode gate it nests over),
so full-size 24 MP exports (~1.2 GB each) run two at a time by default.
Lanes run at most `lanes` photos past the last one placed, which also
bounds the finished files waiting for their turn. Paths, conflict
handling, original protection, writes, results and progress stay in
batch order (Placer), so the files are the same as one at a time; a
panic in a lane becomes that photo's error.

render_bench's batch scenario now goes through export_batch. 8 x 24 MP
JPEGs, 32 cores + RTX 4080 Laptop: CPU 3.87 s -> 2.35 s, GPU 4.21 s ->
1.82 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:00:59 -05:00
midasdf 93b900f8d7 M16: keep grapheme clusters intact in vertical watermarks 2026-10-09 09:02:24 +09:00
Claude 0b101be155 Issue #181: saved presets and Export with Previous drop keys this version doesn't know
#287 made app.export refuse unknown params. Stored settings (a user export
preset, prefs.json lastExport) written by another version are not the
caller's typo, and refusing them would break that preset or Export with
Previous for good after an update: their unknown keys (also inside
watermark) are dropped before the strict check. What the caller passes is
still checked strictly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 17:29:00 +00:00
Claude cb173c22dc Merge PR #311
# Conflicts:
#	docs/parity.md
2026-10-08 17:20:41 +00:00
midasdfandCodex 69640df8a7 LR-EXP-WATERMARK: shape Japanese vertical watermark glyphs
Co-Authored-By: Codex <noreply@openai.com>
2026-10-08 23:34:43 +09:00
wstockandClaude Fable 5.1 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
goujichengandDeepSeek V3.2 22eb4d3515 LR-BEHAV-LOCALIZE: a language table, Simplified Chinese, and per-script font fallback
The localization layer was hardcoded to English and Japanese: an enum with two
variants, an `is_japanese()` gate compiled into the format macro, and one
catalog pair. Adding a language meant editing the menus, the settings row, the
checkmark logic, the format macro and the font stack — each in a different place.

A language is now one entry in `i18n::language_table!`: BCP-47 code, endonym,
ISO 15924 script and catalog. The Language menu, the settings row, the shortcut
sheet and the control channel are all built from that table, and `build.rs`
picks up `locales/<code>-formats.json` by name, so a further language is a table
entry plus two catalogs (docs/localization.md).

Simplified Chinese (`zh-hans`) ships as the second translation: `zh-hans.json`
(1019 entries, key-set and placeholder-identical to `ja.json`) and
`zh-hans-formats.json` (102 entries). Its glyphs come from craft-fonts' Noto
Sans CJK SC, whose `Hans` faces the UI now installs *before* the Japanese ones
for a Chinese reader, so shared Han characters keep the language's forms
(`fonts::cjk_fallback`); the export watermark accepts every CJK face.

`tr_format!` keeps its compile-time check. It formats the English source at the
call site — a runtime template cannot be handed to `format!` — and names those
values so a translation can place them by name (`{n}`) or position (`{_1}`).
Every translation is still checked by `format!`, so a translation with an
unknown placeholder fails the build. The Japanese catalog was rewritten from the
old `{:.0}` plural-discard trick to that by-name form; behaviour is unchanged.

Also: settings persist the BCP-47 code (never the Rust variant name),
`LIGHTCRAFT_LANGUAGE` accepts system-style tags (`zh_CN`, `ja_JP.UTF-8`),
`xtask parity` reads the second command table, and the glyph tests cover every
catalog character plus the interface symbols (`▸`/`▾` turned out to have no
glyph in any installed font and are gone from the Chinese text).

Chinese and Japanese both need `CRAFT_FONTS_DIR` as before; the web build still
embeds only the Japanese face (Noto Sans CJK SC alone would push the wasm past
Cloudflare's 25 MiB per-file cap), documented in docs/localization-zh-hans.md.

Co-Authored-By: DeepSeek V3.2 <noreply@deepseek.com>
2026-10-07 12:22:15 +08:00
Brandon Thomas 07a7316d8b Merge pull request #149 from storytold/pr/craft-fonts
Fonts: use craft-fonts as an optional build input; move the Japanese fonts out
2026-10-06 14:49:22 -04:00
Brandon ThomasandClaude Opus 5.5 2aa236537f Fonts: use craft-fonts as an optional build input; move the Japanese fonts out
The Japanese fonts (BIZ UDPGothic Regular/Bold for the UI, BIZ UDMincho for text
watermarks; #110, #114) now come from storytold/craft-fonts instead of assets/fonts/.

- crates/engine/build.rs: the craft-fonts recipe. With CRAFT_FONTS_DIR set it embeds
  the manifest's fonts as lightcraft_engine::CRAFT_FONTS (wasm32: BIZ UDPGothic Regular
  only, for the web size budget); unset, CRAFT_FONTS is empty. Local files only, no
  network; craft-fonts is not in any Cargo.toml.
- UI: theme::font_definitions adds the Japanese faces after Inter and egui's defaults
  in every family (BIZ UDPGothic first, Bold first for semibold). About box lists the
  embedded families.
- Watermarks: Inter, then the craft-fonts Japanese faces (Mincho first).
- Removed BIZUDPGothic-Regular/Bold.ttf, BIZUDMincho-Regular.ttf and their OFL files,
  and their ATTRIBUTION rows; added an attribution row for craft-fonts as an optional
  build input; NOTICE updated.
- Tests: Japanese glyph tests use CRAFT_FONTS and skip without it; new tests for
  Japanese in the UI with craft-fonts and for the UI and watermarks without it.
- CI: the FreeBSD job builds/tests with craft-fonts. Releases check out craft-fonts at
  a pinned commit, build with CRAFT_FONTS_DIR + CRAFT_FONTS_REQUIRED=1, and ship the
  fonts' OFL licences (plus NOTICE) in every package.
- Docs: AGENTS.md, CLAUDE.md, README, docs/web.md, docs/localization-ja.md, ROADMAP,
  parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:35:02 -04:00
Brandon ThomasandClaude Opus 5.5 7a2596cd43 Issue #134: Exports: atomic without fsync; Copy import verifies against the probe hash
- safe_file: write policy documented (durable for what can't be recreated, atomic-only for
  recreatable outputs); new write_atomic_nosync (temp + rename, temp removed on failure, no
  file/folder sync); per-thread sync counter for tests.
- export::write_file (exports, renders, screenshots, merge previews, CLI/MCP) uses the
  non-syncing writer; new write_file_durable keeps the sync for Edit a Copy (the TIFF becomes a
  library photo). Catalog, sidecars, settings, DNG conversion/merges and smart previews stay
  durable; the #93 original-overwrite guard is untouched.
- Import > Copy verifies the synced copy against the probe's content hash (SipHash-1-3/128 of the
  whole file) instead of re-reading the source; byte compare when there is no hash. A file changed
  since Review Import is now refused instead of imported with stale details. Move keeps its
  byte-for-byte check before deleting sources.
- check_write_target: skip building the library-wide guard when nothing is at the target.
- write_new_unique keeps its read-back (DNGs replace raws; cheap next to encoding).
- Tests: no sync on the export path, durable writers still sync, Copy caught by the probe hash
  (corrupt copy, stale hash, changed source); ignored timing test write_policy_timing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:08:29 -04:00
Brandon Thomas a6f421aafb Merge main into fix/issue-93-export-overwrite 2026-10-05 18:44:02 -04:00
Brandon ThomasandClaude Opus 5.5 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 ThomasandClaude Opus 5.5 58626b982d Fonts: replace Shippori Mincho with BIZ UDMincho (no Adobe-derived glyphs)
Shippori Mincho's name table credits 'FONTDASU / Google Inc. / but / Adobe' — its kanji
come from Adobe's Source Han Serif, which our asset rules exclude even under the OFL
(CLAUDE.md: no Source Han fonts). BIZ UDMincho (Morisawa Inc., design TypeBank Co., Ltd.;
OFL-1.1, from google/fonts at 63833b7) is a Mincho of its own with no Adobe glyphs, and
smaller (6.2 MB vs 8.7 MB). Attribution, licence text and NOTICE updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:19:38 -04:00
midasdf e00025aaf6 i18n: add Japanese menus and vertical Japanese watermarks 2026-10-06 01:07:26 +09:00
Brandon Thomas efdd8a4e10 Merge main into feat/copyright-status
# Conflicts:
#	crates/engine/src/tests_xmp.rs
#	docs/parity.md
2026-10-04 15:26:53 -04:00
Brandon ThomasandClaude Opus 5.5 a8c3f01e1e CI: clean under clippy 1.99 (as_chunks for constant chunk sizes, ? in export resize parsing)
Rust 1.99 (stable since 2026-09-28) adds clippy::chunks_exact_to_as_chunks and widens
clippy::question_mark, which failed `cargo xtask ci` on main. Same behaviour: the
remainder that chunks_exact skipped is the remainder as_chunks returns separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:53:43 -04:00
storybored379andClaude Opus 5.5 18153682f2 LRC-LIB-METADATA: Copyright Status, Rights Usage Terms and Copyright Info URL
Catalog Meta gets copyright_status (unknown / copyrighted / publicDomain), usage_terms and
copyright_url, read and written as XMP Rights Management fields (xmpRights:Marked, UsageTerms,
WebStatement) on import, sidecars and exports (kept by "copyright only"). photo.setMeta,
copy/paste metadata, metadata presets, a Copyright Status smart-album rule and the Info panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:01:00 -07:00
storybored379andClaude Opus 5.5 309922eac9 LR-EXP-NAMING: export file names use the Rename Photos tokens
rename::expand_tokens is shared by Rename Photos, import renaming and export naming, so {seq:N},
{date:%…}, {camera}, {title}, {ext} work in exports too (a bare {seq} still pads to 3 digits there).
New shared tokens: {num} (number at the end of the name), {folder}, {lens}, {iso}, {rating},
{creator}. An export name that expands to nothing keeps the original name. The MCP export tool
passes startNumber through and documents the tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:26:57 -07:00
Brandon ThomasandClaude Opus 5.5 1aaafb6218 LR-EXP-DNGOPT: DNG export compression (lossless / ZIP / none)
ExportOptions.dng_compression (`dngCompression`: lossless (default) | deflate | uncompressed),
in the export dialog when the format is DNG. Corpus-gated test: all three decode, uncompressed
is the biggest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:27:45 -04:00
Brandon ThomasandClaude Opus 5.5 f81c81b7a0 LRC-DEV-AUTOSYNC + LRC-LIB-LABELS + KEY-STRAIGHTEN: auto sync, label sets, ⌘-drag straighten
- develop.autoSync (Edit ▸ Auto Sync, ⌥⇧⌘A): an edit of the active photo carries only the
  settings that changed to the other selected photos, in one undo step; slider drags sync on
  release; spots / red eye and history restores stay per photo. Edit ▸ Sync Settings in the
  menu; Edit panel banner while it's on with several photos selected.
- Colour-label sets: built-in Colors / Review sets plus user sets in prefs (label.sets,
  applySet, saveSet, deleteSet); listed in Photo ▸ Set Color Label; the names dialog can start
  from a set and save one. XMP sidecars and exported originals write the label's name, and
  reading resolves custom names back to their colour.
- Crop: ⌘-drag draws a straighten line and keeps the crop tool active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:41:05 -04:00