use crate::{
icc::profile_desc_to_icc,
metadata::{self, Orientation, METADATA_ORIENTATION, METADATA_PROFILE_DESCRIPTION},
};
use eframe::{
egui,
epaint::{ColorImage, TextureHandle},
};
use image::{DynamicImage, RgbImage};
use lcms2::*;
use std::time::Instant;
use std::{
collections::HashMap,
fs::File,
io::Read,
path::PathBuf,
thread::{self, JoinHandle},
};
use fast_image_resize::{images::Image as FirImage, ResizeOptions};
use fast_image_resize::{PixelType, Resizer};
pub const LOAD_FAIL_PNG: &[u8; 95764] = include_bytes!("../resources/load_fail.png");
pub struct Image {
pub texture: Option<TextureHandle>,
pub metadata: HashMap<String, String>,
color_image: Option<ColorImage>,
}
impl Image {
pub fn load(
path: PathBuf,
image_size: Option<u32>,
output_icc_profile: String,
) -> JoinHandle<Option<Image>> {
thread::spawn(move || {
let file_name = path
.file_name()
.unwrap_or(path.as_os_str())
.to_string_lossy();
let mut now = Instant::now();
let mut f = match File::open(&path) {
Ok(f) => f,
Err(e) => {
println!("Failure opening image: {e}");
return get_error_image();
}
};
let mut buffer = Vec::new();
match f.read_to_end(&mut buffer) {
Ok(_) => {}
Err(e) => {
println!("{file_name} -> Error reading image into buffer: {e}");
return get_error_image();
}
}
println!(
"{} -> Spent {}ms reading into buffer",
file_name,
now.elapsed().as_millis()
);
now = Instant::now();
let mut image = Self::decode(&buffer, &file_name)?;
println!(
"{} -> Spent {}ms decoding",
file_name,
now.elapsed().as_millis()
);
now = Instant::now();
if image_size.is_some() {
image = Self::resize(image, image_size);
}
println!(
"{} -> Spent {}ms resizing",
file_name,
now.elapsed().as_millis()
);
now = Instant::now();
let metadata =
metadata::Metadata::get_image_metadata(&path.to_string_lossy()).unwrap_or_default();
println!(
"{} -> Spent {}ms reading metadata",
file_name,
now.elapsed().as_millis()
);
now = Instant::now();
image = Self::orient(image, &metadata);
println!(
"{} -> Spent {}ms orienting",
file_name,
now.elapsed().as_millis()
);
now = Instant::now();
let size = [image.width() as _, image.height() as _];
let mut flat_samples = image.into_rgb8().into_flat_samples();
let pixels = flat_samples.as_mut_slice();
if let Some(cpd) = metadata.get(METADATA_PROFILE_DESCRIPTION) {
Self::apply_cc(cpd, pixels, &path, &output_icc_profile);
};
println!(
"{} -> Spent {}ms applying CC",
file_name,
now.elapsed().as_millis()
);
Some(Image {
color_image: Some(ColorImage::from_rgb(size, pixels)),
texture: None,
metadata,
})
})
}
pub fn decode(buffer: &[u8], file_name: &str) -> Option<DynamicImage> {
match image::load_from_memory(buffer) {
Ok(img) => Some(img),
Err(e) => {
println!("{file_name} -> Failure decoding image: {e}");
None
}
}
}
pub fn resize(img: DynamicImage, target_size: Option<u32>) -> DynamicImage {
match target_size {
Some(target_size) => {
let aspect_ratio = img.width() as f32 / img.height() as f32;
let dest_width: u32;
let dest_height: u32;
if img.width() > img.height() {
dest_width = if img.width() > target_size {
target_size
} else {
img.width()
};
dest_height = (dest_width as f32 / aspect_ratio) as u32;
} else {
dest_height = if img.height() > target_size {
target_size
} else {
img.width()
};
dest_width = (dest_height as f32 * aspect_ratio) as u32;
};
if dest_width == 0 || dest_height == 0 {
return img;
}
let src_image = match FirImage::from_vec_u8(
img.width(),
img.height(),
img.to_rgb8().into_raw(),
PixelType::U8x3,
) {
Ok(img) => img,
Err(e) => {
println!(
"Failure building fast_image_resize image from dynamic image -> {e}",
);
return img;
}
};
let mut dest_image = FirImage::new(dest_width, dest_height, src_image.pixel_type());
let mut resizer = Resizer::new();
match resizer.resize(&src_image, &mut dest_image, &ResizeOptions::new()) {
Ok(_) => {}
Err(e) => {
println!("Failure resizing image -> {e}");
return img;
}
}
match RgbImage::from_raw(dest_width, dest_height, dest_image.buffer().to_vec()) {
Some(rgb_image) => DynamicImage::from(rgb_image),
None => {
println!("Failure building rgb image from resized image");
img
}
}
}
None => img,
}
}
pub fn orient(img: DynamicImage, metadata: &HashMap<String, String>) -> DynamicImage {
match metadata.get(METADATA_ORIENTATION) {
Some(o) => match metadata::Orientation::from_orientation_metadata(o) {
Orientation::Normal => img,
Orientation::MirrorHorizontal => img.fliph(),
Orientation::Rotate180 => img.rotate180(),
Orientation::MirrorVertical => img.flipv(),
Orientation::MirrorHorizontalRotate270 => img.fliph().rotate270(),
Orientation::Rotate90CW => img.rotate90(),
Orientation::MirrorHorizontalRotate90CW => img.fliph().rotate90(),
Orientation::Rotate270CW => img.rotate270(),
},
None => img,
}
}
pub fn apply_cc(
color_profile_desc: &str,
pixels: &mut [u8],
path: &PathBuf,
output_profile: &String,
) {
if color_profile_desc
.to_lowercase()
.contains(&output_profile.to_lowercase())
{
println!(
"Input {color_profile_desc} and output {output_profile} profiles are the same -> skipping"
);
return;
}
let input_icc_bytes = match profile_desc_to_icc(color_profile_desc) {
Some(icc_bytes) => icc_bytes.to_vec(),
None => {
println!(
"No built-in ICC profile matching {color_profile_desc} extracting from image"
);
match metadata::Metadata::extract_icc_from_image(path) {
Some(icc_bytes) => {
println!("Successfully extracted ICC profile from image");
icc_bytes
}
None => return,
}
}
};
let output_icc_bytes = match profile_desc_to_icc(output_profile) {
Some(icc_bytes) => icc_bytes.to_vec(),
None => {
println!("Badly configured output ICC profile -> {output_profile}");
return;
}
};
let input_profile = match Profile::new_icc(&input_icc_bytes) {
Ok(profile) => profile,
Err(_) => {
println!("Failed constructing input lcms2 profile from ICC data");
return;
}
};
let output_profile = match Profile::new_icc(&output_icc_bytes) {
Ok(profile) => profile,
Err(_) => {
println!("Failed constructing output lcms2 profile from ICC data");
return;
}
};
let transform = match Transform::new(
&input_profile,
PixelFormat::RGB_8,
&output_profile,
PixelFormat::RGB_8,
Intent::Perceptual,
) {
Ok(transform) => transform,
Err(_) => {
println!("Failure applying ICC profile to image");
return;
}
};
transform.transform_in_place(pixels);
}
pub fn get_texture(&mut self, name: &str, ui: &mut egui::Ui) -> &Option<TextureHandle> {
match &self.texture {
Some(_) => &self.texture,
None => match self.color_image.take() {
Some(img) => {
self.texture = Some(ui.ctx().load_texture(name, img, Default::default()));
&self.texture
}
None => &None,
},
}
}
}
pub fn get_error_image() -> Option<Image> {
let image = image::load_from_memory(LOAD_FAIL_PNG).unwrap();
let size = [image.width() as _, image.height() as _];
let mut flat_samples = image.into_rgb8().into_flat_samples();
let pixels = flat_samples.as_mut_slice();
Some(Image {
color_image: Some(ColorImage::from_rgb(size, pixels)),
texture: None,
metadata: HashMap::new(),
})
}