diff --git a/core/Cargo.toml b/core/Cargo.toml index 3cd88d4..e02c176 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -11,8 +11,8 @@ svg_metadata = "0.5.1" kamadak-exif = "0.6.1" # decoder backends -image = {version = "0.25.9", features = ["rayon", "png", "jpeg", "gif", "webp"], default-features = false} +image = {version = "0.25.9", features = ["rayon", "png", "jpeg", "gif", "webp", "tiff"], default-features = false} zune-image = {version = "0.4.15", features = ["threads", "simd", "metadata", "png", "jpeg", "jpeg-xl"], default-features = false} [dev-dependencies] -env_logger = "0.11" \ No newline at end of file +env_logger = "0.11" diff --git a/core/src/backends/image_rs/mod.rs b/core/src/backends/image_rs/mod.rs index caa5a0a..8df1cdd 100644 --- a/core/src/backends/image_rs/mod.rs +++ b/core/src/backends/image_rs/mod.rs @@ -1,9 +1,15 @@ use std::{collections::HashSet, io::BufReader}; -use image::{AnimationDecoder, ImageDecoder, ImageError, codecs::{gif::GifDecoder, jpeg::JpegDecoder, png::PngDecoder, webp::WebPDecoder}}; +use image::{ + AnimationDecoder, ImageDecoder, ImageError, codecs::{ + gif::GifDecoder, jpeg::JpegDecoder, png::PngDecoder, tiff::TiffDecoder, webp::WebPDecoder, + }, +}; use log::debug; -use crate::{backends::{backend::DecodeBackend, image_rs::buffer_image::{BufferImage, BufferImageVariant}}, colour_type::{self, ImageColourType}, decoded_image::{DecodedImage, DecodedImageContent, ImageSize, Pixels}, error::{Error, Result}, format::ImageFormat, image_info::metadata::ImageMetadata, modifications::{ImageModification, ImageModifications}, reader::{ImageReader, ImageReaderData, ReadSeek}}; +use crate::{ + backends::{backend::DecodeBackend, image_rs::buffer_image::{BufferImage, BufferImageVariant}}, colour_type::ImageColourType, decoded_image::{DecodedImage, DecodedImageContent, ImageSize, Pixels}, error::{Error, Result}, format::ImageFormat, image_info::metadata::ImageMetadata, modifications::{ImageModification, ImageModifications}, reader::{ImageReader, ImageReaderData, ReadSeek} +}; mod colour; mod buffer_image; @@ -16,11 +22,12 @@ enum Decoder { Jpeg(JpegDecoder>>), Webp(WebPDecoder>>), Gif(GifDecoder>>), + Tiff(TiffDecoder>>), } enum Buffer { Image(BufferImage), - Animation((Vec<(BufferImage, f32)>, ImageSize, ImageColourType)) + Animation((Vec<(BufferImage, f32)>, ImageSize, ImageColourType)), } enum Source { @@ -41,8 +48,8 @@ impl DecodeBackend for ImageRSBackend { ImageReaderData::BufReader(buf_reader) => { log::debug!("Initializing image-rs backend decoders with buf reader..."); - let error_func = |error: ImageError| { - Error::DecoderInitFailure { error: error.to_string() } + let error_func = |error: ImageError| Error::DecoderInitFailure { + error: error.to_string(), }; let mut image_decoder = match image_reader.image_format { @@ -50,12 +57,13 @@ impl DecodeBackend for ImageRSBackend { ImageFormat::Png => Decoder::Png(PngDecoder::new(buf_reader).map_err(error_func)?), ImageFormat::Jpeg => Decoder::Jpeg(JpegDecoder::new(buf_reader).map_err(error_func)?), ImageFormat::Webp => Decoder::Webp(WebPDecoder::new(buf_reader).map_err(error_func)?), - unsupported_format => return Err( - Error::DecoderNotSupported { + ImageFormat::Tiff => Decoder::Tiff(TiffDecoder::new(buf_reader).map_err(error_func)?), + unsupported_format => { + return Err(Error::DecoderNotSupported { image_format: unsupported_format.to_string(), - backend: String::from("image-rs") - } - ) + backend: String::from("image-rs"), + }); + } }; let exif_chunk = match &mut image_decoder { @@ -63,6 +71,7 @@ impl DecodeBackend for ImageRSBackend { Decoder::Jpeg(jpeg_decoder) => jpeg_decoder.exif_metadata(), Decoder::Webp(web_pdecoder) => web_pdecoder.exif_metadata(), Decoder::Gif(gif_decoder) => gif_decoder.exif_metadata(), + Decoder::Tiff(tiff_decoder) => tiff_decoder.exif_metadata(), }.map_err(|error| Error::DecoderRetrieveExifFailure { error: error.to_string() })?; Ok( @@ -83,7 +92,7 @@ impl DecodeBackend for ImageRSBackend { let image_buffer = BufferImage::from_u8_pixels( pixels, decoded_image.size, - decoded_image.colour_type + decoded_image.colour_type, )?; Ok( @@ -103,7 +112,7 @@ impl DecodeBackend for ImageRSBackend { let image_buffer = BufferImage::from_u8_pixels( pixels, decoded_image.size, - decoded_image.colour_type + decoded_image.colour_type, )?; animated_buffers.push((image_buffer, delay)); @@ -112,91 +121,101 @@ impl DecodeBackend for ImageRSBackend { Ok( Self { source: Source::Buffer( - Buffer::Animation( - (animated_buffers, decoded_image.size, decoded_image.colour_type) - ) + Buffer::Animation(( + animated_buffers, + decoded_image.size, + decoded_image.colour_type + )) ), modifications: HashSet::new(), image_exif_chunk: None, image_format: image_reader.image_format } ) - }, + } } - }, + } } } fn modify(&mut self, modifications: I) where - I: IntoIterator + I: IntoIterator, { self.modifications.extend(modifications); } fn decode(self) -> Result { match self.source { - Source::Decoder(decoder) => { - match decoder { - Decoder::Png(png_decoder) => { - let has_animation = png_decoder.is_apng().map_err( - |error| Error::DecoderAnimationCheckFailure { - error: error.to_string() - } - )?; + Source::Decoder(decoder) => match decoder { + Decoder::Png(png_decoder) => { + let has_animation = png_decoder.is_apng().map_err(|error| { + Error::DecoderAnimationCheckFailure { + error: error.to_string(), + } + })?; - match has_animation { - true => { - let apng_decoder = png_decoder.apng() - .expect("We should have been given the image-rs APNG Decoder but we weren't!"); - - Self::decode_animated_image( - apng_decoder, - self.modifications, - self.image_format, - self.image_exif_chunk - ) - }, - false => Self::decode_image( - png_decoder, + match has_animation { + true => { + let apng_decoder = png_decoder.apng() + .expect("We should have been given the image-rs APNG Decoder but we weren't!"); + + Self::decode_animated_image( + apng_decoder, self.modifications, self.image_format, self.image_exif_chunk ) - } - }, - Decoder::Webp(webp_decoder) => { - match webp_decoder.has_animation() { - true => Self::decode_animated_image( - webp_decoder, - self.modifications, - self.image_format, - self.image_exif_chunk - ), - false => Self::decode_image( - webp_decoder, - self.modifications, - self.image_format, - self.image_exif_chunk - ), - } - }, - Decoder::Gif(gif_decoder) => Self::decode_animated_image( - gif_decoder, - self.modifications, - self.image_format, - self.image_exif_chunk - ), - Decoder::Jpeg(jpeg_decoder) => Self::decode_image( - jpeg_decoder, + }, + false => Self::decode_image( + png_decoder, + self.modifications, + self.image_format, + self.image_exif_chunk + ) + } + }, + Decoder::Webp(webp_decoder) => { + match webp_decoder.has_animation() { + true => Self::decode_animated_image( + webp_decoder, + self.modifications, + self.image_format, + self.image_exif_chunk + ), + false => Self::decode_image( + webp_decoder, + self.modifications, + self.image_format, + self.image_exif_chunk + ), + } + }, + Decoder::Gif(gif_decoder) => Self::decode_animated_image( + gif_decoder, + self.modifications, + self.image_format, + self.image_exif_chunk + ), + Decoder::Jpeg(jpeg_decoder) => Self::decode_image( + jpeg_decoder, + self.modifications, + self.image_format, + self.image_exif_chunk + ), + Decoder::Tiff(tiff_decoder) => { + Self::decode_image( + tiff_decoder, self.modifications, self.image_format, self.image_exif_chunk - ), + ) } }, Source::Buffer(buffer) => { - log::debug!("Image already decoded and constructed as image-rs image buffer, applying modifications..."); + log::debug!( + "Image already decoded and constructed as image-rs image buffer, applying modifications..." + ); match buffer { Buffer::Image(mut buffer_image) => { @@ -220,7 +239,10 @@ impl DecodeBackend for ImageRSBackend { for (index, (mut buffer_image, delay)) in buffer_image_frames.into_iter().enumerate() { debug!("Applying modifications to frame {}...", index); - Self::apply_modifications_to_buffer_image(self.modifications.clone(), &mut buffer_image); + Self::apply_modifications_to_buffer_image( + self.modifications.clone(), + &mut buffer_image, + ); let (pixels, _, _) = buffer_image.to_u8_pixels(); @@ -264,8 +286,11 @@ impl ImageRSBackend { let frame = frame_result.map_err( // NOTE: I might change this to a less generic error. |error| Error::DecodingFailure { - error: format!("Image-rs decoder failed to decode animated frame: {}", error.to_string()) - } + error: format!( + "Image-rs decoder failed to decode animated frame: {}", + error.to_string() + ), + }, )?; let (numerator, denominator) = frame.delay().numer_denom_ms(); @@ -328,8 +353,11 @@ impl ImageRSBackend { return Err( // NOTE: I might change this to a less generic error. Error::DecodingFailure { - error: format!("Image-rs decoder failed to decode image to pixels: {}", error.to_string()), - } + error: format!( + "Image-rs decoder failed to decode image to pixels: {}", + error.to_string() + ), + }, ); } @@ -395,4 +423,4 @@ impl ImageRSBackend { None => ImageMetadata::default(), } } -} \ No newline at end of file +} diff --git a/core/src/format.rs b/core/src/format.rs index 19c0ce6..4dc2772 100644 --- a/core/src/format.rs +++ b/core/src/format.rs @@ -1,6 +1,9 @@ use std::{fmt::Display, fs::File, io::Read, path::PathBuf}; -use crate::{decoded_image::ImageSize, error::{Error, Result}}; +use crate::{ + decoded_image::ImageSize, + error::{Error, Result}, +}; #[derive(Clone, Debug, PartialEq)] pub enum ImageFormat { @@ -8,7 +11,8 @@ pub enum ImageFormat { Jpeg, Svg, Gif, - Webp + Webp, + Tiff, } impl Display for ImageFormat { @@ -19,6 +23,7 @@ impl Display for ImageFormat { ImageFormat::Svg => write!(f, "SVG (Scalable Vector Graphics)"), ImageFormat::Gif => write!(f, "GIF (Graphics Interchange Format)"), ImageFormat::Webp => write!(f, "WEBP (Web Picture)"), + ImageFormat::Tiff => write!(f, "TIFF (Tagged Image File Format)"), } } } @@ -27,28 +32,26 @@ impl Display for ImageFormat { /// /// *It's blazzing fast... 🔥* pub fn determine_image_format_and_size_from_header(path: &PathBuf) -> Result<(ImageFormat, ImageSize)> { - // TODO: figure out how we can share the same buf reader used + // TODO: figure out how we can share the same buf reader used // for image decoding to improve speed and save on I/O calls. let mut buffer = [0u8; 1024]; let number_of_bytes_read = File::open(path) - .map_err( - |error| Error::ImageHeaderReadFailure { - stage: "Failed to open file!".into(), - error: Some(error.to_string()) - } - )? + .map_err(|error| Error::ImageHeaderReadFailure { + stage: "Failed to open file!".into(), + error: Some(error.to_string()), + })? .read(&mut buffer) - .map_err( - |error| Error::ImageHeaderReadFailure { - stage: "Failed to read header of image file!".into(), - error: Some(error.to_string()) - } - )?; - - let image_size_image_type = imagesize::image_type(&buffer[..number_of_bytes_read]) .map_err(|error| Error::ImageHeaderReadFailure { - stage: "Failed to determine format of image!".into(), - error: Some(error.to_string()) + stage: "Failed to read header of image file!".into(), + error: Some(error.to_string()), + })?; + + let image_size_image_type = + imagesize::image_type(&buffer[..number_of_bytes_read]).map_err(|error| { + Error::ImageHeaderReadFailure { + stage: "Failed to determine format of image!".into(), + error: Some(error.to_string()), + } })?; let image_format = match image_size_image_type { @@ -56,30 +59,33 @@ pub fn determine_image_format_and_size_from_header(path: &PathBuf) -> Result<(Im imagesize::ImageType::Jpeg => ImageFormat::Jpeg, imagesize::ImageType::Png => ImageFormat::Png, imagesize::ImageType::Webp => ImageFormat::Webp, - unsupported_format => return Err( - Error::ImageFormatNotSupported { + imagesize::ImageType::Tiff => ImageFormat::Tiff, + unsupported_format => { + return Err(Error::ImageFormatNotSupported { image_format: format!("{:?}", unsupported_format), - } - ), + }); + } }; // TODO: when we switch to shared buf reader we should stop using path let image_size = imagesize::size(path) .map_err(|error| Error::ImageHeaderReadFailure { stage: "Failed to retrieve image dimensions!".into(), - error: Some(error.to_string()) + error: Some(error.to_string()), })?; - Ok((image_format, (image_size.width as u32, image_size.height as u32))) + Ok(( + image_format, + (image_size.width as u32, image_size.height as u32), + )) } pub fn determine_svg_size(path: &PathBuf) -> ImageSize { - let metadata = svg_metadata::Metadata::parse_file(&path).expect( - "Failed to parse metadata of the svg file!" - ); + let metadata = svg_metadata::Metadata::parse_file(&path) + .expect("Failed to parse metadata of the svg file!"); let width = metadata.width().expect("Failed to get SVG width!"); let height = metadata.height().expect("Failed to get SVG height!"); (width as u32, height as u32) -} \ No newline at end of file +} diff --git a/core/tests/backends/mod.rs b/core/tests/backends/mod.rs index f644cc2..215a60b 100644 --- a/core/tests/backends/mod.rs +++ b/core/tests/backends/mod.rs @@ -1,7 +1,10 @@ use std::{fs, path::Path}; -use image::ImageBuffer; -use roseate_core::{format::ImageFormat, decoded_image::{DecodedImage, DecodedImageContent}}; +use image::{ImageBuffer, Rgba}; +use roseate_core::{ + decoded_image::{DecodedImage, DecodedImageContent}, + format::ImageFormat, +}; mod test_image_rs_backend; @@ -19,34 +22,33 @@ pub fn save_image + image::PixelWithColorType>(de ).unwrap(); image.save(Path::new(IMAGE_DUMP_PATH).join(name)).unwrap(); - }, + } DecodedImageContent::Animated(frames) => { for (index, (frame_pixels, _)) in frames.into_iter().enumerate() { let frame_image: ImageBuffer = ImageBuffer::from_raw( width, height, frame_pixels ).unwrap(); - let (file_name, prefix) = name.split_once(".") - .unwrap_or(( - name, - match decoded_image.info.format { - ImageFormat::Png => "png", - ImageFormat::Jpeg => "jpeg", - ImageFormat::Svg => "svg", - ImageFormat::Gif => "gif", - ImageFormat::Webp => "wedp", - } - )); - - let image_path = Path::new(IMAGE_DUMP_PATH) - .join(file_name); + let (file_name, prefix) = name.split_once(".").unwrap_or(( + name, + match decoded_image.info.format { + ImageFormat::Png => "png", + ImageFormat::Jpeg => "jpeg", + ImageFormat::Svg => "svg", + ImageFormat::Gif => "gif", + ImageFormat::Webp => "webp", + ImageFormat::Tiff => "tif", + }, + )); + + let image_path = Path::new(IMAGE_DUMP_PATH).join(file_name); let _ = fs::create_dir(&image_path); - frame_image.save( - image_path.join(format!("frame_{}.{}", index, prefix)) - ).unwrap(); + frame_image + .save(image_path.join(format!("frame_{}.{}", index, prefix))) + .unwrap(); } } } -} \ No newline at end of file +} diff --git a/core/tests/backends/test_image_rs_backend.rs b/core/tests/backends/test_image_rs_backend.rs index 708b595..db3e3d9 100644 --- a/core/tests/backends/test_image_rs_backend.rs +++ b/core/tests/backends/test_image_rs_backend.rs @@ -1,4 +1,4 @@ -use std::{io::Cursor}; +use std::io::Cursor; use image::{Rgb, Rgba}; use roseate_core::{self, backends::{backend::DecodeBackend, image_rs::ImageRSBackend}, error::Result, format::ImageFormat, colour_type::ImageColourType, modifications::ImageModification, reader::ImageReader}; @@ -200,4 +200,24 @@ fn test_modifying_vertical_png() -> Result<()> { save_image::>(decoded_image, "smaller_example.png"); Ok(()) -} \ No newline at end of file +} + +#[test] +fn test_tiff_decode_and_modify() -> Result<()> { + let image_bytes = include_bytes!("../terror_in_resonace_backdrop.tiff"); + + let cursor = Cursor::new(&image_bytes[..]); + let image_reader = ImageReader::new(cursor, ImageFormat::Tiff); + + let mut backend = ImageRSBackend::from_reader(image_reader)?; + backend.modify(vec![ImageModification::Resize(960, 381)]); + + let decoded_image = backend.decode()?; + + assert_eq!(decoded_image.size, (960, 381)); + assert_eq!(decoded_image.colour_type, ImageColourType::Rgba16); + + save_image::>(decoded_image, "resized_terror_in_resonace_backdrop.tiff"); + + Ok(()) +} diff --git a/core/tests/terror_in_resonace_backdrop.tiff b/core/tests/terror_in_resonace_backdrop.tiff new file mode 100644 index 0000000..f32ca19 Binary files /dev/null and b/core/tests/terror_in_resonace_backdrop.tiff differ diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..1a94b66 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,42 @@ +hard_tabs = false +tab_spaces = 4 +newline_style = "Unix" +indent_style = "Block" + +reorder_imports = false +reorder_modules = false +imports_indent = "Block" +imports_granularity = "Crate" +group_imports = "StdExternalCrate" + +blank_lines_upper_bound = 2 + +use_small_heuristics = "Off" +combine_control_expr = false +control_brace_style = "AlwaysSameLine" +brace_style = "SameLineWhere" +wrap_comments = false +condense_wildcard_suffixes = false +empty_item_single_line = true +fn_params_layout = "Tall" +fn_single_line = false +format_code_in_doc_comments = false +format_strings = false +hex_literal_case = "Upper" +match_arm_blocks = false +match_arm_indent = true +match_arm_leading_pipes = "Preserve" +match_block_trailing_comma = true +format_macro_matchers = false +normalize_doc_attributes = true +overflow_delimited_expr = false +reorder_impl_items = true +space_after_colon = true +space_before_colon = false +spaces_around_ranges = false +struct_lit_single_line = false + +# for now I want the formatter disabled, I'll re-enable it in the +# future with some of the configs I set above as I learn more about +# it and can trust it. +disable_all_formatting = true \ No newline at end of file