From c3c69b160f4c5fd851fd1a49c01f633a56351f5d Mon Sep 17 00:00:00 2001 From: Julian T Date: Thu, 22 Jul 2021 21:36:15 +0200 Subject: Add smart image conversion --- .gitignore | 3 +- Cargo.lock | 23 +++++++++ Cargo.toml | 2 + src/context.rs | 19 ++++++++ src/main.rs | 13 ++++- src/picture.rs | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/piece.rs | 22 +++++++++ 7 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/picture.rs create mode 100644 src/piece.rs diff --git a/.gitignore b/.gitignore index 35d134e..6973ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -dist/imgs/* - +build # Added by cargo diff --git a/Cargo.lock b/Cargo.lock index 5f1e4cd..8b9e736 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,6 +258,8 @@ version = "0.1.0" dependencies = [ "glob", "image", + "kamadak-exif", + "md5", "serde", "serde_yaml", "structopt", @@ -400,6 +402,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "kamadak-exif" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70494964492bf8e491eb3951c5d70c9627eb7100ede6cc56d748b9a3f302cfb6" +dependencies = [ + "mutate_once", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -433,6 +444,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.4.0" @@ -467,6 +484,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + [[package]] name = "num-integer" version = "0.1.44" diff --git a/Cargo.toml b/Cargo.toml index 5b0a26f..385d8a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,5 @@ serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" structopt = "0.3" glob = "0.3" +md5 = "0.7" +kamadak-exif = "0.5" diff --git a/src/context.rs b/src/context.rs index 6bc4c05..55f3f97 100644 --- a/src/context.rs +++ b/src/context.rs @@ -14,6 +14,15 @@ pub struct Options { pub config: Option, #[structopt(help = "Picture location")] pub load: PathBuf, + #[structopt(short, long, default_value = "png", help = "Image extension to use for converted files")] + pub ext: String, + #[structopt(long, short, default_value = "build", help = "Where to build site")] + pub builddir: PathBuf, + + #[structopt(long, default_value = "1080", help = "Scaled size for image")] + pub size_scaled: u32, + #[structopt(long, default_value = "720", help = "Thumbnail size for image")] + pub size_thumb: u32, } #[derive(Debug)] @@ -36,16 +45,26 @@ pub struct Config { pub struct Context { pub options: Options, pub config: Config, + pub imgdir: PathBuf, } impl Context { pub fn new_with_args() -> Result { let opts = Options::from_args(); let config = Config::load_with_options(&opts)?; + let imgdir = opts.builddir.join("imgs"); + + // Create img dir + if let Err(err) = fs::create_dir(&imgdir) { + if err.kind() != io::ErrorKind::AlreadyExists { + return Err(ConfigError::from(err)); + } + } Ok(Context { options: opts, config: config, + imgdir: imgdir, }) } diff --git a/src/main.rs b/src/main.rs index 06346a2..28fe0c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,23 @@ mod context; +mod picture; +mod piece; + use context::Context; +use picture::Picture; +use piece::Piece; fn main() { println!("Hello, world!"); let ctx = Context::new_with_args().unwrap(); - println!("{:?}", ctx); - for file in ctx.get_image_files().unwrap() { println!("{}", file.display()); + + let pic = Picture::new_from_file(&file).unwrap(); + println!("{:?}", pic); + + let piece = Piece::new(&ctx, pic).unwrap(); + println!("{:?}", piece); } } diff --git a/src/picture.rs b/src/picture.rs new file mode 100644 index 0000000..f92876c --- /dev/null +++ b/src/picture.rs @@ -0,0 +1,149 @@ +use std::path::{PathBuf, Path}; +use std::fs; +use std::io; +use std::io::{Read, Seek}; +use image::io::Reader as ImageReader; +use image::error::ImageError; +use image::imageops; + +use crate::context::Context; + +#[derive(Debug)] +pub enum LoadError { + Io(io::Error), + ExifParser(exif::Error) +} + +#[derive(Debug)] +pub enum ConversionError { + Io(io::Error), + ImageError(ImageError), +} + +#[derive(Debug)] +pub struct Picture { + taken: Option, + hash: md5::Digest, + path: PathBuf, +} + +pub struct Converter<'a> { + imgdata: Option, + pic: &'a Picture, +} + +fn hash_reader(reader: &mut R) -> Result { + let mut hash = md5::Context::new(); + let mut buff = [0; 1024]; + + loop { + let count = reader.read(&mut buff)?; + if count == 0 { + // Reached end, stopping + break + } + + hash.consume(&buff[..count]) + } + + Ok(hash.compute()) +} + +impl Picture { + /// Hash file content and load exif data. + pub fn new_from_file(path: &Path) -> Result { + let file = fs::File::open(path)?; + let mut reader = io::BufReader::new(&file); + + let taken = match exif::Reader::new().read_from_container(&mut reader) { + Ok(exif) => exif.get_field(exif::Tag::DateTimeOriginal, exif::In::PRIMARY) + .map(|field| field.display_value().with_unit(&exif).to_string()), + Err(err) => { + println!("Could not load exif data for {}: {}", path.to_str().unwrap(), err); + None + } + }; + + // Move back to start of file for hashing + reader.seek(io::SeekFrom::Start(0))?; + + Ok(Picture { + taken, + hash: hash_reader(&mut reader)?, + path: path.to_path_buf(), + }) + } + + pub fn convert(&self) -> Result { + Ok(Converter { + imgdata: None, + pic: self, + }) + } +} + +impl Converter<'_> { + fn convert_image(&mut self, size: u32, dest: &Path) -> Result<(), ImageError> { + let scaled = self.get_imgdata()?.resize( + size, + std::u32::MAX, + imageops::FilterType::Lanczos3); + + scaled.save(dest) + } + + fn get_imgdata(&mut self) -> Result<&image::DynamicImage, ImageError> { + let picpath = &self.pic.path; + + match self.imgdata { + None => self.imgdata = Some( + ImageReader::open(picpath)?.decode()? + ), + _ => () + } + + Ok(self.imgdata.as_ref().unwrap()) + } + + pub fn get_size(&mut self, ctx: &Context, size: u32) -> Result { + let hash = md5::compute(format!("{},{},{:?}", size, ctx.options.ext, self.pic.hash)); + let name = format!("{:?}.{}", hash, ctx.options.ext); + let path = ctx.imgdir.join(name); + + match path.exists() { + true => { + println!("Image of size {} already exists", size); + Ok(path) + }, + false => { + println!("Scaling image to size {}", size); + self.convert_image(size, &path)?; + Ok(path) + } + } + } +} + +impl From for LoadError { + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + +impl From for LoadError { + fn from(error: exif::Error) -> Self { + Self::ExifParser(error) + } +} + +impl From for ConversionError { + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + +impl From for ConversionError { + fn from(error: ImageError) -> Self { + Self::ImageError(error) + } +} diff --git a/src/piece.rs b/src/piece.rs new file mode 100644 index 0000000..a0cc18e --- /dev/null +++ b/src/piece.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; +use crate::picture::{Picture, ConversionError}; +use crate::context::Context; + +#[derive(Debug)] +pub struct Piece { + pic: Picture, + scaled_path: PathBuf, + thumb_path: PathBuf, +} + +impl Piece { + pub fn new(ctx: &Context, pic: Picture) -> Result { + let mut conv = pic.convert()?; + let scaled_path = conv.get_size(ctx, ctx.options.size_scaled)?; + let thumb_path = conv.get_size(ctx, ctx.options.size_thumb)?; + + Ok(Piece { + pic, scaled_path, thumb_path, + }) + } +} -- cgit v1.2.3