diff --git a/Cargo.toml b/Cargo.toml index 6e9f1d9075..aa72fba555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,10 @@ path = "./src/lib.rs" [dependencies] bytemuck = { version = "1.7.0", features = ["extern_crate_alloc"] } # includes cast_vec byteorder = "1.3.2" +image-canvas = "0.4.1" num-rational = { version = "0.4", default-features = false } num-traits = "0.2.0" + gif = { version = "0.11.1", optional = true } jpeg = { package = "jpeg-decoder", version = "0.2.1", default-features = false, optional = true } png = { version = "0.17.0", optional = true } diff --git a/Cargo.toml.public-private-dependencies b/Cargo.toml.public-private-dependencies index bdce0c13d1..918f8cd9a7 100644 --- a/Cargo.toml.public-private-dependencies +++ b/Cargo.toml.public-private-dependencies @@ -31,9 +31,10 @@ path = "./src/lib.rs" [dependencies] bytemuck = { version = "1.7.0", features = ["extern_crate_alloc"] } # includes cast_vec byteorder = "1.3.2" -num-iter = "0.1.32" +image-canvas = "0.4.1" num-rational = { version = "0.4", default-features = false } num-traits = { version = "0.2.0", public = true } + gif = { version = "0.11.1", optional = true } jpeg = { package = "jpeg-decoder", version = "0.2.1", default-features = false, optional = true } png = { version = "0.17.0", optional = true } diff --git a/src/buffer.rs b/src/buffer.rs index 7f79119e4f..2db0f94d77 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,4 +1,19 @@ -//! Contains the generic `ImageBuffer` struct. +//! Defines standard containers for image data. +//! +//! The best container depends on the use: +//! * The generic [`ImageBuffer`] struct, a simplified pixel container with a standard matrix +//! layout of uniform channels. The strength is convenient access for modifications with a direct +//! mapping and access to the underlying data. +//! * The [`DynamicImage`] enumeration builds on `ImageBuffer` to provide a standard selection of +//! basic channel representations, and conventionally in sRGB color space. This is usually enough +//! to represent any source data without loosing much precision. This makes is suitable for +//! basic, but generic, image operations. +//! * The [`Canvas`] struct as general texel container of general layouts as a buffer. The strength +//! is the generality (and extensibility) that allows its use for interchange of image data and +//! reduced processing cost if the convenience above is not required. +#[path = "buffer/canvas.rs"] +mod canvas; + use num_traits::Zero; use std::fmt; use std::marker::PhantomData; @@ -15,6 +30,8 @@ use crate::math::Rect; use crate::traits::{EncodableLayout, Pixel, PixelWithColorType}; use crate::utils::expand_packed; +pub use self::canvas::{Canvas, CanvasLayout, UnknownCanvasTexelError}; + /// Iterate over pixel refs. pub struct Pixels<'a, P: Pixel + 'a> where diff --git a/src/buffer/canvas.rs b/src/buffer/canvas.rs new file mode 100644 index 0000000000..3b7b81d9a3 --- /dev/null +++ b/src/buffer/canvas.rs @@ -0,0 +1,648 @@ +use core::ops::Deref; +use std::io::{Seek, Write}; + +use crate::{ + error::{DecodingError, ImageFormatHint}, + flat::SampleLayout, + traits::{EncodableLayout, PixelWithColorType}, + ColorType, DynamicImage, ExtendedColorType, FlatSamples, ImageBuffer, ImageDecoder, ImageError, + ImageOutputFormat, ImageResult, Luma, LumaA, Rgb, Rgba, +}; + +use image_canvas::layout::{CanvasLayout as InnerLayout, SampleBits, SampleParts, Texel}; +use image_canvas::Canvas as Inner; +use image_canvas::{color::Color, layout::Block}; + +/// A byte buffer for raw image data, with various layout and color possibilities. +/// +/// This container is designed to hold byte data suitably aligned for further processing, without +/// making too many choices on allowed layouts and representations. In contrast to `DynamicImage`, +/// we try to avoid making assumptions about the suitable layout or policy about the color +/// representations. The user is given control over these at the cost of some convenience. +/// +/// This implies that image operations can not be total for all possible `Canvas` data, there are +/// always error cases of unknown or unsupported data. In particular, the design work for data and +/// conversion pipelines for [`Canvas`] is still ongoing. +/// +/// Note that it is possible to convert most [`ImageBuffer`] as well as [`DynamicImage`] instances +/// into a canvas that represents the same pixels and color. See `From` implementations below. +pub struct Canvas { + inner: Inner, +} + +/// The layout of a [`Canvas`]. +/// +/// Apart from the simplistic cases of matrices of uniform channel arrays, this can internally also +/// represent complex cases such as pixels with mixed channel types, matrices of compressed blocks, +/// and planar layouts. The public constructors define the possibilities that are support. +#[derive(Clone, PartialEq)] +pub struct CanvasLayout { + inner: InnerLayout, +} + +/// Signals that the `ExtendedColorType` couldn't be made into a canvas texel. +/// +/// The goal here is to incrementally increase the support for texels and *eventually* turn this +/// struct into a public one, whose only member is an uninhabited (`Never`) type—such that all +/// methods returning it as a result become infallible. +#[derive(Clone, Debug, PartialEq)] +// By design. Please, clippy, recognizing this pattern would be helpful. +#[allow(missing_copy_implementations)] +pub struct UnknownCanvasTexelError { + _inner: (), +} + +impl Canvas { + /// Allocate an image canvas for a given color, and dimensions. + /// + /// # Panics + /// + /// If the layout is invalid for this platform, i.e. requires more than `isize::MAX` bytes to + /// allocate. Also panics if the allocator fails. + pub fn new(color: ColorType, w: u32, h: u32) -> Self { + let texel = color_to_texel(color); + let color = color_to_color(color); + + let mut layout = InnerLayout::with_texel(&texel, w, h).expect("layout error"); + layout.set_color(color).expect("layout error"); + + Canvas { + inner: Inner::new(layout), + } + } + + /// Allocate an image canvas with complex layout. + /// + /// This is a generalization of [`Self::new`] and the error case of an invalid layout is + /// isolated, and can be properly handled. Furthermore, this provides control over the + /// allocation by making its size available to the caller before it is performed. + pub fn with_layout(layout: CanvasLayout) -> Self { + Canvas { + inner: Inner::new(layout.inner), + } + } + + /// Allocate a canvas for the result of an image decoder, then read the image into it. + pub fn from_decoder<'a>(decoder: impl ImageDecoder<'a>) -> ImageResult { + let layout = CanvasLayout::from_decoder(&decoder)?; + let mut canvas = Inner::new(layout); + + decoder.read_image(canvas.as_bytes_mut())?; + + Ok(Canvas { inner: canvas }) + } + + /// Read an image into the canvas. + /// + /// The allocated memory of the current canvas is reused. + /// + /// On success, returns an `Ok`-value. The layout and contents are changed to the new image. On + /// failure, returns an appropriate `Err`-value and the contents of this canvas are not defined + /// (but initialized). The layout may have changed. + pub fn decode<'a>(&mut self, decoder: impl ImageDecoder<'a>) -> ImageResult<()> { + let layout = CanvasLayout::from_decoder(&decoder)?; + self.inner.set_layout(layout); + + decoder.read_image(self.inner.as_bytes_mut())?; + + Ok(()) + } + + /// The width of this canvas, in represented pixels. + pub fn width(&self) -> u32 { + self.inner.layout().width() + } + + /// The height of this canvas, in represented pixels. + pub fn height(&self) -> u32 { + self.inner.layout().height() + } + + /// Get a reference to the raw bytes making up this canvas. + /// + /// The interpretation will very much depend on the current layout. Note that primitive + /// channels will be generally stored in native-endian order. + /// + /// This can be used to pass the bytes through FFI but prefer [`Self::as_flat_samples_u8`] and + /// related methods for a typed descriptor that will check against the underlying type of + /// channels. + pub fn as_bytes(&self) -> &[u8] { + self.inner.as_bytes() + } + + /// Get a mutable reference to the raw bytes making up this canvas. + /// + /// The interpretation will very much depend on the current layout. Note that primitive + /// channels will be generally stored in native-endian order. + /// + /// This can be used to initialize bytes from FFI. + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + self.inner.as_bytes_mut() + } + + /// Drop all layout information and turn into raw bytes. + /// + /// This will not be any less efficient than calling `as_bytes().to_owned()` but it *may* be + /// possible to reuse the allocation in the future. + /// + /// However, note that it is **unsound** to take the underlying allocation as its layout has an + /// alignment mismatch with the layout that `Vec` is expecting (it is generally allocated + /// to a much higher alignment). Instead, a reallocation is required; the allocator might + /// recycle the allocation which avoids the byte copy. + pub fn into_bytes(self) -> Vec { + // No better way in `image-texel = 0.2.0` but may be in the future, we're changing the + // alignment of the layout. + self.as_bytes().to_owned() + } + + /// Get a reference to channels, without conversion. + /// + /// This returns `None` if the channels of the underlying layout are not unsigned bytes, or if + /// the layout is not a single image plane. + pub fn as_flat_samples_u8(&self) -> Option> { + let channels = self.inner.channels_u8()?; + let spec = channels.layout().spec(); + Some(FlatSamples { + samples: channels.into_slice(), + layout: Self::sample_layout(spec), + color_hint: Self::color_type(self.inner.layout()), + }) + } + + /// Get a reference to channels, without conversion. + /// + /// This returns `None` if the channels of the underlying layout are not unsigned shorts, or if + /// the layout is not a single image plane. + pub fn as_flat_samples_u16(&self) -> Option> { + let channels = self.inner.channels_u16()?; + let spec = channels.layout().spec(); + Some(FlatSamples { + samples: channels.into_slice(), + layout: Self::sample_layout(spec), + color_hint: Self::color_type(self.inner.layout()), + }) + } + + /// Get a reference to channels, without conversion. + /// + /// This returns `None` if the channels of the underlying layout are not 32-bit floating point + /// numbers, or if the layout is not a single image plane. + pub fn as_flat_samples_f32(&self) -> Option> { + let channels = self.inner.channels_f32()?; + let spec = channels.layout().spec(); + Some(FlatSamples { + samples: channels.into_slice(), + layout: Self::sample_layout(spec), + color_hint: Self::color_type(self.inner.layout()), + }) + } + + /// Convert the data into an `ImageBuffer`. + /// + /// Performs color conversion if necessary, such as rearranging color channel order, + /// transforming pixels to the `sRGB` color space etc. + /// + /// The subpixel of the result type is constrained by a sealed, internal trait that is + /// implemented for all primitive numeric types. + /// + /// This is essentially an optimized method compared to using an intermediate `DynamicImage`. + pub fn to_buffer(&self) -> ImageResult>> + where + [P::Subpixel]: EncodableLayout, + { + let (width, height) = (self.width(), self.height()); + let mut buffer = ImageBuffer::new(width, height); + + let mut fallback_canvas; + // FIXME(perf): can we wrap the output buffer into a proper layout and have the library + // convert into the output buffer instead? + let canvas = if false { + /* self.inner.layout().texel() == texel */ + // FIXME: can select `self` if it's exactly the correct color type and layout. For now + // we just note the possibility down in the type system. + &self.inner + } else { + let texel = color_to_texel(P::COLOR_TYPE); + let layout = InnerLayout::with_texel(&texel, width, height) + .expect("Valid layout because buffer has one"); + let color = color_to_color(P::COLOR_TYPE); + + fallback_canvas = Inner::new(layout); + fallback_canvas + .set_color(color) + .expect("Valid for rgb layout"); + self.inner.convert(&mut fallback_canvas); + &fallback_canvas + }; + + let destination = buffer.inner_pixels_mut().as_bytes_mut(); + let initializer = canvas.as_bytes(); + let len = destination.len().min(initializer.len()); + + debug_assert!( + destination.len() == initializer.len(), + "The layout computation should not differ" + ); + + destination[..len].copy_from_slice(&initializer[..len]); + + Ok(buffer) + } + + /// Convert this canvas into an enumeration of simple buffer types. + /// + /// If the underlying texels are any of the simple [`ColorType`] formats then this format is + /// chosen as the output variant. Otherwise, prefers `Rgba` if an alpha channel is present + /// and `Rgb` otherwise. The conversion _may_ be lossy. + /// + /// This entails a re-allocation! However, the actual runtime costs vary depending on the + /// actual current image layout. For example, if a color conversion to `sRGB` is necessary then + /// this is more expensive than a channel reordering. + pub fn to_dynamic(&self) -> ImageResult { + let layout = self.inner.layout(); + Ok(match Self::color_type(layout) { + Some(ColorType::L8) => self.to_buffer::>()?.into(), + Some(ColorType::La8) => self.to_buffer::>()?.into(), + Some(ColorType::Rgb8) => self.to_buffer::>()?.into(), + Some(ColorType::Rgba8) => self.to_buffer::>()?.into(), + Some(ColorType::L16) => self.to_buffer::>()?.into(), + Some(ColorType::La16) => self.to_buffer::>()?.into(), + Some(ColorType::Rgb16) => self.to_buffer::>()?.into(), + Some(ColorType::Rgba16) => self.to_buffer::>()?.into(), + Some(ColorType::Rgb32F) => self.to_buffer::>()?.into(), + Some(ColorType::Rgba32F) => self.to_buffer::>()?.into(), + None if !Self::has_alpha(layout) => self.to_buffer::>()?.into(), + None => self.to_buffer::>()?.into(), + }) + } + + fn sample_layout(spec: image_canvas::layout::ChannelSpec) -> SampleLayout { + SampleLayout { + channels: spec.channels, + channel_stride: spec.channel_stride, + width: spec.width, + width_stride: spec.width_stride, + height: spec.height, + height_stride: spec.height_stride, + } + } + + fn color_type(layout: &InnerLayout) -> Option { + let texel = layout.texel(); + let color = layout.color()?; + + if !matches!(texel.block, Block::Pixel) { + return None; + }; + + if let &Color::SRGB = color { + // Recognized color type, no color management required. + if let SampleBits::UInt8x3 = texel.bits { + if let SampleParts::Rgb = texel.parts { + return Some(ColorType::Rgb8); + } + } else if let SampleBits::UInt8x4 = texel.bits { + if let SampleParts::RgbA = texel.parts { + return Some(ColorType::Rgba8); + } + } else if let SampleBits::UInt16x3 = texel.bits { + if let SampleParts::Rgb = texel.parts { + return Some(ColorType::Rgb16); + } + } else if let SampleBits::UInt16x4 = texel.bits { + if let SampleParts::RgbA = texel.parts { + return Some(ColorType::Rgba16); + } + } else if let SampleBits::Float32x3 = texel.bits { + if let SampleParts::Rgb = texel.parts { + return Some(ColorType::Rgb32F); + } + } else if let SampleBits::Float32x4 = texel.bits { + if let SampleParts::RgbA = texel.parts { + return Some(ColorType::Rgba32F); + } + } + } else if let &Color::BT709 = color { + if let SampleBits::UInt8 = texel.bits { + if let SampleParts::Luma = texel.parts { + return Some(ColorType::L8); + } + } else if let SampleBits::UInt8x2 = texel.bits { + if let SampleParts::LumaA = texel.parts { + return Some(ColorType::La8); + } + } else if let SampleBits::UInt16 = texel.bits { + if let SampleParts::Luma = texel.parts { + return Some(ColorType::L16); + } + } else if let SampleBits::UInt16x2 = texel.bits { + if let SampleParts::LumaA = texel.parts { + return Some(ColorType::La16); + } + } + } + + None + } + + fn has_alpha(layout: &InnerLayout) -> bool { + layout.texel().parts.has_alpha() + } +} + +impl CanvasLayout { + /// Construct a row-major matrix for a given color type. + pub fn new(color: ColorType, w: u32, h: u32) -> Option { + let texel = color_to_texel(color); + let color = color_to_color(color); + + let mut layout = InnerLayout::with_texel(&texel, w, h).ok()?; + layout.set_color(color).ok()?; + + Some(CanvasLayout { inner: layout }) + } + + fn from_decoder<'a>(decoder: &impl ImageDecoder<'a>) -> ImageResult { + // FIXME: we can also handle ExtendedColorType... + let texel = color_to_texel(decoder.color_type()); + let (width, height) = decoder.dimensions(); + + // FIXME: What about other scanline sizes? + InnerLayout::with_texel(&texel, width, height).map_err(|layout| { + let decoder = DecodingError::new(ImageFormatHint::Unknown, format!("{:?}", layout)); + ImageError::Decoding(decoder) + }) + } + + /// Construct a row-major matrix for an extended color type, i.e. generic texels. + pub fn with_extended( + color: ExtendedColorType, + w: u32, + h: u32, + ) -> Result { + fn inner(color: ExtendedColorType, w: u32, h: u32) -> Option { + let texel = extended_color_to_texel(color)?; + let mut layout = InnerLayout::with_texel(&texel, w, h).ok()?; + if let Some(color) = extended_color_to_color(color) { + layout.set_color(color).ok()?; + } + + Some(CanvasLayout { inner: layout }) + } + + inner(color, w, h).ok_or(UnknownCanvasTexelError { _inner: () }) + } + + /// Returns the number of bytes required for this layout. + pub fn byte_len(&self) -> usize { + self.inner.byte_len() + } +} + +impl Canvas { + /// Encode the image into the writer, using the specified format. + /// + /// Will internally perform a conversion with [`Self::to_dynamic`]. + pub fn write_to>( + &self, + w: &mut W, + format: F, + ) -> ImageResult<()> { + // FIXME(perf): should check if the byte layout is already correct. + self.to_dynamic()?.write_to(w, format) + } +} + +/// Allocate a canvas with the layout of the buffer. +/// +/// This can't result in an invalid layout, as the `ImageBuffer` certifies that the layout fits +/// into the address space. However, this conversion may panic while performing an allocation. +/// +/// ## Design note. +/// +/// Converting an owned image buffer is not currently implemented, until agreeing upon a design. +/// There's two goals that are subtly at odds: We will want to utilize a zero-copy reallocation as +/// best as possible but this is not possible while being generic over the container in the same +/// manner as here. +/// +/// However, for the near future we will only ever be able to reuse an allocation originating from +/// a standard `Vec<_>` (of the *global* allocator). We can only specialize on this by giving the +/// type explicitly on the impl, or by restricting the container such that `C: Any` is available. +/// Both cases may justify explicit methods as a superior alternative. +/// +/// The first of these options would be consistent with supporting `DynamicImage`. +impl From<&'_ ImageBuffer> for Canvas +where + P: PixelWithColorType, + [P::Subpixel]: EncodableLayout, + C: Deref, +{ + fn from(buf: &ImageBuffer) -> Self { + // Note: if adding any non-sRGB compatible type, be careful. + let texel = color_to_texel(P::COLOR_TYPE); + + let (width, height) = ImageBuffer::dimensions(&buf); + let layout = InnerLayout::with_texel(&texel, width, height) + .expect("Valid layout because buffer has one"); + + let mut canvas = Inner::new(layout); + + let destination = canvas.as_bytes_mut(); + let initializer = buf.inner_pixels().as_bytes(); + let len = destination.len().min(initializer.len()); + + debug_assert!( + destination.len() == initializer.len(), + "The layout computation should not differ" + ); + + destination[..len].copy_from_slice(&initializer[..len]); + + let color = color_to_color(P::COLOR_TYPE); + canvas.set_color(color).expect("Valid for rgb layout"); + Canvas { inner: canvas } + } +} + +impl From<&'_ DynamicImage> for Canvas { + fn from(image: &'_ DynamicImage) -> Self { + image.to_canvas() + } +} + +/// Convert a dynamic image to a canvas. +/// +/// This *may* reuse the underlying buffer if it is suitably aligned already. +impl From for Canvas { + fn from(image: DynamicImage) -> Self { + image.to_canvas() + } +} + +fn color_to_texel(color: ColorType) -> Texel { + match color { + ColorType::L8 => Texel::new_u8(SampleParts::Luma), + ColorType::La8 => Texel::new_u8(SampleParts::LumaA), + ColorType::Rgb8 => Texel::new_u8(SampleParts::Rgb), + ColorType::Rgba8 => Texel::new_u8(SampleParts::RgbA), + ColorType::L16 => Texel::new_u16(SampleParts::Luma), + ColorType::La16 => Texel::new_u16(SampleParts::LumaA), + ColorType::Rgb16 => Texel::new_u16(SampleParts::Rgb), + ColorType::Rgba16 => Texel::new_u16(SampleParts::RgbA), + ColorType::Rgb32F => Texel::new_f32(SampleParts::Rgb), + ColorType::Rgba32F => Texel::new_f32(SampleParts::RgbA), + } +} + +fn color_to_color(color: ColorType) -> Color { + use ColorType::*; + match color { + L8 | La8 | L16 | La16 => Color::BT709, + Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => Color::SRGB, + } +} + +fn extended_color_to_color(color: ExtendedColorType) -> Option { + use ExtendedColorType::*; + match color { + L1 | La1 | L2 | La2 | L4 | La4 | L8 | La8 | L16 | La16 => Some(Color::BT709), + A8 | Rgb1 | Rgb2 | Rgb4 | Rgb8 | Rgb16 | Rgba1 | Rgba2 | Rgba4 | Rgba8 | Rgba16 | Bgr8 + | Bgra8 | Rgb32F | Rgba32F => Some(Color::BT709), + Unknown(_) => return None, + } +} + +fn extended_color_to_texel(color: ExtendedColorType) -> Option { + use ExtendedColorType as ColorType; + + #[allow(unreachable_patterns)] + Some(match color { + ColorType::L8 => Texel::new_u8(SampleParts::Luma), + ColorType::La8 => Texel::new_u8(SampleParts::LumaA), + ColorType::Rgb8 => Texel::new_u8(SampleParts::Rgb), + ColorType::Rgba8 => Texel::new_u8(SampleParts::RgbA), + ColorType::L16 => Texel::new_u16(SampleParts::Luma), + ColorType::La16 => Texel::new_u16(SampleParts::LumaA), + ColorType::Rgb16 => Texel::new_u16(SampleParts::Rgb), + ColorType::Rgba16 => Texel::new_u16(SampleParts::RgbA), + ColorType::Rgb32F => Texel::new_f32(SampleParts::Rgb), + ColorType::Rgba32F => Texel::new_f32(SampleParts::RgbA), + + ColorType::Bgr8 => Texel::new_u8(SampleParts::Bgr), + ColorType::Bgra8 => Texel::new_u8(SampleParts::BgrA), + + ColorType::A8 => Texel::new_u8(SampleParts::A), + + // Layouts with non-byte colors but still pixel + ColorType::Rgba2 => Texel { + block: Block::Pixel, + bits: SampleBits::UInt2x4, + parts: SampleParts::RgbA, + }, + ColorType::Rgba4 => Texel { + block: Block::Pixel, + bits: SampleBits::UInt4x4, + parts: SampleParts::RgbA, + }, + + // Repeated pixel layouts + ColorType::Rgb4 => Texel { + block: Block::Pack1x2, + bits: SampleBits::UInt4x6, + parts: SampleParts::RgbA, + }, + ColorType::L1 => Texel { + block: Block::Pack1x8, + bits: SampleBits::UInt1x8, + parts: SampleParts::Luma, + }, + ColorType::La1 => Texel { + block: Block::Pack1x4, + bits: SampleBits::UInt2x4, + parts: SampleParts::LumaA, + }, + ColorType::Rgba1 => Texel { + block: Block::Pack1x2, + bits: SampleBits::UInt1x8, + parts: SampleParts::RgbA, + }, + ColorType::L2 => Texel { + block: Block::Pack1x4, + bits: SampleBits::UInt2x4, + parts: SampleParts::Luma, + }, + ColorType::La2 => Texel { + block: Block::Pack1x2, + bits: SampleBits::UInt2x4, + parts: SampleParts::LumaA, + }, + ColorType::L4 => Texel { + block: Block::Pack1x2, + bits: SampleBits::UInt4x2, + parts: SampleParts::Luma, + }, + ColorType::La4 => Texel { + block: Block::Pixel, + bits: SampleBits::UInt4x2, + parts: SampleParts::LumaA, + }, + + // Placeholder for non-RGBA formats (CMYK, YUV, Lab). + // … + + // Placeholder for subsampled YUV formats + // … + + // Placeholder for planar formats? + // … + + // Placeholder for Block layouts (ASTC, BC, ETC/EAC, PVRTC) + // … + + // Uncovered variants.. + // + // Rgb1/2 require 1x4 blocks with 12 channels, not supported. + ColorType::Rgb1 + | ColorType::Rgb2 + // Obvious.. + | ColorType::Unknown(_) => return None, + }) +} + +#[test] +fn test_conversions() { + use crate::{buffer::ConvertBuffer, Rgb, RgbImage, Rgba}; + + let buffer = RgbImage::from_fn(32, 32, |x, y| { + if (x + y) % 2 == 0 { + Rgb([0, 0, 0]) + } else { + Rgb([255, 255, 255]) + } + }); + + let canvas = Canvas::from(&buffer); + + assert_eq!(canvas.to_buffer::>().unwrap(), buffer.clone()); + assert_eq!(canvas.to_buffer::>().unwrap(), buffer.convert()); + assert_eq!(canvas.to_buffer::>().unwrap(), buffer.convert()); + + assert!(canvas.to_dynamic().unwrap().as_rgb8().is_some()); +} + +#[test] +#[rustfmt::skip] +fn test_expansion_bits() { + let layout = CanvasLayout::with_extended(ExtendedColorType::L1, 6, 2) + .expect("valid layout type"); + let mut buffer = Canvas::with_layout(layout); + + assert_eq!(buffer.as_bytes(), b"\x00\x00"); + buffer.as_bytes_mut().copy_from_slice(&[0b01101100 as u8, 0b10110111]); + + let image = buffer.to_dynamic().unwrap().into_luma8(); + assert_eq!(image.as_bytes(), vec![ + 0x00, 0xff, 0xff, 0x00, 0xff, 0xff, + 0xff, 0x00, 0xff, 0xff, 0x00, 0xff, + ]); +} diff --git a/src/dynimage.rs b/src/dynimage.rs index 3e0aeec6b9..8ae784fb01 100644 --- a/src/dynimage.rs +++ b/src/dynimage.rs @@ -11,7 +11,7 @@ use crate::codecs::png; use crate::codecs::pnm; use crate::buffer_::{ - ConvertBuffer, Gray16Image, GrayAlpha16Image, GrayAlphaImage, GrayImage, ImageBuffer, + Canvas, ConvertBuffer, Gray16Image, GrayAlpha16Image, GrayAlphaImage, GrayImage, ImageBuffer, Rgb16Image, RgbImage, Rgba16Image, RgbaImage, }; use crate::color::{self, IntoColor}; @@ -237,6 +237,11 @@ impl DynamicImage { dynamic_map!(*self, |ref p| p.convert()) } + /// Returns a copy of this image as an sRGB canvas. + pub fn to_canvas(&self) -> Canvas { + dynamic_map!(*self, |ref p| p.into()) + } + /// Consume the image and returns a RGB image. /// /// If the image was already the correct format, it is returned as is. diff --git a/src/lib.rs b/src/lib.rs index 1f764fa4ad..198746d961 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,6 +159,8 @@ pub mod buffer { ConvertBuffer, EnumeratePixels, EnumeratePixelsMut, EnumerateRows, EnumerateRowsMut, Pixels, PixelsMut, Rows, RowsMut, }; + + pub use crate::buffer_::{Canvas, CanvasLayout, UnknownCanvasTexelError}; } // Math utils diff --git a/src/traits.rs b/src/traits.rs index 56daaa0ddf..27691fbb4c 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -12,24 +12,35 @@ use crate::color::{ColorType, Luma, LumaA, Rgb, Rgba}; pub trait EncodableLayout: seals::EncodableLayout { /// Get the bytes of this value. fn as_bytes(&self) -> &[u8]; + /// Get a mutable reference to all bytes of this value. + fn as_bytes_mut(&mut self) -> &mut [u8]; } impl EncodableLayout for [u8] { fn as_bytes(&self) -> &[u8] { bytemuck::cast_slice(self) } + fn as_bytes_mut(&mut self) -> &mut [u8] { + bytemuck::cast_slice_mut(self) + } } impl EncodableLayout for [u16] { fn as_bytes(&self) -> &[u8] { bytemuck::cast_slice(self) } + fn as_bytes_mut(&mut self) -> &mut [u8] { + bytemuck::cast_slice_mut(self) + } } impl EncodableLayout for [f32] { fn as_bytes(&self) -> &[u8] { bytemuck::cast_slice(self) } + fn as_bytes_mut(&mut self) -> &mut [u8] { + bytemuck::cast_slice_mut(self) + } } /// The type of each channel in a pixel. For example, this can be `u8`, `u16`, `f32`. diff --git a/tests/canvas.rs b/tests/canvas.rs new file mode 100644 index 0000000000..12312bc222 --- /dev/null +++ b/tests/canvas.rs @@ -0,0 +1,102 @@ +use image::{buffer::Canvas, ColorType, DynamicImage}; +#[cfg(feature = "png")] +use std::{fs, io}; + +#[test] +#[cfg(feature = "png")] +fn read_canvas() { + use image::codecs::png::PngDecoder; + + let img_path = format!( + "{}/tests/images/png/interlaced/basi2c08.png", + env!("CARGO_MANIFEST_DIR") + ); + let stream = io::BufReader::new(fs::File::open(&img_path).unwrap()); + let decoder = PngDecoder::new(stream).expect("valid png"); + + let mut canvas = Canvas::from_decoder(decoder).expect("valid png"); + assert_eq!(canvas.width(), 32); + assert_eq!(canvas.height(), 32); + + // No extra allocation is happening here for the canvas. + let stream = io::BufReader::new(fs::File::open(&img_path).unwrap()); + let decoder = PngDecoder::new(stream).expect("valid png"); + canvas.decode(decoder).expect("again, valid png"); + + assert_eq!(canvas.width(), 32); + assert_eq!(canvas.height(), 32); +} + +#[test] +fn conversion_to_buffer() { + let canvas = Canvas::new(ColorType::Rgb8, 32, 32); + + assert!(canvas.as_flat_samples_u8().is_some()); + assert!(canvas.as_flat_samples_u16().is_none()); + assert!(canvas.as_flat_samples_f32().is_none()); + + assert!(matches!( + canvas.to_dynamic(), + Ok(DynamicImage::ImageRgb8(_)) + )); + + let buffer_rgb8 = canvas.to_buffer::>().unwrap(); + let buffer_rgb16 = canvas.to_buffer::>().unwrap(); + + let compare_rgb16 = DynamicImage::ImageRgb8(buffer_rgb8).into_rgb16(); + assert_eq!(buffer_rgb16, compare_rgb16); + + let canvas_rgb16 = Canvas::from(&buffer_rgb16); + assert!(canvas_rgb16.as_flat_samples_u16().is_some()); +} + +#[test] +fn test_dynamic_image() { + for ct in [ + ColorType::L8, + ColorType::La8, + ColorType::Rgb8, + ColorType::Rgba8, + ColorType::L16, + ColorType::La16, + ColorType::Rgb16, + ColorType::Rgba16, + ColorType::Rgb32F, + ColorType::Rgba32F, + ] { + let dynamic = dynamic_image_with_color(ct, 32, 32); + + let canvas = Canvas::new(ct, 32, 32); + assert_eq!( + core::mem::discriminant(&canvas.to_dynamic().unwrap()), + core::mem::discriminant(&dynamic), + "{:?}", + ct + ); + + let canvas = Canvas::from(&dynamic); + assert_eq!( + core::mem::discriminant(&canvas.to_dynamic().unwrap()), + core::mem::discriminant(&dynamic), + "{:?}", + ct + ); + } +} + +/// Creates a dynamic image based off a color type for the buffer. +pub(crate) fn dynamic_image_with_color(color: ColorType, w: u32, h: u32) -> DynamicImage { + (match color { + ColorType::L8 => DynamicImage::new_luma8, + ColorType::La8 => DynamicImage::new_luma_a8, + ColorType::Rgb8 => DynamicImage::new_rgb8, + ColorType::Rgba8 => DynamicImage::new_rgba8, + ColorType::L16 => DynamicImage::new_luma16, + ColorType::La16 => DynamicImage::new_luma_a16, + ColorType::Rgb16 => DynamicImage::new_rgb16, + ColorType::Rgba16 => DynamicImage::new_rgba16, + ColorType::Rgb32F => DynamicImage::new_rgb32f, + ColorType::Rgba32F => DynamicImage::new_rgba32f, + _ => unreachable!(), + })(w, h) +}