mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-13 17:31:09 -06:00
Add flash, exposure bias and serial rules to smart albums (#3821)
* Add flash, exposure bias and serial rules to smart albums Smart albums cannot filter by flash fired, exposure bias or camera serial even though the file metadata is already parsed. Add flash, exposure_bias and camera_serial to Meta, copy them in meta_of and on reload, and add flash, exposureBias and cameraSerial rule fields evaluated in Rule::matches. Test: flash_exposure_bias_and_serial_rules failed before the change and passes after. * Rules editor: Exposure Bias takes negative values Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
eliets2
FredApps
parent
3943279105
commit
2a35db6bcf
@@ -176,6 +176,15 @@ pub struct Meta {
|
||||
pub aperture: Option<f32>,
|
||||
pub shutter: String,
|
||||
pub iso: Option<u32>,
|
||||
/// Whether the flash fired, from Exif `Flash`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub flash: Option<bool>,
|
||||
/// Exposure compensation in EV, from Exif `ExposureBiasValue`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub exposure_bias: Option<f32>,
|
||||
/// 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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -13,6 +13,9 @@ pub(super) fn meta_of(m: &lightcraft_meta::Metadata) -> (Meta, Option<String>) {
|
||||
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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user