# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/raw-parity.md
#	docs/whats-new.md
This commit is contained in:
bflatastic 2026-10-13 11:05:53 +07:00
commit 4e78514948
9 changed files with 217 additions and 10 deletions

View File

@ -2156,6 +2156,37 @@ mod tests {
assert!(e.contains("without an embedded preview"), "{e}"); assert!(e.contains("without an embedded preview"), "{e}");
} }
/// Issue #1275: an uncompressed Leaf MOS (IFD0 is the 16-bit mosaic, no Make tag; Leaf's data block `0x8606`
/// holds the preview) imports as a raw shown from its preview, not as a greyscale image of the mosaic.
#[test]
fn uncompressed_leaf_mos_imports_as_a_preview_only_raw() {
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value, tags as t};
let px: Vec<u8> = (0..40 * 30).flat_map(|i| [(i % 251) as u8, 128, 200]).collect();
let jpeg = encode_jpeg(&EncodeImage::new(40, 30, 3, Samples::U8(&px)), 90, ChromaSubsampling::S444, &EncodeMeta::default()).unwrap();
let mos = |leaf_data: bool| {
let mut ifd0 = IfdBuilder::new();
ifd0.set(t::IMAGE_WIDTH, Value::Long(vec![16]));
ifd0.set(t::IMAGE_LENGTH, Value::Long(vec![12]));
ifd0.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
ifd0.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
ifd0.set(t::PHOTOMETRIC, Value::Short(vec![1]));
ifd0.set(t::COMPRESSION, Value::Short(vec![1]));
ifd0.set_image(ImageData::Tiles { tile_width: 16, tile_height: 12, tiles: vec![vec![0x30; 16 * 12 * 2]] });
if leaf_data {
ifd0.set(0x8606, Value::Undefined([&b"PKTS\0\0\0\x01"[..], &jpeg].concat()));
}
TiffWriter::default().write(&[ifd0]).unwrap()
};
let p = probe_bytes("aptus22.mos", &mos(true)).unwrap();
assert_eq!((p.kind, p.format.as_str(), p.width, p.height), (MediaKind::Raw, "MOS", 40, 30));
assert!(p.preview_only.is_some_and(|why| !why.is_empty()));
let (img, src) = load_bytes(&mos(true), 20).unwrap();
assert_eq!((img.width, img.height, src.raw), (20, 15, false));
// the same grey image without Leaf's block is an ordinary 16-bit TIFF
let plain = probe_bytes("grey.tif", &mos(false)).unwrap();
assert_eq!((plain.kind, plain.preview_only, plain.width, plain.height), (MediaKind::Image, None, 16, 12));
}
/// A GoPro GPR (DNG with VC-5 coded data, no preview anywhere) still imports, described from its /// A GoPro GPR (DNG with VC-5 coded data, no preview anywhere) still imports, described from its
/// headers; loading it says what is wrong instead of naming a bare TIFF compression number. /// headers; loading it says what is wrong instead of naming a bare TIFF compression number.
#[test] #[test]

View File

