Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/src/pixmap.rs

16.1 KiB, 35 runs

created by r1870400018:13944, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A buffer of pixels, and the painting done onto it.
2//!
3//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
4//! Anthropic Claude
5
6use crate::{
7 colour::{
8 Gradient,
9 Rgba,
10 },
11 jpeg,
12 path::{
13 Bounds,
14 Path,
15 Pt,
16 TOLERANCE,
17 },
18 png,
19 raster::{
20 FillRule,
21 Raster,
22 },
23 stroke::Stroke,
24 transform::Transform,
25};
26
27use oxedyne_fe2o3_core::prelude::*;
28
29use std::path::Path as FilePath;
30
31// The most pixels a pixmap may hold, a ceiling against a size that is a mistake or an attack.
32// A 16k by 16k image sits just under it.
33pub const MAX_PIXELS: usize = 1 << 28;
34
35/// A rectangular buffer of RGBA pixels, eight bits per channel, with straight alpha.
36///
37/// The layout is row-major, four bytes per pixel, which is what a PNG wants and what a GPU or a
38/// window surface will take without a further copy.
39#[derive(Clone, Debug, PartialEq)]
40pub struct Pixmap {
41 w: usize, // width in pixels
42 h: usize, // height in pixels
43 data: Vec<u8>, // RGBA bytes, w * h * 4 of them
44}
45
46impl Pixmap {
47
48 /// Creates a transparent pixmap of the given size.
49 pub fn new(w: usize, h: usize) -> Outcome<Self> {
50 if w == 0 || h == 0 {
51 return Err(err!(
52 "A pixmap must have a positive size, but {} by {} was asked for.", w, h;
53 Invalid, Input));
54 }
55 let n = match w.checked_mul(h) {
56 Some(n) => n,
57 None => return Err(err!(
58 "A pixmap of {} by {} pixels overflows a count of pixels.", w, h;
59 Invalid, Input, Overflow)),
60 };
61 if n > MAX_PIXELS {
62 return Err(err!(
63 "A pixmap of {} by {} pixels holds {} pixels, over the ceiling of {}.",
64 w, h, n, MAX_PIXELS;
65 Invalid, Input, Excessive));
66 }
67 Ok(Self {
68 w,
69 h,
70 data: vec![0; n * 4],
71 })
72 }
73
74 /// Takes RGBA bytes that came from somewhere else and calls them a pixmap.
75 ///
76 /// The bytes must be exactly `w * h * 4` of them, and a buffer that is not is refused rather
77 /// than padded or cut: a picture that arrived the wrong length is a picture whose sender and
78 /// receiver disagree about its size, and guessing which of them is right paints something
79 /// nobody drew. A caller with pixels from a decoder, a capture, or another process is the
80 /// reason this exists, since every other constructor here makes its own buffer.
81 pub fn from_data(w: usize, h: usize, data: Vec<u8>) -> Outcome<Self> {
82 let pm = res!(Self::new(w, h));
83 if data.len() != pm.data.len() {
84 return Err(err!(
85 "A pixmap of {} by {} holds {} bytes of RGBA, but {} were given.",
86 w, h, pm.data.len(), data.len();
87 Invalid, Input, Mismatch));
88 }
89 Ok(Self { w, h, data })
90 }
91
92 pub fn filled(w: usize, h: usize, colour: Rgba) -> Outcome<Self> {
93 let mut pm = res!(Self::new(w, h));
94 pm.fill(colour);
95 Ok(pm)
96 }
97
98 pub fn width(&self) -> usize {
99 self.w
100 }
101
102 pub fn height(&self) -> usize {
103 self.h
104 }
105
106 pub fn data(&self) -> &[u8] {
107 &self.data
108 }
109
110 pub fn data_mut(&mut self) -> &mut [u8] {
111 &mut self.data
112 }
113
114 pub fn into_data(self) -> Vec<u8> {
115 self.data
116 }
117
118 pub fn bounds(&self) -> Bounds {
119 Bounds { x0: 0.0, y0: 0.0, x1: self.w as f32, y1: self.h as f32 }
120 }
121
122 pub fn fill(&mut self, colour: Rgba) {
123 for px in self.data.chunks_exact_mut(4) {
124 px[0] = colour.r;
125 px[1] = colour.g;
126 px[2] = colour.b;
127 px[3] = colour.a;
128 }
129 }
130
131 pub fn pixel(&self, x: usize, y: usize) -> Option<Rgba> {
132 if x >= self.w || y >= self.h {
133 return None;
134 }
135 let i = (y * self.w + x) * 4;
136 Some(Rgba::new(self.data[i], self.data[i + 1], self.data[i + 2], self.data[i + 3]))
137 }
138
139 /// Replaces the colour at a pixel, ignoring coordinates that fall outside.
140 pub fn set_pixel(&mut self, x: usize, y: usize, colour: Rgba) {
141 if x >= self.w || y >= self.h {
142 return;
143 }
144 let i = (y * self.w + x) * 4;
145 self.data[i] = colour.r;
146 self.data[i + 1] = colour.g;
147 self.data[i + 2] = colour.b;
148 self.data[i + 3] = colour.a;
149 }
150
151 /// Composites a colour over the pixel already there, ignoring coordinates outside.
152 pub fn blend_pixel(&mut self, x: usize, y: usize, src: Rgba) {
153 if src.is_transparent() || x >= self.w || y >= self.h {
154 return;
155 }
156 let i = (y * self.w + x) * 4;
157 let dst = Rgba::new(self.data[i], self.data[i + 1], self.data[i + 2], self.data[i + 3]);
158 let out = src.over(dst);
159 self.data[i] = out.r;
160 self.data[i + 1] = out.g;
161 self.data[i + 2] = out.b;
162 self.data[i + 3] = out.a;
163 }
164
165 /// Fills a path with a colour, anti-aliased, under the non-zero winding rule.
166 ///
167 /// The clip, if given, is taken at pixel granularity, so it is expected to fall on pixel
168 /// boundaries; layout produces such rectangles, and a fractional clip edge rounds outwards to
169 /// the pixel that contains it.
170 pub fn fill_path(
171 &mut self,
172 path: &Path,
173 t: &Transform,
174 colour: Rgba,
175 clip: Option<Bounds>,
176 )
177 -> Outcome<()>
178 {
179 self.fill_path_with(path, t, colour, clip, FillRule::NonZero)
180 }
181
182 /// Fills a path with a colour, anti-aliased, under a fill rule.
183 ///
184 /// Non-zero is what a glyph outline or a box wants, and is what [`Pixmap::fill_path`] takes.
185 /// Even-odd is for a shape whose overlaps are meant to read as holes: see [`FillRule`].
186 pub fn fill_path_with(
187 &mut self,
188 path: &Path,
189 t: &Transform,
190 colour: Rgba,
191 clip: Option<Bounds>,
192 rule: FillRule,
193 )
194 -> Outcome<()>
195 {
196 if colour.is_transparent() || path.is_empty() {
197 return Ok(());
198 }
199 let bb = match path.bounds(t) {
200 Some(bb) => bb,
201 None => return Ok(()),
202 };
203 let mut win = bb.intersect(self.bounds());
204 if let Some(c) = clip {
205 win = win.intersect(c);
206 }
207 if win.is_empty() {
208 return Ok(());
209 }
210 // The window in whole pixels: any pixel the shape touches at all.
211 let ix0 = win.x0.floor().max(0.0) as usize;
212 let iy0 = win.y0.floor().max(0.0) as usize;
213 let ix1 = (win.x1.ceil() as usize).min(self.w);
214 let iy1 = (win.y1.ceil() as usize).min(self.h);
215 if ix1 <= ix0 || iy1 <= iy0 {
216 return Ok(());
217 }
218 let (ww, wh) = (ix1 - ix0, iy1 - iy0);
219
220 let mut r = Raster::new(ww, wh);
221 let (ox, oy) = (ix0 as f32, iy0 as f32);
222 for contour in path.flatten(t, TOLERANCE) {
223 let local: Vec<Pt> = contour
224 .into_iter()
225 .map(|p| Pt::new(p.x - ox, p.y - oy))
226 .collect();
227 r.add_contour(&local);
228 }
229 let cov = r.coverage_with(rule);
230
231 for wy in 0..wh {
232 for wx in 0..ww {
233 let c = cov[wy * ww + wx];
234 if c > 0.0 {
235 self.blend_pixel(ix0 + wx, iy0 + wy, colour.with_coverage(c));
236 }
237 }
238 }
239 Ok(())
240 }
241
242 /// Fills a path with a gradient, anti-aliased, under a fill rule.
243 ///
244 /// The gradient is expressed in the path's own coordinates and carried through the same
245 /// transform, so a shape and the shading on it move, rotate and scale together -- which is what
246 /// an SVG gradient without its own transform does, and what a caller who has just scaled a
247 /// drawing expects. A gradient that is not invertible under the transform, which is one that
248 /// has been collapsed to a line or a point, degenerates to the last stop's colour rather than
249 /// failing: a shape squashed flat has no shading left to compute.
250 pub fn fill_gradient(
251 &mut self,
252 path: &Path,
253 t: &Transform,
254 grad: &Gradient,
255 clip: Option<Bounds>,
256 rule: FillRule,
257 )
258 -> Outcome<()>
259 {
260 if path.is_empty() {
261 return Ok(());
262 }
263 let grad = res!(grad.prepare());
264 let inv = match t.invert() {
265 Some(inv) => inv,
266 // A degenerate transform paints the shape's own last colour rather than nothing, since
267 // the shape is still there to be filled even where its shading is not.
268 None => {
269 let last = match grad.stops().last() {
270 Some(s) => s.colour,
271 None => return Ok(()),
272 };
273 return self.fill_path_with(path, t, last, clip, rule);
274 },
275 };
276 let bb = match path.bounds(t) {
277 Some(bb) => bb,
278 None => return Ok(()),
279 };
280 let mut win = bb.intersect(self.bounds());
281 if let Some(c) = clip {
282 win = win.intersect(c);
283 }
284 if win.is_empty() {
285 return Ok(());
286 }
287 let ix0 = win.x0.floor().max(0.0) as usize;
288 let iy0 = win.y0.floor().max(0.0) as usize;
289 let ix1 = (win.x1.ceil() as usize).min(self.w);
290 let iy1 = (win.y1.ceil() as usize).min(self.h);
291 if ix1 <= ix0 || iy1 <= iy0 {
292 return Ok(());
293 }
294 let (ww, wh) = (ix1 - ix0, iy1 - iy0);
295
296 let mut r = Raster::new(ww, wh);
297 let (ox, oy) = (ix0 as f32, iy0 as f32);
298 for contour in path.flatten(t, TOLERANCE) {
299 let local: Vec<Pt> = contour
300 .into_iter()
301 .map(|p| Pt::new(p.x - ox, p.y - oy))
302 .collect();
303 r.add_contour(&local);
304 }
305 let cov = r.coverage_with(rule);
306
307 for wy in 0..wh {
308 for wx in 0..ww {
309 let c = cov[wy * ww + wx];
310 if c > 0.0 {
311 // The pixel's centre, carried back into the coordinates the gradient is
312 // expressed in, which is where its position along the gradient is read.
313 let p = inv.apply(Pt::new(ox + (wx as f32) + 0.5, oy + (wy as f32) + 0.5));
314 let colour = grad.sample(grad.position(p.x, p.y));
315 if !colour.is_transparent() {
316 self.blend_pixel(ix0 + wx, iy0 + wy, colour.with_coverage(c));
317 }
318 }
319 }
320 }
321 Ok(())
322 }
323
324 /// Strokes a path with a pen and fills the ink it leaves.
325 ///
326 /// The pen's width is in the path's own coordinates, so the transform scales the line along
327 /// with the shape, which is what a caller drawing the same diagram at two sizes wants. The pen's
328 /// tolerance is taken in pixels and divided by the transform's scale, as [`Path::flatten`] does
329 /// with its own, so that a shape enlarged tenfold is stroked ten times more finely rather than
330 /// coming out faceted.
331 pub fn stroke_path(
332 &mut self,
333 path: &Path,
334 t: &Transform,
335 colour: Rgba,
336 clip: Option<Bounds>,
337 pen: &Stroke,
338 )
339 -> Outcome<()>
340 {
341 let mut pen = pen.clone();
342 pen.tol = (pen.tol / t.scale_factor().max(f32::EPSILON)).max(f32::EPSILON);
343 let outline = res!(path.stroke(&pen));
344 // Non-zero, always: the outline is a union of overlapping pieces. See [`crate::stroke`].
345 self.fill_path(&outline, t, colour, clip)
346 }
347
348 /// Fills an axis-aligned rectangle with a colour, anti-aliased at fractional edges.
349 pub fn fill_bounds(&mut self, b: Bounds, colour: Rgba, clip: Option<Bounds>) -> Outcome<()> {
350 if b.is_empty() {
351 return Ok(());
352 }
353 let path = res!(Path::rect(b));
354 self.fill_path(&path, &Transform::IDENTITY, colour, clip)
355 }
356
357 /// Composites another pixmap over this one, with its top-left corner at `(x, y)`.
358 pub fn blit(&mut self, src: &Pixmap, x: i32, y: i32, clip: Option<Bounds>) {
359 for sy in 0..src.h {
360 for sx in 0..src.w {
361 let dx = x + (sx as i32);
362 let dy = y + (sy as i32);
363 if dx < 0 || dy < 0 {
364 continue;
365 }
366 let (dx, dy) = (dx as usize, dy as usize);
367 if let Some(c) = clip {
368 let (fx, fy) = ((dx as f32) + 0.5, (dy as f32) + 0.5);
369 if fx < c.x0 || fx >= c.x1 || fy < c.y0 || fy >= c.y1 {
370 continue;
371 }
372 }
373 if let Some(s) = src.pixel(sx, sy) {
374 self.blend_pixel(dx, dy, s);
375 }
376 }
377 }
378 }
379
380 pub fn to_png(&self) -> Outcome<Vec<u8>> {
381 png::encode(self)
382 }
383
384 pub fn from_png(buf: &[u8]) -> Outcome<Self> {
385 png::decode(buf)
386 }
387
388 pub fn save_png<P: AsRef<FilePath>>(&self, path: P) -> Outcome<()> {
389 let buf = res!(self.to_png());
390 res!(std::fs::write(path.as_ref(), &buf));
391 Ok(())
392 }
393
394 pub fn load_png<P: AsRef<FilePath>>(path: P) -> Outcome<Self> {
395 let buf = res!(std::fs::read(path.as_ref()));
396 Self::from_png(&buf)
397 }
398
399 /// JPEG carries no alpha channel, so a pixel that is not opaque is composited over white.
400 pub fn to_jpeg(&self) -> Outcome<Vec<u8>> {
401 jpeg::encode(self)
402 }
403
404 pub fn from_jpeg(buf: &[u8]) -> Outcome<Self> {
405 jpeg::decode(buf)
406 }
407
408 pub fn save_jpeg<P: AsRef<FilePath>>(&self, path: P) -> Outcome<()> {
409 let buf = res!(self.to_jpeg());
410 res!(std::fs::write(path.as_ref(), &buf));
411 Ok(())
412 }
413
414 pub fn load_jpeg<P: AsRef<FilePath>>(path: P) -> Outcome<Self> {
415 let buf = res!(std::fs::read(path.as_ref()));
416 Self::from_jpeg(&buf)
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 /// The colour at a pixel a test asserts is in range.
425 fn px(pm: &Pixmap, x: usize, y: usize) -> Outcome<Rgba> {
426 match pm.pixel(x, y) {
427 Some(c) => Ok(c),
428 None => Err(err!(
429 "The pixel ({}, {}) lies outside a pixmap of {} by {}.",
430 x, y, pm.width(), pm.height();
431 Invalid, Input, Range)),
432 }
433 }
434
435 #[test]
436 fn test_a_zero_sized_pixmap_is_refused_00() {
437 assert!(Pixmap::new(0, 10).is_err());
438 assert!(Pixmap::new(10, 0).is_err());
439 }
440
441 #[test]
442 fn test_an_absurd_pixmap_is_refused_01() {
443 assert!(Pixmap::new(1 << 20, 1 << 20).is_err());
444 }
445
446 #[test]
447 fn test_fill_sets_every_pixel_02() -> Outcome<()> {
448 let mut pm = res!(Pixmap::new(4, 4));
449 pm.fill(Rgba::WHITE);
450 for y in 0..4 {
451 for x in 0..4 {
452 assert_eq!(res!(px(&pm, x, y)), Rgba::WHITE);
453 }
454 }
455 Ok(())
456 }
457
458 #[test]
459 fn test_a_filled_rect_lands_where_it_should_03() -> Outcome<()> {
460 let mut pm = res!(Pixmap::filled(10, 10, Rgba::WHITE));
461 res!(pm.fill_bounds(Bounds::new(2.0, 2.0, 8.0, 8.0), Rgba::BLACK, None));
462 assert_eq!(res!(px(&pm, 5, 5)), Rgba::BLACK, "inside");
463 assert_eq!(res!(px(&pm, 0, 0)), Rgba::WHITE, "outside");
464 assert_eq!(res!(px(&pm, 1, 1)), Rgba::WHITE, "just outside");
465 assert_eq!(res!(px(&pm, 2, 2)), Rgba::BLACK, "just inside");
466 Ok(())
467 }
468
469 #[test]
470 fn test_a_clip_holds_paint_back_04() -> Outcome<()> {
471 let mut pm = res!(Pixmap::filled(10, 10, Rgba::WHITE));
472 let clip = Bounds::new(0.0, 0.0, 5.0, 10.0);
473 res!(pm.fill_bounds(Bounds::new(0.0, 0.0, 10.0, 10.0), Rgba::BLACK, Some(clip)));
474 assert_eq!(res!(px(&pm, 4, 5)), Rgba::BLACK, "inside the clip");
475 assert_eq!(res!(px(&pm, 6, 5)), Rgba::WHITE, "outside the clip");
476 Ok(())
477 }
478
479 #[test]
480 fn test_a_half_pixel_edge_is_soft_05() -> Outcome<()> {
481 let mut pm = res!(Pixmap::filled(4, 4, Rgba::WHITE));
482 res!(pm.fill_bounds(Bounds::new(0.0, 0.0, 0.5, 4.0), Rgba::BLACK, None));
483 let p = res!(px(&pm, 0, 0));
484 assert!(p.r > 100 && p.r < 160, "expected a half-covered grey, found {}", p.r);
485 Ok(())
486 }
487
488 #[test]
489 fn test_transparent_paint_changes_nothing_06() -> Outcome<()> {
490 let mut pm = res!(Pixmap::filled(4, 4, Rgba::WHITE));
491 let before = pm.clone();
492 res!(pm.fill_bounds(Bounds::new(0.0, 0.0, 4.0, 4.0), Rgba::TRANSPARENT, None));
493 assert_eq!(pm, before);
494 Ok(())
495 }
496
497 #[test]
498 fn test_blit_composites_and_clips_07() -> Outcome<()> {
499 let mut dst = res!(Pixmap::filled(8, 8, Rgba::WHITE));
500 let src = res!(Pixmap::filled(4, 4, Rgba::BLACK));
501 dst.blit(&src, 6, 6, None); // Half of it hangs off the edge.
502 assert_eq!(res!(px(&dst, 7, 7)), Rgba::BLACK);
503 assert_eq!(res!(px(&dst, 5, 5)), Rgba::WHITE);
504 assert_eq!(dst.width(), 8, "the destination must not have grown");
505 Ok(())
506 }
507 #[test]
508 fn test_a_gradient_fill_shades_along_its_axis_08() -> Outcome<()> {
509 let mut pm = res!(Pixmap::new(64, 8));
510 let path = res!(Path::rect(Bounds::new(0.0, 0.0, 64.0, 8.0)));
511 let g = Gradient::two((0.0, 0.0), (64.0, 0.0), Rgba::BLACK, Rgba::WHITE);
512 res!(pm.fill_gradient(&path, &Transform::IDENTITY, &g, None, FillRule::NonZero));
513 // Along the axis the red channel rises and never falls.
514 let mut last = 0u8;
515 for x in 0..64 {
516 let c = match pm.pixel(x, 4) {
517 Some(c) => c,
518 None => return Err(err!("Reading pixel {}.", x; Invalid, Input, Range)),
519 };
520 req!(c.a, 255);
521 assert!(c.r >= last, "the ramp fell at x = {}: {} after {}", x, c.r, last);
522 last = c.r;
523 }
524 // Across the axis nothing changes.
525 req!(pm.pixel(20, 0), pm.pixel(20, 7));
526 Ok(())
527 }
528
529 #[test]
530 fn test_a_gradient_travels_with_the_transform_09() -> Outcome<()> {
531 // The gradient is expressed in the path\'s coordinates, so scaling the drawing scales the
532 // shading with it: the pixel at twice the distance is the colour that was at half of it.
533 let g = Gradient::two((0.0, 0.0), (32.0, 0.0), Rgba::BLACK, Rgba::WHITE);
534 let path = res!(Path::rect(Bounds::new(0.0, 0.0, 32.0, 4.0)));
535
536 let mut plain = res!(Pixmap::new(64, 8));
537 res!(plain.fill_gradient(&path, &Transform::IDENTITY, &g, None, FillRule::NonZero));
538 let mut twice = res!(Pixmap::new(64, 8));
539 res!(twice.fill_gradient(&path, &Transform::scale(2.0, 2.0), &g, None, FillRule::NonZero));
540
541 for x in (2..30).step_by(4) {
542 let a = match plain.pixel(x, 1) {
543 Some(c) => c,
544 None => return Err(err!("Reading pixel {}.", x; Invalid, Input, Range)),
545 };
546 let b = match twice.pixel(x * 2 + 1, 3) {
547 Some(c) => c,
548 None => return Err(err!("Reading pixel {}.", x * 2 + 1; Invalid, Input, Range)),
549 };
550 assert!((a.r as i32 - b.r as i32).abs() <= 4,
551 "at x = {} the scaled shading reads {} where {} was drawn", x, b.r, a.r);
552 }
553 Ok(())
554 }
555
556}