mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 19:47:35 -06:00
@@ -62,7 +62,7 @@ impl SettingsHashes {
|
||||
}
|
||||
|
||||
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
||||
pub const RENDER_CACHE_VERSION: u64 = 23;
|
||||
pub const RENDER_CACHE_VERSION: u64 = 24;
|
||||
|
||||
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
|
||||
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
|
||||
|
||||
Vendored
+182
-1
@@ -10,6 +10,16 @@
|
||||
//! four bits zero in more than 99% of samples), which we shift down.
|
||||
//! - 12-bit packed (XZ-2): each row is a sequence of little-endian 32-bit words read MSB-first (found by testing
|
||||
//! candidate bit orders for the smoothest image).
|
||||
//! - 12-bit packed in two fields (a few old compacts): IFD0 says uncompressed, 12 bits, many strips, and the strip
|
||||
//! byte counts add up to exactly width × height × 1.5. Samples are packed MSB-first, two per three bytes
|
||||
//! (`b0 << 4 | b1 >> 4`, `(b1 & 15) << 8 | b2`), rows are `width × 1.5` bytes with no padding. Stored rows
|
||||
//! `0..h0` are the even output rows and the rows from `h0` on the odd ones (`h0 = (height + 1) / 2`, height
|
||||
//! odd); the strip table has one gap where the second field starts (follow the offsets) and omits the last row
|
||||
//! of the second field, which sits in the bytes right after the last strip. Derived by black-box analysis of
|
||||
//! six CC0 files (C5060WZ, C7070WZ, SP-510UZ, SP-550UZ, SP-565UZ, SP-570UZ). Their Exif `CFAPattern` states the
|
||||
//! layout (it matches the embedded thumbnails), the maker note's entries `0x1017`/`0x1018` hold the red and
|
||||
//! blue gain (first value, 256 = 1.0; checked against neutral areas of the thumbnail), and nothing states a
|
||||
//! black level or an active area.
|
||||
//! - Olympus's compressed ORF (most interchangeable-lens bodies since ~2008) is not decoded: no permissively
|
||||
//! licensed description exists. It reports [`RawError::Unsupported`]; the embedded preview still works.
|
||||
//! - The colour-filter layout is the file's Exif `CFAPattern`: GRBG on the E-1 and E-400, RGGB on the XZ-2, where
|
||||
@@ -34,6 +44,8 @@ pub(crate) const PREVIEW_START: u16 = 0x0101;
|
||||
pub(crate) const PREVIEW_LENGTH: u16 = 0x0102;
|
||||
const WB_RB: u16 = 0x0100;
|
||||
const BLACK: u16 = 0x0600;
|
||||
const MN_WB_RED: u16 = 0x1017;
|
||||
const MN_WB_BLUE: u16 = 0x1018;
|
||||
const CROP: [u16; 4] = [0x0612, 0x0613, 0x0614, 0x0615];
|
||||
|
||||
/// The Olympus maker note.
|
||||
@@ -98,6 +110,50 @@ pub(crate) fn cfa_from_data(d: &[u16], w: usize, a: Rect) -> Cfa {
|
||||
Cfa::bayer_static(if main < anti { "GRBG" } else { "RGGB" })
|
||||
}
|
||||
|
||||
/// Byte offsets of the stored rows of the two-field 12-bit layout, or `None` unless every condition holds
|
||||
/// exactly: odd height, even width, `strips` (offset, byte count; the whole table, not just the strips that fit the
|
||||
/// rows-per-strip grid, since the field seam leaves a short strip) that are whole rows summing to
|
||||
/// `width × height × 1.5` bytes, exactly one gap between consecutive strips (the second field starts right after
|
||||
/// it, at row `(height + 1) / 2`), and a last row, right after the final strip, inside the file. Returns
|
||||
/// `height + 1` offsets.
|
||||
fn field_rows(strips: &[(u64, u64)], w: usize, h: usize, file_len: usize) -> Option<Vec<usize>> {
|
||||
let rb = w.checked_mul(3)? / 2;
|
||||
if rb == 0 || !w.is_multiple_of(2) || h.is_multiple_of(2) || h < 3 || strips.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let h0 = h.div_ceil(2);
|
||||
let total = strips.iter().try_fold(0u64, |a, s| a.checked_add(s.1))?;
|
||||
if total != (rb as u64).checked_mul(h as u64)? {
|
||||
return None;
|
||||
}
|
||||
let mut rows = Vec::with_capacity(h + 1);
|
||||
let mut gaps = 0;
|
||||
let mut end = None;
|
||||
for &(offset, len) in strips {
|
||||
let next_end = offset.checked_add(len).filter(|e| len > 0 && len % rb as u64 == 0 && *e <= file_len as u64)?;
|
||||
match end {
|
||||
Some(e) if offset < e => return None,
|
||||
// the gap must be the field seam
|
||||
Some(e) if offset > e => {
|
||||
if rows.len() != h0 {
|
||||
return None;
|
||||
}
|
||||
gaps += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
end = Some(next_end);
|
||||
let (offset, rb) = (usize::try_from(offset).ok()?, rb);
|
||||
rows.extend((0..usize::try_from(len).ok()? / rb).map(|r| offset + r * rb));
|
||||
}
|
||||
let tail = usize::try_from(end?).ok()?;
|
||||
if gaps != 1 || rows.len() != h || tail.checked_add(rb)? > file_len {
|
||||
return None;
|
||||
}
|
||||
rows.push(tail);
|
||||
Some(rows)
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let tiff = Tiff::parse(bytes)?;
|
||||
let ifd0 = &tiff.ifds[0];
|
||||
@@ -107,6 +163,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let chunks = info.chunks(bytes.len() as u64);
|
||||
let total: u64 = chunks.iter().map(|c| c.len).sum();
|
||||
let stated = cfa_from_exif(&tiff);
|
||||
let mut two_field = false;
|
||||
let (mut data, bits) = if info.compression != 1 {
|
||||
return Err(RawError::Unsupported(format!("ORF compression {}", info.compression)));
|
||||
} else if total >= (n as u64) * 2 {
|
||||
@@ -125,6 +182,26 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
Vec::new()
|
||||
};
|
||||
(RawData::U16(d), 12)
|
||||
} else if let Some(rows) = (info.bits() == 12 && info.samples_per_pixel == 1 && info.planar != 2 && info.offsets.len() == info.byte_counts.len())
|
||||
.then(|| field_rows(&info.offsets.iter().copied().zip(info.byte_counts.iter().copied()).collect::<Vec<_>>(), w, h, bytes.len()))
|
||||
.flatten()
|
||||
{
|
||||
two_field = true;
|
||||
let d = if mode == Mode::Full || stated.is_none() {
|
||||
let (rb, h0) = (w * 3 / 2, h.div_ceil(2));
|
||||
let mut d = vec![0u16; n];
|
||||
d.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
|
||||
let stored = if y % 2 == 0 { y / 2 } else { h0 + y / 2 };
|
||||
// every offset was checked against the file by `field_rows`
|
||||
if let Some(src) = rows.get(stored).and_then(|&at| bytes.get(at..at + rb)) {
|
||||
unpack_msb(src, 12, row);
|
||||
}
|
||||
});
|
||||
d
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
(RawData::U16(d), 12)
|
||||
} else {
|
||||
return Err(RawError::Unsupported("Olympus compressed ORF".into()));
|
||||
};
|
||||
@@ -165,7 +242,13 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
.as_ref()
|
||||
.and_then(|i| i.f64s(WB_RB))
|
||||
.filter(|v| v.len() >= 2 && v[0] > 0.0 && v[1] > 0.0)
|
||||
.map(|v| [(v[0] / 256.0) as f32, 1.0, (v[1] / 256.0) as f32]);
|
||||
.map(|v| [(v[0] / 256.0) as f32, 1.0, (v[1] / 256.0) as f32])
|
||||
.or_else(|| {
|
||||
// two-field bodies: the red and blue gains lead the entries of the main maker note
|
||||
let m = mn.as_ref().filter(|_| two_field)?;
|
||||
let g = |tag| m.ifd.f64s(tag).and_then(|v| v.first().copied()).filter(|g| *g > 0.0);
|
||||
Some([(g(MN_WB_RED)? / 256.0) as f32, 1.0, (g(MN_WB_BLUE)? / 256.0) as f32])
|
||||
});
|
||||
let white = white_from_data(samples, bits);
|
||||
let mut metadata = lightcraft_meta::from_tiff(&tiff);
|
||||
metadata.width = Some(active.width as u32);
|
||||
@@ -337,6 +420,104 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Output sample (x, y) of the two-field tests: distinct per site, all below 4096.
|
||||
fn field_px(x: usize, y: usize) -> u16 {
|
||||
((y * 331 + x * 17 + 5) % 4096) as u16
|
||||
}
|
||||
|
||||
/// 12 bits MSB-first, two samples per three bytes, for output row `y`.
|
||||
fn packed_row(w: usize, y: usize) -> Vec<u8> {
|
||||
(0..w)
|
||||
.step_by(2)
|
||||
.flat_map(|x| {
|
||||
let (a, b) = (field_px(x, y), field_px(x + 1, y));
|
||||
[(a >> 4) as u8, ((a & 15) << 4 | b >> 8) as u8, b as u8]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A two-field file: strips A1 (two rows), A2 (one row), a gap, B (all but the last row of the second field),
|
||||
/// with the last row of the second field right after B, as the strip table of the old compacts has it (width 8,
|
||||
/// height 5). `table_gap` keeps the gap in the table (else the strips are contiguous and the table is wrong).
|
||||
fn two_field(table_gap: bool) -> Vec<u8> {
|
||||
let (w, h) = (8usize, 5usize);
|
||||
let h0 = h.div_ceil(2);
|
||||
// stored rows: the even output rows, then the odd ones (the last of them is past the picture)
|
||||
let stored: Vec<Vec<u8>> = (0..h0).map(|k| packed_row(w, 2 * k)).chain((0..h0).map(|k| packed_row(w, 2 * k + 1))).collect();
|
||||
let a1 = stored[..2].concat();
|
||||
let a2 = stored[2].clone();
|
||||
let gap = vec![0xffu8; 20];
|
||||
let b = stored[3].clone();
|
||||
let b2 = stored[4].clone();
|
||||
let last = stored[5].clone();
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w as u32]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h as u32]));
|
||||
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![12]));
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
|
||||
ifd.set(t::PHOTOMETRIC, Value::Short(vec![2]));
|
||||
ifd.set(t::MAKE, Value::Ascii("OLYMPUS OPTICAL CO.,LTD".into()));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![a1, a2, gap, b, b2, last] });
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(EXIF_CFA_PATTERN, Value::Undefined(BGGR.to_vec()));
|
||||
ifd.set_child(t::EXIF_IFD, exif);
|
||||
let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap();
|
||||
b[2] = b'R';
|
||||
b[3] = b'S';
|
||||
// keep strips A1, A2, B1 and B2 in the table (drop the gap and the last row): four strips for a grid of three
|
||||
let le = |b: &[u8], at: usize| u32::from_le_bytes(b[at..at + 4].try_into().unwrap());
|
||||
let at0 = le(&b, 4) as usize;
|
||||
let n = u16::from_le_bytes([b[at0], b[at0 + 1]]) as usize;
|
||||
for e in (0..n).map(|i| at0 + 2 + 12 * i) {
|
||||
let tag = u16::from_le_bytes([b[e], b[e + 1]]);
|
||||
if tag != t::STRIP_OFFSETS && tag != t::STRIP_BYTE_COUNTS {
|
||||
continue;
|
||||
}
|
||||
let at = le(&b, e + 8) as usize;
|
||||
let all: Vec<u32> = (0..6).map(|i| le(&b, at + 4 * i)).collect();
|
||||
let keep: Vec<u32> = if table_gap { vec![all[0], all[1], all[3], all[4]] } else { vec![all[0], all[1], all[2], all[3]] };
|
||||
b[e + 4..e + 8].copy_from_slice(&(keep.len() as u32).to_le_bytes());
|
||||
for (i, v) in keep.iter().enumerate() {
|
||||
b[at + 4 * i..at + 4 * i + 4].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_field_12_bit_layout() {
|
||||
let bytes = two_field(true);
|
||||
let r = crate::decode(&bytes).unwrap();
|
||||
let want: Vec<u16> = (0..5).flat_map(|y| (0..8).map(move |x| field_px(x, y))).collect();
|
||||
assert_eq!((r.data.clone(), r.bits, r.width, r.height), (RawData::U16(want), 12, 8, 5));
|
||||
assert_eq!(r.cfa.as_ref().unwrap().name(), "BGGR");
|
||||
assert_eq!(crate::probe_info(&bytes).unwrap(), r.info());
|
||||
// the table without the gap: sizes no longer add up
|
||||
let e = crate::decode(&two_field(false)).map(|_| ());
|
||||
assert!(matches!(e, Err(RawError::Unsupported(_))), "{e:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_field_layout_needs_every_condition() {
|
||||
let chunk = |_index: usize, offset: u64, rows: u64| (offset, rows * 12);
|
||||
// width 8, height 5: rows 0..3 are the first field, the gap, rows 3..5, the last row after them
|
||||
let good = [chunk(0, 100, 2), chunk(1, 124, 1), chunk(2, 200, 2)];
|
||||
let rows = field_rows(&good, 8, 5, 236).unwrap();
|
||||
assert_eq!(rows, [100, 112, 124, 200, 212, 224]);
|
||||
// the last row must be inside the file
|
||||
assert!(field_rows(&good, 8, 5, 235).is_none());
|
||||
// the gap is the field seam: not after two rows, not absent, not twice
|
||||
assert!(field_rows(&[chunk(0, 100, 2), chunk(1, 140, 1), chunk(2, 164, 2)], 8, 5, 300).is_none());
|
||||
assert!(field_rows(&[chunk(0, 100, 2), chunk(1, 124, 1), chunk(2, 136, 2)], 8, 5, 300).is_none());
|
||||
assert!(field_rows(&[chunk(0, 100, 2), chunk(1, 124, 1), chunk(2, 200, 1), chunk(3, 300, 1)], 8, 5, 400).is_none());
|
||||
// overlapping strips, sizes that don't add up, even height, odd width, a single strip
|
||||
assert!(field_rows(&[chunk(0, 100, 2), chunk(1, 120, 1), chunk(2, 200, 2)], 8, 5, 300).is_none());
|
||||
assert!(field_rows(&[chunk(0, 100, 2), chunk(1, 124, 1), chunk(2, 200, 1)], 8, 5, 300).is_none());
|
||||
assert!(field_rows(&good, 8, 4, 300).is_none());
|
||||
assert!(field_rows(&good, 7, 5, 300).is_none());
|
||||
assert!(field_rows(&good[..1], 8, 5, 300).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word16_shifted_and_packed12() {
|
||||
let (w, h) = (16usize, 4usize);
|
||||
|
||||
Reference in New Issue
Block a user