diff --git a/crates/engine/src/lens_profiles.rs b/crates/engine/src/lens_profiles.rs index 65144d5b7..6e7f28120 100644 --- a/crates/engine/src/lens_profiles.rs +++ b/crates/engine/src/lens_profiles.rs @@ -46,7 +46,7 @@ pub fn lookup(db: &LensDb, m: &lightcraft_meta::Metadata, w: f64, h: f64, orient // (a user's alias), since the specification fits other makers' lenses too None => { let name = lightcraft_lensdb::spec_name(m.lens_spec?); - db.find_lens(m.lens_make.as_deref(), &name, camera).filter(|l| l.names_exactly(&name))? + db.find_lens_named(m.lens_make.as_deref(), &name, camera)? } }; let crop = frame_crop_factor(camera.map(|c| c.crop_factor), focal, m.focal_length_35mm); diff --git a/crates/lensdb/src/lib.rs b/crates/lensdb/src/lib.rs index cfdda2eaf..93b75f68b 100644 --- a/crates/lensdb/src/lib.rs +++ b/crates/lensdb/src/lib.rs @@ -169,13 +169,24 @@ impl LensDb { /// The best profile for an EXIF lens model (and lens make, when the file has one), preferring lenses for the /// camera's mount. `None` when no name is a convincing match. pub fn find_lens(&self, lens_make: Option<&str>, lens_model: &str, camera: Option<&Camera>) -> Option<&Profile> { + self.best_lens(lens_make, lens_model, camera, |_| true) + } + + /// [`find_lens`](Self::find_lens) among the profiles one of whose names is exactly `name` + /// ([`Profile::names_exactly`]), for names that carry no maker, such as [`spec_name`]s: another maker's lens with + /// the same specification scores as high, and must neither win nor, by coming first, hide the profile named so. + pub fn find_lens_named(&self, lens_make: Option<&str>, name: &str, camera: Option<&Camera>) -> Option<&Profile> { + self.best_lens(lens_make, name, camera, |l| l.names_exactly(name)) + } + + fn best_lens(&self, lens_make: Option<&str>, lens_model: &str, camera: Option<&Camera>, eligible: impl Fn(&Profile) -> bool) -> Option<&Profile> { let exif = tokens(lens_model); if exif.is_empty() { return None; } let make = lens_make.map(tokens).unwrap_or_default(); let mut best: Option<(f64, &Profile)> = None; - for lens in &self.lenses { + for lens in self.lenses.iter().filter(|l| eligible(l)) { let maker = tokens(&lens.maker); if !make.is_empty() && !maker.is_empty() && !maker_matches(&maker, &make) { continue; diff --git a/crates/lensdb/src/tests.rs b/crates/lensdb/src/tests.rs index f87193fbc..435882381 100644 --- a/crates/lensdb/src/tests.rs +++ b/crates/lensdb/src/tests.rs @@ -265,3 +265,25 @@ fn spec_names_need_an_exact_profile_name() { assert!(aliased.names_exactly(&spec_name([24.0, 70.0, 2.8, 2.8]))); assert!(!aliased.names_exactly("24-105mm f/4")); } + +/// Another maker's lens with the same specification scores as high as the aliased one; coming first in the +/// database it must not hide the profile named for the specification. +#[test] +fn a_named_profile_is_found_behind_a_tie() { + let lens = |maker: &str, models: &str| { + format!( + r#"{maker}{models}Test Mount1 + "# + ) + }; + let xml = format!( + r#"{}{}"#, + lens("Otherco", "Otherco 18-55mm f/3.5-5.6"), + lens("Testcam", "Testcam 18-55mm f/3.5-5.6 VR18-55mm f/3.5-5.6"), + ); + let db = LensDb::parse(&xml).unwrap(); + let name = spec_name([18.0, 55.0, 3.5, 5.6]); + assert_eq!(db.find_lens(None, &name, None).unwrap().maker, "Otherco", "a plain lookup keeps the first of a tie"); + assert_eq!(db.find_lens_named(None, &name, None).unwrap().maker, "Testcam"); + assert!(db.find_lens_named(None, "70-200mm f/4", None).is_none()); +}