From 9db9811759a43aed9f74aa7c07123b433b46c3ee Mon Sep 17 00:00:00 2001 From: eliets2 <287273517+eliets2@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:19:52 +0300 Subject: [PATCH] Read Canon maker-note lens when LensModel is missing LightCraft already fills a missing lens_model from the Panasonic and Nikon maker notes in try_read_exif, but never from the Canon maker note, so Canon bodies such as the EOS 5D that omit EXIF LensModel cannot match any lens profile even when the maker note records the lens. Add a canon_maker_note_lens fallback in try_read_exif, parsed with lightcraft_tiff::makernote::parse_makernote (Canon notes are the Plain kind), mirroring panasonic_maker_note_lens; add a unit test in exif.rs that feeds an Exif block with a Canon maker note but no LensModel and asserts lens_model is populated. Test: canon_maker_note_lens_fills_lens_model failed before the change and passes after. --- crates/meta/src/exif.rs | 67 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/meta/src/exif.rs b/crates/meta/src/exif.rs index adcba0f80..fa589594e 100644 --- a/crates/meta/src/exif.rs +++ b/crates/meta/src/exif.rs @@ -27,9 +27,9 @@ pub fn try_read_exif_with(bytes: &[u8], opts: &ParseOptions) -> Result Option { panasonic_lens(data, &Tiff::parse(data).ok()?) } +/// Canon maker note tag `0x0001` (LensType in the ExifTool Canon tag names): the lens name as ASCII. Canon +/// plain maker notes put the IFD at the note start with offsets relative to the enclosing TIFF header. +const CANON_LENS_TYPE: u16 = 0x0001; + +/// The lens name in a Canon maker note of this Exif stream (`data` starts at its TIFF header): the note of +/// its own Exif IFD. Only consulted when Make is Canon, so other vendors' plain notes are never read as lenses. +fn canon_maker_note_lens(data: &[u8], tiff: &Tiff) -> Option { + let make = tiff.ifds.first().and_then(|i| i.string(t::MAKE)).unwrap_or_default(); + if !make.trim().to_ascii_uppercase().starts_with("CANON") { + return None; + } + let exif = tiff.exif()?; + let e = exif.get(t::MAKER_NOTE)?; + let mn = lightcraft_tiff::makernote::parse_makernote(data, e.offset, e.count() as u64, tiff.order, &make)?; + text(Some(&mn.ifd), CANON_LENS_TYPE) +} + /// Nikon maker note tag `0x0084` (Lens in the ExifTool Nikon tag names): shortest and longest focal length, then the /// widest aperture at each, as rationals, like Exif LensSpecification. Zero for a lens that doesn't report itself. const NIKON_LENS: u16 = 0x0084; @@ -297,6 +314,52 @@ pub(crate) mod tests { assert_eq!(crate::extract(&rw2).lens_model.as_deref(), Some("LUMIX S 85/F1.8")); } + /// Canon maker note tag holding the lens name as ASCII in a plain maker-note IFD (offsets relative to + /// the TIFF header, as Canon writes it). + const CANON_LENS_TAG: u16 = 0x0001; + + /// An Exif block from a Canon EOS 5D: `Canon` / `Canon EOS 5D` in IFD0, no LensModel tag, and a maker + /// note whose plain IFD records the lens name at an offset relative to the TIFF header. + fn canon_exif(order: ByteOrder, lens: Option<&str>) -> Vec { + let name = lens.unwrap_or("Canon EF 50mm f/1.8 II"); + let note = |at: u32| { + let mut v = Vec::new(); + order.put_u16(&mut v, 1); + order.put_u16(&mut v, CANON_LENS_TAG); + order.put_u16(&mut v, 2); // ASCII + order.put_u32(&mut v, name.len() as u32); + // Value offset = at + size of IFD header (num_entries[2] + entry[12] + next_ifd[4] = 18) + // At this point v.len() == 10 (num_entries + tag + type + count); value field (4) + next_ifd (4) = 8 more. + let value_offset = at + v.len() as u32 + 8; + order.put_u32(&mut v, value_offset); + 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![(50, 1)])); + let mut ifd0 = IfdBuilder::new(); + ifd0.set(t::MAKE, Value::Ascii("Canon".into())); + ifd0.set(t::MODEL, Value::Ascii("Canon EOS 5D".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 canon_maker_note_lens_fills_lens_model() { + for order in [ByteOrder::Little, ByteOrder::Big] { + let m = read_exif(&canon_exif(order, Some("Canon EF 50mm f/1.8 II"))); + assert_eq!(m.lens_model.as_deref(), Some("Canon EF 50mm f/1.8 II")); + } + } + /// An Exif block from a Nikon body whose maker note records the lens (tag 0x0084) and no LensModel or /// LensSpecification, as the D700, D750, D80 and D5300 write it. fn nikon_exif(make: &str, lens: Option<[(u32, u32); 4]>, spec: Option<[(u32, u32); 4]>) -> Vec {