Merge PR #1040
# Conflicts: # ROADMAP.md # crates/pipeline/src/spots.rs # docs/gaps.md # docs/parity-checklist.md # docs/raw-parity.md # docs/target-app-parity.md
This commit is contained in:
commit
2020ad1df4
|
|
@ -47,6 +47,8 @@ Hours are calibrated on this repo's PR history; see
|
|||
| Features | ~61% | ~77% (checklist 81.1%) | 600–1,100 h | [target-app-parity](docs/target-app-parity.md#features) |
|
||||
| ↳ RAW cameras, colour, lens profiles | ~56% | 81% of 1,446 Adobe models decode, 16% verified | 255–480 h | [raw-parity](docs/raw-parity.md) |
|
||||
| UI/UX fidelity | ~70% | ~85% | 34–58 h | [ui-parity](docs/ui-parity.md) |
|
||||
| ↳ RAW cameras, colour, lens profiles | ~56% | 82% of 1,446 Adobe models decode, 17% verified | 255–480 h | [raw-parity](docs/raw-parity.md) |
|
||||
| UI/UX fidelity | ~70% | ~85% | 40–70 h | [ui-parity](docs/ui-parity.md) |
|
||||
| File formats (non-raw) | ~70% | ~85% | 30–55 h | [file-format-parity](docs/file-format-parity.md) |
|
||||
| Hardware | ~40% | ~50% | 40–80 h | [hardware-parity](docs/hardware-parity.md) |
|
||||
| Localization | ~52% of the 12 key languages | 7 of 12 present | 130–210 h (+145–255 h new scripts) | [localization-parity](docs/localization-parity.md) |
|
||||
|
|
@ -117,6 +119,9 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
|
|||
- 2026-10-11: Leica Q3 verified by a recorded check of 38 private DNGs (all decode and develop at full
|
||||
size; mean ΔE to the camera JPEG 6.3–13.9). 236 models verified; headline numbers unchanged.
|
||||
- 2026-10-11 (CI): `ci.yml` runs `cargo xtask ci` on Linux and clippy and the tests on macOS and Windows, and the CPU ↔ GPU equivalence tests on lavapipe, Metal and WARP (`LIGHTCRAFT_GPU_ALLOW_SOFTWARE`, tests only), within the documented bounds on all three (max 1 LSB on lavapipe and WARP, 2 in one case on the runner's Metal; the bound is 3). `main` fails there until the #989 fixes and #1052 (Windows) land (#1050). Headline numbers unchanged.
|
||||
- 2026-10-12: Minolta / Konica Minolta MRW decodes from sensor data (12-bit packed and 16-bit words,
|
||||
RGGB and GBRG, as-shot WB from `WBG`); DiMAGE 7, 7Hi, A1, A2, A200 and Dynax / Maxxum 7D verified on
|
||||
CC0 files. 241 models verified, 222 preview-only; headline numbers unchanged.
|
||||
- 2026-10-11: Canon EOS R5 Mark II C-RAW decodes from sensor data (57% of its files opened as a
|
||||
preview): the CRX QP map's first-symbol literal and limited-length escape; four CC0 R5 Mark II files
|
||||
pinned, the QP maps of 33,665 private C-RAW files decode exactly ([cr3.md](docs/cr3.md)). 235 models
|
||||
|
|
@ -166,6 +171,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
|
|||
|---|---|---|
|
||||
| 2026-10-12 | minor | UI/UX remaining 40–70 h → 34–58 h (accessibility audit landed); headline numbers unchanged |
|
||||
| 2026-10-11 | minor | Tests in CI on macOS, Windows and Linux (`ci.yml`, #1050), GPU equivalence tests on software adapters; not green on `main` yet, gap 9 6–12 h → 2–4 h; headline numbers unchanged |
|
||||
| 2026-10-12 | minor | Minolta MRW decoded: 6 more verified models (241), 11 fewer preview-only; headline numbers unchanged |
|
||||
| 2026-10-11 | minor | EOS R5 Mark II CR3 verified: CR3 on four verified bodies, RAW decoding 76% → 77%; headline numbers unchanged |
|
||||
| 2026-10-10 | minor | RAW measured model by model; RAW decode and colour as explicit rows: full 59% → 61%, mainstream 51% → 49%, essentials 63% → 64%; beta ~14 points |
|
||||
| 2026-10-10 | minor | Full ready restored to the additive weighted sum (~59%; method aligned with the standard, no new evidence); hours per audience; beta ~16 points away |
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ pub struct Analysis {
|
|||
/// file's white balance is edited relative to its as-shot look or in Kelvin depends on the camera:
|
||||
/// [`relative_wb_camera`].
|
||||
pub fn relative_wb_format(format: &str) -> bool {
|
||||
["ARW", "NEF", "NRW", "RW2", "RWL", "RAW", "RAF", "CR3", "CR2", "PEF", "SRW", "ORF"].iter().any(|f| format.eq_ignore_ascii_case(f))
|
||||
["ARW", "NEF", "NRW", "RW2", "RWL", "RAW", "RAF", "CR3", "CR2", "PEF", "SRW", "ORF", "MRW"].iter().any(|f| format.eq_ignore_ascii_case(f))
|
||||
}
|
||||
|
||||
/// Whether a raw of `format` shot on `camera` (Exif make and model, as [`Meta::camera`] keeps them) is developed
|
||||
|
|
@ -577,6 +577,7 @@ mod edited_tests {
|
|||
("a.cr2", "CR2", true),
|
||||
("a.pef", "pef", true),
|
||||
("a.orf", "ORF", true),
|
||||
("a.mrw", "MRW", true),
|
||||
("a.dng", "DNG", false),
|
||||
] {
|
||||
let mut p = Photo::new(PhotoId(1), Source::Demo { scene: 0 }, name, format, 10, 10, "2026-10-01T00:00:00");
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ pub(crate) fn file_local_look(format: RawFormat) -> bool {
|
|||
| RawFormat::Pef
|
||||
| RawFormat::Srw
|
||||
| RawFormat::Orf
|
||||
| RawFormat::Mrw
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2190,6 +2191,7 @@ mod tests {
|
|||
RawFormat::Pef,
|
||||
RawFormat::Srw,
|
||||
RawFormat::Orf,
|
||||
RawFormat::Mrw,
|
||||
];
|
||||
assert!(local.into_iter().all(file_local_look));
|
||||
// DNG carries its own colour model
|
||||
|
|
|
|||
|
|
@ -2171,8 +2171,8 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Recognised raw containers we don't decode (Minolta MRW here: the preview's first byte is
|
||||
/// overwritten in the file) import as preview only with the JPEG's size, instead of failing.
|
||||
/// Recognised raw containers we can't decode (an MRW whose block header is damaged here; its preview's first
|
||||
/// byte is overwritten, as in real MRW files) import as preview only with the JPEG's size, instead of failing.
|
||||
#[test]
|
||||
fn undecodable_container_with_a_preview_imports_as_preview_only() {
|
||||
let px: Vec<u8> = (0..40 * 30).flat_map(|i| [(i % 251) as u8, 128, 200]).collect();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
//! - WebP (RIFF container spec): `EXIF`, `XMP `, `ICCP` chunks.
|
||||
//! - TIFF-based files (TIFF, DNG and most raws): the whole file is the Exif block; XMP (700), ICC (34675) and
|
||||
//! IPTC (33723) from IFD0.
|
||||
//! - Minolta MRW (see [`crate::mrw`]): the `TTW` block is the Exif block.
|
||||
//! - Canon CR3 (ISO base media file, see [`crate::cr3`]): `CMT1`/`CMT2`/`CMT4` merged into one Exif block; the
|
||||
//! XMP `uuid` box.
|
||||
|
||||
|
|
@ -36,6 +37,14 @@ pub fn embedded(bytes: &[u8]) -> Embedded {
|
|||
_ => Embedded::default(),
|
||||
};
|
||||
}
|
||||
if let Some(m) = crate::mrw::parse(bytes) {
|
||||
// Minolta MRW: the `TTW` block is a complete TIFF with the Exif data
|
||||
return m
|
||||
.block(b"TTW")
|
||||
.filter(|t| Tiff::parse(t).is_ok())
|
||||
.map(|t| Embedded { exif: Some(t.to_vec()), ..Default::default() })
|
||||
.unwrap_or_default();
|
||||
}
|
||||
if let Some(c) = crate::cr3::parse_cr3(bytes) {
|
||||
return Embedded { exif: crate::cr3::merged_exif(&c), xmp: c.xmp.map(|b| String::from_utf8_lossy(b).into_owned()), ..Default::default() };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ mod datetime;
|
|||
mod exif;
|
||||
mod gpx;
|
||||
mod iptc;
|
||||
pub mod mrw;
|
||||
pub mod tags;
|
||||
mod xmp;
|
||||
mod xmp_merge;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
//! Minolta / Konica Minolta MRW block container.
|
||||
//!
|
||||
//! Sources: the public prose description of the MRW layout (Dalibor Jelínek's MRW notes) and our own black-box
|
||||
//! analysis of the CC0 samples on raw.pixls.us. The file starts with `\0MRM` and a big-endian `u32` length; the
|
||||
//! sensor data begins right after that many bytes (at `8 + length`). In between is a sequence of blocks, each a
|
||||
//! four-byte name (`\0PRD`, `\0TTW`, `\0WBG`, `\0RIF`, `\0PAD`), a big-endian `u32` length and that many bytes.
|
||||
//! `\0TTW` holds a complete big-endian TIFF (IFD0, Exif, maker note) whose offsets are relative to the block.
|
||||
|
||||
/// The blocks and the offset of the sensor data in an MRW file.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Mrw<'a> {
|
||||
/// `(name without the leading NUL, contents)`, in file order.
|
||||
pub blocks: Vec<([u8; 3], &'a [u8])>,
|
||||
/// Absolute offset of the sensor data.
|
||||
pub data_offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> Mrw<'a> {
|
||||
/// The first block called `name` (e.g. `b"PRD"`).
|
||||
pub fn block(&self, name: &[u8; 3]) -> Option<&'a [u8]> {
|
||||
self.blocks.iter().find(|(n, _)| n == name).map(|(_, d)| *d)
|
||||
}
|
||||
}
|
||||
|
||||
/// At most this many blocks are walked (real files have four or five).
|
||||
const MAX_BLOCKS: usize = 64;
|
||||
|
||||
fn be32(b: &[u8], at: usize) -> Option<usize> {
|
||||
let s = b.get(at..at.checked_add(4)?)?;
|
||||
Some(u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as usize)
|
||||
}
|
||||
|
||||
/// Parse the block list of an MRW file. `None` when the magic is missing or the header length points past the
|
||||
/// file; a block that runs past the header is dropped together with everything after it.
|
||||
pub fn parse(bytes: &[u8]) -> Option<Mrw<'_>> {
|
||||
if !bytes.starts_with(b"\0MRM") {
|
||||
return None;
|
||||
}
|
||||
let data_offset = be32(bytes, 4)?.checked_add(8).filter(|&o| o <= bytes.len())?;
|
||||
let mut blocks = Vec::new();
|
||||
let mut at = 8usize;
|
||||
while blocks.len() < MAX_BLOCKS && at.saturating_add(8) <= data_offset {
|
||||
let (Some(name), Some(len)) = (bytes.get(at..at + 4), be32(bytes, at + 4)) else { break };
|
||||
let start = at + 8;
|
||||
let Some(end) = start.checked_add(len).filter(|&e| e <= data_offset) else { break };
|
||||
let Some(data) = bytes.get(start..end) else { break };
|
||||
if name[0] == 0 {
|
||||
blocks.push(([name[1], name[2], name[3]], data));
|
||||
}
|
||||
at = end;
|
||||
}
|
||||
Some(Mrw { blocks, data_offset })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn block(name: &[u8; 3], data: &[u8]) -> Vec<u8> {
|
||||
let mut v = vec![0];
|
||||
v.extend_from_slice(name);
|
||||
v.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||
v.extend_from_slice(data);
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walks_blocks_and_finds_the_data() {
|
||||
let mut body = block(b"PRD", &[1, 2, 3]);
|
||||
body.extend(block(b"WBG", &[4; 12]));
|
||||
let mut f = b"\0MRM".to_vec();
|
||||
f.extend_from_slice(&(body.len() as u32).to_be_bytes());
|
||||
f.extend_from_slice(&body);
|
||||
f.extend_from_slice(&[9, 9]);
|
||||
let m = parse(&f).unwrap();
|
||||
assert_eq!(m.block(b"PRD"), Some(&[1u8, 2, 3][..]));
|
||||
assert_eq!(m.block(b"WBG").map(<[u8]>::len), Some(12));
|
||||
assert_eq!(m.block(b"TTW"), None);
|
||||
assert_eq!(m.data_offset, f.len() - 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_lengths_never_panic() {
|
||||
assert_eq!(parse(b"\0MRM"), None);
|
||||
assert_eq!(parse(b"\0MRM\xff\xff\xff\xff"), None);
|
||||
// a block claiming more than the header holds is dropped
|
||||
let f = b"\0MRM\0\0\0\x0c\0PRD\xff\xff\xff\xff\0\0\0\0";
|
||||
assert_eq!(parse(f).map(|m| m.blocks.len()), Some(0));
|
||||
for n in 0..f.len() {
|
||||
let _ = parse(&f[..n]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ pub enum RawFormat {
|
|||
Srw,
|
||||
/// Canon CRW (CIFF heap), the bodies before the CR2 era.
|
||||
Crw,
|
||||
/// Minolta MRW (`\0MRM` block container).
|
||||
/// Minolta MRW (`\0MRM` block container, `vendor::mrw`).
|
||||
Mrw,
|
||||
/// Sigma / Foveon X3F (`FOVb`).
|
||||
X3f,
|
||||
|
|
@ -132,6 +132,7 @@ impl RawFormat {
|
|||
| RawFormat::CfaTiff
|
||||
| RawFormat::Srw
|
||||
| RawFormat::KodakDcs
|
||||
| RawFormat::Mrw
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -385,6 +386,7 @@ fn decode_with(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
|||
RawFormat::CfaTiff => dng::decode_as(bytes, mode, RawFormat::CfaTiff).map(lift_clipped),
|
||||
RawFormat::Srw => vendor::srw::decode(bytes, mode),
|
||||
RawFormat::KodakDcs => vendor::kodak::decode(bytes, mode),
|
||||
RawFormat::Mrw => vendor::mrw::decode(bytes, mode),
|
||||
RawFormat::OtherTiff => Err(RawError::Unsupported(other_tiff_reason(bytes))),
|
||||
other => Err(RawError::Unsupported(format!("{other:?} files are not decoded yet"))),
|
||||
}
|
||||
|
|
@ -1021,10 +1023,10 @@ mod tests {
|
|||
assert_eq!(probe(b"II\x1a\0\0\0HEAPCCDR\0\0"), Some(RawFormat::Crw));
|
||||
assert_eq!(probe(b"\0MRM\0\x01\0\0"), Some(RawFormat::Mrw));
|
||||
assert_eq!(probe(b"FOVb\x02\0\x02\0"), Some(RawFormat::X3f));
|
||||
for f in [RawFormat::Crw, RawFormat::Mrw, RawFormat::X3f] {
|
||||
for f in [RawFormat::Crw, RawFormat::X3f] {
|
||||
assert!(!f.is_supported());
|
||||
}
|
||||
assert!(matches!(decode(b"\0MRM\0\x01\0\0"), Err(RawError::Unsupported(w)) if w.contains("Mrw")));
|
||||
assert!(matches!(decode(b"FOVb\x02\0\x02\0"), Err(RawError::Unsupported(w)) if w.contains("X3f")));
|
||||
// too short for the CIFF signature
|
||||
assert_eq!(probe(b"II\x1a\0\0\0HEAP"), None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -616,7 +616,8 @@ mod tests {
|
|||
f.extend_from_slice(part);
|
||||
f.extend_from_slice(&[0u8; 7]);
|
||||
}
|
||||
assert!(crate::probe(&f).is_some_and(|p| !p.is_supported()), "{head:?}");
|
||||
// MRW decodes; its preview is still found by the scan
|
||||
assert!(crate::probe(&f).is_some_and(|p| !p.is_supported() || p == crate::RawFormat::Mrw), "{head:?}");
|
||||
assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..]), "{head:?}");
|
||||
for n in (0..f.len()).step_by(11) {
|
||||
let _ = embedded_preview(&f[..n]);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub mod cr3;
|
|||
pub mod crx;
|
||||
pub(crate) mod crx_wavelet;
|
||||
pub mod kodak;
|
||||
pub mod mrw;
|
||||
pub mod nef;
|
||||
pub mod nefc;
|
||||
pub mod orf;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
//! Minolta / Konica Minolta MRW: a block container (see [`lightcraft_meta::mrw`]) around a 12-bit Bayer mosaic.
|
||||
//!
|
||||
//! Sources: the public prose description of the MRW layout (Dalibor Jelínek's MRW notes) and our own black-box
|
||||
//! analysis of the eight CC0 MRW samples on raw.pixls.us (DiMAGE 7, 7Hi, A1, A2, A200, Dynax / Maxxum / ALPHA-7
|
||||
//! Digital); every number below was measured on them.
|
||||
//!
|
||||
//! - `PRD` (24 bytes, big-endian): 8 ASCII version bytes, sensor rows and columns (`u16` each), image rows and
|
||||
//! columns, data size (12 or 16 bits per stored sample), pixel size (12), storage method (`0x59` = 12-bit
|
||||
//! packed: two samples in three bytes, most significant bits first; `0x52` = one 12-bit sample per big-endian
|
||||
//! 16-bit word), two unknown bytes and the Bayer layout as a `u16` (`1` = RGGB, `4` = GBRG). The sensor data
|
||||
//! starts after the header and is exactly sensor rows x columns samples in every sample.
|
||||
//! - The sensor frame is 8 columns and 6 to 8 rows larger than the image size, but every edge holds picture (no
|
||||
//! masked border), so the whole frame is the active area and the image size is the default crop.
|
||||
//! - Black is 0: the darkest samples are 0 in every packed file and 13..34 in the unpacked ones, with no offset
|
||||
//! left to subtract. White is the saturation plateau where the frame has one (3964 on the A1, 3965 on a clipped
|
||||
//! DiMAGE 7, 4020 on the A200, 4076 on the 7D), else 4095.
|
||||
//! - The layout code was checked against the embedded preview: with `1` the two greens are the equal (0,1) and
|
||||
//! (1,0) positions; in the A200 (`4`) the greens are (0,0) and (1,1), and only red at (1,0) reproduces the
|
||||
//! preview's red-dominated colour.
|
||||
//! - `WBG` (12 bytes): four scale bytes, then four big-endian `u16` coefficients in the order of the Bayer cell
|
||||
//! read row by row (RGGB: R G G B, GBRG: G B R G). Red over green and blue over green are the as-shot gains;
|
||||
//! applied to the raw channel means they reproduce the preview's colour balance within the colour matrix's
|
||||
//! reach on all eight samples.
|
||||
//! - `TTW` is a TIFF with IFD0 (orientation), Exif and the maker note; the preview JPEG sits in it with its first
|
||||
//! byte overwritten (see `preview::scan_for_jpeg`).
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result, unpack};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::{Tiff, tags as t};
|
||||
|
||||
/// Storage method codes in `PRD`.
|
||||
const PACKED_12: u8 = 0x59;
|
||||
const WORDS_16: u8 = 0x52;
|
||||
|
||||
/// What `PRD` says about the sensor data.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct Prd {
|
||||
sensor: (usize, usize),
|
||||
image: (usize, usize),
|
||||
packed: bool,
|
||||
cfa: &'static str,
|
||||
}
|
||||
|
||||
fn be16(b: &[u8], at: usize) -> Option<u16> {
|
||||
b.get(at..at + 2).map(|s| u16::from_be_bytes([s[0], s[1]]))
|
||||
}
|
||||
|
||||
fn prd(b: &[u8]) -> Result<Prd> {
|
||||
let u = |at| be16(b, at).map(usize::from).ok_or_else(|| RawError::Corrupt("Mrw PRD block too short".into()));
|
||||
let (sh, sw, ih, iw) = (u(8)?, u(10)?, u(12)?, u(14)?);
|
||||
let (size, storage) = (b.get(16).copied(), b.get(18).copied());
|
||||
let packed = match (size, storage) {
|
||||
(Some(12), Some(PACKED_12)) => true,
|
||||
(Some(16), Some(WORDS_16)) => false,
|
||||
_ => return Err(RawError::Unsupported(format!("Mrw storage {size:?}/{storage:?} is not decoded yet"))),
|
||||
};
|
||||
let cfa = match u(22)? {
|
||||
1 => "RGGB",
|
||||
4 => "GBRG",
|
||||
other => return Err(RawError::Unsupported(format!("Mrw Bayer layout {other}"))),
|
||||
};
|
||||
if sw == 0 || sh == 0 || (packed && sw % 2 != 0) {
|
||||
return Err(RawError::Corrupt("Mrw sensor size".into()));
|
||||
}
|
||||
Ok(Prd { sensor: (sw, sh), image: (iw.min(sw), ih.min(sh)), packed, cfa })
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let m = lightcraft_meta::mrw::parse(bytes).ok_or_else(|| RawError::Corrupt("Mrw block header".into()))?;
|
||||
let p = prd(m.block(b"PRD").ok_or_else(|| RawError::Unsupported("Mrw file without a PRD block".into()))?)?;
|
||||
let (w, h) = p.sensor;
|
||||
let n = w.checked_mul(h).filter(|n| *n <= crate::MAX_SAMPLES).ok_or(RawError::Limit("image too large"))?;
|
||||
let need = if p.packed { n / 2 * 3 } else { n * 2 };
|
||||
let src = bytes
|
||||
.get(m.data_offset..)
|
||||
.and_then(|s| s.get(..need))
|
||||
.ok_or_else(|| RawError::Corrupt(format!("Mrw sensor data truncated (needs {need} bytes)")))?;
|
||||
let cfa = Cfa::bayer_static(p.cfa);
|
||||
let (data, white) = if mode == Mode::Full {
|
||||
let mut d = vec![0u16; n];
|
||||
if p.packed {
|
||||
unpack::unpack_msb(src, 12, &mut d);
|
||||
} else {
|
||||
unpack::read_u16s(src, lightcraft_tiff::ByteOrder::Big, &mut d);
|
||||
for v in &mut d {
|
||||
*v = (*v).min(4095);
|
||||
}
|
||||
}
|
||||
let white = super::white_from_data(&d, 12);
|
||||
(RawData::U16(d), white)
|
||||
} else {
|
||||
(RawData::U16(Vec::new()), 4095.0)
|
||||
};
|
||||
let wb = m.block(b"WBG").and_then(|b| wb_from(b, &cfa));
|
||||
let tiff = m.block(b"TTW").and_then(|b| Tiff::parse(b).ok());
|
||||
let mut metadata = tiff.as_ref().map(lightcraft_meta::from_tiff).unwrap_or_default();
|
||||
metadata.width = Some(p.image.0 as u32);
|
||||
metadata.height = Some(p.image.1 as u32);
|
||||
let orientation = tiff.as_ref().and_then(|t| t.ifds.first()).and_then(|i| i.u16(t::ORIENTATION)).unwrap_or(1);
|
||||
let img = RawImage {
|
||||
format: RawFormat::Mrw,
|
||||
width: w,
|
||||
height: h,
|
||||
cpp: 1,
|
||||
data,
|
||||
cfa: Some(cfa),
|
||||
bits: 12,
|
||||
black: BlackLevel::uniform(0.0),
|
||||
white: vec![white],
|
||||
active_area: Rect::new(0, 0, w, h),
|
||||
crop: Rect::new(0, 0, p.image.0, p.image.1),
|
||||
orientation: Orientation::from_exif(orientation),
|
||||
color: ColorData::default(),
|
||||
wb_multipliers: wb,
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
/// As-shot gains from `WBG`: four coefficients in Bayer-cell order after four scale bytes.
|
||||
fn wb_from(b: &[u8], cfa: &Cfa) -> Option<[f32; 3]> {
|
||||
let mut sum = [0f32; 3];
|
||||
let mut count = [0f32; 3];
|
||||
for (i, &c) in cfa.pattern.iter().enumerate().take(4) {
|
||||
let v = f32::from(be16(b, 4 + 2 * i)?);
|
||||
let k = usize::from(c).min(2);
|
||||
sum[k] += v;
|
||||
count[k] += 1.0;
|
||||
}
|
||||
if count.contains(&0.0) || sum.iter().any(|&s| s <= 0.0) {
|
||||
return None;
|
||||
}
|
||||
let mean = |k: usize| sum[k] / count[k];
|
||||
Some([mean(0) / mean(1), 1.0, mean(2) / mean(1)])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A synthetic MRW: `PRD` with the given storage, `WBG`, a `TTW` TIFF with orientation 6, then the samples.
|
||||
pub(crate) fn sample(w: u16, h: u16, packed: bool, layout: u16, wbg: [u16; 4], samples: &[u16]) -> Vec<u8> {
|
||||
let mut prd = b"21810002".to_vec();
|
||||
for v in [h, w, h - 2, w - 2] {
|
||||
prd.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
prd.extend_from_slice(&[if packed { 12 } else { 16 }, 12, if packed { PACKED_12 } else { WORDS_16 }, 0, 0, 0]);
|
||||
prd.extend_from_slice(&layout.to_be_bytes());
|
||||
let mut wb = vec![2u8; 4];
|
||||
for v in wbg {
|
||||
wb.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
let mut ifd0 = lightcraft_tiff::IfdBuilder::new();
|
||||
ifd0.set(t::MAKE, lightcraft_tiff::Value::Ascii("Minolta Co., Ltd.".into()));
|
||||
ifd0.set(t::ORIENTATION, lightcraft_tiff::Value::Short(vec![6]));
|
||||
let ttw = lightcraft_tiff::TiffWriter::default().write(&[ifd0]).unwrap();
|
||||
let mut body = Vec::new();
|
||||
for (name, data) in [(b"PRD", &prd), (b"WBG", &wb), (b"TTW", &ttw)] {
|
||||
body.push(0);
|
||||
body.extend_from_slice(name);
|
||||
body.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||
body.extend_from_slice(data);
|
||||
}
|
||||
let mut f = b"\0MRM".to_vec();
|
||||
f.extend_from_slice(&(body.len() as u32).to_be_bytes());
|
||||
f.extend_from_slice(&body);
|
||||
if packed {
|
||||
for p in samples.chunks(2) {
|
||||
let (a, b) = (p[0], p.get(1).copied().unwrap_or(0));
|
||||
f.extend_from_slice(&[(a >> 4) as u8, ((a & 15) << 4) as u8 | (b >> 8) as u8, b as u8]);
|
||||
}
|
||||
} else {
|
||||
for v in samples {
|
||||
f.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_packed_and_word_storage() {
|
||||
let (w, h) = (8u16, 6u16);
|
||||
let px: Vec<u16> = (0..48).map(|i| (i * 85) as u16 % 4096).collect();
|
||||
for packed in [true, false] {
|
||||
let f = sample(w, h, packed, 1, [408, 256, 256, 450], &px);
|
||||
assert_eq!(crate::probe(&f), Some(RawFormat::Mrw));
|
||||
let r = crate::decode(&f).unwrap();
|
||||
assert_eq!((r.width, r.height, r.bits), (8, 6, 12));
|
||||
assert_eq!(r.data, RawData::U16(px.clone()), "packed {packed}");
|
||||
assert_eq!(r.cfa.as_ref().map(Cfa::name).as_deref(), Some("RGGB"));
|
||||
assert_eq!(r.crop, Rect::new(0, 0, 6, 4));
|
||||
assert_eq!(r.orientation, Orientation::from_exif(6));
|
||||
assert_eq!(r.metadata.make.as_deref(), Some("Minolta Co., Ltd."));
|
||||
let wb = r.wb_multipliers.unwrap();
|
||||
assert!((wb[0] - 408.0 / 256.0).abs() < 1e-6 && (wb[2] - 450.0 / 256.0).abs() < 1e-6, "{wb:?}");
|
||||
assert_eq!(crate::probe_info(&f).unwrap(), r.info());
|
||||
}
|
||||
}
|
||||
|
||||
/// GBRG (layout 4): the coefficients follow the cell, so the second is blue and the third red.
|
||||
#[test]
|
||||
fn gbrg_white_balance_follows_the_cell() {
|
||||
let f = sample(4, 4, true, 4, [255, 416, 480, 255], &[100; 16]);
|
||||
let r = crate::decode(&f).unwrap();
|
||||
assert_eq!(r.cfa.as_ref().map(Cfa::name).as_deref(), Some("GBRG"));
|
||||
let wb = r.wb_multipliers.unwrap();
|
||||
assert!((wb[0] - 480.0 / 255.0).abs() < 1e-6 && (wb[2] - 416.0 / 255.0).abs() < 1e-6, "{wb:?}");
|
||||
}
|
||||
|
||||
/// Truncations and corrupted header bytes give errors, never panics.
|
||||
#[test]
|
||||
fn malformed_files_never_panic() {
|
||||
let f = sample(8, 6, true, 1, [408, 256, 256, 450], &[7; 48]);
|
||||
for n in 0..f.len() {
|
||||
assert!(crate::decode(&f[..n]).is_err() || n == f.len());
|
||||
}
|
||||
for i in 8..f.len().min(120) {
|
||||
for v in [0u8, 0xff, 0x52, 0x59] {
|
||||
let mut g = f.clone();
|
||||
g[i] = v;
|
||||
let _ = crate::decode(&g);
|
||||
let _ = crate::probe_info(&g);
|
||||
}
|
||||
}
|
||||
assert!(matches!(crate::decode(b"\0MRM\0\x01\0\0"), Err(RawError::Corrupt(_))));
|
||||
assert!(matches!(crate::decode(b"\0MRM\0\0\0\0"), Err(RawError::Unsupported(w)) if w.contains("Mrw")));
|
||||
}
|
||||
}
|
||||
|
|
@ -1133,3 +1133,51 @@ fn fuji_embedded_lens_tables_follow_the_validated_models() {
|
|||
}
|
||||
eprintln!("Fuji lens tables: {checked} RAFs, {warped} warps, {vignetted} vignette gains");
|
||||
}
|
||||
|
||||
/// Minolta MRW (`crates/raw/src/vendor/mrw.rs`): packed and 16-bit-word storage, RGGB and GBRG cells, the image
|
||||
/// size as the crop, the `WBG` gains in cell order and an embedded preview. Skips files that are absent.
|
||||
#[test]
|
||||
fn corpus_minolta_mrw() {
|
||||
let root = corpus_root().join("raw");
|
||||
let mut checked = 0;
|
||||
// (file, sensor size, crop size, cell, white, red gain, blue gain)
|
||||
for (name, sensor, crop, cell, white, r, b) in [
|
||||
("mrw-minolta-dimage7hi-words16.mrw", (2568, 1928), (2560, 1920), "RGGB", 4095.0, 411.0 / 256.0, 343.0 / 256.0),
|
||||
("mrw-minolta-dimage-a1.mrw", (2568, 1928), (2560, 1920), "RGGB", 3964.0, 341.0 / 256.0, 539.0 / 256.0),
|
||||
("mrw-minolta-dimage-a200-gbrg.mrw", (3280, 2454), (3264, 2448), "GBRG", 4020.0, 480.0 / 255.0, 416.0 / 255.0),
|
||||
("mrw-minolta-dynax7d.mrw", (3016, 2008), (3008, 2000), "RGGB", 4076.0, 435.0 / 256.0, 435.0 / 256.0),
|
||||
] {
|
||||
let Ok(bytes) = std::fs::read(root.join(name)) else {
|
||||
eprintln!("skip: {name} absent");
|
||||
continue;
|
||||
};
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::Mrw), "{name}");
|
||||
let img = decode(&bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(((img.width, img.height), img.bits), (sensor, 12), "{name}");
|
||||
assert_eq!((img.crop.width, img.crop.height), crop, "{name}");
|
||||
assert_eq!(img.cfa.as_ref().map(|c| c.name()).as_deref(), Some(cell), "{name}");
|
||||
assert!((img.white_at(0) - white).abs() <= 8.0, "{name}: white {}", img.white_at(0));
|
||||
assert_eq!(img.black.mean(), 0.0, "{name}");
|
||||
let wb = img.wb_multipliers.unwrap_or_else(|| panic!("{name}: no white balance"));
|
||||
assert!((wb[0] - r).abs() < 1e-4 && (wb[2] - b).abs() < 1e-4, "{name}: {wb:?}");
|
||||
assert!(img.metadata.make.as_deref().is_some_and(|m| m.to_ascii_uppercase().contains("MINOLTA")), "{name}: {:?}", img.metadata.make);
|
||||
// the two greens of the cell agree (a wrong cell or a packing slip would split them)
|
||||
let lightcraft_raw::RawData::U16(d) = &img.data else { panic!("float data") };
|
||||
let (w, h) = sensor;
|
||||
let mean = |y0: usize, x0: usize| {
|
||||
let (mut s, mut n) = (0f64, 0f64);
|
||||
for y in (y0..h).step_by(2) {
|
||||
for x in (x0..w).step_by(2) {
|
||||
s += f64::from(d[y * w + x]);
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
s / n
|
||||
};
|
||||
let (g1, g2) = if cell == "RGGB" { (mean(0, 1), mean(1, 0)) } else { (mean(0, 0), mean(1, 1)) };
|
||||
assert!((g1 - g2).abs() < 0.02 * g1, "{name}: greens {g1} vs {g2}");
|
||||
assert!(embedded_preview(&bytes).is_some(), "{name}: no preview");
|
||||
checked += 1;
|
||||
}
|
||||
eprintln!("MRW: {checked} corpus files checked");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3/CR2, Pentax PEF, Samsung SRW and Olympus ORF starting look
|
||||
# Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3/CR2, Pentax PEF, Samsung SRW, Olympus ORF and Minolta MRW starting look
|
||||
|
||||
ARW, NEF, RW2, RAF, CR3, CR2, PEF, SRW and ORF decoding supply sensor data and camera white-balance multipliers, but currently no measured camera colour matrix. Treating camera RGB as linear sRGB produces a dark, muted default rendering. The loader now estimates a file-local starting look from that file's embedded camera JPEG, only when a usable camera matrix is absent. All output pixels still come from the RAW sensor data; there is no JPEG replacement or uniform saturation boost.
|
||||
ARW, NEF, RW2, RAF, CR3, CR2, PEF, SRW, ORF and MRW decoding supply sensor data and camera white-balance multipliers, but currently no measured camera colour matrix. Treating camera RGB as linear sRGB produces a dark, muted default rendering. The loader now estimates a file-local starting look from that file's embedded camera JPEG, only when a usable camera matrix is absent. All output pixels still come from the RAW sensor data; there is no JPEG replacement or uniform saturation boost.
|
||||
|
||||
## Colour and tone are separate
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
|
|||
| 9 B | **Tests in CI on macOS, Windows and Linux x64 not green yet**: `ci.yml` (#1050) runs `cargo xtask ci` on Linux, clippy and the tests on macOS and Windows, and the GPU tests on lavapipe, Metal and WARP (`LIGHTCRAFT_GPU_ALLOW_SOFTWARE`); `main` fails there until the #989 fixes land (#990, #997, #1003, #1007, #1018) and the Windows failures the fork runs found are fixed (#1052: clippy `dead_code` on `LibraryLock::descriptor_copy`, `editors::tests::desktop_entries_give_their_program`, 21 engine rename / sidecar tests on NTFS) | `.github/workflows/ci.yml`; fork runs in #1050; the linux-riscv64 release job (#844) runs only `lightcraft-cli --version` under QEMU | until the jobs are green and required checks, regressions on the main platforms still reach `main` | 2–4 | [target-app-parity](target-app-parity.md#by-dimension) |
|
||||
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 235 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
|
||||
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 235 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 regression (#702); F828 SRF now discovered and imported as preview-only, sensor decoding still missing, DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
|
||||
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 241 of 1,446 Adobe models verified (17%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
|
||||
| 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) |
|
||||
| 12 | **Content-aware / generative remove, detect distractions, reflections removal** | REM rows 75%; patch synthesis missing | cleanup work goes back to Photoshop | 25–45 (+ generative model) | [target-app-parity](target-app-parity.md#ai) |
|
||||
| 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) |
|
||||
|
|
@ -41,7 +42,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
|
|||
| 17 | **Tethered capture** | ⬜ | studio photographers | 25–45 + cameras | [hardware-parity](hardware-parity.md) |
|
||||
| 18 | **Video**: playback, trim, global edits, export | VID rows 0% | users who mix stills and clips | 25–45 | [target-app-parity](target-app-parity.md#features) |
|
||||
| 19 | **Plug-in ecosystem**: no Lightroom SDK equivalent (export / publish plug-ins, metadata plug-ins) | ⬜ | pro workflows built on plug-ins (LR/Enfuse, Jeffrey's exporters, Negative Lab Pro) | 40–80 + owner decision | [target-app-parity](target-app-parity.md#by-dimension) |
|
||||
| 20 | **Long-tail raw formats**: compressed SRW, ARQ pixel shift, IIQ, Hasselblad compression, X3F, CRW, MRW, ERF, KDC, DCR, MEF, GPR, old FinePix | raw-parity.md | small user share each | 45–85 | [raw-parity](raw-parity.md) |
|
||||
| 20 | **Long-tail raw formats**: compressed SRW, ARQ pixel shift, IIQ, Hasselblad compression, X3F, CRW, ERF, KDC, DCR, MEF, GPR, old FinePix (MRW decodes since 2026-10-12) | raw-parity.md | small user share each | 40–80 | [raw-parity](raw-parity.md) |
|
||||
| 21 | **Localization**: 9 of 15 shipped languages partial (74–99% of catalog messages), lower-layer errors English; Hindi, Arabic, Indonesian, Korean, Vietnamese absent; no RTL or complex shaping | measured from catalogs | non-English users see mixed English | 55–90 (shipped to full); 145–255 (five new) | [localization-parity](localization-parity.md) |
|
||||
| 22 | **Pen pressure**, OS monitor profiles, follow-window colour | hardware-parity.md | brush users with tablets; multi-monitor setups | 11–22 | [hardware-parity](hardware-parity.md) |
|
||||
| 23 | **Accessibility**: AccessKit tree audited by a test (every control named, sliders with range); left: canvas handles, spot and mask placement by keyboard, a session with a real screen reader | LR-BEHAV-ACCESS 🟡 | blind and low-vision users | 4–8 | [ui-parity](ui-parity.md) |
|
||||
|
|
@ -96,6 +97,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
|
|||
| 2026-10-12 | minor | Gap 28 shrinks: F8 and Shift+Tab land; F1 and `[` `]` were already implemented; gap not closed |
|
||||
| 2026-10-11 | minor | Gap 9 shrinks: `ci.yml` (#1050) runs the tests on Linux, macOS and Windows and the GPU equivalence tests on lavapipe, Metal and WARP; red on `main` until the #989 fixes and #1052 (Windows) land, 6–12 h → 2–4 h |
|
||||
| 2026-10-12 | minor | Gap 10: SR2/SRF folder discovery fixed; F828 imports as preview-only. Sensor decoding remains a gap; readiness scores unchanged |
|
||||
| 2026-10-12 | minor | Gap 20 shrinks: Minolta MRW decodes (`vendor/mrw.rs`), 6 bodies verified; gap 10 count 235 → 241 |
|
||||
| 2026-10-11 | minor | Reviewed against main after 0.6.0: CR3 entry names the CRX variants `crx.rs` still refuses and the R7 / R6 Mark II C-RAW state (decoding, not verified, so the measured numbers don't move); CI and Windows / Linux entries note the riscv64 release build (#844); R7 fix added to Closed recently. Panel docking (UI.9, #869) closes no listed gap |
|
||||
| 2026-10-11 | minor | Camera colour entry: raws with a black-and-white camera JPEG take its tone curve (#577); gap not closed |
|
||||
| 2026-10-11 | minor | Gap 4: Compare and Reference views get zoom windows per pane (1:1 was a 2,560 px render magnified); overlays and soft proofing remain, 8–15 h → 4–8 h |
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -25,6 +25,10 @@ ranked in [gaps.md](gaps.md). Details per format: [cr3.md](cr3.md),
|
|||
| Decoder path should handle it, **unverified** | **942** (65%) | measured from the format rules (632 of them DNG, mostly phones) |
|
||||
| **Preview-only** (embedded JPEG) | **234** (16%) | measured |
|
||||
| **Unsupported** (not imported, refused or decodes wrongly) | **35** (2%) | measured |
|
||||
| Decoded from sensor data and **verified** on a real file | **241** (17%) | measured: [raw/evidence.tsv](raw/evidence.tsv) |
|
||||
| Decoder path should handle it, **unverified** | **947** (65%) | measured from the format rules (632 of them DNG, mostly phones) |
|
||||
| **Preview-only** (embedded JPEG) | **222** (15%) | measured |
|
||||
| **Unsupported** (not imported, refused or decodes wrongly) | **36** (2%) | measured |
|
||||
| Models added since 2017 (Camera Raw 10+): verified / unverified / preview / unsupported | 82 / 640 / 24 / 8 of 754 | measured |
|
||||
| **Share of photographers whose camera fully works** (decodes from sensor data), use-weighted | **~85%** | estimated: measured classes × written usage weights × per-format confidence |
|
||||
| …of which on a body we have verified | **~42%** | estimated, same weights |
|
||||
|
|
@ -55,7 +59,7 @@ unsupported to preview-only in the counts; sensor-decoding readiness is unchange
|
|||
`lightcraft_engine::import::EXTENSIONS`, and `KNOWN_UNSUPPORTED` in `crates/raw/tests/corpus.rs`. Doc
|
||||
claims were not used.
|
||||
- **Verified** means a real file from that body decoded from sensor data in: the corpus test over the
|
||||
`RAW_SAMPLES` manifest (`xtask/src/main.rs`, 101 CC0 files; bodies whose sample is in
|
||||
`RAW_SAMPLES` manifest (`xtask/src/main.rs`, 105 CC0 files; bodies whose sample is in
|
||||
`KNOWN_UNSUPPORTED` excluded), `crates/raw/tests/cr3_corpus.rs` (3 bodies exact against an external decoder;
|
||||
the EOS R5 Mark II pinned, with a QP-map check of 33,665 private files), the RAF reference
|
||||
check ([raf-compression.md](raf-compression.md)), and the raw.pixls.us bulk checks recorded in issue
|
||||
|
|
@ -98,6 +102,8 @@ unsupported to preview-only in the counts; sensor-decoding readiness is unchange
|
|||
| OM Digital Solutions | 7 | 0 | 0 | 7 | 0 | **compressed ORF** |
|
||||
| Kodak, Konica Minolta, Epson, Sigma, GoPro, Contax, others | 56 | 0 | 14 | 27 | 15 | DCR, GPR not imported; KDC, MRW, ERF, X3F preview |
|
||||
| **All** | **1,446** | **236 (16%)** | **941 (65%)** | **233 (16%)** | **36 (2%)** | |
|
||||
| Kodak, Konica Minolta, Epson, Sigma, GoPro, Contax, others | 56 | 6 | 19 | 16 | 15 | DCR, GPR not imported; KDC, ERF, X3F preview (MRW decodes) |
|
||||
| **All** | **1,446** | **241 (17%)** | **947 (65%)** | **222 (15%)** | **36 (2%)** | |
|
||||
|
||||
## By use (estimated)
|
||||
|
||||
|
|
@ -114,6 +120,7 @@ unsupported to preview-only in the counts; sensor-decoding readiness is unchange
|
|||
| Leica | 1% | 96% | 21% | the Q3 verified |
|
||||
| DJI | 1% | 95% | 0% | |
|
||||
| Medium format and others | 0.5% | 23% | 1% | IIQ, 3FR, X3F |
|
||||
| Medium format and others | 0.5% | 28% | 3% | IIQ, 3FR, X3F |
|
||||
| **Weighted** | 100% | **85%** | **42%** | |
|
||||
|
||||
About 5 in 6 raw photographers' cameras decode, but only about 2 in 5 use a body we have actually
|
||||
|
|
@ -162,6 +169,8 @@ checked, and Canon, the largest bucket, is the weakest of the big four.
|
|||
| 3FR / FFF | DNG-like layouts | 🟡 | decoded when uncompressed or LJ92 |
|
||||
| 3FR / FFF | Hasselblad compression | ⬜ | preview |
|
||||
| IIQ, MOS, MRW, ERF, KDC, X3F | all | ⬜ | preview; an uncompressed MOS, whose IFD0 is the mosaic itself with no Make tag, is recognised by Leaf's data block `0x8606` (#1275) |
|
||||
| MRW | 12-bit packed, 16-bit words; RGGB and GBRG | ✅ | 4 CC0 corpus files (DiMAGE 7Hi, A1, A200, Dynax 7D) plus DiMAGE 7 and A2 checked against their previews; `vendor/mrw.rs` |
|
||||
| IIQ, MOS, ERF, KDC, X3F | all | ⬜ | preview |
|
||||
| DCR, MEF, MFW, DXO, GPR, TIFF raws | all | ⬜ | not imported |
|
||||
|
||||
### Compressed ORF: a clean-room starting point
|
||||
|
|
@ -270,7 +279,7 @@ works, from [raw/usage-weights.tsv](raw/usage-weights.tsv).
|
|||
| 7 | Sony A100 regression (#702) and DSC-F828 sensor decoding (preview-only) | +0.1 | 1–2 | ~0.07 | samples on raw.pixls.us |
|
||||
| 8 | Canon CRW (2000–2004 bodies) | +1.2 by the uniform tail weight, far less by real use | 6–12 | ≤ 0.1 | samples |
|
||||
| 9 | Nikon HE / HE★ NEF (Z 8, Z 9, Z 6III, Z f, Z50II, Z5II set to HE) | +1.1 | 20–40 | ~0.04 | a pure-Rust JPEG XS decoder |
|
||||
| 10 | Compressed SRW, Hasselblad 3FR, IIQ, X3F, MRW, KDC, ERF, DCR, MEF, GPR | +0.6 together | 45–85 | ~0.01 | samples; low use |
|
||||
| 10 | Compressed SRW, Hasselblad 3FR, IIQ, X3F, KDC, ERF, DCR, MEF, GPR (MRW done) | +0.6 together | 45–85 | ~0.01 | samples; low use |
|
||||
|
||||
Beyond decoding:
|
||||
|
||||
|
|
@ -292,6 +301,7 @@ Beyond decoding:
|
|||
| 2026-10-13 | minor | Nikon D90 image area (4288 × 2848 at 10, 10) and white level (3830) measured on 10 of 550 NEFs from one body: its 42 filler columns no longer develop as a pink band |
|
||||
| 2026-10-12 | minor | Hasselblad Lunar verified (a Sony NEX-7 ARW under Make HASSELBLAD, now decoded): 236 verified, 232 preview; an uncompressed Leaf MOS is recognised as a raw (preview only) instead of opening as a greyscale image of its mosaic (#1275) |
|
||||
| 2026-10-12 | minor | SR2/SRF folder discovery and shared CLI/MCP extensions; public DSC-R1 sensor import and DSC-F828 preview-only import agree with direct import. F828 reclassified as preview-only (234 preview, 35 unsupported); sensor-decoding scores unchanged |
|
||||
| 2026-10-12 | minor | Minolta MRW decoded (`vendor/mrw.rs`): DiMAGE 7, 7Hi, A1, A2, A200 and Dynax / Maxxum 7D verified; 241 verified, 947 unverified, 222 preview |
|
||||
| 2026-10-10 | minor | Compressed ORF: pointer to the two Olympus patents that describe the method, as a clean-room starting point |
|
||||
| 2026-10-12 | minor | PEF without area tags: masked border rows and columns trimmed against the BlackPoint (issue #1271; KP, KF, K-3 Mark III, K-70, K-1, K-1 Mark II, K-S1, K-S2, 645Z); counts unchanged |
|
||||
| 2026-10-11 | minor | EOS R5 Mark II verified (CRX QP-map first literal and limited-length escape, odd-height Bayer planes; 4 CC0 files pinned, QP maps of 33,665 private files): 235 verified, 942 unverified; use-weighted 85% (42% on verified bodies); decoding 77%, RAW readiness unchanged at ~56% |
|
||||
|
|
|
|||
|
|
@ -85,7 +85,9 @@ def classify(maker, camera, ext_field):
|
|||
return "preview", "SRW", "compressed SRW is not decoded"
|
||||
return "unverified", "SRW", "uncompressed / packed SRW"
|
||||
e = es[0] if es else ""
|
||||
if e in ("CRW", "3FR", "FFF", "IIQ", "MOS", "ERF", "KDC", "MRW", "X3F", "RAW", "ORI"):
|
||||
if e == "MRW":
|
||||
return "unverified", "MRW", "MRW decoder (12-bit packed, 16-bit words)"
|
||||
if e in ("CRW", "3FR", "FFF", "IIQ", "MOS", "ERF", "KDC", "X3F", "RAW", "ORI"):
|
||||
return "preview", e, f"{e} is opened from its embedded JPEG only"
|
||||
return "unsupported", e, f"{e or 'unknown'} is not imported"
|
||||
|
||||
|
|
|
|||
|
|
@ -552,17 +552,17 @@ Kodak EasyShare Z1015 IS KDC 2008 preview KDC is opened from its embedded JPEG o
|
|||
Kodak EasyShare Z980 KDC 2008 preview KDC is opened from its embedded JPEG only
|
||||
Kodak EasyShare Z981 RAW 2010 preview RAW is opened from its embedded JPEG only
|
||||
Kodak EasyShare Z990 RAW 2010 preview RAW is opened from its embedded JPEG only
|
||||
Konica Minolta Alpha-5 Digital (China) MRW 2005 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta Alpha Sweet Digital (Japan) MRW 2005 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta DiMAGE 5 MRW 2003 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta DiMAGE 7 MRW 2003 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta DiMAGE 7i MRW 2003 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta DiMAGE 7Hi MRW 2003 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta DiMAGE A1 MRW 2003 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta DiMAGE A2 MRW 2003 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta DiMAGE A200 MRW 2003 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta Maxxum 5D Dynax 5D MRW 2005 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta Maxxum 7D Dynax 7D MRW 2003 preview MRW is opened from its embedded JPEG only
|
||||
Konica Minolta Alpha-5 Digital (China) MRW 2005 unverified MRW decoder (12-bit packed, 16-bit words)
|
||||
Konica Minolta Alpha Sweet Digital (Japan) MRW 2005 unverified MRW decoder (12-bit packed, 16-bit words)
|
||||
Konica Minolta DiMAGE 5 MRW 2003 unverified MRW decoder (12-bit packed, 16-bit words)
|
||||
Konica Minolta DiMAGE 7 MRW 2003 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs)
|
||||
Konica Minolta DiMAGE 7i MRW 2003 unverified MRW decoder (12-bit packed, 16-bit words)
|
||||
Konica Minolta DiMAGE 7Hi MRW 2003 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Konica Minolta DiMAGE A1 MRW 2003 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Konica Minolta DiMAGE A2 MRW 2003 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs)
|
||||
Konica Minolta DiMAGE A200 MRW 2003 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Konica Minolta Maxxum 5D Dynax 5D MRW 2005 unverified MRW decoder (12-bit packed, 16-bit words)
|
||||
Konica Minolta Maxxum 7D Dynax 7D MRW 2003 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Leaf AFi II 6 MOS 2008 preview MOS is opened from its embedded JPEG only
|
||||
Leaf AFi II 7 MOS 2008 preview MOS is opened from its embedded JPEG only
|
||||
Leaf Aptus 17 MOS 2007 preview MOS is opened from its embedded JPEG only
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 2.
|
|
|
@ -250,3 +250,9 @@ Sony RX1R II (DSC-RX1RM2) verified issue #535: raw.pixls.us bulk check of all 10
|
|||
Sony ZV-1 (DCZV1/B) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool)
|
||||
Sony ZV-E1 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool)
|
||||
Sony ZV-E10 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool)
|
||||
Konica Minolta DiMAGE 7Hi verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Konica Minolta DiMAGE A1 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Konica Minolta DiMAGE A200 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Konica Minolta Maxxum 7D Dynax 7D verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Konica Minolta DiMAGE 7 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs)
|
||||
Konica Minolta DiMAGE A2 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 3.
|
|
|
@ -49,6 +49,8 @@ phase #85, duplicate Local entries #22, black GPU exports on an Intel iGPU #78).
|
|||
| Features (weighted areas below) | ~77% (checklist 81.1%, measured) | ~61% | 600–1,100 h | [Features](#features) |
|
||||
| ↳ RAW cameras, colour and lens profiles (part of features) | 81% of Adobe's 1,446 models decode (16% verified), measured | ~56% | 255–480 h | [raw-parity.md](raw-parity.md) |
|
||||
| UI/UX fidelity | ~85% | ~70% | 34–58 h | [ui-parity.md](ui-parity.md) |
|
||||
| ↳ RAW cameras, colour and lens profiles (part of features) | 82% of Adobe's 1,446 models decode (17% verified), measured | ~56% | 255–480 h | [raw-parity.md](raw-parity.md) |
|
||||
| UI/UX fidelity | ~85% | ~70% | 40–70 h | [ui-parity.md](ui-parity.md) |
|
||||
| File formats (non-raw: images, sidecars, presets, catalogs) | ~85% | ~70% | 30–55 h | [file-format-parity.md](file-format-parity.md) |
|
||||
| Hardware | ~50% | ~40% | 40–80 h (+ tethering and printing, counted under Classic modules) | [hardware-parity.md](hardware-parity.md) |
|
||||
| Localization (12 key languages) | 7 of 12 present | ~52% (mean of the 12, measured) | 130–210 h for the target's set; +145–255 h for Hindi, Arabic, Indonesian, Vietnamese, Korean | [localization-parity.md](localization-parity.md) |
|
||||
|
|
@ -299,6 +301,7 @@ P2 items 12–25 h; look tuning, performance, packaging, hardening 25–45 h.
|
|||
|---|---|---|
|
||||
| 2026-10-12 | minor | UI/UX remaining 40–70 h → 34–58 h (accessibility audit landed, ui-parity.md); percentages unchanged |
|
||||
| 2026-10-11 | minor | Stability evidence: tests in CI on Linux, macOS and Windows (`ci.yml`, #1050), GPU equivalence on software adapters; not green on `main` yet, so the ×0.92 stability discount and the numbers are unchanged |
|
||||
| 2026-10-12 | minor | Minolta MRW decoded (raw-parity.md): 241 models verified, 222 preview-only; use-weighted shares and the rounded numbers unchanged |
|
||||
| 2026-10-11 | minor | EOS R5 Mark II CR3 verified (raw-parity.md): use-weighted share whose camera decodes 84% → 85%, RAW decoding 76% → 77%; full 60.6% → 60.7%, mainstream depth 71.8% → 72.1%, essentials depth 77.4% → 77.5%; the rounded numbers (~61%, ~49%, ~64%) are unchanged |
|
||||
| 2026-10-10 | minor | RAW measured model by model (raw-parity.md): RAW decoding (76%) and colour (35%) are explicit rows of the full sum (59% → 61%); mainstream uses the use-weighted 84% plus a ×0.85 colour discount (51% → 49%); essentials uses 84% × colour 0.75 (63% → 64%); beta ~14 points |
|
||||
| 2026-10-10 | minor | Full number restored to an additive weighted sum over the dimensions (~59%; method aligned with the standard, no new evidence); hours to ~95% for each of the three audiences; beta distance ~16 points |
|
||||
|
|
|
|||
|
|
@ -885,6 +885,26 @@ const RAW_SAMPLES: &[(&str, &str, &str)] = &[
|
|||
"https://raw.pixls.us/getfile.php/4204/nice/Leica%20-%20D-Lux%207%20-%204:3.RWL",
|
||||
"4f6db6492da83f7f2a5012591e370557249d1ed44d4c948784016605b799327c",
|
||||
),
|
||||
(
|
||||
"mrw-minolta-dimage7hi-words16.mrw",
|
||||
"https://raw.pixls.us/data/Minolta/DiMAGE%207Hi/RAW_MINOLTA_DIMAGE_7HI.MRW",
|
||||
"2919a05757197a5424e02970c016893bca5aa29363a67b62b1a4b36326a10f6a",
|
||||
),
|
||||
(
|
||||
"mrw-minolta-dimage-a1.mrw",
|
||||
"https://raw.pixls.us/data/Minolta/DiMAGE%20A1/RAW_MINOLTA_A1.MRW",
|
||||
"bde1e2cd788e49c9bef20dfdd41be948fb02c2ee198c6b0959a67a3f79fd41c9",
|
||||
),
|
||||
(
|
||||
"mrw-minolta-dimage-a200-gbrg.mrw",
|
||||
"https://raw.pixls.us/data/Minolta/DiMAGE%20A200/RAW_MINOLTA_DIMAGE_A200.MRW",
|
||||
"2a95f6af2168e536dfb6d02b47e3b6119b35b497eebf179d7c20a6e496c26641",
|
||||
),
|
||||
(
|
||||
"mrw-minolta-dynax7d.mrw",
|
||||
"https://raw.pixls.us/data/Minolta/Dynax%207D/PICT3410.MRW",
|
||||
"713819e33e5302ec864d576d57223cfc8dcf46e8d45e0ae744d1fec40290a89f",
|
||||
),
|
||||
];
|
||||
|
||||
fn cmd_corpus(download: bool) -> Result<(), String> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue