Merge upstream/main into pr/canvas-keyboard

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrew Frahn
2026-10-14 20:00:12 +11:00
co-authored by Claude Opus 5.5
148 changed files with 6650 additions and 662 deletions
+5
View File
@@ -191,6 +191,11 @@ Therefore:
| `segment.model.cancel` | `{}` | stop it (it resumes next time) → `{cancelled}` |
`remote` reports the configured endpoint, not its health. `installed` describes local weights only.
`download` is `{running, done, total, file, error, cancelled, finished}`: `cancelled` is true when the last
download was cancelled (its `error` says so in words; decide on the flag). An AI mask command refused because
the model is not installed (or still downloading) fails with an error that starts with "The SAM 3 model is
not installed"; inside LightCraft it is `ErrorKind::ModelMissing(Sam3)`, which is what the app offers the
download on.
Coordinates are normalized to the uncropped, oriented photo, like every mask shape. `seg` passes
a stored segmentation (as `mask.list` / the develop settings hold it): the mask is made without
+1
View File
@@ -28,6 +28,7 @@ An export can hand the files it wrote to Apple Photos, optionally into an album
An import can take minutes, and the first one waits for the user to answer macOS's permission prompt. Each
import is a job: `{job, running, requested, album}`, plus, once Photos is done, `{imported, ids, warning?}`,
`{error}` or `{skipped}` (`exporting: true` while an export that will add its files is still writing them).
`skipped` says why Photos wasn't asked, in words; `cancelled: true` comes with it when the export was cancelled.
While Photos works, the activity stack ([background-tasks.md](background-tasks.md)) shows it. An export adding its
files keeps its own row, which reads "Adding to Apple Photos…" and loses its ✕, since Photos can't be stopped once
+18 -1
View File
@@ -62,6 +62,10 @@ control. The exact inputs and reference hashes are:
| Canon R100 C-RAW, raw.pixls.us 7897 | `0b66842b2fe00329ebc05fe8ab9357ddbbe1a0a2d2e69a2893102de7fdf5c942` | `341c706b37c38bbf` |
| Canon R8 RAW, raw.pixls.us 6585 | `7d5c6dbb11ff7e6ee58715d90a20f5d801e8b103f4711ff6ea329ceafcef254c` | `63c1c6d8d1eb2312` |
| Canon R8 C-RAW, raw.pixls.us 6587 | `df33cf394573645ce03dea1b2e9f0b5cc2b7734e9e3391ee7114151414cf2812` | `f836a663a795a375` |
| Canon RP RAW, raw.pixls.us 4674 | `dcbd0f6f9929de2e2331b0fd10a9d01e7cfd04c5a5e4038456de1647d714cdf2` | `5cb0f46404b8b663` |
| Canon RP C-RAW, raw.pixls.us 4672 | `bd3270610593d37ff8a399908bb9406a4e32f9151d55bf19ac1cf8f10d787b46` | `f48df06ca1d67382` |
| Canon RP RAW, 1.6× crop, raw.pixls.us 4673 | `7d4ba45dc9264d598336133593c677b09774bcdf13b602ee770e72415a652a16` | `46808a9de3b3dcc7` |
| Canon RP C-RAW, 1.6× crop, raw.pixls.us 4671 | `4a859ab137f792aedaf3416b0aad1497ebaab02052a49453902e226699154aa7` | `39a60c4efed4e1f4` |
The EOS R5 Mark II files (raw.pixls.us 7881–7884, CC0) are pinned to LightCraft's own decode; no external
decoder was run on them:
@@ -77,7 +81,11 @@ Reference pixels were observed from the DNGLab 0.8.0 macOS arm64 release
binary; no decoder source was read or copied, and it is not shipped or linked.
The release ZIP SHA-256 was
`ee70805cb60f18d5ed62548c4e595f6cee0a33407e15358eb1766a46afc1c16e`.
All six sensor comparisons pass. R8 HEVC preview tracks are distinguished from
The RP references came from the same release's Windows x64 binary (ZIP SHA-256
`d3add6b71568bf5f7938468b0b40a0ee1eaec01e0057fe78dcc453188010be9a`), after it
reproduced the M50 RAW hash above. The RP's 1.6× crop mode writes a single
4032×2656 tile, where full frame (6384×4224) uses two 3192-wide horizontal
tiles. All ten sensor comparisons pass. R8 HEVC preview tracks are distinguished from
the primary Bayer RAW; the HEVC preview itself is not decoded. Missing corpus
files are reported as skipped by the tests.
@@ -129,6 +137,15 @@ tile count, marker headers and temporary allocations. The full sensor buffer is
allocated only after the first plane decodes, and plane buffers grow row by row,
so a small crafted file fails before reserving memory for the image it claims.
## White level
The white level is Canon's specular white from `ColorData` (the word after the
normal white level), not the sensor's saturation point. On the RP at ISO 100
it is 14558 while samples clip at 16383. At the −⅓-stop ISOs, which pull a
higher analog ISO down digitally, it drops further: 11062 at ISO 5000, where
red saturates near 13668. Samples above the white level render as clipped.
Whether Lightroom recovers them has not been measured.
## Colour limits
CR3 carries as-shot white balance but no measured camera matrix is imported
+31
View File
@@ -292,6 +292,37 @@ shot), so check a model on your own photos before trusting it:
(ignoring shots taken within a few seconds of it, which would make it too easy) and reports, for each threshold, how many
suggestions it would make and how many were right.
## Face pictures in the People view
Each face on a person card, in Unnamed faces and on a person's page is a square around the face, about 1.7 times the
face box, cut out of the photo with the photo's own look (`Session::face_job`). Where the pixels come from:
- **An unedited raw** (it still looks the way it was imported, the case in which its grid thumbnail is the camera's
embedded JPEG too) is cut from its **largest embedded preview** (`crates/engine/src/face_tiles.rs`). The preview is
read the planned way ([raw-preview-reads](raw-preview-reads.md)): the file's header and the JPEG, never the sensor
data, and the raw is not developed. The preview is used when it holds the square at **at least ¾ of the pixels the
tile asks for** (up to the 2,560 px the develop path would use); the square is then enlarged by at most 4⁄3, which
is not visible at tile size (the camera's JPEG is sharpened, so its pixels carry more detail than a binned raw's),
and is the bound the raw path already accepts when it bins an X-Trans sensor for a preview. A smaller preview, one of
another shape than the photo (letterboxed, another aspect ratio), one that does not decode, or a file that is offline
leaves the face to the develop path below.
- **Everything else** (an edited raw, whose edits the camera's JPEG can't take the way the raw does; JPEG, HEIC and
other files; a raw whose preview is too small) is a develop render of the photo with its crop replaced by the square,
from the smallest source level that keeps the face sharp, as before.
Measured on two Sony raws on an external 5400 rpm disk (file cache dropped before each face, a centred face box of 4 %
and 10 % of the frame, 176 and 352 px tiles): an α1 ARW (74 MB, 8640 × 5760 Adobe RGB preview) read 70.6 MiB in
0.77–0.92 s per face before and reads 7.8 MiB in 0.20–0.26 s; an A7S III ARW (25 MB, full-size 4240 × 2832 preview)
read 24.3 MiB in 0.38–0.56 s and reads 1.1 MiB in 0.04–0.09 s. The tiles differ from the develop path's by 2–6 levels
of 255 on average (the camera's rendering against LightCraft's camera look, which is fitted to it).
Rendered faces are kept like other thumbnails: in memory, and in the library's thumbnail cache on disk (`thumbs/`,
within the Settings ▸ Performance cache budget, least recently used files go first). The key is the photo's content
hash, its develop settings with the face's square as the crop, the tile size, the renderer version and the camera
profiles, so an edit, another square or a new renderer makes a new picture, and the next launch shows the faces
without reading the originals. Tile sizes are rounded up to a multiple of 16 px, so moving the thumbnail slider
reuses the pictures of its step instead of rendering every face again.
## Commands
All of this is reachable from the control channel, the CLI and MCP: `faces.models.list`, `faces.models.inspect {path}`,
+5 -4
View File
@@ -1,6 +1,6 @@
# Gaps: where LightCraft falls short of Lightroom
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (gap 23: canvas tools by keyboard, 4–8 h → 2–4 h) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** patch (gap 8: Nikon Z 6II / Z 8 / Z f in-file distortion; gap 23: canvas tools by keyboard, 4–8 h → 2–4 h; gaps 10, 22 and 23 listed once again: merges had left two copies of each, and `cargo xtask parity` now fails on a gap listed twice)
Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and
how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is
@@ -22,10 +22,9 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 5 B | **AI masks: Subject, Sky, People, Background, Landscape** are heuristics; Object / Describe run SAM 3 but its download has no mirror configured | `crates/segment`; ai-masks.md; MASK rows 67% | masking is how most Lightroom users edit today | 40–70 + model decision | [target-app-parity](target-app-parity.md#ai) |
| 6 B | **Compressed ORF** (every Olympus / OM body since ~2008) | measured: 66 of 78 Olympus / OM models preview-only; `orf.rs` refuses; OM bucket 3% of photographers, 3% of them fully work | +2.6 points; best gain per hour of any raw gap (~0.2) | 8–15 | [raw-parity](raw-parity.md#compressed-orf-a-clean-room-starting-point) (the two Olympus patents that describe the method) |
| 7 | **Nikon High Efficiency NEF** (HE / HE★ on Z 8, Z 9, Z 6III, Z f, Z50II, Z5II when set to HE) | named and preview only (#193); needs a pure-Rust JPEG XS decoder | +1.1 points (only shots taken in HE; lossless works on the same bodies); low gain per hour (~0.04), so not a beta blocker | 20–40 | [raw-parity](raw-parity.md) |
| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion and Sony ILCE-7RM4A distortion | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) |
| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion, Sony ILCE-7RM4A distortion and Nikon Z 6II / Z 8 / Z f distortion (`docs/nikon-lens-corrections.md`) | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) |
| 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) |
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12, Leica 14 (the Q3 by a recorded check of 38 private DNGs). Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
| 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) |
| 12 | **Content-aware / generative remove, detect distractions, reflections removal** | REM rows 75%; patch synthesis missing | cleanup work goes back to Photoshop | 25–45 (+ generative model) | [target-app-parity](target-app-parity.md#ai) |
| 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) |
@@ -37,8 +36,8 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 19 | **Plug-in ecosystem**: no Lightroom SDK equivalent (export / publish plug-ins, metadata plug-ins) | ⬜ | pro workflows built on plug-ins (LR/Enfuse, Jeffrey's exporters, Negative Lab Pro) | 40–80 + owner decision | [target-app-parity](target-app-parity.md#by-dimension) |
| 20 | **Long-tail raw formats**: compressed SRW, ARQ pixel shift, IIQ, Hasselblad compression, X3F, CRW, MRW, ERF, KDC, DCR, MEF, GPR, old FinePix | raw-parity.md | small user share each | 45–85 | [raw-parity](raw-parity.md) |
| 21 | **Localization**: 9 of 15 shipped languages partial (74–99% of catalog messages), lower-layer errors English; Hindi, Arabic, Indonesian, Korean, Vietnamese absent; no RTL or complex shaping | measured from catalogs | non-English users see mixed English | 55–90 (shipped to full); 145–255 (five new) | [localization-parity](localization-parity.md) |
| 22 | **Pen pressure**, OS monitor profiles on Windows (Linux reads colord's, macOS the screen's), follow-window colour on Linux / Windows | hardware-parity.md | brush users with tablets; multi-monitor setups | 11–22 | [hardware-parity](hardware-parity.md) |
| 23 | **Accessibility**: AccessKit tree audited by a test; canvas handles, spots and mask pins by keyboard; left: brush and red-eye by keyboard, a session with a real screen reader | LR-BEHAV-ACCESS 🟡 | blind and low-vision users | 2–4 | [ui-parity](ui-parity.md) |
| 22 | **Pen pressure**, OS monitor profiles on Windows (Linux reads colord's, macOS the screen's), follow-window colour on Linux / Windows | hardware-parity.md | brush users with tablets; multi-monitor setups | 11–22 | [hardware-parity](hardware-parity.md) |
| 24 | **Brush live preview and masking feel** (#517, #518); Feather / Edge mask refinement (15.5) | issues | masking feels slower than Lightroom | 8–12 | [ui-parity](ui-parity.md) |
| 25 | **File writes**: lossy DNG; AVIF import; Render to DNG as a catalog command (the export format landed) (PSD export landed with Edit in PhotoCraft, #953) | file-format-parity.md | interchange edge cases | 19–34 | [file-format-parity](file-format-parity.md) |
| 26 | **Performance at scale**: 85k-photo library opens in 1.7–4.7 s; 100k grid test pending; colour NR at half resolution | ROADMAP log, gpu-pipeline.md | large-catalog users | 15–30 | [target-app-parity](target-app-parity.md#by-dimension) |
@@ -73,6 +72,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | patch | Gaps 10, 22 and 23 each had two rows (two branches edited them; the merges kept both): the older copies are removed, and `cargo xtask parity` (in `ci`) now fails when a gap number is listed twice |
| 2026-10-11 | minor | Gap 10 shrinks: Leica Q3 verified by a recorded check of 38 private DNGs (236 models verified; Leica 14); gap not closed |
| 2026-10-12 | minor | Camera-look deep shadows follow the JPEG toe (#1307); camera calibration remains partial |
| 2026-10-12 | minor | Gap 23 (accessibility): crop handles, spots, gradient handles and mask pins by keyboard; 4–8 h → 2–4 h; gap not closed |
@@ -91,3 +91,4 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| 2026-10-10 | minor | RAW gaps re-measured model by model (raw-parity.md): CR3, compressed ORF and verification entries carry measured shares; HE NEF no longer a beta blocker (+1.1 points for 20–40 h); verification entry absorbs the cheap per-body fixes (#769, #702, A200 series) |
| 2026-10-10 | minor | Beta total raised to 340–600 h to include the ~92 open core-path user issues |
| 2026-10-10 | major | Created from the checklist's Top gaps (whose items 1–2 had been duplicated by a merge) and ROADMAP's Where we stand; re-ranked; added evidence, impact, hours and beta flags; added platform-CI, lens-database, HE NEF and localization gaps |
| 2026-10-13 | minor | Gap 8 narrows: Nikon Z 6II / Z 8 / Z f NEFs use the camera's in-file distortion correction (`docs/nikon-lens-corrections.md`) |
+1 -1
View File
@@ -1,7 +1,7 @@
# Lens profiles from a lens database
Raws that carry no lens correction of their own (DNG `OpcodeList3`, or a camera correction a raw reader converts,
see [sony-lens-corrections.md](sony-lens-corrections.md)) can be corrected from a **lens database in the lensfun
see [sony-lens-corrections.md](sony-lens-corrections.md) and [nikon-lens-corrections.md](nikon-lens-corrections.md)) can be corrected from a **lens database in the lensfun
XML format** that you supply. The app does not ship any lens database: lensfun's data is licensed CC-BY-SA 3.0 and
stays a separate download. Adobe LCP profiles are never used.
+5
View File
@@ -4,6 +4,7 @@ Select photos in Photo Grid or Square Grid, then use:
| Key | Action |
|---|---|
| `Cmd+Shift+D` (macOS) / `Ctrl+Shift+D` (Windows/Linux) | Select only the active photo |
| `0` | Clear the star rating |
| `1`–`5` | Set the star rating |
| `[` / `]` | Decrease / increase each selected photo's rating by one, within 0–5 |
@@ -13,6 +14,10 @@ Select photos in Photo Grid or Square Grid, then use:
| `U` | Clear the flag |
| `Shift` + `0`–`9`, `P`, `X` or `U` | Apply the action and select the next photo |
Select Only Active Photo (Edit menu) narrows the selection to the active photo. It does not change
the photo, its edits or the last edit Undo step, and it works in any view that has an active photo.
It can be reassigned or disabled in Help → Keyboard Shortcuts; text fields and dialogs keep the key.
Purple and clearing a colour label are available in Photo → Set Color Label and the photo context
menu. Purple has no default number key, matching Lightroom Classic. Rejection marks a photo for
culling; it does not delete the original or remove it from the library.
+5 -2
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first per-language measurement from the catalogs) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (the interface no longer decides on error text, a prerequisite for translating lower-layer errors) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
How far each interface language is from `full`. How to add or maintain a language is in
[localization.md](localization.md); per-language notes are in `localization-<code>.md`
@@ -73,7 +73,9 @@ Target languages LightCraft lacks: Italian, Korean, Dutch, Polish, Swedish, Norw
## Shared work before any language is `full` (counted once, ~20–35 h)
- Route lower-layer error messages (catalog, raw, codecs, engine) through translatable message ids
instead of English `format!` strings (~12–20 h).
instead of English `format!` strings (~12–20 h). Translating them must not change what the app does:
since #1046 the interface decides on an error's kind (`lightcraft_engine::ErrorKind`, a download's or
Apple Photos import's `cancelled` flag), not its words; new decisions on an error go through a kind too.
- A check in `cargo xtask ci` that every user-visible literal reaches a catalog (~4–8 h).
- A user manual / help pages beyond What's New and the shortcut sheet (Lightroom links to online
help; ~4–8 h for a translatable in-app help index).
@@ -89,4 +91,5 @@ Target languages LightCraft lacks: Italian, Korean, Dutch, Polish, Swedish, Norw
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Shared work: the interface decides on error kinds, not English error text (#1046) |
| 2026-10-10 | major | Created: per-language counts measured from the catalogs, the twelve key languages, script support, estimates |
+4 -1
View File
@@ -22,6 +22,9 @@ it open, `--library` on the same folder fails with "This library is already open
`--demo` starts the headless session with the procedurally generated demo library;
`--compact` lists only the helper tools (see below). In connect mode the server starts even when
the app is not running yet and connects on the first call (and reconnects if the app restarts).
A request is sent again only when it never reached the app. If the connection drops after a request
was sent but before its reply, the tool fails with "the outcome is unknown" and nothing is resent, so a
command such as undo can't run twice: check the app's state before retrying.
Logs go to stderr; stdout carries only protocol messages.
@@ -285,7 +288,7 @@ lightcraft-cli calibrate --max 300 ~/Pictures/2026 # camera colour profiles (d
`app.export` (MCP `export` tool, `lightcraft-cli run`, the desktop app) takes `addToPhotos: true` and
`photosAlbum` to hand the files it wrote to Apple Photos, into that album (a top-level album of exactly that
name, made when missing). The result's `applePhotos` is the import job: `{job, running, requested, album}`
plus `{imported, ids, warning?}`, `{error}` or `{skipped}` once Photos is done; the files are written either
plus `{imported, ids, warning?}`, `{error}` or `{skipped}` (with `cancelled: true` when the export was cancelled) once Photos is done; the files are written either
way. Headless (this server, `lightcraft-cli`) waits for Photos by default; the desktop app (connect mode)
answers with the running job and `export.photosImports {job}` gives the outcome. `export.addToPhotos {paths,
album?, wait?}` does the same for files already on disk (`wait` defaults to true headless, false in the desktop
+8 -1
View File
@@ -27,6 +27,11 @@ homography (the simplest model that explains ≥ 97 % of the matches). Exposure
Radiance is a weighted mean in linear light, weight `k · (1 − smoothstep(0.8·clip, 0.96·clip, max
channel))`. Deghost: a per-pixel consistency test against the reference radiance (with a noise
floor), dilated and feathered masks; the union is the deghost overlay. Output: 16-bit float DNG.
Lens corrections: frames are merged uncorrected, and the DNG's `OpcodeList3` carries the reference frame's
(`WarpRectilinear`, `WarpRectilinear2`, `FixVignetteRadial`: a DNG's own, or a camera's correction a raw reader
converts, e.g. Sony, Panasonic), re-expressed for its default crop (`Opcode::cropped`). So Optics ▸ Enable Lens
Corrections works on the result as on the sources; a vertical bracket keeps its pixels unturned and its
corrections.
**Panorama** (`pano/`): MOPS-style features, pairwise RANSAC homographies with Brown & Lowe's
verification, focal from homographies (or EXIF 35 mm), rotations chained along a maximum spanning
@@ -54,7 +59,9 @@ scale in `BaselineExposure`; raw sources keep their DNG colour tags (camera RGB)
## Limits (known gaps)
- No lens-distortion model in the panorama bundle adjustment (rectilinear lenses assumed); no
360° wrap-around; flat (translating-camera) scans are fitted with the rotation model.
360° wrap-around; flat (translating-camera) scans are fitted with the rotation model. Panoramas and HDR
panoramas carry no lens corrections, nor does an HDR whose frames don't share an orientation (they are turned
before merging).
- No camera response calibration for JPEG brackets (sRGB decoding is taken as linear light).
- Boundary Warp is a separable stretch, not a content-preserving mesh warp; Fill Edges is diffusion,
not patch synthesis.
+101
View File
@@ -0,0 +1,101 @@
# Nikon embedded distortion corrections
Nikon writes its own distortion correction into NEFs from its mirrorless bodies, in the raw SubIFD tag `0xc7d5`
(NikonNEFInfo). LightCraft reads it for the NIKON Z 6_2 (Z 6II), the NIKON Z 8 and the NIKON Z f and turns it into an
`OpcodeList3` `WarpRectilinear`, so it goes through the same profile correction control, optics path, orientation
handling and DNG export as the other embedded corrections. Other bodies are left alone for now even if they have the
same tag, because I've only checked files from these three.
This is distortion only. The vignetting block (`0x0006`) and what looks like lateral CA (`0x0007`) aren't applied
yet. Like the Sony, Fujifilm and Panasonic corrections it's on by default and can be turned off under Lens
Corrections.
## Where the data comes from
The [ExifTool Nikon tag names](https://exiftool.org/TagNames/Nikon.html) give the names of NikonNEFInfo and its
DistortionInfo entry (`0x0005`): DistortionCorrection and RadialDistortionCoefficient1 to 3. Nothing else was taken
from other software. The rest was worked out by comparing LightCraft's uncorrected render of each NEF with the
full-size JPEG the camera embeds in it. That JPEG is always distortion-corrected with Z lenses ("On (Required)"), and
with other lenses when Auto distortion control is on. The comparison matched textured patches between the two images
(normalised cross-correlation on the green channel at quarter size).
NikonNEFInfo starts with `"Nikon\0"` and a 4-byte version, followed by a small TIFF stream. The two entries used here:
| Entry | Bytes | What it holds |
|---|---|---|
| `0x0005` DistortionInfo | 0–3 | `"0100"` |
| | 4 | correction mode: 1 "On (Optional)", 3 "On (Required)", 0 none |
| | 8–15 | normalisation radius in mm, a signed rational. It's 21.6 (the FX half diagonal) in every file I looked at |
| | 16–19 | number of terms, 4 |
| | 20, 28, 36 | the three coefficients, called `k1`, `k2`, `k3` here after ExifTool's names |
| | 64–71 | the zoom `q` the camera uses to fill the frame |
| `0x0008` | 0–3, 4–11, 12–19 | `"0100"`, then the optical center's offset from the center of the image area in pixels (x right, y down) |
The `0x0008` offset is where the in-body stabilization left the sensor. It's zero with a VR lens, since the lens does
the stabilizing, and up to about 100 px with the Z lenses. The center of the distortion measured in the JPEG follows
it closely, for example (−90.1, +18.6) recorded and (−91.2, +17.5) measured.
From an output pixel to the raw, around the optical center, the mapping is
```
rho = r_out / half diagonal of the image area
r_src = r_out * (1 + k3*rho^2 + k2*rho^4 + k1*rho^6) / (1 + q)
```
So the stored order goes from the highest power down. Reading them the other way round (`k1` with `rho^2`) doesn't fit
the JPEG at all. `q` really is the zoom: fitting a separate zoom for every file gives `1 / (1 + q)` to four decimals
(0.9675 fitted and 0.9676 from `q` on one of them), so nothing is fitted per file. The decoder turns this into DNG
terms the same way `docs/sony-lens-corrections.md` describes for Sony, with the center in active-area coordinates and
the coefficients rescaled to the farthest corner of the active area.
## How well it matches
I checked it on 83 of my own Z 6II photos with three lenses between 20 and 200 mm, 45 Z 8 photos (below), plus the
CC0 Z f sample in the corpus. The numbers are the radial error against the camera JPEG at full size (6048 px wide), out to 0.95 half
diagonals:
| Lens | Focal length | Frames | Uncorrected median / p95 | Corrected median / p95 |
|---|---:|---:|---:|---:|
| VR 70-200mm f/2.8G (F-mount, FTZ) | 70 mm | 11 | 10.9 / 15.9 px | 0.19 / 0.57 px |
| VR 70-200mm f/2.8G (F-mount, FTZ) | 105–140 mm | 15 | 34–42 / 38–46 px | 0.13–0.18 / 0.59–0.90 px |
| VR 70-200mm f/2.8G (F-mount, FTZ) | 175–200 mm | 21 | 45–46 / 50–51 px | 0.10–0.18 / 0.48–0.95 px |
| NIKKOR Z 24-70mm f/4 S | 70 mm | 8 | 126.4 / 141.2 px | 0.48 / 4.38 px |
| NIKKOR Z 24-70mm f/4 S | 38–59 mm | 6 | 59–101 / 69–121 px | 0.36–0.76 / 4.6–6.8 px |
| NIKKOR Z 24-70mm f/4 S | 33–34 mm | 4 | 32–44 / 43–53 px | 1.0–1.3 / 8.5–9.6 px |
| NIKKOR Z 24-70mm f/4 S | 24 mm | 7 | 16.0 / 105.6 px | 6.90 / 23.56 px |
| NIKKOR Z 20mm f/1.8 S | 20 mm | 5 | 8.2 / 53.9 px | 1.49 / 5.46 px |
| Tamron 35-150mm (Z f, CC0 corpus) | 150 mm | 1 | 70.4 px median | 0.22 px median |
On the Z 8 (8256 px wide JPEGs) it looks the same. The 70-200 f/2.8G on the FTZ matches within 0.14 to 0.21 px median
at 70, 135 and 200 mm. The NIKKOR Z 24-120mm f/4 S is within 0.6 to 1.0 px median from 36 to 120 mm and 1.5 px at 24
to 28 mm, with the same kind of error toward the edges at the wide end as the Z lenses on the Z 6II. A few 14-24mm
f/2.8G frames had Auto distortion control off, so their JPEGs aren't corrected and match about the same either way.
I also rendered a few with `lightcraft-cli render` at default settings and measured those against the camera JPEG.
The median error was 0.10 px for the 70-200 at 200 mm and 0.12 px for the Z f sample. The 24-70 was 0.32 px at 70 mm,
on a frame with a 92 px sensor shift, and 8.4 px at 24 mm. The 20 mm came out at 1.3 px. On the Z 8 it was 0.14 px for the 70-200 at 200 mm, 0.45 and 0.68 px for the 24-120
at 68 and 120 mm, 1.8 px for the Z 20 mm and 3.7 px for the 24-120 at 24 mm. That checks the
center, crop and DNG conversion along with the model itself.
The F-mount lens on the FTZ matches about as well as I can measure. The native Z lenses still have a smooth error
toward the edges that doesn't depend on the sensor shift and is the same in every frame at a given focal length: about
+22 px at 0.7 to 0.9 half diagonals at 24 mm, +8 px at 33 mm and +3.5 px at 70 mm. It seems the camera applies an
extra correction for Z lenses that isn't in the readable coefficients. NikonNEFInfo `0x000e` looks encrypted and I
didn't try to decode it. Even so, the readable correction takes most of the distortion out. At 24 mm the error at 0.7
half diagonals goes from −82 px to +25 px, and at 70 mm from about −137 px to under 4 px.
## What's left out
Only `NIKON Z 6_2`, `NIKON Z 8` and `NIKON Z f` are corrected, matched exactly. A matching tag on another body doesn't tell us its
pixel pitch or image area, so other bodies can be added once someone checks their files. Only the full-frame 3:2
image area is corrected, because the stored 21.6 mm radius is the FX half diagonal, so DX and other crops are skipped.
The decoder also skips data it doesn't recognise (another layout, radius or term count, or no correction mode),
values that are clearly wrong (coefficients past ±1, a zoom past ±0.5, a sensor shift over 5% of the frame) and any
mapping that folds back on itself before the farthest corner.
The tests in `nef_lens` cover the polynomial and zoom, the sensor-shift center and refusing other bodies, layouts
and crops. `nikon_in_file_distortion_becomes_a_warp` runs a synthetic Z 6II NEF through the header and full decode
and a DNG export. `nikon_in_file_distortion_is_limited_to_validated_bodies` checks the corpus: the Z f sample gets the
warp and the D5100, D7000, D7500 and Coolpix B700 samples don't.
None of my photos, crops or reference images are committed, only the numbers above.
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -1,12 +1,12 @@
# RAW parity: cameras, formats, demosaic, colour and lens profiles
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Leica Q3 verified by a recorded check of 38 private DNGs; EOS R5 Mark II decoded and verified: CRX QP-map escape, odd-height planes) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Nikon Z 6II / Z 8 / Z f in-file distortion correction; Leica Q3 verified by a recorded check of 38 private DNGs; EOS R5 Mark II decoded and verified: CRX QP-map escape, odd-height planes) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
Raw processing is the deepest single dimension of Lightroom parity: a photographer whose camera
does not decode, or decodes with the wrong colour, cannot switch. This document measures it model by
model. The numbers feed [target-app-parity.md](target-app-parity.md); individual shortfalls are
ranked in [gaps.md](gaps.md). Details per format: [cr3.md](cr3.md),
[raf-compression.md](raf-compression.md), [sony-lens-corrections.md](sony-lens-corrections.md),
[raf-compression.md](raf-compression.md), [sony-lens-corrections.md](sony-lens-corrections.md), [nikon-lens-corrections.md](nikon-lens-corrections.md),
[camera-preview-colour.md](camera-preview-colour.md), [denoise.md](denoise.md).
## Headline
@@ -219,7 +219,7 @@ essentials user (the default look is what they keep).
| Item | Adobe | LightCraft |
|---|---|---|
| Profile database | 3,635 profile files, 56 makers, auto-selected | **0** (Adobe LCPs never used; no database of our own) |
| In-file corrections | ✅ | DNG opcodes (WarpRectilinear, FixVignetteRadial, GainMap…) applied; Panasonic / Leica RW2 distortion (79 photos, 13 bodies, 11 lenses); Sony tag 0x7037 distortion for the ILCE-7RM4A (2 lenses) |
| In-file corrections | ✅ | DNG opcodes (WarpRectilinear, FixVignetteRadial, GainMap…) applied; Panasonic / Leica RW2 distortion (79 photos, 13 bodies, 11 lenses); Sony tag 0x7037 distortion for the ILCE-7RM4A (2 lenses); Nikon NikonNEFInfo distortion (tag `0xc7d5`, about the sensor-shifted optical centre) for the Z 6II and Z f: F-mount lenses on the FTZ within 0.2 px of the camera JPEG, Z lenses within 0.5–1.5 px except the 24-70's wide end (7 px median; `docs/nikon-lens-corrections.md`) |
| Vignetting / lateral CA from maker notes | ✅ | ⬜ |
| Manual distortion, vignetting, CA, defringe | ✅ | ✅ |
@@ -280,6 +280,7 @@ Beyond decoding:
| 2026-10-12 | minor | Hasselblad Lunar verified (a Sony NEX-7 ARW under Make HASSELBLAD, now decoded): 236 verified, 232 preview; an uncompressed Leaf MOS is recognised as a raw (preview only) instead of opening as a greyscale image of its mosaic (#1275) |
| 2026-10-10 | minor | Compressed ORF: pointer to the two Olympus patents that describe the method, as a clean-room starting point |
| 2026-10-12 | minor | PEF without area tags: masked border rows and columns trimmed against the BlackPoint (issue #1271; KP, KF, K-3 Mark III, K-70, K-1, K-1 Mark II, K-S1, K-S2, 645Z); counts unchanged |
| 2026-10-13 | minor | Lens profiles: Nikon in-file distortion (NikonNEFInfo `0xc7d5`) for the Z 6II, Z 8 and Z f, validated on 83 frames from three lenses and the CC0 Z f sample (`docs/nikon-lens-corrections.md`); lens score unchanged |
| 2026-10-11 | minor | EOS R5 Mark II verified (CRX QP-map first literal and limited-length escape, odd-height Bayer planes; 4 CC0 files pinned, QP maps of 33,665 private files): 235 verified, 942 unverified; use-weighted 85% (42% on verified bodies); decoding 77%, RAW readiness unchanged at ~56% |
| 2026-10-10 (later) | major | Model-by-model measurement against Adobe's published list (1,446 models: 234 verified, 943 unverified, 233 preview, 36 unsupported); `docs/raw/` data and re-measure script; use-weighted share 84% (40% on verified bodies); variant matrix; colour and lens sections with scores; ranked next cameras; RAW readiness ~56% from written parts |
| 2026-10-10 | major | Created: formats, makers, demosaic, colour and lens checklists; target counts from the installed Lightroom 9.6 bundle listing |
+35 -10
View File
@@ -60,11 +60,13 @@ those:
header probe reads samples and whose header plan is `Whole`, which is where that plan was untested
before;
- the quick path must be able to show what the whole-file path would. Two container facts are not in
the JPEG, so a file that states one where the bounded header cannot see it declines and the whole
file keeps the preview: a preview colour space of **Adobe RGB** (the whole-file path applies it
through `decode_raw_preview`; a JPEG-only decode has the JPEG's own space, else sRGB — a container
that says sRGB does not decline, since that is what the decode assumes anyway), and the file's own
EXIF orientation when the preview JPEG carries none of its own;
the JPEG. A preview colour space of **Adobe RGB** is read from the planned buffer and handed on
(`PreviewRead::adobe_rgb`), and the quick decode applies it as `decode_raw_preview` does (issue
#1440: a Sony α1 writes Adobe RGB previews, which until then sent every one of them to the
whole-file read — 70 MB for an 8 MB JPEG); a container that says sRGB needs nothing, since that is
what the decode assumes anyway. The file's own EXIF orientation, when the preview JPEG carries none
of its own and the bounded header cannot see it, still declines and the whole file keeps the
preview;
- a format the planner cannot describe — NEF, ORF and PEF probe from samples, CRW/MRW/X3F are found
by scanning — a plan that does not fit the buffer or wants more than 60 % of the file (the bound the
import path's own partial read uses), or a window that does not decode all answer `None`, and the
@@ -75,8 +77,8 @@ route**, reading a median **7.8 %** of their bytes (0.9 %–27.7 %) — by forma
4.1 %, ARW 7.5 %, CR3 8.4 %, DNG 9.7 %, CR2 14.3 % — while the 21 RAFs stay on the header route
(median 4.5 %). The remaining 38 read whole: NEF, NRW, ORF, PEF and Samsung SRW because the planner
does not describe them (the first four say so; an SRW plans to the whole file, and the 60 % budget
then keeps the whole-file read), and the ARWs, CR2s, CR3s and DNGs that declare Adobe RGB or whose
plan wants more than that budget. Across the whole corpus the plan handed on **the same preview as
then keeps the whole-file read), and the ARWs, CR2s, CR3s and DNGs that declared Adobe RGB (these take
the plan route since #1440; the counts here are from before) or whose plan wants more than that budget. Across the whole corpus the plan handed on **the same preview as
the whole file, every time** — and wrote it down: `cargo test -p lightcraft-raw --release --test
sparse_corpus -- --nocapture` prints these numbers. `LIGHTCRAFT_PROFILE=1` says which route answered:
@@ -101,6 +103,9 @@ loupe render of a cold photo finished 174 s after the click, most of it spent be
- `grid::skips_the_real_render(embedded_is_final, from_embedded)` is the decision, unit-tested:
a **cached render** as the stand-in, an **edited photo** and a **raw with no embedded JPEG** all
still render, because their thumbnail must show the pipeline's output rather than the camera's.
- Whether the embedded JPEG can actually be read is only known when the quick job runs, so
`Session::quick_thumb_job` carries the real thumbnail job as its last resort: a raw whose preview
the loader can't read renders the raw instead of leaving the cell empty (issue #1448).
- The thumbnail key still changes with the settings hash, so editing a photo re-renders its
thumbnail through the ordinary background path.
@@ -108,17 +113,37 @@ Measured on the real folder: idle for a minute, the app read 12 previews (**26.9
**zero** raws for thumbnails, against 638 MB of whole-file reads for the same 12 photos before. The
render queue stayed empty.
## The smallest JPEG that covers the box
Many TIFF-based raws keep more than one embedded JPEG. A Sony A7R VI ARW holds a 1616 × 1080 one
(0.3 MB) beside the 9984 × 6656 one (5.6 MB), and Nikon NEF and Panasonic RW2 files have mid-sized
copies too. The loupe stand-in and the grid thumbnail (`embedded_preview_srgb`) decode the smallest
JPEG whose long edge is at least the size asked for (`lightcraft_raw::embedded_preview_covering`).
They take the largest one when no other JPEG is big enough. The plan route picks the same way
(`preview_window` takes the size asked for), so a raw read through the plan gets the JPEG the whole
file would give. A smaller JPEG only counts as a copy of the picture when its aspect ratio is within
1 % of the largest one's and it has the same number of colour components. That rules out letterboxed
thumbnails of another shape and the grey masks an iPhone ProRAW DNG keeps at the picture's shape.
Sources for editing (an unsupported raw's JPEG, `load_embedded_preview`) and the camera-look fit still
use the largest JPEG, so their geometry and colour do not depend on the size level.
## Tests
- `crates/raw/src/preview.rs`: the header-only range matches `embedded_preview` byte for byte; a
clamped prefix, a padded extent, a late pointer, a lying pointer and a non-JPEG window are all
covered.
covered. `a_stand_in_takes_the_smallest_covering_jpeg_of_the_same_shape` covers the picker: a
smaller JPEG that covers the box is taken, the largest when none does, and a thumbnail of another
shape or a grey mask never is.
- `crates/engine/src/files.rs`: on a synthetic RAF with a 200 KB tail, the quick path and the
whole-file path produce identical pixels, and the read stays under half the file. On a synthetic
TIFF/DNG whose preview only its tags locate — the plan path — the same identity holds and the CFA
strip is not read; the same structure without the tag that makes it a raw declines instead of
guessing, and the caller keeps reading it whole. So do a file whose own orientation or Adobe RGB
colour space only the whole-file path can show, while one that states sRGB stays on the fast path.
guessing, and the caller keeps reading it whole. So does a file whose own orientation only the
whole-file path can show. One that states Adobe RGB takes the plan route and shows the same pixels
as the whole-file path (the space changes them: decoded as sRGB they differ), and one that states
sRGB stays on the fast path. A synthetic DNG with a full-size and a half-size JPEG gets, on the plan
path, the half-size one for a box it covers and the full-size one otherwise, with the same pixels as
the whole-file path (`preview_loading_plans_the_smallest_covering_jpeg`).
- `crates/raw/tests/sparse_corpus.rs`: `preview_plan` is asked for **every** corpus file, not only
the ones whose header plan is `Ready`, and the preview it yields must equal the whole file's with
the skipped bytes `0` and `0xFF`. The test prints what each format reads.
+117 -1
View File
@@ -1,13 +1,129 @@
# What's new in LightCraft
## 2026-10-14
### Hard-edged Heal and Clone spots
- A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity
changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged.
## 2026-10-13
### HDR merges keep the lens correction
- An HDR made with Photo ▸ Photo Merge ▸ HDR now keeps the lens correction the photos carry (Sony's, Panasonic's
and Leica's, and the one some DNGs carry, e.g. from drones): Optics ▸ Enable Lens Corrections straightens it as it
does the photos it was made from. HDRs merged before this don't have it; merge them again.
### Portable mode
- A file named `portable.txt` next to the LightCraft program makes it keep its settings, logs and downloaded
models in a `LightCraftData` folder beside it instead of the per-user settings folder (issue #1200), as
PhotoCraft does. If that folder can't be written, the usual folder is used and the log says why.
### Reliability
- A photo whose Defringe amount was stored far past the sliders' 20 (in a damaged catalog or sidecar) no longer
crashes LightCraft each time it renders: the amount acts as 20.
### Reliability
- Importing a preset zip that unpacks to far more than any preset (a "zip bomb") is refused with a message naming
the file, instead of asking for gigabytes of memory, which could end LightCraft.
### Reliability
- A crop of no size (from an agent or a sidecar) no longer gives the Detail view a photo of no size to lay out
(its width over its height was 0 / 0, which stopped debug builds); it shows as a single pixel's colour.
### Reliability
- A crop a hair wide (or of no size), set by an agent or read from a sidecar, no longer crashes or hangs the
render with Clarity, Dehaze, Texture or Highlights / Shadows on: their blurs are held to the picture's size.
- A crop of no size with healing spots or masks no longer crashes the render: samples outside the picture read its
edge.
### Reliability
- A library whose catalog file was damaged (or edited by hand) in a way that still reads no longer crashes
LightCraft when it shows a stack, or hangs it when a folder is moved: on opening, records are put back under
their own ids, stacks keep only photos that exist (at least two), folders inside themselves move to the top,
and new photos never reuse an id in use. What was repaired is written to the log.
### Nikon lens corrections
- Raws from the Nikon Z 6II, Z 8 and Z f now open with the camera's own distortion correction, as Lightroom applies a
built-in profile: straight lines near the edges stay straight and the framing matches the camera's JPEG. It works
with Z lenses and with F-mount lenses on the FTZ adapter, follows the in-body stabilisation's shift of the optical
centre, and can be turned off under Lens Corrections. Vignetting correction from the file comes later.
### Thumbnail cache stays in its folder
- The thumbnail cache no longer follows a shard folder (`thumbs/ab/`) that is a symlink or junction to somewhere
else (issue #1504): trimming or clearing the cache can't delete files outside it, and thumbnails are never read
from or written through such a link.
### Large libraries: Synchronize Folder, import and sidecars start sooner
- Opening Synchronize Folder, an import review or an import no longer makes a table of every photo in the library
before it looks at the first file. In a library of 3.5 million photos that held the window for about 6 s before
a Synchronize scan began (now under 1 s), 2.6 s before an import review (now 0.05 s) and 1.7 s before an import
(now 0.05 s). The scan looks up only the files it finds and the contents it reads, on its own thread.
- Writing one photo's XMP sidecar (Save Metadata to File, or automatically after an edit) no longer works out who
owns every shared sidecar name in the library: 2.4 s a write in that library, now under 1 s.
- Nothing else changes: the same files are found new, already there, moved or missing, and sidecars get the same
names.
### Faces load faster in the People view
- Faces on unedited raws are cut from the camera's preview inside the file instead of developing the raw (issue
#1440): a Sony α1 face reads 8 MB instead of 70 MB, an A7S III face about 1 MB instead of 24 MB, several times
faster on an external hard disk. A face whose preview is too small, and any edited photo, is still developed from
the raw, so it keeps its look.
- Raws whose preview is in Adobe RGB (Sony α1 among others) no longer read the whole file for their grid thumbnail:
only the header and the preview are read, as for other raws.
- Faces are kept in the thumbnail cache at a few fixed sizes, so moving the thumbnail slider no longer renders them
all again.
### Reliability
- A raw file whose black level is stored as "not a number" (or infinity), or a floating-point DNG with such
samples, no longer crashes LightCraft at 1:1 or on export: the bad values read as black.
### Reliability
- Exporting a photo more than 65,535 times as long as it is wide (a one-pixel strip), or opening the Export dialog's
watermark preview for it, no longer crashes LightCraft; the export is capped at the encoders' 65,535 pixels.
### Reliability
- Dragging a Tone Curve point that sits very close to its neighbours (two clicks side by side, or a curve from a
sidecar, a preset or an agent) no longer crashes LightCraft; the point stays between them. Curve points sent to
`develop.curve` are kept in order, and a sidecar curve with a "NaN" or "inf" point is ignored.
### Reliability
- A demo photo whose stored size was damaged (a width of 4294967295) is rendered at its own size instead of
one that overflows.
### DNG camera identity
- DNG export and Convert to DNG no longer duplicate the camera make when the model already
contains it: `Canon EOS R6` stays `Canon EOS R6`, rather than `Canon Canon EOS R6` (the camera
identity subissue in #840). The separate Make and Model tags keep their original values.
This does not change colour matrices, camera calibration or RAW noise rendering.
### Building from source
- The English and Simplified Chinese build guides now describe command-local job and release
profile overrides for memory-constrained builders (issue #1196), including why packaging
recipes can override inherited settings. Default release profiles and application behavior
are unchanged; the guidance does not guarantee a build will fit every 16 GB machine.
### Lightroom catalog import
- A Lightroom Classic catalog whose changes are all still in its `.lrcat-wal` file (a new catalog Lightroom has
open and hasn't written back yet) imports instead of failing with "unsupported SQLite schema format".
### Reliability
- An export enlarged far past the photo's size (20000 typed for 2000 with Don't Enlarge off) no longer ends
LightCraft: an enlargement stops at 512 megapixels, as in Lightroom, and one that needs more memory than the
computer has is refused with the size it would take, before it starts.
### Slider drags on large raws
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you
let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's
previews for up to a second.
- A thumbnail of the photo open in Detail is made from its already decoded preview instead of decoding the file
again.
### Dehaze while zoomed in
- With Dehaze on, panning a photo zoomed to 1:1 or moving a slider while zoomed no longer samples the whole
original again to find the haze colour. On a 64 MP raw (Sony A7R VI) that took ~0.2 s of CPU per pan and per
slider step; a slider step now takes ~5 ms on the GPU.
### Thumbnails of raw files appear sooner
- Grid thumbnails and the first picture in the loupe of an unedited raw now come from the smallest JPEG the camera
stored that is big enough, not always from the largest one. On a Sony A7R VI file a 512 px thumbnail decodes
0.3 MB of JPEG instead of 5.6 MB and is ready several times sooner; Nikon and Panasonic raws with a mid-sized
JPEG get faster too.
## 2026-10-12
### Canon CR2 highlights
@@ -526,7 +642,7 @@
12–32 mm kit zoom was off by about 5 % of the image width at the corners before. Files shot with the correction off
are unchanged; photos imported before this change get it when imported again.
- Canon CR3 raws now develop from their sensor data: lossless RAW and C-RAW, checked sample for sample on the EOS
M50, R100 and R8. CR3 files the decoder can't read yet still open from their embedded JPEG, as before.
M50, R100, R8 and RP (on the RP also in 1.6× crop mode). CR3 files the decoder can't read yet still open from their embedded JPEG, as before.
- Canon EOS R7 C-RAW files develop from their sensor data too, instead of opening from their embedded JPEG. For R7
C-RAWs imported earlier, Photo ▸ Reload from Disk picks up the raw data.
- Canon CRW, Minolta MRW, Sigma X3F, Kodak KDC, Leaf MOS and Epson ERF files that LightCraft can't decode yet