@ -190,7 +190,10 @@ pub fn probe(bytes: &[u8]) -> Option<RawFormat> {
if make.starts_with("NIKON") && has_cfa { if make.starts_with("NIKON") && has_cfa {
return Some(if t.all_ifds().len() > 1 { RawFormat::Nef } else { RawFormat::Nrw }); return Some(if t.all_ifds().len() > 1 { RawFormat::Nef } else { RawFormat::Nrw });
} }
if make.starts_with("SONY") && sony_raw { // Hasselblad's Sony-built bodies (the Lunar is a NEX-7 inside) write a Sony ARW under their own Make. Sony's
// raw coding gives them away: compression 32767 or the raw IFD's tone-curve tag 0x7010, which no 3FR / FFF has
let sony_coded = || t.all_ifds().iter().any(|i| i.u16(lightcraft_tiff::tags::COMPRESSION) == Some(32767) || i.contains(0x7010));
if (make.starts_with("SONY") && sony_raw) || (make.starts_with("HASSELBLAD") && sony_coded()) {
return Some(RawFormat::Arw); return Some(RawFormat::Arw);
} }
if (make.starts_with("PENTAX") || make.starts_with("RICOH")) && pentax_raw { if (make.starts_with("PENTAX") || make.starts_with("RICOH")) && pentax_raw {
@ -209,12 +212,23 @@ pub fn probe(bytes: &[u8]) -> Option<RawFormat> {
if dng::is_plain_cfa_tiff(&t, bytes) { if dng::is_plain_cfa_tiff(&t, bytes) {
return Some(RawFormat::CfaTiff); return Some(RawFormat::CfaTiff);
} }
if has_cfa || thumbnail_shell(&t, bytes.len()).is_some() || is_preview_container(ifd0) { if has_cfa || thumbnail_shell(&t, bytes.len()).is_some() || is_preview_container(ifd0) || is_leaf_mosaic(ifd0) {
return Some(RawFormat::OtherTiff); return Some(RawFormat::OtherTiff);
} }
None None
} }
/// Leaf's private data block in IFD0, tag `0x8606` (`LeafData` in the ExifTool EXIF tag-name documentation, which
/// says the Leaf tags "are found in .MOS images from Leaf digital camera backs").
const LEAF_DATA: u16 = 0x8606;
/// An uncompressed Leaf MOS: IFD0 is the 16-bit mosaic itself (compression 1, BlackIsZero, one sample per pixel),
/// with no Make tag and no CFA photometric, so only Leaf's data block marks it as a raw (issue #1275). A rendered
/// RGB image that kept the block stays an ordinary image.
fn is_leaf_mosaic(ifd0: &lightcraft_tiff::Ifd) -> bool {
ifd0.contains(LEAF_DATA) && ifd0.u16(lightcraft_tiff::tags::SAMPLES_PER_PIXEL).unwrap_or(1) == 1
}
/// Whether the Exif maker note starts with the signature of the Pentax layouts (`AOC\0` or `PENTAX \0`). /// Whether the Exif maker note starts with the signature of the Pentax layouts (`AOC\0` or `PENTAX \0`).
fn has_pentax_maker_note(t: &Tiff, bytes: &[u8]) -> bool { fn has_pentax_maker_note(t: &Tiff, bytes: &[u8]) -> bool {
let Some(e) = t.exif().and_then(|e| e.get(lightcraft_tiff::tags::MAKER_NOTE)) else { return false }; let Some(e) = t.exif().and_then(|e| e.get(lightcraft_tiff::tags::MAKER_NOTE)) else { return false };
@ -1071,6 +1085,66 @@ mod tests {
assert_eq!(probe(&write(&[ifd])), Some(RawFormat::OtherTiff)); assert_eq!(probe(&write(&[ifd])), Some(RawFormat::OtherTiff));
} }
/// Issue #1275: an uncompressed Leaf MOS (Aptus 22) is a full-size 16-bit grey IFD0 with no Make tag; only Leaf's
/// data block (`0x8606`) says it is a raw. It must not import as a greyscale image of its mosaic. The same IFD
/// without the block, or an RGB image carrying it, stays an ordinary image.
#[test]
fn uncompressed_leaf_mos_is_a_raw() {
let mosaic = |leaf_data: bool| {
let mut ifd = IfdBuilder::new();
ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0]));
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![16]));
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![12]));
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
ifd.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::BLACK_IS_ZERO]));
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
ifd.set_image(ImageData::Tiles { tile_width: 16, tile_height: 12, tiles: vec![vec![0x40; 16 * 12 * 2]] });
if leaf_data {
ifd.set(LEAF_DATA, Value::Undefined(b"PKTS\0\0\0\x01".to_vec()));
}
ifd
};
let bytes = TiffWriter::new(lightcraft_tiff::ByteOrder::Big, false).write(&[mosaic(true)]).unwrap();
assert_eq!(probe(&bytes), Some(RawFormat::OtherTiff));
assert!(matches!(decode(&bytes), Err(RawError::Unsupported(_))));
assert!(matches!(probe_info(&bytes), Err(RawError::Unsupported(_))));
assert_eq!(probe(&write(&[mosaic(false)])), None, "a 16-bit grey TIFF");
let mut rgb = rgb_ifd(16, 12);
rgb.set(LEAF_DATA, Value::Undefined(b"PKTS\0\0\0\x01".to_vec()));
assert_eq!(probe(&write(&[rgb])), None, "a rendered RGB image that kept the block");
}
/// Issue #1275: a Hasselblad Lunar is a Sony NEX-7 ARW under Make HASSELBLAD. Sony's raw coding (compression 32767
/// or the tone-curve tag `0x7010`) sends it to the ARW reader; a Hasselblad file without it does not go there.
#[test]
fn hasselblad_make_with_sony_raw_coding_is_arw() {
let file = |make: &str, compression: u16, tone_curve: bool| {
let mut raw = IfdBuilder::new();
raw.set(t::IMAGE_WIDTH, Value::Long(vec![64]));
raw.set(t::IMAGE_LENGTH, Value::Long(vec![2]));
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![12]));
raw.set(t::COMPRESSION, Value::Short(vec![compression]));
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
if tone_curve {
raw.set(0x7010, Value::Short(vec![8000, 10400, 12900, 14100]));
}
raw.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![vec![0; 128]] });
let mut ifd0 = rgb_ifd(4, 3);
ifd0.set(t::MAKE, Value::Ascii(make.into()));
ifd0.set(t::MODEL, Value::Ascii("Lunar".into()));
ifd0.add_sub_ifd(raw);
write(&[ifd0])
};
assert_eq!(probe(&file("HASSELBLAD", 32767, false)), Some(RawFormat::Arw));
assert_eq!(probe(&file("Hasselblad", 1, true)), Some(RawFormat::Arw));
let lunar = decode(&file("HASSELBLAD", 32767, true)).unwrap();
assert_eq!((lunar.format, lunar.width, lunar.height, lunar.bits), (RawFormat::Arw, 64, 2, 14));
// a Hasselblad CFA image in another coding is not an ARW
assert_ne!(probe(&file("HASSELBLAD", 1, false)), Some(RawFormat::Arw));
assert_ne!(probe(&file("HASSELBLAD", 7, false)), Some(RawFormat::Arw));
}
// --- TIFF raws that describe their raw IFD the way DNG does but carry no DNG version tag --- // --- TIFF raws that describe their raw IFD the way DNG does but carry no DNG version tag ---
/// 8 x 6 samples, 16 bits, value `1000 + 100 y + x`. /// 8 x 6 samples, 16 bits, value `1000 + 100 y + x`.

