1#[cfg(feature = "tooling")]
12pub mod tooling;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct SensorInfo {
17 pub black: i32,
18 pub white: i32,
19 pub black_colors: &'static [i32],
21 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#[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 pub aliases: &'static [(&'static str, &'static str)],
46 pub cfa: Option<&'static str>,
47 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 pub color_matrix: &'static [i32],
54 pub hints: &'static [(&'static str, &'static str)],
55 pub no_size_match: bool,
56 pub orientation: Option<CameraOrientation>,
58 pub image_crops: &'static [ImageCrop],
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct ImageCrop {
65 pub area: [u32; 2],
67 pub crop: [u32; 4],
69}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
73pub struct CameraOrientation {
74 pub policy: &'static str,
76 pub maker_note_offset: Option<u32>,
78 pub auto_rotate_offset: Option<u32>,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct OrientationRule {
85 pub canonical_make: &'static str,
86 pub model_prefix: &'static str,
88 pub policy: &'static str,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct FallbackMatrix {
94 pub canonical_make: &'static str,
95 pub model_prefix: &'static str,
96 pub black: i32,
98 pub white: i32,
100 pub color_matrix: &'static [i32],
102}
103
104#[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#[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
126pub 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
141pub 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
148pub 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 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
175pub 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 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 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 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 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
242pub 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
251pub 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
258pub fn all() -> &'static [Camera] {
260 CAMERAS
261}
262
263pub 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 let t3i = find("Canon", "Canon EOS REBEL T3i", "").and_then(|c| c.orientation).unwrap();
359 assert_eq!(t3i.maker_note_offset, Some(0x38));
360 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}