Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronleopold committed Feb 23, 2021
0 parents commit e8eab6d
Show file tree
Hide file tree
Showing 5 changed files with 340 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/target
Cargo.lock

test_images
destination
17 changes: 17 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "imageconverter"
version = "0.1.0"
authors = ["Aaron Leopold <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
magick_rust = "0.14.0"
imagepipe = "0.3.5"
rawloader = "0.36.2"
image = "0.23.9"
clap = "2.33.3"
rayon = "1.3.1"
glob = "0.3.0"
error-chain = "0.12.4"
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Image Converter

Converts images from CR2 to JPG formats. Developed for the McGuire Center for Lepidoptera at the Florida Museum of Natural History.

Given starting and destination paths, the program will collect and convert .CR2 files to .JPG. [magick_rust](https://github.com/nlfiedler/magick-rust), a wrapper around [ImageMagick](https://imagemagick.org/index.php), will be attempted first, falling back on [imagepipe](https://github.com/pedrocr/imagepipe). Support for other file conversions is possible, but will not be implemented until the Museum's needs require it.

### Installation and Usage

Be sure to install [Rust](https://www.rust-lang.org/) and [ImageMagick](https://imagemagick.org/index.php) on your system.

You may run the program with cargo:

```bash
$ cargo run --release -- --help
$ cargo run --release -- [options] [flags]
```

Alternatively, you may build an executable and run that directly:

```bash
-- linux / macos --
$ cargo build --release
$ ./target/release/imageconverter [options] [flags]

-- windows --
$ cargo build --release
$ .\target\release\imageconverter.exe [options] [flags]
```

## Benchmarks

[magick_rust](https://github.com/nlfiedler/magick-rust) conversions average 40s for Hi-Resolution CR2 files, while [imagepipe](https://github.com/pedrocr/imagepipe) conversions average 90s for the same files. As such, the order is magick -> imagepipe.

[rayon](https://docs.rs/rayon/1.5.0/rayon/) is used to expedite the overall conversion times, offloading the conversions to separate threads in parallel execution.
224 changes: 224 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use std::path::Path;
use std::fs::File;
use std::io::BufWriter;
use image::{ ColorType};
use std::fs::create_dir_all;
use std::path::PathBuf;
use magick_rust::{MagickWand, magick_wand_genesis};
use std::sync::Once;

extern crate imagepipe;
extern crate rawloader;
extern crate error_chain;

pub mod errors {
use error_chain::{
error_chain,
};

error_chain! {}
}

extern crate glob;
use glob::{glob_with, MatchOptions};
use rayon::prelude::*;

static START: Once = Once::new();

/// Ensure the passed in path is valid (exists and is a directory), will exit with code 1 on invalid.
/// If create = true, will create directory instead of erroring out
fn path_exists(path: &str, create: bool) {
println!("Checking the existence of {}...", path);
let path_obj = Path::new(path);

if (!path_obj.exists() || !path_obj.is_dir()) && !create {
println!("failed! The path either doesn't exist in the filesystem or is not a directory: {}", path);
println!("The program will now exit...");
std::process::exit(1);
} else if (!path_obj.exists() || !path_obj.is_dir()) && create {
create_dir_all(path).expect("Could not create directory!");
println!("created destination path...\n");
} else {
println!("passed!\n");
}
}

pub struct Converter {
start_dir: String,
destination: String,
recursive: bool
}

impl Converter {
pub fn new(start_dir: &str, destination: &str, recursive: bool) -> Self {
Self {
start_dir: String::from(start_dir),
destination: String::from(destination),
recursive
}
}

/// Returns nothing - checks installation of required libraries and the validity of
/// the passed in path argument
///
/// # Arguments
///
/// * `path` - A str filesystem path, the starting directory to scan images
/// * `destination` - A str filesystem path, the directory to store converted images
fn sanity_checks(&self) {
// check_installations();
path_exists(self.start_dir.as_str(), false);
path_exists(self.destination.as_str(), true);
}

/// Returns Result - Ok on successful read/write of source/converted images
///
/// # Arguments
///
/// * `source` - A PathBuf representing the target .CR2 image to convert
fn imagepipe(&self) -> Result<(), &'static str> {

let file = "./src/sample1.cr2";
let filejpg = "./src/output_pipeline.jpg";

println!("Loading file \"{}\" and saving it as \"{}\"", file, filejpg);

let image = rawloader::decode_file(file);

if image.is_err() {
return Err("Could not decode file...");
}

let decoded_wrapped = imagepipe::simple_decode_8bit(file, 0, 0);

if decoded_wrapped.is_err() {
return Err("Could not decode file...");
}

let uf = File::create(filejpg);

if uf.is_err() {
return Err("Could not create JPG file...");
}

let mut f = BufWriter::new(uf.unwrap());
let decoded = decoded_wrapped.unwrap();

let mut jpg_encoder = image::jpeg::JpegEncoder::new_with_quality(&mut f, 100);
jpg_encoder
.encode(&decoded.data, decoded.width as u32, decoded.height as u32, ColorType::Rgb8)
.expect("Encoding image in JPEG format failed.");

Ok(())
}

/// Returns Result - Ok on successful read/write of source/converted images
///
/// # Arguments
///
/// * `source` - A PathBuf representing the target .CR2 image to convert
fn magick(&self, source: &PathBuf) -> Result<(), &'static str> {
START.call_once(|| {
magick_wand_genesis();
});

let wand = MagickWand::new();

let stem = source.file_stem().unwrap();
let target = format!("{}/{}.jpg", self.destination.as_str(), stem.to_str().unwrap());

wand.read_image(source.to_str().unwrap())?;
wand.write_image(target.as_str())?;

Ok(())
}

/// Returns Result - Ok on successful read/write of source/converted images
///
/// This function calls either of the two wrapper functions for converting images:
/// magick will be attempted first, on an Err imagepipe will be attempted afterwards.
///
/// # Arguments
///
/// * `source` - A PathBuf representing the target .CR2 image to convert
fn convert(&self, source: &PathBuf) -> Result<(), &'static str> {
match self.magick(source) {
Ok(_) => Ok(()),
Err(_) => {
println!("Could not convert image with magick, trying pipeline library...");
return match self.imagepipe() {
Ok(_) => Ok(()),
Err(_) => Err("Could not convert image")
}
}
}
}

fn generate_error_message(&self, path: &str, error: &str) -> String{
format!("{}: {}", path, error)
}

/// Returns Result - Sucessful execution
///
/// This is the main driver for the Converter struct
pub fn run<'a>(&mut self) -> Result<usize, &'a str> {
self.sanity_checks();

println!("Searching for CR2 images...");

let glob_string = if self.recursive {
format!("{}/**/*.cr2", self.start_dir)
} else {
format!("{}/*.cr2", self.start_dir)
};

let options = MatchOptions {
case_sensitive: false,
require_literal_separator: false,
require_literal_leading_dot: false,
};

let files: Vec<_> = glob_with(glob_string.as_str(), options)
.expect("There was an error configuring the Regex used in the Glob")
.filter_map(|x| x.ok())
.collect();

if files.len() == 0 {
error_chain::bail!("No .CR2 files could be found");
} else {
println!("{} images found...", files.len());
}

println!("\nStarting conversions on multiple threads...");

let image_failures: Vec<_> = files
.par_iter()
.map(|path| {
self.convert(path)
.map_err(|e| self.generate_error_message(path.to_str().unwrap(), e))
})
.filter_map(|x| x.err())
.collect();

println!("Completed...");
println!("\n{} failed conversions occurred...", image_failures.len());

Ok(files.len())
}
}




#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_recursive() {
let mut converter = Converter::new("./src/test_images", "./src/destination", false);

let ret = converter.run();

assert_eq!(ret, Ok(1));
}
}
60 changes: 60 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