View File

@ -202,9 +202,28 @@ const TAG2010_WB: &[(&[&str], usize)] = &[
), ),
]; ];
/// Maker-note `SonyModelID` (`0xb001`, int16u, ExifTool Sony tag documentation).
const MN_SONY_MODEL_ID: u16 = 0xb001;
/// `SonyModelID` values (ExifTool Sony tag documentation) of the Sony bodies Hasselblad sold under its own Make and
/// model (the Lunar, a NEX-7; the Stellar and Stellar II, an RX100 and RX100 II; the HV, an SLT-A99), so that
/// [`TAG2010_WB`] finds their layout. Measured on the CC0 Lunar (raw.pixls.us 4051): Make HASSELBLAD, Model Lunar,
/// SonyModelID 289 (issue #1275).
const SONY_MODEL_IDS: &[(u64, &str)] = &[(289, "NEX-7"), (294, "SLT-A99"), (297, "DSC-RX100"), (308, "DSC-RX100M2")];
/// The Sony body for the model-keyed tables: the Exif `model` of a Sony-branded file, else the one the maker note's
/// `SonyModelID` names (a Lunar says "Lunar"), else `model` as it is.
fn sony_body<'a>(make: &str, model: &'a str, mn: Option<&makernote::MakerNote>) -> &'a str {
if make.trim().to_ascii_uppercase().starts_with("SONY") {
return model;
}
let id = mn.and_then(|m| m.ifd.u64(MN_SONY_MODEL_ID));
SONY_MODEL_IDS.iter().find(|(i, _)| Some(*i) == id).map_or(model, |(_, name)| name)
}
/// White balance from the maker note's enciphered `Tag2010` block, the fallback described at [`TAG2010_WB`]. /// White balance from the maker note's enciphered `Tag2010` block, the fallback described at [`TAG2010_WB`].
/// `model` is the Exif model; Sony appends a regional "V" to some names (SLT-A77V), which the documented lists /// `model` is the Sony body ([`sony_body`]); Sony appends a regional "V" to some names (SLT-A77V), which the
/// omit. /// documented lists omit.
fn tag2010_wb(model: &str, block: &[u8], order: lightcraft_tiff::ByteOrder) -> Option<[f32; 3]> { fn tag2010_wb(model: &str, block: &[u8], order: lightcraft_tiff::ByteOrder) -> Option<[f32; 3]> {
let model = model.trim(); let model = model.trim();
let &(_, offset) = TAG2010_WB.iter().find(|(models, _)| models.iter().any(|m| model == *m || model.strip_suffix('V') == Some(*m)))?; let &(_, offset) = TAG2010_WB.iter().find(|(models, _)| models.iter().any(|m| model == *m || model.strip_suffix('V') == Some(*m)))?;
@ -710,11 +729,12 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
.exif() .exif()
.and_then(|e| e.get(t::MAKER_NOTE)) .and_then(|e| e.get(t::MAKER_NOTE))
.and_then(|e| makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, &ifd0.string(t::MAKE).unwrap_or_default())); .and_then(|e| makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, &ifd0.string(t::MAKE).unwrap_or_default()));
let body = sony_body(&ifd0.string(t::MAKE).unwrap_or_default(), &model, mn.as_ref());
let wb = raw let wb = raw
.f64s(WB_RGGB) .f64s(WB_RGGB)
.and_then(|v| rggb_gains(&v)) .and_then(|v| rggb_gains(&v))
.or_else(|| rggb_gains(&sr2()?.shorts(WB_RGGB)?)) .or_else(|| rggb_gains(&sr2()?.shorts(WB_RGGB)?))
.or_else(|| mn.as_ref().and_then(|m| tag2010_wb(&model, m.ifd.bytes(MN_TAG2010)?, m.order))) .or_else(|| mn.as_ref().and_then(|m| tag2010_wb(body, m.ifd.bytes(MN_TAG2010)?, m.order)))
.or_else(|| sr2_old_wb(sr2()?)); .or_else(|| sr2_old_wb(sr2()?));
let crop = match (raw.u64s(CROP_TOP_LEFT).as_deref(), raw.u64s(CROP_SIZE).as_deref()) { let crop = match (raw.u64s(CROP_TOP_LEFT).as_deref(), raw.u64s(CROP_SIZE).as_deref()) {
(Some([x, y]), Some([cw, ch])) if *cw > 0 && *ch > 0 => Rect::new(*x as usize, *y as usize, *cw as usize, *ch as usize).clipped(w, h), (Some([x, y]), Some([cw, ch])) if *cw > 0 && *ch > 0 => Rect::new(*x as usize, *y as usize, *cw as usize, *ch as usize).clipped(w, h),
@ -1191,6 +1211,73 @@ mod tests {
file file
} }
/// Issue #1275: a Hasselblad Lunar (Make HASSELBLAD, Model Lunar) is a NEX-7. Its maker note (header-less, as the
/// Lunar's is) names the body by `SonyModelID`, which picks the NEX-7's `Tag2010` layout for the fallback white
/// balance; without the ID, or with one that isn't listed, there is no layout to read.
#[test]
fn rebadged_body_takes_its_tag2010_layout_from_the_sony_model_id() {
use lightcraft_tiff::{ByteOrder::Little, IfdBuilder, ImageData, TiffWriter, Value};
let file = |make: &str, model: &str, id: Option<u16>| {
let mut block = vec![0u8; 4600];
for (i, v) in [639u16, 256, 368].iter().enumerate() {
block[4480 + 2 * i..4482 + 2 * i].copy_from_slice(&v.to_le_bytes());
}
let block = encipher(&block);
let note = |at: usize| -> Vec<u8> {
let entries = 1 + u16::from(id.is_some());
let mut n = entries.to_le_bytes().to_vec();
n.extend(MN_TAG2010.to_le_bytes());
n.extend(7u16.to_le_bytes());
n.extend((block.len() as u32).to_le_bytes());
n.extend(((at + 2 + 12 * entries as usize + 4) as u32).to_le_bytes());
if let Some(id) = id {
n.extend(MN_SONY_MODEL_ID.to_le_bytes());
n.extend(3u16.to_le_bytes());
n.extend(1u32.to_le_bytes());
n.extend(id.to_le_bytes());
n.extend([0, 0]);
}
n.extend([0; 4]);
n.extend(&block);
n
};
let mut raw = IfdBuilder::new();
raw.set(t::IMAGE_WIDTH, Value::Long(vec![32]));
raw.set(t::IMAGE_LENGTH, Value::Long(vec![4]));
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![12]));
raw.set(t::PHOTOMETRIC, Value::Short(vec![photometric::CFA]));
raw.set(t::COMPRESSION, Value::Short(vec![32767]));
raw.set(TONE_CURVE, Value::Short(vec![8000, 10400, 12900, 14100]));
raw.set_image(ImageData::Strips { rows_per_strip: 4, strips: vec![vec![0u8; 32 * 4]] });
let mut ifd0 = IfdBuilder::new();
ifd0.set(t::MAKE, Value::Ascii(make.into()));
ifd0.set(t::MODEL, Value::Ascii(model.into()));
ifd0.set_child(t::EXIF_IFD, IfdBuilder::new().with(t::MAKER_NOTE, Value::Undefined(note(0))));
ifd0.add_sub_ifd(raw);
let mut file = TiffWriter::new(Little, false).write(&[ifd0]).unwrap();
let parsed = Tiff::parse(&file).unwrap();
let e = parsed.exif().unwrap().get(t::MAKER_NOTE).unwrap();
let at = e.offset as usize;
file[at..at + e.count()].copy_from_slice(&note(at));
file
};
let wb = |make: &str, model: &str, id: Option<u16>| {
let bytes = file(make, model, id);
assert_eq!(crate::probe(&bytes), Some(RawFormat::Arw), "{make} {model}");
let full = crate::decode(&bytes).unwrap();
assert_eq!(crate::probe_info(&bytes).unwrap().wb_multipliers, full.wb_multipliers);
full.wb_multipliers
};
let nex7 = Some([639.0 / 256.0, 1.0, 368.0 / 256.0]);
assert_eq!(wb("HASSELBLAD", "Lunar", Some(289)), nex7);
assert_eq!(wb("SONY", "NEX-7", None), nex7, "the Sony body itself");
assert_eq!(wb("HASSELBLAD", "Lunar", None), None, "no SonyModelID");
assert_eq!(wb("HASSELBLAD", "Lunar", Some(288)), None, "an ID that is not listed (NEX-5N)");
// a Sony file keeps its Exif model whatever the ID says
assert_eq!(wb("SONY", "ILCE-1", Some(289)), None);
assert_eq!(sony_body("HASSELBLAD", "HV", None), "HV");
}
#[test] #[test]
fn dsc_r1_sr2_subifd_gives_black_wb_and_crop() { fn dsc_r1_sr2_subifd_gives_black_wb_and_crop() {
use lightcraft_tiff::ByteOrder::Little; use lightcraft_tiff::ByteOrder::Little;

View File

@ -25,6 +25,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion and Sony ILCE-7RM4A distortion | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) | | 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion and Sony ILCE-7RM4A distortion | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) |
| 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) | | 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) |
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12, Leica 14 (the Q3 by a recorded check of 38 private DNGs). 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**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12, Leica 14 (the Q3 by a recorded check of 38 private DNGs). 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**: 236 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) |
| 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) | | 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) | | 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) | | 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) |
@ -80,6 +81,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| 2026-10-12 | minor | Gap 22: Linux previews use the display profile from colord | | 2026-10-12 | minor | Gap 22: Linux previews use the display profile from colord |
| 2026-10-12 | minor | Gap 27: the CPU ↔ GPU equivalence suite passes on an AMD Radeon 8060S (Vulkan, Linux), the first non-Apple GPU verified; 15–30 h → 13–27 h; gap not closed | | 2026-10-12 | minor | Gap 27: the CPU ↔ GPU equivalence suite passes on an AMD Radeon 8060S (Vulkan, Linux), the first non-Apple GPU verified; 15–30 h → 13–27 h; gap not closed |
| 2026-10-12 | minor | Gap 25: Render to DNG lands as an export format; lossy DNG and AVIF import remain | | 2026-10-12 | minor | Gap 25: Render to DNG lands as an export format; lossy DNG and AVIF import remain |
| 2026-10-12 | minor | Gap 10 shrinks: the Hasselblad Lunar (a Sony NEX-7 ARW under its own Make) decodes and is verified, 236 models (#1275) |
| 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 | 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 | 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 | | 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

View File

@ -17,6 +17,8 @@ ranked in [gaps.md](gaps.md). Details per format: [cr3.md](cr3.md),
| Decoded from sensor data and **verified** on a real file | **236** (16%) | measured: [raw/evidence.tsv](raw/evidence.tsv) | | Decoded from sensor data and **verified** on a real file | **236** (16%) | measured: [raw/evidence.tsv](raw/evidence.tsv) |
| Decoder path should handle it, **unverified** | **941** (65%) | measured from the format rules (631 of them DNG, mostly phones) | | Decoder path should handle it, **unverified** | **941** (65%) | measured from the format rules (631 of them DNG, mostly phones) |
| **Preview-only** (embedded JPEG) | **233** (16%) | measured | | **Preview-only** (embedded JPEG) | **233** (16%) | measured |
| Decoder path should handle it, **unverified** | **942** (65%) | measured from the format rules (632 of them DNG, mostly phones) |
| **Preview-only** (embedded JPEG) | **232** (16%) | measured |
| **Unsupported** (not imported, refused or decodes wrongly) | **36** (2%) | measured | | **Unsupported** (not imported, refused or decodes wrongly) | **36** (2%) | measured |
| Models added since 2017 (Camera Raw 10+): verified / unverified / preview / unsupported | 83 / 639 / 24 / 8 of 754 | measured | | Models added since 2017 (Camera Raw 10+): verified / unverified / preview / unsupported | 83 / 639 / 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 | | **Share of photographers whose camera fully works** (decodes from sensor data), use-weighted | **~85%** | estimated: measured classes × written usage weights × per-format confidence |
@ -77,7 +79,7 @@ committed table is stale.
| Leica | 61 | 14 | 47 | 0 | 0 | — | | Leica | 61 | 14 | 47 | 0 | 0 | — |
| Xiaomi, Oppo, LG, Huawei, OnePlus, Motorola, Nokia | 139 | 0 | 139 | 0 | 0 | — (DNG) | | Xiaomi, Oppo, LG, Huawei, OnePlus, Motorola, Nokia | 139 | 0 | 139 | 0 | 0 | — (DNG) |
| Pentax / Ricoh | 60 | 4 | 56 | 0 | 0 | — | | Pentax / Ricoh | 60 | 4 | 56 | 0 | 0 | — |
| Hasselblad | 34 | 0 | 2 | 32 | 0 | Hasselblad-compressed 3FR / FFF | | Hasselblad | 34 | 1 | 2 | 31 | 0 | Hasselblad-compressed 3FR / FFF |
| Phase One / Leaf / Mamiya | 59 | 0 | 0 | 43 | 16 | IIQ, MOS preview; TIFF raws and MEF not imported | | Phase One / Leaf / Mamiya | 59 | 0 | 0 | 43 | 16 | IIQ, MOS preview; TIFF raws and MEF not imported |
| Casio | 26 | 0 | 26 | 0 | 0 | — (DNG) | | Casio | 26 | 0 | 26 | 0 | 0 | — (DNG) |
| DJI | 21 | 0 | 21 | 0 | 0 | — (DNG) | | DJI | 21 | 0 | 21 | 0 | 0 | — (DNG) |
@ -99,7 +101,7 @@ committed table is stale.
| Pentax / Ricoh | 1.5% | 94% | 14% | | | Pentax / Ricoh | 1.5% | 94% | 14% | |
| Leica | 1% | 96% | 21% | the Q3 verified | | Leica | 1% | 96% | 21% | the Q3 verified |
| DJI | 1% | 95% | 0% | | | DJI | 1% | 95% | 0% | |
| Medium format and others | 0.5% | 23% | 0% | IIQ, 3FR, X3F | | Medium format and others | 0.5% | 23% | 1% | IIQ, 3FR, X3F |
| **Weighted** | 100% | **85%** | **42%** | | | **Weighted** | 100% | **85%** | **42%** | |
About 5 in 6 raw photographers' cameras decode, but only about 2 in 5 use a body we have actually About 5 in 6 raw photographers' cameras decode, but only about 2 in 5 use a body we have actually
@ -123,7 +125,7 @@ checked, and Canon, the largest bucket, is the weakest of the big four.
| NEF | uncompressed, lossless, lossy type 1 / 2, lossy after split | ✅ | D5100, D7000, D7500, Z f corpus; ~60 NEFs by hand; #633; D4 and D90 image area and white level measured against the camera JPEG | | NEF | uncompressed, lossless, lossy type 1 / 2, lossy after split | ✅ | D5100, D7000, D7500, Z f corpus; ~60 NEFs by hand; #633; D4 and D90 image area and white level measured against the camera JPEG |
| NEF | packed 14-bit stored under a compressed tag (Z 6II uncompressed) | ⬜ | #769; fix in PR #671 (not merged) | | NEF | packed 14-bit stored under a compressed tag (Z 6II uncompressed) | ⬜ | #769; fix in PR #671 (not merged) |
| NEF | High Efficiency / HE★ (JPEG XS) | ⬜ | named, preview (#193); needs a JPEG XS decoder | | NEF | High Efficiency / HE★ (JPEG XS) | ⬜ | named, preview (#193); needs a JPEG XS decoder |
| ARW | uncompressed, cRAW (ARW2), lossless LJ92 (L / M / S) | ✅ | 86 bodies (#535); M/S black level 1024 | | ARW | uncompressed, cRAW (ARW2), lossless LJ92 (L / M / S) | ✅ | 86 bodies (#535); M/S black level 1024; the Hasselblad Lunar, a NEX-7 under Make HASSELBLAD, routed by Sony's raw coding (#1275) |
| ARW | packed 12-bit (A900) | ✅ | one CC0 file; A850 unmeasured | | ARW | packed 12-bit (A900) | ✅ | one CC0 file; A850 unmeasured |
| ARW | ARW6 Compressed / HQ (compression 32766; ILCE-7RM6) | ✅ | 8 CC0 files; 12-bit mode unsupported; `docs/arw6-compression.md` | | ARW | ARW6 Compressed / HQ (compression 32766; ILCE-7RM6) | ✅ | 8 CC0 files; 12-bit mode unsupported; `docs/arw6-compression.md` |
| ARW | 12-bit compressed of DSLR-A200…A390 | ⬜ | preview (#535) | | ARW | 12-bit compressed of DSLR-A200…A390 | ⬜ | preview (#535) |
@ -146,7 +148,7 @@ checked, and Canon, the largest bucket, is the weakest of the big four.
| DNG | GoPro GPR (VC-5) | ⬜ | refused | | DNG | GoPro GPR (VC-5) | ⬜ | refused |
| 3FR / FFF | DNG-like layouts | 🟡 | decoded when uncompressed or LJ92 | | 3FR / FFF | DNG-like layouts | 🟡 | decoded when uncompressed or LJ92 |
| 3FR / FFF | Hasselblad compression | ⬜ | preview | | 3FR / FFF | Hasselblad compression | ⬜ | preview |
| IIQ, MOS, MRW, ERF, KDC, X3F | all | ⬜ | 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) |
| DCR, MEF, MFW, DXO, GPR, TIFF raws | all | ⬜ | not imported | | DCR, MEF, MFW, DXO, GPR, TIFF raws | all | ⬜ | not imported |
### Compressed ORF: a clean-room starting point ### Compressed ORF: a clean-room starting point
@ -275,6 +277,7 @@ Beyond decoding:
| 2026-10-12 | minor | Canon PowerShot G9: as-shot white balance from WBInfo (`0x0029`, the row of the preset in ShotInfo), issue #1273; render unchanged (the camera-look fit already absorbed it), As Shot now the camera's; counts unchanged | | 2026-10-12 | minor | Canon PowerShot G9: as-shot white balance from WBInfo (`0x0029`, the row of the preset in ShotInfo), issue #1273; render unchanged (the camera-look fit already absorbed it), As Shot now the camera's; counts unchanged |
| 2026-10-12 | minor | Olympus E-10: as-shot white balance (`0x1017`/`0x1018`) and black level (`0x1012` in `0x102c` ValidBits units) from the main maker note, issue #1273; ΔE76 against its thumbnail 18.5 → 11.5, camera-look fit now accepted; counts unchanged | | 2026-10-12 | minor | Olympus E-10: as-shot white balance (`0x1017`/`0x1018`) and black level (`0x1012` in `0x102c` ValidBits units) from the main maker note, issue #1273; ΔE76 against its thumbnail 18.5 → 11.5, camera-look fit now accepted; counts unchanged |
| 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-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-10 | minor | Compressed ORF: pointer to the two Olympus patents that describe the method, as a clean-room starting point | | 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-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% | | 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% |

View File

@ -521,7 +521,7 @@ Hasselblad H5D-50c 3FR 2013 preview 3FR is opened from its embedded JPEG only
Hasselblad H5D-60 3FR 2013 preview 3FR is opened from its embedded JPEG only Hasselblad H5D-60 3FR 2013 preview 3FR is opened from its embedded JPEG only
Hasselblad H6D-100c 3FR 2015 preview 3FR is opened from its embedded JPEG only Hasselblad H6D-100c 3FR 2015 preview 3FR is opened from its embedded JPEG only
Hasselblad HV ARW 2013 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) Hasselblad HV ARW 2013 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr)
Hasselblad Lunar 3FR 2012 preview 3FR is opened from its embedded JPEG only Hasselblad Lunar 3FR 2012 verified issue #1275: raw.pixls.us 4051 (a Sony NEX-7 ARW under Make HASSELBLAD) decodes from sensor data; black 512 and SR2SubIFD WB as on a NEX-7
Hasselblad Stellar II 3FR 2015 preview 3FR is opened from its embedded JPEG only Hasselblad Stellar II 3FR 2015 preview 3FR is opened from its embedded JPEG only
Hasselblad X1D 3FR 2015 preview 3FR is opened from its embedded JPEG only Hasselblad X1D 3FR 2015 preview 3FR is opened from its embedded JPEG only
Hasselblad X2D II 100C (**) 3FR 2024 preview 3FR is opened from its embedded JPEG only Hasselblad X2D II 100C (**) 3FR 2024 preview 3FR is opened from its embedded JPEG only

Can't render this file because it has a wrong number of fields in line 2.

View File

@ -31,6 +31,7 @@ Fujifilm X-T4** verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
Fujifilm X-T5 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES) Fujifilm X-T5 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
Fujifilm X-T50 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES) Fujifilm X-T50 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
Google Pixel 2 XL verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES) Google Pixel 2 XL verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
Hasselblad Lunar verified issue #1275: raw.pixls.us 4051 (a Sony NEX-7 ARW under Make HASSELBLAD) decodes from sensor data; black 512 and SR2SubIFD WB as on a NEX-7
Leica C (Typ 112) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format Leica C (Typ 112) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format
Leica D-Lux (Typ 109) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format Leica D-Lux (Typ 109) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format
Leica D-Lux 3 verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body Leica D-Lux 3 verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body

Can't render this file because it contains an unexpected character in line 2 and column 3.

View File

@ -58,6 +58,13 @@
- When GPU rendering is on but no graphics processor can be found, LightCraft now says so once at start-up instead - When GPU rendering is on but no graphics processor can be found, LightCraft now says so once at start-up instead
of quietly rendering everything on the CPU, much more slowly. Help ▸ System Info says why. On NixOS this happens of quietly rendering everything on the CPU, much more slowly. Help ▸ System Info says why. On NixOS this happens
when a `cargo` build runs outside `nix develop` (no Vulkan loader); start it from there. when a `cargo` build runs outside `nix develop` (no Vulkan loader); start it from there.
## 2026-10-12
### Leaf and Hasselblad raws
- The Hasselblad Lunar's raws (a Sony NEX-7 inside) now develop from their sensor data with every editing tool,
instead of opening as their embedded preview. Photos imported earlier switch over with Photo ▸ Reload from Disk.
- An uncompressed Leaf Aptus 22 MOS no longer opens as a dark, colourless picture of its sensor mosaic: it imports
as a raw and shows its embedded preview (preview only, with the reason), as the compressed MOS files do.
## 2026-10-11 ## 2026-10-11