Fitted with `lightcraft-cli calibrate` to 144 usable ILCA-99M2 ARWs (of
150, seven shooting days, Standard creative style, DRO Auto) and their
in-camera JPEGs; aggregate colour statistics only.
On 30 withheld photos (profile fitted to the other 120) mean dE76 against
the camera JPEG goes 5.11 -> 4.87; 25 of 30 improve, the worst
regression is 0.24. All 150 photos stay accepted. The public raw.pixls.us
ILCA-99M2 file, not used for fitting, stays accepted (5.57 -> 5.73).
A corpus test checks that this public sample's fit keeps exactly the
built-in profile's matrix and table; the ILCE-7CR test now shares its
body. LOOK_VERSION 5 -> 6, as the profile changes the fit of this model.
Setting up Synchronize Folder, an import review or an import made a table
of every photo in the library on the session's thread before the first
file was looked at, and naming one photo's sidecar worked out who owns
every shared sidecar name in the library. In a library of 3.5 million
photos (`scan_setup_bench`, release, a folder of 500): 5.8 s before a
Synchronize scan, 2.6 s before an import review, 1.7 s before an import,
2.4 s for each sidecar written.
- A scan keeps the library as it is (photos are shared, not copied) and
looks up the files it finds (`library_files`) and the contents it probes
(`library_hashes`) on its own thread.
- An import looks the library up with its first files
(`ImportJob::look_up_library`), from the library and the options as they
were when it was set up.
- The owners of shared sidecar names are worked out for the photos asked
about (`StemOwners::among`): all five callers name only those photos.
- Synchronize Folder names its photos' sidecars in the scan, after it has
seen that the folder is there.
Now 0.9 s, 0.05 s, 0.05 s and under 1 s. What is found, and how sidecars
are named, is unchanged: the tests hold the owners against the table of
the whole library as it was made before, and the key made without
allocating against the key, for every string of up to six of the
characters that decide it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in panels/dialogs.rs: keep upstream's PQ/HLG HDR curve choice
and route both labels through i18n::tr. Czech for the new strings:
8 catalog entries (import, missing files, GPU notice, mask overlays),
3 format strings (other editor, identical copies) and the HLG label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Photos imported before focus points were read got one only through Reload from Disk, which
hashes whole files on the UI thread. Now:
- lightcraft_meta::focus::read_header reads only what the decoder looks at: IFD0 (Make,
Model), the Exif IFD, the maker note's IFD and the focus tags' values (a JPEG: its segments
up to the Exif one), walked round by round and decoded with the whole-file code from a
buffer of the file's length holding just those ranges. Bounded rounds, bytes (2 MB) and
size (1 GB). One 32 KB read per α1 / A7S III ARW; on 109 real files (83 with a point)
it gives the whole-file read's point for every one.
- A background job (crates/engine/src/focus_scan.rs) reads photos with no point from a
supported body (by the stored camera; nothing else is opened), ARW or JPEG, one at a
time in path order, with a "Reading focus points" activity row (Cancel::Flag). The UI's
per-frame focus.pump gives it the face scan's pace (paused while dragging or typing) and
starts it once per library opening. Commits every 200 photos or 3 s as system ops (no
undo step, no sidecar write), only meta.focus, only on photos that still have none at the
same path. "Looked, none" goes to focus-scanned.bin beside the library, headed by the
decoder's tag so a decoder knowing more bodies starts afresh; unreadable files are retried
at the next opening.
- photo.readFocusPoints {ids?, wait?} (Photo > Read Focus Points) runs it on demand.
Measured cold on a 5400 rpm USB drive: 200 α1 / A7S III ARWs in 1.9 s (~100 photos/s,
32 KB each), so 58,796 raws take about 10 minutes instead of re-reading 2.25 TB.
Soft-delete and remove-folder now unstack in the same undo step as Recently Deleted, so leftover members show 1/2 not 3/3. Restore-from-trash does not restack.
Co-authored-by: Cursor <cursoragent@cursor.com>
`embedded_preview_srgb` (the loupe stand-in and the grid thumbnail of an unedited raw) decoded the
largest embedded JPEG whatever the size asked for: a 512 px thumbnail of a Sony A7R VI ARW decoded
its 9984 x 6656 JPEG (5.6 MB) although the file holds a 1616 x 1080 one (0.3 MB).
`lightcraft_raw::embedded_preview_covering(bytes, min_edge)` returns the smallest of a TIFF-based
raw's JPEGs whose long edge is at least `min_edge` and that is a copy of the largest one: aspect
ratio within 1 % (not a letterboxed thumbnail of another shape) and the same number of components
(not the grey semantic masks of an iPhone ProRAW DNG); else the largest, as `embedded_preview`.
The colour-space handling stays in `decode_raw_preview` (a `covering` argument); an unsupported
raw's editing source and the camera-look fit keep the largest JPEG, so a source never depends on
its size level.
On 120 raws of every make in the corpus at 256 / 512 / 1600 px: wherever the largest JPEG is still
chosen the output is identical byte for byte; where a smaller one is (Sony, Nikon, Panasonic,
Adobe DNG) orientation matches, the size is equal or 1-2 px off, mean difference <= 4.7 / 255.
- Rebased onto #1020: its plan route (`preview_window` -> `planned_preview`) now takes the box size and picks
with `embedded_preview_covering` too, so a TIFF raw read through the sparse plan gets the whole file's JPEG.
Co-Authored-By: Claude <noreply@anthropic.com>
Cover the chord in the photo grid, the square grid and the detail view
(selection narrowed, active photo kept, last edit undo step untouched), a
focused text field keeping the key, rebinding and unbinding in the keymap,
and the command refusing to run without an active photo.
A new library command narrows the selection to the active photo and keeps
that photo active. It is bound to Cmd+Shift+D (Ctrl+Shift+D on Windows and
Linux), listed in the Edit menu and the keyboard shortcut editor, and
disabled while there is no active photo. The binding does not collide with
any existing binding or alias.
Cover Home/End and Shift+Left/Right in both grids (ends of the view, the
loupe, the user's own binding winning), Cmd+O asking for a folder once,
Shift+H toggling the HDR view, and the new keys staying out of the way
while a text field has the keyboard, while the full-screen preview covers
the grid, and while a dialog is open.
In Photo Grid and Square Grid, Home and End make the first or last photo of
the view the active, selected one, and Shift+Left / Shift+Right add the
previous or next photo of the view to the selection. These keys wait while
the full-screen preview covers the grid, in every other view, while a text
field or a dialog has the keyboard, and when the user gave the key to a
command. They are listed under Fixed keys in the keyboard shortcuts dialog.
Open Library now has Cmd+O and Visualize HDR has Shift+H (Cmd+Shift+H stays
the Histogram).
Calibration's header runs section.calibration, now a registered command (no menu
item); section.<id> refuses an id the Edit panel hasn't got instead of closing
the real sections for a phantom one. "Solo Mode" sits with the other So… keys
in the catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Option-click goes through section.<id> {solo: true}, so agents can open just one
section; view.soloSections and section.* refuse flags that are not true / false
(bool_param); toggle_section reuses solo_section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Window > Edit Sections > Solo Mode (view.soloSections {on?}) and a right-click on
any section header turn it on: opening one section closes the others. Option-click
on a header opens just that section once. Kept with the UI state (singlePanel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One slow drag only makes the pair suspect, so a single hidden-window
drag passed even without the FRAME_MS_KNOWN upper bound. Two such drags
followed by a full-size third fail (0.6) when the bound is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split out of #1286 (Dehaze V3) so it can be reviewed and land on its own.
`min_filter` / `max_filter` are separable van Herk / Gil-Werman filters:
three comparisons per pixel per pass whatever the radius, edges clamped,
NaNs skipped like f32::min, a short buffer or empty plane returned
unchanged. `Image::transpose` (row-parallel) does the column pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A busy moment (another app, a backup) could make a slider draft small for
the rest of the session. DragScale keeps timing in wall-clock time, what the
user waits for, but now:
- a (photo, control) pair is slow only after two drags in a row over 20 ms
(1.25x the 16 ms budget); the first makes it suspect, and a drag within
20 ms forgets the verdict;
- a slow verdict lasts 30 s, then a drag tries full size again; a slow
retry doubles the wait, up to 240 s (after WebRTC's CPU overuse detector:
40 s ramp-up delay, doubled after a failed up-switch, capped at 240 s).
The time comes from egui's input clock, which the web build has too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verified on 54 ILCE-7RM6 photos, the CA planes made the render worse: our
demosaic already hides about 70 % of the lens CA (the render shows 0.29x
the raw-CFA CA) while the post-demosaic warp moves R and B by 0.84x the
table, so the corner R/G and B/G CA went from rms 0.032 / max 0.125 px
(off) to 0.052 / 0.219 px (on) at 1440 px; only the 16-35 mm improved.
warp_opcode takes a lateral_ca flag; lens_opcodes passes false, so a Sony
ARW gets the one-plane distortion warp. The CA conversion stays live and
tested (FF and APS-C examples, both lossless corpus files' tables). The
follow-up is to warp the CFA colour planes before demosaic. The doc,
tracker (docs/parity-checklist.md), docs/gaps.md, docs/raw-parity.md and
whats-new say distortion applied, CA parsed and off
by default, vignetting not applied; the one-plane default costs 24 -> 45 ms
in the sample stage at 1920 px (three planes: 112 ms).
Co-Authored-By: Claude <noreply@anthropic.com>
Read the autofocus point from Sony maker notes (FocusLocation 0x2027, FocusFrameSize
0x2037, FocusMode 0x201b, AFAreaModeSetting 0x201c, AFTracking 0x2021 and the area mode
used from the enciphered 0x9402 block), for the ILCE-1 and ILCE-7SM3, whose layout was
checked against ExifTool's output on 333 real ARWs. Manual focus, centre-only Multi
reports and unchecked bodies give no point. The box is stored on the upright photo as
Meta.focus (omitted when unknown; no catalog format change), filled at import and by
Reload from Disk.
View > Show Focus Point (Alt+F) draws it in Detail, Compare and Survey through rotation
and crop; View > Zoom to Focus Point (on by default) makes Toggle Zoom centre on it; the
Info panel shows "AF-C · Wide · Animal Eye Tracking". photo.inspect returns meta.focus.
An ILCE-7RM6 ARW with Sony's distortion table now imports with Enable
Lens Corrections on: Photo::camera_defaults already turns the correction
on for any file whose probe yields embedded lens data, and the ARW
decoder emits it since the previous commit. No new default rule is
needed, so RW2 and DNG keep theirs (on when the file carries a
correction) and stored settings are never rewritten.
Tests pin it: a synthetic ARW imports with the correction on (another
body's tables are not read: off), RW2 / DNG defaults are unchanged, and
an ARW catalogued without lens data or switched off by the user keeps
its settings across a library reopen. The Optics panel's hint drops
"(DNG lens opcodes only)" in every locale.
Co-Authored-By: Claude <noreply@anthropic.com>
vendor::arw_lens_rm6::warp_opcode (was distortion_opcode) turns the CA table
(0x7035: red half, blue half, on the distortion knots) into the R and B
planes of the WarpRectilinear: a colour's source radius is
g (1 + c(g) / 3.36e6) at the green source radius g. The unit is provisional
(borderline measurement, about +-30 % scene bias). Each colour plane goes
through the same fit gate as G, and a table with a value beyond 2e-3 relative
(about 12 px at the full-frame corner; the 54 photos' tables stay below
5.1e-4, i.e. 1724) is refused; without a CA table, or when it is refused, the
warp keeps its single G plane, and CA alone is never applied. fit_radial_warp
now rounds its gate up to the next sample, capped at 1 (a NaN gates every
sample), so the band between the last sample inside the crop and the crop
corner is checked too (RW2 passes 1.0: unchanged).
Co-Authored-By: Claude <noreply@anthropic.com>
New vendor::arw_lens_rm6 reads the raw IFD's lens tables (0x7037 distortion,
0x7032 vignetting, 0x7035 lateral CA, their flags) and turns the distortion
table into a one-plane WarpRectilinear in OpcodeList3, with the conventions
measured on 54 ILCE-7RM6 photos: knots at i/15 of the full-frame crop half
diagonal, S(r) = r (1 + v(r)/15000), frame-filling zoom, centred on the
default crop. Only the ILCE-7RM6 is enabled; vignetting is read, not applied.
The radial least-squares fit is split out as vendor::fit_radial_warp (RW2
results unchanged); its error gate can be limited to the radii the crop
shows, so a padded active area (lossless files) doesn't refuse a fit over
pixels that are never rendered.
It sits next to main's vendor::arw_lens (#498), which holds the ILCE-7RM4A
conventions measured against Sony's exports (unit 16384, table indexed at the
output radius); the two are selected by camera model (disjoint allowlists).
arw::decode chains this module's opcodes after arw_lens::distortion, with the
same active area and the same crop (the DNG-style default crop when the file
has one, which on the ILCE-7RM6 equals Sony's crop tags), and like it only
for CFA data (not the downsized linear-RGB lossless sizes, where the knot
spacing in full-frame pixels would not hold).
The corpus test checks the conversion on the two lossless RawDB files (CC0).
The APS-C one is pinned in xtask RAW_SAMPLES with its sha256; the full-frame
one is not (at most two pins per PR, and #625 pins two Compressed ARW 6.0
files), so its case runs only where the file is present.
RENDER_CACHE_VERSION 62 -> 63.
Co-Authored-By: Claude <noreply@anthropic.com>
A windowed render dehazes with the whole frame's airlight, estimated on a 384 px proxy of the frame
(`local::frame_airlight`). Every window asked for that proxy again, so every pan of a 1:1 view and every
slider step while zoomed sampled the whole original down to it on the CPU: ~200 ms of CPU on a 64 MP raw.
The proxy (taken before white balance) is now kept for the last 4 (source fingerprint, frame, size); white
balance, the dark channel and the percentile still run on it every time. Same pixels: the GPU window of
`render_bench` is byte-identical.
`optics::fingerprint` indexed the first pixel of an empty image; it now hashes only the dimensions then.
render_bench ONLY=window GPU_ONLY=1 on a 64 MP Sony ILCE-7RM6 raw (RTX 4070 Ti SUPER, 4-CPU quota), CPU time:
pan 208-212 -> 9-11 ms, exposure change 200-204 -> 4 ms, NR change 211-217 -> 13 ms.
Co-Authored-By: Claude <noreply@anthropic.com>
Move the forward-map inversion, least-squares fit and solve4 out of the RW2
decoder into vendor::warp_from_forward_map; RW2 calls it in DNG-normalised units.
Co-Authored-By: Claude <noreply@anthropic.com>
- A full set of slow drags forgets only the one remembered longest ago
instead of all 256 (each forgotten pair costs one more slow full-size drag).
- DragScale's doc says a slow drag is remembered for its photo only.
- A host frame time over 1 s (a hidden, occluded or stalled window) counts
as unknown, like no frame time at all, so it can't mark a drag slow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Grid thumbnails looked much more denoised than the loupe. `nr_params` sized
the radii from the output's scale relative to the source buffer it was given,
not the original: a 256-px cell drawn from the 512-px thumbnail level used
half the full-size radius on pixels that each average ~23 sensor pixels, and
below 0.4x (0.35x for colour) the radius stopped shrinking at all. The
guided filter's eps (the variance treated as noise) also stayed at its
full-size value although downscaling divides the noise variance by 1/scale²,
so texture in small previews was flattened as noise.
- `SourceInfo::native_long` (filled by the engine from the catalog's photo
size, kept when the decoder supplies its own facts, carried to web
workers) and `Plan::detail_scale` = output pixels per original pixel
(from `px_per_long`, so a crop no longer changes it).
- `nr_params(s, scale)`: radii x scale with no floor, eps x scale². Below
1 px the smallest blur the box Gaussian makes is blended in by the ratio
of variances (`blur::box_sigma`); a fade under 1% skips the filter.
At scale 1 the parameters are unchanged: 1:1 views and full-size exports
render as before. The GPU takes the same parameters.
- RENDER_CACHE_VERSION 63: cached thumbnails are redrawn.
Measured on a 1536x1024 noisy synthetic scene (tests_nr_scale.rs), error
of the preview against the full-size render scaled down, as a fraction of
leaving NR off, luminance NR 50 at preview scales 0.5 / 0.33 / 0.25 /
0.167 / 0.1 / 0.05: before 0.53 / 0.70 / 1.04 / 2.26 / 3.87 / 6.07, after
0.28 / 0.37 / 0.66 / 0.90 / 0.99 / 1.00. A 256-px render from the 512-px
level now matches one from the original (mean 0.07 LSB vs 0.71 before).
View ▸ Increase / Decrease Grid Thumbnail Size (view.thumbSizeUp / view.thumbSizeDown)
step the bottom-bar slider by 30 pt within its 90–480 range in Photo Grid, Square
Grid and People, as Lightroom Classic's Library Grid keys do; + also enlarges.
Detail, Compare, Survey and Reference ignore the keys, text fields keep them, and
⌘= / ⌘− still zoom the photo. The slider's range is now shared constants.
photo.cycleFlag steps each selected photo's own flag unflagged -> pick ->
reject -> unflagged; photo.increaseFlag / photo.decreaseFlag move it up or
down reject < unflagged < pick without wrapping. One batch undo step, Compare
and Survey act on the active photo, Auto Advance follows, text fields keep
the key. Shown in Photo > Set Flag and Help > Keyboard Shortcuts.
One notch scrolls the photo grid by a third of its visible height and the
filmstrip by a third of its width, as in Lightroom Classic, instead of the fixed
100 points a side panel scrolls. It jumps at once instead of gliding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hue/saturation table of a fitted camera look (ILCE-7RM4A, APS-C ILCE-7RM6, ...) ran
HsvTable::apply pixel by pixel: branches on the data (largest channel, hue sector),
saturating float-to-usize casts and bounds-checked table reads, ~550 instructions and 1.2
branch misses per pixel. CameraColourModel::apply now runs it 16 pixels at a time
(HueSat::apply_lanes -> HsvTable::apply_lanes, structure of arrays): per pixel the same IEEE
operations in the same order, with the branches turned into selects and the table indices
computed as exact f32 integers, so the compiler vectorises everything but the table reads,
powf and the rare rem_euclid, which run lane by lane. DNG profile tables and the other colour
models keep the per-pixel path.
Measured on a Core Ultra 9 185H P-core (busy machine, min of N):
- HsvTable alone, 72x5x5 fitted-look-shaped table, random colours, one thread:
243 -> 106 cycles (57 -> 26 ns) per pixel.
- Colour stage of a full-size load, one thread: 60 MP ILCE-7RM4A 3.80 -> 1.99 s,
26 MP APS-C ILCE-7RM6 HQ 1.88 -> 0.98 s; 4 CPUs: 1.02 -> 0.67 s, 0.56 -> 0.30 s.
Exactness: new tests compare the lanes with the one-by-one path bit for bit (to_bits) on tens
of thousands of colours (every combination of zero, -0, tiny, negative, around 1, huge, NaN and
+-inf components; grey; hues at the 0/360 degree wrap; near-black noise in the restore's blend)
for fitted, value-scaled, 2D, many-turn and degenerate tables, 16 and 5 lanes, and on an image
whose rows aren't a whole number of lanes. Stage fingerprints (decode, AHD, 2560 px and
full-size loads) of 7 Sony raws are identical to origin/main.
Sources: own design.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The D1X/D1H raw SubIFD is 4028x1324 with XResolution 600 / YResolution 300:
the pixel is twice as tall as it is wide. Nothing read that, so the
picture came out about 3:1. Add a pixel aspect to RawImage/RawInfo, read
it from the raw IFD resolution ratio (NEF) and from DefaultScale (DNG),
accept only 1.5..4, and resample the rows after demosaic. developed_size()
reports the stretched size, binning is off for non-square pixels, and the
catalog and lens-profile sizing use developed_size().