-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e8eab6d
Showing
5 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/target | ||
Cargo.lock | ||
|
||
test_images | ||
destination |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
|