docs/superpowers/ held the working spec and plan, which pointed at the
local plan/ folder. The ARW6 module docs now cite the sections of
docs/arw6-compression.md, which gains the facts they relied on from the
spec: the NP field of word 0, the stream header word layout and how the
first half's residual lines are counted.
Co-Authored-By: Claude <noreply@anthropic.com>
`cargo xtask corpus --download` now fetches one full-frame Compressed and
one APS-C Compressed HQ file (about 94 MB) instead of all 12 (about
630 MB); the corpus test checks those two and the lossless-sibling
regression test is gone, since nothing would download those files.
docs/arw6-compression.md keeps the full table with source paths and
SHA-256, says which two xtask fetches and how to fetch the rest with
`curl -L`, and records the CC0 1.0 statement of RawDB's set record
(accessed 2026-10-10).
Co-Authored-By: Claude <noreply@anthropic.com>
The RawDB entries in xtask carry their SHA-256 as the third field, which
`cargo xtask corpus --download` checks; the corpus test keeps only the
sensor sum checks.
Co-Authored-By: Claude <noreply@anthropic.com>
A notched wheel (line/page units, no modifiers) zoomed nothing; only
double-click and pinch/Ctrl+wheel did. Trackpad scrolls still pan.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, model-by-model RAW coverage against Adobe's camera list (docs/raw/), and format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reset Edits rebuilt a photo without a default-preset look from DevelopSettings::default(),
so a raw imported with capture sharpening 40 and colour noise reduction 25
(DevelopSettings::for_raw) came back with both at 0, and with its embedded lens
corrections off. It now starts from the photo's own built-in defaults
(Photo::camera_defaults, the same as import) and keeps the white balance of its
current as-shot reference, as before. Rendered files are unchanged (their defaults
are DevelopSettings::default()). Reported in #763's notes.
Previews looked more saturated than the exported files on Mac screens. The
pixels are the same: preview and export match to within one level on the CPU
and GPU paths, and exports embed a correct sRGB profile. But wgpu leaves the
window's Metal layer without a colour space for sRGB surfaces, and Core
Animation shows such a layer without colour matching ("If nil, no
colormatching occurs", CAMetalLayer.h). On a P3 screen the sRGB previews
were therefore shown too saturated, while Preview and Photos convert the
exports properly.
While no monitor profile is chosen in Settings > Display, the desktop app now
shows previews through the profile of the screen the window is on
(NSWindow.screen.colorSpace.ICCProfileData, safe objc2 calls), through the
existing display-profile path. It is read again about once a second, so
moving the window to another screen is followed. A chosen profile still wins;
"Use Screen Profile" goes back to it. app.displayProfile reports it as
path: null, screen: true. Other platforms and headless sessions are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The highlights/shadows base (fast guided filter of log luminance, eps 0.35)
followed thin high-contrast detail, so the per-pixel shift flattened twigs,
lines and texture in bright areas (and dark ones under Shadows); Exposure
kept them.
This changes how every existing edit with Highlights or Shadows renders, so
it ships as a new rendering process (docs/process-versions.md):
- Process::V2 (version 2) is added and made LATEST. New photos and Reset get
it; existing edits stay on V1 until Update to Current Process. Settings
saved without the field still load as V1.
- From V2 the base is the same guided filter of a separable median of log
luminance (radius 0.15 % of the long edge, <= 24 px): fine detail takes its
surroundings' shift and keeps its contrast, region steps stay put (no new
halos). V1's base is untouched: local::tone_base_detail is 0 under V1 and
tone_base then is exactly guided_fast(l, sigma, BASE_EPS), as before.
- CPU (local::prepare) and GPU prepare (median_h / median_v kernels, bit for
bit with the CPU median) both branch on it via PlaneSigmas::base_detail.
- The base plane's cache key is now (sigma, process, median radius)
(local::BaseKey), so a warm cache never serves one process the other's
base. plane_at is shared with the GPU crate.
- finish::base_tone: V2 uses V1's tone map.
Tests: V2 keeps twig/local contrast (>= 0.9 of Exposure's) with no halo; V1
plane sigmas have no median and its base equals guided_fast bit for bit; V1
and V2 render alike without Highlights/Shadows; warm-cache renders across
V1/V2 switches equal fresh ones (fails if the key drops the process); GPU
matches CPU on both processes at 720 and 2400 px, cached and not.
Pentax 645D 2401 at 4000 px: main and V1 renders bit-identical (plain,
Highlights -100, Shadows +100). Highlights -100 bright rel. detail
0.095 (V1) -> 0.147 (V2), Exposure -1: 0.124; Shadows +100 dark rel. detail
0.126 -> 0.148.
RENDER_CACHE_VERSION 54 (adjust at merge).
A keyword made from Keywords' + is shown (the section and its parents open); a
press on a header button never starts a section drag; a section dragged to the
top edge scrolls past a pinned header again (only a drag picked up by the
pinned header waits until it has left it). Tests that could not fail are
replaced, and the parity rows say what the code does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Albums' + is no longer a special case of the header: header_button says which
section has a button and whether it acts or opens a menu, and the frame draws
it in the header and its pinned copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every dialog but Import draws its options in a vertical scroll area capped at the
window height minus the title bar, frame and button row (measured on the previous
frame), so on a 1366x768 screen the Export dialog's title and Cancel / OK stay on
screen. Dialogs that fit keep their size. Lists that scroll on their own (What's
New, All Metadata, shortcuts, smart-album rules, credits) get shorter first rather
than scrolling inside a scrolling dialog. Import keeps its resizable window.
An album or photos released on the pinned Albums header went to the row
scrolled under it. Folding from the pinned header keeps the sideways scroll,
its hand-off to the next frame can't go stale, and a section picked up by the
pinned header doesn't start scrolling at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Their confirm does nothing, so they join About, Shortcuts and Settings in the
footer's informational list: one Close, no Cancel / OK. System Info keeps Copy
to Clipboard; Esc still closes them.
Pinned header at the top of the sidebar, hairlines between sections, the fold
chevron in the rows' triangle column, a count on folded sections, fainter zero
counts. From review: a move nobody could see is no move, ui.set refuses
`sidebar` together with `collapsedSidebar` and unknown names in the latter.
Translations and What's New for the sidebar work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
view.sidebarMoveSection; a section with nothing to list stays with a note
instead of disappearing (so every move is one you can see). ui.set reads
`sidebar` strictly and still takes the older collapsedSidebar list; an album
folder's triangle and row can no longer toggle twice in one click.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
view.sidebarSection / view.sidebarReset; the same list opens from a section
header's and the My Photos title's context menu. A new album brings a hidden
Albums section back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Albums, Local, By Date, Folders and Keywords are drawn by one section frame from
SidebarLayout (order, hidden, collapsed per section), which is whole whatever a
settings file says. The folded sections of older settings files stay folded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a 2015 MacBook Pro (Intel Iris/Metal), 0.5.0 never opened its window, even with
LIGHTCRAFT_GPU=0: wgpu's indirect-call validation builds an internal compute pipeline when the
window's device is created, Metal's compiler fails on it ("Compiler encountered an internal
error"), the device is lost and eframe exits. That validation only guards indirect draws and
dispatches, which neither LightCraft nor egui issue.
backend::instance_flags clears InstanceFlags::VALIDATION_INDIRECT_CALL for every instance
LightCraft creates (the window's, Gpu::new and the denoise device), unless
WGPU_VALIDATION_INDIRECT_CALL asks for it. Tests: the flag rule, and that the window's instance
descriptor goes without it.
Since #281, probe takes a Sony file as a raw only with raw evidence, and
two layouts have none of the kinds has_raw_ifd looks for: SRF (DSC-F828,
an RGB-looking IFD0 of 14-bit samples) and the DSLR-A100 (raw data
addressed by SubIFDs). Both were "not a readable photo". For Sony, IFD0
samples of 12 or 14 bits, or model DSLR-A100 with an old-style JPEG IFD0
(compression 6) and SubIFDs, now count too; an 8- or 16-bit camera TIFF,
or an exported A100 TIFF with a SubIFDs thumbnail, stays an image. The public F828 and
A100 samples open through their previews as on main 5436951; 133 other Sony files unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stationary hold recorded one pointer sample per frame and each sample laid
another dab at the same spot, so flow accumulated with hold time and the
feathered edge hardened. brush_dabs now lays no dab for a sample that hasn't
moved from the last one, and the canvas no longer records repeated samples at
the same position. Moving strokes and passes back over an area are unchanged.
While the button was held the Detail view drew one hard tinted circle per
pointer sample, so a fast stroke showed separate dabs; the committed stroke
interpolates dabs a quarter radius apart with feather and flow. The live
stroke now uses the pipeline's dab placement (push_dabs, shared with
brush_dabs), dab falloff and flow accumulation in an incremental screen
buffer (crates/ui-egui/src/brush_live.rs): each frame stamps only the new
samples' dabs and uploads only the rectangle they touched. push_dabs also
caps the dabs of a hostile segment instead of looping without bound.
Return presses OK in Choose Edit Settings to Copy and Paste Selected
Settings when no widget has the keyboard focus (a Tab-focused button or
checkbox takes Return itself, as egui does). Esc already cancelled them;
tests now cover both keys.
The R5 Mark II's APS-C crop-mode files (5376 x 3574) state an IAD1 active area of x 132..5243,
but columns 132..~256 sit at the black level and the image runs to the right edge (measured on
raw.pixls.us 7883/7884), while their recommended crop, x 272..5359 (5088 x 3392, the camera JPEG's
size), lies on the image. The recommended crop didn't fit inside the stated active area, so the
whole active area was kept: a black band down the left and ~130 image columns lost.
When the recommended crop doesn't fit inside the stated active area, it is now the valid area.
On 7883 the render's high-pass correlation with the camera JPEG goes from -0.13 to 0.99. The
active area and crop of the other 108 decodable CC0 CR3s on raw.pixls.us are unchanged.
RENDER_CACHE_VERSION 49.
Raws without matrices in the file (ARW, NEF, RW2, RAF, CR2, CR3, PEF, SRW,
ORF) were all edited on the relative white-balance scale (6500 K / 0 = as
shot), even when lightcraft_raw::spectral has measured matrices for the
camera. Those 52 bodies (ILCE-7M3, ILCE-7M4, D850, EOS 5D Mark III...) now
edit in Kelvin like a DNG and like Lightroom.
Design:
- One decision on both sides: spectral::find_camera("Make Model") backs
catalog::relative_wb_camera (Photo::relative_wb); files::WbScaleOf decides
the same from the decoded file. A corpus test asserts they agree.
Photo::fitted_look keeps the old predicate for smart previews.
- The matrices only convert: As Shot is the file's multipliers read through
the model's ColorMatrix1/2 (spectral::as_shot_white_through). The look
(JPEG fit / profile / spectral fallback) is unchanged, and the slider
adapts from that white with Bradford. The camera-space rebalance stays
reserved for files with their own matrices.
- Presets resolve to their Kelvin values (WbMode::preset, sources cited).
- The Color panel shows the white balance in effect (effective_wb).
- XMP: crs::Target::for_file takes the camera; matrix cameras read
crs:Temperature as Kelvin (marked "scale": "kelvin"), others keep the
#510 as-shot shift.
Migration (no look changes):
- WhiteBalance.scale: WbScale { Legacy (absent in JSON), Kelvin }. A
white balance without a scale keeps its old meaning. On a spectral-Kelvin
raw (SourceInfo.legacy_relative_wb) the pipeline composes the old
adaptation (6500 -> value) with the new one (as shot -> ...) in Bradford
cone space (local::legacy_white). Von Kries scalings compose exactly, so
the matrix matches to f32 and the pixels are identical
(pipeline legacy_relative_white_balance_renders_as_before_on_a_kelvin_raw,
media old_relative_white_balance_on_a_kelvin_camera_keeps_its_look...).
- Every white-balance writer (develop.set/adjust/quickAdjust/wb/wbPick/
reset) first runs local::normalize_wb: a legacy value becomes the Custom
Kelvin value with the same look and is marked Kelvin. Partials without a
scale (old presets, develop.merge) stay legacy.
- Catalog format 7: Op::SetAsShotWb replaces the 6500/0 as-shot recorded
by older imports when the file is next decoded or reloaded (system op,
not an undo step). Format-version test updated.
- The web wire carries legacy_relative_wb.
Docs: parity (LR-EDIT-COLOR-TEMP), camera-preview-colour, xmp-interop,
lightroom-catalog-import, control-protocol, mcp, whats-new.
white_from_data anchored the plateau search on the absolute maximum, so a
handful of stray samples above saturation (Canon EOS 70D CR2: 83 of 20.6M,
up to 14426 vs. a plateau at 13583) made it fall back to 2^bits-1. The
image rendered ~0.3 EV dark and clipped highlights were never seen as
clipped. Skip up to half a plateau's worth of strays when locating the top.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
develop.set / develop.adjust / develop.quickAdjust / develop.resetControl on crop.angle
(MCP set_develop, the control channel, lightcraft-cli render --set) and crop.set {angle}
now turn the crop as crop.straighten does, through the new CropGeometry::with_angle:
kept if it still fits, else the largest crop of its aspect inside the rotated photo.
Before, they left the full rect, and the export took in smeared edge pixels in the corners.
Thai folder and file names drew boxes: the UI fell back only on craft-fonts' CJK faces, so a
Thai face (craft-fonts #15) or the Arabic one already there was never used. fonts::ui_fallback
keeps the CJK faces first, in their per-language order, then appends every face of another
non-Latin script, whatever the interface language (a Latin-only face is no fallback: Inter covers
Latin). theme.rs builds the Proportional, SemiBold and Monospace families from it.
Tests: the fallback order on synthetic faces; with a craft-fonts build that has a Thai face,
every UI family falls back on it, and the shipped Thai and Arabic faces have their script's glyphs.
The interface ships in nine languages but the README only read in English, so a
Chinese reader met the project in a language it does not have to use.
README.zh-hans.md follows README.md section by section (569 lines, 27 headings,
7 tables, 8 code blocks), keeping every link, image, command, shortcut and
identifier byte-identical; only prose, alt text, code comments and table headers
are translated. Terminology follows the shipped interface catalog
(crates/ui-egui/locales/zh-hans.json) so the README and the app agree:
曝光, 白平衡, 颜色混合器, 胶片窗格, 留用, 正方形网格, 垂直校正.
Both files gain a language switcher at the top.
The D4 records no CropArea. Its 4992 x 3292 data keeps 2 columns of optical
black left of the image and 42 right of it (then 8 empty ones), which showed
as purple stripes on both sides; the camera's own 4928 x 3280 JPEG sits at
(8, 6). Green saturates at 15546-15672 while red and blue run on to 16383,
so the data's own top left clipped highlights magenta; one level of 15520
keeps them white without moving the white balance.
Measured on 148 uncompressed 14-bit NEFs from one body by aligning the
sensor data with the embedded camera JPEG.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>