lrcat/
images.rs

1/*
2 This Source Code Form is subject to the terms of the Mozilla Public
3 License, v. 2.0. If a copy of the MPL was not distributed with this
4 file, You can obtain one at http://mozilla.org/MPL/2.0/.
5*/
6
7use 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/// Some misc properties of the image specific to Lr
16#[derive(Default, Debug)]
17pub struct Properties {
18    /// Where the loupe is focused
19    loupe_focus: Option<Point>,
20    /// Aspect ration of the crop
21    crop_aspect_ratio: Option<AspectRatio>,
22    /// Default crop
23    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        // This triggers clippy::unnecessary_unwrap
45        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        // This triggers clippy::unnecessary_unwrap
103        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        // This triggers clippy::unnecessary_unwrap
110        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
142/// An image in the `Catalog`. Requires a `LibraryFile` backing it
143pub struct Image {
144    id: LrId,
145    uuid: String,
146    /// If this a copy, id of the `Image` it is a copy of
147    pub master_image: Option<LrId>,
148    /// Name of copy.
149    pub copy_name: Option<String>,
150    /// Star rating
151    pub rating: Option<i64>,
152    /// Backing `LibraryFile` id.
153    pub root_file: LrId,
154    /// File format
155    pub file_format: String,
156    /// Pick. -1, 0, 1
157    pub pick: i64,
158    /// Orientation string (set Lr format documentation)
159    /// Convert to EXIF orientation with `self.exif_orientation()`.
160    pub orientation: Option<String>,
161    /// Capture date.
162    pub capture_time: String,
163    /// XMP block as stored in the database. If len() == 0,
164    /// there is no XMP.
165    pub xmp: String,
166    /// XMP is embedded: whether the XMP packet in the file
167    /// like a JPEG, or in a sidecar like in a RAW (non DNG)
168    /// file, regardless of `xmp`.
169    pub xmp_embedded: bool,
170    /// The external XMP (ie not in the database) is different.
171    pub xmp_external_dirty: bool,
172    /// Misc properties from the Adobe_imageProperties table
173    pub properties: Option<Properties>,
174}
175
176impl Image {
177    /// Return the Exif value for the image orientation
178    /// No orientation = 0.
179    /// Error = -1 or unknown value
180    /// Otherwise the Exif value for `orientation`
181    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}