492 lines
15 KiB
Rust
492 lines
15 KiB
Rust
use image::DynamicImage;
|
|
use image::RgbaImage;
|
|
use image::imageops::FilterType;
|
|
use image::metadata::Orientation;
|
|
use itertools::Itertools;
|
|
use jpegxl_rs::Endianness;
|
|
use jpegxl_rs::decode::PixelFormat;
|
|
use jpegxl_rs::decoder_builder;
|
|
use libheif_rs::{HeifContext, LibHeif, RgbChroma};
|
|
use rexiv2::Metadata;
|
|
use sha2::Digest;
|
|
use sha2::Sha256;
|
|
use sha2::digest::consts::U32;
|
|
use sha2::digest::generic_array::GenericArray;
|
|
use zune_image::codecs::jpeg::JpegDecoder;
|
|
use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace;
|
|
use zune_image::codecs::qoi::zune_core::options::DecoderOptions;
|
|
|
|
use std::env;
|
|
use std::fs;
|
|
use std::fs::File;
|
|
use std::fs::read;
|
|
use std::hash::Hash;
|
|
use std::io::BufReader;
|
|
use std::io::Cursor;
|
|
use std::io::Read;
|
|
use std::io::Write;
|
|
use std::mem;
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
use std::time::Instant;
|
|
|
|
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd)]
|
|
pub enum ImageFormat {
|
|
Jpg,
|
|
Jxl,
|
|
Heif,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct ImageData {
|
|
pub path: PathBuf,
|
|
pub format: ImageFormat,
|
|
pub embedded_thumbnail: bool,
|
|
pub orientation: Orientation,
|
|
pub hash: GenericArray<u8, U32>,
|
|
}
|
|
|
|
impl ImageData {
|
|
pub fn get_cache_path(&self) -> PathBuf {
|
|
let home_dir = PathBuf::from_str(&env::var("HOME").unwrap()).unwrap();
|
|
let cache_dir = home_dir.join(".cache/imflow");
|
|
if !cache_dir.exists() {
|
|
fs::create_dir(&cache_dir).unwrap();
|
|
}
|
|
let hash_hex = format!("{:x}", self.hash);
|
|
return cache_dir.join(hash_hex).to_path_buf();
|
|
}
|
|
}
|
|
|
|
impl Hash for ImageData {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
state.write(self.path.to_str().unwrap().as_bytes());
|
|
state.write(self.hash.as_slice());
|
|
}
|
|
}
|
|
|
|
impl PartialEq for ImageData {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.hash.eq(&other.hash)
|
|
}
|
|
}
|
|
|
|
impl Eq for ImageData {}
|
|
|
|
#[derive(Clone)]
|
|
pub struct ImflowImageBuffer {
|
|
pub width: usize,
|
|
pub height: usize,
|
|
pub rgba_buffer: Vec<u32>,
|
|
pub rating: i32,
|
|
}
|
|
|
|
pub fn get_rating(image: &ImageData) -> i32 {
|
|
let meta = Metadata::new_from_path(&image.path);
|
|
match meta {
|
|
Ok(meta) => {
|
|
let rating = meta.get_tag_numeric("Xmp.xmp.Rating");
|
|
rating
|
|
}
|
|
Err(e) => panic!("{:?}", e),
|
|
}
|
|
}
|
|
|
|
pub fn get_orientation(path: &PathBuf) -> Orientation {
|
|
let meta = Metadata::new_from_path(path);
|
|
match meta {
|
|
Ok(meta) => Orientation::from_exif(meta.get_orientation() as u8)
|
|
.unwrap_or(Orientation::NoTransforms),
|
|
Err(_) => Orientation::NoTransforms,
|
|
}
|
|
}
|
|
|
|
fn swap_wh<T>(width: T, height: T, orientation: Orientation) -> (T, T) {
|
|
if [
|
|
Orientation::Rotate90,
|
|
Orientation::Rotate270,
|
|
Orientation::Rotate90FlipH,
|
|
Orientation::Rotate270FlipH,
|
|
]
|
|
.contains(&orientation)
|
|
{
|
|
return (height, width);
|
|
}
|
|
(width, height)
|
|
}
|
|
|
|
fn get_format(path: &PathBuf) -> Option<ImageFormat> {
|
|
if !path.is_file() {
|
|
return None;
|
|
}
|
|
let os_str = path.extension().unwrap().to_ascii_lowercase();
|
|
let extension = &os_str.to_str().unwrap();
|
|
if ["heic", "heif"].contains(extension) {
|
|
Some(ImageFormat::Heif)
|
|
} else if ["jpg", "jpeg"].contains(extension) {
|
|
Some(ImageFormat::Jpg)
|
|
} else if ["jxl"].contains(extension) {
|
|
Some(ImageFormat::Jxl)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
|
|
let total_start = Instant::now();
|
|
|
|
match image.format {
|
|
ImageFormat::Heif => {
|
|
let img = load_heif(image, false);
|
|
let total_time = total_start.elapsed();
|
|
println!("Total HEIF loading time: {:?}", total_time);
|
|
img
|
|
}
|
|
ImageFormat::Jxl => {
|
|
let rating = get_rating(image);
|
|
|
|
let file = read(image.path.clone()).unwrap();
|
|
use jpegxl_rs::ThreadsRunner;
|
|
let runner = ThreadsRunner::default();
|
|
let decoder = decoder_builder()
|
|
.parallel_runner(&runner)
|
|
.pixel_format(PixelFormat {
|
|
num_channels: 4,
|
|
endianness: Endianness::Big,
|
|
align: 8,
|
|
})
|
|
.build()
|
|
.unwrap();
|
|
|
|
let (metadata, buffer) = decoder.decode_with::<u8>(&file).unwrap();
|
|
let width = metadata.width as usize;
|
|
let height = metadata.height as usize;
|
|
|
|
let rgba_buffer = unsafe {
|
|
Vec::from_raw_parts(
|
|
buffer.as_ptr() as *mut u32,
|
|
buffer.len() / 4,
|
|
buffer.len() / 4,
|
|
)
|
|
};
|
|
std::mem::forget(buffer);
|
|
|
|
println!("Total JXL loading time: {:?}", total_start.elapsed());
|
|
|
|
ImflowImageBuffer {
|
|
width,
|
|
height,
|
|
rgba_buffer,
|
|
rating,
|
|
}
|
|
}
|
|
ImageFormat::Jpg => {
|
|
let rating = get_rating(image);
|
|
|
|
let mut buffer: Vec<u8>;
|
|
let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::RGBA);
|
|
let file = read(image.path.clone()).unwrap();
|
|
let mut decoder = JpegDecoder::new(&file);
|
|
decoder.set_options(options);
|
|
|
|
decoder.decode_headers().unwrap();
|
|
let info = decoder.info().unwrap();
|
|
let width = info.width as usize;
|
|
let height = info.height as usize;
|
|
buffer = vec![0; width * height * 4];
|
|
decoder.decode_into(buffer.as_mut_slice()).unwrap();
|
|
|
|
let orientation_start = Instant::now();
|
|
// TODO: Optimize rotation
|
|
let orientation = image.orientation;
|
|
let image = RgbaImage::from_raw(width as u32, height as u32, buffer).unwrap();
|
|
let mut dynamic_image = DynamicImage::from(image);
|
|
dynamic_image.apply_orientation(orientation);
|
|
let buffer = dynamic_image.as_rgba8().unwrap();
|
|
let (width, height) = swap_wh(width, height, orientation);
|
|
let orientation_time = orientation_start.elapsed();
|
|
|
|
// Reinterpret to avoid copying
|
|
let rgba_buffer = unsafe {
|
|
Vec::from_raw_parts(
|
|
buffer.as_ptr() as *mut u32,
|
|
buffer.len() / 4,
|
|
buffer.len() / 4,
|
|
)
|
|
};
|
|
std::mem::forget(dynamic_image);
|
|
let total_time = total_start.elapsed();
|
|
println!("Orientation time: {:?}", orientation_time);
|
|
println!("Total loading time: {:?}", total_time);
|
|
ImflowImageBuffer {
|
|
width,
|
|
height,
|
|
rgba_buffer,
|
|
rating,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn image_to_rgba_buffer(img: DynamicImage) -> Vec<u32> {
|
|
let flat = img.to_rgba8();
|
|
let mut buffer = flat.to_vec();
|
|
let vec = unsafe {
|
|
Vec::from_raw_parts(
|
|
buffer.as_mut_ptr() as *mut u32,
|
|
buffer.len() / 4,
|
|
buffer.len() / 4,
|
|
)
|
|
};
|
|
mem::forget(buffer);
|
|
vec
|
|
}
|
|
|
|
pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
|
|
fs::read_dir(dir)
|
|
.unwrap()
|
|
.map(|f| f.unwrap().path().to_path_buf())
|
|
.sorted()
|
|
.filter_map(|path| {
|
|
if let Some(format) = get_format(&path) {
|
|
let meta = Metadata::new_from_path(&path)
|
|
.expect(&format!("Image has no metadata: {:?}", path).to_string());
|
|
let embedded_thumbnail = if format == ImageFormat::Heif {
|
|
let ctx = HeifContext::read_from_file(path.to_str().unwrap()).unwrap();
|
|
let binding = ctx.top_level_image_handles();
|
|
let handle = binding.get(0).unwrap();
|
|
handle.number_of_thumbnails() > 0
|
|
} else {
|
|
meta.get_preview_images().is_some()
|
|
};
|
|
let orientation = Orientation::from_exif(meta.get_orientation() as u8)
|
|
.unwrap_or(Orientation::NoTransforms);
|
|
let hash = get_file_hash(&path);
|
|
Some(ImageData {
|
|
path,
|
|
format,
|
|
embedded_thumbnail,
|
|
orientation,
|
|
hash,
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<ImageData>>()
|
|
}
|
|
|
|
pub fn check_embedded_thumbnail(path: &PathBuf) -> bool {
|
|
if let Ok(meta) = Metadata::new_from_path(&path) {
|
|
meta.get_preview_images().is_some()
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn get_embedded_thumbnail(image: &ImageData) -> Option<Vec<u8>> {
|
|
let meta = Metadata::new_from_path(&image.path);
|
|
match meta {
|
|
Ok(meta) => {
|
|
if let Some(previews) = meta.get_preview_images() {
|
|
for preview in previews {
|
|
return Some(preview.get_data().unwrap());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
Err(_) => None,
|
|
}
|
|
}
|
|
|
|
pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer {
|
|
let cache_path = path.get_cache_path();
|
|
if cache_path.exists() {
|
|
let bytes = fs::read(cache_path).unwrap();
|
|
let width = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize;
|
|
let height = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
|
|
let buffer: &[u8] = &bytes[8..];
|
|
let mut buffer_u8 = buffer.to_vec();
|
|
let buffer_u32 = unsafe {
|
|
Vec::from_raw_parts(
|
|
buffer_u8.as_mut_ptr() as *mut u32,
|
|
buffer_u8.len() / 4,
|
|
buffer_u8.len() / 4,
|
|
)
|
|
};
|
|
std::mem::forget(buffer_u8);
|
|
|
|
assert_eq!(width * height, buffer_u32.len());
|
|
|
|
return ImflowImageBuffer {
|
|
width,
|
|
height,
|
|
rgba_buffer: buffer_u32,
|
|
rating: 0,
|
|
};
|
|
}
|
|
let thumbnail = if path.format == ImageFormat::Heif {
|
|
load_heif(path, true)
|
|
} else {
|
|
load_thumbnail_exif(path).unwrap_or(load_thumbnail_full(path))
|
|
};
|
|
|
|
save_thumbnail(&cache_path, thumbnail.clone());
|
|
thumbnail
|
|
}
|
|
|
|
pub fn load_thumbnail_exif(path: &ImageData) -> Option<ImflowImageBuffer> {
|
|
match get_embedded_thumbnail(path) {
|
|
Some(thumbnail) => {
|
|
let decoder = image::ImageReader::new(Cursor::new(thumbnail))
|
|
.with_guessed_format()
|
|
.unwrap();
|
|
let mut image = decoder.decode().unwrap();
|
|
|
|
image.apply_orientation(path.orientation);
|
|
let width: usize = image.width() as usize;
|
|
let height: usize = image.height() as usize;
|
|
let flat = image.into_rgba8().into_raw();
|
|
let mut buffer = flat.to_vec();
|
|
let buffer_u32 = unsafe {
|
|
Vec::from_raw_parts(
|
|
buffer.as_mut_ptr() as *mut u32,
|
|
buffer.len() / 4,
|
|
buffer.len() / 4,
|
|
)
|
|
};
|
|
std::mem::forget(buffer);
|
|
|
|
let rating = get_rating(path.into());
|
|
|
|
Some(ImflowImageBuffer {
|
|
width,
|
|
height,
|
|
rgba_buffer: buffer_u32,
|
|
rating,
|
|
})
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn load_thumbnail_full(path: &ImageData) -> ImflowImageBuffer {
|
|
let file = BufReader::new(File::open(path.path.clone()).unwrap());
|
|
let reader = image::ImageReader::new(file);
|
|
let image = reader
|
|
.with_guessed_format()
|
|
.unwrap()
|
|
.decode()
|
|
.unwrap()
|
|
.resize_to_fill(1920, 1920, FilterType::Lanczos3);
|
|
let width = image.width() as usize;
|
|
let height = image.height() as usize;
|
|
let buffer = image_to_rgba_buffer(image);
|
|
let rating = get_rating(path.into());
|
|
|
|
ImflowImageBuffer {
|
|
width,
|
|
height,
|
|
rgba_buffer: buffer,
|
|
rating,
|
|
}
|
|
}
|
|
|
|
pub fn load_heif(path: &ImageData, resize: bool) -> ImflowImageBuffer {
|
|
let lib_heif = LibHeif::new();
|
|
let ctx = HeifContext::read_from_file(path.path.to_str().unwrap()).unwrap();
|
|
|
|
let image = if resize {
|
|
let binding = ctx.top_level_image_handles();
|
|
let handle = binding.get(0).unwrap();
|
|
let thumbnail_count = handle.number_of_thumbnails() as u32;
|
|
let mut thumbnail_ids = vec![0u32, thumbnail_count];
|
|
handle.thumbnail_ids(&mut thumbnail_ids);
|
|
let handle = &handle.thumbnail(thumbnail_ids[0]).unwrap();
|
|
|
|
lib_heif
|
|
.decode(handle, libheif_rs::ColorSpace::Rgb(RgbChroma::Rgba), None)
|
|
.unwrap()
|
|
} else {
|
|
let binding = ctx.top_level_image_handles();
|
|
let handle = binding.get(0).unwrap();
|
|
|
|
lib_heif
|
|
.decode(handle, libheif_rs::ColorSpace::Rgb(RgbChroma::Rgba), None)
|
|
.unwrap()
|
|
};
|
|
|
|
assert_eq!(
|
|
image.color_space(),
|
|
Some(libheif_rs::ColorSpace::Rgb(RgbChroma::Rgba)),
|
|
);
|
|
|
|
let width = image.width() as usize;
|
|
let height = image.height() as usize;
|
|
|
|
// Scale the image
|
|
// if resize {
|
|
// const MAX: usize = 3000;
|
|
// let scale = max(width, height) as f32 / MAX as f32;
|
|
// width = (width as f32 / scale) as usize;
|
|
// height = (height as f32 / scale) as usize;
|
|
// // image = image.scale(width as u32, height as u32, None).unwrap();
|
|
// image = image.scale(599, 300, None).unwrap();
|
|
// width = image.width() as usize;
|
|
// height = image.height() as usize;
|
|
// }
|
|
|
|
let rating = get_rating(path);
|
|
|
|
// Get "pixels"
|
|
let planes = image.planes();
|
|
let interleaved_plane = planes.interleaved.unwrap();
|
|
assert!(!interleaved_plane.data.is_empty());
|
|
assert!(interleaved_plane.stride > 0);
|
|
assert_eq!(interleaved_plane.storage_bits_per_pixel, 32);
|
|
|
|
let rgba_buffer = interleaved_plane.data;
|
|
let u32_slice = unsafe {
|
|
std::slice::from_raw_parts(rgba_buffer.as_ptr() as *const u32, rgba_buffer.len() / 4)
|
|
};
|
|
|
|
ImflowImageBuffer {
|
|
width,
|
|
height,
|
|
rgba_buffer: u32_slice.to_vec(),
|
|
rating,
|
|
}
|
|
}
|
|
|
|
pub fn get_file_hash(path: &PathBuf) -> GenericArray<u8, U32> {
|
|
let mut file = File::open(path).unwrap();
|
|
let mut buf = [0u8; 16 * 1024];
|
|
file.read(&mut buf).unwrap();
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(&buf);
|
|
hasher.update(file.metadata().unwrap().len().to_le_bytes());
|
|
hasher.finalize()
|
|
}
|
|
|
|
// TODO: optimize
|
|
pub fn save_thumbnail(path: &PathBuf, image: ImflowImageBuffer) {
|
|
let cache_dir = path.parent().unwrap();
|
|
if !cache_dir.exists() {
|
|
fs::create_dir(cache_dir).unwrap();
|
|
}
|
|
println!("path: {:?}", path);
|
|
let mut file = File::create(path).unwrap();
|
|
let buffer = image.rgba_buffer;
|
|
let u8_buffer = unsafe {
|
|
Vec::from_raw_parts(
|
|
buffer.as_ptr() as *mut u8,
|
|
buffer.len() * 4,
|
|
buffer.len() * 4,
|
|
)
|
|
};
|
|
std::mem::forget(buffer);
|
|
file.write(&(image.width as u32).to_le_bytes()).unwrap();
|
|
file.write(&(image.height as u32).to_le_bytes()).unwrap();
|
|
file.write(&u8_buffer).unwrap();
|
|
}
|