diff --git a/crates/catalog/src/model.rs b/crates/catalog/src/model.rs index cc28609941..69a99bd803 100644 --- a/crates/catalog/src/model.rs +++ b/crates/catalog/src/model.rs @@ -176,6 +176,15 @@ pub struct Meta { pub aperture: Option, pub shutter: String, pub iso: Option, + /// Whether the flash fired, from Exif `Flash`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flash: Option, + /// Exposure compensation in EV, from Exif `ExposureBiasValue`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exposure_bias: Option, + /// The camera body's serial number. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub serial_number: String, /// Place (sublocation, e.g. a landmark). pub location: String, pub city: String, diff --git a/crates/catalog/src/rules.rs b/crates/catalog/src/rules.rs index 50960cc9a8..6b8c272dc0 100644 --- a/crates/catalog/src/rules.rs +++ b/crates/catalog/src/rules.rs @@ -105,6 +105,9 @@ pub const FIELDS: &[(&str, &str, Kind)] = &[ ("aperture", "Aperture", Kind::Number), ("shutterSpeed", "Shutter Speed", Kind::Number), ("iso", "ISO Speed", Kind::Number), + ("flash", "Flash", Kind::Bool), + ("exposureBias", "Exposure Bias", Kind::Number), + ("cameraSerial", "Camera Serial Number", Kind::Text), // Location ("location", "Location", Kind::Text), ("city", "City", Kind::Text), @@ -136,7 +139,7 @@ pub const FIELD_GROUPS: &[(&str, &[&str])] = &[ ("Date", &["captureDate", "importDate", "editDate"]), ("Keywords & People", &["keywords", "keywordCount", "person", "personCount"]), ("Description", &["title", "caption", "altText", "creator", "copyright", "copyrightStatus"]), - ("Camera Info", &["camera", "lens", "focalLength", "aperture", "shutterSpeed", "iso"]), + ("Camera Info", &["camera", "lens", "focalLength", "aperture", "shutterSpeed", "iso", "flash", "exposureBias", "cameraSerial"]), ("Location", &["location", "city", "state", "country", "hasGps"]), ("Size", &["longEdge", "shortEdge", "aspect", "megapixels"]), ("Develop", &["edited", "cropped", "treatment"]), @@ -1099,6 +1102,9 @@ impl Rule { "editDate" => date_op(op, p.edited.as_deref(), value), "shutterSpeed" => num_op(op, crate::parse_shutter_seconds(&m.shutter), &shutter_value(value)), "iso" => num_op(op, m.iso.map(|v| v as f64), value), + "flash" => yes(m.flash.unwrap_or(false)), + "exposureBias" => num_op(op, m.exposure_bias.map(f32_as_decimal), value), + "cameraSerial" => text(&m.serial_number), "aperture" => num_op(op, m.aperture.map(f32_as_decimal), value), "focalLength" => num_op(op, m.focal_mm.map(f32_as_decimal), value), "longEdge" | "shortEdge" => { @@ -1146,7 +1152,7 @@ impl Rule { "text" | "filePath" | "location" => COST_TEXT, "keywords" | "person" | "personCount" | "stacked" | "copyName" | "fileName" | "extension" | "format" | "title" | "caption" | "altText" | "city" | "state" | "country" | "camera" | "lens" | "creator" | "copyright" | "captureDate" | "importDate" - | "editDate" | "shutterSpeed" | "aspect" => COST_SMALL, + | "editDate" | "shutterSpeed" | "aspect" | "flash" | "cameraSerial" | "exposureBias" => COST_SMALL, _ => COST_SCALAR, }, } diff --git a/crates/catalog/src/rules/tests.rs b/crates/catalog/src/rules/tests.rs index 8d9d649b9a..e3685997a0 100644 --- a/crates/catalog/src/rules/tests.rs +++ b/crates/catalog/src/rules/tests.rs @@ -385,6 +385,27 @@ fn in_the_last_survives_hostile_counts() { set_now(None); } +/// Flash, exposure bias and camera serial number can be tested by a rule, like the other +/// camera info; an unrecorded one is empty (or zero bias is no bias). +#[test] +fn flash_exposure_bias_and_serial_rules() { + let cat = Catalog::new(); + // the values arrive with the photo's metadata, as fields on its meta, as probed from a file + let mut v = serde_json::to_value(photo(1)).unwrap(); + let meta = v.get_mut("meta").unwrap(); + meta["flash"] = json!(true); + meta["exposure_bias"] = json!(-0.67); + meta["serial_number"] = json!("SN-X2"); + let p: Photo = serde_json::from_value(v).unwrap(); + let m = |field: &str, op: &str, value: serde_json::Value| rs(json!({"rules": [{"field": field, "op": op, "value": value}]})).matches(&p, &cat); + assert!(m("flash", "is", json!(true)) && !m("flash", "is", json!(false))); + assert!(m("exposureBias", "lt", json!(0)) && m("exposureBias", "between", json!([-1.0, -0.3]))); + assert!(m("cameraSerial", "contains", json!("x2"))); + assert_eq!(field_group("flash"), Some("Camera Info")); + assert_eq!(field_group("exposureBias"), Some("Camera Info")); + assert_eq!(field_group("cameraSerial"), Some("Camera Info")); +} + /// Every rule's value is checked against its field, so a rule that can't mean anything is /// reported instead of quietly matching nothing (or everything). Each problem says which rule /// it is about: "rule 2", or "rule 2.1" inside a group. diff --git a/crates/engine/src/cmd/convert.rs b/crates/engine/src/cmd/convert.rs index 24b4165278..c3dd8733f1 100644 --- a/crates/engine/src/cmd/convert.rs +++ b/crates/engine/src/cmd/convert.rs @@ -239,6 +239,11 @@ fn fill_missing_meta(id: lightcraft_catalog::PhotoId, ph: &lightcraft_catalog::P m.focal_mm = m.focal_mm.or(src.focal_mm); m.aperture = m.aperture.or(src.aperture); m.iso = m.iso.or(src.iso); + m.flash = m.flash.or(src.flash); + m.exposure_bias = m.exposure_bias.or(src.exposure_bias); + if m.serial_number.is_empty() { + m.serial_number = src.serial_number.clone(); + } m.gps = m.gps.or(src.gps); // the camera's focus point: photos imported before it was read get it here if m.focus.is_none() { diff --git a/crates/engine/src/files/probe.rs b/crates/engine/src/files/probe.rs index a63446401e..481d4aabc9 100644 --- a/crates/engine/src/files/probe.rs +++ b/crates/engine/src/files/probe.rs @@ -13,6 +13,9 @@ pub(super) fn meta_of(m: &lightcraft_meta::Metadata) -> (Meta, Option) { aperture: m.f_number.map(|f| f as f32), shutter, iso: m.iso, + flash: m.flash.map(|f| f.fired), + exposure_bias: m.exposure_bias.map(|v| v as f32), + serial_number: m.serial_number.clone().unwrap_or_default(), location: m.sublocation.clone().unwrap_or_default(), city: m.city.clone().unwrap_or_default(), state: m.state.clone().unwrap_or_default(), diff --git a/crates/ui-egui/src/dialogs/rules_editor.rs b/crates/ui-egui/src/dialogs/rules_editor.rs index 67f5e4bb49..23677cfd70 100644 --- a/crates/ui-egui/src/dialogs/rules_editor.rs +++ b/crates/ui-egui/src/dialogs/rules_editor.rs @@ -80,6 +80,7 @@ fn number_value(ui: &mut egui::Ui, v: &mut Value, field: &str) { "rating" => (0.0, 5.0, 0.05), "iso" => (0.0, 409_600.0, 10.0), "aperture" => (0.0, 64.0, 0.05), + "exposureBias" => (-20.0, 20.0, 0.05), "keywordCount" | "personCount" => (0.0, 1000.0, 0.05), "longEdge" | "shortEdge" => (0.0, 100_000.0, 10.0), _ => (0.0, 100_000.0, 0.5),