Skip to content

Commit

Permalink
Merge pull request #1 from i5-650/feat/tests
Browse files Browse the repository at this point in the history
Tests and structure
  • Loading branch information
i5-650 authored Sep 6, 2024
2 parents 76932d7 + 6962c25 commit 0ac06d9
Show file tree
Hide file tree
Showing 21 changed files with 525 additions and 212 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ Cargo.lock
*.png
*.jpeg
*.jpg
*.heic
*.heic
!/tests/resources/*
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 0 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
71 changes: 71 additions & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String, BTreeMap<String,String>> {

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<String, BTreeMap<String, String>>, (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<Image> {
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::<Vec<Image>>()
}

111 changes: 4 additions & 107 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String, BTreeMap<String,String>> {

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<String, BTreeMap<String, String>>, (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<String, BTreeMap<String, String>>) -> HashMap<String, BTreeMap<String, String>> {

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<Image> {
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::<Vec<Image>>()
}
pub use core::*;
96 changes: 7 additions & 89 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
};

Expand All @@ -79,74 +68,3 @@ fn main() -> Result<(), Error> {
}
}


fn file_module(filename: String, export_file: Option<String>, 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 <String, String>");

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<File, io::Error> {
if !filename.ends_with(".json") {
filename.push_str(".json");
}
File::create(filename)
}


fn folder_module(folder_name: &String, split: bool, export_file: &Option<String>) -> 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::<Vec<Result<()>>>();

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 <String, String>");
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))
}
}

Loading

0 comments on commit 0ac06d9

Please sign in to comment.