mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 04:57:22 -06:00
Merge remote-tracking branch 'upstream/main' into feat/system-appearance
# Conflicts: # 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
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# Long-running tasks and the activity stack
|
||||
|
||||
Everything that takes a while runs off the UI thread and shows up in **one** place: the activity stack in the
|
||||
top-left corner under the top bar (issue #345). Each task has a row with its name, a progress bar (a sweep while
|
||||
the amount of work isn't known), the count and what it's working on, and ✕ if it can stop. Quitting asks first while
|
||||
a task that can stop is running. Agents see the same list through `activity.list` / `activity.cancel`, and
|
||||
`ui.inspect` reports it as `activity`.
|
||||
|
||||
The registry is `lightcraft_engine::activity` (`crates/engine/src/activity.rs`, `Session::activity`). The stack is
|
||||
`crates/ui-egui/src/panels/activity.rs`. Every job that can last more than a moment uses it; **don't add a progress
|
||||
window, panel or progress toast of your own**. A dialog may still show progress inline (the Synchronize Folder
|
||||
dialog counts files while it scans), and a toast at the end saying what happened is fine.
|
||||
|
||||
## Adding a task
|
||||
|
||||
```rust
|
||||
use lightcraft_engine::activity::{Cancel, Unit};
|
||||
|
||||
// when the job starts, with the cancel flag the worker already checks between files
|
||||
let guard = app.session.activity.start("export", "Exporting", Cancel::Flag(cancel.clone()));
|
||||
guard.progress(0, total as u64);
|
||||
|
||||
// every frame (or from the worker, through `guard.handle()`)
|
||||
guard.progress(done as u64, total as u64);
|
||||
guard.detail(&file_name);
|
||||
```
|
||||
|
||||
- **Keep the guard for exactly as long as the job runs.** Put it in the struct the UI polls (`ExportTask`, `ScanTask`,
|
||||
`SyncRun`…), or move it into the worker thread. Dropping it removes the row, also when the worker panics, so a
|
||||
dead job never leaves a stuck bar. A worker that only reports progress takes a `TaskHandle` (`guard.handle()`,
|
||||
`Clone + Send`).
|
||||
- **`kind`** is a stable camelCase id that agents filter by (`export`, `import`, `scan`, `sync`, `previews`,
|
||||
`download`, `faces`…). **`label`** is a short English name in sentence case ("Building previews"). The stack shows it
|
||||
translated, so add it to every catalog in `crates/ui-egui/locales/`.
|
||||
- **Progress:** `progress(done, total)`. A `total` of 0 means the amount of work isn't known yet, and the bar sweeps.
|
||||
`set_unit(Unit::Bytes)` makes the count read "12 of 340 MB"; `Unit::Percent` reads "40 %". The default `Unit::Count`
|
||||
reads "3 of 25".
|
||||
- **Detail:** what the job is on (a file name, a model's name). Detail set by engine code is a name, never a sentence.
|
||||
UI code may set translated text.
|
||||
- **Cancel:**
|
||||
- `Cancel::Flag(Arc<AtomicBool>)` adopts the flag the job already checks. The row's ✕, `activity.cancel` and the
|
||||
job's own Cancel then all set the same atomic.
|
||||
- `Cancel::Yes`: the job polls `guard.is_cancelled()` itself.
|
||||
- `Cancel::No`: the job can't stop; it gets no ✕ and doesn't make Quit ask.
|
||||
- `set_cancellable(false)` for a phase that has to finish once started (a Lightroom import adding its photos).
|
||||
- **Quiet periodic work** shows no row: the auto-import folder listing runs every few seconds through
|
||||
`tasks::spawn(.., None, ..)`.
|
||||
|
||||
## Examples in the code
|
||||
|
||||
| Job | Where | Shows how to |
|
||||
|---|---|---|
|
||||
| Export, contact sheet | `crates/ui-egui/src/export_task.rs` | adopt the job's flag; update the row each frame |
|
||||
| Build Previews | `crates/engine/src/cmd/previews.rs` | move the guard into the worker thread |
|
||||
| Model downloads | `crates/engine/src/face_download.rs` | report progress through a `TaskHandle` in shared state |
|
||||
| Lightroom catalog import | `crates/ui-egui/src/lightroom_import.rs` | stop being cancellable for a phase that must finish |
|
||||
| Face scan, AI Denoise | `crates/engine/src/cmd/face_recognize.rs`, `crates/engine/src/denoise.rs` | a continuous job with no ✕, kept in session state |
|
||||
| Find Missing Photos | `crates/ui-egui/src/tasks.rs` | a generic background task with a row (`kind: Some(..)`) |
|
||||
|
||||
## Testing
|
||||
|
||||
Assert the row (`session.activity.list()`: kind, label, total, `cancellable`), that `activity.cancel(id)` stops the
|
||||
job, and that the row is gone once the job ends. `crates/ui-egui/src/tests_activity.rs` has the helpers, and the
|
||||
job-specific tests show the pattern (`tests_sync.rs`, `export_task.rs`, `tests_denoise.rs`…). A row only appears
|
||||
on screen once its task is 0.5 s old, so tests that click its ✕ wait for `activity:cancel:<id>` first.
|
||||
@@ -99,6 +99,49 @@ Measured before → after (mean L* and CIE76 ΔE at 200 px): on the 24 ILCE-7CR
|
||||
|
||||
Picture styles other than Standard are lowered to the Standard curve too where it is darker; Lightroom ignores them as well. Nikon Active D-Lighting, Canon Auto Lighting Optimizer and Fujifilm D-Range settings are likely to have the same effect and are not handled yet. Render cache version 23 redraws thumbnails; smart previews built before keep the brightened tone curve in their header until rebuilt.
|
||||
|
||||
## Spectral camera matrices and the order of precedence
|
||||
|
||||
For 52 camera models LightCraft has colour matrices fitted to the camera's measured spectral sensitivities
|
||||
(`crates/raw/src/spectral.rs`, generated into `spectral_table.rs` by `tools/spectral_camera_table.py` from
|
||||
[rawtoaces-data](https://github.com/AcademySoftwareFoundation/rawtoaces-data) v1.1.0, Apache-2.0): 22 Canon, 11 Nikon,
|
||||
11 Sony, 3 Fujifilm and 5 others, plus the other names rawtoaces-data lists for them (EOS Rebel T3i and Kiss X5 for
|
||||
the EOS 600D; bodies it lists as sharing a sensor, such as the GFX100S with the GFX 100). Per model the generator fits,
|
||||
under CIE Standard Illuminant A and under D65, a white-preserving 3×3 from white-balanced camera RGB to XYZ by the
|
||||
least mean CIE76 error over rawtoaces-data's 190 training reflectances, and writes DNG `ColorMatrix1/2` and
|
||||
`ForwardMatrix1/2`. The existing DNG colour model then interpolates them for the file's as-shot white, which the
|
||||
vendor white-balance multipliers give through the same matrices. No number comes from Adobe or from a raw decoder.
|
||||
|
||||
A raw starts from the first of these that is available (`camera_preview::starting_colour`):
|
||||
|
||||
1. the file's own colour matrices (DNG);
|
||||
2. a camera profile for its model (bundled or local), with tone and chroma fitted to the file's JPEG;
|
||||
3. the file-local fit to its own JPEG described above;
|
||||
4. the model's spectral matrices, with LightCraft's default tone curve and no JPEG fit;
|
||||
5. the neutral fallback.
|
||||
|
||||
The spectral matrices only replace the neutral fallback: a photo whose JPEG fit is accepted (with or without a
|
||||
profile) renders exactly as before, and a covered model takes its spectral matrices only when that fit fails the
|
||||
gates or the file has no usable JPEG. White balance stays relative to the as-shot look for every raw without matrices
|
||||
of its own, as before, so the catalog's `Photo::relative_wb` and the controls are unchanged. `lightcraft-cli
|
||||
calibrate` still pools the uncorrected camera colour of covered models, so a profile can be fitted for them.
|
||||
|
||||
No colour-difference measurement of the spectral matrices against the camera JPEG, or against the file-local fit,
|
||||
is recorded here yet. Whether they should come before the file-local fit for these models is open until one is.
|
||||
|
||||
## Smart previews and the look version
|
||||
|
||||
A smart preview keeps the result of the fit twice: the colour matrix is baked into its pixels and the tone and chroma curves are stored in its header. When the fit changes, a preview built earlier keeps the old look until it is rebuilt, and the render cache version does not reach it (renders are keyed on the library's files, proxies are files of their own).
|
||||
|
||||
The fit therefore has a version, `LOOK_VERSION` in `crates/engine/src/camera_preview.rs`, and every smart preview records the version that wrote it in its header line (`look_version`). **Bump the constant in any change that makes the fit give a different result for the same file**: the matrix, hue/saturation, tone or chroma fit, their gates and fallbacks, or how a camera profile feeds them. Changes that leave the fitted look alone don't need it. A header without the field (everything built before it existed) counts as version 0, and so does a value that is not a non-negative integer; a value from a newer build is left alone.
|
||||
|
||||
A proxy older than the constant is brought up to date by the existing Build Smart Previews path (`smart_run`), which also repairs damaged proxies:
|
||||
|
||||
- only proxies of raws that take the per-file look (`file_local_look`: ARW, NEF, RW2, RAF, CR3, CR2, PEF, SRW, ORF…) go stale; the fit never reaches JPEG, PNG, TIFF or DNG proxies, which are never rebuilt for it;
|
||||
- when the library is opened and its smart previews folder has not been checked at this `LOOK_VERSION` (the `look-version` marker file in the folder), a background thread looks at those proxies, rebuilds the stale ones from their originals and then writes the marker, so the scan runs once per version bump, not at every opening (no proxy is created, nothing is shown, the UI never waits on a drive);
|
||||
- Build Smart Previews does the same for the photos it is run on (`refreshed` in its result), whatever the marker says;
|
||||
- when the original can't be read (offline drive), the proxy and its curve are kept exactly as they are (`staleKept`). The old version in its header is the mark: the next Build Smart Previews with the original online rebuilds it. A rebuilt proxy carries the current version, so nothing is rebuilt twice. A proxy is only ever rebuilt from the original, never from another proxy;
|
||||
- the two never write the same proxy at once: each proxy is checked and written under one lock.
|
||||
|
||||
## Nikon crop and preview colour metadata
|
||||
|
||||
Nikon maker-note `CropArea` (0x0045) supplies the default `[left, top, width, height]` crop. The decoder validates the rectangle against the active sensor area and falls back to that area for missing or invalid values. The CFA origin is unchanged; cropping follows demosaicing.
|
||||
|
||||
@@ -30,7 +30,7 @@ mode ([mcp.md](mcp.md)) is a thin layer over this channel. Implementation:
|
||||
| `engine.commands` | — | Engine + UI commands: id, label, menu, shortcut, params doc, enabled |
|
||||
| `ui.menu.list` | — | Menu entries (flat: id, label, menu path, shortcut, enabled) |
|
||||
| `ui.menu.tree` | — | The menu bar as shown (File … Help): items `{id, params?, label, shortcut?, enabled, checked?}`, separators, submenus — the model behind the native macOS menu bar and the in-window menus |
|
||||
| `ui.inspect` | — | UI state, window, `loupe` (`source`, and `region: {full, window, pending}` when a zoomed view also renders the window on screen at no more than 100 % — `full` is the frame it is cut from, `window` its `[x, y, w, h]` — and `regionBefore` for the Before side of a Before/After view), canvas/image rects, `scroll: {grid, filmstrip}` (scroll offsets in points, `null` until drawn), active photo, selection, perf (`frameMs` = layout, `logicMs` = per-frame logic before it, `updateMs` = both, `maxUpdateMs`, `fps`, render queue …, `gpu` = adapter in use, `gpuReason` = why renders don't use the GPU, `gpuFallback` = latest render redone on the CPU and why — see `docs/gpu-pipeline.md`), status, memory (bytes per cache, see `library.memory`; plus stage caches — `budgetBytes`, `trimmed`, `sharedSourceBytes` (the one device copy of a big original) — and textures), `export: {running: {total, done, current} \| null, last}`, `notices` (warnings waiting to be shown, e.g. a damaged settings file; OK = `button:noticeOk`), `quitPrompt` (why quitting was stopped: unsaved changes; `button:quitRetry` / `button:quitAnyway` / `button:quitCancel`), `import: {done, total, imported, cancelled} \| null` (an import runs on a worker thread), `tasks` (other background work: `Find Missing Photos`, `Auto Import`), `fileDialogs` (commands waiting on a native file dialog, which runs off the UI thread; they run again with the answer when it closes), `copied` (the text the UI last put on the clipboard, `null` until something is copied) |
|
||||
| `ui.inspect` | — | UI state, window, `loupe` (`source`, and `region: {full, window, pending}` when a zoomed view also renders the window on screen at no more than 100 % — `full` is the frame it is cut from, `window` its `[x, y, w, h]` — and `regionBefore` for the Before side of a Before/After view), canvas/image rects, `scroll: {grid, filmstrip}` (scroll offsets in points, `null` until drawn), active photo, selection, perf (`frameMs` = layout, `logicMs` = per-frame logic before it, `updateMs` = both, `maxUpdateMs`, `fps`, render queue …, `gpu` = adapter in use, `gpuReason` = why renders don't use the GPU, `gpuFallback` = latest render redone on the CPU and why — see `docs/gpu-pipeline.md`), status, memory (bytes per cache, see `library.memory`; plus stage caches — `budgetBytes`, `trimmed`, `sharedSourceBytes` (the one device copy of a big original) — and textures), `export: {running: {total, done, current} \| null, last}`, `notices` (warnings waiting to be shown, e.g. a damaged settings file; OK = `button:noticeOk`), `quitPrompt` (why quitting was stopped: tasks that would be cut short are running — `button:quitAnyway` cancels them and quits, `button:quitCancel` keeps working — or unsaved changes: `button:quitRetry` / `button:quitAnyway` / `button:quitCancel`), `import: {done, total, imported, cancelled} \| null` (an import runs on a worker thread), `tasks` (other background work: `Find Missing Photos`, `Auto Import`), `activity` (every long-running task in flight as the activity stack in the top-left corner shows it, same shape as `activity.list`; a row appears once its task is 0.5 s old, ids `activity:row:<id>`, ✕ = `activity:cancel:<id>`, `activity:more` expands past 3 rows), `fileDialogs` (commands waiting on a native file dialog, which runs off the UI thread; they run again with the answer when it closes), `copied` (the text the UI last put on the clipboard, `null` until something is copied) |
|
||||
| `ui.widgets` | `{filter?}` | On-screen widgets `{id, rect: [x, y, w, h]}` (screen points) |
|
||||
| `ui.clickWidget` / `ui.dragWidget` / `ui.hoverWidget` | `{id, count?, button?, fx?, fy?}` (`button: "right"` right-clicks) / `{id, toX?, toY?, dx?, dy?, steps?}` / `{id, fx?, fy?}` | Real egui input on a widget (hover: the pointer rests on it, e.g. for preset/profile previews) |
|
||||
| `ui.move` / `ui.click` / `ui.drag` | `{x, y, count?, button?}` / `{x, y, toX, toY, steps?}` | Raw pointer input, screen points |
|
||||
@@ -46,6 +46,8 @@ mode ([mcp.md](mcp.md)) is a thin layer over this channel. Implementation:
|
||||
| `ui.zoomFactor` | `{factor}` | Set the interface scale (0.5–3; egui's zoom keys are off, Cmd+= / Cmd+- zoom the photo) |
|
||||
| `ui.screenshot` | `{path?, headless?}` | `{path, width, height}` once the frame (with finished renders) is captured. `headless: true` draws the UI on the CPU (no compositor needed); a windowed capture that gets no frame within 2 s falls back to headless automatically |
|
||||
| `engine.execute {command: "app.export", params}` | export params (see `docs/mcp.md`), plus `preset`, `dir` / `path`, `ids`, `background` | Writes the files and returns `{files}`; with `background: true` (what the Export dialog and menus use) it returns `{background: true, total}` at once and the batch runs on a worker thread — poll `ui.inspect` → `export` |
|
||||
| `engine.execute {command: "activity.list"}` | — | The long-running tasks in flight (imports, exports, preview builds, downloads, the face scan…), oldest first: `{tasks: [{id, kind, label, done, total, unit, detail, cancellable, cancelling, ageMs}]}`; `total` 0 = not known yet; `unit` is `count`, `bytes` or `percent`; `label` is English (the app shows it translated) |
|
||||
| `engine.execute {command: "activity.cancel"}` | `{id}` or `{all: true}` | Asks the task (or every cancellable one) to stop at its next file or photo → `{cancelled: n}`; an unknown id (it may have just finished) or a task that can't be cancelled is an error |
|
||||
| `ui.render` | `{id?, size?, path?}` | Render a photo (PNG to `path`), `{width, height}` |
|
||||
| `app.quit` | — | Close the app |
|
||||
|
||||
|
||||
+1
-1
@@ -248,7 +248,7 @@ Turning recognition on starts a scan of the whole library, once, in the backgrou
|
||||
the running ones, so a worker that finishes has its next photo at once.
|
||||
- **Earlier scan speed (a different runner).** A mixed raw and JPEG library of 184 photos (the raws read through their embedded previews, 73 photos searched
|
||||
for faces) took about 29 seconds on that machine at full pace: roughly 6 photos a second, so the first scan of 10,000 photos
|
||||
is a matter of half an hour; after that only new photos are looked at. Settings ▸ Faces shows how many are left, with a progress bar. After you accept a model's terms
|
||||
is a matter of half an hour; after that only new photos are looked at. Settings ▸ Faces shows how many are left, with a progress bar, and the activity stack shows "Finding faces" meanwhile. After you accept a model's terms
|
||||
you are returned to that tab, so the download and then the scan can be watched there; the main window has no status bar.
|
||||
- Opening another library starts a fresh scan state; nothing learned about one library is used in another.
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ All algorithms are our own implementations from published papers (cited in the m
|
||||
- **App:** select the photos, then *Photo Merge ▸ HDR…* (`Ctrl+H`), *Panorama…* (`Ctrl+M`) or
|
||||
*HDR Panorama…* from the photo context menu (or the `dialog.merge*` commands). The dialog previews
|
||||
the merge at ≤ 1024 px and re-runs the preview when an option changes; *Merge* runs the full merge
|
||||
in the background (progress in a toast). The result is written next to the first photo as
|
||||
in the background (a row in the activity stack shows its progress, with ✕ to stop it). The result is written next to the first photo as
|
||||
`<name>-HDR.dng`, `-Pano.dng` or `-HDR-Pano.dng`, imported and selected.
|
||||
- **Commands** (CLI, MCP, control channel):
|
||||
- `merge.hdr {ids?, align=true, deghost=none|low|medium|high, autoSettings=true, stack=false, preview=false, showOverlay=false, previewPath?}`
|
||||
|
||||
+12
-10
File diff suppressed because one or more lines are too long
@@ -0,0 +1,117 @@
|
||||
# Sony embedded distortion corrections
|
||||
|
||||
LightCraft reads the signed 16-sample distortion table in the raw image IFD (`0x7037`) of
|
||||
Sony **ILCE-7RM4A (A7R IVA)** Bayer ARWs. Other camera models are deliberately left uncorrected,
|
||||
even when they contain a table with the same layout.
|
||||
It converts that table into `OpcodeList3` / `WarpRectilinear`, so the existing profile correction control,
|
||||
CPU/GPU optics path, EXIF orientation handling, and DNG export use the same correction.
|
||||
|
||||
This adds **distortion only**. Sony vignetting and lateral chromatic-aberration tables are not decoded.
|
||||
The enabled and independently validated model is ILCE-7RM4A with the FE 24–105mm F4 G OSS and
|
||||
FE 200–600mm F5.6–6.3 G OSS. Linear YCbCr ARWs and non-3:2 camera crops are excluded pending validation. An aspect crop may
|
||||
retain the full-frame radial normalization; the decoder does not guess that relationship. Older files that carry
|
||||
only encrypted correction metadata, different table lengths, and rejected tables remain uncorrected.
|
||||
The camera's distortion Off setting does not erase its table. Newly imported photos use LightCraft's existing
|
||||
embedded-profile default (enabled); the profile correction control can disable or adjust it.
|
||||
|
||||
## Clean-room evidence
|
||||
|
||||
The [ExifTool EXIF tag-name documentation](https://exiftool.org/TagNames/EXIF.html) identifies
|
||||
`0x7037` as 17 signed 16-bit integers. The first value is the count, 16, followed by radial samples.
|
||||
No external photo decoder implementation or Adobe lens profile was consulted.
|
||||
Sony Imaging Edge Edit 4.1 was used only as a black-box reference on local scratch copies.
|
||||
|
||||
The observed inverse mapping, with radius `r` normalized to the default crop's half diagonal, is:
|
||||
|
||||
```
|
||||
f(r) = 1 + interpolate(table, r) / 16384
|
||||
source = center + (output - center) * zoom * f(r)
|
||||
zoom = 1 / max(f(r)) over radii on the rectangular output boundary
|
||||
```
|
||||
|
||||
The border radii range from the shorter half edge / half diagonal to 1. Table knots span 0 through 1.
|
||||
Linear interpolation approximates the observed map closely; we do not claim to reproduce Sony's
|
||||
internal interpolator. A fixed-size weighted least-squares fit expresses source radius as
|
||||
`r * (k0 + k1*r² + k2*r⁴ + k3*r⁶)`. The fit is rejected if radial error exceeds 0.0005 of the
|
||||
half diagonal (about 0.43 px at a 1440 px 3:2 preview), or the mapping folds. Table values are bounded
|
||||
and the input must have precisely the supported signed-short layout. Crop offset and normalization
|
||||
are converted into DNG's active-area coordinate system before creating the opcode. The warp's centre
|
||||
and normalization come from the standard `DefaultCropOrigin` / `DefaultCropSize` when the file has them,
|
||||
not from Sony's crop tags: on the tested A7R IVA files the former start at raw x=32 and the latter at
|
||||
x=0, and the standard origin is the one that aligns with Sony's exports. The image itself keeps the
|
||||
decoder's crop precedence for every Sony body (Sony's crop tags, then the default crop, then the
|
||||
recorded image size), so the framing is unchanged; the opcode's centre is in active-area coordinates,
|
||||
so the warp stays on the optical centre either way. On the A7R IVA that framing still sits 32 raw
|
||||
pixels left of Sony's exports (eight pixels in a 2376-pixel-wide export).
|
||||
|
||||
The main raw-IFD table is important: the similarly named MakerNote table has different values and
|
||||
must not be substituted into this formula. Main raw-IFD and SR2SubIFD values agreed in the test files.
|
||||
|
||||
## Validation
|
||||
|
||||
At 2376×1584, independent SIFT correspondences between Sony distortion On and Off TIFF exports gave:
|
||||
|
||||
| Lens / focal length | Median residual | 95th percentile | Maximum matched radius |
|
||||
|---|---:|---:|---:|
|
||||
| FE 200–600 / 200 mm | 0.043 px | 0.191 px | 0.97 half diagonals |
|
||||
| FE 200–600 / 600 mm | 0.055 px | 0.231 px | 0.95 half diagonals |
|
||||
|
||||
These use the fixed 16384 scale and automatic border framing, not fitted per-image scale or zoom.
|
||||
A median translation below 0.004 px was removed from the residuals. Correspondences do not cover
|
||||
all pixels; these are geometric feature residuals, not photometric equality or a worst-case guarantee.
|
||||
Across 20 sampled RAWs (16 at 24–105 mm, four at 200–600 mm), the polynomial's maximum deviation
|
||||
from the interpolated file table was below 0.12 px at 1440 px. That measures approximation error,
|
||||
not agreement with an independent renderer. Camera JPEG and Apple Core Image comparisons support
|
||||
the wide-lens interpretation but are weaker evidence than same-renderer On/Off pairs.
|
||||
|
||||
Actual LightCraft exports after crop normalization were also compared directly with Sony's corrected
|
||||
TIFFs. At 2376 px wide, median / 95th-percentile feature residuals were 0.35 / 1.04 px at 24 mm,
|
||||
0.15 / 0.40 px at 200 mm, and 0.13 / 0.56 px at 600 mm (median translation below 0.024 px).
|
||||
The uncorrected medians at 200 and 600 mm were 8.63 and 10.82 px respectively. Different colour,
|
||||
sharpening, and demosaicing affect feature localization; these are geometry checks, not colour parity.
|
||||
An isolated desktop run on Apple M1 Metal detected the profile, rendered without GPU fallback or
|
||||
warnings, and exposed the existing Optics controls. Existing library edits are not reset: once the
|
||||
source has loaded, enable lens corrections if they were previously disabled.
|
||||
|
||||
Tests cover signedness/length rejection, zero and malformed tables, independent 600 mm reference
|
||||
geometry, barrel framing, offset crop normalization, both TIFF byte orders, header/full consistency,
|
||||
and preservation through DNG export. No private photographs or reference TIFFs are committed.
|
||||
|
||||
## Camera coverage and regression boundary
|
||||
|
||||
The model check is exact: `ILCE-7RM4` (A7R IV without the A suffix), `ILCE-7M3`, unknown/missing
|
||||
model names, and every other model remain unsupported by this distortion decoder. Matching tag
|
||||
numbers and table lengths do not establish the scale, interpolation or crop normalization on another
|
||||
body. Support can expand after independent geometry validation; do not add model-name prefix matching.
|
||||
|
||||
The original nine Sony samples listed by `cargo xtask corpus --download` were downloaded from its
|
||||
CC0 URLs. The manifest now also includes CC0 samples 4822 (A7R IVA / 24–105 mm) and 3989
|
||||
(A9 II / 200–600 mm), with their SHA-256 hashes recorded alongside the URLs. All eleven files
|
||||
are checked with `sony_embedded_distortion_is_limited_to_validated_models`:
|
||||
|
||||
| Camera | Corpus samples | Raw-IFD table observed | Expected correction |
|
||||
|---|---|---|---|
|
||||
| ILCE-7RM4A | Compressed (4822) | 16 samples | One distortion warp |
|
||||
| ILCE-9M2 | Compressed (3989) | 16 samples | None |
|
||||
| ILCE-7M3 | Compressed, uncompressed | 16 samples | None |
|
||||
| ILCE-7M4 | 14-bit, lossless L/M/S | 11 samples in the 14-bit file; 16 in L/M/S | None |
|
||||
| ILCE-7RM2 | 12-bit uncompressed | 16 samples | None |
|
||||
| DSC-RX100 | One | No raw-IFD table | None |
|
||||
| DSC-RX100M3 | One | 11 samples | None |
|
||||
|
||||
These are **model-boundary regression checks, not new geometric validation of those models**. In particular,
|
||||
refusing the A7 III tables resolves the unverified corner warp identified in review. The lossless M/S
|
||||
files are linear YCbCr and remain excluded independently of the model check. Header and full decode
|
||||
must agree for every sample. Synthetic tests also verify both TIFF byte orders, exact model matching,
|
||||
that the warp is centred on the standard default crop while the image keeps Sony's crop, and that DNG export preserves an accepted warp without adding one to rejected
|
||||
models. The existing independent Sony geometry tests continue to cover the enabled A7R IVA path.
|
||||
Re-exporting the 24, 200 and 600 mm reference files after the model restriction produced byte-identical
|
||||
corrected PNGs to the earlier validated exports. Isolated headless app checks showed the A7R IVA profile
|
||||
controls present and the A7 III profile controls absent, with no notices and completed RAW renders.
|
||||
|
||||
Before enabling another body, compare corrected and uncorrected renders against independent references
|
||||
across multiple lenses/focal lengths and photographs with features near the edges and corners. Record
|
||||
residuals and spatial coverage, and check crop/aspect and compression variants. Embedded JPEGs are
|
||||
useful when the camera actually applied distortion correction; an Off JPEG cannot certify an On warp.
|
||||
Prefer same-renderer Sony On/Off exports when the camera setting or geometry is ambiguous. Table-fit
|
||||
error alone only checks our polynomial approximation, not whether the table's interpretation is correct.
|
||||
+16
-1
@@ -9,6 +9,16 @@
|
||||
- The top bar's appearance button cycles Auto, Light, Dark (a monitor, sun or moon), and View ▸
|
||||
Appearance lists the modes and themes. On Linux, Auto follows the desktop portal's colour scheme
|
||||
as it changes, without polling.
|
||||
|
||||
### Activity stack
|
||||
- Long-running tasks show in one place, top left under the top bar, as in Lightroom Classic (issue #345): imports and
|
||||
folder scans, exports (and the contact sheet PDF), Synchronize Folder, Build / Smart Previews, Lightroom catalog import, Photo Merge, model downloads, the face scan,
|
||||
AI Denoise and Find Missing Photos. Each row has the task's name, a progress bar, the count and what it is working
|
||||
on; ✕ stops the tasks that can stop. Three rows show, then "+N more". The separate export panel and the progress
|
||||
windows and toasts are gone.
|
||||
- Quitting while a task that can be stopped is running asks first ("Quit Anyway" stops it).
|
||||
- Agents list and stop tasks with `activity.list` and `activity.cancel`; `ui.inspect` reports them as `activity`.
|
||||
|
||||
### Smart album rules
|
||||
- The rule editor's field menu is grouped: Rating, Pick Flag, Color Label and Any Searchable Text at the top, then
|
||||
submenus for Source, File, Date, Keywords & People, Description, Camera Info, Location, Size, Develop and Assisted
|
||||
@@ -99,7 +109,7 @@
|
||||
file is gone, photos whose file was renamed or moved within the folder, and XMP sidecars changed by other apps; then
|
||||
choose what to do: import the new photos and relink the moved ones (the defaults), move the missing ones to Recently
|
||||
Deleted, read the metadata updates. A folder on a disk that isn't connected is reported as such. Synchronizing
|
||||
runs in the background with a progress window and Cancel; everything it does is one undo step, and no file on disk
|
||||
runs in the background with a row in the activity stack (✕ stops it); everything it does is one undo step, and no file on disk
|
||||
is touched. Agents use `folder.scanChanges` and `folder.synchronize`.
|
||||
|
||||
### Folder colour labels
|
||||
@@ -164,6 +174,11 @@
|
||||
- Samsung SRW files without compression now open as raws: NX5, NX10, NX11, NX20, NX200, NX210, NX1000, NX1100, EX1
|
||||
and WB2000. The compressed ones (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini) still open from their
|
||||
camera JPEG. Photos already imported pick the change up on Reload.
|
||||
- Raws from 52 camera models (22 Canon, 11 Nikon, 11 Sony, 3 Fujifilm and 5 others, plus their other names) whose
|
||||
camera JPEG can't be fitted no longer open with the neutral fallback: they start from colour matrices fitted to the
|
||||
camera's measured spectral sensitivities, from the Academy Software Foundation's rawtoaces-data. A camera profile
|
||||
and the fit to the photo's own JPEG still come first, so photos that had a fitted look keep it. See
|
||||
[camera preview colour](camera-preview-colour.md#spectral-camera-matrices-and-the-order-of-precedence).
|
||||
- Canon CR2 raws no longer open too dark with blocked-up shadows: the black level was measured over border columns
|
||||
that light already reaches (left of the image area Canon records), 4 % of the range too high on the EOS 6D. It is
|
||||
now measured on the masked columns alone (8 corpus bodies, EOS 40D to 5DS R). On a 6D photo the starting render
|
||||
|
||||
Reference in New Issue
Block a user