mod lib;
extern crate image;
use std::time::Instant;
extern crate clap;
use clap::{Arg, App};

/// Program usage: run with --help flag to see usage
pub fn main() {
let matches = App::new("Datamatrix Scanner")
.version("1.0")
.author("Aaron Leopold <[email protected]>")
.about("Converts CR2 Images to JPG in Batch")
.arg(Arg::with_name("start_dir")
.short("s")
.long("start_dir")
.value_name("DIR")
.help("Sets the starting path")
.required(true)
.takes_value(true))
.arg(Arg::with_name("destination")
.short("d")
.long("destination")
.value_name("TO_DIR")
.help("Sets the path to save converted images")
.required(true)
.takes_value(true))
.arg(Arg::with_name("recursive")
.short("r")
.long("recursive")
.help("Sets the program to search for images at and below the start_dir")
.required(false)
.takes_value(false))
.get_matches();

let start_dir = matches.value_of("start_dir").unwrap();
let destination = matches.value_of("destination").unwrap();
let recursive = matches.is_present("recursive");

let mut converter = lib::Converter::new(start_dir, destination, recursive);

let start = Instant::now();

let num_files = converter.run();

let end = start.elapsed();

match num_files {
Ok(num) => {
println!("\nCompleted... {} files handled in {:?}.", num, end);

if num != 0 {
println!("Average time per image: {:?}", end / num as u32);
}
},

Err(err) => println!("{:?}", err)
}
}

0 comments on commit e8eab6d

Please sign in to comment.