- `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>
#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
`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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
- 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>
- Export watermark can be a graphic (watermark {image, imageWidth}): a logo with transparency,
scaled premultiplied to a share of the photo's width, converted into the output colour
space, placed with the same anchor / inset / opacity as text; decoded once per file.
Export dialog: Text / Graphic, Choose…, width; size and shadow apply to text only.
- Recently Added: everything imported in the 30 days up to the latest import, newest import
first, grouped by import day (was the latest import day only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Metadata gains alt text, extended description, sublocation, city, state/province, country:
catalog, photo.setMeta, copy/paste metadata, XMP read/write (Iptc4xmpCore AltText /
ExtDescrAccessibility / Location, photoshop City / State / Country), sidecars and export
(place fields dropped with "remove location").
- Info panel: camera card (camera, lens, size · file size, format badge, focal length,
shutter, aperture, ISO), then title, caption, alt text, extended description, copyright,
creator, file name (rename), file path (reveal), captured (edit), place fields, GPS.
- Text fields keep what is typed across frames and save on focus loss (they were rebound to
the stored value every frame). New pencil icon (drawn in code); dates::display_time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- Built-in export presets (JPEG Small 2048 px, JPEG Large full size, Original + Settings, DNG)
and user presets saved with the library (export.presets / savePreset / deletePreset);
app.export {preset, …overrides} everywhere (desktop, MCP, lightcraft-cli run).
- File → Export with Preset submenu (built-ins, user presets, Custom…); dialog: preset
picker, Save preset, Choose… folder picker, subfolder, if-file-exists (add number /
overwrite / skip), start number, TIFF compression (none / LZW / ZIP); {date} naming token.
- One export_batch for the desktop app and headless MCP (paths, conflicts, sidecars);
ExportOptions::to_json is the exact inverse of from_json.
- ⌘E = Export with Previous (alias); parity: Show in Finder row was stale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
RenderRequest gains a sample format (OutputDepth: U8 for previews, U16, F32Linear); deep renders
run the per-pixel stage with the exact sRGB curve and return Rendered::deep alongside the 8-bit
image (GPU stays 8-bit: deep exports render on the CPU). Export encodes deep renders: 16-bit PNG and
TIFF, 32-bit float linear TIFF with a linear ICC profile we generate, 10-bit AVIF (YCbCr planes into
ravif). Output sharpening and watermarks work on deep images. ExportOptions.bitDepth with per-format
defaults (TIFF 16-bit, others 8-bit), export dialog bit depth choice (remembered with the last
export), MCP export schema. Tests: a smooth gradient has 1024 levels in a 16-bit TIFF vs 13 in
8-bit, float TIFF decodes to the same light as 16-bit, per-format depth rules, AVIF 10-bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderRequest gains an output space (default sRGB; previews stay sRGB). The per-pixel stage converts
scene-linear Rec.2020 to the target primaries, gamut-maps into the target gamut and encodes with the
target curve, on CPU and GPU (WGSL finish kernel reads the matrix, luma weights and curve from its
parameter block). sRGB output is byte-identical to before on the CPU. Export embeds our own
generated ICC profile for the space; AVIF stays sRGB. ExportOptions.colorSpace, export dialog and
MCP export schema. Tests: P3 colour survives a P3 export (decoded through the embedded profile),
every space/format round-trips its profile, GPU equivalence per space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export encoding lives in lightcraft-engine::export (pure bytes, shared by app/CLI/MCP/web).
Embeds our own sRGB ICC; writer creates the output folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>