mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-25 02:30:15 -06:00
Merge PR #1703
This commit is contained in:
+111
-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_maker_note_lens(data, &tiff);
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
fn text(ifd: Option<&Ifd>, tag: u16) -> Option<String> {
|
||||
@@ -138,11 +144,114 @@ 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 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<String> {
|
||||
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<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).
|
||||
pub(crate) 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);
|
||||
}
|
||||
|
||||
/// 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<u8> {
|
||||
let mut jpg = vec![0xff, 0xd8, 0xff, 0xe1];
|
||||
let mut app1 = b"Exif\0\0".to_vec();
|
||||
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]);
|
||||
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);
|
||||
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));
|
||||
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)]));
|
||||
|
||||
+34
-3
@@ -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<TagRow>) {
|
||||
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<TagRow> {
|
||||
{
|
||||
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* | ||||