Skip to main content

dinamika_cpu/pixmap/
mod.rs

1//! [`Pixmap`] — a premultiplied RGBA pixel buffer
2//! and drawing operations on top of it.
3
4use crate::color::{Color, PremultipliedColor, PremultipliedColorU8};
5use crate::geometry::{Point, Transform};
6use crate::paint::{blend, BlendMode, Paint, Shader};
7use crate::path::stroke::{build_stroke, Stroke};
8use crate::path::{FillRule, Path};
9use crate::raster::mask::Mask;
10use crate::raster::Rasterizer;
11use crate::text::Font;
12
13mod decode;
14mod encode;
15
16/// The accuracy of curve splitting in pixels
17/// (before taking into account the transformation scale).
18pub(crate) const FLATTEN_TOLERANCE: f32 = 0.1;
19
20/// Bitmap: `width × height` RGBA pixels, premultiplied alpha.
21#[derive(Clone)]
22pub struct Pixmap {
23    width: u32,
24    height: u32,
25    data: Vec<u8>,
26}
27
28impl Pixmap {
29    /// Creates a transparent image. `None` for zero dimensions or overflow.
30    pub fn new(width: u32, height: u32) -> Option<Pixmap> {
31        if width == 0 || height == 0 {
32            return None;
33        }
34        let len = (width as usize).checked_mul(height as usize)?.checked_mul(4)?;
35        Some(Pixmap { width, height, data: vec![0; len] })
36    }
37
38    /// Getter for getting the width size
39    #[inline]
40    pub fn width(&self) -> u32 {
41        self.width
42    }
43
44    /// Getter for getting the height size
45    #[inline]
46    pub fn height(&self) -> u32 {
47        self.height
48    }
49
50    /// Raw RGBA bytes (premultiplied), 4 per pixel.
51    #[inline]
52    pub fn data(&self) -> &[u8] {
53        &self.data
54    }
55
56    /// Mutable access to raw RGBA bytes (premultiplied), 4 per pixel.
57    #[inline]
58    pub fn data_mut(&mut self) -> &mut [u8] {
59        &mut self.data
60    }
61
62    /// Takes the buffer, consuming the pixmap.
63    pub fn take(self) -> Vec<u8> {
64        self.data
65    }
66
67    /// Pixel color (premultiplied). `None` outside the image.
68    pub fn pixel(&self, x: u32, y: u32) -> Option<PremultipliedColorU8> {
69        if x >= self.width || y >= self.height {
70            return None;
71        }
72        let i = (y as usize * self.width as usize + x as usize) * 4;
73        Some(PremultipliedColorU8::from_rgba_unchecked(
74            self.data[i],
75            self.data[i + 1],
76            self.data[i + 2],
77            self.data[i + 3],
78        ))
79    }
80
81    /// Fills the image completely with color (without blending).
82    pub fn fill(&mut self, color: Color) {
83        let p = color.premultiply().to_color_u8();
84        for px in self.data.chunks_exact_mut(4) {
85            px[0] = p.red();
86            px[1] = p.green();
87            px[2] = p.blue();
88            px[3] = p.alpha();
89        }
90    }
91
92    /// Fills a path with a brush according to the specified rule.
93    ///
94    /// If a clipping mask is specified, the coverage of each
95    /// pixel is multiplied by its value, so the shape is visible
96    /// only where the mask is non-zero—for example, within the rounded
97    /// contour of the parent. `None` disables clipping.
98    pub fn fill_path(
99        &mut self,
100        path: &Path,
101        paint: &Paint,
102        fill_rule: FillRule,
103        transform: Transform,
104        clip: Option<&Mask>,
105    ) {
106        let tol = FLATTEN_TOLERANCE / transform.max_scale().max(1e-3);
107        let contours = path.to_contours(transform, tol);
108        // Each contour is implicitly closed during the fill.
109        let polys: Vec<&[Point]> = contours.iter().map(|c| c.points.as_slice()).collect();
110        self.fill_polys(&polys, paint, fill_rule, clip);
111    }
112
113    /// Brushes a path.
114    ///
115    /// If a clipping mask (see [`Pixmap::fill_path`]) is specified, the coverage
116    /// is multiplied by its value; `None` disables clipping.
117    ///
118    /// # Limitation: Non-uniform scaling and bevel
119    /// First, the path is converted to screen coordinates, and then a stroke
120    /// of constant width [`Stroke::width`] multiplied by a single
121    /// scalar—[`Transform::max_scale`]—is constructed from it.
122    /// Therefore, rotation and *uniform* scale are handled correctly, but
123    /// non-uniform scaling (e.g., `scale(2.0, 1.0)`) or bevel will
124    /// produce a uniform (circular) thickness instead of the expected elliptical one.
125    /// A correct anisotropic stroke would require constructing the path
126    /// before the transformation and then transforming it.
127    pub fn stroke_path(
128        &mut self,
129        path: &Path,
130        paint: &Paint,
131        stroke: &Stroke,
132        transform: Transform,
133        clip: Option<&Mask>,
134    ) {
135        let scale = transform.max_scale().max(1e-3);
136        let tol = FLATTEN_TOLERANCE / scale;
137        let contours = path.to_contours(transform, tol);
138
139        // The outline is constructed in screen coordinates; arc precision is in pixels.
140        let polys = build_stroke(&contours, &scaled_stroke(stroke, scale), FLATTEN_TOLERANCE);
141        let refs: Vec<&[Point]> = polys.iter().map(|p| p.as_slice()).collect();
142
143        // Stamps are combined according to the non-zero bypass rule.
144        self.fill_polys(&refs, paint, FillRule::NonZero, clip);
145    }
146
147    /// Draws a string by filling its glyph outlines with a brush.
148    ///
149    /// A convenience wrapper over [`Font::text_path`] + [`Pixmap::fill_path`]:
150    /// the outline of `text` is built at em `size` (in pixels) with the first
151    /// baseline origin at `(x, y)`, then filled with `paint` using the non-zero
152    /// winding rule — the rule TrueType/OpenType outlines are authored for.
153    /// `transform` and `clip` behave exactly as in [`Pixmap::fill_path`]; for
154    /// example, pass [`Transform::from_rotate_at`] to draw rotated text or a
155    /// [`Mask`] to clip it. Whitespace-only or empty `text` draws nothing.
156    ///
157    /// See the [`text`](crate::Font) module for the (deliberately minimal)
158    /// layout rules — single line per `\n`, no kerning or shaping.
159    // Positional API in the spirit of `fill_path`/`stroke_path`; text just needs
160    // the extra string/size/origin inputs.
161    #[allow(clippy::too_many_arguments)]
162    pub fn fill_text(
163        &mut self,
164        font: &Font,
165        text: &str,
166        size: f32,
167        x: f32,
168        y: f32,
169        paint: &Paint,
170        transform: Transform,
171        clip: Option<&Mask>,
172    ) {
173        if let Some(path) = font.text_path(text, size, x, y) {
174            self.fill_path(&path, paint, FillRule::NonZero, transform, clip);
175        }
176    }
177
178    /// Overlays the image `src` on top of this one, placing its upper-left
179    /// corner at `(x, y)` (negative coordinates allowed), with an overall
180    /// transparency multiplier of `opacity` (`0..=1`) and blend mode of `blend_mode`.
181    ///
182    /// Only the intersection with the canvas is processed. This is the basic primitive
183    /// of pixmap-on-pixmap compositing: offscreen layers, group subtree transparency
184    /// (where opacity is applied to the rendered result as a whole),
185    /// and sprite overlays with arbitrary blending modes.
186    pub fn draw_pixmap(&mut self, src: &Pixmap, x: i32, y: i32, opacity: f32, blend_mode: BlendMode) {
187        let opacity = opacity.clamp(0.0, 1.0);
188        if opacity <= 0.0 {
189            return;
190        }
191
192        // The destination area is the intersection of the shifted `src` with the canvas.
193        let dx0 = x.max(0);
194        let dy0 = y.max(0);
195        let dx1 = (x + src.width as i32).min(self.width as i32);
196        let dy1 = (y + src.height as i32).min(self.height as i32);
197        if dx1 <= dx0 || dy1 <= dy0 {
198            return;
199        }
200
201        let dst_w = self.width as usize;
202        let src_w = src.width as usize;
203        for dy in dy0..dy1 {
204            let sy = (dy - y) as usize;
205            let dst_row = dy as usize * dst_w * 4;
206            let src_row = sy * src_w * 4;
207            for dx in dx0..dx1 {
208                let si = src_row + (dx - x) as usize * 4;
209                let sa = src.data[si + 3];
210                if sa == 0 && blend_mode == BlendMode::SourceOver {
211                    continue; // transparent source with SourceOver doesn't change anything
212                }
213
214                // Source Premultiplied: A common transparency multiplier is applied to all
215                // four channels, preserving the premultiplication.
216                let s = PremultipliedColor {
217                    r: src.data[si] as f32 / 255.0 * opacity,
218                    g: src.data[si + 1] as f32 / 255.0 * opacity,
219                    b: src.data[si + 2] as f32 / 255.0 * opacity,
220                    a: sa as f32 / 255.0 * opacity,
221                };
222                let di = dst_row + dx as usize * 4;
223                let d = PremultipliedColor {
224                    r: self.data[di] as f32 / 255.0,
225                    g: self.data[di + 1] as f32 / 255.0,
226                    b: self.data[di + 2] as f32 / 255.0,
227                    a: self.data[di + 3] as f32 / 255.0,
228                };
229                let out = blend(blend_mode, s, d).to_color_u8();
230                self.data[di] = out.red();
231                self.data[di + 1] = out.green();
232                self.data[di + 2] = out.blue();
233                self.data[di + 3] = out.alpha();
234            }
235        }
236    }
237
238    /// Rasterizes a set of closed polygons and blends them with the image.
239    ///
240    /// If `clip` is given, the coverage of each pixel is multiplied by the
241    /// clipping mask value.
242    fn fill_polys(
243        &mut self,
244        polys: &[&[Point]],
245        paint: &Paint,
246        fill_rule: FillRule,
247        clip: Option<&Mask>,
248    ) {
249        if polys.is_empty() {
250            return;
251        }
252        // The clipping mask must match the canvas in size, otherwise we drop
253        // it (safer than drawing with an offset).
254        let clip = clip.filter(|m| m.width() == self.width && m.height() == self.height);
255
256        // The shape's bounding box in pixmap coordinates. The points are already
257        // transformed and flattened, so we take the bbox directly from them. The
258        // raster buffer is allocated only for the intersection of the bbox with
259        // the canvas — allocation, zeroing and the pixel pass become O(shape
260        // area) rather than O(canvas area).
261        let (mut min_x, mut min_y) = (f32::INFINITY, f32::INFINITY);
262        let (mut max_x, mut max_y) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
263        for poly in polys {
264            for p in *poly {
265                min_x = min_x.min(p.x);
266                min_y = min_y.min(p.y);
267                max_x = max_x.max(p.x);
268                max_y = max_y.max(p.y);
269            }
270        }
271        if !(min_x <= max_x && min_y <= max_y) {
272            return; // no points (or NaN coordinates)
273        }
274
275        let pm_w = self.width as i32;
276        let pm_h = self.height as i32;
277        // +1 pixel of margin for sub-pixel coverage at the edges.
278        let x0 = (min_x.floor() as i32 - 1).clamp(0, pm_w);
279        let y0 = (min_y.floor() as i32 - 1).clamp(0, pm_h);
280        let x1 = (max_x.ceil() as i32 + 1).clamp(0, pm_w);
281        let y1 = (max_y.ceil() as i32 + 1).clamp(0, pm_h);
282        let bw = (x1 - x0) as usize;
283        let bh = (y1 - y0) as usize;
284        if bw == 0 || bh == 0 {
285            return; // the shape is entirely outside the pixmap
286        }
287
288        let mut rast = Rasterizer::new(x0, y0, bw, bh);
289        for poly in polys {
290            let n = poly.len();
291            if n < 2 {
292                continue;
293            }
294            for i in 0..n {
295                rast.add_line(poly[i], poly[(i + 1) % n]);
296            }
297        }
298
299        let width = self.width as usize;
300        let data = &mut self.data;
301        let shader = &paint.shader;
302        let mode = paint.blend_mode;
303        let anti_alias = paint.anti_alias;
304        let opacity = paint.opacity.clamp(0.0, 1.0);
305
306        // For a solid color the components are constant across the whole shape —
307        // compute them once, and in the loop only the multiplication by the
308        // pixel coverage remains.
309        let solid = match shader {
310            Shader::SolidColor(c) => Some((c.red(), c.green(), c.blue(), c.alpha() * opacity)),
311            _ => None,
312        };
313        // Reused across rows: the batched source colors for a non-solid shader.
314        let mut span: Vec<Color> = Vec::new();
315
316        rast.for_each_row(fill_rule, |y, x_start, coverages| {
317            // Shade the whole run once (one transform map + a per-pixel add)
318            // instead of recomputing the shader per pixel. A solid color needs
319            // no per-pixel shading at all.
320            if solid.is_none() {
321                span.clear();
322                shader.shade_span(x_start, y, coverages.len(), &mut span);
323            }
324
325            for (i, &coverage) in coverages.iter().enumerate() {
326                let x = x_start + i;
327                let mut cov = if anti_alias {
328                    coverage
329                } else if coverage >= 0.5 {
330                    1.0
331                } else {
332                    0.0
333                };
334                // Clipping: multiply the coverage by the mask value.
335                if let Some(m) = clip {
336                    cov *= m.coverage_at(x, y);
337                }
338                if cov <= 0.0 {
339                    continue;
340                }
341
342                let src = if let Some((r, g, b, a_opacity)) = solid {
343                    let alpha = a_opacity * cov;
344                    PremultipliedColor { r: r * alpha, g: g * alpha, b: b * alpha, a: alpha }
345                } else {
346                    let color = span[i];
347                    let alpha = color.alpha() * cov * opacity;
348                    PremultipliedColor {
349                        r: color.red() * alpha,
350                        g: color.green() * alpha,
351                        b: color.blue() * alpha,
352                        a: alpha,
353                    }
354                };
355
356                let idx = (y * width + x) * 4;
357                let dst = PremultipliedColor {
358                    r: data[idx] as f32 / 255.0,
359                    g: data[idx + 1] as f32 / 255.0,
360                    b: data[idx + 2] as f32 / 255.0,
361                    a: data[idx + 3] as f32 / 255.0,
362                };
363
364                let out = blend(mode, src, dst).to_color_u8();
365                data[idx] = out.red();
366                data[idx + 1] = out.green();
367                data[idx + 2] = out.blue();
368                data[idx + 3] = out.alpha();
369            }
370        });
371    }
372}
373
374/// Scales the stroke parameters into screen coordinates.
375///
376/// The width and dash intervals are multiplied by a single scalar `scale`
377/// ([`Transform::max_scale`]), so under non-uniform scaling/shearing the width
378/// comes out isotropic (see the limitation in [`Pixmap::stroke_path`]).
379///
380/// A "hairline" ([`Stroke::is_hairline`], width `<= 0`) is drawn exactly one
381/// device pixel wide regardless of scale, while the dash intervals are still
382/// scaled.
383fn scaled_stroke(stroke: &Stroke, scale: f32) -> Stroke {
384    let width = if stroke.is_hairline() { 1.0 } else { stroke.width * scale };
385    Stroke {
386        width,
387        line_cap: stroke.line_cap,
388        line_join: stroke.line_join,
389        miter_limit: stroke.miter_limit,
390        dash: stroke.dash.iter().map(|&d| d * scale).collect(),
391        dash_offset: stroke.dash_offset * scale,
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::path::PathBuilder;
399    use crate::geometry::Rect;
400
401    #[test]
402    fn fill_then_read_pixel() {
403        let mut pm = Pixmap::new(8, 8).unwrap();
404        pm.fill(Color::from_rgba8(255, 0, 0, 255));
405        let p = pm.pixel(3, 3).unwrap();
406        assert_eq!((p.red(), p.green(), p.blue(), p.alpha()), (255, 0, 0, 255));
407    }
408
409    #[test]
410    fn fill_rect_path_covers_interior() {
411        let mut pm = Pixmap::new(20, 20).unwrap();
412        let path = PathBuilder::from_rect(Rect::from_xywh(4.0, 4.0, 12.0, 12.0).unwrap());
413        let paint = Paint::from_color(Color::from_rgba8(0, 128, 255, 255));
414        pm.fill_path(&path, &paint, FillRule::NonZero, Transform::identity(), None);
415        let inside = pm.pixel(10, 10).unwrap();
416        assert_eq!(inside.alpha(), 255);
417        let outside = pm.pixel(1, 1).unwrap();
418        assert_eq!(outside.alpha(), 0);
419    }
420
421    /// A small shape far from the origin: checks that the offset bbox buffer
422    /// hands out pixels in the correct absolute coordinates.
423    #[test]
424    fn fill_offset_rect_maps_to_absolute_coords() {
425        let mut pm = Pixmap::new(200, 200).unwrap();
426        let path = PathBuilder::from_rect(Rect::from_xywh(150.0, 150.0, 20.0, 20.0).unwrap());
427        let paint = Paint::from_color(Color::from_rgba8(0, 128, 255, 255));
428        pm.fill_path(&path, &paint, FillRule::NonZero, Transform::identity(), None);
429        // Inside the shape — painted.
430        assert_eq!(pm.pixel(160, 160).unwrap().alpha(), 255);
431        // The same relative position near the origin — empty (no offset).
432        assert_eq!(pm.pixel(10, 10).unwrap().alpha(), 0);
433        // Just past the edge of the shape — empty.
434        assert_eq!(pm.pixel(175, 160).unwrap().alpha(), 0);
435    }
436
437    #[test]
438    fn stroke_line_paints_pixels() {
439        let mut pm = Pixmap::new(20, 20).unwrap();
440        let mut b = PathBuilder::new();
441        b.move_to(2.0, 10.0).line_to(18.0, 10.0);
442        let path = b.finish().unwrap();
443        let paint = Paint::from_color(Color::BLACK);
444        let stroke = Stroke { width: 4.0, ..Stroke::default() };
445        pm.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
446        assert_eq!(pm.pixel(10, 10).unwrap().alpha(), 255);
447        assert_eq!(pm.pixel(10, 0).unwrap().alpha(), 0);
448    }
449
450    #[test]
451    fn hairline_stroke_paints_thin_line() {
452        // Width 0 — a "hairline": a one-device-pixel line must be drawn
453        // (previously a zero width produced nothing).
454        let mut pm = Pixmap::new(20, 20).unwrap();
455        let mut b = PathBuilder::new();
456        b.move_to(2.0, 10.0).line_to(18.0, 10.0);
457        let path = b.finish().unwrap();
458        let paint = Paint::from_color(Color::BLACK);
459        let stroke = Stroke { width: 0.0, ..Stroke::default() };
460        pm.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
461        // There is coverage on the line.
462        let mut painted = false;
463        for y in 9..=10 {
464            if pm.pixel(10, y).unwrap().alpha() > 0 {
465                painted = true;
466            }
467        }
468        assert!(painted, "hairline was not drawn");
469    }
470
471    #[test]
472    fn draw_pixmap_offset_places_source() {
473        let mut dst = Pixmap::new(20, 20).unwrap();
474        let mut src = Pixmap::new(5, 5).unwrap();
475        src.fill(Color::from_rgba8(0, 200, 0, 255));
476        dst.draw_pixmap(&src, 10, 10, 1.0, BlendMode::SourceOver);
477        // In the destination area (10..15) — green, outside it — empty.
478        assert_eq!(dst.pixel(12, 12).unwrap().alpha(), 255);
479        assert_eq!(dst.pixel(2, 2).unwrap().alpha(), 0);
480        assert_eq!(dst.pixel(16, 16).unwrap().alpha(), 0);
481    }
482
483    #[test]
484    fn draw_pixmap_negative_offset_clips() {
485        let mut dst = Pixmap::new(10, 10).unwrap();
486        let mut src = Pixmap::new(8, 8).unwrap();
487        src.fill(Color::from_rgba8(255, 0, 0, 255));
488        // Offset past the top-left corner — only the bottom-right part is visible.
489        dst.draw_pixmap(&src, -4, -4, 1.0, BlendMode::SourceOver);
490        assert_eq!(dst.pixel(0, 0).unwrap().alpha(), 255); // src(4,4)
491        assert_eq!(dst.pixel(5, 5).unwrap().alpha(), 0); // outside src
492    }
493
494    #[test]
495    fn draw_pixmap_opacity_halves_alpha() {
496        let mut dst = Pixmap::new(4, 4).unwrap();
497        let mut src = Pixmap::new(4, 4).unwrap();
498        src.fill(Color::from_rgba8(255, 255, 255, 255));
499        dst.draw_pixmap(&src, 0, 0, 0.5, BlendMode::SourceOver);
500        let a = dst.pixel(2, 2).unwrap().alpha();
501        assert!((a as i32 - 128).abs() <= 2, "alpha={a}");
502    }
503
504    #[test]
505    fn fill_clipped_by_rounded_parent() {
506        // The mask is a circle; the large rectangle fill is visible only inside the circle.
507        let clip_path = PathBuilder::from_circle(10.0, 10.0, 8.0).unwrap();
508        let mask =
509            Mask::from_path(20, 20, &clip_path, FillRule::NonZero, true, Transform::identity())
510                .unwrap();
511        let mut pm = Pixmap::new(20, 20).unwrap();
512        let rect = PathBuilder::from_rect(Rect::from_xywh(0.0, 0.0, 20.0, 20.0).unwrap());
513        let paint = Paint::from_color(Color::from_rgba8(255, 0, 0, 255));
514        pm.fill_path(&rect, &paint, FillRule::NonZero, Transform::identity(), Some(&mask));
515        // The center of the circle — painted, the corner outside the circle — empty.
516        assert_eq!(pm.pixel(10, 10).unwrap().alpha(), 255);
517        assert_eq!(pm.pixel(1, 1).unwrap().alpha(), 0);
518    }
519}