From 49a03803c1ecabfe6f6090607b9de4a8b4a58e47 Mon Sep 17 00:00:00 2001 From: M Stoeckl Date: Sat, 29 Aug 2026 18:08:27 -0400 Subject: [PATCH 1/3] Distinguish mask and pixmap types until RasterPipelineBlitter Interpreting parts of a Mask as a "SubPixmapMut" was confusing because SubPixmapMut (typically) has 4-byte pixels, while Mask uses 1-byte pixels. The new GenericPixmapMut struct should also make it easier to handle non-u8888 pixel formats in the future. --- src/mask.rs | 24 +++++++------ src/painter.rs | 4 +-- src/pipeline/blitter.rs | 76 ++++++++++++++++++++++++----------------- src/pipeline/highp.rs | 6 ++-- src/pipeline/lowp.rs | 6 ++-- src/pipeline/mod.rs | 49 +++++++++++++++++++------- src/pixmap.rs | 7 ---- 7 files changed, 103 insertions(+), 69 deletions(-) diff --git a/src/mask.rs b/src/mask.rs index 420a813..f24273a 100644 --- a/src/mask.rs +++ b/src/mask.rs @@ -14,7 +14,6 @@ use tiny_skia_path::{IntRect, IntSize, Path, Scalar, Transform}; use crate::geom::IntSizeExt; use crate::painter::DrawTiler; use crate::pipeline::RasterPipelineBlitter; -use crate::pixmap::SubPixmapMut; use crate::scan; use crate::{FillRule, PixmapRef}; @@ -157,21 +156,19 @@ impl Mask { }) } - pub(crate) fn as_subpixmap(&mut self) -> SubPixmapMut<'_> { - SubPixmapMut { - size: self.size, + pub(crate) fn as_submask_mut(&mut self) -> SubMaskMut<'_> { + SubMaskMut { real_width: self.size.width() as usize, data: &mut self.data, } } - pub(crate) fn subpixmap(&mut self, rect: IntRect) -> Option> { + pub(crate) fn submask_mut(&mut self, rect: IntRect) -> Option> { let rect = self.size.to_int_rect(0, 0).intersect(&rect)?; let row_bytes = self.width() as usize; let offset = rect.top() as usize * row_bytes + rect.left() as usize; - Some(SubPixmapMut { - size: rect.size(), + Some(SubMaskMut { real_width: self.size.width() as usize, data: &mut self.data[offset..], }) @@ -294,12 +291,12 @@ impl Mask { }; let clip_rect = tile.size().to_screen_int_rect(0, 0); - let mut subpix = match self.subpixmap(tile.to_int_rect()) { + let mut submask = match self.submask_mut(tile.to_int_rect()) { Some(v) => v, None => continue, // technically unreachable }; - let mut blitter = match RasterPipelineBlitter::new_mask(&mut subpix) { + let mut blitter = match RasterPipelineBlitter::new_mask(&mut submask) { Some(v) => v, None => continue, // nothing to do, all good }; @@ -321,8 +318,8 @@ impl Mask { } } else { let clip_rect = self.size().to_screen_int_rect(0, 0); - let mut subpix = self.as_subpixmap(); - let mut blitter = match RasterPipelineBlitter::new_mask(&mut subpix) { + let mut submask = self.as_submask_mut(); + let mut blitter = match RasterPipelineBlitter::new_mask(&mut submask) { Some(v) => v, None => return, // nothing to do, all good }; @@ -402,3 +399,8 @@ impl<'a> SubMaskRef<'a> { } } } + +pub(crate) struct SubMaskMut<'a> { + pub(crate) data: &'a mut [u8], + pub(crate) real_width: usize, +} diff --git a/src/painter.rs b/src/painter.rs index 624e00a..bbbed97 100644 --- a/src/painter.rs +++ b/src/painter.rs @@ -10,7 +10,7 @@ use tiny_skia_path::{PathStroker, Scalar, SCALAR_MAX}; use crate::geom::ScreenIntRect; use crate::mask::SubMaskRef; -use crate::pipeline::{RasterPipelineBlitter, RasterPipelineBuilder}; +use crate::pipeline::{GenericPixmapMut, RasterPipelineBlitter, RasterPipelineBuilder}; use crate::pixmap::SubPixmapMut; use crate::scan; @@ -536,7 +536,7 @@ impl PixmapMut<'_> { pipeline::AAMaskCtx::default(), mask.as_submask().mask_ctx(), pixmap_src, - &mut self.as_subpixmap(), + GenericPixmapMut::from_pixmap(&mut self.as_subpixmap()), ); } } diff --git a/src/pipeline/blitter.rs b/src/pipeline/blitter.rs index 794c3d0..2fb0c36 100644 --- a/src/pipeline/blitter.rs +++ b/src/pipeline/blitter.rs @@ -11,20 +11,33 @@ use crate::alpha_runs::AlphaRun; use crate::blitter::{Blitter, Mask}; use crate::color::AlphaU8; use crate::geom::ScreenIntRect; -use crate::mask::SubMaskRef; +use crate::mask::{SubMaskMut, SubMaskRef}; use crate::math::LENGTH_U32_ONE; -use crate::pipeline::{self, RasterPipeline, RasterPipelineBuilder}; +use crate::pipeline::{self, GenericPixmapMut, RasterPipeline, RasterPipelineBuilder}; use crate::pixmap::SubPixmapMut; +enum PipelineDest<'a, 'b: 'a> { + Mask(&'a mut SubMaskMut<'b>), + Pixmap(&'a mut SubPixmapMut<'b>), +} + +impl<'a: 'c, 'b: 'a, 'c> PipelineDest<'a, 'b> { + fn to_generic_pixmap(&'c mut self) -> GenericPixmapMut<'c> { + match self { + PipelineDest::Mask(mask) => GenericPixmapMut::from_mask(mask), + PipelineDest::Pixmap(pixmap) => GenericPixmapMut::from_pixmap(pixmap), + } + } +} + pub struct RasterPipelineBlitter<'a, 'b: 'a> { mask: Option>, pixmap_src: PixmapRef<'a>, - pixmap: &'a mut SubPixmapMut<'b>, + pixmap_dst: PipelineDest<'a, 'b>, memset2d_color: Option, blit_anti_h_rp: RasterPipeline, blit_rect_rp: RasterPipeline, blit_mask_rp: RasterPipeline, - is_mask: bool, } impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { @@ -202,16 +215,15 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { Some(RasterPipelineBlitter { mask, pixmap_src, - pixmap, + pixmap_dst: PipelineDest::Pixmap(pixmap), memset2d_color, blit_anti_h_rp, blit_rect_rp, blit_mask_rp, - is_mask: false, }) } - pub fn new_mask(pixmap: &'a mut SubPixmapMut<'b>) -> Option { + pub fn new_mask(mask: &'a mut SubMaskMut<'b>) -> Option { let color = Color::WHITE.premultiply(); let memset2d_color = Some(color.to_color_u8()); @@ -244,12 +256,11 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { Some(RasterPipelineBlitter { mask: None, pixmap_src: PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap(), - pixmap, + pixmap_dst: PipelineDest::Mask(mask), memset2d_color, blit_anti_h_rp, blit_rect_rp, blit_mask_rp, - is_mask: true, }) } } @@ -283,7 +294,7 @@ impl Blitter for RasterPipelineBlitter<'_, '_> { pipeline::AAMaskCtx::default(), mask_ctx, self.pixmap_src, - self.pixmap, + self.pixmap_dst.to_generic_pixmap(), ); } } @@ -333,25 +344,23 @@ impl Blitter for RasterPipelineBlitter<'_, '_> { fn blit_rect(&mut self, rect: &ScreenIntRect) { if let Some(c) = self.memset2d_color { - if self.is_mask { - for y in 0..rect.height() { - let start = self - .pixmap - .offset(rect.x() as usize, (rect.y() + y) as usize); - let end = start + rect.width() as usize; - self.pixmap.data[start..end] - .iter_mut() - .for_each(|p| *p = c.alpha()); + match &mut self.pixmap_dst { + PipelineDest::Mask(mask) => { + for y in 0..rect.height() { + let start = mask.real_width * ((rect.y() + y) as usize) + rect.x() as usize; + let end = start + rect.width() as usize; + mask.data[start..end].fill(c.alpha()); + } } - } else { - for y in 0..rect.height() { - let start = self - .pixmap - .offset(rect.x() as usize, (rect.y() + y) as usize); - let end = start + rect.width() as usize; - self.pixmap.pixels_mut()[start..end] - .iter_mut() - .for_each(|p| *p = c); + PipelineDest::Pixmap(pixmap) => { + for y in 0..rect.height() { + let start = + 4 * pixmap.real_width * (rect.y() + y) as usize + 4 * rect.x() as usize; + let row_pixels = bytemuck::cast_slice_mut::<_, PremultipliedColorU8>( + &mut pixmap.data[start..start + 4 * rect.width() as usize], + ); + row_pixels.fill(c); + } } } @@ -365,7 +374,7 @@ impl Blitter for RasterPipelineBlitter<'_, '_> { pipeline::AAMaskCtx::default(), mask_ctx, self.pixmap_src, - self.pixmap, + self.pixmap_dst.to_generic_pixmap(), ); } @@ -378,7 +387,12 @@ impl Blitter for RasterPipelineBlitter<'_, '_> { let mask_ctx = self.mask.map(|c| c.mask_ctx()).unwrap_or_default(); - self.blit_mask_rp - .run(clip, aa_mask_ctx, mask_ctx, self.pixmap_src, self.pixmap); + self.blit_mask_rp.run( + clip, + aa_mask_ctx, + mask_ctx, + self.pixmap_src, + self.pixmap_dst.to_generic_pixmap(), + ); } } diff --git a/src/pipeline/highp.rs b/src/pipeline/highp.rs index 214e31a..ba9c12c 100644 --- a/src/pipeline/highp.rs +++ b/src/pipeline/highp.rs @@ -18,7 +18,7 @@ and we're using a manual implementation. use crate::{PremultipliedColorU8, SpreadMode, PixmapRef}; use crate::geom::ScreenIntRect; -use crate::pixmap::SubPixmapMut; +use crate::pipeline::GenericPixmapMut; use crate::wide::{f32x8, i32x8, u32x8}; pub const STAGE_WIDTH: usize = 8; @@ -29,7 +29,7 @@ pub struct Pipeline<'a, 'b: 'a> { index: usize, functions: &'a [StageFn], pixmap_src: PixmapRef<'a>, - pixmap_dst: &'a mut SubPixmapMut<'b>, + pixmap_dst: GenericPixmapMut<'b>, ctx: &'a mut super::Context, // TODO: remove mut mask_ctx: super::MaskCtx<'a>, aa_mask_ctx: super::AAMaskCtx, @@ -151,7 +151,7 @@ pub fn start( mask_ctx: super::MaskCtx, ctx: &mut super::Context, pixmap_src: PixmapRef, - pixmap_dst: &mut SubPixmapMut, + pixmap_dst: GenericPixmapMut, ) { let mut p = Pipeline { index: 0, diff --git a/src/pipeline/lowp.rs b/src/pipeline/lowp.rs index fc1a6c5..1f55e7b 100644 --- a/src/pipeline/lowp.rs +++ b/src/pipeline/lowp.rs @@ -30,7 +30,7 @@ On ARM AArch64 the story is different and explicit SIMD make our code up to 2-3x use crate::PremultipliedColorU8; -use crate::pixmap::SubPixmapMut; +use crate::pipeline::GenericPixmapMut; use crate::wide::{f32x8, u16x16, f32x16}; use crate::geom::ScreenIntRect; @@ -41,7 +41,7 @@ pub type StageFn = fn(p: &mut Pipeline); pub struct Pipeline<'a, 'b: 'a> { index: usize, functions: &'a [StageFn], - pixmap: &'a mut SubPixmapMut<'b>, + pixmap: GenericPixmapMut<'b>, mask_ctx: super::MaskCtx<'a>, aa_mask_ctx: super::AAMaskCtx, ctx: &'a mut super::Context, @@ -169,7 +169,7 @@ pub fn start( aa_mask_ctx: super::AAMaskCtx, mask_ctx: super::MaskCtx, ctx: &mut super::Context, - pixmap: &mut SubPixmapMut, + pixmap: GenericPixmapMut, ) { let mut p = Pipeline { index: 0, diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index b3612ee..2dcca8e 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -50,6 +50,7 @@ use arrayvec::ArrayVec; use tiny_skia_path::NormalizedF32; +use crate::mask::SubMaskMut; use crate::{Color, PremultipliedColor, PremultipliedColorU8, SpreadMode}; use crate::{PixmapRef, Transform}; @@ -169,7 +170,27 @@ impl PixmapRef<'_> { } } -impl SubPixmapMut<'_> { +/// The target of a pipeline, can be either from a PixmapMut or a MaskMut. The pixel format +/// is left implicit. +pub(crate) struct GenericPixmapMut<'a> { + pub(crate) data: &'a mut [u8], + pub(crate) real_width: usize, +} + +impl<'b> GenericPixmapMut<'b> { + pub(crate) fn from_pixmap<'a: 'b>(pixmap: &'b mut SubPixmapMut<'a>) -> Self { + Self { + data: pixmap.data, + real_width: pixmap.real_width, + } + } + pub(crate) fn from_mask<'a: 'b>(mask: &'b mut SubMaskMut<'a>) -> Self { + Self { + data: mask.data, + real_width: mask.real_width, + } + } + #[inline(always)] pub(crate) fn offset(&self, dx: usize, dy: usize) -> usize { self.real_width * dy + dx @@ -178,13 +199,7 @@ impl SubPixmapMut<'_> { #[inline(always)] pub(crate) fn slice_at_xy(&mut self, dx: usize, dy: usize) -> &mut [PremultipliedColorU8] { let offset = self.offset(dx, dy); - &mut self.pixels_mut()[offset..] - } - - #[inline(always)] - pub(crate) fn slice_mask_at_xy(&mut self, dx: usize, dy: usize) -> &mut [u8] { - let offset = self.offset(dx, dy); - &mut self.data[offset..] + &mut bytemuck::cast_slice_mut(self.data)[offset..] } #[inline(always)] @@ -194,7 +209,9 @@ impl SubPixmapMut<'_> { dy: usize, ) -> &mut [PremultipliedColorU8; highp::STAGE_WIDTH] { let offset = self.offset(dx, dy); - self.pixels_mut()[offset..].first_chunk_mut().unwrap() + bytemuck::cast_slice_mut(self.data)[offset..] + .first_chunk_mut() + .unwrap() } #[inline(always)] @@ -204,7 +221,15 @@ impl SubPixmapMut<'_> { dy: usize, ) -> &mut [PremultipliedColorU8; lowp::STAGE_WIDTH] { let offset = self.offset(dx, dy); - self.pixels_mut()[offset..].first_chunk_mut().unwrap() + bytemuck::cast_slice_mut(self.data)[offset..] + .first_chunk_mut() + .unwrap() + } + + #[inline(always)] + pub(crate) fn slice_mask_at_xy(&mut self, dx: usize, dy: usize) -> &mut [u8] { + let offset = self.offset(dx, dy); + &mut self.data[offset..] } #[inline(always)] @@ -508,7 +533,7 @@ impl RasterPipeline { aa_mask_ctx: AAMaskCtx, mask_ctx: MaskCtx, pixmap_src: PixmapRef, - pixmap_dst: &mut SubPixmapMut, + pixmap_dst: GenericPixmapMut, ) { match self.kind { RasterPipelineKind::High { @@ -578,7 +603,7 @@ mod blend_tests { let mut p = p.compile(); let rect = pixmap.size().to_screen_int_rect(0, 0); p.run(&rect, AAMaskCtx::default(), MaskCtx::default(), pixmap_src, - &mut pixmap.as_mut().as_subpixmap()); + GenericPixmapMut::from_pixmap(&mut pixmap.as_mut().as_subpixmap())); assert_eq!( pixmap.as_ref().pixel(0, 0).unwrap(), diff --git a/src/pixmap.rs b/src/pixmap.rs index e576ef2..fc5faed 100644 --- a/src/pixmap.rs +++ b/src/pixmap.rs @@ -580,13 +580,6 @@ pub struct SubPixmapMut<'a> { pub real_width: usize, } -impl SubPixmapMut<'_> { - /// Returns a mutable slice of pixels. - pub fn pixels_mut(&mut self) -> &mut [PremultipliedColorU8] { - bytemuck::cast_slice_mut(self.data) - } -} - /// Returns minimum bytes per row as usize. /// /// Pixmap's maximum value for row bytes must fit in 31 bits. From 3360cefbe0f26266c9f60ee5e8d10ec0353e0240 Mon Sep 17 00:00:00 2001 From: M Stoeckl Date: Tue, 25 Aug 2026 19:45:47 -0400 Subject: [PATCH 2/3] Extend Pixmap to support strides and new pixel types This only implements a single pixel type (the current RgbaU8), but introduces some stride alignment checks and match expressions that should be useful when implementing this in the future. Unfortunately the direct pixel access methods like pixels_mut() had to be removed for the Pixmap to remain general over PixelType, and handle strides which are not a multiple of the pixel byte size. While it may be possible to add simple abstractions for efficient pixel and row access, the logic for users to do this themselves with data_mut() and bytemuck is not very complex. The (currently inactive) constraints on pixel type alignment will likely be needed to efficiently (in both time and code size) operate on pixel types with higher bit depth. This eliminates the internal SubPixmapMut type, and makes public the power to construct lightweight views for rectangles of PixmapMut and PixmapRef through their ::subpixmap methods. --- README.md | 2 +- src/color.rs | 10 + src/lib.rs | 2 +- src/mask.rs | 71 ++- src/painter.rs | 20 +- src/pipeline/blitter.rs | 27 +- src/pipeline/highp.rs | 2 +- src/pipeline/mod.rs | 97 ++-- src/pixmap.rs | 544 +++++++++++++++++------ tests/images/pixmap/overlay-8u-on-8u.png | Bin 0 -> 2330 bytes tests/integration/fill.rs | 4 +- tests/integration/pixmap.rs | 76 +++- tests/integration/png.rs | 18 +- 13 files changed, 627 insertions(+), 246 deletions(-) create mode 100644 tests/images/pixmap/overlay-8u-on-8u.png diff --git a/README.md b/README.md index 4583982..1e170e8 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ And more importantly, we do not plan to support many feature at all. - GPU rendering. - Text rendering (maybe someday). - PDF generation. -- Non-RGBA8888 images. +- Non-RGBA images. - Non-PNG image formats. - Advanced Bézier path operations. - Conic path segments. diff --git a/src/color.rs b/src/color.rs index 07af604..b9f50e2 100644 --- a/src/color.rs +++ b/src/color.rs @@ -171,6 +171,16 @@ impl PremultipliedColorU8 { ) } } + + /// Converts into `PremultipliedColor`. + pub fn to_color(&self) -> PremultipliedColor { + PremultipliedColor { + r: NormalizedF32::new_clamped(f32::from(self.red()) / 255.0), + g: NormalizedF32::new_clamped(f32::from(self.green()) / 255.0), + b: NormalizedF32::new_clamped(f32::from(self.blue()) / 255.0), + a: NormalizedF32::new_clamped(f32::from(self.alpha()) / 255.0), + } + } } impl core::fmt::Debug for PremultipliedColorU8 { diff --git a/src/lib.rs b/src/lib.rs index 6660881..57f921e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,7 @@ pub use color::{Color, ColorSpace, ColorU8, PremultipliedColor, PremultipliedCol pub use color::{ALPHA_OPAQUE, ALPHA_TRANSPARENT, ALPHA_U8_OPAQUE, ALPHA_U8_TRANSPARENT}; pub use mask::{Mask, MaskType}; pub use painter::{FillRule, Paint}; -pub use pixmap::{Pixmap, PixmapMut, PixmapRef, BYTES_PER_PIXEL}; +pub use pixmap::{PixelType, Pixmap, PixmapMut, PixmapRef}; pub use shaders::{FilterQuality, GradientStop, PixmapPaint, SpreadMode}; pub use shaders::{LinearGradient, Pattern, RadialGradient, Shader, SweepGradient}; diff --git a/src/mask.rs b/src/mask.rs index f24273a..2c48f82 100644 --- a/src/mask.rs +++ b/src/mask.rs @@ -14,7 +14,8 @@ use tiny_skia_path::{IntRect, IntSize, Path, Scalar, Transform}; use crate::geom::IntSizeExt; use crate::painter::DrawTiler; use crate::pipeline::RasterPipelineBlitter; -use crate::scan; +use crate::pixmap::PixelType; +use crate::{scan, PremultipliedColorU8}; use crate::{FillRule, PixmapRef}; /// A mask type. @@ -60,30 +61,49 @@ impl Mask { size: pixmap.size(), }; - // TODO: optimize - match mask_type { - MaskType::Alpha => { - for (p, a) in pixmap.pixels().iter().zip(mask.data.as_mut_slice()) { - *a = p.alpha(); - } - } - MaskType::Luminance => { - for (p, ma) in pixmap.pixels().iter().zip(mask.data.as_mut_slice()) { - // Normalize. - let mut r = f32::from(p.red()) / 255.0; - let mut g = f32::from(p.green()) / 255.0; - let mut b = f32::from(p.blue()) / 255.0; - let a = f32::from(p.alpha()) / 255.0; - - // Demultiply. - if p.alpha() != 0 { - r /= a; - g /= a; - b /= a; + for y in 0..pixmap.height() { + let pix_bpp = usize::from(pixmap.pixel_type().size()); + + let width = pixmap.width() as usize; + let pix_row_len = pix_bpp * width; + let row = &pixmap.data() + [y as usize * pixmap.stride()..y as usize * pixmap.stride() + pix_row_len]; + let dst = &mut mask.data[y as usize * width..y as usize * width + width]; + + // TODO: optimize + match mask_type { + MaskType::Alpha => match pixmap.pixel_type() { + PixelType::Rgba8U => { + let pixels: &[PremultipliedColorU8] = bytemuck::cast_slice(row); + for (p, a) in pixels.iter().zip(dst) { + *a = p.alpha(); + } + } + }, + MaskType::Luminance => { + match pixmap.pixel_type() { + PixelType::Rgba8U => { + let pixels: &[PremultipliedColorU8] = bytemuck::cast_slice(row); + + for (p, ma) in pixels.iter().zip(dst) { + // Normalize. + let mut r = f32::from(p.red()) / 255.0; + let mut g = f32::from(p.green()) / 255.0; + let mut b = f32::from(p.blue()) / 255.0; + let a = f32::from(p.alpha()) / 255.0; + + // Demultiply. + if p.alpha() != 0 { + r /= a; + g /= a; + b /= a; + } + + let luma = r * 0.2126 + g * 0.7152 + b * 0.0722; + *ma = ((luma * a) * 255.0).clamp(0.0, 255.0).ceil() as u8; + } + } } - - let luma = r * 0.2126 + g * 0.7152 + b * 0.0722; - *ma = ((luma * a) * 255.0).clamp(0.0, 255.0).ceil() as u8; } } } @@ -158,6 +178,7 @@ impl Mask { pub(crate) fn as_submask_mut(&mut self) -> SubMaskMut<'_> { SubMaskMut { + size: self.size, real_width: self.size.width() as usize, data: &mut self.data, } @@ -169,6 +190,7 @@ impl Mask { let offset = rect.top() as usize * row_bytes + rect.left() as usize; Some(SubMaskMut { + size: rect.size(), real_width: self.size.width() as usize, data: &mut self.data[offset..], }) @@ -402,5 +424,6 @@ impl<'a> SubMaskRef<'a> { pub(crate) struct SubMaskMut<'a> { pub(crate) data: &'a mut [u8], + pub(crate) size: IntSize, pub(crate) real_width: usize, } diff --git a/src/painter.rs b/src/painter.rs index bbbed97..4625b50 100644 --- a/src/painter.rs +++ b/src/painter.rs @@ -11,7 +11,7 @@ use tiny_skia_path::{PathStroker, Scalar, SCALAR_MAX}; use crate::geom::ScreenIntRect; use crate::mask::SubMaskRef; use crate::pipeline::{GenericPixmapMut, RasterPipelineBlitter, RasterPipelineBuilder}; -use crate::pixmap::SubPixmapMut; +use crate::pixmap::PixmapMut; use crate::scan; use crate::geom::IntSizeExt; @@ -196,8 +196,7 @@ impl PixmapMut<'_> { let clip = self.size().to_screen_int_rect(0, 0); let mask = mask.map(|mask| mask.as_submask()); - let mut subpix = self.as_subpixmap(); - let mut blitter = match RasterPipelineBlitter::new(paint, mask, &mut subpix) { + let mut blitter = match RasterPipelineBlitter::new(paint, mask, self) { Some(v) => v, None => return, // nothing to do, all good }; @@ -286,8 +285,7 @@ impl PixmapMut<'_> { } else { let clip_rect = self.size().to_screen_int_rect(0, 0); let submask = mask.map(|mask| mask.as_submask()); - let mut subpix = self.as_subpixmap(); - let mut blitter = match RasterPipelineBlitter::new(paint, submask, &mut subpix) { + let mut blitter = match RasterPipelineBlitter::new(paint, submask, self) { Some(v) => v, None => return, // nothing to do, all good }; @@ -414,7 +412,6 @@ impl PixmapMut<'_> { paint.shader.transform(ts); } } else { - let subpix = &mut self.as_subpixmap(); let submask = mask.map(|mask| mask.as_submask()); if !transform.is_identity() { paint.shader.transform(transform); @@ -428,9 +425,9 @@ impl PixmapMut<'_> { } }; - Self::stroke_hairline(&path, &paint, stroke.line_cap, submask, subpix); + Self::stroke_hairline(&path, &paint, stroke.line_cap, submask, self); } else { - Self::stroke_hairline(path, &paint, stroke.line_cap, submask, subpix); + Self::stroke_hairline(path, &paint, stroke.line_cap, submask, self); } } } else { @@ -452,9 +449,9 @@ impl PixmapMut<'_> { paint: &Paint, line_cap: LineCap, mask: Option, - pixmap: &mut SubPixmapMut, + pixmap: &mut PixmapMut, ) { - let clip = pixmap.size.to_screen_int_rect(0, 0); + let clip = pixmap.size().to_screen_int_rect(0, 0); let mut blitter = match RasterPipelineBlitter::new(paint, mask, pixmap) { Some(v) => v, None => return, // nothing to do, all good @@ -531,12 +528,13 @@ impl PixmapMut<'_> { p.push(pipeline::Stage::Store); let mut p = p.compile(); let rect = self.size().to_screen_int_rect(0, 0); + p.run( &rect, pipeline::AAMaskCtx::default(), mask.as_submask().mask_ctx(), pixmap_src, - GenericPixmapMut::from_pixmap(&mut self.as_subpixmap()), + GenericPixmapMut::from_pixmap(self), ); } } diff --git a/src/pipeline/blitter.rs b/src/pipeline/blitter.rs index 2fb0c36..314f73f 100644 --- a/src/pipeline/blitter.rs +++ b/src/pipeline/blitter.rs @@ -14,11 +14,11 @@ use crate::geom::ScreenIntRect; use crate::mask::{SubMaskMut, SubMaskRef}; use crate::math::LENGTH_U32_ONE; use crate::pipeline::{self, GenericPixmapMut, RasterPipeline, RasterPipelineBuilder}; -use crate::pixmap::SubPixmapMut; +use crate::pixmap::{PixelType, PixmapMut}; enum PipelineDest<'a, 'b: 'a> { Mask(&'a mut SubMaskMut<'b>), - Pixmap(&'a mut SubPixmapMut<'b>), + Pixmap(&'a mut PixmapMut<'b>), } impl<'a: 'c, 'b: 'a, 'c> PipelineDest<'a, 'b> { @@ -44,12 +44,12 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { pub fn new( paint: &Paint<'a>, mask: Option>, - pixmap: &'a mut SubPixmapMut<'b>, + pixmap: &'a mut PixmapMut<'b>, ) -> Option { // Make sure that `mask` has the same size as `pixmap`. if let Some(mask) = mask { - if mask.size.width() != pixmap.size.width() - || mask.size.height() != pixmap.size.height() + if mask.size.width() != pixmap.size().width() + || mask.size.height() != pixmap.size().height() { log::warn!("Pixmap and Mask are expected to have the same size"); return None; @@ -354,12 +354,17 @@ impl Blitter for RasterPipelineBlitter<'_, '_> { } PipelineDest::Pixmap(pixmap) => { for y in 0..rect.height() { - let start = - 4 * pixmap.real_width * (rect.y() + y) as usize + 4 * rect.x() as usize; - let row_pixels = bytemuck::cast_slice_mut::<_, PremultipliedColorU8>( - &mut pixmap.data[start..start + 4 * rect.width() as usize], - ); - row_pixels.fill(c); + match pixmap.pixel_type() { + PixelType::Rgba8U => { + let start = pixmap.stride() * (rect.y() + y) as usize + + 4 * rect.x() as usize; + let row_pixels = bytemuck::cast_slice_mut::<_, PremultipliedColorU8>( + &mut pixmap.data_mut() + [start..start + 4 * rect.width() as usize], + ); + row_pixels.fill(c); + } + } } } } diff --git a/src/pipeline/highp.rs b/src/pipeline/highp.rs index ba9c12c..31be9ed 100644 --- a/src/pipeline/highp.rs +++ b/src/pipeline/highp.rs @@ -321,7 +321,7 @@ fn gather_ix(pixmap: PixmapRef, mut x: f32x8, mut y: f32x8) -> u32x8 { x = x.max(f32x8::default()).min(f32x8::splat(w)); y = y.max(f32x8::default()).min(f32x8::splat(h)); - (y.trunc_int() * i32x8::splat(pixmap.width() as i32) + x.trunc_int()).to_u32x8_bitcast() + (y.trunc_int() * i32x8::splat(pixmap.stride() as i32) + x.trunc_int() * i32x8::splat(4)).to_u32x8_bitcast() } #[inline(always)] diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index 2dcca8e..21620c4 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -48,7 +48,7 @@ use alloc::vec::Vec; use arrayvec::ArrayVec; -use tiny_skia_path::NormalizedF32; +use tiny_skia_path::{IntSize, NormalizedF32}; use crate::mask::SubMaskMut; use crate::{Color, PremultipliedColor, PremultipliedColorU8, SpreadMode}; @@ -57,7 +57,7 @@ use crate::{PixmapRef, Transform}; pub use blitter::RasterPipelineBlitter; use crate::geom::ScreenIntRect; -use crate::pixmap::SubPixmapMut; +use crate::pixmap::PixmapMut; use crate::wide::u32x8; mod blitter; @@ -156,17 +156,17 @@ impl PixmapRef<'_> { #[inline(always)] pub(crate) fn gather(&self, index: u32x8) -> [PremultipliedColorU8; highp::STAGE_WIDTH] { let index: [u32; 8] = bytemuck::cast(index); - let pixels = self.pixels(); - [ - pixels[index[0] as usize], - pixels[index[1] as usize], - pixels[index[2] as usize], - pixels[index[3] as usize], - pixels[index[4] as usize], - pixels[index[5] as usize], - pixels[index[6] as usize], - pixels[index[7] as usize], - ] + let data = self.data(); + bytemuck::cast::<[[u8; 4]; highp::STAGE_WIDTH], _>([ + *data[index[0] as usize..].first_chunk().unwrap(), + *data[index[1] as usize..].first_chunk().unwrap(), + *data[index[2] as usize..].first_chunk().unwrap(), + *data[index[3] as usize..].first_chunk().unwrap(), + *data[index[4] as usize..].first_chunk().unwrap(), + *data[index[5] as usize..].first_chunk().unwrap(), + *data[index[6] as usize..].first_chunk().unwrap(), + *data[index[7] as usize..].first_chunk().unwrap(), + ]) } } @@ -174,32 +174,37 @@ impl PixmapRef<'_> { /// is left implicit. pub(crate) struct GenericPixmapMut<'a> { pub(crate) data: &'a mut [u8], - pub(crate) real_width: usize, + pub(crate) size: IntSize, + pub(crate) stride: usize, } impl<'b> GenericPixmapMut<'b> { - pub(crate) fn from_pixmap<'a: 'b>(pixmap: &'b mut SubPixmapMut<'a>) -> Self { + pub(crate) fn from_pixmap<'a: 'b>(pixmap: &'b mut PixmapMut<'a>) -> Self { + let (stride, size) = (pixmap.stride(), pixmap.size()); Self { - data: pixmap.data, - real_width: pixmap.real_width, + data: pixmap.data_mut(), + stride, + size, } } pub(crate) fn from_mask<'a: 'b>(mask: &'b mut SubMaskMut<'a>) -> Self { Self { data: mask.data, - real_width: mask.real_width, + stride: mask.real_width, + size: mask.size, } } #[inline(always)] pub(crate) fn offset(&self, dx: usize, dy: usize) -> usize { - self.real_width * dy + dx + self.stride * dy + dx * 4 } #[inline(always)] pub(crate) fn slice_at_xy(&mut self, dx: usize, dy: usize) -> &mut [PremultipliedColorU8] { let offset = self.offset(dx, dy); - &mut bytemuck::cast_slice_mut(self.data)[offset..] + let len = (self.size.width() as usize - dx) * 4; + bytemuck::cast_slice_mut(&mut self.data[offset..offset + len]) } #[inline(always)] @@ -209,9 +214,11 @@ impl<'b> GenericPixmapMut<'b> { dy: usize, ) -> &mut [PremultipliedColorU8; highp::STAGE_WIDTH] { let offset = self.offset(dx, dy); - bytemuck::cast_slice_mut(self.data)[offset..] - .first_chunk_mut() - .unwrap() + bytemuck::cast_mut( + self.data[offset..] + .first_chunk_mut::<{ highp::STAGE_WIDTH * 4 }>() + .unwrap(), + ) } #[inline(always)] @@ -221,14 +228,21 @@ impl<'b> GenericPixmapMut<'b> { dy: usize, ) -> &mut [PremultipliedColorU8; lowp::STAGE_WIDTH] { let offset = self.offset(dx, dy); - bytemuck::cast_slice_mut(self.data)[offset..] - .first_chunk_mut() - .unwrap() + bytemuck::cast_mut( + self.data[offset..] + .first_chunk_mut::<{ lowp::STAGE_WIDTH * 4 }>() + .unwrap(), + ) + } + + #[inline(always)] + pub(crate) fn offset_mask(&self, dx: usize, dy: usize) -> usize { + self.stride * dy + dx } #[inline(always)] pub(crate) fn slice_mask_at_xy(&mut self, dx: usize, dy: usize) -> &mut [u8] { - let offset = self.offset(dx, dy); + let offset = self.offset_mask(dx, dy); &mut self.data[offset..] } @@ -238,7 +252,7 @@ impl<'b> GenericPixmapMut<'b> { dx: usize, dy: usize, ) -> &mut [u8; lowp::STAGE_WIDTH] { - let offset = self.offset(dx, dy); + let offset = self.offset_mask(dx, dy); self.data[offset..].first_chunk_mut().unwrap() } } @@ -451,18 +465,21 @@ impl RasterPipelineBuilder { // While the only difference is the load/store methods. let mut tail_functions = functions.clone(); for fun in &mut tail_functions { + // RgbaU8 load/store if highp::fn_ptr(*fun) == highp::fn_ptr(highp::load_dst) { *fun = highp::load_dst_tail as highp::StageFn; } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::store) { *fun = highp::store_tail as highp::StageFn; - } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::load_dst_u8) { - *fun = highp::load_dst_u8_tail as highp::StageFn; - } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::store_u8) { - *fun = highp::store_u8_tail as highp::StageFn; } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::source_over_rgba) { // SourceOverRgba calls load/store manually, without the pipeline, // therefore we have to switch it too. *fun = highp::source_over_rgba_tail as highp::StageFn; + + // U8 load/store + } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::load_dst_u8) { + *fun = highp::load_dst_u8_tail as highp::StageFn; + } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::store_u8) { + *fun = highp::store_u8_tail as highp::StageFn; } } @@ -484,18 +501,21 @@ impl RasterPipelineBuilder { // See above. let mut tail_functions = functions.clone(); for fun in &mut tail_functions { + // RgbaU8 load/store if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::load_dst) { *fun = lowp::load_dst_tail as lowp::StageFn; } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::store) { *fun = lowp::store_tail as lowp::StageFn; - } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::load_dst_u8) { - *fun = lowp::load_dst_u8_tail as lowp::StageFn; - } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::store_u8) { - *fun = lowp::store_u8_tail as lowp::StageFn; } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::source_over_rgba) { // SourceOverRgba calls load/store manually, without the pipeline, // therefore we have to switch it too. *fun = lowp::source_over_rgba_tail as lowp::StageFn; + + // U8 load/store + } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::load_dst_u8) { + *fun = lowp::load_dst_u8_tail as lowp::StageFn; + } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::store_u8) { + *fun = lowp::store_u8_tail as lowp::StageFn; } } @@ -603,10 +623,11 @@ mod blend_tests { let mut p = p.compile(); let rect = pixmap.size().to_screen_int_rect(0, 0); p.run(&rect, AAMaskCtx::default(), MaskCtx::default(), pixmap_src, - GenericPixmapMut::from_pixmap(&mut pixmap.as_mut().as_subpixmap())); + GenericPixmapMut::from_pixmap(&mut pixmap.as_mut())); + let pixels = bytemuck::cast_slice::(pixmap.as_ref().data()); assert_eq!( - pixmap.as_ref().pixel(0, 0).unwrap(), + pixels[0], PremultipliedColorU8::from_rgba($r, $g, $b, $a).unwrap() ); } diff --git a/src/pixmap.rs b/src/pixmap.rs index fc5faed..72c8e23 100644 --- a/src/pixmap.rs +++ b/src/pixmap.rs @@ -12,24 +12,54 @@ use core::num::NonZeroUsize; use tiny_skia_path::IntSize; -use crate::{Color, IntRect}; +use crate::{Color, IntRect, PremultipliedColor}; use crate::color::PremultipliedColorU8; -use crate::geom::{IntSizeExt, ScreenIntRect}; #[cfg(feature = "png-format")] use crate::color::{premultiply_u8, ALPHA_U8_OPAQUE}; -/// Number of bytes per pixel. -pub const BYTES_PER_PIXEL: usize = 4; +/// This determines how pixels are encoded in memory and map to real values. +/// +/// This does NOT include color space information. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum PixelType { + /// 8 bit premultiplied integer values in R,G,B,A memory order. + /// + /// See [PremultipliedColorU8] + /// + /// Unit white is (255,255,255,255). + Rgba8U, +} + +impl PixelType { + /// Number of bytes per pixel + #[inline(always)] + pub const fn size(&self) -> u8 { + match self { + PixelType::Rgba8U => 4, + } + } + + /// Required memory alignment of each pixel + #[inline(always)] + pub const fn alignment(&self) -> u8 { + match self { + PixelType::Rgba8U => 1, + } + } +} /// A container that owns premultiplied RGBA pixels. /// -/// The data is not aligned, therefore width == stride. +/// The data is only guaranteed to be aligned to match the PixelType. #[derive(Clone, PartialEq)] pub struct Pixmap { data: Vec, size: IntSize, + /// The number of bytes between the starts of two rows + stride: usize, + pixel_type: PixelType, } impl Pixmap { @@ -42,7 +72,8 @@ impl Pixmap { /// Pixmap's width is limited by i32::MAX/4. pub fn new(width: u32, height: u32) -> Option { let size = IntSize::from_wh(width, height)?; - let data_len = data_len_for_size(size)?; + let stride = min_row_bytes(size, PixelType::Rgba8U)?; + let data_len = data_len_for_size(size, stride.get())?; // We cannot check that allocation was successful yet. // We have to wait for https://github.com/rust-lang/rust/issues/48043 @@ -50,22 +81,109 @@ impl Pixmap { Some(Pixmap { data: vec![0; data_len], size, + stride: stride.get(), + pixel_type: PixelType::Rgba8U, + }) + } + + /// Allocates a new pixmap with the provided parameters. + /// + /// A pixmap is filled with transparent black by default. + /// + /// Zero size in an error. + /// + /// Pixmap's width is limited by i32::MAX/pix_type.size(). + /// + /// This may arbitrarily fail with allocators that do not align + /// Vec to multiples of pix_type.alignment() in practice. For + /// reliable construction, manage your own memory and use PixmapMut + /// or PixmapRef. + pub fn new_with_type( + width: u32, + height: u32, + stride: usize, + pixel_type: PixelType, + ) -> Option { + let size = IntSize::from_wh(width, height)?; + let min_stride = min_row_bytes(size, pixel_type)?; + if min_stride.get() > stride { + return None; + } + let data_len = data_len_for_size(size, stride)?; + let data: Vec = vec![0; data_len]; + + // TODO: Rust does not provide a way to reliably produce an aligned + // Vec, as its allocators may make type alignment dependent decisions. + // If this becomes an issue in practice, Pixmap could overallocate by + // pixel_type.alignment() and offset any access to the data accordingly; + // but this will break methods like 'take()'. + if data.as_ptr() as usize % usize::from(pixel_type.alignment()) != 0 { + return None; + } + + Some(Pixmap { + data, + size, + stride, + pixel_type, + }) + } + + /// Allocates a new pixmap with the provided parameters. + /// + /// A pixmap is filled with transparent black by default. + /// + /// Zero size in an error. + /// + /// Pixmap's width is limited by i32::MAX/pix_type.size(). + pub fn from_vec_with_type( + data: Vec, + width: u32, + height: u32, + stride: usize, + pixel_type: PixelType, + ) -> Option { + let size = IntSize::from_wh(width, height)?; + let min_stride = min_row_bytes(size, pixel_type)?; + if min_stride.get() > stride { + return None; + } + let data_len = data_len_for_size(size, stride)?; + if data.len() != data_len { + return None; + } + + if data.as_ptr() as usize % usize::from(pixel_type.alignment()) != 0 { + return None; + } + + Some(Pixmap { + data, + size, + stride, + pixel_type, }) } /// Creates a new pixmap by taking ownership over an image buffer - /// (premultiplied RGBA pixels). + /// containing tightly packed premultiplied RgbaU8 pixels. /// /// The size needs to match the data provided. /// /// Pixmap's width is limited by i32::MAX/4. pub fn from_vec(data: Vec, size: IntSize) -> Option { - let data_len = data_len_for_size(size)?; + let stride = min_row_bytes(size, PixelType::Rgba8U)?; + let data_len = data_len_for_size(size, stride.get())?; if data.len() != data_len { return None; } - Some(Pixmap { data, size }) + Some(Pixmap { + data, + size, + stride: stride.get(), + pixel_type: PixelType::Rgba8U, + }) } /// Decodes a PNG data into a `Pixmap`. @@ -93,8 +211,10 @@ impl Pixmap { let size = IntSize::from_wh(info.width, info.height) .ok_or_else(|| make_custom_png_error("invalid image size"))?; - let data_len = - data_len_for_size(size).ok_or_else(|| make_custom_png_error("image is too big"))?; + let stride = min_row_bytes(size, PixelType::Rgba8U) + .ok_or_else(|| make_custom_png_error("image is too big"))?; + let data_len = data_len_for_size(size, stride.get()) + .ok_or_else(|| make_custom_png_error("image is too big"))?; img_data = match info.color_type { png::ColorType::Rgb => { @@ -146,7 +266,10 @@ impl Pixmap { // // Also, in our tests unsafe version (no bound checking) // had roughly the same performance. So we keep the safe one. - for pixel in img_data.as_mut_slice().chunks_mut(BYTES_PER_PIXEL) { + for pixel in img_data + .as_mut_slice() + .chunks_mut(usize::from(PixelType::Rgba8U.size())) + { let a = pixel[3]; pixel[0] = premultiply_u8(pixel[0], a); pixel[1] = premultiply_u8(pixel[1], a); @@ -187,6 +310,8 @@ impl Pixmap { PixmapRef { data: &self.data, size: self.size, + stride: self.stride, + pixel_type: self.pixel_type, } } @@ -195,6 +320,8 @@ impl Pixmap { PixmapMut { data: &mut self.data, size: self.size, + stride: self.stride, + pixel_type: self.pixel_type, } } @@ -210,6 +337,18 @@ impl Pixmap { self.size.height() } + /// Returns pixmap's pixel type + #[inline] + pub fn pixel_type(&self) -> PixelType { + self.pixel_type + } + + /// Returns pixmap's stride (spacing in bytes between the starts of rows) + #[inline] + pub fn stride(&self) -> usize { + self.stride + } + /// Returns pixmap's size. #[allow(dead_code)] pub(crate) fn size(&self) -> IntSize { @@ -218,10 +357,7 @@ impl Pixmap { /// Fills the entire pixmap with a specified color. pub fn fill(&mut self, color: Color) { - let c = color.premultiply().to_color_u8(); - for p in self.as_mut().pixels_mut() { - *p = c; - } + self.as_mut().fill(color); } /// Returns the internal data. @@ -238,43 +374,42 @@ impl Pixmap { self.data.as_mut_slice() } - /// Returns a pixel color. + /// Returns a pixel color, converted to `Color`. /// /// Returns `None` when position is out of bounds. - pub fn pixel(&self, x: u32, y: u32) -> Option { - let idx = self.width().checked_mul(y)?.checked_add(x)?; - self.pixels().get(idx as usize).cloned() - } - - /// Returns a mutable slice of pixels. - pub fn pixels_mut(&mut self) -> &mut [PremultipliedColorU8] { - bytemuck::cast_slice_mut(self.data_mut()) - } - - /// Returns a slice of pixels. - pub fn pixels(&self) -> &[PremultipliedColorU8] { - bytemuck::cast_slice(self.data()) + pub fn pixel(&self, x: u32, y: u32) -> Option { + self.as_ref().pixel(x, y) } /// Consumes the internal data. /// - /// Byteorder: RGBA + /// See [PixelType] and [Self::stride()] for the data layout. pub fn take(self) -> Vec { self.data } /// Consumes the pixmap and returns the internal data as demultiplied RGBA bytes. /// - /// Byteorder: RGBA + /// See [PixelType] for the data layout. pub fn take_demultiplied(mut self) -> Vec { // Demultiply alpha. // // RasterPipeline is 15% faster here, but produces slightly different results // due to rounding. So we stick with this method for now. - for pixel in self.pixels_mut() { - let c = pixel.demultiply(); - *pixel = - PremultipliedColorU8::from_rgba_unchecked(c.red(), c.green(), c.blue(), c.alpha()); + match self.pixel_type { + PixelType::Rgba8U => { + let pixels_mut: &mut [PremultipliedColorU8] = + bytemuck::cast_slice_mut(self.data_mut()); + for pixel in pixels_mut { + let c = pixel.demultiply(); + *pixel = PremultipliedColorU8::from_rgba_unchecked( + c.red(), + c.green(), + c.blue(), + c.alpha(), + ); + } + } } self.data } @@ -293,6 +428,8 @@ impl core::fmt::Debug for Pixmap { .field("data", &"...") .field("width", &self.size.width()) .field("height", &self.size.height()) + .field("stride", &self.stride()) + .field("pixel_type", &self.pixel_type()) .finish() } } @@ -300,39 +437,103 @@ impl core::fmt::Debug for Pixmap { /// A container that references premultiplied RGBA pixels. /// /// Can be created from `Pixmap` or from a user provided data. -/// -/// The data is not aligned, therefore width == stride. #[derive(Clone, Copy, PartialEq)] pub struct PixmapRef<'a> { data: &'a [u8], size: IntSize, + stride: usize, + pixel_type: PixelType, } impl<'a> PixmapRef<'a> { /// Creates a new `PixmapRef` from bytes. /// - /// The size must be at least `size.width() * size.height() * BYTES_PER_PIXEL`. + /// The size must be at least `size.width() * size.height() * PixelType::Rgba8U.size()`. /// Zero size in an error. Width is limited by i32::MAX/4. /// /// The `data` is assumed to have premultiplied RGBA pixels (byteorder: RGBA). pub fn from_bytes(data: &'a [u8], width: u32, height: u32) -> Option { let size = IntSize::from_wh(width, height)?; - let data_len = data_len_for_size(size)?; + let stride = min_row_bytes(size, PixelType::Rgba8U)?; + let data_len = data_len_for_size(size, stride.get())?; if data.len() < data_len { return None; } - Some(PixmapRef { data, size }) + Some(PixmapRef { + data, + size, + stride: stride.get(), + pixel_type: PixelType::Rgba8U, + }) + } + + /// Creates a new `PixmapRef` from bytes. + /// + /// The size must be at least `size.width() * size.height() * pixel_type.size()`. + /// Zero size in an error. Width is limited by i32::MAX/pixel_type.size(). + /// + /// The `data` is assumed to have pixels following the given [PixelType]); it + /// must have the minimum alignment of the [PixelType]). + pub fn from_bytes_with_type( + data: &'a [u8], + width: u32, + height: u32, + stride: usize, + pixel_type: PixelType, + ) -> Option { + let size = IntSize::from_wh(width, height)?; + let min_stride = min_row_bytes(size, pixel_type)?; + if min_stride.get() > stride { + return None; + } + let data_len = data_len_for_size(size, stride)?; + if data.len() != data_len { + return None; + } + + if data.as_ptr() as usize % usize::from(pixel_type.alignment()) != 0 { + return None; + } + + Some(PixmapRef { + data, + size, + stride, + pixel_type, + }) } /// Creates a new `Pixmap` from the current data. /// - /// Clones the underlying data. + /// Clones the underlying data; panics on allocation failure. pub fn to_owned(&self) -> Pixmap { - Pixmap { - data: self.data.to_vec(), - size: self.size, + // Create a tightly packed copy, so that views of larger images don't copy + // the entire thing + let new_stride = min_row_bytes(self.size(), self.pixel_type) + .expect("new stride should be no more than old stride") + .get(); + let mut new = + Pixmap::new_with_type(self.width(), self.height(), new_stride, self.pixel_type) + .unwrap(); + { + let old_data = self.data(); + let bpp = usize::from(self.pixel_type.size()); + let new_stride = new.stride(); + let mut new_mut = new.as_mut(); + let new_mut = new_mut.data_mut(); + + for y in 0..self.height() { + let old_start = y as usize * self.stride(); + let new_start = y as usize * new_stride; + let slice_len = (self.width() as usize) * bpp; + + new_mut[new_start..new_start + slice_len] + .copy_from_slice(&old_data[old_start..old_start + slice_len]); + } } + + new } /// Returns pixmap's width. @@ -347,62 +548,54 @@ impl<'a> PixmapRef<'a> { self.size.height() } + /// Returns pixmap's pixel type + #[inline] + pub fn pixel_type(&self) -> PixelType { + self.pixel_type + } + + /// Returns pixmap's stride (spacing in bytes between the starts of rows) + #[inline] + pub fn stride(&self) -> usize { + self.stride + } + /// Returns pixmap's size. pub(crate) fn size(&self) -> IntSize { self.size } - /// Returns pixmap's rect. - pub(crate) fn rect(&self) -> ScreenIntRect { - self.size.to_screen_int_rect(0, 0) - } - /// Returns the internal data. /// - /// Byteorder: RGBA + /// See the output of [Pixmap::pixel_type] for the pixel format. pub fn data(&self) -> &'a [u8] { self.data } - /// Returns a pixel color. + /// Returns a pixel color, converted to `Color`. /// /// Returns `None` when position is out of bounds. - pub fn pixel(&self, x: u32, y: u32) -> Option { - let idx = self.width().checked_mul(y)?.checked_add(x)?; - self.pixels().get(idx as usize).cloned() - } - - /// Returns a slice of pixels. - pub fn pixels(&self) -> &'a [PremultipliedColorU8] { - bytemuck::cast_slice(self.data()) + pub fn pixel(&self, x: u32, y: u32) -> Option { + if y >= self.height() || x >= self.width() { + return None; + } + let row = &self.data()[y as usize * self.stride..y as usize * self.stride + self.stride]; + match self.pixel_type { + PixelType::Rgba8U => { + let row = bytemuck::cast_slice::<_, PremultipliedColorU8>( + &row[..self.width() as usize * 4], + ); + let color_u8 = row[x as usize]; + Some(color_u8.to_color()) + } + } } - // TODO: add rows() iterator - /// Returns a copy of the pixmap that intersects the `rect`. /// /// Returns `None` when `Pixmap`'s rect doesn't contain `rect`. pub fn clone_rect(&self, rect: IntRect) -> Option { - // TODO: to ScreenIntRect? - - let rect = self.rect().to_int_rect().intersect(&rect)?; - let mut new = Pixmap::new(rect.width(), rect.height())?; - { - let old_pixels = self.pixels(); - let mut new_mut = new.as_mut(); - let new_pixels = new_mut.pixels_mut(); - - // TODO: optimize - for y in 0..rect.height() { - for x in 0..rect.width() { - let old_idx = (y + rect.y() as u32) * self.width() + (x + rect.x() as u32); - let new_idx = y * rect.width() + x; - new_pixels[new_idx as usize] = old_pixels[old_idx as usize]; - } - } - } - - Some(new) + Some(self.subpixmap(rect)?.to_owned()) } /// Encodes pixmap into a PNG data. @@ -411,15 +604,16 @@ impl<'a> PixmapRef<'a> { // Skia uses skcms here, which is somewhat similar to RasterPipeline. // Sadly, we have to copy the pixmap here, because of demultiplication. - // Not sure how to avoid this. + // Not sure how to avoid this. (png::Encoder::stream_writer_with_size?) // TODO: remove allocation let demultiplied_data = self.to_owned().take_demultiplied(); - let mut data = Vec::new(); { let mut encoder = png::Encoder::new(&mut data, self.width(), self.height()); encoder.set_color(png::ColorType::Rgba); - encoder.set_depth(png::BitDepth::Eight); + match self.pixel_type { + PixelType::Rgba8U => encoder.set_depth(png::BitDepth::Eight), + } let mut writer = encoder.write_header()?; writer.write_image_data(&demultiplied_data)?; } @@ -434,6 +628,22 @@ impl<'a> PixmapRef<'a> { std::fs::write(path, data)?; Ok(()) } + + /// Returns a reference to the pixmap region that intersects the `rect`. + /// + /// Returns `None` when `Pixmap`'s rect doesn't contain `rect`. + pub fn subpixmap(&self, rect: IntRect) -> Option> { + let rect = self.size.to_int_rect(0, 0).intersect(&rect)?; + let offset = rect.top() as usize * self.stride + + rect.left() as usize * usize::from(self.pixel_type.size()); + + Some(PixmapRef { + size: rect.size(), + stride: self.stride, + data: &self.data[offset..], + pixel_type: self.pixel_type, + }) + } } impl core::fmt::Debug for PixmapRef<'_> { @@ -442,6 +652,8 @@ impl core::fmt::Debug for PixmapRef<'_> { .field("data", &"...") .field("width", &self.size.width()) .field("height", &self.size.height()) + .field("stride", &self.stride()) + .field("pixel_type", &self.pixel_type()) .finish() } } @@ -450,38 +662,79 @@ impl core::fmt::Debug for PixmapRef<'_> { /// /// Can be created from `Pixmap` or from a user provided data. /// -/// The data is not aligned, therefore width == stride. +/// This may have any stride >= width * sizeof(one pixel of PixelType) #[derive(PartialEq)] pub struct PixmapMut<'a> { data: &'a mut [u8], size: IntSize, + stride: usize, + pixel_type: PixelType, } impl<'a> PixmapMut<'a> { /// Creates a new `PixmapMut` from bytes. /// - /// The size must be at least `size.width() * size.height() * BYTES_PER_PIXEL`. + /// The size must be at least `size.width() * size.height() * 4`. /// Zero size in an error. Width is limited by i32::MAX/4. /// - /// The `data` is assumed to have premultiplied RGBA pixels (byteorder: RGBA). + /// The `data` is assumed to have premultiplied RGBA pixels (see [PixelType::Rgba8U]). pub fn from_bytes(data: &'a mut [u8], width: u32, height: u32) -> Option { let size = IntSize::from_wh(width, height)?; - let data_len = data_len_for_size(size)?; + let stride = min_row_bytes(size, PixelType::Rgba8U)?; + let data_len = data_len_for_size(size, stride.get())?; if data.len() < data_len { return None; } - Some(PixmapMut { data, size }) + Some(PixmapMut { + data, + size, + stride: stride.get(), + pixel_type: PixelType::Rgba8U, + }) + } + + /// Creates a new `PixmapMut` from bytes. + /// + /// The size must be at least `size.width() * size.height() * pixel_type.size()`. + /// Zero size in an error. Width is limited by i32::MAX/pixel_type.size(). + /// + /// The `data` is assumed to have pixels following the given [PixelType]); it + /// must have the minimum alignment of the [PixelType]). + pub fn from_bytes_with_type( + data: &'a mut [u8], + width: u32, + height: u32, + stride: usize, + pixel_type: PixelType, + ) -> Option { + let size = IntSize::from_wh(width, height)?; + let min_stride = min_row_bytes(size, pixel_type)?; + if min_stride.get() > stride { + return None; + } + let data_len = data_len_for_size(size, stride)?; + if data.len() != data_len { + return None; + } + + if data.as_ptr() as usize % usize::from(pixel_type.alignment()) != 0 { + return None; + } + + Some(PixmapMut { + data, + size, + stride, + pixel_type, + }) } /// Creates a new `Pixmap` from the current data. /// /// Clones the underlying data. pub fn to_owned(&self) -> Pixmap { - Pixmap { - data: self.data.to_vec(), - size: self.size, - } + self.as_ref().to_owned() } /// Returns a container that references Pixmap's data. @@ -489,6 +742,8 @@ impl<'a> PixmapMut<'a> { PixmapRef { data: self.data, size: self.size, + stride: self.stride, + pixel_type: self.pixel_type, } } @@ -504,6 +759,18 @@ impl<'a> PixmapMut<'a> { self.size.height() } + /// Returns pixmap's pixel type + #[inline] + pub fn pixel_type(&self) -> PixelType { + self.pixel_type + } + + /// Returns pixmap's stride (spacing in bytes between the starts of rows) + #[inline] + pub fn stride(&self) -> usize { + self.stride + } + /// Returns pixmap's size. pub(crate) fn size(&self) -> IntSize { self.size @@ -511,45 +778,47 @@ impl<'a> PixmapMut<'a> { /// Fills the entire pixmap with a specified color. pub fn fill(&mut self, color: Color) { - let c = color.premultiply().to_color_u8(); - for p in self.pixels_mut() { - *p = c; + let stride = self.stride; + let row_len = (self.width() as usize) * usize::from(self.pixel_type.size()); + + match self.pixel_type { + PixelType::Rgba8U => { + let c = color.premultiply().to_color_u8(); + for y in 0..self.height() { + let row = &mut self.data[y as usize * stride..y as usize * stride + row_len]; + for p in bytemuck::cast_slice_mut(row) { + *p = c; + } + } + } } } /// Returns the mutable internal data. /// - /// Byteorder: RGBA + /// See the output of [Pixmap::pixel_type] for the pixel format. pub fn data_mut(&mut self) -> &mut [u8] { self.data } - /// Returns a mutable slice of pixels. - pub fn pixels_mut(&mut self) -> &mut [PremultipliedColorU8] { - bytemuck::cast_slice_mut(self.data_mut()) - } - - /// Creates `SubPixmapMut` that contains the whole `PixmapMut`. - pub(crate) fn as_subpixmap(&mut self) -> SubPixmapMut<'_> { - SubPixmapMut { - size: self.size(), - real_width: self.width() as usize, - data: self.data, - } - } + // /// Returns a mutable slice of pixels. + // pub fn pixels_mut(&mut self) -> &mut [PremultipliedColorU8] { + // bytemuck::cast_slice_mut(self.data_mut()) + // } /// Returns a mutable reference to the pixmap region that intersects the `rect`. /// /// Returns `None` when `Pixmap`'s rect doesn't contain `rect`. - pub(crate) fn subpixmap(&mut self, rect: IntRect) -> Option> { + pub fn subpixmap(&mut self, rect: IntRect) -> Option> { let rect = self.size.to_int_rect(0, 0).intersect(&rect)?; - let row_bytes = self.width() as usize * BYTES_PER_PIXEL; - let offset = rect.top() as usize * row_bytes + rect.left() as usize * BYTES_PER_PIXEL; + let offset = rect.top() as usize * self.stride + + rect.left() as usize * usize::from(self.pixel_type.size()); - Some(SubPixmapMut { + Some(PixmapMut { size: rect.size(), - real_width: self.width() as usize, + stride: self.stride, data: &mut self.data[offset..], + pixel_type: self.pixel_type, }) } } @@ -560,46 +829,27 @@ impl core::fmt::Debug for PixmapMut<'_> { .field("data", &"...") .field("width", &self.size.width()) .field("height", &self.size.height()) + .field("stride", &self.stride()) + .field("pixel_type", &self.pixel_type()) .finish() } } -/// A `PixmapMut` subregion. -/// -/// Unlike `PixmapMut`, contains `real_width` which references the parent `PixmapMut` width. -/// This way we can operate on a `PixmapMut` subregion without reallocations. -/// Primarily required because of `DrawTiler`. -/// -/// We cannot implement it in `PixmapMut` directly, because it will brake `fill`, `data_mut` -/// `pixels_mut` and other similar methods. -/// This is because `SubPixmapMut.data` references more "data" than it actually allowed to access. -/// On the other hand, `PixmapMut.data` can access all it's data and it's stored linearly. -pub struct SubPixmapMut<'a> { - pub data: &'a mut [u8], - pub size: IntSize, - pub real_width: usize, -} - /// Returns minimum bytes per row as usize. /// /// Pixmap's maximum value for row bytes must fit in 31 bits. -fn min_row_bytes(size: IntSize) -> Option { +fn min_row_bytes(size: IntSize, pixel_type: PixelType) -> Option { let w = i32::try_from(size.width()).ok()?; - let w = w.checked_mul(BYTES_PER_PIXEL as i32)?; + let w = w.checked_mul(pixel_type.size() as i32)?; NonZeroUsize::new(w as usize) } -/// Returns storage size required by pixel array. -fn compute_data_len(size: IntSize, row_bytes: usize) -> Option { - let h = size.height().checked_sub(1)?; - let h = (h as usize).checked_mul(row_bytes)?; - - let w = (size.width() as usize).checked_mul(BYTES_PER_PIXEL)?; - - h.checked_add(w) -} - -fn data_len_for_size(size: IntSize) -> Option { - let row_bytes = min_row_bytes(size)?; - compute_data_len(size, row_bytes.get()) +/// Returns storage size required by pixel array. Assumes stride is valid. +/// +/// The caller must have validated `stride` beforehand. +/// +/// For simplicity, this requires the trailing row include the entire padding +/// up to 'stride'. +fn data_len_for_size(size: IntSize, stride: usize) -> Option { + (size.height() as usize).checked_mul(stride) } diff --git a/tests/images/pixmap/overlay-8u-on-8u.png b/tests/images/pixmap/overlay-8u-on-8u.png new file mode 100644 index 0000000000000000000000000000000000000000..b83cb92986701f063ae79fd29725dfbba5a1bc0b GIT binary patch literal 2330 zcmXw)2~^VA7sqi+q+D`bFeS+(_bn|G8u|SIMOJY`q+Al0mN8OEmNGFd(~OF7151$6 zv_&1wPBl(xT4`lwmgQ*UzoN_TqedoMl7L#m>ScQPW zV5XEXQY19@Ee*He1aUui{7yhU_=CwssGl-<`?*pS-kz_fsEMj zQ~@j|#yXgjBCU?e5GRU`dk|&n~h8xi%oz8su8xxwd#N5(40)=2fi^ z4PQ?VPt8|Ctsy65*nu2+Y7U(z3C`t>FG=}L3P2>p z1EMnuWxZOYP$*lJ)#`eYsHX9>mx86m|{d77ZLHqUS!Pf6@U~Arb$vXuDoDY#r~OdA5!g z^L_Pq@V;NYU3TkLjAWt=(>lc~0*hbgGc9 z-N0oREI*~Oc>l-jjX}LndKF$sgq{!8`1IS6X?SNH(2$@%ZGospy{Hrn7RZ`p?`bxZ zl$S4q&8KB=R$SX=*k3!j(uf!N(cOq>X+=m7b+h1NhO2~ZkZ7^Sbnc#quh}o|k8`pM zM$g&oZ*3bJ>tC!q|9JX}ia9+?qw=f0u~Sp@vbyn&@q#0kiE}l(gqxXpO?1agv@F)` zV*;edive?zVDqWdHD6B~|0#d=G(V-oC4Cn&$3L7bUp48=l^?Y1{)p?Z@O}9nAoYTI zJ^8MgB4>I>kI2NpX!(8F4fW!Ng}jRrbzB=vtm2~h1zzLr)T|Ss?O3x;{@%tI0>CC; z?1`ipr$j8sJ`8&n=Z#`iQ@dw%XNAEIUlqd-COXHXCUiM@v@zb+R!Nb0jStenW0IX-FGkR^z^w06~(ZrKe<1>c}wbz;#FcDLG zSjm|oq@ttpn?of>WR63m;(E*-Ypi#R^;Y+?SL~d4!TfyIwT;JCAF=cp8>quI-!5sc zqag!Aqp{D*0bcPBy_aWF*4LC)`YYylPY+0xM>H^*%H~Tl{n$xkUhey~S8P&PAZcp^ z=+-vfp{wfqiZIz`Y7w@^KfC9Rz@hMwG-_h1auc4JBpGV|r^t7(qVDf&Gd_p7qC`Hg zc=8cjFQeDMyB%YcHE{jTmXRCWEDyIg^ZQ7{nSbkD?%+6moUc~vSX<>5b_Y@Bhq^3l z*SO<;Ceh$IeI75&uQDIj*1p0PTDSdlO-pGGSl#A@9VJ%45jSM*NgNH@5cg&1)((Ej z!i;(5$uk@)t;U>|Qj|odwHkVHoSafT=)F;H-DUHZ!Y_=CyrN0@M4kM9Ye#BZ3h{a`&lmIl*M0fDRhZv7<8s1E^SwW2Xe;!! z85GFXim}{#7utSoz3`}~De+~)hI{4D2_2!Q- zY)264?}VK>J`E|hlMcp$?LI-vsKrvV`xr8C9)_d4yTQ1AI^VO-%O1<-WmMj0%v0%j z?bDgYA@uQI<-{c40@RtZe%R69u2OsY@9Q5OnLmFn*5%wDg4a1$8!Mroqrtl6F^A(| z8*jE-=$AgZ|BhqKesw7V?z`oD^MOBeU(t^oDoR`V#rugArP$9arRY1X>o-xrydUvR z{Y>|tp_?f5ndj347xaDF{u+RJ7+%Tf4D8wnXVrE71SjpO*emqJp$q3ge_O-fo{Y7c zc3ZvGx$0-0GR;NVo59uVoK6WzduHP9vEYjkF>Ntb(j<#+m)N>cJ z@UZyT^4%w1y|G?Apf0;BvkEKwqM;+|vT(b~Ck0^JeI^D~jXv$~+Q}Q#v(}CNGK$yc z-Bn<(8;xsBORmiAwfwU~UFz@I!W+>m*DbU}>c-T_w~OmQyx#1ygok$5yeGFEFX=~K z0`28Zw(Dqs%TRex%(Qs|;?kz!5FjwwF=Z2K#E||x39;p{!ENNb!!v;BTwTe694n+Vv^IzFlWo9sc-RhAku8-hiJfD0{wL`N(htGOh`KTe`wwE ASO5S3 literal 0 HcmV?d00001 diff --git a/tests/integration/fill.rs b/tests/integration/fill.rs index 4737f48..fd54a9d 100644 --- a/tests/integration/fill.rs +++ b/tests/integration/fill.rs @@ -141,7 +141,7 @@ fn tiny_float_rect() { pixmap.fill_rect(rect, &paint, Transform::identity(), None); assert_eq!( - pixmap.pixels(), + bytemuck::cast_slice::<_, PremultipliedColorU8>(pixmap.data()), &[ ColorU8::from_rgba(0, 0, 0, 0).premultiply(), ColorU8::from_rgba(0, 0, 0, 0).premultiply(), @@ -170,7 +170,7 @@ fn tiny_float_rect_aa() { pixmap.fill_rect(rect, &paint, Transform::identity(), None); assert_eq!( - pixmap.pixels(), + bytemuck::cast_slice::<_, PremultipliedColorU8>(pixmap.data()), &[ ColorU8::from_rgba(0, 0, 0, 0).premultiply(), ColorU8::from_rgba(0, 0, 0, 0).premultiply(), diff --git a/tests/integration/pixmap.rs b/tests/integration/pixmap.rs index 1dda753..348f21a 100644 --- a/tests/integration/pixmap.rs +++ b/tests/integration/pixmap.rs @@ -44,6 +44,7 @@ fn clone_rect_2() { assert_eq!(part, expected); } + #[test] fn clone_rect_out_of_bound() { let mut pixmap = Pixmap::new(200, 200).unwrap(); @@ -70,7 +71,8 @@ fn fill() { let c = Color::from_rgba8(50, 100, 150, 200); let mut pixmap = Pixmap::new(10, 10).unwrap(); pixmap.fill(c); - assert_eq!(pixmap.pixel(1, 1).unwrap(), c.premultiply().to_color_u8()); + let pixels = bytemuck::cast_slice::<_, PremultipliedColorU8>(pixmap.data()); + assert!(pixels.iter().all(|p| { *p == c.premultiply().to_color_u8() })); } #[test] @@ -171,3 +173,75 @@ fn draw_pixmap_opacity() { let expected = Pixmap::load_png("tests/images/canvas/draw-pixmap-opacity.png").unwrap(); assert_eq!(pixmap, expected); } + +#[test] +fn type_overlay() { + let stacks: [(PixelType, PixelType, &'static str); _] = [ + (PixelType::Rgba8U, PixelType::Rgba8U, "8u-on-8u"), + ]; + + for (base_type, over_type, name) in stacks { + // Ensure image is not tightly packed, so that indexing calculations are exercised + let over_stride = + 100 * usize::from(over_type.size()) + 19 * usize::from(over_type.alignment()); + let mut over = Pixmap::new_with_type(100, 100, over_stride, over_type).unwrap(); + + let mut paint = Paint::default(); + paint.set_color_rgba8(50, 127, 150, 200); + paint.anti_alias = true; + paint.shader = LinearGradient::new( + Point::from_xy(10.0, 10.0), + Point::from_xy(90.0, 90.0), + vec![ + GradientStop::new(0.0, Color::from_rgba8(50, 127, 150, 200)), + GradientStop::new(1.0, Color::from_rgba8(220, 140, 75, 180)), + ], + SpreadMode::Pad, + Transform::identity(), + ) + .unwrap(); + + let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 90.0, 90.0).unwrap()); + over.fill_path( + &path, + &paint, + FillRule::Winding, + Transform::identity(), + None, + ); + + let base_stride = 200 * usize::from(base_type.size()) + usize::from(base_type.alignment()); + let mut pixmap = Pixmap::new_with_type(200, 200, base_stride, base_type).unwrap(); + + // Overlay with rotation + let transform = Transform::from_rotate(45.0) + .post_translate(50.0, 15.0) + .post_scale(2.0, 2.0); + + let mut paint = Paint::default(); + paint.shader = Pattern::new( + over.as_ref(), + SpreadMode::Pad, // Pad, otherwise we will get weird borders overlap. + FilterQuality::Bilinear, + 0.9, + transform, + ); + paint.blend_mode = BlendMode::Modulate; + paint.anti_alias = true; + paint.colorspace = ColorSpace::default(); + + pixmap.fill(Color::from_rgba(0.4, 0.5, 0.6, 0.9).unwrap()); + pixmap.fill_path( + &PathBuilder::from_circle(100.0, 100.0, 80.0).unwrap(), + &paint, + FillRule::Winding, + Transform::identity(), + None, + ); + + let expected = + Pixmap::load_png(format!("tests/images/pixmap/overlay-{}.png", name)).unwrap(); + let packed = pixmap.as_ref().to_owned(); + assert_eq!(expected, packed); + } +} diff --git a/tests/integration/png.rs b/tests/integration/png.rs index d7761b8..ee23f50 100644 --- a/tests/integration/png.rs +++ b/tests/integration/png.rs @@ -3,30 +3,30 @@ use tiny_skia::*; #[test] fn decode_grayscale() { let pixmap = Pixmap::load_png("tests/images/pngs/grayscale.png").unwrap(); - assert_eq!(pixmap.pixel(10, 10).unwrap(), ColorU8::from_rgba(255, 255, 255, 255).premultiply()); - assert_eq!(pixmap.pixel(50, 50).unwrap(), ColorU8::from_rgba(0, 0, 0, 255).premultiply()); + assert_eq!(pixmap.pixel(10, 10).unwrap(), ColorU8::from_rgba(255, 255, 255, 255).premultiply().to_color()); + assert_eq!(pixmap.pixel(50, 50).unwrap(), ColorU8::from_rgba(0, 0, 0, 255).premultiply().to_color()); } #[test] fn decode_grayscale_alpha() { let pixmap = Pixmap::load_png("tests/images/pngs/grayscale-alpha.png").unwrap(); - assert_eq!(pixmap.pixel(10, 10).unwrap(), ColorU8::from_rgba(0, 0, 0, 0).premultiply()); - assert_eq!(pixmap.pixel(50, 50).unwrap(), ColorU8::from_rgba(0, 0, 0, 255).premultiply()); + assert_eq!(pixmap.pixel(10, 10).unwrap(), ColorU8::from_rgba(0, 0, 0, 0).premultiply().to_color()); + assert_eq!(pixmap.pixel(50, 50).unwrap(), ColorU8::from_rgba(0, 0, 0, 255).premultiply().to_color()); } #[test] fn decode_rgb() { let pixmap = Pixmap::load_png("tests/images/pngs/rgb.png").unwrap(); - assert_eq!(pixmap.pixel(10, 10).unwrap(), ColorU8::from_rgba(255, 255, 255, 255).premultiply()); - assert_eq!(pixmap.pixel(50, 50).unwrap(), ColorU8::from_rgba(36, 191, 49, 255).premultiply()); + assert_eq!(pixmap.pixel(10, 10).unwrap(), ColorU8::from_rgba(255, 255, 255, 255).premultiply().to_color()); + assert_eq!(pixmap.pixel(50, 50).unwrap(), ColorU8::from_rgba(36, 191, 49, 255).premultiply().to_color()); } #[test] fn decode_rgba() { let pixmap = Pixmap::load_png("tests/images/pngs/rgba.png").unwrap(); - assert_eq!(pixmap.pixel(10, 10).unwrap(), ColorU8::from_rgba(0, 0, 0, 0).premultiply()); - assert_eq!(pixmap.pixel(25, 25).unwrap(), ColorU8::from_rgba(161, 227, 165, 108).premultiply()); - assert_eq!(pixmap.pixel(50, 50).unwrap(), ColorU8::from_rgba(33, 190, 47, 252).premultiply()); + assert_eq!(pixmap.pixel(10, 10).unwrap(), ColorU8::from_rgba(0, 0, 0, 0).premultiply().to_color()); + assert_eq!(pixmap.pixel(25, 25).unwrap(), ColorU8::from_rgba(161, 227, 165, 108).premultiply().to_color()); + assert_eq!(pixmap.pixel(50, 50).unwrap(), ColorU8::from_rgba(33, 190, 47, 252).premultiply().to_color()); } // TODO: test encoding, somehow From b3ae97e054ce8f03b053fcaa80123ea09465888d Mon Sep 17 00:00:00 2001 From: M Stoeckl Date: Sun, 30 Aug 2026 07:17:56 -0400 Subject: [PATCH 3/3] Implement aligned backing memory for Pixmap This will be needed to allocate memory for future PixelTypes which have alignment constraints. --- src/lib.rs | 2 +- src/pixmap.rs | 90 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 57f921e..4508184 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,7 @@ pub use color::{Color, ColorSpace, ColorU8, PremultipliedColor, PremultipliedCol pub use color::{ALPHA_OPAQUE, ALPHA_TRANSPARENT, ALPHA_U8_OPAQUE, ALPHA_U8_TRANSPARENT}; pub use mask::{Mask, MaskType}; pub use painter::{FillRule, Paint}; -pub use pixmap::{PixelType, Pixmap, PixmapMut, PixmapRef}; +pub use pixmap::{AlignedMemory, PixelType, Pixmap, PixmapMut, PixmapRef}; pub use shaders::{FilterQuality, GradientStop, PixmapPaint, SpreadMode}; pub use shaders::{LinearGradient, Pattern, RadialGradient, Shader, SweepGradient}; diff --git a/src/pixmap.rs b/src/pixmap.rs index 72c8e23..47707b5 100644 --- a/src/pixmap.rs +++ b/src/pixmap.rs @@ -4,6 +4,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; @@ -50,12 +51,34 @@ impl PixelType { } } +/// Utility type for an owned buffer which may have a particular alignment +/// constraint, used by [Pixmap]. +/// +/// [PixelType]s with alignment requirements greater than 1 cannot be stored +/// directly in a [`Box<[u8]>`], because Rust's allocation rules do not guarantee +/// that [`Box<[u8]>`]'s data is aligned to a multiple of any value other than 1, +/// and allocator implementations (for example, a bump allocator sharded by +/// alignment requirement) could indeed produce [`Box<[u8]>`]s with memory address +/// value ≡ 1 mod 2. +/// +/// Because strides need not be a multiple of the pixel size, transmuting the +/// entire contents to a slice of pixels will not work unless you know the Pixmap +/// that produced this was tightly packed (which [Pixmap::new] does by default). +/// +/// If a new [PixelType] with a higher alignment requirement is added, this enum +/// will gain an option like `Align2`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AlignedMemory { + /// Used for [PixelType::Rgba8U] + Align1(Box<[u8]>), +} + /// A container that owns premultiplied RGBA pixels. /// /// The data is only guaranteed to be aligned to match the PixelType. #[derive(Clone, PartialEq)] pub struct Pixmap { - data: Vec, + data: AlignedMemory, size: IntSize, /// The number of bytes between the starts of two rows stride: usize, @@ -78,8 +101,10 @@ impl Pixmap { // We cannot check that allocation was successful yet. // We have to wait for https://github.com/rust-lang/rust/issues/48043 + let data = AlignedMemory::Align1(vec![0; data_len].into_boxed_slice()); + Some(Pixmap { - data: vec![0; data_len], + data, size, stride: stride.get(), pixel_type: PixelType::Rgba8U, @@ -93,11 +118,6 @@ impl Pixmap { /// Zero size in an error. /// /// Pixmap's width is limited by i32::MAX/pix_type.size(). - /// - /// This may arbitrarily fail with allocators that do not align - /// Vec to multiples of pix_type.alignment() in practice. For - /// reliable construction, manage your own memory and use PixmapMut - /// or PixmapRef. pub fn new_with_type( width: u32, height: u32, @@ -110,17 +130,8 @@ impl Pixmap { return None; } let data_len = data_len_for_size(size, stride)?; - let data: Vec = vec![0; data_len]; - - // TODO: Rust does not provide a way to reliably produce an aligned - // Vec, as its allocators may make type alignment dependent decisions. - // If this becomes an issue in practice, Pixmap could overallocate by - // pixel_type.alignment() and offset any access to the data accordingly; - // but this will break methods like 'take()'. - if data.as_ptr() as usize % usize::from(pixel_type.alignment()) != 0 { - return None; - } + let data = AlignedMemory::Align1(vec![0; data_len].into_boxed_slice()); Some(Pixmap { data, size, @@ -136,8 +147,8 @@ impl Pixmap { /// Zero size in an error. /// /// Pixmap's width is limited by i32::MAX/pix_type.size(). - pub fn from_vec_with_type( - data: Vec, + pub fn from_mem_with_type( + data: AlignedMemory, width: u32, height: u32, stride: usize, @@ -149,11 +160,10 @@ impl Pixmap { return None; } let data_len = data_len_for_size(size, stride)?; - if data.len() != data_len { - return None; - } - if data.as_ptr() as usize % usize::from(pixel_type.alignment()) != 0 { + let AlignedMemory::Align1(buf) = &data; + + if buf.len() != data_len { return None; } @@ -179,7 +189,7 @@ impl Pixmap { } Some(Pixmap { - data, + data: AlignedMemory::Align1(data.into_boxed_slice()), size, stride: stride.get(), pixel_type: PixelType::Rgba8U, @@ -307,21 +317,23 @@ impl Pixmap { /// Returns a container that references Pixmap's data. pub fn as_ref(&self) -> PixmapRef<'_> { + let (size, stride, pixel_type) = (self.size, self.stride, self.pixel_type); PixmapRef { - data: &self.data, - size: self.size, - stride: self.stride, - pixel_type: self.pixel_type, + data: self.data(), + size, + stride, + pixel_type, } } /// Returns a container that references Pixmap's data. pub fn as_mut(&mut self) -> PixmapMut<'_> { + let (size, stride, pixel_type) = (self.size, self.stride, self.pixel_type); PixmapMut { - data: &mut self.data, - size: self.size, - stride: self.stride, - pixel_type: self.pixel_type, + data: self.data_mut(), + size, + stride, + pixel_type, } } @@ -364,14 +376,18 @@ impl Pixmap { /// /// Byteorder: RGBA pub fn data(&self) -> &[u8] { - self.data.as_slice() + match &self.data { + AlignedMemory::Align1(mem) => mem, + } } /// Returns the mutable internal data. /// /// Byteorder: RGBA pub fn data_mut(&mut self) -> &mut [u8] { - self.data.as_mut_slice() + match &mut self.data { + AlignedMemory::Align1(mem) => mem, + } } /// Returns a pixel color, converted to `Color`. @@ -384,14 +400,14 @@ impl Pixmap { /// Consumes the internal data. /// /// See [PixelType] and [Self::stride()] for the data layout. - pub fn take(self) -> Vec { + pub fn take(self) -> AlignedMemory { self.data } /// Consumes the pixmap and returns the internal data as demultiplied RGBA bytes. /// /// See [PixelType] for the data layout. - pub fn take_demultiplied(mut self) -> Vec { + pub fn take_demultiplied(mut self) -> AlignedMemory { // Demultiply alpha. // // RasterPipeline is 15% faster here, but produces slightly different results @@ -606,7 +622,7 @@ impl<'a> PixmapRef<'a> { // Sadly, we have to copy the pixmap here, because of demultiplication. // Not sure how to avoid this. (png::Encoder::stream_writer_with_size?) // TODO: remove allocation - let demultiplied_data = self.to_owned().take_demultiplied(); + let AlignedMemory::Align1(demultiplied_data) = self.to_owned().take_demultiplied(); let mut data = Vec::new(); { let mut encoder = png::Encoder::new(&mut data, self.width(), self.height());