[niepce/lr-import: 10/20] Issue #40 - npc-engine: Add image to albums and set properties




commit d6bba01cb79e4c502c90818c8ed679932fb7c203
Author: Hubert Figuière <hub figuiere net>
Date:   Wed Nov 17 22:07:54 2021 -0500

    Issue #40 - npc-engine: Add image to albums and set properties
    
    - more properties need to be handled
    - LrImport imports the collections into albums
    
    https://gitlab.gnome.org/GNOME/niepce/-/issues/40

 Cargo.lock                                         |   4 +-
 crates/npc-engine/Cargo.toml                       |   2 +-
 crates/npc-engine/src/db/library.rs                |  79 ++++++++++++-
 crates/npc-engine/src/db/props.rs                  |   2 +
 crates/npc-engine/src/importer/lrimporter.rs       | 124 +++++++++++++++------
 crates/npc-engine/src/library/commands.rs          |  43 +++++++
 crates/npc-engine/src/library/notification.rs      |   3 +
 crates/npc-engine/src/libraryclient.rs             |  11 ++
 .../src/libraryclient/clientinterface.rs           |   7 ++
 9 files changed, 233 insertions(+), 42 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index ce38620..f7874ed 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -680,9 +680,9 @@ dependencies = [
 
 [[package]]
 name = "lrcat-extractor"
-version = "0.1.0"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "ee7ae5136c7085d85886d3a31e9bda2bce03c750b143beca4d8d1073e30bef7e"
+checksum = "fafe70f346fd8591519612694441d6e6a0f7aa4402a5e007b8e916754686b633"
 dependencies = [
  "chrono",
  "peg",
diff --git a/crates/npc-engine/Cargo.toml b/crates/npc-engine/Cargo.toml
index 0c2090a..2425eaf 100644
--- a/crates/npc-engine/Cargo.toml
+++ b/crates/npc-engine/Cargo.toml
@@ -21,7 +21,7 @@ glib = "*"
 lazy_static = "^1.2.0"
 libc = "0.2.39"
 maplit = "1.0.2"
-lrcat-extractor = { version = "0.1.0" }
+lrcat-extractor = { version = "0.2.0" }
 rusqlite = { version = "0.26.1", features = ["functions"] }
 
 npc-fwk = { path = "../npc-fwk" }
diff --git a/crates/npc-engine/src/db/library.rs b/crates/npc-engine/src/db/library.rs
index 6e010a4..48aaa0b 100644
--- a/crates/npc-engine/src/db/library.rs
+++ b/crates/npc-engine/src/db/library.rs
@@ -26,8 +26,6 @@ use std::result;
 use chrono::Utc;
 use rusqlite::{functions::FunctionFlags, params};
 
-use super::props::NiepceProperties as Np;
-use super::props::NiepcePropertyIdx::*;
 use super::{FromDb, LibraryId};
 use crate::db::album::Album;
 use crate::db::filebundle::{FileBundle, Sidecar};
@@ -39,6 +37,9 @@ use crate::db::libfolder;
 use crate::db::libfolder::LibFolder;
 use crate::db::libmetadata::LibMetadata;
 use crate::library::notification::LibNotification;
+use crate::NiepceProperties as Np;
+use crate::NiepcePropertyBag;
+use crate::NiepcePropertyIdx::*;
 use npc_fwk::toolkit;
 use npc_fwk::PropertyValue;
 
@@ -54,6 +55,7 @@ const DATABASENAME: &str = "niepcelibrary.db";
 
 #[derive(Debug)]
 pub enum Error {
+    Unimplemented,
     NotFound,
     NoSqlDb,
     IncorrectDbVersion,
@@ -662,6 +664,21 @@ impl Library {
         Err(Error::NoSqlDb)
     }
 
+    /// Add an image to an album.
+    pub fn add_to_album(&self, image_id: LibraryId, album_id: LibraryId) -> Result<()> {
+        if let Some(ref conn) = self.dbconn {
+            let c = conn.execute(
+                "INSERT INTO albuming (file_id, album_id) VALUES(?1, ?2)",
+                params![image_id, album_id],
+            )?;
+            if c != 1 {
+                return Err(Error::InvalidResult);
+            }
+            return Ok(());
+        }
+        Err(Error::NoSqlDb)
+    }
+
     pub fn add_fs_file<P: AsRef<Path>>(&self, f: P) -> Result<LibraryId> {
         if let Some(ref conn) = self.dbconn {
             let file = f.as_ref().to_string_lossy();
@@ -936,6 +953,24 @@ impl Library {
         Err(Error::NoSqlDb)
     }
 
+    /// Set properties for an image.
+    pub fn set_image_properties(
+        &self,
+        image_id: LibraryId,
+        props: &NiepcePropertyBag,
+    ) -> Result<()> {
+        if let Some(ref conn) = self.dbconn {
+            if let Some(PropertyValue::String(xmp)) = props.get(&Np::Index(NpNiepceXmpPacket)) {
+                conn.execute(
+                    "UPDATE files SET xmp=?1 WHERE id=?2;",
+                    params![xmp, image_id],
+                )?;
+            }
+            return Ok(());
+        }
+        Err(Error::NoSqlDb)
+    }
+
     fn set_internal_metadata(&self, file_id: LibraryId, column: &str, value: i32) -> Result<()> {
         if let Some(ref conn) = self.dbconn {
             let c = conn.execute(
@@ -968,7 +1003,6 @@ impl Library {
     }
 
     pub fn set_metadata(&self, file_id: LibraryId, meta: Np, value: &PropertyValue) -> Result<()> {
-        #[allow(non_upper_case_globals)]
         match meta {
             Np::Index(NpXmpRatingProp)
             | Np::Index(NpXmpLabelProp)
@@ -1201,6 +1235,9 @@ impl Library {
 #[cfg(test)]
 mod test {
     use crate::db::filebundle::FileBundle;
+    use crate::NiepceProperties as Np;
+    use crate::NiepcePropertyBag;
+    use crate::NiepcePropertyIdx as NpI;
 
     use super::{Library, Managed};
 
@@ -1293,11 +1330,21 @@ mod test {
 
         let bundle_id = lib.add_bundle(folder_added.id(), &bundle, Managed::NO);
         assert!(bundle_id.is_ok());
-        assert!(bundle_id.ok().unwrap() > 0);
+        assert!(bundle_id.unwrap() > 0);
     }
 
+    const XMP_PACKET: &str =
+        "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"Exempi + XMP Core 5.1.2\"> \
+ <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\";> \
+ </rdf:RDF> \
+</x:xmpmeta>";
+
     #[test]
     fn file_bundle_import() {
+        use npc_fwk::utils::exempi::XmpMeta;
+
+        exempi::init();
+
         let lib = Library::new_in_memory();
 
         assert!(lib.is_ok());
@@ -1320,6 +1367,28 @@ mod test {
 
         let bundle_id = lib.add_bundle(folder_added.id(), &bundle, Managed::NO);
         assert!(bundle_id.is_ok());
-        assert!(bundle_id.ok().unwrap() > 0);
+        let bundle_id = bundle_id.unwrap();
+        assert!(bundle_id > 0);
+
+        // Test setting properties
+
+        let mut props = NiepcePropertyBag::new();
+        props.set_value(Np::Index(NpI::NpNiepceXmpPacket), XMP_PACKET.into());
+        // one of the problem with XMP packet serialisation is that the version
+        // of the XMP SDK is written in the header so we can do comparisons
+        // byte by byte
+        let original_xmp_packet =
+            exempi::Xmp::from_buffer(XMP_PACKET.as_bytes()).expect("XMP packet created");
+        let original_xmp_packet = XmpMeta::new_with_xmp(original_xmp_packet);
+        let result = lib.set_image_properties(bundle_id, &props);
+        result.expect("Setting the XMP works");
+
+        let result = lib.get_metadata(bundle_id);
+        let metadata = result.expect("Have retrieved metadata");
+        let xmp_packet = metadata.serialize_inline();
+        assert_eq!(
+            xmp_packet.as_str(),
+            original_xmp_packet.serialize_inline().as_str()
+        );
     }
 }
diff --git a/crates/npc-engine/src/db/props.rs b/crates/npc-engine/src/db/props.rs
index 180fff0..35c705b 100644
--- a/crates/npc-engine/src/db/props.rs
+++ b/crates/npc-engine/src/db/props.rs
@@ -35,6 +35,8 @@ pub enum NiepcePropertyIdx {
     NpIptcDescriptionProp,
     NpIptcKeywordsProp,
     NpNiepceFlagProp,
+    /// The XMP Packet
+    NpNiepceXmpPacket,
 }
 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
 #[allow(unused_parens)]
diff --git a/crates/npc-engine/src/importer/lrimporter.rs b/crates/npc-engine/src/importer/lrimporter.rs
index 7f6c602..7db02e1 100644
--- a/crates/npc-engine/src/importer/lrimporter.rs
+++ b/crates/npc-engine/src/importer/lrimporter.rs
@@ -23,15 +23,19 @@ use std::collections::BTreeMap;
 use std::path::Path;
 
 use lrcat::{
-    Catalog, CatalogVersion, Collection, Folder, Keyword, KeywordTree, LibraryFile, LrId, LrObject,
+    Catalog, CatalogVersion, Collection, Folder, Image, Keyword, KeywordTree, LibraryFile, LrId,
+    LrObject,
 };
 
 use super::libraryimporter::LibraryImporter;
 use crate::db::filebundle::FileBundle;
+use crate::db::props::NiepceProperties as Np;
+use crate::db::props::NiepcePropertyIdx as NpI;
 use crate::db::LibraryId;
-use crate::libraryclient::{ClientInterfaceSync, LibraryClient};
+use crate::libraryclient::{ClientInterface, ClientInterfaceSync, LibraryClient};
+use crate::NiepcePropertyBag;
 
-/// Library importer for Lightroom
+/// Library importer for Lightroom™
 pub struct LrImporter {
     /// map keyword LrId to LibraryId
     folder_map: BTreeMap<LrId, (LibraryId, String)>,
@@ -41,6 +45,10 @@ pub struct LrImporter {
     collection_map: BTreeMap<LrId, LibraryId>,
     /// map files LrId to file LibraryId
     file_map: BTreeMap<LrId, LibraryId>,
+    /// map image LrId to file LibraryId
+    ///
+    /// XXX longer term is to have an image table.
+    image_map: BTreeMap<LrId, LibraryId>,
 }
 
 impl LrImporter {
@@ -71,10 +79,52 @@ impl LrImporter {
         self.folder_map.insert(folder.id(), (nid, path.into()));
     }
 
-    fn import_collection(&mut self, collection: &Collection, libclient: &mut LibraryClient) {
+    fn import_collection(
+        &mut self,
+        collection: &Collection,
+        images: Option<&Vec<LrId>>,
+        libclient: &mut LibraryClient,
+    ) {
         let parent = self.collection_map.get(&collection.parent).unwrap_or(&-1);
         let nid = libclient.create_album_sync(collection.name.clone(), *parent);
         self.collection_map.insert(collection.id(), nid);
+
+        if let Some(images) = images {
+            dbg_out!("Has images");
+            images.iter().for_each(|id| {
+                self.image_map.get(&id).map(|npc_image_id| {
+                    dbg_out!("adding {} to album {}", npc_image_id, nid);
+                    libclient.add_to_album(*npc_image_id, nid);
+                });
+            });
+        }
+    }
+
+    fn populate_bundle(file: &LibraryFile, folder_path: &str, bundle: &mut FileBundle) {
+        let mut xmp_file: Option<String> = None;
+        let mut jpeg_file: Option<String> = None;
+        let sidecar_exts = file.sidecar_extensions.split(",");
+        sidecar_exts.for_each(|ext| {
+            if !ext.is_empty() {
+                return;
+            }
+            if ext.to_lowercase() == "xmp" {
+                xmp_file = Some(format!("{}/{}.{}", &folder_path, &file.basename, &ext));
+            } else if jpeg_file.is_some() {
+                err_out!("JPEG sidecar already set: {}", ext);
+            } else {
+                jpeg_file = Some(format!("{}/{}.{}", &folder_path, &file.basename, &ext));
+            }
+        });
+
+        if let Some(jpeg_file) = jpeg_file {
+            dbg_out!("Adding JPEG {}", &jpeg_file);
+            bundle.add(jpeg_file);
+        }
+        if let Some(xmp_file) = xmp_file {
+            dbg_out!("Adding XMP {}", &xmp_file);
+            bundle.add(xmp_file);
+        }
     }
 
     fn import_library_file(&mut self, file: &LibraryFile, libclient: &mut LibraryClient) {
@@ -85,35 +135,28 @@ impl LrImporter {
             bundle.add(main_file);
 
             if !file.sidecar_extensions.is_empty() {
-                let mut xmp_file: Option<String> = None;
-                let mut jpeg_file: Option<String> = None;
-                let sidecar_exts = file.sidecar_extensions.split(",");
-                sidecar_exts.for_each(|ext| {
-                    if !ext.is_empty() {
-                        return;
-                    }
-                    if ext.to_lowercase() == "xmp" {
-                        xmp_file = Some(format!("{}/{}.{}", &folder_id.1, &file.basename, &ext));
-                    } else if jpeg_file.is_some() {
-                        err_out!("JPEG sidecar already set: {}", ext);
-                    } else {
-                        jpeg_file = Some(format!("{}/{}.{}", &folder_id.1, &file.basename, &ext));
-                    }
-                });
-
-                if let Some(jpeg_file) = jpeg_file {
-                    dbg_out!("Adding JPEG {}", &jpeg_file);
-                    bundle.add(jpeg_file);
-                }
-                if let Some(xmp_file) = xmp_file {
-                    dbg_out!("Adding XMP {}", &xmp_file);
-                    bundle.add(xmp_file);
-                }
+                Self::populate_bundle(file, &folder_id.1, &mut bundle);
             }
+
             let nid = libclient.add_bundle_sync(&bundle, folder_id.0);
             self.file_map.insert(file.id(), nid);
         }
     }
+
+    fn import_image(&mut self, image: &Image, libclient: &mut LibraryClient) {
+        let root_file = image.root_file;
+        if let Some(file_id) = self.file_map.get(&root_file) {
+            let mut metadata = NiepcePropertyBag::new();
+            metadata.set_value(
+                Np::Index(NpI::NpTiffOrientationProp),
+                image.exif_orientation().into(),
+            );
+            metadata.set_value(Np::Index(NpI::NpNiepceFlagProp), (image.pick as i32).into());
+            metadata.set_value(Np::Index(NpI::NpNiepceXmpPacket), image.xmp.as_str().into());
+            libclient.set_image_properties(*file_id, &metadata);
+            self.image_map.insert(image.id(), *file_id);
+        }
+    }
 }
 
 impl LibraryImporter for LrImporter {
@@ -123,6 +166,7 @@ impl LibraryImporter for LrImporter {
             keyword_map: BTreeMap::new(),
             collection_map: BTreeMap::new(),
             file_map: BTreeMap::new(),
+            image_map: BTreeMap::new(),
         }
     }
 
@@ -149,18 +193,30 @@ impl LibraryImporter for LrImporter {
         let keywords = catalog.load_keywords();
         self.import_keyword(root_keyword_id, libclient, keywords, &keywordtree);
 
-        let collections = catalog.load_collections();
+        let library_files = catalog.load_library_files();
+        library_files.iter().for_each(|library_file| {
+            self.import_library_file(library_file, libclient);
+        });
+
+        let images = catalog.load_images();
+        images.iter().for_each(|image| {
+            self.import_image(image, libclient);
+        });
+
+        catalog.load_collections();
+        let collections = catalog.collections();
         collections.iter().for_each(|collection| {
             if !collection.system_only {
                 dbg_out!("Found collection {}", &collection.name);
-                self.import_collection(&collection, libclient);
+                let images = catalog.images_for_collection(collection.id()).ok();
+                dbg_out!(
+                    "Found {} images in collection",
+                    images.as_ref().map(Vec::len).unwrap_or(0)
+                );
+                self.import_collection(&collection, images.as_ref(), libclient);
             }
         });
 
-        let library_files = catalog.load_library_files();
-        library_files.iter().for_each(|library_file| {
-            self.import_library_file(library_file, libclient);
-        });
         true
     }
 
diff --git a/crates/npc-engine/src/library/commands.rs b/crates/npc-engine/src/library/commands.rs
index 38e66f2..a60999a 100644
--- a/crates/npc-engine/src/library/commands.rs
+++ b/crates/npc-engine/src/library/commands.rs
@@ -30,6 +30,7 @@ use crate::db::library;
 use crate::db::library::{Library, Managed};
 use crate::db::props::NiepceProperties as Np;
 use crate::db::LibraryId;
+use crate::NiepcePropertyBag;
 use npc_fwk::PropertyValue;
 
 pub fn cmd_list_all_keywords(lib: &Library) -> bool {
@@ -213,6 +214,30 @@ pub fn cmd_create_album(lib: &Library, name: &str, parent: LibraryId) -> Library
     }
 }
 
+/// Command to add an image to an album.
+pub fn cmd_add_to_album(lib: &Library, image_id: LibraryId, album_id: LibraryId) -> bool {
+    match lib.add_to_album(image_id, album_id) {
+        Ok(_) => {
+            if lib
+                .notify(LibNotification::AddedToAlbum((image_id, album_id)))
+                .is_err()
+            {
+                err_out!("Failed to notify AddedToAlbum");
+            }
+            true
+        }
+        Err(err) => {
+            err_out_line!(
+                "Adding image {} to album {} failed {:?}",
+                image_id,
+                album_id,
+                err
+            );
+            false
+        }
+    }
+}
+
 pub fn cmd_request_metadata(lib: &Library, file_id: LibraryId) -> bool {
     match lib.get_metadata(file_id) {
         Ok(lm) => {
@@ -233,6 +258,24 @@ pub fn cmd_request_metadata(lib: &Library, file_id: LibraryId) -> bool {
     }
 }
 
+/// Command to set image properties.
+pub fn cmd_set_image_properties(
+    lib: &Library,
+    image_id: LibraryId,
+    props: &NiepcePropertyBag,
+) -> bool {
+    match lib.set_image_properties(image_id, props) {
+        Ok(_) => {
+            // XXX set the image properties.
+            true
+        }
+        Err(err) => {
+            err_out_line!("Setting image metadata failed {:?}", err);
+            false
+        }
+    }
+}
+
 pub fn cmd_query_folder_content(lib: &Library, folder_id: LibraryId) -> bool {
     match lib.get_folder_content(folder_id) {
         Ok(fl) => {
diff --git a/crates/npc-engine/src/library/notification.rs b/crates/npc-engine/src/library/notification.rs
index 86bf61e..9747ce2 100644
--- a/crates/npc-engine/src/library/notification.rs
+++ b/crates/npc-engine/src/library/notification.rs
@@ -39,6 +39,7 @@ pub enum NotificationType {
     ADDED_KEYWORD,
     ADDED_LABEL,
     ADDED_ALBUM,
+    ADDED_TO_ALBUM,
     FOLDER_CONTENT_QUERIED,
     FOLDER_DELETED,
     FOLDER_COUNTED,
@@ -123,6 +124,7 @@ pub enum LibNotification {
     AddedKeyword(Keyword),
     AddedLabel(Label),
     AddedAlbum(Album),
+    AddedToAlbum((LibraryId, LibraryId)),
     FileMoved(FileMove),
     FileStatusChanged(FileStatusChange),
     FolderContentQueried(QueriedContent),
@@ -178,6 +180,7 @@ pub unsafe extern "C" fn engine_library_notification_type(
         Some(&LibNotification::AddedKeyword(_)) => NotificationType::ADDED_KEYWORD,
         Some(&LibNotification::AddedLabel(_)) => NotificationType::ADDED_LABEL,
         Some(&LibNotification::AddedAlbum(_)) => NotificationType::ADDED_ALBUM,
+        Some(&LibNotification::AddedToAlbum(_)) => NotificationType::ADDED_TO_ALBUM,
         Some(&LibNotification::FileMoved(_)) => NotificationType::FILE_MOVED,
         Some(&LibNotification::FileStatusChanged(_)) => NotificationType::FILE_STATUS_CHANGED,
         Some(&LibNotification::FolderContentQueried(_)) => NotificationType::FOLDER_CONTENT_QUERIED,
diff --git a/crates/npc-engine/src/libraryclient.rs b/crates/npc-engine/src/libraryclient.rs
index e6bb2bc..36c262c 100644
--- a/crates/npc-engine/src/libraryclient.rs
+++ b/crates/npc-engine/src/libraryclient.rs
@@ -38,6 +38,7 @@ use crate::db::{NiepceProperties, NiepcePropertyIdx};
 use crate::library::commands;
 use crate::library::notification::{LcChannel, LibNotification};
 use crate::library::op::Op;
+use crate::NiepcePropertyBag;
 use npc_fwk::base::PropertyValue;
 use npc_fwk::toolkit::PortableChannel;
 use npc_fwk::utils::files::FileList;
@@ -198,6 +199,11 @@ impl ClientInterface for LibraryClient {
         self.schedule_op(move |lib| commands::cmd_create_album(&lib, &name, parent) != 0);
     }
 
+    /// Add an image to an album.
+    fn add_to_album(&mut self, image_id: LibraryId, album_id: LibraryId) {
+        self.schedule_op(move |lib| commands::cmd_add_to_album(&lib, image_id, album_id));
+    }
+
     fn request_metadata(&mut self, file_id: LibraryId) {
         self.schedule_op(move |lib| commands::cmd_request_metadata(&lib, file_id));
     }
@@ -207,6 +213,11 @@ impl ClientInterface for LibraryClient {
         let value2 = value.clone();
         self.schedule_op(move |lib| commands::cmd_set_metadata(&lib, file_id, meta, &value2));
     }
+    fn set_image_properties(&mut self, image_id: LibraryId, props: &NiepcePropertyBag) {
+        let props = props.clone();
+        self.schedule_op(move |lib| commands::cmd_set_image_properties(&lib, image_id, &props));
+    }
+
     fn write_metadata(&mut self, file_id: LibraryId) {
         self.schedule_op(move |lib| commands::cmd_write_metadata(&lib, file_id));
     }
diff --git a/crates/npc-engine/src/libraryclient/clientinterface.rs 
b/crates/npc-engine/src/libraryclient/clientinterface.rs
index 80ba1ee..ab666d8 100644
--- a/crates/npc-engine/src/libraryclient/clientinterface.rs
+++ b/crates/npc-engine/src/libraryclient/clientinterface.rs
@@ -23,6 +23,7 @@ use crate::db::filebundle::FileBundle;
 use crate::db::library::Managed;
 use crate::db::props::NiepceProperties as Np;
 use crate::db::LibraryId;
+use crate::NiepcePropertyBag;
 use npc_fwk::base::PropertyValue;
 
 /// Client interface.
@@ -39,12 +40,18 @@ pub trait ClientInterface {
     fn create_folder(&mut self, name: String, path: Option<String>);
     fn delete_folder(&mut self, id: LibraryId);
 
+    /// get all the albums
     fn get_all_albums(&mut self);
+    /// Create an album (async)
     fn create_album(&mut self, name: String, parent: LibraryId);
+    /// Add an image to an album.
+    fn add_to_album(&mut self, image_id: LibraryId, album_id: LibraryId);
 
     fn request_metadata(&mut self, id: LibraryId);
     /// set the metadata
     fn set_metadata(&mut self, id: LibraryId, meta: Np, value: &PropertyValue);
+    /// set some properties for an image.
+    fn set_image_properties(&mut self, id: LibraryId, props: &NiepcePropertyBag);
     fn write_metadata(&mut self, id: LibraryId);
 
     fn move_file_to_folder(&mut self, file_id: LibraryId, from: LibraryId, to: LibraryId);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]