Skip to main content

flashraw_camera_db/
lib.rs

1//! # flashraw-camera-db
2//!
3//! Static camera database compiled from `data/cameras.json` at build time
4//! (see `build.rs`). Lookups are binary searches over a sorted, normalised key
5//! index — no parsing happens at runtime.
6//!
7//! The database adds per-camera knowledge (crops, masked areas, black/white levels,
8//! CFA layout, colour matrices, decoder hints). Decoders must not require an entry
9//! to decode a file: standards-based metadata comes first, the database refines it.
10
11#[cfg(feature = "tooling")]
12pub mod tooling;
13
14/// Per-ISO sensor levels.
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct SensorInfo {
17    pub black: i32,
18    pub white: i32,
19    /// Per-CFA-colour black levels (in CFA pattern order), if specified.
20    pub black_colors: &'static [i32],
21    /// ISO range this entry applies to (0 = unbounded).
22    pub iso_min: u32,
23    pub iso_max: u32,
24    pub iso_list: &'static [u32],
25}
26
27impl SensorInfo {
28    pub fn applies_to(&self, iso: u32) -> bool {
29        if !self.iso_list.is_empty() {
30            return self.iso_list.contains(&iso);
31        }
32        (self.iso_min == 0 || iso >= self.iso_min) && (self.iso_max == 0 || iso <= self.iso_max)
33    }
34}
35
36/// A camera database entry.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct Camera {
39    pub make: &'static str,
40    pub model: &'static str,
41    pub mode: &'static str,
42    pub canonical_make: &'static str,
43    pub canonical_model: &'static str,
44    /// `(model string as written by camera, canonical alias name)`.
45    pub aliases: &'static [(&'static str, &'static str)],
46    pub cfa: Option<&'static str>,
47    /// `[x, y, width, height]`; width/height <= 0 are relative to the right/bottom edge.
48    pub crop: Option<[i32; 4]>,
49    pub black_areas_v: &'static [[u32; 2]],
50    pub black_areas_h: &'static [[u32; 2]],
51    pub sensors: &'static [SensorInfo],
52    /// XYZ(D65) → camera, scaled by 10000.
53    pub color_matrix: &'static [i32],
54    pub hints: &'static [(&'static str, &'static str)],
55    pub no_size_match: bool,
56    /// Orientation policy override and vendor-record layout.
57    pub orientation: Option<CameraOrientation>,
58    /// The camera's own image area per active-area size (measured from files).
59    pub image_crops: &'static [ImageCrop],
60}
61
62/// The image area a camera produces from an active area of `area` size.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct ImageCrop {
65    /// Active-area `[width, height]`.
66    pub area: [u32; 2],
67    /// `[x, y, width, height]`, x/y relative to the active area's top-left corner.
68    pub crop: [u32; 4],
69}
70
71/// Per-camera orientation data (see `orientation` in docs/camera-db.md).
72#[derive(Debug, Clone, Copy, PartialEq)]
73pub struct CameraOrientation {
74    /// Policy name overriding the make's rule ("" = use the rule).
75    pub policy: &'static str,
76    /// Byte offset of the orientation-sensor field in the vendor camera-info record.
77    pub maker_note_offset: Option<u32>,
78    /// Byte offset of the Auto Rotate setting in the same record.
79    pub auto_rotate_offset: Option<u32>,
80}
81
82/// Orientation policy for a canonical make, optionally narrowed by a model prefix.
83#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct OrientationRule {
85    pub canonical_make: &'static str,
86    /// Canonical model prefix ("" = all models).
87    pub model_prefix: &'static str,
88    pub policy: &'static str,
89}
90
91/// Fallback colour data (prefix matched), used when no exact camera entry has a matrix.
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct FallbackMatrix {
94    pub canonical_make: &'static str,
95    pub model_prefix: &'static str,
96    /// Black level hint (0 = unknown).
97    pub black: i32,
98    /// White level hint (0 = unknown).
99    pub white: i32,
100    /// XYZ(D65) → camera, scaled by 10000.
101    pub color_matrix: &'static [i32],
102}
103
104/// Rebadged / regional model names sharing a canonical model's data.
105#[derive(Debug, Clone, Copy, PartialEq)]
106pub struct AliasGroup {
107    pub canonical_make: &'static str,
108    pub model: &'static str,
109    pub aliases: &'static [&'static str],
110}
111
112/// Sensor geometry selected by exact raw buffer size (bodies without margin metadata).
113#[derive(Debug, Clone, Copy, PartialEq)]
114pub struct SizeRule {
115    pub canonical_make: &'static str,
116    pub raw_width: u32,
117    pub raw_height: u32,
118    pub left: i32,
119    pub top: i32,
120    pub width_delta: i32,
121    pub height_delta: i32,
122}
123
124include!(concat!(env!("OUT_DIR"), "/cameras.rs"));
125
126/// Map a rebadged / regional model name to its canonical model (case-insensitive);
127/// returns `model` unchanged when it is not an alias.
128pub fn base_model<'a>(canonical_make: &str, model: &'a str) -> &'a str
129where
130    'static: 'a,
131{
132    let m = model.trim();
133    ALIAS_GROUPS
134        .iter()
135        .filter(|g| g.canonical_make.eq_ignore_ascii_case(canonical_make))
136        .find(|g| g.aliases.iter().any(|a| a.eq_ignore_ascii_case(m)))
137        .map(|g| g.model)
138        .unwrap_or(m)
139}
140
141/// Geometry rule for a canonical make and exact raw buffer size.
142pub fn size_rule(canonical_make: &str, raw_width: u32, raw_height: u32) -> Option<&'static SizeRule> {
143    SIZE_RULES.iter().find(|r| {
144        r.raw_width == raw_width && r.raw_height == raw_height && r.canonical_make.eq_ignore_ascii_case(canonical_make)
145    })
146}
147
148/// Fallback matrix lookup by canonical make and model (first prefix match in table order,
149/// preferring the longest prefix on ties of the same make).
150pub fn find_matrix(canonical_make: &str, canonical_model: &str) -> Option<&'static FallbackMatrix> {
151    let model = norm(canonical_model);
152    let make = norm(canonical_make);
153    MATRICES
154        .iter()
155        .filter(|m| norm(m.canonical_make) == make)
156        .filter(|m| {
157            let p = norm(m.model_prefix);
158            // Whole-word prefix only: "EOS R" must not match "EOS REBEL SL3".
159            model.starts_with(&p) && (model.len() == p.len() || !model.as_bytes()[p.len()].is_ascii_alphanumeric())
160        })
161        .max_by_key(|m| m.model_prefix.len())
162}
163
164fn norm(s: &str) -> String {
165    let mut out = String::with_capacity(s.len());
166    for w in s.split_whitespace() {
167        if !out.is_empty() {
168            out.push(' ');
169        }
170        out.push_str(&w.to_uppercase());
171    }
172    out
173}
174
175/// Orientation policy name for a camera: its own override, else the longest matching
176/// make/model-prefix rule, else `"exif-only"`.
177pub fn orientation_policy(canonical_make: &str, canonical_model: &str, cam: Option<&Camera>) -> &'static str {
178    if let Some(p) = cam.and_then(|c| c.orientation).map(|o| o.policy).filter(|p| !p.is_empty()) {
179        return p;
180    }
181    let (make, model) = (norm(canonical_make), norm(canonical_model));
182    ORIENTATION_RULES
183        .iter()
184        .filter(|r| norm(r.canonical_make) == make)
185        .filter(|r| {
186            let p = norm(r.model_prefix);
187            model.starts_with(&p)
188                && (p.is_empty() || model.len() == p.len() || !model.as_bytes()[p.len()].is_ascii_alphanumeric())
189        })
190        .max_by_key(|r| r.model_prefix.len())
191        .map(|r| r.policy)
192        .unwrap_or("exif-only")
193}
194
195impl Camera {
196    /// Sensor levels for an ISO value (falls back to the first/default entry).
197    pub fn sensor(&self, iso: u32) -> Option<&'static SensorInfo> {
198        let specific = self
199            .sensors
200            .iter()
201            .find(|s| (s.iso_min != 0 || s.iso_max != 0 || !s.iso_list.is_empty()) && s.applies_to(iso));
202        specific
203            .or_else(|| self.sensors.iter().find(|s| s.iso_min == 0 && s.iso_max == 0 && s.iso_list.is_empty()))
204            .or(self.sensors.first())
205    }
206    pub fn hint(&self, key: &str) -> Option<&'static str> {
207        self.hints.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
208    }
209    /// Crop resolved against an uncropped `width x height` buffer as `(x, y, w, h)`.
210    pub fn resolve_crop(&self, width: u32, height: u32) -> Option<(u32, u32, u32, u32)> {
211        let [x, y, w, h] = self.crop?;
212        if x < 0 || y < 0 || x as u32 >= width || y as u32 >= height {
213            return None;
214        }
215        let w = if w <= 0 { width as i64 - x as i64 + w as i64 } else { w as i64 };
216        let h = if h <= 0 { height as i64 - y as i64 + h as i64 } else { h as i64 };
217        if w <= 0 || h <= 0 {
218            return None;
219        }
220        let w = (w as u32).min(width - x as u32);
221        let h = (h as u32).min(height - y as u32);
222        Some((x as u32, y as u32, w, h))
223    }
224    /// The camera's image area inside an active area of `width x height`, as
225    /// `[x, y, w, h]` relative to the active area. With `size`, only a rule producing an
226    /// image of that size (either orientation) matches.
227    pub fn image_crop(&self, width: u32, height: u32, size: Option<(u32, u32)>) -> Option<[u32; 4]> {
228        self.image_crops
229            .iter()
230            .filter(|r| r.area == [width, height])
231            .filter(|r| r.crop[0] + r.crop[2] <= width && r.crop[1] + r.crop[3] <= height)
232            .find(|r| size.is_none_or(|(w, h)| (r.crop[2], r.crop[3]) == (w, h) || (r.crop[2], r.crop[3]) == (h, w)))
233            .map(|r| r.crop)
234    }
235    /// Canonical model name for the given raw model string (alias aware).
236    pub fn canonical_model_for(&self, model: &str) -> &'static str {
237        let n = norm(model);
238        self.aliases.iter().find(|(m, _)| norm(m) == n).map(|(_, c)| *c).unwrap_or(self.canonical_model)
239    }
240}
241
242/// Look up a camera by EXIF make/model and decoder mode. Falls back to the
243/// default mode (`""`) when no mode-specific entry exists.
244pub fn find(make: &str, model: &str, mode: &str) -> Option<&'static Camera> {
245    let (mk, md) = (norm(make), norm(model));
246    let key = |mode: &str| format!("{mk}\t{md}\t{}", norm(mode));
247    let lookup = |k: &str| INDEX.binary_search_by(|(e, _)| (*e).cmp(k)).ok().map(|i| &CAMERAS[INDEX[i].1 as usize]);
248    lookup(&key(mode)).or_else(|| if mode.is_empty() { None } else { lookup(&key("")) })
249}
250
251/// Look up a camera ignoring mode: returns the first entry for make/model.
252pub fn find_any(make: &str, model: &str) -> Option<&'static Camera> {
253    let prefix = format!("{}\t{}\t", norm(make), norm(model));
254    let i = INDEX.partition_point(|(e, _)| *e < prefix.as_str());
255    INDEX.get(i).filter(|(e, _)| e.starts_with(&prefix)).map(|(_, idx)| &CAMERAS[*idx as usize])
256}
257
258/// All entries.
259pub fn all() -> &'static [Camera] {
260    CAMERAS
261}
262
263/// Normalise a maker string to its canonical brand name.
264pub fn canonical_make(make: &str) -> &'static str {
265    let m = make.trim().to_ascii_uppercase();
266    const TABLE: &[(&str, &str)] = &[
267        ("AGFA", "AgfaPhoto"),
268        ("ALCATEL", "Alcatel"),
269        ("APPLE", "Apple"),
270        ("ASUS", "Asus"),
271        ("BLACKMAGIC", "Blackmagic"),
272        ("CANON", "Canon"),
273        ("CASIO", "Casio"),
274        ("DJI", "DJI"),
275        ("EPSON", "Epson"),
276        ("SEIKO EPSON", "Epson"),
277        ("FUJI", "Fujifilm"),
278        ("GOOGLE", "Google"),
279        ("GOPRO", "GoPro"),
280        ("HASSELBLAD", "Hasselblad"),
281        ("HTC", "HTC"),
282        ("HUAWEI", "Huawei"),
283        ("KODAK", "Kodak"),
284        ("EASTMAN KODAK", "Kodak"),
285        ("KONICA", "Konica Minolta"),
286        ("MINOLTA", "Minolta"),
287        ("LEAF", "Leaf"),
288        ("LEICA", "Leica"),
289        ("LG", "LG"),
290        ("MAMIYA", "Mamiya"),
291        ("MOTOROLA", "Motorola"),
292        ("NIKON", "Nikon"),
293        ("NOKIA", "Nokia"),
294        ("OLYMPUS", "Olympus"),
295        ("OM DIGITAL", "OM Digital Solutions"),
296        ("OM SYSTEM", "OM Digital Solutions"),
297        ("ONEPLUS", "OnePlus"),
298        ("PANASONIC", "Panasonic"),
299        ("PENTAX", "Pentax"),
300        ("RICOH", "Ricoh"),
301        ("PHASE ONE", "Phase One"),
302        ("SAMSUNG", "Samsung"),
303        ("SIGMA", "Sigma"),
304        ("SINAR", "Sinar"),
305        ("SONY", "Sony"),
306        ("XIAOMI", "Xiaomi"),
307        ("XIAOYI", "Xiaoyi"),
308        ("YI TECHNOLOGY", "Xiaoyi"),
309        ("PARROT", "Parrot"),
310        ("RASPBERRYPI", "Raspberry Pi"),
311        ("ZEISS", "Zeiss"),
312        ("ARRI", "Arri"),
313        ("YUNEEC", "Yuneec"),
314        ("LEICA CAMERA", "Leica"),
315        ("PHASEONE", "Phase One"),
316    ];
317    let mut best: Option<(&str, &str)> = None;
318    for &(k, v) in TABLE {
319        if m.starts_with(k) && best.is_none_or(|(bk, _)| k.len() > bk.len()) {
320            best = Some((k, v));
321        }
322    }
323    best.map(|(_, v)| v).unwrap_or("")
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn index_is_sorted_and_resolvable() {
332        assert!(INDEX.windows(2).all(|w| w[0].0 < w[1].0));
333        for (k, i) in INDEX {
334            let parts: Vec<&str> = k.split('\t').collect();
335            let c = &CAMERAS[*i as usize];
336            assert_eq!(norm(c.make), parts[0]);
337            assert!(find(parts[0], parts[1], parts[2]).is_some());
338        }
339    }
340
341    #[test]
342    fn matrix_prefix_is_word_bounded() {
343        if let Some(m) = find_matrix("Canon", "EOS Rebel SL3") {
344            assert_ne!(m.model_prefix, "EOS R");
345        }
346        assert!(find_matrix("Canon", "EOS 5D Mark IV").is_some_and(|m| m.model_prefix == "EOS 5D Mark IV"));
347    }
348
349    #[test]
350    fn orientation_policies() {
351        assert_eq!(orientation_policy("Canon", "EOS 6D", None), "canon-auto-rotate-aware");
352        assert_eq!(orientation_policy("Nikon", "D70", None), "makernote-when-exif-normal");
353        assert_eq!(orientation_policy("Kodak", "EasyShare Z980", None), "makernote-when-exif-normal");
354        assert_eq!(orientation_policy("Sony", "ILCE-7M3", None), "exif-only");
355        let six_d = find("Canon", "Canon EOS 6D", "").and_then(|c| c.orientation).unwrap();
356        assert_eq!((six_d.maker_note_offset, six_d.auto_rotate_offset), (Some(0x83), Some(0x3b2)));
357        // Aliases share the base model's layout.
358        let t3i = find("Canon", "Canon EOS REBEL T3i", "").and_then(|c| c.orientation).unwrap();
359        assert_eq!(t3i.maker_note_offset, Some(0x38));
360        // A per-camera policy overrides the make's rule.
361        let mut c = *find("Canon", "Canon EOS 6D", "").unwrap();
362        c.orientation =
363            Some(CameraOrientation { policy: "exif-only", maker_note_offset: None, auto_rotate_offset: None });
364        assert_eq!(orientation_policy("Canon", "EOS 6D", Some(&c)), "exif-only");
365    }
366
367    #[test]
368    fn make_normalisation() {
369        assert_eq!(canonical_make("NIKON CORPORATION"), "Nikon");
370        assert_eq!(canonical_make("OM Digital Solutions"), "OM Digital Solutions");
371        assert_eq!(canonical_make("LEICA CAMERA AG"), "Leica");
372        assert_eq!(canonical_make("FUJIFILM"), "Fujifilm");
373    }
374
375    #[test]
376    fn crop_resolution() {
377        let c = Camera {
378            make: "",
379            model: "",
380            mode: "",
381            canonical_make: "",
382            canonical_model: "",
383            aliases: &[],
384            cfa: None,
385            crop: Some([10, 4, -8, 0]),
386            black_areas_v: &[],
387            black_areas_h: &[],
388            sensors: &[],
389            color_matrix: &[],
390            hints: &[],
391            no_size_match: false,
392            orientation: None,
393            image_crops: &[
394                ImageCrop { area: [100, 50], crop: [2, 1, 96, 48] },
395                ImageCrop { area: [100, 50], crop: [26, 1, 48, 48] },
396            ],
397        };
398        assert_eq!(c.resolve_crop(100, 50), Some((10, 4, 82, 46)));
399        assert_eq!(c.image_crop(100, 50, None), Some([2, 1, 96, 48]));
400        assert_eq!(c.image_crop(100, 50, Some((48, 48))), Some([26, 1, 48, 48]));
401        assert_eq!(c.image_crop(100, 50, Some((40, 30))), None);
402        assert_eq!(c.image_crop(98, 50, None), None);
403    }
404}