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:
Gert Pellin
2026-10-14 11:09:25 +02:00
parent a4b53b28cc
commit f9c97ab57b
+101 -2
View File
@@ -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)]));