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..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::{Pixmap, PixmapMut, PixmapRef, BYTES_PER_PIXEL}; +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/mask.rs b/src/mask.rs index 420a813..2c48f82 100644 --- a/src/mask.rs +++ b/src/mask.rs @@ -14,8 +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::pixmap::SubPixmapMut; -use crate::scan; +use crate::pixmap::PixelType; +use crate::{scan, PremultipliedColorU8}; use crate::{FillRule, PixmapRef}; /// A mask type. @@ -61,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; } } } @@ -157,20 +176,20 @@ impl Mask { }) } - pub(crate) fn as_subpixmap(&mut self) -> SubPixmapMut<'_> { - SubPixmapMut { + pub(crate) fn as_submask_mut(&mut self) -> SubMaskMut<'_> { + SubMaskMut { size: self.size, 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 { + Some(SubMaskMut { size: rect.size(), real_width: self.size.width() as usize, data: &mut self.data[offset..], @@ -294,12 +313,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 +340,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 +421,9 @@ 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 624e00a..4625b50 100644 --- a/src/painter.rs +++ b/src/painter.rs @@ -10,8 +10,8 @@ use tiny_skia_path::{PathStroker, Scalar, SCALAR_MAX}; use crate::geom::ScreenIntRect; use crate::mask::SubMaskRef; -use crate::pipeline::{RasterPipelineBlitter, RasterPipelineBuilder}; -use crate::pixmap::SubPixmapMut; +use crate::pipeline::{GenericPixmapMut, RasterPipelineBlitter, RasterPipelineBuilder}; +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, - &mut self.as_subpixmap(), + GenericPixmapMut::from_pixmap(self), ); } } diff --git a/src/pipeline/blitter.rs b/src/pipeline/blitter.rs index 794c3d0..314f73f 100644 --- a/src/pipeline/blitter.rs +++ b/src/pipeline/blitter.rs @@ -11,32 +11,45 @@ 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::pixmap::SubPixmapMut; +use crate::pipeline::{self, GenericPixmapMut, RasterPipeline, RasterPipelineBuilder}; +use crate::pixmap::{PixelType, PixmapMut}; + +enum PipelineDest<'a, 'b: 'a> { + Mask(&'a mut SubMaskMut<'b>), + Pixmap(&'a mut PixmapMut<'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> { 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; @@ -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,28 @@ 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() { + 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); + } + } + } } } @@ -365,7 +379,7 @@ impl Blitter for RasterPipelineBlitter<'_, '_> { pipeline::AAMaskCtx::default(), mask_ctx, self.pixmap_src, - self.pixmap, + self.pixmap_dst.to_generic_pixmap(), ); } @@ -378,7 +392,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..31be9ed 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, @@ -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/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..21620c4 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -48,15 +48,16 @@ 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}; 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; @@ -155,36 +156,55 @@ 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(), + ]) } } -impl SubPixmapMut<'_> { - #[inline(always)] - pub(crate) fn offset(&self, dx: usize, dy: usize) -> usize { - self.real_width * dy + dx +/// 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) size: IntSize, + pub(crate) stride: usize, +} + +impl<'b> GenericPixmapMut<'b> { + pub(crate) fn from_pixmap<'a: 'b>(pixmap: &'b mut PixmapMut<'a>) -> Self { + let (stride, size) = (pixmap.stride(), pixmap.size()); + Self { + data: pixmap.data_mut(), + stride, + size, + } + } + pub(crate) fn from_mask<'a: 'b>(mask: &'b mut SubMaskMut<'a>) -> Self { + Self { + data: mask.data, + stride: mask.real_width, + size: mask.size, + } } #[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..] + pub(crate) fn offset(&self, dx: usize, dy: usize) -> usize { + self.stride * dy + dx * 4 } #[inline(always)] - pub(crate) fn slice_mask_at_xy(&mut self, dx: usize, dy: usize) -> &mut [u8] { + pub(crate) fn slice_at_xy(&mut self, dx: usize, dy: usize) -> &mut [PremultipliedColorU8] { let offset = self.offset(dx, dy); - &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)] @@ -194,7 +214,11 @@ 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_mut( + self.data[offset..] + .first_chunk_mut::<{ highp::STAGE_WIDTH * 4 }>() + .unwrap(), + ) } #[inline(always)] @@ -204,7 +228,22 @@ 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_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_mask(dx, dy); + &mut self.data[offset..] } #[inline(always)] @@ -213,7 +252,7 @@ impl SubPixmapMut<'_> { 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() } } @@ -426,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; } } @@ -459,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; } } @@ -508,7 +553,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,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, - &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 e576ef2..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; @@ -12,24 +13,76 @@ 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, + } + } +} + +/// 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 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, + data: AlignedMemory, size: IntSize, + /// The number of bytes between the starts of two rows + stride: usize, + pixel_type: PixelType, } impl Pixmap { @@ -42,30 +95,105 @@ 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 + let data = AlignedMemory::Align1(vec![0; data_len].into_boxed_slice()); + + Some(Pixmap { + data, + 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(). + 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 = AlignedMemory::Align1(vec![0; data_len].into_boxed_slice()); Some(Pixmap { - data: vec![0; data_len], + 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_mem_with_type( + data: AlignedMemory, + 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 AlignedMemory::Align1(buf) = &data; + + if buf.len() != data_len { + 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: AlignedMemory::Align1(data.into_boxed_slice()), + size, + stride: stride.get(), + pixel_type: PixelType::Rgba8U, + }) } /// Decodes a PNG data into a `Pixmap`. @@ -93,8 +221,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 +276,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); @@ -184,17 +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, + 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, + data: self.data_mut(), + size, + stride, + pixel_type, } } @@ -210,6 +349,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,63 +369,63 @@ 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. /// /// 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. + /// 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 - pub fn take(self) -> Vec { + /// See [PixelType] and [Self::stride()] for the data layout. + pub fn take(self) -> AlignedMemory { self.data } /// Consumes the pixmap and returns the internal data as demultiplied RGBA bytes. /// - /// Byteorder: RGBA - pub fn take_demultiplied(mut self) -> Vec { + /// See [PixelType] for the data layout. + pub fn take_demultiplied(mut self) -> AlignedMemory { // 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 +444,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 +453,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 +564,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 +620,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 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()); 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 +644,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 +668,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 +678,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 +758,8 @@ impl<'a> PixmapMut<'a> { PixmapRef { data: self.data, size: self.size, + stride: self.stride, + pixel_type: self.pixel_type, } } @@ -504,6 +775,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 +794,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,53 +845,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, -} - -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. -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 0000000..b83cb92 Binary files /dev/null and b/tests/images/pixmap/overlay-8u-on-8u.png differ 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