1use rusqlite::Row;
8
9use crate::catalog::CatalogVersion;
10use crate::fromdb::FromDb;
11use crate::lrobject::{LrId, LrObject};
12use crate::lron;
13use crate::{AspectRatio, Point, Rect};
14
15#[derive(Default, Debug)]
17pub struct Properties {
18 loupe_focus: Option<Point>,
20 crop_aspect_ratio: Option<AspectRatio>,
22 default_crop: Option<Rect>,
24}
25
26impl Properties {
27 #[allow(clippy::unnecessary_unwrap)]
28 fn loupe_focus(value: &[lron::Object]) -> Option<Point> {
29 use crate::lron::{Object, Value};
30
31 let mut x: Option<f64> = None;
32 let mut y: Option<f64> = None;
33 let mut is_point = false;
34 value.iter().for_each(|o| {
35 if let Object::Pair(p) = o {
36 match p.key.as_str() {
37 "_ag_className" => is_point = p.value == Value::Str("AgPoint".to_owned()),
38 "y" => y = p.value.to_number(),
39 "x" => x = p.value.to_number(),
40 _ => {}
41 }
42 }
43 });
44 if is_point && x.is_some() && y.is_some() {
46 Some(Point {
47 x: x.unwrap(),
48 y: y.unwrap(),
49 })
50 } else {
51 None
52 }
53 }
54
55 #[allow(clippy::unnecessary_unwrap)]
56 fn properties_from(value: &[lron::Object]) -> Self {
57 use crate::lron::{Object, Value};
58
59 let mut props = Properties::default();
60 let mut crop_aspect_h: Option<i32> = None;
61 let mut crop_aspect_w: Option<i32> = None;
62
63 let mut top: Option<f64> = None;
64 let mut bottom: Option<f64> = None;
65 let mut left: Option<f64> = None;
66 let mut right: Option<f64> = None;
67 value.iter().for_each(|o| {
68 if let Object::Pair(p) = o {
69 match p.key.as_str() {
70 "loupeFocusPoint" => {
71 if let Value::Dict(ref v) = p.value {
72 props.loupe_focus = Self::loupe_focus(v);
73 }
74 }
75 "cropAspectH" => {
76 if let Value::Int(i) = p.value {
77 crop_aspect_h = Some(i);
78 }
79 }
80 "cropAspectW" => {
81 if let Value::Int(i) = p.value {
82 crop_aspect_w = Some(i);
83 }
84 }
85 "defaultCropBottom" => {
86 bottom = p.value.to_number();
87 }
88 "defaultCropLeft" => {
89 left = p.value.to_number();
90 }
91 "defaultCropRight" => {
92 right = p.value.to_number();
93 }
94 "defaultCropTop" => {
95 top = p.value.to_number();
96 }
97 _ => {}
98 }
99 }
100 });
101
102 if crop_aspect_h.is_some() && crop_aspect_w.is_some() {
104 props.crop_aspect_ratio = Some(AspectRatio {
105 width: crop_aspect_w.unwrap(),
106 height: crop_aspect_h.unwrap(),
107 });
108 }
109 if top.is_some() && bottom.is_some() && left.is_some() && right.is_some() {
111 props.default_crop = Some(Rect {
112 top: top.unwrap(),
113 bottom: bottom.unwrap(),
114 left: left.unwrap(),
115 right: right.unwrap(),
116 });
117 }
118 props
119 }
120}
121
122impl From<lron::Object> for Properties {
123 fn from(object: lron::Object) -> Self {
124 use crate::lron::{Object, Value};
125
126 match object {
127 Object::Pair(ref s) => {
128 if &s.key == "properties" {
129 match s.value {
130 Value::Dict(ref dict) => Self::properties_from(dict),
131 _ => Properties::default(),
132 }
133 } else {
134 Properties::default()
135 }
136 }
137 _ => Properties::default(),
138 }
139 }
140}
141
142pub struct Image {
144 id: LrId,
145 uuid: String,
146 pub master_image: Option<LrId>,
148 pub copy_name: Option<String>,
150 pub rating: Option<i64>,
152 pub root_file: LrId,
154 pub file_format: String,
156 pub pick: i64,
158 pub orientation: Option<String>,
161 pub capture_time: String,
163 pub xmp: String,
166 pub xmp_embedded: bool,
170 pub xmp_external_dirty: bool,
172 pub properties: Option<Properties>,
174}
175
176impl Image {
177 pub fn exif_orientation(&self) -> i32 {
182 self.orientation.as_ref().map_or(0, |s| match s.as_ref() {
183 "AB" => 1,
184 "DA" => 8,
185 "BC" => 6,
186 "CD" => 3,
187 _ => -1,
188 })
189 }
190}
191
192impl LrObject for Image {
193 fn id(&self) -> LrId {
194 self.id
195 }
196 fn uuid(&self) -> &str {
197 &self.uuid
198 }
199}
200
201impl FromDb for Image {
202 fn read_from(_version: CatalogVersion, row: &Row) -> crate::Result<Self> {
203 let properties = row
204 .get::<usize, String>(13)
205 .ok()
206 .and_then(|v| lron::Object::from_string(&v).ok())
207 .map(Properties::from);
208 Ok(Image {
209 id: row.get(0)?,
210 uuid: row.get(1)?,
211 master_image: row.get(2).ok(),
212 rating: row.get(3).ok(),
213 root_file: row.get(4)?,
214 file_format: row.get(5)?,
215 pick: row.get(6)?,
216 orientation: row.get(7).ok(),
217 capture_time: row.get(8)?,
218 copy_name: row.get(9).ok(),
219 xmp: row.get(10)?,
220 xmp_embedded: row.get(11)?,
221 xmp_external_dirty: row.get(12)?,
222 properties,
223 })
224 }
225 fn read_db_tables(_version: CatalogVersion) -> &'static str {
226 "Adobe_images as img,Adobe_AdditionalMetadata as meta,Adobe_imageProperties as props"
227 }
228 fn read_db_columns(_version: CatalogVersion) -> &'static str {
229 "img.id_local,img.id_global,img.masterImage,img.rating,img.rootFile,img.fileFormat,cast(img.pick as integer) as pick,img.orientation,img.captureTime,img.copyName,meta.xmp,meta.embeddedXmp,meta.externalXmpIsDirty,props.propertiesString"
230 }
231 fn read_join_where(_version: CatalogVersion) -> &'static str {
232 "meta.image = img.id_local and props.image = img.id_local"
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::Image;
239 use super::Properties;
240 use crate::lron;
241
242 #[test]
243 fn test_exif_orientation() {
244 let mut image = Image {
245 id: 1,
246 uuid: String::new(),
247 master_image: None,
248 rating: None,
249 root_file: 2,
250 file_format: String::from("RAW"),
251 pick: 0,
252 orientation: None,
253 capture_time: String::new(),
254 copy_name: None,
255 xmp: String::new(),
256 xmp_embedded: false,
257 xmp_external_dirty: false,
258 properties: None,
259 };
260
261 assert_eq!(image.exif_orientation(), 0);
262 image.orientation = Some(String::from("ZZ"));
263 assert_eq!(image.exif_orientation(), -1);
264
265 image.orientation = Some(String::from("AB"));
266 assert_eq!(image.exif_orientation(), 1);
267 image.orientation = Some(String::from("DA"));
268 assert_eq!(image.exif_orientation(), 8);
269 image.orientation = Some(String::from("BC"));
270 assert_eq!(image.exif_orientation(), 6);
271 image.orientation = Some(String::from("CD"));
272 assert_eq!(image.exif_orientation(), 3);
273 }
274
275 #[test]
276 fn test_properties_loading() {
277 const LRON1: &str = "properties = { \
278 cropAspectH = 9, \
279 cropAspectW = 16, \
280 defaultCropBottom = 0.92105263157895, \
281 defaultCropLeft = 0, \
282 defaultCropRight = 1, \
283 defaultCropTop = 0.078947368421053, \
284 loupeFocusPoint = { \
285 _ag_className = \"AgPoint\", \
286 x = 0.6377015605549, \
287 y = 0.70538265910057, \
288 }, \
289 }";
290
291 let object = lron::Object::from_string(LRON1);
292
293 assert!(object.is_ok());
294 let object = object.unwrap();
295 let properties = Properties::from(object);
296
297 assert!(properties.loupe_focus.is_some());
298 if let Some(ref loupe_focus) = properties.loupe_focus {
299 assert_eq!(loupe_focus.x, 0.6377015605549);
300 assert_eq!(loupe_focus.y, 0.70538265910057);
301 }
302
303 assert!(properties.crop_aspect_ratio.is_some());
304 if let Some(ref ar) = properties.crop_aspect_ratio {
305 assert_eq!(ar.height, 9);
306 assert_eq!(ar.width, 16);
307 }
308
309 assert!(properties.default_crop.is_some());
310 if let Some(ref crop) = properties.default_crop {
311 assert_eq!(crop.top, 0.078947368421053);
312 assert_eq!(crop.bottom, 0.92105263157895);
313 assert_eq!(crop.left, 0.0);
314 assert_eq!(crop.right, 1.0);
315 }
316 }
317}