Merge PR #1324
# Conflicts: # docs/gaps.md # docs/parity-checklist.md # docs/raw-parity.md # docs/whats-new.md
This commit is contained in:
commit
4e78514948
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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(¬e(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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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% |
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue