Skip to content

Commit

Permalink
Chores: refactor + cleaning
Browse files Browse the repository at this point in the history
  • Loading branch information
i5-650 committed Aug 23, 2024
1 parent 433f681 commit 74505a4
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 130 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/exif-samples/
data.zip

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Expand Down
4 changes: 0 additions & 4 deletions .vscode/settings.json

This file was deleted.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rusty-exif"
version = "0.2.2"
name = "rsexif"
version = "0.0.2"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand All @@ -13,3 +13,4 @@ serde_json = "1.0.94"
clap = {version = "4.1.11", features = ["derive"]}
sscanf = "0.4.0"
chrono = "0.4.24"
anyhow = "1.0.86"
44 changes: 34 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
# rusty-exif
<img src="froggy.png" alt="rusty-exif-icon" style="width:300px;height:auto;"/>

A simple exif tool for the command line written in Rust.

## Usage

The base tool is designed to be used in two modes
```
Usage: rsexif <COMMAND>
Commands:
file, -f, --file Extract exif from a single file
dir, -d, --dir Extract exif from every files in a directory
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
```
### Mode file
```
Usage: rusty-exif [OPTIONS]
Usage: rsexif {file|--file|-f} [OPTIONS] <file>
Arguments:
<file> image to extract exif from
Options:
-e, --export <export> Json file to output exifs to
-h, --help Print help
```

### Mode Directory
```
Usage: rsexif {dir|--dir|-d} [OPTIONS] <folder>
Arguments:
<folder> directory containing images to extract exifs from
Options:
-e, --export <EXPORT>
-f, --file <FILE>
-F, --folder <FOLDER>
-s, Create a single json file for each image
-h, --help Print help information
-V, --version Print version information
-s, --split <split> Wether you decide to store all exifs into one file or multiples [possible values: true, false]
-e, --export <export> The name of the Json file containing all the exifs
-h, --help Print help
```

## Examples
Expand All @@ -37,4 +61,4 @@ rusty-exif -f image.jpg
- [ ] Add a GUI
- [X] Add the argument to convert GPS coordinates into a google maps link
- [ ] Add the argument to convert GPS coordinates into an address and/or a screenshot of the location on a map.
- [ ] Modify parameters to use rusty-exif the same way as exiftool
- [X] Make a cleaner version of the CLI
31 changes: 16 additions & 15 deletions src/exif_mapper/mod.rs → src/exifs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ use std::{fs, collections::HashMap};
use sscanf::scanf;
use rayon::prelude::*;

use self::data_structures::Image;
use self::structures::Image;

pub mod data_structures;
pub mod structures;

const GOOGLE_MAP: &str = "googleMap";

pub fn map_exif_from_file(_path_to_image: PathBuf) -> HashMap<String, String> {
pub fn from_file(_path_to_image: String) -> HashMap<String, String> {
let mut map_data = HashMap::new();

let file = std::fs::File::open(_path_to_image.as_path()).expect("Couldn't open the image");
let file = std::fs::File::open(_path_to_image).expect("Couldn't open the image");
let mut bufreader = std::io::BufReader::new(&file);
let exifreader = exif::Reader::new();

Expand All @@ -26,7 +26,7 @@ pub fn map_exif_from_file(_path_to_image: PathBuf) -> HashMap<String, String> {

});
}
return add_google_map(map_data);
add_google_map(map_data)
}

fn add_google_map(mut map_data :HashMap<String, String>) -> HashMap<String, String> {
Expand All @@ -38,30 +38,31 @@ fn add_google_map(mut map_data :HashMap<String, String>) -> HashMap<String, Stri
convert_dms_to_decimal(map_data.get("GPSLongitude").unwrap())
));
}
return map_data;
map_data
}

#[inline(always)]
fn format_value(input: String) -> String {
return input.replace("\"", "").to_string();
input.replace("\"", "").to_string()
}

fn convert_dms_to_decimal(dms: &String) -> f64 {
let parsed = scanf!(dms, "{} deg {} min {} sec", f64, f64, f64).unwrap();
let (degrees, minutes, seconds) = parsed;
return degrees + minutes / 60.0 + seconds / 3600.0;
degrees + minutes / 60.0 + seconds / 3600.0
}

pub fn map_exif_from_folder(_path: PathBuf) -> Vec<Image> {
pub fn from_folder(_path: PathBuf) -> Vec<Image> {
let files = fs::read_dir(_path).expect("Couldn't read the directory given");

return 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();
return data_structures::Image{
name: entry_path.display().to_string(),
exifs: map_exif_from_file(entry_path)
};
let entry_path = f.path().display().to_string();
structures::Image{
name: entry_path.clone(),
exifs: from_file(entry_path)
}
}).collect::<Vec<Image>>();
}
}
File renamed without changes.
213 changes: 114 additions & 99 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,119 +1,134 @@
/**
* Licence: MIT
* Author: loic-prn
* Date: 2022-11-19
*
*/

use std::any::Any;
use std::collections::HashMap;
use clap::{ArgGroup, Parser};
use clap::{Parser, Subcommand};
use rayon::{
prelude::ParallelSliceMut,
iter::{
IntoParallelRefMutIterator,
ParallelIterator
}
};
use exifs::structures::Image;
use std::fs::File;
use std::println;
use rayon::prelude::{ParallelSliceMut, IntoParallelRefMutIterator, ParallelIterator};
use std::io::Write;
use std::io::{self, Write};
use std::path::PathBuf;
use exif_mapper::data_structures::Image;
use std::println;
use anyhow::{Result, anyhow};

pub mod exif_mapper;
pub mod exifs;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(group(
ArgGroup::new("incompatibles")
.required(false)
.args(["file", "folder"])
))]
#[command(
author="i5-650",
about="Exif extraction tools",
long_about = None)]
struct Args {
#[arg(short = 'e', long = "export")]
export: Option<String>,
file: Option<PathBuf>,
#[arg(short = 'f', long = "folder")]
folder: Option<String>,
#[arg(short = 's', long = "split")]
separate: bool,
#[command(subcommand)]
command: Commands
}

#[derive(Debug, Subcommand)]
enum Commands {

#[command(arg_required_else_help = true, long_flag = "file", short_flag = 'f', about = "Extract exif from a single file")]
File {
#[arg(value_name = "file", required = true, help = "image to extract exif from")]
file: String,

#[arg(value_name = "export", required = false, short = 'e', long = "export", help = "Json file to output exifs to")]
export: Option<String>,
},

#[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 = "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: Option<bool>,

#[arg(value_name = "export", required_unless_present = "split", short = 'e', long = "export", help = "The name of the Json file containing all the exifs")]
export_folder: Option<String>,
}
}

impl Args {
pub fn export_and_file(&self) -> bool{
return self.export.is_some() && self.file.is_some();
}

pub fn file_no_export(&self) -> bool {
return self.file.is_some() && self.export.is_none();
}
fn main() -> Result<()> {
let args = Args::parse();

let status = match &args.command {
Commands::File { file, export } => {
let m_file = file.as_str();
let export = export.to_owned();
file_module(m_file.to_string(), export)
},

pub fn folder_no_split(&self) -> bool {
return self.folder.is_some() && !self.separate;
}
Commands::Dir { folder, split, export_folder } => {
folder_module(folder, *split, export_folder)
}
};

pub fn folder_split(&self) -> bool {
return self.folder.is_some() && self.separate;
}
if let Err(e) = status {
println!("Error while extracing exifs");
Err(anyhow!(e))
} else {
Ok(())
}
}

fn main() {
let args = &Args::parse();

if args.export.is_some() || args.folder_no_split() {
let filename = args.export.as_deref().expect("Couldn't get the export filename");
let mut output_file = check_extension(&mut filename.to_string());

let to_write :Box<dyn Any>;

if args.export_and_file() {
let file_par = args.file.as_deref().expect("Invalid file given as parameter");
let image_path = PathBuf::from(file_par);
to_write = Box::new(exif_mapper::map_exif_from_file(image_path));

} else {
let folder_par = args.folder.as_deref().expect("Couldn't get the input folder");
let folder_path = PathBuf::from(folder_par);
to_write = Box::new(exif_mapper::map_exif_from_folder(folder_path));
};

if let Some(map) = to_write.downcast_ref::<HashMap<String, String>>() {
let serialized = serde_json::to_string_pretty(map).expect("Couldn't serialize data");
output_file.write(serialized.as_bytes()).expect("Couldn't write data in the file.");
} else if let Some(list_img) = to_write.downcast_ref::<Vec<Image>>() {
let serialized = serde_json::to_string_pretty(list_img).expect("Couldn't serialize data");
output_file.write(serialized.as_bytes()).expect("Couldn't write data in the file.");
}

return;
}
fn file_module(filename: String, export_file: Option<String>) -> Result<(), io::Error> {
let exifs = exifs::from_file(filename);

if args.file_no_export() {
let file_par = args.file.as_deref().expect("Invalid file given as parameter");
let image_path = PathBuf::from(file_par);
let to_print= exif_mapper::map_exif_from_file(image_path);
for (key, value) in to_print {
if export_file.is_some() {
let serialized = serde_json::to_string_pretty(&exifs).expect("Map must be <String, String>");
let mut export = export_file.unwrap();
let mut json_file = create_json_file(&mut export)?;
json_file.write_all(serialized.as_bytes())?
} else {
for (key, value) in exifs {
println!("{}: {}", key, value);
}
return;
}

if args.folder_split() {
let folder_par = args.folder.as_deref().expect("Couldn't get the input folder");
let folder_path = PathBuf::from(folder_par);
let mut to_write = exif_mapper::map_exif_from_folder(folder_path);
to_write.as_parallel_slice_mut()
.par_iter_mut()
.for_each(|img| {
let serialized = serde_json::to_string_pretty(&img).expect("Couldn't serialize data");
img.name.push_str(".json");
let mut output_file = File::create(&img.name).expect("Coulnd't create an output file named as one of the input files");
output_file.write(serialized.as_bytes()).expect("Couldn't write data in the file.");
});
return;
}

println!("Something went wrong with parameters");
}
Ok(())
}

fn check_extension(filename: &mut String) -> File {
if !filename.ends_with(".json") {
filename.push_str(".json");
}
return File::create(filename).expect("Couldn't create the ouput file.");
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: Option<bool>, export_file: &Option<String>) -> Result<(), io::Error> {
let folder = PathBuf::from(folder_name);
let mut exifs = exifs::from_folder(folder);

if split.is_some() && split.unwrap() {
let list_err = exifs.as_parallel_slice_mut()
.par_iter_mut()
.map(|img: &mut Image| {
let serialized = serde_json::to_string_pretty(&img).expect("Map must be <String, String>");
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))
}
}

0 comments on commit 74505a4

Please sign in to comment.