M11.3: Olympus ORF decoding (uncompressed)
16-bit LE words (12-bit-in-top-bits detected and shifted), 12-bit rows of LE 32-bit words read MSB-first (black-box), maker-note ImageProcessing crop, BlackLevel2, WB_RBLevels for old- and new-style notes, CFA phase from the data (GRBG/RGGB), CameraSettings preview. Compressed ORF reports Unsupported (documented gap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f86346c5d
commit
f5b28bfca9
|
|
@ -72,8 +72,8 @@ fn services() -> Services {
|
|||
.add_filter(
|
||||
"Photos",
|
||||
&[
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "nef", "nrw", "arw", "raf", "rw2", "pef", "psd", "jxl", "gif",
|
||||
"bmp",
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "nef", "nrw", "arw", "raf", "rw2", "pef", "orf", "psd", "jxl",
|
||||
"gif", "bmp",
|
||||
],
|
||||
)
|
||||
.pick_files()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
//! Formats: DNG (uncompressed, lossless JPEG, Deflate incl. floating point, tiled/stripped, CFA and LinearRaw),
|
||||
//! Canon CR2, Nikon NEF (uncompressed), Sony ARW (uncompressed, lossless), Fujifilm RAF (uncompressed Bayer and
|
||||
//! X-Trans), Panasonic RW2 (packed 12/14-bit), Pentax PEF
|
||||
//! (uncompressed, Huffman). See the crate README for sources and
|
||||
//! (uncompressed, Huffman), Olympus ORF (uncompressed). See the crate README for sources and
|
||||
//! gaps. The decoders never panic on malformed input.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
|
|
@ -149,6 +149,7 @@ pub fn decode(bytes: &[u8]) -> Result<RawImage> {
|
|||
RawFormat::Raf => vendor::raf::decode(bytes),
|
||||
RawFormat::Rw2 => vendor::rw2::decode(bytes),
|
||||
RawFormat::Pef => vendor::pef::decode(bytes),
|
||||
RawFormat::Orf => vendor::orf::decode(bytes),
|
||||
other => Err(RawError::Unsupported(format!("{other:?} files are not decoded yet"))),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ pub fn embedded_preview(bytes: &[u8]) -> Option<Vec<u8>> {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Olympus: CameraSettings preview
|
||||
if let Some(p) = crate::vendor::orf::preview(bytes) {
|
||||
found.push(p);
|
||||
}
|
||||
found.into_iter().filter(|s| is_dct_jpeg(s)).max_by_key(|s| s.len()).map(|s| trim_eoi(s).to_vec())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
pub mod arw;
|
||||
pub mod cr2;
|
||||
pub mod nef;
|
||||
pub mod orf;
|
||||
pub mod pef;
|
||||
pub mod raf;
|
||||
pub mod rw2;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
//! Olympus ORF — uncompressed variants.
|
||||
//!
|
||||
//! Sources: TIFF 6.0 (the `IIRO`/`MMOR` container is a TIFF with a different magic number), the ExifTool
|
||||
//! Olympus tag-name documentation (maker-note sub-directories `0x2020` CameraSettings — `0x0101/0x0102`
|
||||
//! PreviewImageStart/Length — and `0x2040` ImageProcessing — `0x0100` WB_RBLevels, `0x0600` BlackLevel2,
|
||||
//! `0x0612–0x0615` CropLeft/Top/Width/Height) and our own black-box analysis of CC0 samples from raw.pixls.us
|
||||
//! (E-1, E-400, XZ-2):
|
||||
//!
|
||||
//! - 16 bits per sample: little-endian words; some bodies (E-1, E-400) store 12-bit values in the top bits (the low
|
||||
//! 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).
|
||||
//! - 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 files carry no CFA tag. The green diagonal is found from the data (the diagonal whose two samples differ
|
||||
//! least); bodies with greens on the main diagonal (E-1, E-400) are GRBG, the others (XZ-2) RGGB — verified by
|
||||
//! colour renders.
|
||||
|
||||
use super::white_from_data;
|
||||
use crate::tiffraw::{Packing, read_image};
|
||||
use crate::unpack::unpack_msb;
|
||||
use crate::{BlackLevel, Cfa, ColorData, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::image::chunk_bytes;
|
||||
use lightcraft_tiff::makernote::MakerNote;
|
||||
use lightcraft_tiff::{Ifd, Tiff, Value, makernote, tags as t};
|
||||
use rayon::prelude::*;
|
||||
|
||||
pub(crate) const CAMERA_SETTINGS: u16 = 0x2020;
|
||||
const IMAGE_PROCESSING: u16 = 0x2040;
|
||||
pub(crate) const PREVIEW_START: u16 = 0x0101;
|
||||
pub(crate) const PREVIEW_LENGTH: u16 = 0x0102;
|
||||
const WB_RB: u16 = 0x0100;
|
||||
const BLACK: u16 = 0x0600;
|
||||
const CROP: [u16; 4] = [0x0612, 0x0613, 0x0614, 0x0615];
|
||||
|
||||
/// The Olympus maker note.
|
||||
pub(crate) fn maker_note(bytes: &[u8], tiff: &Tiff) -> Option<MakerNote> {
|
||||
let make = tiff.find(t::MAKE).and_then(|e| e.value.as_str()).unwrap_or_default().to_string();
|
||||
let e = tiff.exif().and_then(|e| e.get(t::MAKER_NOTE))?;
|
||||
makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, &make)
|
||||
}
|
||||
|
||||
/// A maker-note sub-directory: an IFD pointer (offset relative to the note's base) in new-style notes, or the
|
||||
/// IFD stored inline as an undefined blob (offsets relative to the base) in old-style ones.
|
||||
pub(crate) fn sub_ifd(bytes: &[u8], mn: &MakerNote, tag: u16) -> Option<Ifd> {
|
||||
let e = mn.ifd.get(tag)?;
|
||||
let at = match &e.value {
|
||||
Value::Undefined(_) | Value::Byte(_) => e.offset,
|
||||
_ => mn.base.checked_add(mn.ifd.u64(tag)?)?,
|
||||
};
|
||||
let opts = lightcraft_tiff::ParseOptions { max_ifds: 4, max_depth: 1, follow_children: false, ..Default::default() };
|
||||
lightcraft_tiff::parse_ifd_at(bytes, at, mn.order, mn.base, false, &opts).ok().map(|(i, _)| i)
|
||||
}
|
||||
|
||||
/// Unpack one row of 12-bit samples stored as little-endian 32-bit words read MSB-first.
|
||||
pub(crate) fn unpack_row_le32_msb(src: &[u8], bits: u32, out: &mut [u16]) {
|
||||
let swapped: Vec<u8> = src
|
||||
.chunks(4)
|
||||
.flat_map(|c| {
|
||||
let mut w = [0u8; 4];
|
||||
w[..c.len()].copy_from_slice(c);
|
||||
[w[3], w[2], w[1], w[0]]
|
||||
})
|
||||
.collect();
|
||||
unpack_msb(&swapped, bits, out);
|
||||
}
|
||||
|
||||
/// GRBG when the greens sit on the main diagonal of the 2×2 cell at the sensor origin, else RGGB.
|
||||
pub(crate) fn cfa_from_data(d: &[u16], w: usize, a: Rect) -> Cfa {
|
||||
let (x0, y0) = (a.x + ((a.width / 8) & !1), a.y + ((a.height / 8) & !1));
|
||||
let (x1, y1) = (a.x + a.width * 7 / 8, a.y + a.height * 7 / 8);
|
||||
let (mut main, mut anti) = (0u64, 0u64);
|
||||
for y in (y0..y1.saturating_sub(1)).step_by(16) {
|
||||
for x in (x0..x1.saturating_sub(1)).step_by(8) {
|
||||
let (x, y) = (x & !1, y & !1);
|
||||
let p = |dx: usize, dy: usize| d[(y + dy) * w + x + dx] as i64;
|
||||
main += (p(0, 0) - p(1, 1)).unsigned_abs();
|
||||
anti += (p(1, 0) - p(0, 1)).unsigned_abs();
|
||||
}
|
||||
}
|
||||
Cfa::bayer(if main < anti { "GRBG" } else { "RGGB" }).expect("static")
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
let tiff = Tiff::parse(bytes)?;
|
||||
let ifd0 = &tiff.ifds[0];
|
||||
let info = ifd0.image()?;
|
||||
let (w, h) = (info.width as usize, info.height as usize);
|
||||
let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= crate::MAX_SAMPLES).ok_or(RawError::Limit("image too large"))?;
|
||||
let chunks = info.chunks(bytes.len() as u64);
|
||||
let total: u64 = chunks.iter().map(|c| c.len).sum();
|
||||
let (mut data, bits) = if info.compression != 1 {
|
||||
return Err(RawError::Unsupported(format!("ORF compression {}", info.compression)));
|
||||
} else if total >= (n as u64) * 2 {
|
||||
let d = read_image(bytes, &info, tiff.order, Packing::Word16)?;
|
||||
(d, 16)
|
||||
} else if total * 8 >= (n as u64) * 12 && total * 8 < (n as u64) * 13 && chunks.len() == 1 {
|
||||
let src = chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("ORF strip outside file".into()))?;
|
||||
let stride = src.len() / h;
|
||||
let mut d = vec![0u16; n];
|
||||
d.par_chunks_mut(w).enumerate().for_each(|(y, row)| unpack_row_le32_msb(&src[y * stride..(y + 1) * stride], 12, row));
|
||||
(RawData::U16(d), 12)
|
||||
} else {
|
||||
return Err(RawError::Unsupported("Olympus compressed ORF".into()));
|
||||
};
|
||||
let RawData::U16(ref mut samples) = data else { return Err(RawError::Unsupported("float ORF".into())) };
|
||||
let bits = if bits == 16 && samples.par_iter().filter(|v| *v & 15 != 0).count() * 100 <= n {
|
||||
// 12-bit values stored in the top bits
|
||||
samples.par_iter_mut().for_each(|v| *v >>= 4);
|
||||
12
|
||||
} else if bits == 16 {
|
||||
let mx = samples.iter().step_by(31).max().copied().unwrap_or(0);
|
||||
if mx < 4096 {
|
||||
12
|
||||
} else if mx < 16384 {
|
||||
14
|
||||
} else {
|
||||
16
|
||||
}
|
||||
} else {
|
||||
bits
|
||||
};
|
||||
|
||||
let mn = maker_note(bytes, &tiff);
|
||||
let ip = mn.as_ref().and_then(|m| sub_ifd(bytes, m, IMAGE_PROCESSING));
|
||||
let active = match ip.as_ref().map(|i| CROP.map(|tag| i.u64(tag).map(|v| v as usize))) {
|
||||
Some([Some(x), Some(y), Some(cw), Some(ch)]) if cw > 0 && ch > 0 && x + cw <= w && y + ch <= h => Rect::new(x, y, cw, ch),
|
||||
_ => Rect::new(0, 0, w, h),
|
||||
};
|
||||
let cfa = cfa_from_data(samples, w, active);
|
||||
let black = match ip.as_ref().and_then(|i| i.f64s(BLACK)).as_deref() {
|
||||
Some(v @ [_, _, _, _]) => {
|
||||
let a = cfa.shifted(active.x, active.y);
|
||||
let values = a.pattern.iter().map(|&c| [v[0], (v[1] + v[2]) / 2.0, v[3]][c as usize] as f32).collect();
|
||||
BlackLevel { repeat_rows: 2, repeat_cols: 2, values, ..Default::default() }
|
||||
}
|
||||
_ => BlackLevel::uniform(0.0),
|
||||
};
|
||||
let wb = ip
|
||||
.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]);
|
||||
let white = white_from_data(samples, bits);
|
||||
let mut metadata = lightcraft_meta::from_tiff(&tiff);
|
||||
metadata.width = Some(active.width as u32);
|
||||
metadata.height = Some(active.height as u32);
|
||||
let img = RawImage {
|
||||
format: RawFormat::Orf,
|
||||
width: w,
|
||||
height: h,
|
||||
cpp: 1,
|
||||
data,
|
||||
cfa: Some(cfa),
|
||||
bits,
|
||||
black,
|
||||
white: vec![white],
|
||||
active_area: active,
|
||||
crop: Rect::new(0, 0, active.width, active.height),
|
||||
orientation: Orientation::from_exif(ifd0.u16(t::ORIENTATION).unwrap_or(1)),
|
||||
color: ColorData::default(),
|
||||
wb_multipliers: wb,
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate()?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
/// The large preview JPEG referenced by CameraSettings `PreviewImageStart/Length` (relative to the note base).
|
||||
pub(crate) fn preview(bytes: &[u8]) -> Option<&[u8]> {
|
||||
let tiff = Tiff::parse(bytes).ok()?;
|
||||
let mn = maker_note(bytes, &tiff)?;
|
||||
let cs = sub_ifd(bytes, &mn, CAMERA_SETTINGS)?;
|
||||
let start = mn.base.checked_add(cs.u64(PREVIEW_START)?)? as usize;
|
||||
let len = cs.u64(PREVIEW_LENGTH)? as usize;
|
||||
bytes.get(start..start.checked_add(len)?.min(bytes.len()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter};
|
||||
|
||||
fn orf(w: u32, h: u32, bits: u16, strip: Vec<u8>) -> Vec<u8> {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
|
||||
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![bits]));
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
|
||||
ifd.set(t::PHOTOMETRIC, Value::Short(vec![1]));
|
||||
ifd.set(t::MAKE, Value::Ascii("OLYMPUS IMAGING CORP.".into()));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![strip] });
|
||||
let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap();
|
||||
b[2] = b'R';
|
||||
b[3] = b'O';
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word16_shifted_and_packed12() {
|
||||
let (w, h) = (16usize, 4usize);
|
||||
let px: Vec<u16> = (0..w * h).map(|i| ((i * 211) % 4096) as u16).collect();
|
||||
let words: Vec<u8> = px.iter().flat_map(|v| (v << 4).to_le_bytes()).collect();
|
||||
let bytes = orf(w as u32, h as u32, 16, words);
|
||||
assert_eq!(crate::probe(&bytes), Some(RawFormat::Orf));
|
||||
let r = crate::decode(&bytes).unwrap();
|
||||
assert_eq!((r.data.clone(), r.bits), (RawData::U16(px.clone()), 12));
|
||||
// 12-bit: MSB-first stream, every 32-bit word byte-swapped
|
||||
let mut be = Vec::new();
|
||||
for p in px.chunks(2) {
|
||||
let v = (p[0] as u32) << 12 | p[1] as u32;
|
||||
be.extend_from_slice(&[(v >> 16) as u8, (v >> 8) as u8, v as u8]);
|
||||
}
|
||||
let le: Vec<u8> = be.chunks(4).flat_map(|c| [c[3], c[2], c[1], c[0]]).collect();
|
||||
let bytes = orf(w as u32, h as u32, 12, le);
|
||||
assert_eq!(crate::decode(&bytes).unwrap().data, RawData::U16(px));
|
||||
let bytes = orf(w as u32, h as u32, 16, vec![0; 40]);
|
||||
assert!(matches!(crate::decode(&bytes), Err(RawError::Unsupported(_))));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue