[niepce] Issue #2 - engine: use Exiv2 to import RAW file metadata



commit 68564dbbd1a34bd6bda98e5e7bf32ced1135f005
Author: Hubert Figuière <hub figuiere net>
Date:   Thu Nov 22 00:47:00 2018 -0500

    Issue #2 - engine: use Exiv2 to import RAW file metadata
    
    - add Exif to XmpDate
    - bump exempi dependency
    
    https://gitlab.gnome.org/GNOME/niepce/issues/2

 Cargo.lock                   |  31 +++++-----
 Cargo.toml                   |   3 +-
 src/Makefile.am              |   1 +
 src/engine/db/libmetadata.rs |   4 +-
 src/engine/db/library.rs     |  11 ++--
 src/fwk/base/date.rs         |  13 +---
 src/fwk/utils/exempi.rs      | 140 ++++++++++++++++++++++++++++++++++---------
 src/fwk/utils/exiv2.rs       | 132 ++++++++++++++++++++++++++++++++++++++++
 src/fwk/utils/mod.rs         |   3 +-
 src/lib.rs                   |   2 +
 10 files changed, 276 insertions(+), 64 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index 08314d4..606a47a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -45,7 +45,7 @@ dependencies = [
  "clang-sys 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.31.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "env_logger 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "peeking_take_while 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "proc-macro2 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -175,17 +175,17 @@ dependencies = [
 [[package]]
 name = "exempi"
 version = "2.5.0"
-source = 
"git+https://github.com/hfiguiere/exempi-rs.git?rev=065721c4#065721c48de36cf8f3e3d89691e3891a3d3230cc";
+source = 
"git+https://github.com/hfiguiere/exempi-rs.git?rev=d5c013c#d5c013caf38351e69bca13a74cb0fd727d752ef4";
 dependencies = [
  "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "exempi-sys 2.4.1 (git+https://github.com/hfiguiere/exempi-rs.git?rev=065721c4)",
+ "exempi-sys 2.5.0 (git+https://github.com/hfiguiere/exempi-rs.git?rev=d5c013c)",
  "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "exempi-sys"
-version = "2.4.1"
-source = 
"git+https://github.com/hfiguiere/exempi-rs.git?rev=065721c4#065721c48de36cf8f3e3d89691e3891a3d3230cc";
+version = "2.5.0"
+source = 
"git+https://github.com/hfiguiere/exempi-rs.git?rev=d5c013c#d5c013caf38351e69bca13a74cb0fd727d752ef4";
 dependencies = [
  "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -295,7 +295,7 @@ dependencies = [
  "glib 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "glib-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gobject-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -318,7 +318,7 @@ dependencies = [
  "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "glib-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gobject-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -365,7 +365,7 @@ dependencies = [
  "glib-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gobject-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gtk-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
  "pango 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -411,7 +411,7 @@ dependencies = [
 
 [[package]]
 name = "lazy_static"
-version = "1.0.0"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 
 [[package]]
@@ -489,7 +489,7 @@ dependencies = [
  "bindgen 0.37.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cbindgen 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "exempi 2.5.0 (git+https://github.com/hfiguiere/exempi-rs.git?rev=065721c4)",
+ "exempi 2.5.0 (git+https://github.com/hfiguiere/exempi-rs.git?rev=d5c013c)",
  "gettext-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gio 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gio-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -497,6 +497,7 @@ dependencies = [
  "glib-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gtk 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gtk-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "rexiv2 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -562,7 +563,7 @@ dependencies = [
  "glib 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "glib-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gobject-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
  "pango-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -834,7 +835,7 @@ name = "thread_local"
 version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 dependencies = [
- "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -973,8 +974,8 @@ dependencies = [
 "checksum clap 2.31.1 (registry+https://github.com/rust-lang/crates.io-index)" = 
"5dc18f6f4005132120d9711636b32c46a233fad94df6217fa1d81c5e97a9f200"
 "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = 
"09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
 "checksum env_logger 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = 
"0e6e40ebb0e66918a37b38c7acab4e10d299e0463fe2af5d29b9cc86710cfd2a"
-"checksum exempi 2.5.0 (git+https://github.com/hfiguiere/exempi-rs.git?rev=065721c4)" = "<none>"
-"checksum exempi-sys 2.4.1 (git+https://github.com/hfiguiere/exempi-rs.git?rev=065721c4)" = "<none>"
+"checksum exempi 2.5.0 (git+https://github.com/hfiguiere/exempi-rs.git?rev=d5c013c)" = "<none>"
+"checksum exempi-sys 2.5.0 (git+https://github.com/hfiguiere/exempi-rs.git?rev=d5c013c)" = "<none>"
 "checksum fragile 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = 
"05f8140122fa0d5dcb9fc8627cfce2b37cc1500f752636d46ea28bc26785c2f9"
 "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = 
"2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
 "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = 
"3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
@@ -995,7 +996,7 @@ dependencies = [
 "checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = 
"0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e"
 "checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = 
"8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
 "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = 
"7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
-"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = 
"c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d"
+"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = 
"a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1"
 "checksum libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)" = 
"f54263ad99207254cf58b5f701ecb432c717445ea2ee8af387334bdd1a03fdff"
 "checksum libloading 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = 
"9c3ad660d7cb8c5822cd83d10897b0f1f1526792737a179e73896152f85b88c2"
 "checksum libsqlite3-sys 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = 
"d3711dfd91a1081d2458ad2d06ea30a8755256e74038be2ad927d94e1c955ca8"
diff --git a/Cargo.toml b/Cargo.toml
index 5870b21..068e3b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ build = "build.rs"
 
 [dependencies]
 chrono = "0.4.0"
-exempi = { version = "2.5.0", git = "https://github.com/hfiguiere/exempi-rs.git";, rev="065721c4" }
+exempi = { version = "2.5.0", git = "https://github.com/hfiguiere/exempi-rs.git";, rev="d5c013c" }
 gettext-rs = "0.3.0"
 glib-sys = "0.7.0"
 glib = "0.6.0"
@@ -14,6 +14,7 @@ gio-sys = "0.7.0"
 gio = "0.5.0"
 gtk-sys = { version = "0.7.0", features = ["v3_16"] }
 gtk = "0.5.0"
+lazy_static = "^1.2.0"
 libc = "0.2.39"
 rexiv2 = "^0.6.0"
 rusqlite = { version = "0.14.0", features = ["functions"] }
diff --git a/src/Makefile.am b/src/Makefile.am
index 93306b0..8949ab0 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -34,6 +34,7 @@ RUST_SOURCES = \
        @top_srcdir@/src/fwk/toolkit/mimetype.rs \
        @top_srcdir@/src/fwk/toolkit/mod.rs \
        @top_srcdir@/src/fwk/utils/exempi.rs \
+       @top_srcdir@/src/fwk/utils/exiv2.rs \
        @top_srcdir@/src/fwk/utils/files.rs \
        @top_srcdir@/src/fwk/utils/mod.rs \
        @top_srcdir@/src/lib.rs \
diff --git a/src/engine/db/libmetadata.rs b/src/engine/db/libmetadata.rs
index d78f09a..1c99a52 100644
--- a/src/engine/db/libmetadata.rs
+++ b/src/engine/db/libmetadata.rs
@@ -117,9 +117,9 @@ impl LibMetadata {
                 PropertyValue::String(ref s) => {
                     if s.is_empty() {
                         return self.xmp.xmp.delete_property(&ix.ns, &ix.property).is_ok();
-                    } else if !self.xmp
+                    } else if self.xmp
                         .xmp
-                        .set_property(&ix.ns, &ix.property, s, exempi::PROP_NONE).is_ok()
+                        .set_property(&ix.ns, &ix.property, s, exempi::PROP_NONE).is_err()
                     {
                         if exempi::get_error() == exempi::Error::BadXPath {
                             return self.xmp.xmp.set_localized_text(
diff --git a/src/engine/db/library.rs b/src/engine/db/library.rs
index 683b20f..1cc7064 100644
--- a/src/engine/db/library.rs
+++ b/src/engine/db/library.rs
@@ -630,14 +630,13 @@ impl Library {
         // Until we get better metadata support for RAW files, we use the Exif reconcile
         // from the sidecar JPEG to get the initial metadata.
         let meta = if let Some(bundle) = bundle {
-            match bundle.bundle_type() {
-                libfile::FileType::RAW_JPEG => {
-                    fwk::XmpMeta::new_from_file(bundle.jpeg(), false)
-                },
-                _ => fwk::XmpMeta::new_from_file(file, file_type == libfile::FileType::RAW)
+            if bundle.bundle_type() == libfile::FileType::RAW_JPEG {
+                fwk::XmpMeta::new_from_file(bundle.jpeg(), false)
+            } else {
+                fwk::XmpMeta::new_from_file(file, false)
             }
         } else {
-            fwk::XmpMeta::new_from_file(file, file_type == libfile::FileType::RAW)
+            fwk::XmpMeta::new_from_file(file, false)
         };
 
         if let Some(ref meta) = meta {
diff --git a/src/fwk/base/date.rs b/src/fwk/base/date.rs
index 0645047..10463bf 100644
--- a/src/fwk/base/date.rs
+++ b/src/fwk/base/date.rs
@@ -29,16 +29,9 @@ pub type Date = chrono::DateTime<chrono::Utc>;
 
 pub fn xmp_date_from(d: &chrono::DateTime<chrono::Utc>) -> exempi::DateTime {
     let mut xmp_date = exempi::DateTime::new();
-    xmp_date.c.year = d.year();
-    xmp_date.c.month = d.month() as i32;
-    xmp_date.c.day = d.day() as i32;
-    xmp_date.c.hour = d.hour() as i32;
-    xmp_date.c.minute = d.minute() as i32;
-    xmp_date.c.second = d.second() as i32;
-    xmp_date.c.tz_sign = exempi::XmpTzSign::UTC;
-    xmp_date.c.tz_hour = 0;
-    xmp_date.c.tz_minute = 0;
-    xmp_date.c.nano_second = 0;
+    xmp_date.set_date(d.year(), d.month() as i32, d.day() as i32);
+    xmp_date.set_time(d.hour() as i32, d.minute() as i32, d.second() as i32);
+    xmp_date.set_timezone(exempi::XmpTzSign::UTC, 0, 0);
 
     xmp_date
 }
diff --git a/src/fwk/utils/exempi.rs b/src/fwk/utils/exempi.rs
index 7ec415d..e303f17 100644
--- a/src/fwk/utils/exempi.rs
+++ b/src/fwk/utils/exempi.rs
@@ -1,7 +1,7 @@
 /*
  * niepce - fwk/utils/exempi.rs
  *
- * Copyright (C) 2017 Hubert Figuière
+ * Copyright (C) 2017-2018 Hubert Figuière
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,17 +25,18 @@ use chrono::{DateTime, Utc};
 use exempi;
 use exempi::Xmp;
 
-static NIEPCE_XMP_NAMESPACE: &'static str = "http://xmlns.figuiere.net/ns/niepce/1.0";;
-static NIEPCE_XMP_NS_PREFIX: &'static str = "niepce";
-static UFRAW_INTEROP_NAMESPACE: &'static str = "http://xmlns.figuiere.net/ns/ufraw_interop/1.0";;
-static UFRAW_INTEROP_NS_PREFIX: &'static str = "ufrint";
+use super::exiv2;
 
+const NIEPCE_XMP_NAMESPACE: &str = "http://xmlns.figuiere.net/ns/niepce/1.0";;
+const NIEPCE_XMP_NS_PREFIX: &str = "niepce";
+const UFRAW_INTEROP_NAMESPACE: &str = "http://xmlns.figuiere.net/ns/ufraw_interop/1.0";;
+const UFRAW_INTEROP_NS_PREFIX: &str = "ufrint";
 
-pub static NS_TIFF: &'static str = "http://ns.adobe.com/tiff/1.0/";;
-pub static NS_XAP: &'static str = "http://ns.adobe.com/xap/1.0/";;
-pub static NS_EXIF: &'static str = "http://ns.adobe.com/exif/1.0/";;
-pub static NS_DC: &'static str = "http://purl.org/dc/elements/1.1/";;
-pub static NS_AUX: &'static str = "http://ns.adobe.com/exif/1.0/aux/";;
+pub const NS_TIFF: &str = "http://ns.adobe.com/tiff/1.0/";;
+pub const NS_XAP: &str = "http://ns.adobe.com/xap/1.0/";;
+pub const NS_EXIF: &str = "http://ns.adobe.com/exif/1.0/";;
+pub const NS_DC: &str = "http://purl.org/dc/elements/1.1/";;
+pub const NS_AUX: &str = "http://ns.adobe.com/exif/1.0/aux/";;
 
 pub struct NsDef {
     ns: String,
@@ -82,16 +83,21 @@ impl XmpMeta {
         }
     }
 
+    pub fn new_with_xmp(xmp: exempi::Xmp) -> XmpMeta {
+        XmpMeta {
+            xmp,
+            keywords: Vec::new(),
+            keywords_fetched: false,
+        }
+    }
+
     pub fn new_from_file(file: &str, sidecar_only: bool) -> Option<XmpMeta> {
         let mut meta: Option<XmpMeta> = None;
         if !sidecar_only {
             if let Ok(xmpfile) = exempi::XmpFile::open_new(file, exempi::OPEN_READ) {
-                if let Ok(xmp) = xmpfile.get_new_xmp() {
-                    meta = Some(XmpMeta {
-                        xmp,
-                        keywords: Vec::new(),
-                        keywords_fetched: false
-                    });
+                meta = match xmpfile.get_new_xmp() {
+                    Ok(xmp) => Some(Self::new_with_xmp(xmp)),
+                    _ => exiv2::xmp_from_exiv2(file)
                 }
             }
         }
@@ -104,12 +110,8 @@ impl XmpMeta {
             let mut sidecarcontent = String::new();
             if sidecarfile.read_to_string(&mut sidecarcontent).is_ok() {
                 let mut xmp = exempi::Xmp::new();
-                if let Ok(_) = xmp.parse(sidecarcontent.into_bytes().as_slice()) {
-                    sidecar_meta = Some(XmpMeta {
-                        xmp,
-                        keywords: Vec::new(),
-                        keywords_fetched: false
-                    });
+                if xmp.parse(sidecarcontent.into_bytes().as_slice()).is_ok() {
+                    sidecar_meta = Some(Self::new_with_xmp(xmp));
                 }
             }
         }
@@ -147,7 +149,7 @@ impl XmpMeta {
             return false;
         }
         if source_date > dest_date {
-            dbg_out!("file meta is more recent than sidecar");
+            dbg_out!("source meta is more recent than sidecar");
             return false;
         }
 
@@ -169,11 +171,10 @@ impl XmpMeta {
                     continue;
                 }
 
-                if !dest.xmp.has_property(schema.to_str(), name.to_str()) {
-                    if dest.xmp.set_property(schema.to_str(), name.to_str(),
+                if !dest.xmp.has_property(schema.to_str(), name.to_str()) &&
+                    dest.xmp.set_property(schema.to_str(), name.to_str(),
                                           value.to_str(), exempi::PROP_NONE).is_err() {
                         err_out!("Can set property {}", name);
-                    }
                 }
             }
         }
@@ -239,12 +240,22 @@ impl XmpMeta {
         Some(String::from(xmpstring.to_str()))
     }
 
-    /// Get the date property and return a DateTime<Utc> parsed
+    /// Get the date property and return a Option<DateTime<Utc>> parsed
     /// from the string value.
     pub fn get_date_property(&self, ns: &str, property: &str) -> Option<DateTime<Utc>> {
         let mut flags: exempi::PropFlags = exempi::PropFlags::empty();
-        let xmpstring = try_opt!(self.xmp.get_property(ns, property, &mut flags).ok());
-        let date = try_opt!(DateTime::parse_from_rfc3339(xmpstring.to_str()).ok());
+        let property = self.xmp.get_property(ns, property, &mut flags);
+        if property.is_err() {
+            err_out!("Error getting date property {:?}", property.err());
+            return None;
+        }
+        let xmpstring = property.unwrap();
+        let parsed = DateTime::parse_from_rfc3339(xmpstring.to_str());
+        if parsed.is_err() {
+            err_out!("Error parsing property value '{}': {:?}", xmpstring, parsed.err());
+            return None;
+        }
+        let date = parsed.unwrap();
         Some(date.with_timezone(&Utc))
     }
 
@@ -324,6 +335,57 @@ pub fn gps_coord_from_xmp(xmps: &str) -> Option<f64> {
     Some(deg)
 }
 
+/// Get and XMP date from an Exif date string
+/// XXX Currently assume it is UTC.
+pub fn xmp_date_from_exif(d: &str) -> Option<exempi::DateTime> {
+    let v: Vec<&str> = d.split(' ').collect();
+    if v.len() != 2 {
+        err_out!("Space split failed {:?}", v);
+        return None;
+    }
+
+    let ymd: Vec<&str> = v[0].split(':').collect();
+    if ymd.len() != 3 {
+        err_out!("ymd split failed {:?}", ymd);
+        return None;
+    }
+    let year = try_opt!(i32::from_str_radix(ymd[0], 10).ok());
+    let month = try_opt!(i32::from_str_radix(ymd[1], 10).ok());
+    if month < 1 || month > 12 {
+        return None;
+    }
+    let day = try_opt!(i32::from_str_radix(ymd[2], 10).ok());
+    if day < 1 || day > 31 {
+        return None;
+    }
+    let hms: Vec<&str> = v[1].split(':').collect();
+    if hms.len() != 3 {
+        err_out!("hms split failed {:?}", hms);
+        return None;
+    }
+    let hour = try_opt!(i32::from_str_radix(hms[0], 10).ok());
+    if hour < 0 || hour > 23 {
+        return None;
+    }
+    let min = try_opt!(i32::from_str_radix(hms[1], 10).ok());
+    if min < 0 || min > 59 {
+        return None;
+    }
+    let sec = try_opt!(i32::from_str_radix(hms[2], 10).ok());
+    if sec < 0 || sec > 59 {
+        return None;
+    }
+
+    let mut xmp_date = exempi::DateTime::new();
+
+    xmp_date.set_date(year, month, day);
+    xmp_date.set_time(hour, min, sec);
+    // XXX use an actual timezone
+    xmp_date.set_timezone(exempi::XmpTzSign::UTC, 0, 0);
+
+    Some(xmp_date)
+}
+
 #[no_mangle]
 pub extern "C" fn fwk_exempi_manager_new() -> *mut ExempiManager {
     Box::into_raw(Box::new(ExempiManager::new(None)))
@@ -339,6 +401,7 @@ mod tests {
     use std::path::PathBuf;
     use super::ExempiManager;
     use super::XmpMeta;
+    use super::xmp_date_from_exif;
     use exempi;
 
     fn get_xmp_sample_path() -> PathBuf {
@@ -476,4 +539,23 @@ mod tests {
         assert!(output.is_some());
         assert_eq!(output.unwrap(), 45.49179166666666418450404307805001735687255859375);
     }
+
+    #[test]
+    fn test_xmp_date_from_exif() {
+        let d = xmp_date_from_exif("2012:02:17 11:10:49");
+        assert!(d.is_some());
+        let d = d.unwrap();
+        assert_eq!(d.year(), 2012);
+        assert_eq!(d.month(), 02);
+        assert_eq!(d.day(), 17);
+        assert_eq!(d.hour(), 11);
+        assert_eq!(d.minute(), 10);
+        assert_eq!(d.second(), 49);
+
+        let d = xmp_date_from_exif("2012:02:17/11:10:49");
+        assert!(d.is_none());
+
+        let d = xmp_date_from_exif("2012.02.17 11.10.49");
+        assert!(d.is_none());
+    }
 }
diff --git a/src/fwk/utils/exiv2.rs b/src/fwk/utils/exiv2.rs
new file mode 100644
index 0000000..fc0e268
--- /dev/null
+++ b/src/fwk/utils/exiv2.rs
@@ -0,0 +1,132 @@
+/*
+ * niepce - fwk/utils/exiv2.rs
+ *
+ * Copyright (C) 2018 Hubert Figuière
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use std::ffi::OsStr;
+use std::collections::HashMap;
+
+use exempi;
+use rexiv2;
+
+use super::exempi::{XmpMeta, NS_TIFF, NS_EXIF, NS_XAP, xmp_date_from_exif};
+
+#[derive(Clone, Copy)]
+enum Conversion {
+    None,
+    ExifDate,
+}
+
+#[derive(Clone)]
+enum Converted {
+    Str(String),
+    Date(Option<exempi::DateTime>),
+}
+
+lazy_static! {
+    static ref exiv2_to_xmp: HashMap<&'static str, (&'static str, &'static str, Conversion)> = {
+        [
+            ("Exif.Photo.DateTimeOriginal", (NS_EXIF, "DateTimeOriginal", Conversion::ExifDate)),
+            ("Exif.Photo.DateTimeDigitized", (NS_XAP, "CreateDate", Conversion::ExifDate)),
+            ("Exif.Image.DateTime", (NS_XAP, "ModifyDate", Conversion::ExifDate)),
+            ("Exif.Image.Make", (NS_TIFF, "Make", Conversion::None)),
+            ("Exif.Image.Model", (NS_TIFF, "Model", Conversion::None))
+        ].iter().cloned().collect()
+    };
+}
+
+fn convert(conversion: Conversion, value: &str) -> Converted {
+    match conversion {
+        Conversion::None => Converted::Str(value.to_string()),
+        Conversion::ExifDate => Converted::Date(xmp_date_from_exif(&value)),
+    }
+}
+
+pub fn xmp_from_exiv2<S: AsRef<OsStr>>(file: S) -> Option<XmpMeta> {
+    if let Ok(meta) = rexiv2::Metadata::new_from_path(file) {
+        let mut xmp = exempi::Xmp::new();
+        let mut all_tags : Vec<String> = vec!();
+        if let Ok(mut tags) = meta.get_exif_tags() {
+            all_tags.append(&mut tags);
+        }
+        if let Ok(mut tags) = meta.get_iptc_tags() {
+            all_tags.append(&mut tags);
+        }
+        if let Ok(mut tags) = meta.get_xmp_tags() {
+            all_tags.append(&mut tags);
+        }
+        for tag in all_tags {
+            if let Some(xmp_prop) = exiv2_to_xmp.get(tag.as_str()) {
+                let tagtype = rexiv2::get_tag_type(&tag);
+                match tagtype {
+                    Ok(rexiv2::TagType::AsciiString) => {
+                        if let Ok(value) = meta.get_tag_string(&tag) {
+                            let converted = convert(xmp_prop.2, &value);
+                            match converted {
+                                Converted::Str(s) => {
+                                    xmp.set_property(xmp_prop.0, xmp_prop.1, &s, exempi::PROP_NONE);
+                                },
+                                Converted::Date(d) => {
+                                    if let Some(d) = d {
+                                        xmp.set_property_date(xmp_prop.0, xmp_prop.1, &d, exempi::PROP_NONE);
+                                    } else {
+                                        err_out!("Couldn't convert Exif date {}", &value);
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    Ok(rexiv2::TagType::UnsignedShort) |
+                    Ok(rexiv2::TagType::UnsignedLong) |
+                    Ok(rexiv2::TagType::SignedShort) |
+                    Ok(rexiv2::TagType::SignedLong) => {
+                        // XXX rexiv2 returns an i32, which is a problem for UnsignedLong
+                        let value = meta.get_tag_numeric(&tag);
+                        xmp.set_property_i32(xmp_prop.0, xmp_prop.1, value, exempi::PROP_NONE);
+                    },
+                    Ok(rexiv2::TagType::UnsignedRational) |
+                    Ok(rexiv2::TagType::SignedRational) => {
+                        if let Some(value) = meta.get_tag_rational(&tag) {
+                            let value_str = format!("{}/{}", value.numer(), value.denom());
+                            xmp.set_property(xmp_prop.0, xmp_prop.1, &value_str, exempi::PROP_NONE);
+                        }
+                        println!("value {} = {:?}", &tag, meta.get_tag_rational(&tag));
+                    },
+                    _ => {
+                        println!("Unhandled type {:?} for {}", tagtype, &tag);
+                    }
+                }
+            } else {
+                println!("Unknown property {}", &tag);
+            }
+        }
+        meta.get_gps_info();
+
+        let mut options = exempi::PROP_NONE;
+        if let Ok(date) = xmp.get_property_date(NS_XAP, "ModifyDate", &mut options) {
+            if xmp.set_property_date(NS_XAP, "MetadataDate", &date, exempi::PROP_NONE).is_err() {
+                err_out!("Error setting MetadataDate");
+            }
+        } else {
+            err_out!("Couldn't get the ModifyDate");
+        }
+        Some(XmpMeta::new_with_xmp(xmp))
+    } else {
+        None
+    }
+}
+
diff --git a/src/fwk/utils/mod.rs b/src/fwk/utils/mod.rs
index 765dc18..7f5f3ac 100644
--- a/src/fwk/utils/mod.rs
+++ b/src/fwk/utils/mod.rs
@@ -1,7 +1,7 @@
 /*
  * niepce - fwk/utils/mod.rs
  *
- * Copyright (C) 2017 Hubert Figuière
+ * Copyright (C) 2017-2018 Hubert Figuière
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,4 +18,5 @@
  */
 
 pub mod exempi;
+pub mod exiv2;
 pub mod files;
diff --git a/src/lib.rs b/src/lib.rs
index b614f06..77e5ed1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -26,6 +26,8 @@ extern crate glib;
 extern crate glib_sys;
 extern crate gtk;
 extern crate gtk_sys;
+#[macro_use]
+extern crate lazy_static;
 extern crate libc;
 extern crate rexiv2;
 extern crate rusqlite;


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