mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-25 18:47:42 -06:00
Metadata: read the Panasonic lens name from maker note tag 0x0051 (#1671)
Panasonic writes no Exif LensModel. The lens name is in its maker note (LensType, 0x0051), and a RW2 or RWL keeps that maker note only in the Exif of its embedded JpgFromRaw (IFD0 0x002E), not in the raw's own Exif IFD. When LensModel is absent, read it from the maker note, and for a raw from the JpgFromRaw's Exif. Checked on the CC0 samples DC-GX9 gx9_1_1.RW2 (LUMIX G 25/F1.7) and DC-S5 P1047510.RW2 (LUMIX S 85/F1.8), also through the sparse header plan an import uses.
This commit is contained in:
+101
-2
@@ -16,8 +16,14 @@ pub fn read_exif(bytes: &[u8]) -> Metadata {
|
||||
|
||||
/// Like [`read_exif`] but reports a structurally invalid TIFF header / IFD0.
|
||||
pub fn try_read_exif(bytes: &[u8]) -> Result<Metadata, TiffError> {
|
||||
let tiff = Tiff::parse(strip_exif_header(bytes))?;
|
||||
Ok(from_tiff(&tiff))
|
||||
let data = strip_exif_header(bytes);
|
||||
let tiff = Tiff::parse(data)?;
|
||||
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));
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
fn text(ifd: Option<&Ifd>, tag: u16) -> Option<String> {
|
||||
@@ -138,11 +144,104 @@ fn read_gps(g: &Ifd) -> Option<Gps> {
|
||||
Some(Gps { latitude: lat, longitude: lon, altitude })
|
||||
}
|
||||
|
||||
/// Panasonic maker note tag `0x0051` (LensType in the ExifTool Panasonic tag names): the lens name, ASCII.
|
||||
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 a `Panasonic\0` maker note of this Exif stream (`data` starts at its TIFF header).
|
||||
fn panasonic_lens(data: &[u8], tiff: &Tiff) -> Option<String> {
|
||||
let exif = tiff.exif()?;
|
||||
if !exif.bytes(t::MAKER_NOTE)?.starts_with(b"Panasonic\0") {
|
||||
return None;
|
||||
}
|
||||
let e = exif.get(t::MAKER_NOTE)?;
|
||||
let make = tiff.ifds.first().and_then(|i| i.string(t::MAKE)).unwrap_or_default();
|
||||
let mn = lightcraft_tiff::makernote::parse_makernote(data, e.offset, e.count() as u64, tiff.order, &make)?;
|
||||
text(Some(&mn.ifd), PANASONIC_LENS_TYPE)
|
||||
}
|
||||
|
||||
/// The lens name from the Exif of a RW2's (or Leica RWL's) embedded JpgFromRaw.
|
||||
fn panasonic_jpg_from_raw_lens(tiff: &Tiff) -> Option<String> {
|
||||
let jpg = tiff.ifds.first()?.bytes(PANASONIC_JPG_FROM_RAW)?;
|
||||
let exif = crate::jpeg_segments(jpg).exif?;
|
||||
let data = strip_exif_header(&exif);
|
||||
panasonic_lens(data, &Tiff::parse(data).ok()?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use lightcraft_tiff::{ByteOrder, IfdBuilder, TiffWriter, Value};
|
||||
|
||||
/// 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<u8> {
|
||||
let note = |at: u32| {
|
||||
let mut v = b"Panasonic\0\0\0".to_vec();
|
||||
order.put_u16(&mut v, 1);
|
||||
order.put_u16(&mut v, if lens.is_some() { PANASONIC_LENS_TYPE } else { 0x0001 });
|
||||
order.put_u16(&mut v, 2);
|
||||
let name = format!("{:<34}\0", lens.unwrap_or("0210"));
|
||||
order.put_u32(&mut v, name.len() as u32);
|
||||
order.put_u32(&mut v, at + 30);
|
||||
order.put_u32(&mut v, 0);
|
||||
v.extend_from_slice(name.as_bytes());
|
||||
v
|
||||
};
|
||||
let build = |at: u32| {
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(t::MAKER_NOTE, Value::Undefined(note(at)));
|
||||
exif.set(t::FOCAL_LENGTH, Value::Rational(vec![(25, 1)]));
|
||||
if let Some(l) = lens_model {
|
||||
exif.set(t::LENS_MODEL, Value::Ascii(l.into()));
|
||||
}
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(t::MAKE, Value::Ascii("Panasonic".into()));
|
||||
ifd0.set_child(t::EXIF_IFD, exif);
|
||||
TiffWriter::new(order, false).write(&[ifd0]).unwrap()
|
||||
};
|
||||
// the note's own offset is only known once it is laid out; its length doesn't change
|
||||
let first = build(0);
|
||||
let at = Tiff::parse(&first).unwrap().exif().unwrap().get(t::MAKER_NOTE).unwrap().offset as u32;
|
||||
build(at)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panasonic_maker_note_names_the_lens() {
|
||||
for order in [ByteOrder::Little, ByteOrder::Big] {
|
||||
let m = read_exif(&panasonic_exif(order, Some("LUMIX G 25/F1.7"), None));
|
||||
assert_eq!(m.lens_model.as_deref(), Some("LUMIX G 25/F1.7"));
|
||||
assert_eq!(m.focal_length, Some(25.0));
|
||||
}
|
||||
// Exif's own LensModel wins, and a note without 0x0051 leaves the lens empty
|
||||
let m = read_exif(&panasonic_exif(ByteOrder::Little, Some("LUMIX G 25/F1.7"), Some("Other 25mm")));
|
||||
assert_eq!(m.lens_model.as_deref(), Some("Other 25mm"));
|
||||
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
|
||||
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));
|
||||
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]);
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(t::FOCAL_LENGTH, Value::Rational(vec![(85, 1)]));
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
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();
|
||||
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));
|
||||
assert_eq!(crate::extract(&rw2).lens_model.as_deref(), Some("LUMIX S 85/F1.8"));
|
||||
}
|
||||
|
||||
pub(crate) fn sample_exif(order: ByteOrder) -> Vec<u8> {
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(t::EXPOSURE_TIME, Value::Rational(vec![(1, 250)]));
|
||||
|
||||
Reference in New Issue
Block a user