Move image helpers to xinu.tv/imageutils
This commit is contained in:
211
src/library.rs
211
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<DB>,
|
||||
}
|
||||
|
||||
pub fn load_image<P>(
|
||||
path: P,
|
||||
width_hint: Option<u32>,
|
||||
height_hint: Option<u32>,
|
||||
) -> Result<DynamicImage, Box<dyn std::error::Error>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
// 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<u32>, Option<u32>),
|
||||
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<u32>, Option<u32>),
|
||||
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<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
img.write_to(&mut buf, ImageFormat::Jpeg)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
impl Library {
|
||||
pub fn new(root: PathBuf) -> Result<Library, Box<dyn std::error::Error>> {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user