diff --git a/crates/meta/src/exif.rs b/crates/meta/src/exif.rs index 51da692e8..2ce5ff1a2 100644 --- a/crates/meta/src/exif.rs +++ b/crates/meta/src/exif.rs @@ -21,7 +21,7 @@ pub fn try_read_exif(bytes: &[u8]) -> Result { let mut m = from_tiff(&tiff); // Panasonic writes no LensModel, only the name in its maker note (and in a RW2 that note is not in the raw's own Exif) if m.lens_model.is_none() { - m.lens_model = panasonic_lens(data, &tiff).or_else(|| panasonic_jpg_from_raw_lens(&tiff)); + m.lens_model = panasonic_maker_note_lens(data, &tiff); } Ok(m) } @@ -149,6 +149,12 @@ const PANASONIC_LENS_TYPE: u16 = 0x0051; /// RW2/RWL IFD0 tag `0x002E` (JpgFromRaw): a JPEG whose APP1 holds the full Exif, maker note included. const PANASONIC_JPG_FROM_RAW: u16 = 0x002E; +/// The lens name in the Panasonic maker note (tag `0x0051`) of this Exif or RW2 stream (`data` starts at its +/// TIFF header): the note of its own Exif IFD, else, for a raw, the note in its JpgFromRaw's Exif. +pub(crate) fn panasonic_maker_note_lens(data: &[u8], tiff: &Tiff) -> Option { + panasonic_lens(data, tiff).or_else(|| panasonic_jpg_from_raw_lens(tiff)) +} + /// The lens name in a `Panasonic\0` maker note of this Exif stream (`data` starts at its TIFF header). fn panasonic_lens(data: &[u8], tiff: &Tiff) -> Option { let exif = tiff.exif()?; @@ -176,7 +182,7 @@ pub(crate) mod tests { /// An Exif block from a Panasonic body: no LensModel, the lens name in maker note tag 0x0051, whose value /// lies outside the note's IFD at an offset relative to the Exif TIFF header (as Panasonic writes it). - fn panasonic_exif(order: ByteOrder, lens: Option<&str>, lens_model: Option<&str>) -> Vec { + pub(crate) fn panasonic_exif(order: ByteOrder, lens: Option<&str>, lens_model: Option<&str>) -> Vec { let note = |at: u32| { let mut v = b"Panasonic\0\0\0".to_vec(); order.put_u16(&mut v, 1); @@ -220,12 +226,11 @@ pub(crate) mod tests { assert_eq!(read_exif(&panasonic_exif(ByteOrder::Little, None, None)).lens_model, None); } - #[test] - fn rw2_reads_the_lens_from_its_jpg_from_raw() { - // a RW2 has the full Exif only in the APP1 of its JpgFromRaw (IFD0 0x002E), not in its own Exif IFD + /// A RW2 has the full Exif only in the APP1 of its JpgFromRaw (IFD0 0x002E), not in its own Exif IFD. + pub(crate) fn panasonic_rw2(lens: &str) -> Vec { let mut jpg = vec![0xff, 0xd8, 0xff, 0xe1]; let mut app1 = b"Exif\0\0".to_vec(); - app1.extend(panasonic_exif(ByteOrder::Little, Some("LUMIX S 85/F1.8"), None)); + app1.extend(panasonic_exif(ByteOrder::Little, Some(lens), None)); jpg.extend_from_slice(&((app1.len() + 2) as u16).to_be_bytes()); jpg.extend(app1); jpg.extend_from_slice(&[0xff, 0xda, 0, 2, 0xff, 0xd9]); @@ -235,7 +240,12 @@ pub(crate) mod tests { ifd0.set(t::MAKE, Value::Ascii("Panasonic".into())); ifd0.set(PANASONIC_JPG_FROM_RAW, Value::Undefined(jpg)); ifd0.set_child(t::EXIF_IFD, exif); - let rw2 = TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap(); + TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap() + } + + #[test] + fn rw2_reads_the_lens_from_its_jpg_from_raw() { + let rw2 = panasonic_rw2("LUMIX S 85/F1.8"); let m = read_exif(&rw2); assert_eq!(m.lens_model.as_deref(), Some("LUMIX S 85/F1.8")); assert_eq!(m.focal_length, Some(85.0)); diff --git a/crates/meta/src/tags.rs b/crates/meta/src/tags.rs index 39e26097d..3cbab5cc2 100644 --- a/crates/meta/src/tags.rs +++ b/crates/meta/src/tags.rs @@ -1,14 +1,15 @@ //! Every EXIF / TIFF / GPS tag of a file as readable `(group, name, value)` rows (the Info //! panel's "All Metadata"). Names follow the public TIFF 6.0 / EXIF 2.3 / DNG specifications; //! common enumerations are spelled out, everything else is shown as stored. Maker notes and -//! binary blobs are summarised by size. +//! binary blobs are summarised by size; of a maker note only the lens name is listed (the +//! Panasonic one, as Panasonic writes no Exif LensModel). use lightcraft_tiff::{Ifd, Tiff, Value}; /// One metadata row. #[derive(Clone, Debug, PartialEq)] pub struct TagRow { - /// `TIFF`, `EXIF`, `GPS`, `Interop`. + /// `TIFF`, `EXIF`, `GPS`, `Interop`, or a maker note's (`Panasonic`). pub group: &'static str, pub tag: u16, pub name: String, @@ -309,6 +310,13 @@ fn nikon_nef_compression(tiff: &Tiff, bytes: &[u8]) -> Option<&'static str> { nef_compression(mn.ifd.u32(0x0093)?) } +/// The Panasonic maker note's lens name (LensType, `0x0051`) as a row, the name `photo.inspect` shows as the lens. +fn panasonic_lens_row(data: &[u8], tiff: &Tiff, out: &mut Vec) { + if let Some(lens) = crate::exif::panasonic_maker_note_lens(data, tiff) { + out.push(TagRow { group: "Panasonic", tag: 0x0051, name: "Lens Type".into(), value: lens.trim().to_string() }); + } +} + /// Every tag of a parsed TIFF structure (a TIFF-based raw, a TIFF, or an EXIF block): IFD0, /// then the EXIF, GPS and interoperability IFDs. A raw file's Compression row is the sensor /// image's, not IFD0's thumbnail's. @@ -347,10 +355,15 @@ pub fn file_tag_rows(bytes: &[u8]) -> Vec { { row.value = words.to_string(); } + panasonic_lens_row(bytes, &t, &mut rows); return rows; } let Some(exif) = crate::embedded(bytes).exif else { return Vec::new() }; - Tiff::parse(crate::strip_exif_header(&exif)).map(|t| tag_rows(&t)).unwrap_or_default() + let data = crate::strip_exif_header(&exif); + let Ok(t) = Tiff::parse(data) else { return Vec::new() }; + let mut rows = tag_rows(&t); + panasonic_lens_row(data, &t, &mut rows); + rows } #[cfg(test)] @@ -461,4 +474,22 @@ mod tests { let b = [b"II*".to_vec(), ifd(&[entry(0x0103, 3, 1, 8)])].concat(); assert_eq!(compression(&file_tag_rows(&b)), ["Deflate"]); } + + #[test] + fn panasonic_lens_is_listed_from_the_maker_note() { + let lens = |rows: &[TagRow]| rows.iter().filter(|r| r.group == "Panasonic" && r.tag == 0x0051).map(|r| r.value.clone()).collect::>(); + // a RW2: the note is only in the JpgFromRaw's Exif + let rw2 = crate::exif::tests::panasonic_rw2("LUMIX S 85/F1.8"); + assert_eq!(lens(&file_tag_rows(&rw2)), ["LUMIX S 85/F1.8"]); + // a JPEG: the note is in its own Exif + let exif = crate::exif::tests::panasonic_exif(lightcraft_tiff::ByteOrder::Big, Some("LUMIX G 25/F1.7"), None); + let mut jpg = vec![0xff, 0xd8, 0xff, 0xe1]; + jpg.extend_from_slice(&((exif.len() + 8) as u16).to_be_bytes()); + jpg.extend(b"Exif\0\0"); + jpg.extend(exif); + jpg.extend_from_slice(&[0xff, 0xda, 0, 2, 0xff, 0xd9]); + assert_eq!(lens(&file_tag_rows(&jpg)), ["LUMIX G 25/F1.7"]); + // no Panasonic note, no row + assert!(lens(&file_tag_rows(&synthetic_raw(Some(3)))).is_empty()); + } }