diff --git a/Cargo.lock b/Cargo.lock index 0a7a2f4..37adfb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,9 +448,9 @@ dependencies = [ [[package]] name = "deflate" -version = "0.7.20" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" +checksum = "050ef6de42a33903b30a7497b76b40d3d58691d4d3eec355348c122444a388f0" dependencies = [ "adler32", "byteorder 1.3.4", @@ -1043,9 +1043,9 @@ dependencies = [ [[package]] name = "image" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4e336ec01a678e7ab692914c641181528e8656451e6252f8f9e33728882eaf" +checksum = "9062b90712d25bc6bb165d110aa59c6b47c849246e341e7b86a98daff9d49f60" dependencies = [ "bytemuck", "byteorder 1.3.4", @@ -1059,6 +1059,15 @@ dependencies = [ "tiff", ] +[[package]] +name = "imageutils" +version = "0.1.0" +source = "git+https://git.z.xinu.tv/wathiede/imageutils#d6804fa0f8e0afe2deb54354acc983a1ab59d794" +dependencies = [ + "image", + "jpeg-decoder", +] + [[package]] name = "imgref" version = "1.4.0" @@ -1678,6 +1687,7 @@ dependencies = [ "google_api_auth", "hexihasher", "image", + "imageutils", "jpeg-decoder", "lazy_static 1.4.0", "load_image", @@ -1750,9 +1760,9 @@ dependencies = [ [[package]] name = "png" -version = "0.15.3" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef859a23054bbfee7811284275ae522f0434a3c8e7f4b74bd4a35ae7e1c4a283" +checksum = "46060468187c21c00ffa2a920690b29997d7fd543f5a4d400461e4a7d4fccde8" dependencies = [ "bitflags", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index a673481..a2cefec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,12 @@ structopt = "0.3.9" yup-oauth2 = "^3.1" warp = "0.1" serde = { version = "1.0.104", features = ["derive"] } -image = { version = "0.23.0" } #, default-features = false, features = ["jpeg"] } +image = { version = "0.23.2" } #, default-features = false, features = ["jpeg"] } rust-embed = "5.2.0" mime_guess = "2.0.1" rocksdb = "0.13.0" jpeg-decoder = "0.1.18" +imageutils = { git = "https://git.z.xinu.tv/wathiede/imageutils" } [dependencies.prometheus] features = ["process"] diff --git a/src/library.rs b/src/library.rs index e5e1ba8..5802069 100644 --- a/src/library.rs +++ b/src/library.rs @@ -1,20 +1,13 @@ use std::fs; use std::fs::File; use std::io; -use std::io::BufReader; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use google_photoslibrary1 as photos; use image::imageops; -use image::DynamicImage; -use image::GenericImage; -use image::GenericImageView; -use image::ImageBuffer; -use image::ImageFormat; -use image::ImageResult; -use jpeg_decoder::Decoder; +use imageutils::{load_image, resize, resize_to_fill, save_to_jpeg_bytes, FilterType}; use log::error; use log::info; use log::warn; @@ -34,128 +27,6 @@ pub struct Library { cache_db: Arc, } -pub fn load_image

( - path: P, - width_hint: Option, - height_hint: Option, -) -> Result> -where - P: AsRef, -{ - // TODO(wathiede): fall back to image::load_image when jpeg decoding fails. - let file = File::open(path).expect("failed to open file"); - let mut decoder = Decoder::new(BufReader::new(file)); - let (w, h) = match (width_hint, height_hint) { - (Some(w), Some(h)) => { - let got = decoder.scale(w as u16, h as u16)?; - //info!("Hinted at {}x{}, got {}x{}", w, h, got.0, got.1); - (got.0 as u32, got.1 as u32) - } - // TODO(wathiede): handle partial hints by grabbing info and then computing the absent - // dimenison. - _ => { - decoder.read_info()?; - let info = decoder.info().unwrap(); - (info.width as u32, info.height as u32) - } - }; - let pixels = decoder.decode().expect("failed to decode image"); - Ok(DynamicImage::ImageRgb8( - ImageBuffer::from_raw(w, h, pixels).expect("pixels to small for given dimensions"), - )) -} - -#[derive(Clone, Copy, Debug)] -pub enum FilterType { - Builtin(imageops::FilterType), - Nearest, -} - -/// fill_size computes the largest rectangle that fits in src with the aspect ratio of dst. -fn fill_size(src: (u32, u32), dst: (u32, u32)) -> (u32, u32) { - debug_assert!(src.0 >= dst.0); - debug_assert!(src.1 >= dst.1); - let x_scale = src.0 as f32 / dst.0 as f32; - let y_scale = src.1 as f32 / dst.1 as f32; - if x_scale > y_scale { - // Height will fill, width will crop. - ( - (dst.0 as f32 * y_scale) as u32, - (dst.1 as f32 * y_scale) as u32, - ) - } else { - // Width will fill, height will crop. - ( - (dst.0 as f32 * x_scale) as u32, - (dst.1 as f32 * x_scale) as u32, - ) - } -} - -fn resize_to_fill_nearest(w: u32, h: u32, img: &DynamicImage) -> DynamicImage { - let mut dst = DynamicImage::new_rgb8(w, h); - let (src_w, src_h) = img.dimensions(); - let (crop_w, crop_h) = fill_size((src_w, src_h), (w, h)); - let off_x = (src_w - crop_w) / 2; - let off_y = (src_h - crop_h) / 2; - let src = img.view(off_x, off_y, crop_w, crop_h); - let x_scale = crop_w as f32 / w as f32; - let y_scale = crop_h as f32 / h as f32; - - for y in 0..h { - for x in 0..w { - let x_idx = (x as f32 * x_scale).round() as u32; - let y_idx = (y as f32 * y_scale).round() as u32; - dst.put_pixel(x, y, src.get_pixel(x_idx, y_idx)) - } - } - dst -} - -pub fn resize( - img: &DynamicImage, - dimensions: (Option, Option), - filter: FilterType, -) -> DynamicImage { - let (w, h) = dimensions; - let (orig_w, orig_h) = img.dimensions(); - let (w, h) = match (w, h) { - (Some(w), Some(h)) => (w, h), - (Some(w), None) => (w, orig_h * w / orig_w), - (None, Some(h)) => (orig_w * h / orig_h, h), - (None, None) => (orig_w, orig_h), - }; - match filter { - FilterType::Builtin(filter) => img.resize(w, h, filter), - FilterType::Nearest => unimplemented!(), //resize_to_fill_nearest(w, h, img), - } -} - -pub fn resize_to_fill( - img: &DynamicImage, - dimensions: (Option, Option), - filter: FilterType, -) -> DynamicImage { - let (w, h) = dimensions; - let (orig_w, orig_h) = img.dimensions(); - let (w, h) = match (w, h) { - (Some(w), Some(h)) => (w, h), - (Some(w), None) => (w, orig_h * w / orig_w), - (None, Some(h)) => (orig_w * h / orig_h, h), - (None, None) => (orig_w, orig_h), - }; - match filter { - FilterType::Builtin(filter) => img.resize_to_fill(w, h, filter), - FilterType::Nearest => resize_to_fill_nearest(w, h, img), - } -} - -pub fn save_to_jpeg_bytes(img: &DynamicImage) -> ImageResult> { - let mut buf = Vec::new(); - img.write_to(&mut buf, ImageFormat::Jpeg)?; - Ok(buf) -} - impl Library { pub fn new(root: PathBuf) -> Result> { let db = DB::open_default(root.join("cache"))?; @@ -364,86 +235,6 @@ mod test { use super::*; use tempdir::TempDir; - fn compare_images(lhs: DynamicImage, rhs: DynamicImage) { - let lhs = lhs.to_rgb(); - let rhs = rhs.to_rgb(); - // Based on https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio#Definition - // - let mut mse: [i64; 3] = [0, 0, 0]; - for (l, r) in lhs.pixels().zip(rhs.pixels()) { - let image::Rgb(l_pix) = l; - let image::Rgb(r_pix) = r; - - { - for i in 0..3 { - let d = l_pix[i] as i64 - r_pix[i] as i64; - let d2 = d * d; - mse[i] += d2; - } - } - - // assert_eq!(l_pix, r_pix, "{:?} != {:?} @ {} x {} ", l_pix, r_pix, x, y); - } - let (w, h) = lhs.dimensions(); - let mn = (w * h) as i64; - mse.iter_mut().for_each(|i| *i = *i / mn); - let psnr: Vec<_> = mse - .iter() - .map(|i| 20. * 255_f32.log10() - 10. * (*i as f32).log10()) - .collect(); - // Uncomment to explore differences - /* - lhs.save("/tmp/lhs.png").expect("failed to write lhs.png"); - rhs.save("/tmp/rhs.png").expect("failed to write rhs.png"); - assert!(false, "MSE {:?} PSNR {:?} dB", mse, psnr); - */ - } - - #[test] - fn fill_sizes() { - let srcs = vec![(400, 300), (300, 400)]; - - let dsts = vec![(225, 300), (300, 225), (100, 100)]; - - let want = vec![ - (225, 300), - (400, 300), - (300, 300), - (300, 400), - (300, 225), - (300, 300), - ]; - let mut i = 0; - for s in &srcs { - for d in &dsts { - let w = want[i]; - dbg!(s, d, w); - let got = fill_size(*s, *d); - assert_eq!( - got, w, - "{}. src {:?} dst {:?} want {:?} got {:?}", - i, s, d, w, got - ); - i += 1; - } - } - } - - #[test] - fn resize_to_fill_nearest() { - let w = 256; - let h = 256; - const TEST_IMAGE_PATH: &'static str = "testdata/image.jpg"; - let img = load_image(TEST_IMAGE_PATH).expect("failed to load test image"); - let reference = resize( - &img, - (Some(w), Some(h)), - FilterType::Builtin(imageops::FilterType::Nearest), - ); - let got = resize(&img, (Some(w), Some(h)), FilterType::Nearest); - compare_images(reference, got); - } - #[test] fn clean_db() { let td = TempDir::new("photosync_test").expect("failed to create temporary directory"); diff --git a/testdata/image.jpg b/testdata/image.jpg deleted file mode 100644 index cf94abb..0000000 Binary files a/testdata/image.jpg and /dev/null differ