use std::path::Path;
extern crate image;
use image::{GenericImage, GenericImageView, Rgb, RgbImage};
use log::{debug, info};
use crate::error::Error;
use crate::utils;
#[derive(Debug, Clone)]
pub struct Master {
pub img: RgbImage,
pub cells: Vec<RgbImage>,
pub cell_size: (u32, u32),
grid_size: (u32, u32),
}
impl Master {
pub fn from_image(img: RgbImage, grid_size: (u32, u32)) -> Result<Self, Error> {
let (img_width, img_height) = img.dimensions();
let (grid_width, grid_height) = grid_size;
let (cell_width, cell_height) = (img_width / grid_width, img_height / grid_height);
info!("Grid Cell size: {}x{}", cell_width, cell_height);
let (width_extra, height_extra) = (img_width % grid_width, img_height % grid_height);
let img = if width_extra > 0 || height_extra > 0 {
info!("Cropping image to fit grid");
let img_cropped =
utils::crop_imm_centered(&img, img_width - width_extra, img_height - height_extra);
img_cropped.to_image()
} else {
img
};
debug!("Image dimensions: {}x{}", img.width(), img.height());
let cells = Self::construct_regions(&img, grid_size)?;
Ok(Master {
img,
cells,
cell_size: (cell_width, cell_height),
grid_size,
})
}
pub fn from_file<P: AsRef<Path>>(file: P, grid_size: (u32, u32)) -> Result<Self, Error> {
let img = image::open(file)?.to_rgb8();
Self::from_image(img, grid_size)
}
fn construct_regions(img: &RgbImage, grid_size: (u32, u32)) -> Result<Vec<RgbImage>, Error> {
let (grid_width, grid_height) = grid_size;
let (cell_width, cell_height) = (img.width() / grid_width, img.height() / grid_height);
if img.width() % grid_width != 0 || img.height() % grid_height != 0 {
return Err(
format!("Invalid grid shape, the imge dimensions: {}x{} are not divisible by the cell size: {}x{}",
img.width(), img.height(), cell_width, cell_height)
.into(),
);
}
let cells = (0..grid_height)
.flat_map(|y| {
(0..grid_width).map(move |x| {
img.view(x * cell_width, y * cell_height, cell_width, cell_height)
.to_image()
})
})
.collect();
Ok(cells)
}
pub fn overlay_grid(&self) -> Result<RgbImage, Error> {
let (grid_width, grid_height) = self.grid_size;
let mut grid_img = RgbImage::from_pixel(
self.img.width() + grid_width - 1,
self.img.height() + grid_height - 1,
Rgb([255, 255, 255]),
);
for (i, region) in self.cells.iter().enumerate() {
let col_i = i as u32 % grid_width;
let row_i = i as u32 / grid_width;
let x = col_i * self.cell_size.0 + col_i;
let y = row_i * self.cell_size.1 + row_i;
grid_img.copy_from(region, x, y)?;
}
Ok(grid_img)
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use image::RgbImage;
fn create_test_image(width: u32, height: u32) -> RgbImage {
let mut img = RgbImage::new(width, height);
for (x, y, pixel) in img.enumerate_pixels_mut() {
*pixel = image::Rgb([(x % 256) as u8, (y % 256) as u8, 0]);
}
img
}
fn test_dir() -> PathBuf {
PathBuf::from("tests/data/master")
}
fn test_master_img() -> PathBuf {
test_dir().join("master.png")
}
fn check_master(master: Master) {
let test_dir = test_dir();
assert_eq!(master.img.width(), 256);
assert_eq!(master.img.height(), 256);
assert_eq!(master.cells.len(), 16);
for region in &master.cells {
assert_eq!(region.width(), 64);
assert_eq!(region.height(), 64);
}
for (i, cell) in master.cells.iter().enumerate() {
let expected_file = test_dir.join(format!("cells/{:02}.png", i));
if std::env::var("PHOMO_UPDATE_EXPECTED").is_ok() {
cell.save(&expected_file).unwrap();
}
let expected_cell = image::open(&expected_file).unwrap().to_rgb8();
assert_eq!(cell, &expected_cell);
}
}
#[test]
fn test_from_image_buffer() {
let image_path = test_master_img();
let img = image::open(&image_path).unwrap().to_rgb8();
let grid_size = (4, 4);
let master = Master::from_image(img, grid_size).unwrap();
check_master(master);
}
#[test]
fn test_from_file_valid_image() {
let image_path = test_master_img();
let grid_size = (4, 4);
let master = Master::from_file(image_path, grid_size).unwrap();
check_master(master);
}
#[test]
fn test_from_image_non_divisible() {
let image_path = test_master_img();
let img = image::open(&image_path).unwrap().to_rgb8();
let grid_size = (4, 4);
let img_resized =
image::imageops::resize(&img, 255, 255, image::imageops::FilterType::Lanczos3);
let master = Master::from_image(img_resized, grid_size).unwrap();
assert_eq!(master.img.width(), 252);
assert_eq!(master.img.height(), 252);
assert_eq!(master.cells.len(), 16);
for region in &master.cells {
assert_eq!(region.width(), 63);
assert_eq!(region.height(), 63);
}
}
#[test]
fn test_construct_regions() {
let img = create_test_image(256, 256);
let grid_size = (4, 4);
let cells = Master::construct_regions(&img, grid_size).unwrap();
assert_eq!(cells.len(), 16); for region in &cells {
assert_eq!(region.width(), 64); assert_eq!(region.height(), 64);
}
}
#[test]
fn test_overlay_grid() {
let img = create_test_image(128, 256);
let grid_size = (4, 4);
let master = Master::from_image(img, grid_size).unwrap();
let grid_overlay = master.overlay_grid();
assert!(grid_overlay.is_ok());
let grid_overlay = grid_overlay.unwrap();
let expected_path = test_dir().join("grid_overlay.png");
if std::env::var("PHOMO_UPDATE_EXPECTED").is_ok() {
grid_overlay.save(&expected_path).unwrap();
}
let expected_img = image::open(expected_path).unwrap().to_rgb8();
assert_eq!(expected_img, grid_overlay);
}
#[test]
fn test_invalid_grid_shape() {
let img = create_test_image(256, 256);
let result = Master::construct_regions(&img, (5, 4));
assert!(result.is_err());
}
#[test]
fn test_invalid_file_path() {
let result = Master::from_file("invalid/path/to/file.png", (4, 4));
assert!(result.is_err());
}
}