From 6962c253adc0ae095db28d13a7ad64a6ce0fb2e2 Mon Sep 17 00:00:00 2001 From: i5-650 <77734008+i5-650@users.noreply.github.com> Date: Thu, 5 Sep 2024 23:40:38 +0200 Subject: [PATCH] chores: add some tests --- .github/workflows/rust.yml | 24 ++++ .gitignore | 3 +- Cargo.toml | 5 + LICENSE | 2 +- README.md | 14 --- src/core/mod.rs | 71 +++++++++++ src/lib.rs | 111 +--------------- src/main.rs | 96 ++------------ src/modules/dir.rs | 34 +++++ src/modules/file.rs | 29 +++++ src/modules/mod.rs | 6 + src/utils/mod.rs | 56 +++++++++ tests/core/mod.rs | 66 ++++++++++ tests/lib.rs | 6 + tests/modules/dir.rs | 51 ++++++++ tests/modules/file.rs | 42 +++++++ tests/modules/mod.rs | 2 + tests/resources/Canon_40D.jpg | Bin 0 -> 7958 bytes .../resources/Canon_40D_photoshop_import.jpg | Bin 0 -> 9686 bytes tests/resources/Nikon_D70.jpg | Bin 0 -> 14034 bytes tests/utils/mod.rs | 119 ++++++++++++++++++ 21 files changed, 525 insertions(+), 212 deletions(-) create mode 100644 .github/workflows/rust.yml create mode 100644 src/core/mod.rs create mode 100644 src/modules/dir.rs create mode 100644 src/modules/file.rs create mode 100644 src/modules/mod.rs create mode 100644 src/utils/mod.rs create mode 100644 tests/core/mod.rs create mode 100644 tests/lib.rs create mode 100644 tests/modules/dir.rs create mode 100644 tests/modules/file.rs create mode 100644 tests/modules/mod.rs create mode 100644 tests/resources/Canon_40D.jpg create mode 100644 tests/resources/Canon_40D_photoshop_import.jpg create mode 100644 tests/resources/Nikon_D70.jpg create mode 100644 tests/utils/mod.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..4c9c992 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,24 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Install gexiv2 + run: sudo apt update && sudo apt install libgexiv2-2 libgexiv2-dev -y + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore index 67210e1..8733ace 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ Cargo.lock *.png *.jpeg *.jpg -*.heic \ No newline at end of file +*.heic +!/tests/resources/* diff --git a/Cargo.toml b/Cargo.toml index 61c93a3..14952c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,11 @@ anyhow = "1.0" rexiv2 = "0.10" +[dev-dependencies] +regex = "1.10" +tempdir = "0.3" + + [lib] name = "rsexif" path = "src/lib.rs" diff --git a/LICENSE b/LICENSE index 30f778c..4f10a3e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 loic-prn +Copyright (c) 2024 i5-650 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a676b6f..7c7e05b 100644 --- a/README.md +++ b/README.md @@ -40,20 +40,6 @@ Options: -h, --help Print help ``` -## Examples -### Read exif data from an image and save it to a json file -``` -rusty-exif -f image.jpg -e exif.json -``` -### Read exif data from multiple images and save it to a json file -``` -rusty-exif -F folder -e exif.json -``` -### Read exif data from a file and print everything -``` -rusty-exif -f image.jpg -``` - ## To-do - [x] Read exif data - [x] Write exif data in a json file (for one or multiple files) diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..594556f --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,71 @@ +use crate::utils; +use std::collections::{HashMap, BTreeMap}; +use crate::models::Image; +use std::fs; +use std::path::PathBuf; +use rayon::prelude::*; + +pub fn from_file(path: String) -> HashMap> { + + let metadata = match rexiv2::Metadata::new_from_path(path) { + Ok(m) => m, + Err(e) => { + println!("[*] Error while reading the exif from a file: {}", e); + return HashMap::new(); + } + }; + + let tags = match metadata.get_exif_tags() { + Ok(t) => t, + Err(e) => { + println!("[*] Error while retreving the exif: {}", e); + return HashMap::new(); + } + }; + + let map_data = tags.iter() + .map(|tag| { + let value = match metadata.get_tag_interpreted_string(tag) { + Ok(val) => val, + Err(_) => String::from("Failed to convert to string"), + }; + + // Exifs tags are like: Exif.Categ.TheTag + let parts: Vec<&str> = tag.split('.').collect(); + if parts.len() >= 3 { + let category = parts[1].to_string(); + let tag_name = parts[2..].join("."); + (category, tag_name, value) + } else { + let category = "Unknown".to_string(); + let tag_name = parts[parts.len() -1].to_string(); + (category, tag_name, value) + } + }) + // We want the exifs to be in categories so we make a map of map + .fold(HashMap::new(), |mut acc: HashMap>, (category, tag_name, value)| { + // Use a BTreeMap to keep the elements sorted (better readability) + acc.entry(category) + .or_default() + .insert(tag_name, value); + acc + }); + + utils::add_google_map(map_data) +} + +pub fn from_dir(_path: PathBuf) -> Vec { + let files = fs::read_dir(_path).expect("Couldn't read the directory given"); + + files.par_bridge() + .filter_map(|f| f.ok()) + .filter(|f| !f.path().ends_with(".DS_Store") && !f.path().ends_with("/")) + .map(|f| { + let entry_path = f.path().display().to_string(); + Image{ + name: entry_path.clone(), + exifs: from_file(entry_path) + } + }).collect::>() +} + diff --git a/src/lib.rs b/src/lib.rs index 3156cd6..9b63cf5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,109 +1,6 @@ -use std::path::PathBuf; -use std::{fs, collections::HashMap, collections::BTreeMap}; -use scanf::sscanf; -use rayon::prelude::*; - -use models::image::Image; - pub mod models; +pub mod utils; +pub mod modules; +pub mod core; -const GOOGLE_MAP: &str = "googleMap"; - - -pub fn from_file(path: String) -> HashMap> { - - let metadata = match rexiv2::Metadata::new_from_path(path) { - Ok(m) => m, - Err(e) => { - println!("[*] Error while reading the exif from a file: {}", e); - return HashMap::new(); - } - }; - - let tags = match metadata.get_exif_tags() { - Ok(t) => t, - Err(e) => { - println!("[*] Error while retreving the exif: {}", e); - return HashMap::new(); - } - }; - - let map_data = tags.iter() - .map(|tag| { - let value = match metadata.get_tag_interpreted_string(tag) { - Ok(val) => val, - Err(_) => String::from("Failed to convert to string"), - }; - - // Exifs tags are like: Exif.Categ.TheTag - let parts: Vec<&str> = tag.split('.').collect(); - if parts.len() >= 3 { - let category = parts[1].to_string(); - let tag_name = parts[2..].join("."); - (category, tag_name, value) - } else { - let category = "Unknown".to_string(); - let tag_name = parts[parts.len() -1].to_string(); - (category, tag_name, value) - } - }) - // We want the exifs to be in categories so we make a map of map - .fold(HashMap::new(), |mut acc: HashMap>, (category, tag_name, value)| { - // Use a BTreeMap to keep the elements sorted (better readability) - acc.entry(category) - .or_default() - .insert(tag_name, value); - acc - }); - - add_google_map(map_data) -} - - - -fn add_google_map(mut map_data :HashMap>) -> HashMap> { - - if !map_data.contains_key("GPSInfo") { - return map_data; - } - - let gps_info = map_data.get_mut("GPSInfo").expect("Impossible missing GPSInfo"); - - if let (Some(longitude), Some(latitude)) = (gps_info.get("GPSLatitude"), gps_info.get("GPSLongitude")) { - gps_info.insert( - GOOGLE_MAP.to_string(), - format!("https://www.google.com/maps/search/?api=1&query={},{}", - to_decimal(latitude), - to_decimal(longitude) - ) - ); - } - map_data -} - -fn to_decimal(dms: &str) -> f64 { - let mut degrees: f64 = 0.0; - let mut minutes: f64 = 0.0; - let mut seconds: f64 = 0.0; - - if sscanf!(dms, "{f64} deg {f64}' {f64}\"", degrees, minutes, seconds).is_err() { - return 0.0; - } - - degrees + minutes / 60.0 + seconds / 3600.0 -} - -pub fn from_folder(_path: PathBuf) -> Vec { - let files = fs::read_dir(_path).expect("Couldn't read the directory given"); - - files.par_bridge() - .filter_map(|f| f.ok()) - .filter(|f| !f.path().ends_with(".DS_Store") && !f.path().ends_with("/")) - .map(|f| { - let entry_path = f.path().display().to_string(); - Image{ - name: entry_path.clone(), - exifs: from_file(entry_path) - } - }).collect::>() -} +pub use core::*; diff --git a/src/main.rs b/src/main.rs index 4f772c7..6517d8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,9 @@ use clap::{Parser, Subcommand}; -use rayon::{ - prelude::ParallelSliceMut, - iter::{ - IntoParallelRefMutIterator, - ParallelIterator - } -}; - -extern crate rsexif; - -use std::fs::File; -use std::io::{self, Write}; -use std::path::PathBuf; use std::println; use anyhow::{Result, anyhow, Error}; +extern crate rsexif; +use rsexif::modules; #[derive(Parser)] #[command( @@ -43,8 +32,8 @@ enum Commands { #[command(arg_required_else_help = true, long_flag = "dir", short_flag = 'd', about = "Extract exif from every files in a directory")] Dir { - #[arg(value_name = "folder", required = true, help = "directory containing images to extract exifs from")] - folder: String, + #[arg(value_name = "dir", required = true, help = "directory containing images to extract exifs from")] + dir: String, #[arg(value_name = "split", required = false, conflicts_with = "export_folder", short = 's', long = "split", help = "Wether you decide to store all exifs into one file or multiples")] split: bool, @@ -63,11 +52,11 @@ fn main() -> Result<(), Error> { let m_file = file.as_str(); let export = export.to_owned(); let json = json.to_owned(); - file_module(m_file.to_string(), export, json) + modules::file_module(m_file.to_string(), export, json) }, - Commands::Dir { folder, split, export_folder } => { - folder_module(folder, *split, export_folder) + Commands::Dir { dir, split, export_folder } => { + modules::dir_module(dir, *split, export_folder) } }; @@ -79,74 +68,3 @@ fn main() -> Result<(), Error> { } } - -fn file_module(filename: String, export_file: Option, json: bool) -> Result<(), Error> { - let exifs = rsexif::from_file(filename); - - if export_file.is_some() || json { - let serialized = serde_json::to_string_pretty(&exifs).expect("Map must be "); - - if json { - println!("{}", serialized); - } else { - let mut export = export_file.unwrap(); - let mut json_file = create_json_file(&mut export)?; - json_file.write_all(serialized.as_bytes())? - } - - } else { - exifs.iter().for_each(|(categ, sub_exifs)| { - println!("{}", categ); - sub_exifs.iter().for_each(|(key, val)| { - println!("\t{}: {}", key, val); - }) - }); - } - Ok(()) -} - -#[inline(always)] -fn create_json_file(filename: &mut String) -> Result { - if !filename.ends_with(".json") { - filename.push_str(".json"); - } - File::create(filename) -} - - -fn folder_module(folder_name: &String, split: bool, export_file: &Option) -> Result<(), Error> { - let folder = PathBuf::from(folder_name); - let mut exifs = rsexif::from_folder(folder); - - if split { - - let list_err = exifs.as_parallel_slice_mut() - .par_iter_mut() - .map(|img: &mut rsexif::models::Image| { - let serialized = serde_json::to_string_pretty(&img).expect("Must be a Map"); - img.name.push_str(".json"); - exif_to_json(serialized, &img.name) - }) - .collect::>>(); - - if !list_err.is_empty() { - println!("[/!\\] Error encountered while creating/writing to files."); - } - - } else if let Some(mut export) = export_file.to_owned() { - let mut export = create_json_file(&mut export)?; - let serialized = serde_json::to_string_pretty(&exifs).expect("Map must be "); - export.write_all(serialized.as_bytes())? - } - - Ok(()) -} - -fn exif_to_json(content: String, path: &String) -> Result<()> { - let mut json_file = File::create(path)?; - match json_file.write_all(content.as_bytes()) { - Ok(_) => Ok(()), - Err(e) => Err(anyhow!(e)) - } -} - diff --git a/src/modules/dir.rs b/src/modules/dir.rs new file mode 100644 index 0000000..79741fb --- /dev/null +++ b/src/modules/dir.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; +use rayon::prelude::*; +use anyhow::{Result, Error}; + +use crate::models::Image; +use crate::core; +use crate::utils; + +pub fn dir_module(folder_name: &String, split: bool, export_file: &Option) -> Result<(), Error> { + let folder = PathBuf::from(folder_name); + let mut exifs = core::from_dir(folder); + + if split { + + let list_err = exifs.as_parallel_slice_mut() + .par_iter_mut() + .map(|img: &mut Image| { + let serialized = serde_json::to_string_pretty(&img).expect("Must be a Map"); + utils::write_json_to_file(serialized, &mut img.name) + }) + .collect::>>(); + + if !list_err.is_empty() { + println!("[/!\\] Error encountered while creating/writing to files."); + } + + } else if let Some(mut export) = export_file.to_owned() { + let serialized = serde_json::to_string_pretty(&exifs).expect("Map must be "); + utils::write_json_to_file(serialized, &mut export)?; + } + + Ok(()) +} + diff --git a/src/modules/file.rs b/src/modules/file.rs new file mode 100644 index 0000000..81cc945 --- /dev/null +++ b/src/modules/file.rs @@ -0,0 +1,29 @@ +use anyhow::{Error, Result}; + +use crate::core; +use crate::utils; + +pub fn file_module(filename: String, export_file: Option, json: bool) -> Result<(), Error> { + let exifs = core::from_file(filename); + + if export_file.is_some() || json { + let serialized = serde_json::to_string_pretty(&exifs).expect("Map must be "); + + if json { + println!("{}", serialized); + } else { + let mut export = export_file.unwrap(); + utils::write_json_to_file(serialized, &mut export)? + } + + } else { + exifs.iter().for_each(|(categ, sub_exifs)| { + println!("{}", categ); + sub_exifs.iter().for_each(|(key, val)| { + println!(" {}: {}", key, val); + }) + }); + } + Ok(()) +} + diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..65f8824 --- /dev/null +++ b/src/modules/mod.rs @@ -0,0 +1,6 @@ +pub mod file; +pub mod dir; + + +pub use file::*; +pub use dir::*; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..0f9109b --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,56 @@ +use scanf::sscanf; +use std::collections::{HashMap, BTreeMap}; +use anyhow::{anyhow, Result}; +use std::fs::File; +use std::io::Write; + +pub const GOOGLE_MAP: &str = "googleMap"; + +pub fn to_decimal(dms: &str) -> f64 { + let mut degrees: f64 = 0.0; + let mut minutes: f64 = 0.0; + let mut seconds: f64 = 0.0; + + if sscanf!(dms, "{f64} deg {f64}' {f64}\"", degrees, minutes, seconds).is_err() { + return 0.0; + } + + degrees + minutes / 60.0 + seconds / 3600.0 +} + +pub fn add_google_map(mut map_data :HashMap>) -> HashMap> { + + if !map_data.contains_key("GPSInfo") { + return map_data; + } + + let gps_info = map_data.get_mut("GPSInfo").expect("Impossible missing GPSInfo"); + + if let (Some(longitude), Some(latitude)) = (gps_info.get("GPSLatitude"), gps_info.get("GPSLongitude")) { + gps_info.insert( + GOOGLE_MAP.to_string(), + format!("https://www.google.com/maps/search/?api=1&query={},{}", + to_decimal(latitude), + to_decimal(longitude) + ) + ); + } + map_data +} + +#[inline(always)] +pub fn create_json_file(filename: &mut String) -> Result { + if !filename.ends_with(".json") { + filename.push_str(".json"); + } + File::create(filename) +} + +pub fn write_json_to_file(content: String, path: &mut String) -> Result<()> { + let mut json_file = create_json_file(path)?; + match json_file.write_all(content.as_bytes()) { + Ok(_) => Ok(()), + Err(e) => Err(anyhow!(e)) + } +} + diff --git a/tests/core/mod.rs b/tests/core/mod.rs new file mode 100644 index 0000000..ff09535 --- /dev/null +++ b/tests/core/mod.rs @@ -0,0 +1,66 @@ + +extern crate rsexif; + +use rsexif::core; +use std::path::PathBuf; + +#[test] +fn from_file_test() { + // Act + let exifs = core::from_file("./tests/resources/Canon_40D.jpg".to_string()); + + + // Assert + assert!(!exifs.is_empty()); + assert_eq!(5, exifs.len()); + assert!(exifs.contains_key("Thumbnail")); + assert!(exifs.contains_key("GPSInfo")); + assert!(exifs.contains_key("Photo")); + assert!(exifs.contains_key("Image")); + assert!(exifs.contains_key("Iop")); +} + + +#[test] +fn from_dir_test() { + // Given + let folder_pathbuf = PathBuf::from("./tests/resources/"); + + // Act + let exifs = core::from_dir(folder_pathbuf); + + + // Assert + assert!(!exifs.is_empty()); + assert_eq!(3, exifs.len()); + + let mut checked = 0; + exifs.iter().for_each(|e| { + let name = e.name.as_str(); + if name == "./tests/resources/Canon_40D.jpg" { + checked+=1; + assert!(e.exifs.contains_key("Thumbnail")); + assert!(e.exifs.contains_key("GPSInfo")); + assert!(e.exifs.contains_key("Photo")); + assert!(e.exifs.contains_key("Image")); + assert!(e.exifs.contains_key("Iop")); + } + + if name == "./tests/resources/Canon_40D_photoshop_import.jpg" { + checked+=1; + assert!(e.exifs.contains_key("Photo")); + assert!(e.exifs.contains_key("Image")); + assert!(e.exifs.contains_key("Thumbnail")); + } + + + if name == "./tests/resources/Nikon_D70.jpg" { + checked+=1; + assert!(e.exifs.contains_key("Photo")); + assert!(e.exifs.contains_key("Image")); + assert!(e.exifs.contains_key("Thumbnail")); + } + }); + + assert_eq!(3, checked); +} diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..209a9a8 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,6 @@ +#[cfg(test)] +pub mod core; +#[cfg(test)] +pub mod modules; +#[cfg(test)] +pub mod utils; diff --git a/tests/modules/dir.rs b/tests/modules/dir.rs new file mode 100644 index 0000000..4ccf762 --- /dev/null +++ b/tests/modules/dir.rs @@ -0,0 +1,51 @@ +use tempdir::TempDir; +use anyhow::{Error, Result}; +use std::fs; + +extern crate rsexif; + +use rsexif::modules; + + +#[test] +fn dir_module_no_split() -> Result<(), Error>{ + // Given + let tempdir = TempDir::new("dir_module_split")?; + + let export_file = tempdir.path().join("export_test_file.json"); + + + // Act + let res = modules::dir_module(&"./tests/resources/".to_string(), false, &Some(export_file.display().to_string())); + + // Assert + assert!(res.is_ok()); + assert!(export_file.as_path().exists()); + assert_ne!(0, std::fs::metadata(export_file)?.len()); + + Ok(()) +} + +#[test] +fn dir_module_split() -> Result<(), Error>{ + // Act + let res = modules::dir_module(&"./tests/resources/".to_string(), true, &None); + + // Assert + assert!(res.is_ok()); + + let paths = fs::read_dir("./tests/resources/")?; + + let mut count = 0; + for path in paths { + let path = path?.path(); + if path.extension().unwrap() == "json" { + assert_ne!(0, std::fs::metadata(path.clone())?.len()); + count+=1; + std::fs::remove_file(path)?; + } + } + assert_eq!(3, count); + + Ok(()) +} diff --git a/tests/modules/file.rs b/tests/modules/file.rs new file mode 100644 index 0000000..9e66f0b --- /dev/null +++ b/tests/modules/file.rs @@ -0,0 +1,42 @@ +use anyhow::{Result, Error}; +use tempdir::TempDir; + +extern crate rsexif; + +use rsexif::modules; + +#[test] +fn file_module_export() -> Result<(), Error>{ + // Given + let tempdir = TempDir::new("file_module_export")?; + + let export_file = tempdir.path().join("export_test_file.json"); + + + // Act + let res = modules::file_module("./tests/resources/Canon_40D.jpg".to_string(), Some(export_file.display().to_string()), false); + + // Assert + assert!(res.is_ok()); + assert!(export_file.as_path().exists()); + + Ok(()) +} + +#[test] +fn file_module_print_json() { + // Act + let res = modules::file_module("./tests/resources/Canon_40D.jpg".to_string(), None, true); + + // Assert + assert!(res.is_ok()); +} + +#[test] +fn file_module_print() { + // Act + let res = modules::file_module("./tests/resources/Canon_40D.jpg".to_string(), None, false); + + // Assert + assert!(res.is_ok()); +} diff --git a/tests/modules/mod.rs b/tests/modules/mod.rs new file mode 100644 index 0000000..25e8dd1 --- /dev/null +++ b/tests/modules/mod.rs @@ -0,0 +1,2 @@ +pub mod dir; +pub mod file; diff --git a/tests/resources/Canon_40D.jpg b/tests/resources/Canon_40D.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6eb33f12d1d9b3e837c151e9913f0a82061028e1 GIT binary patch literal 7958 zcmeI0cUV(Rx9ImyB_Wj1q!ViBh8{qrmr$f5B1lOPLJ7T!1q7r-1q4(;1q75L*eGHH zL}`i?L9rl31QiiP1j{RTM|fZT&U4Q>_kMq!KhE&Xp5LBXduFXQCBs^Kx;74QTbWbL z0T2WMOXv?+o50kX#nJZw0EMCmumJ$T4sd}m00mJll(`}1gvygp<^kaV2$ifX3?hG( zl@N<;a2>?*8!|A6)u3JalmWjmC4Ve9>{Wax(xzxo-0ST=o~2jKwl8pBgh#DT)( zbxlp7;ZAR`4a8FEzpWu|Fb(P*$VLMQs357UtHHC^wRVQ&5gQx;F_^P11F|g1#s1PL z5vqsfu8)<%lHaIjLX6H`9}5Zfi`ifd#Mlk_JV+)j7Y(pMt}TW*I~N8(4gg3b0Lb}+ zbN^tVWJ6Ci5(zB%Z40bq-HR++;2UfTF>-^iK^=3Jt?OA1G1msyLd;#Z-VdY#078wd znPw%}2D9Gbf7GM?V3r)Ky#&e@P{u-eeMYqOG)M$BEj=x5J(Al0wGQw`L((CkOmS6)$h{z6stm8;hp8k?H0 zx3u5wxOcy^tGj1#=<)E#lc%H4CMKs|PrsR&efw_l)6(bVFDt8Gzp?BBp{f2mtsC|q z?7~BK!4L>I0?o1ugvCM`jz=IR)lfV}4rrfnUMck?Ha_E=(#vfaX${8(e&2`ztbmN> zgzO^Av|pC}zZsVNZ&~(_VZZHq2H@b(ipRt8fB`TODy!o+FJ#V%dhtEFx^etkvQDUi z;HQL z=jCY8f&u#2+-x}LTvbuxxiJ=+|U zn?1{_YlwLmk4szNo;}Y@u#Kxmu80su!qktSTKBx-t@o0X!fy1b{g6t@bu|u1H+Az% z?_Mb@u7)j_9Wj1zCpI)%b8gVEtq;KJ?{%r!E96o7SXCtOSiVpO`Af}^*Opv`COefE zNleu^=5 zi;(%GLGAfyskgO;H?oiN4ST*J=j^U4QCD_uit>sJ>#Q^AQ#4IirrC5J-9E5A=#A@l z19{87)*@>zd$u2BG&3}IHQX=K%lG#Asy4=vYa!(Eoa1wLy7$8e zo4e0FsQqsIQD)KZ%)F=ZjtI_&o#G`2id}Pejw<7mls;}z^BH>U5-o;hGW^^2d@A+B znTHQ%zWgpljV}HBj3d;;3DEF!jd={^qnw^H;Nc<3!ISiK@H~z z<%*+#@)F8V3M4OB*^P6vhnzA&jU9M+;*D zbhHrAebCPV20HN70KkVD8D{Hb?#wy{C}t*vNJwbotlKEQjQsks0ESd7?Foc`ZToNE z@YJw~DCl6cha@#nGjS6GcFF?%a8^mJh2L$2>NI{4NS?s^g%D-gR*^tGw z5C#pBVd)%33!$;N1LE|UXg`SIHV~)9(EVZ|ehjfpaC8VA;#G+GLi~Ipq5Bt(RUhR? z4S*QBM*+A9XGar=w*UYe&i@bg{ez?Y;vhYtyJu)vd<5M;Ac`PQRUoM8>XHeTezC!R zQBf-PKGYze2pYj8G$hQ25f1?CHnYA0cv!wAKu*?H)792iQCEfB|4;fa8~>5|uYonU z>k>;2zvc`Sc<{UK_qV_6LdyVvybSp!`*)r1DFA3n1pvWkzw2a5pu1oSbX$M)hdlzU z^%4*j6{fd&b8Kv^D&3E&%5vyG>Aw~DNAf?1KgLsKjrWi52&R5}e4>M+2rQ>kLxV%3 zBM6aUK2$$~%70wM|MrJ}sP%^)%8q_}{384q&`_?>Dx)*}q3LGO=uz}g27%7_&t~}F z{Ok`MSh&8fA>b|l3GjKT0^E=I0QiRo0D_ktfP0*QTEKti%?9HNuy)=KiJA3v4>8pK z>-jGa*fHo87D@Lfu&PZQoe9+Fh!_?_dxG^qLRWS!=$|wp=$|HOKps#5)BznpA20;W z04u-_a0Ye)y8$W?0E7S$KrE05Bmrqa7LX0(1BJj@pd6?I>VPXi6L1q~1MUO8KtC`H zJO{>sY2ZDu0DJ+yLl+V>$N}<#LZAdF3o3!?AQ{{Snt?W;Be)av2K_+>7!4+Zhrlc_ z7c2xz!7A`FcpYp5yTJi)6r2Fxf}g-|FgOecc>PGvNjBGI$-l z1>OlCf{(-J;9n6a1TTVsP(kP;ED#8`2*ci%dfnAS;m#$a~0PvlXyav)yJJWSe38 zj^W10V)QU}7#b!Xa|~0CX~8_gOk=)d@mL~uE7lnsh)u$t#MWW&VP9aE*xA{o*mc<* z*y-#C*-x@xV((&~VE>Ba!71WQa2~j5+)>~vxu{ibC7eM3&SPLWyH0cD}k$k>nhhHt~qWDw;Z<#w-5Jz?jr8% z+)ucd@w|97yd6FS&%{^byYaI;XdXEpGaedG3eP#7yF638a9(L%6J9FsA>MM{d%V+p zC_W+|g)fjVi|-;|AKwB$55FeAD}Nk+5&tdzaRIo1oB%~2Sm2nz6@d|fZ=1w68Ex|0 zl(DI9)8M8RK@mYiK|jGv!Fs`A!EZtYAv2*Mp&X$mp_jr4VMSpF;aK4_!uN#dMEFGX zMW`Z~B3DG7iNZt`MIA-=ik=gFAi69jE@mMXCU#2fuGpNofViP}pm?76ZSl7fyb{|a z=n{DncO>2s_z6aYV8Tg42Vp@{RMJv1TC!BKPZGM9NV!NQOVvxgl*USvrT0kZNw-Tc z$VkZ8$|TCv$UKw9$dY9PWKYO;%C5)}<#xy&mTQumB?=NL!~|ju@r68xyn%eUe3|@X z1(X6=AxPo0LcbzRQA;sEu}JZe5==>3iLP{7X+RmNtgFmWE>nJ@f>SY4iBYLhnNSr_ zwNXu0ZBm`zEVJ2tbI#_@%|A(6q+n7x>A4z@nw46zT8r8zbwzcmdXf5L4Gs-+jRP9Z z8jG4rntqyRG@okmYT0TX)@s-KOIt@fLc31;t&WV2w@$In6EYv!p3Ee7=^}NFbr0y? z)cvZbtrw+tMen1&ihhuOwf^iD;ugOxsfB5#X`dO7nTuJW*_gSMxxaah z`GSR(#a@e6OQfZ>Wsc<&iWr4TsiG`c=~(Tv>afOHJ6RW6PueKjMA)?0!fdTRC~|n?sOFgH*y+UUe0ty1==qB_+`eLAE;Mu^HAiJRRL2JP~g6l)rLcBw+Gk6$5 zjE+$8(74cnFr~1xu<>xc@ci(F2l%AC z4j;#edl0V}pAkQkV4hI17qOSRw>?oZF)49kpV7Y3{osDz{p|;&52PHJK4@|9LJ~G9 zDCuD`DLFU!^PwGwT2jPP4yH_{TBO#bai&G2Jvpp@_-r~Nou1y8p^Hl{$ji+8obQ$2 zU7%TT_5}7s^ojA4HYb};$(+hQ^`kJLaJb00sJ>XVIHP#wH0|`@8N)MmXT{EDo&8oq zFL_#OQQBB0S5|NibuQ-In{wCk&I)oxRi$ucR^?yk8Ry3@I9}+e(y6Md7Og&31FMOy zd0XpI+kesYVso8JU1_~Qeby!5QuL*Fm%T5KT(Q2=ewBQ+?wahi;s)M^%to*=zHy-` zplQ5$XY;^ytLyD8`YjDNRBxQWDS5N#7XPhdx3RZVZm-=*xU<|E);ibb-!|3m)&Bgh z+ug?<4jp~>tnYQ*H@|MdHTfT$@o*+)7jDB(Zy#m&%QrD_yYMN z^CkDof-%vtvR4YP>c_RmZ%vp^JeYKvd_F~;djC4|^^fV4Hym#YW+Y~+X4PkJzBPZ_ z|8Cd2>Gxspzt5$8z<(&7m!H4((eUGg1-FIi#fZhVPs}BurSqRPKesR2FOPi*`SN`w zb5&^d!dLRw&Tl)u&3upjf&OvwFNMEaep>&0u@MF%qan5*iswtP%fi24Q$fH6A0ll!Ff@x0-Aj&M|?grLuK-7k0U9ZyV$l=tAF zSGR&cQ>sj3zFmHk{a31Na1@9ArM}N|)czN2B3)X3VWYJ#LjAN04xYxn#xu|OSGZa} z=L*yaFVoG}!toY6QwIB;l*6(t4{u7-op#_Ju08Ult-E%9Al#6A0&#|Yw5+c==Mq!z zXnwhbZ1Rp-a&2Ws)YUiIA4aZCgi6Pmb0q5`($ED)V>OxFuM;t@qzN0JI~un&D2sFQ zU*l+vw0vgvPtQOj{ZALdnKy)6*$lWchD#kMcSKxg7#&vYee9ZC=D7y2pH1y4v)7S< z>1S#y)~Y#8zg!`fR^8}7kW^rN_u;|4*fR>qO>eILFh~uUSN(AMwrg}xLqyd93%UkH z@=)8w%>)%huX1b1c$M#~Wsw01<)JUmcPuH{BcpSw5|h4!PvGN2U$lk;gK&e2S9*%I zQD+?-UVj$b`pw~fqjnF^`81&{A+BlaZT$lY!^%QW$=-{Hi^9zJ=efxU$e)&LuT0Po zPpjyT(c4ncW8O57cFdY4X1Y&D>$vR=6-S|_jEQ5@k0r3`V<-C*^QHD;7jKf)FL7d8 zfy7RstxFA>bHciJI#%`vP!nl9Z3)a}t?i?@nU&by&}S(ZcsO#7eC@ZN{xCS!Q;}m_ zor>jbUAd4FSFKg_dd^)zRps@=o4L~3V-|$74NeXP1E=|wo1Iq>kHUm@2=)4_d>G5J zX2bS-C@cCJUM`(}vy_lYkLeEnG?7JiDn8!(-8CgoMWth;qRF15Q6mxBKz)4z?q=4V z7DT>=iNETdDpBvA@7SfePnaTlU&o>OarYy!*taHqhy`-UsiRdy_TxDRQDZ-c&q$|; zo;>~VUM<6rmeGKu)AsNBG*&0@S^CISZlc-K&Ac)>)0WDD)Kc`5v!5hG#5aLA6(Uitjfd9V5| zL-%;~Xl-XpRPhlk+ScT4Z$m(? zHo~rkS2WwolcEdY`2Td=Gj&S8zpU;-txA5~1srpmZ_F#7Vk*OJ#&vptPh+7iJ^!5gwL{xn+ACj2O<(7e z)65ncxbGXKXhLrdIlF}EUgY*cVhL!URrXmxLM-EOzx76p~w{ofDhwX8}z*;dcoEkLMlnKTo& zRzSvzjL+#UKT0^;p6-=FzTAWf74>(2oA0?D))=3_r0om5)>A8b@uuoHb3WcNL~>|z zSW$g4=+3t%Rh_cO7xuRfEfpF3xcrfA6PI)9=5Z_d*Wd=Ho&7ea}WZOU(=B=z;g!=pEj?t8BgN=V9j9~O1)Lim!TKI#3uLV0It ze6hZ-!4fb?iqWET-wTN=+I4eG(`G`*4WXa3TulM*Ou5h7w%?D^n=O!jP;c`7*o(oS zah1`faK4s_69T3A1-|OL$ev&B$39wqYiz65{Ym%|r&VQRP6TQuGU@Tp$BX#>dg1F$ zCu{M~m5-kBv?B41*o{8Za&zY4!E2^ip z8Ah6WedOwKvjT^PAM#BC?}_*6k8B)YcUn%%2J@s$r$(JSS>Y42v{S0Nx4i?^$0lZw z&=*8tw%i;Yu?AlnNspykdKV7=C{wVyq#t?DGm~i%E)(m+IH8i1nuDL(k=; zOJcDS3EH)%HnHIx` zWjbBY(fob4TseGm^baqxpM|}9Nt3^9`pk`s^sTTeVaQ0r-{#HkZ*-dcH>+c-lMR+> d?CFX{0jN|FapO9!Jq9UvVYCnKZVqds{|5vCi01$R literal 0 HcmV?d00001 diff --git a/tests/resources/Canon_40D_photoshop_import.jpg b/tests/resources/Canon_40D_photoshop_import.jpg new file mode 100644 index 0000000000000000000000000000000000000000..10ee97e2aa0069c7dd1e787d99fcf42b9e08adbb GIT binary patch literal 9686 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 literal 0 HcmV?d00001 diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs new file mode 100644 index 0000000..6f41568 --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,119 @@ +use std::collections::{HashMap, BTreeMap}; +use tempdir::TempDir; +use regex::Regex; + +extern crate rsexif; + +use rsexif::utils; + + +#[test] +fn to_decimal_test_ok() { + // Given + let dms = "50 deg 28' 36.63\""; + + // Act + let decimal = utils::to_decimal(dms); + + // Assert + assert_ne!(0.0, decimal); +} + + +#[test] +fn to_decimal_test_ko() { + // Given + let dms = "someRandomValue"; + + // Act + let decimal = utils::to_decimal(dms); + + // Assert + assert_eq!(0.0, decimal); +} + + +#[test] +fn add_google_map_existing() { + // Given + let mut map = HashMap::new(); + + let mut sub_map = BTreeMap::new(); + sub_map.insert("GPSLatitude".to_string(), "48 deg 51' 52.9776\"".to_string()); + sub_map.insert("GPSLongitude".to_string(), "2 deg 20' 56.4504\"".to_string()); + map.insert("GPSInfo".to_string(), sub_map); + + + // Act + let result_map = utils::add_google_map(map); + + // Assert + assert!(!result_map.is_empty()); + assert!(result_map.contains_key("GPSInfo")); + assert!(result_map.get("GPSInfo").unwrap().contains_key(utils::GOOGLE_MAP)); + + // Assert + let re = Regex::new(r"^https://www\.google\.com/maps/search/\?api=1&query=(-?\d+\.\d+),(-?\d+\.\d+)$").unwrap(); + let google_map = result_map.get("GPSInfo").unwrap().get(utils::GOOGLE_MAP).unwrap(); + assert!(re.is_match(google_map)); +} + + +#[test] +fn add_google_map_nothing() { + // Given + let map: HashMap> = HashMap::new(); + + // Act + let result_map = utils::add_google_map(map); + + // Assert + assert!(result_map.is_empty()); +} + +#[test] +fn add_google_map_not_enough() { + // Given + let mut map = HashMap::new(); + map.insert("GPSInfo".to_string(), BTreeMap::new()); + + // Act + let result_map = utils::add_google_map(map); + + + // Assert + assert!(!result_map.is_empty()); + assert!(result_map.contains_key("GPSInfo")); + assert!(!result_map.get("GPSInfo").unwrap().contains_key(utils::GOOGLE_MAP)); +} + + +#[test] +fn create_json_file_test() -> Result<(), anyhow::Error> { + // Given + let tmp_dir = TempDir::new("create_json_file_test")?; + let tmp_file_path = tmp_dir.path().join("tmp_json_file"); + + // Act + let res = utils::create_json_file(&mut tmp_file_path.display().to_string()); + + // Assert + assert!(res.is_ok()); + tmp_dir.close()?; + Ok(()) +} + + +#[test] +fn write_json_to_file_test() -> Result<(), anyhow::Error> { + // Given + let tmp_dir = TempDir::new("write_json_to_file_test")?; + let tmp_file_path = tmp_dir.path().join("tmp_json_file"); + + // Act + let res = utils::write_json_to_file("{\"example\":\"test\"}".to_string(), &mut tmp_file_path.display().to_string()); + + // Assert + assert!(res.is_ok()); + Ok(()) +}