LR-IMP-FORMATS: Panasonic RW2 / Leica RWL / Panasonic RAW in every raw format
Decode every raw encoding Panasonic and Leica bodies write, from the DMC-LX1 (2005) to the DC-S1RM2 (2025): compressed formats 4 (most bodies, 2008-2022) and 6 (S1 / S1R / S1H / S5, GH5M2), the prefix-coded strips of format 8 (GH6 and later), packed formats 2, 5 and 7, and the 16-bit words of the 2005-2007 .RAW files. Until now only format 5 decoded; every other Panasonic file opened as preview only. Clean-room: no raw-decoder source was read. Tag names come from tag documentation; every encoding was recovered by black-box analysis of 178 CC0 files from raw.pixls.us (118 bodies), and the module docs note the measurement that settled each rule. Format-8 tags of unknown meaning must carry the values every sample has, otherwise the file is reported as unsupported rather than guessed at. Levels and geometry: formats 4 and 6 store samples 15 above the recorded black level (files without one use 15); white is the active area's saturation plateau (the linearity limits are wrong on extended low-ISO shots, the GF1 and the Digilux 3); the in-camera aspect ratio is the default crop; mapped-out defect pixels are filled from same-colour neighbours; the CFA pattern applies at sample (0, 0). .rwl and .raw files are imported (engine, MCP folder expansion, file dialog, Linux MIME types). One file per encoding joins the never-panic proptest; 11 CC0 files join `cargo xtask corpus`, and corpus_panasonic_encodings checks each encoding's bit depth, black level, crop and CFA layout against the camera's own JPEG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtQmZ6peZXGn3wiqeowV2v
This commit is contained in:
parent
b35487add2
commit
6bd64e4ad4
|
|
@ -258,8 +258,8 @@ lightcraft --control 7980 ~/Pictures/trip
|
|||
## Fast, native, private
|
||||
|
||||
- **Pure Rust, no C.** Our own RAW decoders (DNG, Canon CR2, Sony ARW, Nikon NEF, Fujifilm RAF incl. X-Trans,
|
||||
Panasonic RW2, Pentax PEF, Olympus ORF), our own colour science, our own pipeline. JPEG, PNG, TIFF, WebP, PSD
|
||||
composites and JPEG XL open today.
|
||||
Panasonic RW2 / Leica RWL, Pentax PEF, Olympus ORF), our own colour science, our own pipeline. JPEG, PNG, TIFF, WebP,
|
||||
PSD composites and JPEG XL open today.
|
||||
- **Scene-referred & wide-gamut.** Linear Rec.2020 float internally, Bradford-adapted white balance, gamut mapping
|
||||
instead of clipping, a filmic shoulder for raw and pixel-exact pass-through for JPEGs you haven't touched.
|
||||
- **Resolution-independent edits.** Radii and brush sizes are relative to the image, so a 400 px preview, your
|
||||
|
|
@ -303,7 +303,7 @@ LightCraft is young and moving fast. **Where we honestly stand** (details in the
|
|||
| Profiles (Color, Neutral, Vivid, Landscape, Portrait, Monochrome: our own looks), presets, versions, history, copy/paste/sync settings | ✅ |
|
||||
| Camera colour: DNG files use their own matrices | ✅ · our own calibration for other raws ⬜ (top priority; neutral fallback today) |
|
||||
| Native macOS menu bar (generated from the command registry), control channel + every widget addressable, headless UI snapshots | ✅ |
|
||||
| RAW: DNG, CR2, ARW, NEF (uncompressed + lossless/lossy compressed), Fujifilm RAF (uncompressed, Bayer + X-Trans), Panasonic RW2, Pentax PEF, Olympus ORF (uncompressed); embedded previews for every format incl. CR3 | ✅ · CR3, compressed RAF/ORF decode ⬜ |
|
||||
| RAW: DNG, CR2, ARW, NEF (uncompressed + lossless/lossy compressed), Fujifilm RAF (uncompressed, Bayer + X-Trans), Panasonic RW2 / Leica RWL / Panasonic RAW (every raw format, DMC-LX1 to DC-S1RM2), Pentax PEF, Olympus ORF (uncompressed); embedded previews for every format incl. CR3 | ✅ · CR3, compressed RAF/ORF decode ⬜ |
|
||||
| Detail: sharpening, luminance + colour noise reduction | ✅ · AI Denoise, Super Resolution ⬜ |
|
||||
| Remove / Heal / Clone spots (auto source), Visualize Spots, Red Eye and Pet Eye (auto pupil detection, catchlight) | ✅ · content-aware fill, spot pin editing 🚧 |
|
||||
| Export: JPEG / PNG / TIFF / WebP / AVIF / DNG / original, sizing, file-size limit, output sharpening, naming templates, batch, metadata policy, text or image watermark | ✅ · HDR export ⬜ |
|
||||
|
|
|
|||
22
ROADMAP.md
22
ROADMAP.md
|
|
@ -23,7 +23,7 @@ duplicate Local entries #22, black GPU exports on an Intel iGPU #78).
|
|||
| Dimension | Estimate | What's true today | Biggest gaps |
|
||||
|---|---:|---|---|
|
||||
| **Feature checklist** | 79% | P0 core and P1 nearly complete: import (Add / Copy / Move, templates, devices), library, grid/loupe/compare/survey, every Edit slider, curves, colour grading, masking tools, crop/Upright, heal/clone, presets/profiles, versions/history, sync, export, menus, shortcuts | P1: lens-profile database, content-aware fill (patch synthesis), video playback/trim |
|
||||
| **RAW coverage** (formats people shoot) | ~50% | DNG (all kinds), CR2, ARW, NEF (uncompressed + Huffman lossless/lossy), uncompressed RAF/ORF, packed RW2, PEF; every container's embedded preview (incl. CR3) | **CR3** (every Canon since ~2018), compressed RAF/ORF, RW2 v4, Nikon lossy-after-split, Canon sRAW, HEIC/AVIF. NEFs labelled compressed but stored uncompressed (Z 6 packed 14-bit, D850 12-bit uncompressed). Per-model verification is thin (~40 corpus files vs >1,000 models) |
|
||||
| **RAW coverage** (formats people shoot) | ~55% | DNG (all kinds), CR2, ARW, NEF (uncompressed + Huffman lossless/lossy), uncompressed RAF/ORF, RW2 / RWL / Panasonic RAW (every raw format, checked on 178 files from 118 Panasonic and Leica bodies), PEF; every container's embedded preview (incl. CR3) | **CR3** (every Canon since ~2018), compressed RAF/ORF, Nikon lossy-after-split, Canon sRAW, HEIC/AVIF. NEFs labelled compressed but stored uncompressed (Z 6 packed 14-bit, D850 12-bit uncompressed). Per-model verification is thin (~55 corpus files vs >1,000 models) |
|
||||
| **Colour & image quality** | ~55–65% | Pipeline is complete and fast; GPU path CPU-exact within 1/255 | **No measured camera calibration database**: ARW and NEF have a guarded per-file embedded-JPEG colour estimate (docs/camera-preview-colour.md); other non-DNG raws and rejected estimates use a neutral matrix. Colour fidelity remains incomplete. No lens-profile database. No measured fidelity against Lightroom (tone, highlights, texture/clarity, NR, sharpening are tuned by eye) |
|
||||
| **AI & computational** | ~15–20% | Assisted culling (focus, bursts), auto tone, HDR/panorama merge; subject/sky/background masks as classical heuristics | Real segmentation masks (subject, sky, people, objects, landscape, depth), AI denoise, super resolution, lens blur, generative remove, faces/people, natural-language search. **Blocked on a model strategy** (licensable weights or our own training; pure-Rust inference is feasible) |
|
||||
| **Workflow & library** | ~85% (single machine) | Robust catalog (journal + snapshots, background compaction, crash-tested), 85k-photo libraries stay responsive, Local browsing with automatic cleanup, XMP interop, keywords, smart albums, Move import | Opening an 85k library takes 1.7–4.7 s; no cloud sync (out of scope), no tablet companion (#74, roadmap), shared albums, publish services, tethering |
|
||||
|
|
@ -36,7 +36,7 @@ duplicate Local entries #22, black GPU exports on an Intel iGPU #78).
|
|||
| User | Readiness | What blocks them |
|
||||
|---|---:|---|
|
||||
| JPEG / DNG shooter, single machine | ~85% | Fidelity polish, AI masks |
|
||||
| Nikon / Sony / older-Canon raw shooter | ~65% | Camera colour fidelity and coverage (ARW and NEF preview estimates are only a starting point) |
|
||||
| Nikon / Sony / Panasonic / older-Canon raw shooter | ~65% | Camera colour fidelity and coverage (ARW and NEF preview estimates are only a starting point); Panasonic compacts and kit zooms also miss their embedded distortion correction |
|
||||
| Canon CR3 / Fujifilm / Olympus shooter | ~35% | Their raws open as embedded previews only (CR3: the full-size JPEG with full metadata) |
|
||||
| Lightroom Classic power user | ~45% | Print, Book, Map, publish, tethering |
|
||||
| Relies on AI (masks, denoise) | ~25% | Object / Describe masks via optional SAM 3 (download not yet hosted); no AI denoise |
|
||||
|
|
@ -48,7 +48,7 @@ Priorities, in order. Each points at tracker rows in [`docs/parity.md`](docs/par
|
|||
1. **Camera colour calibration of our own** (LR-PROF-CAMERACOLOR, P0): fit each camera to its own embedded JPEG, use
|
||||
matrices the files carry themselves, then chart shots. Sony ARW and Nikon NEF have the file-local fit (matrix + tone
|
||||
curve from their own JPEG); generalise it to the other makes' raws (RW2, PEF, ORF…), then validate fidelity.
|
||||
2. **Raw formats, clean-room** (LR-IMP-FORMATS, P0): **CR3** first, then compressed RAF / ORF, RW2 v4, NEF
|
||||
2. **Raw formats, clean-room** (LR-IMP-FORMATS, P0): **CR3** first, then compressed RAF / ORF, NEF
|
||||
lossy-after-split, sRAW. Decided 2026-10-05: write our own decoders from prose descriptions (never decoder source,
|
||||
no LGPL dependency); compressed NEF (#86) is the template.
|
||||
3. **Verified camera coverage** (LR-IMP-CAMERA-COVERAGE, P0): a CC0 sample per model in the corpus, each decoded and
|
||||
|
|
@ -84,7 +84,7 @@ hardening (#78), copyright metadata (#51), GPX geotagging (#60), import tag help
|
|||
| M8 | Heal / Remove | content-aware remove (PatchMatch), heal, clone, brush spots, visualize spots, red/pet eye | 6–10 | 🚧 (heal, clone, auto source, visualize spots, red/pet eye ✅; PatchMatch remove ⬜) |
|
||||
| M9 | Presets, profiles, versions, sync | preset browser + amount, create/import presets, profile browser, versions, history, copy/paste/sync settings | 5–8 | ✅ |
|
||||
| M10 | Export & share | export dialog (JPEG/PNG/TIFF/DNG/AVIF/JXL/original), sizing, sharpening, metadata, watermark, naming, batch jobs, XMP sidecars, HDR export | 6–10 | 🚧 (all formats incl. DNG/original, sizing, presets, background jobs ✅; JXL encode, HDR export ⬜) |
|
||||
| M11 | RAW II | CR3, RAF (X-Trans), ORF, RW2, PEF, SRW, 3FR, IIQ + long tail; camera calibration DB; HEIC/AVIF/JXL import | 20–35 | 🚧 (RAF uncompressed, RW2 packed, PEF, ORF uncompressed ✅; **camera colour calibration** 🚧 (guarded ARW and NEF preview fitting; measured database still missing), CR3, compressed ORF/RAF ⬜) |
|
||||
| M11 | RAW II | CR3, RAF (X-Trans), ORF, RW2, PEF, SRW, 3FR, IIQ + long tail; camera calibration DB; HEIC/AVIF/JXL import | 20–35 | 🚧 (RAF uncompressed, RW2 (every raw format), PEF, ORF uncompressed ✅; **camera colour calibration** 🚧 (guarded ARW and NEF preview fitting; measured database still missing), CR3, compressed ORF/RAF ⬜) |
|
||||
| M12 | AI & smart features | subject/sky/background/people/object masks, semantic search, faces/People (permissively licensed models, pure-Rust inference) | 20–40 | ⬜ |
|
||||
| M13 | Merge | HDR merge (deghost), panorama (projections, boundary warp, fill edges), HDR panorama | 10–15 | ✅ |
|
||||
| M14 | Video | import/playback/trim via FilmCraft crates, global edits + presets on video, video export | 6–10 | ⬜ |
|
||||
|
|
@ -118,7 +118,7 @@ trip, slideshow, auto import…) in ≈ 12 h on 2026-10-02; four to six parallel
|
|||
| Work package | Tracker rows | Agent-hours | Risk |
|
||||
|---|---|---:|---|
|
||||
| Remaining P0/P1 UI and library features (folder rename/move, keyword painter, people view…) | ≈ 8 | 5–10 | low |
|
||||
| Raw codecs: CR3 (CRX), compressed ORF / RAF, NEF lossy-after-split, RW2 v4, HEIC/AVIF decode, JPEG XL DNG | LR-IMP-FORMATS | 40–80 | **high** — clean-room black-box analysis, no permissive specs |
|
||||
| Raw codecs: CR3 (CRX), compressed ORF / RAF, NEF lossy-after-split, HEIC/AVIF decode, JPEG XL DNG | LR-IMP-FORMATS | 40–80 | **high** — clean-room black-box analysis, no permissive specs |
|
||||
| Lens-profile database of our own (calibration targets, fitting, data) | LR-EDIT-OPTICS-PROFILE | 15–30 | data collection |
|
||||
| Video: playback, trim, edits, export (pure-Rust decode, ideally shared with FilmCraft) | R. Video | 20–40 | medium |
|
||||
| AI: subject / sky / background / people / object masks, object-aware remove, AI denoise, super resolution, lens blur, people & faces, natural-language search, culling | ≈ 30 | 80–150 | **high** — permissively licensed weights, pure-Rust inference, maybe training |
|
||||
|
|
@ -165,7 +165,9 @@ The milestone estimates in the table above were made before work started and are
|
|||
Decoded (CC0 corpus from raw.pixls.us, `cargo xtask corpus --download`, `crates/raw/tests/corpus.rs`): DNG (uncompressed,
|
||||
LJ92, lossy JPEG / Smart Previews, Deflate, float, linear), CR2, ARW (uncompressed, ARW2, LJ92; as-shot white balance and black level of pre-2017 bodies from the enciphered
|
||||
maker-note `Tag2010` and the encrypted `SR2SubIFD`, both recovered by black-box analysis, `crates/raw/src/vendor/arw.rs`), NEF/NRW uncompressed and Huffman-compressed (lossless, lossy type 1/2, 12/14-bit), RAF uncompressed (Bayer and
|
||||
X-Trans), RW2 packed 12/14-bit, PEF (uncompressed and Huffman), ORF uncompressed (16-bit and 12-bit packed). Every
|
||||
X-Trans), RW2 / Leica RWL / Panasonic RAW in every raw format (compressed formats 4 and 6, the prefix-coded strips of format 8,
|
||||
packed formats 2, 5 and 7, the 16-bit words of the 2005–2007 bodies; all recovered by black-box analysis of 178 CC0 files from
|
||||
118 bodies, `crates/raw/src/vendor/rw2.rs`), PEF (uncompressed and Huffman), ORF uncompressed (16-bit and 12-bit packed). Every
|
||||
supported container also yields its embedded JPEG preview (CR3 too), and the engine shows that preview for raw variants
|
||||
it can't decode yet.
|
||||
|
||||
|
|
@ -174,14 +176,11 @@ Not decoded yet — preview only (no permissively licensed description; black-bo
|
|||
files): rows above the split decode with the regular lossy table; from the split row on a different code is used
|
||||
that our black-box analysis has not recovered yet (none of the four regular tables fits, also not with byte
|
||||
alignment or reset predictors at the split row). The other Nikon Huffman variants are decoded (`crates/raw/src/vendor/nefc.rs` documents the analysis).
|
||||
- **Panasonic RW2 raw format 4** (quantised): the block layout is known (0x4000-byte chunks rotated by 0x1ff8; 128-bit
|
||||
blocks of two 12-bit seeds plus four groups of a 2-bit scale and three 8-bit codes; scales 0/1 are ×1/×2 differences
|
||||
from the same-colour pixel two to the left), the reconstruction rule for scales 2/3 is not established.
|
||||
- **Olympus compressed ORF**, **Fujifilm compressed RAF**, **Canon CR3/CRX** (M11.1), **Canon sRAW/mRAW**, lossy DNG.
|
||||
|
||||
**Camera colour matrices:** ARW files can use guarded, separate chromaticity and tone estimates from their own embedded JPEG (see `docs/camera-preview-colour.md`); this is a per-file camera-look estimate with relative WB, not measured calibration or absolute-Kelvin WB. Other non-DNG raws and rejected fits use the documented neutral fallback (camera RGB ≈ linear sRGB, flagged
|
||||
`matrix_is_fallback`) with the file's as-shot white-balance multipliers. Clean sources to evaluate next: manufacturer
|
||||
matrices stored in the files themselves (Olympus ImageProcessing `ColorMatrix`, Pentax/Panasonic equivalents) and our
|
||||
matrices stored in the files themselves (Olympus ImageProcessing `ColorMatrix`, Pentax equivalents; Panasonic RW2 files carry none) and our
|
||||
own chart-based calibration (M11.4). Adobe matrices are never used.
|
||||
|
||||
## Log
|
||||
|
|
@ -193,6 +192,9 @@ own chart-based calibration (M11.4). Adobe matrices are never used.
|
|||
Local roots and cleanup, 85k-photo grid and catalog performance, background compaction, failed-save errors, GPU
|
||||
export hardening, clippy 1.99. Added the honest *Where we stand* assessment and *Where we're going* priorities; added
|
||||
tracker rows for camera colour, camera coverage and render fidelity.
|
||||
- 2026-10-07: Panasonic RW2 / Leica RWL / Panasonic RAW decode in every raw format (compressed 4/6/8, packed 2/5/7,
|
||||
16-bit words), black levels, in-camera aspect crops and mapped-out defects established on 178 CC0 files from 118
|
||||
bodies.
|
||||
|
||||
## Chinese and Japanese interfaces, and text watermarks
|
||||
|
||||
|
|
|
|||
|
|
@ -286,8 +286,8 @@ fn services() -> Services {
|
|||
.add_filter(
|
||||
lightcraft_ui_egui::i18n::tr("Photos"),
|
||||
&[
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "cr3", "nef", "nrw", "arw", "raf", "orf", "rw2", "pef", "psd",
|
||||
"jxl", "gif", "bmp",
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "cr3", "nef", "nrw", "arw", "raf", "orf", "rw2", "rwl", "raw",
|
||||
"pef", "psd", "jxl", "gif", "bmp",
|
||||
],
|
||||
)
|
||||
.pick_files()
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ use crate::media::ProbeInfo;
|
|||
|
||||
/// File extensions LightCraft imports (lower case).
|
||||
pub const EXTENSIONS: &[&str] = &[
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "cr3", "nef", "nrw", "arw", "raf", "orf", "rw2", "pef", "psd", "jxl", "gif", "bmp",
|
||||
"heic", "avif",
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "cr3", "nef", "nrw", "arw", "raf", "orf", "rw2", "rwl", "raw", "pef", "psd", "jxl",
|
||||
"gif", "bmp", "heic", "avif",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ use serde_json::{Value, json};
|
|||
use crate::backend::Backend;
|
||||
|
||||
/// File extensions recognised as photos when expanding folders.
|
||||
pub const PHOTO_EXTENSIONS: &[&str] =
|
||||
&["jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "cr3", "nef", "arw", "raf", "orf", "rw2", "pef", "psd", "jxl", "gif", "bmp", "avif"];
|
||||
pub const PHOTO_EXTENSIONS: &[&str] = &[
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "cr3", "nef", "arw", "raf", "orf", "rw2", "rwl", "raw", "pef", "psd", "jxl", "gif",
|
||||
"bmp", "avif",
|
||||
];
|
||||
|
||||
/// Headless backend: a [`Session`] with filesystem hooks.
|
||||
pub struct Headless {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@
|
|||
//!
|
||||
//! Formats: DNG (uncompressed, lossless JPEG, lossy JPEG (Smart Previews), Deflate incl. floating point, tiled/stripped, CFA and LinearRaw),
|
||||
//! Canon CR2, Nikon NEF/NRW (uncompressed, Huffman lossless / lossy compressed), Sony ARW (uncompressed, ARW2, lossless), Fujifilm RAF (uncompressed Bayer
|
||||
//! and X-Trans), Panasonic RW2 (packed 12/14-bit), Pentax PEF (uncompressed, Huffman), Olympus ORF (uncompressed).
|
||||
//! and X-Trans), Panasonic RW2 / Leica RWL / Panasonic RAW (every raw format: compressed 4 and 6, the prefix-coded strips of 8,
|
||||
//! packed 2/5/7, the 16-bit words of the oldest bodies), Pentax PEF (uncompressed, Huffman), Olympus ORF (uncompressed).
|
||||
//! [`embedded_preview`] covers all of them plus CR3. Variants we can't decode yet (Nikon "lossy after split" NEF,
|
||||
//! Panasonic quantised RW2, compressed ORF/RAF, CR3) return [`RawError::Unsupported`]; each vendor module documents its sources
|
||||
//! compressed ORF/RAF, CR3) return [`RawError::Unsupported`]; each vendor module documents its sources
|
||||
//! (public specifications, tag-name documentation, black-box analysis of CC0 samples) and gaps. Non-DNG files carry no
|
||||
//! colour matrix: [`color`] falls back to a documented neutral model. The decoders never panic on malformed input.
|
||||
#![forbid(unsafe_code)]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -19,9 +19,6 @@ const KNOWN_UNSUPPORTED: &[&str] = &[
|
|||
"arw-sony-a7m4-lossless-m", // Sony lossless compressed M/S: subsampled (YCbCr) lossless JPEG
|
||||
"arw-sony-a7m4-lossless-s", // "
|
||||
"raf-fuji-xt20-compressed", // Fujifilm compressed RAF
|
||||
"rw2-panasonic-gh5.", // Panasonic raw format 4 (quantised)
|
||||
"rw2-panasonic-gx80", // "
|
||||
"rw2-panasonic-g9-b", // "
|
||||
"orf-olympus-em", // Olympus compressed ORF
|
||||
"sraw", // Canon sRAW / mRAW
|
||||
];
|
||||
|
|
@ -46,11 +43,12 @@ fn corpus_raw_decodes() {
|
|||
let t0 = Instant::now();
|
||||
let preview = embedded_preview(&bytes);
|
||||
let tp = t0.elapsed().as_secs_f64() * 1e3;
|
||||
// DNG previews are optional (and some carry only an uncompressed RGB thumbnail); vendor raws always embed a JPEG
|
||||
// DNG previews are optional (and some carry only an uncompressed RGB thumbnail); vendor raws embed a JPEG,
|
||||
// except the Panasonic `.RAW` files of 2005–2007
|
||||
if let Some(p) = &preview {
|
||||
assert!(p.starts_with(&[0xff, 0xd8]) && p.ends_with(&[0xff, 0xd9]), "{name}: preview is not a JPEG");
|
||||
} else {
|
||||
assert_eq!(fmt, RawFormat::Dng, "{name}: no embedded preview");
|
||||
assert!(fmt == RawFormat::Dng || name.starts_with("raw-panasonic-"), "{name}: no embedded preview");
|
||||
}
|
||||
let preview_kb = preview.as_ref().map_or(0, |p| p.len() / 1024);
|
||||
let t1 = Instant::now();
|
||||
|
|
@ -289,3 +287,145 @@ fn corpus_sony_pre2017_colour_metadata() {
|
|||
}
|
||||
eprintln!("pre-2017 Sony ARW colour metadata checked on {seen} files");
|
||||
}
|
||||
|
||||
/// The embedded JPEG as linear RGB, reduced to `gw × gh` cells.
|
||||
fn jpeg_cells(jpeg: &[u8], gw: usize, gh: usize) -> Vec<[f64; 3]> {
|
||||
use zune_core::{bytestream::ZCursor, colorspace::ColorSpace, options::DecoderOptions};
|
||||
let opts = DecoderOptions::default().jpeg_set_out_colorspace(ColorSpace::RGB);
|
||||
let mut d = zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(jpeg), opts);
|
||||
let px = d.decode().unwrap();
|
||||
let info = d.info().unwrap();
|
||||
let (w, h) = (info.width as usize, info.height as usize);
|
||||
let (mut sum, mut n) = (vec![[0f64; 3]; gw * gh], vec![0f64; gw * gh]);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = (y * gh / h) * gw + x * gw / w;
|
||||
for c in 0..3 {
|
||||
let v = px[(y * w + x) * 3 + c] as f64 / 255.0;
|
||||
sum[i][c] += if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) };
|
||||
}
|
||||
n[i] += 1.0;
|
||||
}
|
||||
}
|
||||
sum.iter().zip(&n).map(|(s, n)| s.map(|v| v / n)).collect()
|
||||
}
|
||||
|
||||
/// Black-subtracted means of the four sites of the 2×2 cells (anchored at sample (0, 0)) of the active area,
|
||||
/// reduced to `gw × gh` cells.
|
||||
fn raw_sites(img: &lightcraft_raw::RawImage, gw: usize, gh: usize) -> Vec<[f64; 4]> {
|
||||
let lightcraft_raw::RawData::U16(d) = &img.data else { panic!("float data") };
|
||||
let (w, a, black) = (img.width, img.active_area, img.black.mean() as f64);
|
||||
let (x0, y0) = ((a.x + 1) & !1, (a.y + 1) & !1);
|
||||
let (cw, ch) = ((a.x + a.width - x0) / 2, (a.y + a.height - y0) / 2);
|
||||
let (mut sum, mut n) = (vec![[0f64; 4]; gw * gh], vec![0f64; gw * gh]);
|
||||
for cy in 0..ch {
|
||||
for cx in 0..cw {
|
||||
let i = (cy * gh / ch) * gw + cx * gw / cw;
|
||||
for site in 0..4 {
|
||||
sum[i][site] += d[(y0 + 2 * cy + site / 2) * w + x0 + 2 * cx + site % 2] as f64 - black;
|
||||
}
|
||||
n[i] += 1.0;
|
||||
}
|
||||
}
|
||||
sum.iter().zip(&n).map(|(s, n)| s.map(|v| v / n)).collect()
|
||||
}
|
||||
|
||||
fn ranks(v: &[f64]) -> Vec<f64> {
|
||||
let mut idx: Vec<usize> = (0..v.len()).collect();
|
||||
idx.sort_by(|&a, &b| v[a].total_cmp(&v[b]));
|
||||
let mut r = vec![0f64; v.len()];
|
||||
for (rank, &i) in idx.iter().enumerate() {
|
||||
r[i] = rank as f64;
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// How well the raw's chromaticities (log r/g, log b/g per cell) follow the JPEG's when the mosaic is read with
|
||||
/// `layout` (colour of each 2×2 site at sample (0, 0)).
|
||||
fn chroma_agreement(sites: &[[f64; 4]], jpeg: &[[f64; 3]], layout: &[u8]) -> f64 {
|
||||
let (mut x, mut y) = ([vec![], vec![]], [vec![], vec![]]);
|
||||
for (s, j) in sites.iter().zip(jpeg) {
|
||||
let (mut rgb, mut n) = ([0f64; 3], [0f64; 3]);
|
||||
for (site, &c) in layout.iter().enumerate() {
|
||||
rgb[c as usize] += s[site];
|
||||
n[c as usize] += 1.0;
|
||||
}
|
||||
let rgb = [rgb[0] / n[0], rgb[1] / n[1], rgb[2] / n[2]];
|
||||
if rgb.iter().all(|v| *v > 1.0) && j.iter().all(|v| (0.002..0.9).contains(v)) {
|
||||
for (k, c) in [0, 2].into_iter().enumerate() {
|
||||
x[k].push((rgb[c] / rgb[1]).ln());
|
||||
y[k].push((j[c] / j[1]).ln());
|
||||
}
|
||||
}
|
||||
}
|
||||
(correlation(&x[0], &y[0]) + correlation(&x[1], &y[1])) / 2.0
|
||||
}
|
||||
|
||||
/// Panasonic RW2 / RWL / RAW (`crates/raw/src/vendor/rw2.rs`): one file per raw encoding (formats 2, 4, 5, 6, 7 and
|
||||
/// 8 — at 12, 14 and 16 bits — and the 16-bit words of the oldest bodies) decodes to the image of the camera's own
|
||||
/// JPEG, with the black levels, crops and colour-filter layouts the module docs derive and no defect markers left.
|
||||
#[test]
|
||||
fn corpus_panasonic_encodings() {
|
||||
let dir = corpus_root().join("raw");
|
||||
// (file, bits, mean black level, crop width × height, CFA layout)
|
||||
let cases = [
|
||||
("raw-panasonic-fz50.raw", 12, 15.0, (3648, 2736), "BGGR"),
|
||||
("raw-panasonic-fz8.raw", 12, 15.0, (3072, 2304), "RGGB"),
|
||||
("rw2-panasonic-fz1000m2-4x3.rw2", 12, 143.0, (4864, 3648), "GBRG"),
|
||||
("rw2-panasonic-g9-b.rw2", 12, 143.0, (5184, 3888), "RGGB"),
|
||||
("rw2-panasonic-g9.rw2", 12, 127.5, (5184, 3888), "RGGB"),
|
||||
("rw2-panasonic-gh1.rw2", 12, 15.0, (4000, 3000), "GBRG"),
|
||||
("rw2-panasonic-gh5.rw2", 12, 143.5, (5184, 3888), "RGGB"),
|
||||
("rw2-panasonic-gh5m2.rw2", 12, 144.5, (5184, 3888), "RGGB"),
|
||||
("rw2-panasonic-gh5s.rw2", 14, 509.5, (3680, 2760), "RGGB"),
|
||||
("rw2-panasonic-gh6.rw2", 16, 2048.0, (5776, 4336), "RGGB"),
|
||||
("rw2-panasonic-gx80.rw2", 12, 143.0, (4592, 3448), "BGGR"),
|
||||
("rw2-panasonic-s1.rw2", 14, 526.0, (6000, 4000), "RGGB"),
|
||||
("rw2-panasonic-s5-format7.rw2", 14, 512.0, (6000, 4000), "RGGB"),
|
||||
("rw2-panasonic-s5m2.rw2", 14, 512.0, (6000, 4000), "RGGB"),
|
||||
("rw2-panasonic-s9.rw2", 12, 128.0, (6000, 4000), "RGGB"),
|
||||
("rwl-leica-dlux7.rwl", 12, 143.0, (4736, 3552), "BGGR"),
|
||||
];
|
||||
let mut seen = 0;
|
||||
for (name, bits, black, (cw, ch), cfa) in cases {
|
||||
let Ok(bytes) = std::fs::read(dir.join(name)) else {
|
||||
eprintln!("skip: {name} absent");
|
||||
continue;
|
||||
};
|
||||
let img = decode(&bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(img.bits, bits, "{name}: bits");
|
||||
assert_eq!(img.black.mean(), black, "{name}: black level");
|
||||
assert_eq!((img.crop.width, img.crop.height), (cw, ch), "{name}: crop");
|
||||
let layout = img.cfa.clone().unwrap_or_else(|| panic!("{name}: no CFA"));
|
||||
assert_eq!(layout.name(), cfa, "{name}: CFA layout");
|
||||
let lightcraft_raw::RawData::U16(d) = &img.data else { panic!("float data") };
|
||||
let a = img.active_area;
|
||||
let zeros =
|
||||
(a.y..a.y + a.height).map(|y| d[y * img.width + a.x..y * img.width + a.x + a.width].iter().filter(|&&v| v == 0).count()).sum::<usize>();
|
||||
assert_eq!(zeros, 0, "{name}: defect markers left in the active area");
|
||||
let Some(jpeg) = embedded_preview(&bytes) else {
|
||||
// the 2005–2007 `.RAW` files have no preview; their flat scenes make the green diagonal unambiguous
|
||||
assert_eq!(green_on_main_diagonal(&img), cfa.starts_with('G'), "{name}: mosaic statistics disagree with {cfa}");
|
||||
seen += 1;
|
||||
continue;
|
||||
};
|
||||
// the camera's JPEG shows the whole active area (whatever the crop): the raw's green must rank like it, and
|
||||
// its colours must follow the JPEG's best with the decoded colour-filter layout
|
||||
let (gw, gh) = (24, 18);
|
||||
let (sites, jpeg) = (raw_sites(&img, gw, gh), jpeg_cells(&jpeg, gw, gh));
|
||||
let green: Vec<f64> = sites.iter().map(|s| (0..4).filter(|&i| layout.pattern[i] == 1).map(|i| s[i]).sum::<f64>()).collect();
|
||||
let jg: Vec<f64> = jpeg.iter().map(|j| j[1]).collect();
|
||||
let rho = correlation(&ranks(&green), &ranks(&jg));
|
||||
let agree = |l: &str| chroma_agreement(&sites, &jpeg, &lightcraft_raw::Cfa::bayer(l).unwrap().pattern);
|
||||
let best_other = ["RGGB", "GRBG", "GBRG", "BGGR"].into_iter().filter(|l| *l != cfa).map(agree).fold(f64::MIN, f64::max);
|
||||
let own = agree(cfa);
|
||||
eprintln!(
|
||||
"{name:32} {}x{} {bits}-bit: green rank correlation {rho:.3}, colour agreement {own:.3} (others ≤ {best_other:.3})",
|
||||
img.width, img.height
|
||||
);
|
||||
assert!(rho > 0.9, "{name}: rank correlation with the camera JPEG {rho}");
|
||||
assert!(own > 0.5 && own > best_other, "{name}: colours follow the JPEG better with another layout ({own} vs {best_other})");
|
||||
seen += 1;
|
||||
}
|
||||
eprintln!("Panasonic encodings checked on {seen} files");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Malformed vendor raw files (NEF, ARW, PEF, ORF, RW2, CR2, RAF) must decode to an error, never panic.
|
||||
//! Malformed vendor raw files (NEF, ARW, PEF, ORF, RW2 of every raw format, CR2, RAF) must decode to an error, never panic.
|
||||
|
||||
use lightcraft_tiff::tags as t;
|
||||
use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, Value};
|
||||
|
|
@ -68,6 +68,66 @@ fn rw2() -> Vec<u8> {
|
|||
b
|
||||
}
|
||||
|
||||
/// A Panasonic RW2 of raw format `format` (or, with `format` 0, the oldest 16-bit-word kind): sensor size, borders,
|
||||
/// bit depth, black levels, an in-camera crop and `n` bytes of payload at the end, at `RawDataOffset`. Format 8 also
|
||||
/// gets two strips covering the payload and a code table.
|
||||
fn rw2_of(format: u16, bits: u16, n: usize) -> Vec<u8> {
|
||||
let u16s = |v: &[u16]| Value::Undefined(v.iter().flat_map(|x| x.to_le_bytes()).collect());
|
||||
let build = |off: u32| {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
for (tag, v) in [(2, W as u16), (3, H as u16), (4, 2), (5, 4), (6, H as u16), (7, W as u16), (9, 2), (0x0a, bits), (0x1c, 128), (0x1d, 128)] {
|
||||
ifd.set(tag, Value::Short(vec![v]));
|
||||
}
|
||||
for (tag, v) in [(0x2f, 2u16), (0x30, 6), (0x31, H as u16 - 2), (0x32, W as u16 - 2)] {
|
||||
ifd.set(tag, Value::Short(vec![v]));
|
||||
}
|
||||
if format == 0 {
|
||||
ifd.set(0x000b, Value::Short(vec![34828]));
|
||||
ifd.set(t::STRIP_OFFSETS, Value::Long(vec![off]));
|
||||
} else {
|
||||
ifd.set(0x002d, Value::Short(vec![format]));
|
||||
ifd.set(0x0118, Value::Long(vec![off]));
|
||||
}
|
||||
if format == 8 {
|
||||
let table = [
|
||||
(6, 62),
|
||||
(7, 126),
|
||||
(6, 61),
|
||||
(5, 28),
|
||||
(4, 12),
|
||||
(3, 4),
|
||||
(3, 2),
|
||||
(2, 0),
|
||||
(3, 3),
|
||||
(3, 5),
|
||||
(4, 13),
|
||||
(5, 29),
|
||||
(6, 60),
|
||||
(8, 254),
|
||||
(8, 255),
|
||||
(12, 4094),
|
||||
(12, 4095),
|
||||
];
|
||||
ifd.set(0x0040, u16s(&std::iter::once(17).chain(table.iter().flat_map(|&(l, c)| [l, c])).collect::<Vec<u16>>()));
|
||||
let half = (n / 2) as u32;
|
||||
let (a, b) = (off, off + half);
|
||||
ifd.set(0x0044, u16s(&[2, a as u16, (a >> 16) as u16, b as u16, (b >> 16) as u16]));
|
||||
ifd.set(0x0045, u16s(&[2, 0, 0, W as u16 / 2, 0]));
|
||||
ifd.set(0x0046, u16s(&[2, (half * 8) as u16, ((half * 8) >> 16) as u16, (half * 8) as u16, ((half * 8) >> 16) as u16]));
|
||||
ifd.set(0x0047, u16s(&[2, W as u16 / 2, W as u16 / 2]));
|
||||
ifd.set(0x0048, u16s(&[2, H as u16, H as u16]));
|
||||
}
|
||||
ifd.set(t::MAKE, Value::Ascii("Panasonic".into()));
|
||||
let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap();
|
||||
b[..4].copy_from_slice(b"IIU\0");
|
||||
b
|
||||
};
|
||||
let off = build(0).len();
|
||||
let mut b = build(off as u32);
|
||||
b.extend((0..n).map(|i| (i * 89 % 253) as u8));
|
||||
b
|
||||
}
|
||||
|
||||
/// A Pentax PEF: the raw image in IFD0, wide enough for the dark-border trim.
|
||||
fn pef(compression: u16) -> Vec<u8> {
|
||||
let (w, h) = (264u32, 4u32);
|
||||
|
|
@ -181,6 +241,20 @@ fn samples() -> Vec<Vec<u8>> {
|
|||
cr2(),
|
||||
raf(),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(rw2_formats())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One RW2 per encoding with a sample decoder of its own (formats 4, 6 at 14 and 12 bits, 8, and 16-bit words).
|
||||
fn rw2_formats() -> Vec<Vec<u8>> {
|
||||
vec![
|
||||
rw2_of(4, 12, (W * H).div_ceil(14) as usize * 16),
|
||||
rw2_of(6, 14, (W * H).div_ceil(11) as usize * 16),
|
||||
rw2_of(6, 12, (W * H).div_ceil(14) as usize * 16),
|
||||
rw2_of(8, 14, (W * H) as usize * 2),
|
||||
rw2_of(0, 12, (W * H) as usize * 2),
|
||||
]
|
||||
}
|
||||
|
||||
fn exercise(bytes: &[u8]) {
|
||||
|
|
@ -208,7 +282,7 @@ proptest! {
|
|||
#![proptest_config(ProptestConfig { cases: 2000, .. ProptestConfig::default() })]
|
||||
|
||||
#[test]
|
||||
fn mutated_vendor_files_never_panic(kind in 0usize..12, flips in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..16), cut in any::<usize>()) {
|
||||
fn mutated_vendor_files_never_panic(kind in 0usize..17, flips in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..16), cut in any::<usize>()) {
|
||||
let mut data = samples().swap_remove(kind);
|
||||
let n = data.len();
|
||||
for (i, v) in flips {
|
||||
|
|
@ -218,3 +292,12 @@ proptest! {
|
|||
exercise(&data[..keep]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The RW2 samples decode as they are, so the mutations above reach every format's sample decoder.
|
||||
#[test]
|
||||
fn rw2_samples_decode_unmutated() {
|
||||
for s in &rw2_formats() {
|
||||
let r = lightcraft_raw::decode(s).unwrap();
|
||||
assert_eq!((r.width, r.height), (W as usize, H as usize));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ that decide whether a photographer can switch (see the honest assessment in [ROA
|
|||
Take the first one nobody is working on.
|
||||
|
||||
1. **LR-PROF-CAMERACOLOR** (P0): our own camera colour calibration. Sony ARW and Nikon NEF now get a guarded file-local fit to their own embedded JPEG (colour matrix + hue/saturation/value table + tone and chroma curves, relative WB; per-model profiles pooled from many photos via `lightcraft-cli calibrate`; ARW: 7 of 10 public samples accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples from 6 bodies accepted, ΔE 13–46 → 3–8, one mixed-light scene 13 → 12); measured calibration and fidelity remain missing. Other non-DNG raws and rejected fits still use a neutral matrix. Expand validated preview fitting and use matrices the files carry themselves; never Adobe data.
|
||||
2. **LR-IMP-FORMATS** (P0): **CR3** first (every Canon body since ~2018), then compressed RAF / ORF, RW2 v4, Nikon
|
||||
2. **LR-IMP-FORMATS** (P0): **CR3** first (every Canon body since ~2018), then compressed RAF / ORF, Nikon
|
||||
"lossy after split" NEF, Canon sRAW; HEIC/AVIF decode. Clean-room, from prose descriptions only (see
|
||||
`crates/raw/src/vendor/nefc.rs` for how compressed NEF was done). Until decoded, such photos are `preview_only`.
|
||||
3. **LR-IMP-CAMERA-COVERAGE** (P0): per-model verification; grow the CC0 corpus and fix per-model bugs (like the CR2
|
||||
|
|
@ -124,8 +124,8 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
|||
| LR-IMP-PROFILES | Import profiles & presets | P1 | ✅ | `cmd:file.importPresets`, `cmd:preset.import`, `cmd:profile.import`, `crates/engine/src/preset_import.rs`, `crates/engine/src/preset_luminar.rs` | presets: .lcpreset, XMP, classic .lrtemplate, photos carrying edits ("DNG presets"), Luminar looks (.lmp, .mplumpack collections; sliders with a counterpart), .zip bundles, folders (folder → group), drag & drop; masks carried over; unmapped settings reported; profiles: .cube 3D LUTs; Adobe profile formats deliberately unsupported |
|
||||
| LR-IMP-LOCAL | Work on files in place | P0 | ✅ | `cmd:library.browse`, `cmd:photo.addToLibrary`, `cmd:library.import` (mode add), `cmd:local.addRoot`, `cmd:local.hide`, `cmd:local.restoreHidden`, `crates/engine/src/cmd/browse.rs`, `crates/ui-egui/src/panels/left.rs` (`local_section`), `crates/ui-egui/src/panels/grid.rs` (`folder_header`) | Local: browse Pictures / Desktop / Downloads / Home or any folder without adding it (breadcrumb, Include subfolders, Add N to My Photos); browsed photos stay out of All Photos, albums and counts; folders picked with Browse Folder… (or Keep in Local / Add to Local on a folder row) stay listed across restarts, and browsing a folder inside a listed one highlights it inside that tree (opened down to it, siblings reachable) instead of adding a root; edits go to XMP sidecars; importing promotes them; right-click a top-level location → Remove from Local hides the shortcut only (nothing on disk or in the catalog changes), saved with the UI state, and “Show N hidden locations” (or Browse Folder… on it) brings it back; a folder is one location however its path is spelled (`/` or `\`, trailing separator, `.`/`..`, drive-letter case; `folder_key` in `crates/catalog/src/query.rs`); browsed photos nobody changed are forgotten once their folder has not been browsed for 30 days (Settings → Performance → Local folders, 0 = never; checked when the library opens, or `cmd:library.forgetLocal` with `dryRun`; files and sidecars stay, browsing again brings them back; see `crates/catalog/src/local.rs` for what counts as a change) |
|
||||
| LR-IMP-SIDECAR-SPLIT | Separate XMP sidecar variants | P2 | ⬜ | `cmd:library.xmpPreferences` | sidecar naming option exists (stem/full), no split sidecars |
|
||||
| LR-IMP-FORMATS | Supported formats | P0 | 🟡 | `crates/codecs/src/lib.rs`, `crates/raw/src/lib.rs` | JPEG, PNG, TIFF, WebP, JXL, PSD, GIF, BMP; DNG (incl. lossy / Smart Preview DNG), CR2 (colour-filter layout from the file's `CR2CFAPattern` tag, issue #85), ARW (pre-2017 bodies: white balance and black level from enciphered maker-note / `SR2SubIFD` data, issue #148), NEF (uncompressed, lossless and lossy compressed 12/14-bit, `crates/raw/src/vendor/nefc.rs`), RAF, RW2, PEF, ORF. 12-bit NEF black level: maker note `0x003d` is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). CR3 opens from its full-size embedded JPEG with full metadata (Exif, GPS, XMP from the `CMT1`/`CMT2`/`CMT4` boxes, `crates/meta/src/cr3.rs`) but its raw data is not decoded yet. Missing: CR3 raw decoding (CRX), compressed RAF/ORF, RW2 v4, NEFs labelled compressed (34713) but stored uncompressed without a `0x0096` table (Z 6 packed 14-bit, D850 12-bit uncompressed), NEF "lossy after split" (preview only: imported with `preview_only` = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, `previewOnly` in `catalog.query`; Reload clears it once the file decodes), HEIC/AVIF decode |
|
||||
| LR-IMP-CAMERA-COVERAGE | Camera coverage, verified per model | P0 | 🟡 | `crates/raw/tests/corpus.rs`, `xtask/src/main.rs` | a supported container is not the same as every camera that writes it decoding correctly: ~40 CC0 corpus files (plus ~60 NEFs checked by hand) against the >1,000 models Lightroom lists; per-model differences still surface (e.g. the CR2 colour-filter layout differed by model until it was read from the file's own tag, issue #85, checked on 35 CR2 bodies; pre-2017 Sony ARWs opened green until their scrambled white balance / black level were read, issue #148, checked on 16 ARWs from 15 Sony bodies). Grow the CC0 corpus per model and test each sample decodes with plausible colour |
|
||||
| LR-IMP-FORMATS | Supported formats | P0 | 🟡 | `crates/codecs/src/lib.rs`, `crates/raw/src/lib.rs` | JPEG, PNG, TIFF, WebP, JXL, PSD, GIF, BMP; DNG (incl. lossy / Smart Preview DNG), CR2 (colour-filter layout from the file's `CR2CFAPattern` tag, issue #85), ARW (pre-2017 bodies: white balance and black level from enciphered maker-note / `SR2SubIFD` data, issue #148), NEF (uncompressed, lossless and lossy compressed 12/14-bit, `crates/raw/src/vendor/nefc.rs`), RAF, RW2 / Leica RWL / Panasonic RAW in every raw format (compressed 4 and 6, the prefix-coded strips of format 8 from the GH6 on, packed 2/5/7, the 16-bit words of the 2005–2007 bodies; black levels, in-camera aspect crops and mapped-out defect pixels; 178 CC0 files from 118 bodies checked, `crates/raw/src/vendor/rw2.rs`), PEF, ORF. 12-bit NEF black level: maker note `0x003d` is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). CR3 opens from its full-size embedded JPEG with full metadata (Exif, GPS, XMP from the `CMT1`/`CMT2`/`CMT4` boxes, `crates/meta/src/cr3.rs`) but its raw data is not decoded yet. Missing: CR3 raw decoding (CRX), compressed RAF/ORF, NEFs labelled compressed (34713) but stored uncompressed without a `0x0096` table (Z 6 packed 14-bit, D850 12-bit uncompressed), NEF "lossy after split" (preview only: imported with `preview_only` = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, `previewOnly` in `catalog.query`; Reload clears it once the file decodes), HEIC/AVIF decode |
|
||||
| LR-IMP-CAMERA-COVERAGE | Camera coverage, verified per model | P0 | 🟡 | `crates/raw/tests/corpus.rs`, `xtask/src/main.rs` | a supported container is not the same as every camera that writes it decoding correctly: ~55 CC0 corpus files (plus ~60 NEFs checked by hand and 178 Panasonic / Leica RW2, RWL and RAW files from 118 bodies, checked against each file's own JPEG) against the >1,000 models Lightroom lists; per-model differences still surface (e.g. the CR2 colour-filter layout differed by model until it was read from the file's own tag, issue #85, checked on 35 CR2 bodies; pre-2017 Sony ARWs opened green until their scrambled white balance / black level were read, issue #148, checked on 16 ARWs from 15 Sony bodies). Grow the CC0 corpus per model and test each sample decodes with plausible colour |
|
||||
| LR-IMP-CULL-AT-IMPORT | Culling analysis at import | P2 | 🟡 | `cmd:photo.analyze` | run Assisted Culling on the imported photos (they're selected after an import); not automatic |
|
||||
| LR-IMP-MOVE | Move on import [Classic] | P1 | ✅ | `cmd:library.import` (`mode` move), `crates/engine/src/import_move.rs`, `crates/engine/src/tests_import_move.rs`, `crates/ui-egui/src/import.rs` | Transfer ▸ Move with Copy's destination, folders (day / month / one folder / custom template), rename template and example destination; XMP sidecars (both namings) move along; each source is removed only after its destination is written (hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate, unchecked files keep their sources, taken names get -1, -2…, never overwritten; files already in the destination/library are added in place; sources that can't be removed (read-only card) are kept and reported (`kept`). Undo removes the photos from the library but leaves the moved files at the destination. No Copy as DNG while moving |
|
||||
| LR-IMP-DNG-CONVERT | Convert to DNG on import [Classic] | P2 | ✅ | `cmd:library.import` (`dng`), `crates/ui-egui/src/import.rs` | copy imports: Raw files ▸ Copy as DNG (lossless; the card is untouched); the raw copy is removed only once its DNG is verified and on disk (issue #106) |
|
||||
|
|
|
|||
|
|
@ -153,6 +153,11 @@
|
|||
now develop from the raw data instead of the camera's embedded JPEG, so a B&W or other picture style set in the
|
||||
camera no longer gets baked in. Photos already imported as "preview only" switch over on Reload. (Files that
|
||||
Nikon splits into two differently compressed halves still use the preview for now.)
|
||||
- Panasonic and Leica raws (RW2, RWL and the older RAW files, DMC-LX1 to DC-S1R II) develop from the raw data in
|
||||
every format the cameras write. Most bodies opened as preview only before: GH1–GH5, the G, GX, GF, GM, FZ, LX and
|
||||
TZ/ZS series, Leica D-Lux, V-Lux and C-Lux, and the GH6, GH7, G9 II, S5 II and S9 generation. They open framed in
|
||||
the aspect ratio set in the camera, with mapped-out sensor defects filled in. `.rwl` and `.raw` files are imported
|
||||
too. Photos already imported as "preview only" switch over on Reload.
|
||||
- Sony ARWs from before about 2017 (RX100, RX100 II–V, RX10, NEX, SLT, ILCE-6000, A7 / A7 II / A7R II and their
|
||||
siblings) no longer open bright green (issue #148): their as-shot white balance and black level are read from the
|
||||
file (Sony stores them only in scrambled maker-note data on these bodies), the few columns of padding at the right
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ Terminal=false
|
|||
Categories=Graphics;2DGraphics;RasterGraphics;Photography;
|
||||
Keywords=photo;raw;camera;library;develop;
|
||||
StartupWMClass=ai.storyteller.lightcraft
|
||||
MimeType=image/jpeg;image/png;image/tiff;image/webp;image/x-adobe-dng;image/x-sony-arw;image/x-canon-cr2;image/x-canon-cr3;image/x-nikon-nef;image/x-fuji-raf;image/x-olympus-orf;image/x-panasonic-rw2;image/x-pentax-pef;
|
||||
MimeType=image/jpeg;image/png;image/tiff;image/webp;image/x-adobe-dng;image/x-sony-arw;image/x-canon-cr2;image/x-canon-cr3;image/x-nikon-nef;image/x-fuji-raf;image/x-olympus-orf;image/x-panasonic-rw2;image/x-panasonic-rw;image/x-pentax-pef;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ use std::process::Command;
|
|||
/// Extensions treated as assets (lower-case).
|
||||
const ASSET_EXT: &[&str] = &[
|
||||
"png", "jpg", "jpeg", "gif", "webp", "avif", "svg", "ico", "icns", "bmp", "ttf", "otf", "woff", "woff2", "wav", "mp3", "ogg", "flac", "mp4",
|
||||
"mov", "tif", "tiff", "dng", "cr2", "cr3", "nef", "arw", "raf", "orf", "rw2", "pef", "heic", "jxl", "psd", "cube", "icc", "icm", "xmp",
|
||||
"mov", "tif", "tiff", "dng", "cr2", "cr3", "nef", "arw", "raf", "orf", "rw2", "rwl", "raw", "pef", "heic", "jxl", "psd", "cube", "icc", "icm",
|
||||
"xmp",
|
||||
];
|
||||
/// Adobe-specific formats we never ship (camera/lens profiles, Lightroom templates).
|
||||
const FORBIDDEN_EXT: &[&str] = &["dcp", "lcp", "lrtemplate", "lrcat", "lrsmcol", "aco", "ase", "abr"];
|
||||
|
|
|
|||
|
|
@ -315,11 +315,22 @@ const RAW_SAMPLES: &[(&str, &str)] = &[
|
|||
"raf-fuji-xt20-compressed.raf",
|
||||
"https://raw.pixls.us/getfile.php/1178/nice/Fujifilm%20-%20X-T20%20-%2014bit%2014bit%20compressed%20%283:2%29.RAF",
|
||||
),
|
||||
("raw-panasonic-fz50.raw", "https://raw.pixls.us/getfile.php/2234/nice/Panasonic%20-%20DMC-FZ50%20-%204:3.RAW"),
|
||||
("raw-panasonic-fz8.raw", "https://raw.pixls.us/getfile.php/2282/nice/Panasonic%20-%20DMC-FZ8%20-%204:3.RAW"),
|
||||
("rw2-panasonic-fz1000m2-4x3.rw2", "https://raw.pixls.us/getfile.php/4706/nice/Panasonic%20-%20DC-FZ10002%20-%204:3.RW2"),
|
||||
("rw2-panasonic-g9-b.rw2", "https://raw.pixls.us/getfile.php/2348/nice/Panasonic%20-%20DC-G9%20-%204:3.RW2"),
|
||||
("rw2-panasonic-g9.rw2", "https://raw.pixls.us/getfile.php/2585/nice/Panasonic%20-%20DC-G9%20-%204:3.RW2"),
|
||||
("rw2-panasonic-gh1.rw2", "https://raw.pixls.us/getfile.php/1323/nice/Panasonic%20-%20DMC-GH1%20-%204:3.RW2"),
|
||||
("rw2-panasonic-gh5.rw2", "https://raw.pixls.us/getfile.php/1517/nice/Panasonic%20-%20DC-GH5%20-%204:3.RW2"),
|
||||
("rw2-panasonic-gh5m2.rw2", "https://raw.pixls.us/getfile.php/5082/nice/Panasonic%20-%20DC-GH5M2%20-%204:3.RW2"),
|
||||
("rw2-panasonic-gh5s.rw2", "https://raw.pixls.us/getfile.php/2603/nice/Panasonic%20-%20DC-GH5S%20-%204:3.RW2"),
|
||||
("rw2-panasonic-gh6.rw2", "https://raw.pixls.us/getfile.php/5876/nice/Panasonic%20-%20DC-GH6%20-%204:3.RW2"),
|
||||
("rw2-panasonic-gx80.rw2", "https://raw.pixls.us/getfile.php/1569/nice/Panasonic%20-%20DMC-GX80%20-%204:3.RW2"),
|
||||
("rw2-panasonic-s1.rw2", "https://raw.pixls.us/getfile.php/3038/nice/Panasonic%20-%20DC-S1%20-%203:2.RW2"),
|
||||
("rw2-panasonic-s5-format7.rw2", "https://raw.pixls.us/getfile.php/6339/nice/Panasonic%20-%20DC-S5%20-%203:2.RW2"),
|
||||
("rw2-panasonic-s5m2.rw2", "https://raw.pixls.us/getfile.php/7790/nice/Panasonic%20-%20DC-S5M2%20-%2014bit%20%283:2%29.RW2"),
|
||||
("rw2-panasonic-s9.rw2", "https://raw.pixls.us/getfile.php/7702/nice/Panasonic%20-%20DC-S9%20-%203:2.RW2"),
|
||||
("rwl-leica-dlux7.rwl", "https://raw.pixls.us/getfile.php/4204/nice/Leica%20-%20D-Lux%207%20-%204:3.RWL"),
|
||||
];
|
||||
|
||||
fn cmd_corpus(download: bool) -> Result<(), String> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue