1use super::*;
2
3use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
4use agg_rust::basics::{FillingRule, VertexSource};
5use agg_rust::color::Rgba8;
6use agg_rust::conv_transform::ConvTransform;
7use agg_rust::gradient_lut::GradientLut;
8use agg_rust::path_storage::PathStorage;
9use agg_rust::pixfmt_rgba::PixfmtRgba32;
10use agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa;
11use agg_rust::renderer_base::RendererBase;
12use agg_rust::renderer_scanline::{render_scanlines_aa, render_scanlines_aa_solid};
13use agg_rust::rendering_buffer::RowAccessor;
14use agg_rust::scanline_u::ScanlineU8;
15use agg_rust::span_allocator::SpanAllocator;
16use agg_rust::span_gradient::{GradientFunction, SpanGradient};
17use agg_rust::span_interpolator_linear::SpanInterpolatorLinear;
18use agg_rust::trans_affine::TransAffine;
19
20pub const IDENTITY_EPSILON_F64: f64 = 0.0001;
21
22#[must_use] pub fn rect_intersection(a: &LogicalRect, b: &LogicalRect) -> Option<LogicalRect> {
24 let x1 = a.origin.x.max(b.origin.x);
25 let y1 = a.origin.y.max(b.origin.y);
26 let x2 = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
27 let y2 = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
28 if x2 > x1 && y2 > y1 {
29 Some(LogicalRect {
30 origin: LogicalPosition { x: x1, y: y1 },
31 size: LogicalSize {
32 width: x2 - x1,
33 height: y2 - y1,
34 },
35 })
36 } else {
37 None
38 }
39}
40
41#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] pub fn blit_pixmap(src: &AzulPixmap, dst: &mut AzulPixmap, px_x: i32, px_y: i32, opacity: f32) {
44 let sw = src.width as i32;
45 let sh = src.height as i32;
46 let dw = dst.width as i32;
47 let dh = dst.height as i32;
48 let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
49
50 for sy in 0..sh {
51 let dy = px_y.saturating_add(sy);
55 if dy < 0 || dy >= dh {
56 continue;
57 }
58 for sx in 0..sw {
59 let dx = px_x.saturating_add(sx);
60 if dx < 0 || dx >= dw {
61 continue;
62 }
63 let si = ((sy * sw + sx) * 4) as usize;
64 let di = ((dy * dw + dx) * 4) as usize;
65 if si + 3 >= src.data.len() || di + 3 >= dst.data.len() {
66 continue;
67 }
68
69 let sr = u32::from(src.data[si]);
70 let sg = u32::from(src.data[si + 1]);
71 let sb = u32::from(src.data[si + 2]);
72 let sa = (u32::from(src.data[si + 3]) * op) / 255;
73
74 if sa == 0 {
75 continue;
76 }
77 if sa == 255 {
78 dst.data[di] = sr as u8;
79 dst.data[di + 1] = sg as u8;
80 dst.data[di + 2] = sb as u8;
81 dst.data[di + 3] = 255;
82 } else {
83 let inv_sa = 255 - sa;
84 dst.data[di] = ((sr * sa + u32::from(dst.data[di]) * inv_sa) / 255) as u8;
85 dst.data[di + 1] = ((sg * sa + u32::from(dst.data[di + 1]) * inv_sa) / 255) as u8;
86 dst.data[di + 2] = ((sb * sa + u32::from(dst.data[di + 2]) * inv_sa) / 255) as u8;
87 dst.data[di + 3] = ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
88 }
89 }
90 }
91}
92
93#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] pub fn shift_pixbuf(pixmap: &mut AzulPixmap, dx: i32, dy: i32) {
96 use core::cmp::Ordering;
97 let w = pixmap.width as i32;
98 let h = pixmap.height as i32;
99 if dx.unsigned_abs() >= w.unsigned_abs() || dy.unsigned_abs() >= h.unsigned_abs() {
102 pixmap.fill(0, 0, 0, 0);
104 return;
105 }
106
107 let stride = (w * 4) as usize;
108 let data = &mut pixmap.data;
109
110 match dy.cmp(&0) {
112 Ordering::Greater => {
113 for row in (0..h - dy).rev() {
115 let src_start = (row * w * 4) as usize;
116 let dst_start = ((row + dy) * w * 4) as usize;
117 data.copy_within(src_start..src_start + stride, dst_start);
118 }
119 for row in 0..dy {
121 let start = (row * w * 4) as usize;
122 data[start..start + stride].fill(0);
123 }
124 }
125 Ordering::Less => {
126 let ady = -dy;
127 for row in ady..h {
129 let src_start = (row * w * 4) as usize;
130 let dst_start = ((row - ady) * w * 4) as usize;
131 data.copy_within(src_start..src_start + stride, dst_start);
132 }
133 for row in (h - ady)..h {
135 let start = (row * w * 4) as usize;
136 data[start..start + stride].fill(0);
137 }
138 }
139 Ordering::Equal => {}
140 }
141
142 match dx.cmp(&0) {
144 Ordering::Greater => {
145 for row in 0..h {
146 let row_start = (row * w * 4) as usize;
147 let shift = (dx * 4) as usize;
148 data.copy_within(row_start..row_start + stride - shift, row_start + shift);
150 data[row_start..row_start + shift].fill(0);
152 }
153 }
154 Ordering::Less => {
155 let adx = (-dx * 4) as usize;
156 for row in 0..h {
157 let row_start = (row * w * 4) as usize;
158 data.copy_within(row_start + adx..row_start + stride, row_start);
159 data[row_start + stride - adx..row_start + stride].fill(0);
161 }
162 }
163 Ordering::Equal => {}
164 }
165}
166
167#[derive(Debug)]
169pub struct AzulPixmap {
170 pub(crate) data: Vec<u8>,
171 pub(crate) width: u32,
172 pub(crate) height: u32,
173}
174
175impl AzulPixmap {
176 #[must_use] pub fn new(width: u32, height: u32) -> Option<Self> {
178 if width == 0 || height == 0 {
179 return None;
180 }
181 let len = (width as usize)
185 .checked_mul(height as usize)
186 .and_then(|n| n.checked_mul(4))?;
187 let data = vec![255u8; len]; Some(Self {
189 data,
190 width,
191 height,
192 })
193 }
194
195 pub fn fill(&mut self, r: u8, g: u8, b: u8, a: u8) {
197 for chunk in self.data.chunks_exact_mut(4) {
198 chunk[0] = r;
199 chunk[1] = g;
200 chunk[2] = b;
201 chunk[3] = a;
202 }
203 }
204
205 #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::many_single_char_names)] pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, r: u8, g: u8, b: u8, a: u8) {
209 let pw = self.width as i32;
210 let ph = self.height as i32;
211 let x0 = x.max(0).min(pw);
212 let y0 = y.max(0).min(ph);
213 let x1 = x.saturating_add(w).clamp(x0, pw);
218 let y1 = y.saturating_add(h).clamp(y0, ph);
219 for row in y0..y1 {
220 let start = (row * pw + x0) as usize * 4;
221 let end = (row * pw + x1) as usize * 4;
222 if end <= self.data.len() {
223 for chunk in self.data[start..end].chunks_exact_mut(4) {
224 chunk[0] = r;
225 chunk[1] = g;
226 chunk[2] = b;
227 chunk[3] = a;
228 }
229 }
230 }
231 }
232
233 #[must_use] pub fn data(&self) -> &[u8] {
235 &self.data
236 }
237
238 pub fn data_mut(&mut self) -> &mut [u8] {
240 &mut self.data
241 }
242
243 #[must_use] pub const fn width(&self) -> u32 {
245 self.width
246 }
247
248 #[must_use] pub const fn height(&self) -> u32 {
250 self.height
251 }
252
253 #[must_use] pub fn clone_pixmap(&self) -> Self {
255 Self {
256 data: self.data.clone(),
257 width: self.width,
258 height: self.height,
259 }
260 }
261
262 pub fn resize_grow_only(
266 &mut self,
267 new_width: u32,
268 new_height: u32,
269 fill_r: u8,
270 fill_g: u8,
271 fill_b: u8,
272 fill_a: u8,
273 ) -> Option<()> {
274 if new_width < self.width || new_height < self.height {
275 return None;
276 }
277 if new_width == self.width && new_height == self.height {
278 return Some(());
279 }
280
281 let old_w = self.width as usize;
282 let old_h = self.height as usize;
283 let new_w = new_width as usize;
284 let new_h = new_height as usize;
285 let mut new_data = vec![fill_a; new_w * new_h * 4];
286
287 for chunk in new_data.chunks_exact_mut(4) {
289 chunk[0] = fill_r;
290 chunk[1] = fill_g;
291 chunk[2] = fill_b;
292 chunk[3] = fill_a;
293 }
294
295 let old_stride = old_w * 4;
297 let new_stride = new_w * 4;
298 for row in 0..old_h {
299 let src = row * old_stride;
300 let dst = row * new_stride;
301 new_data[dst..dst + old_stride].copy_from_slice(&self.data[src..src + old_stride]);
302 }
303
304 self.data = new_data;
305 self.width = new_width;
306 self.height = new_height;
307 Some(())
308 }
309
310 pub fn resize_reuse(
313 &mut self,
314 new_width: u32,
315 new_height: u32,
316 fill_r: u8,
317 fill_g: u8,
318 fill_b: u8,
319 fill_a: u8,
320 ) {
321 if new_width == self.width && new_height == self.height {
322 return;
323 }
324
325 let old_w = self.width as usize;
326 let old_h = self.height as usize;
327 let new_w = new_width as usize;
328 let new_h = new_height as usize;
329 let new_stride = new_w * 4;
330 let old_stride = old_w * 4;
331
332 let mut new_data = vec![0u8; new_w * new_h * 4];
333
334 for chunk in new_data.chunks_exact_mut(4) {
336 chunk[0] = fill_r;
337 chunk[1] = fill_g;
338 chunk[2] = fill_b;
339 chunk[3] = fill_a;
340 }
341
342 let copy_rows = old_h.min(new_h);
344 let copy_cols_bytes = old_stride.min(new_stride);
345 for row in 0..copy_rows {
346 let src = row * old_stride;
347 let dst = row * new_stride;
348 new_data[dst..dst + copy_cols_bytes]
349 .copy_from_slice(&self.data[src..src + copy_cols_bytes]);
350 }
351
352 self.data = new_data;
353 self.width = new_width;
354 self.height = new_height;
355 }
356
357 pub fn encode_png(&self) -> Result<Vec<u8>, String> {
362 let mut buf = Vec::new();
363 {
364 let mut encoder = png::Encoder::new(&mut buf, self.width, self.height);
365 encoder.set_color(png::ColorType::Rgba);
366 encoder.set_depth(png::BitDepth::Eight);
367 let mut writer = encoder
368 .write_header()
369 .map_err(|e| format!("PNG header error: {e}"))?;
370 writer
371 .write_image_data(&self.data)
372 .map_err(|e| format!("PNG write error: {e}"))?;
373 }
374 Ok(buf)
375 }
376
377 pub fn decode_png(png_bytes: &[u8]) -> Result<Self, String> {
382 let decoder = png::Decoder::new(std::io::Cursor::new(png_bytes));
383 let mut reader = decoder
384 .read_info()
385 .map_err(|e| format!("PNG decode error: {e}"))?;
386 let buf_size = reader
387 .output_buffer_size()
388 .ok_or_else(|| "PNG: unknown output buffer size".to_string())?;
389 let mut buf = vec![0u8; buf_size];
390 let info = reader
391 .next_frame(&mut buf)
392 .map_err(|e| format!("PNG frame error: {e}"))?;
393 reader
397 .finish()
398 .map_err(|e| format!("PNG truncated or corrupt: {e}"))?;
399 let width = info.width;
400 let height = info.height;
401
402 let data = match info.color_type {
404 png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(),
405 png::ColorType::Rgb => {
406 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
407 for chunk in buf[..info.buffer_size()].chunks_exact(3) {
408 rgba.push(chunk[0]);
409 rgba.push(chunk[1]);
410 rgba.push(chunk[2]);
411 rgba.push(255);
412 }
413 rgba
414 }
415 png::ColorType::Grayscale => {
416 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
417 for &v in &buf[..info.buffer_size()] {
418 rgba.push(v);
419 rgba.push(v);
420 rgba.push(v);
421 rgba.push(255);
422 }
423 rgba
424 }
425 other => return Err(format!("Unsupported PNG color type: {other:?}")),
426 };
427
428 Ok(Self {
429 data,
430 width,
431 height,
432 })
433 }
434}
435
436#[derive(Copy, Debug, Clone)]
442pub struct PixelDiffResult {
443 pub diff_count: u64,
445 pub total_pixels: u64,
447 pub max_delta: u8,
449 pub dimensions_match: bool,
451 pub ref_width: u32,
453 pub ref_height: u32,
455 pub test_width: u32,
457 pub test_height: u32,
459}
460
461impl PixelDiffResult {
462 #[must_use] pub const fn is_match(&self) -> bool {
464 self.dimensions_match && self.diff_count == 0
465 }
466
467 #[allow(clippy::cast_precision_loss)] #[must_use] pub fn diff_ratio(&self) -> f64 {
470 if self.total_pixels == 0 {
471 0.0
472 } else {
473 self.diff_count as f64 / self.total_pixels as f64
474 }
475 }
476}
477
478#[allow(clippy::cast_possible_truncation)] #[must_use] pub fn pixel_diff(reference: &AzulPixmap, test: &AzulPixmap, threshold: u8) -> PixelDiffResult {
484 let dimensions_match = reference.width == test.width && reference.height == test.height;
485 if !dimensions_match {
486 return PixelDiffResult {
487 diff_count: 0,
488 total_pixels: 0,
489 max_delta: 0,
490 dimensions_match: false,
491 ref_width: reference.width,
492 ref_height: reference.height,
493 test_width: test.width,
494 test_height: test.height,
495 };
496 }
497
498 let total_pixels = u64::from(reference.width) * u64::from(reference.height);
499 let mut diff_count = 0u64;
500 let mut max_delta = 0u8;
501
502 for (ref_chunk, test_chunk) in reference
503 .data
504 .chunks_exact(4)
505 .zip(test.data.chunks_exact(4))
506 {
507 let mut pixel_differs = false;
508 for c in 0..4 {
509 let delta = (i16::from(ref_chunk[c]) - i16::from(test_chunk[c])).unsigned_abs() as u8;
510 if delta > threshold {
511 pixel_differs = true;
512 }
513 if delta > max_delta {
514 max_delta = delta;
515 }
516 }
517 if pixel_differs {
518 diff_count += 1;
519 }
520 }
521
522 PixelDiffResult {
523 diff_count,
524 total_pixels,
525 max_delta,
526 dimensions_match: true,
527 ref_width: reference.width,
528 ref_height: reference.height,
529 test_width: test.width,
530 test_height: test.height,
531 }
532}
533
534pub fn compare_against_reference(
542 rendered: &AzulPixmap,
543 reference_png_path: &str,
544 threshold: u8,
545) -> Result<PixelDiffResult, String> {
546 let ref_bytes = std::fs::read(reference_png_path)
547 .map_err(|e| format!("Cannot read reference image {reference_png_path}: {e}"))?;
548 let reference = AzulPixmap::decode_png(&ref_bytes)?;
549 Ok(pixel_diff(&reference, rendered, threshold))
550}
551
552#[derive(Debug, Clone, Copy)]
557pub struct AzRect {
558 pub(crate) x: f32,
559 pub(crate) y: f32,
560 pub(crate) width: f32,
561 pub(crate) height: f32,
562}
563
564#[must_use] pub fn intersect_clips(current: Option<AzRect>, new: Option<AzRect>) -> Option<AzRect> {
571 match (current, new) {
572 (Some(cur), Some(new)) => {
573 let x0 = cur.x.max(new.x);
574 let y0 = cur.y.max(new.y);
575 let x1 = (cur.x + cur.width).min(new.x + new.width);
576 let y1 = (cur.y + cur.height).min(new.y + new.height);
577 Some(AzRect {
578 x: x0,
579 y: y0,
580 width: (x1 - x0).max(0.0),
581 height: (y1 - y0).max(0.0),
582 })
583 }
584 (Some(cur), None) => Some(cur),
585 (None, new) => new,
586 }
587}
588
589impl AzRect {
590 pub(crate) const DENY_ALL: Self = Self { x: 0.0, y: 0.0, width: 0.0, height: 0.0 };
593
594 pub(crate) fn from_xywh(x: f32, y: f32, w: f32, h: f32) -> Option<Self> {
595 if w <= 0.0
596 || h <= 0.0
597 || !x.is_finite()
598 || !y.is_finite()
599 || !w.is_finite()
600 || !h.is_finite()
601 {
602 return None;
603 }
604 Some(Self {
605 x,
606 y,
607 width: w,
608 height: h,
609 })
610 }
611
612 pub(crate) fn clip(&self, clip: &Self) -> Option<Self> {
614 let x1 = self.x.max(clip.x);
615 let y1 = self.y.max(clip.y);
616 let x2 = (self.x + self.width).min(clip.x + clip.width);
617 let y2 = (self.y + self.height).min(clip.y + clip.height);
618 if x2 > x1 && y2 > y1 {
619 Some(Self {
620 x: x1,
621 y: y1,
622 width: x2 - x1,
623 height: y2 - y1,
624 })
625 } else {
626 None
627 }
628 }
629}
630
631struct ClampVertexSource<'a> {
643 inner: &'a mut dyn VertexSource,
644 limit: f64,
645}
646
647impl VertexSource for ClampVertexSource<'_> {
648 fn rewind(&mut self, path_id: u32) {
649 self.inner.rewind(path_id);
650 }
651
652 fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
653 let cmd = self.inner.vertex(x, y);
654 *x = if x.is_nan() { 0.0 } else { x.clamp(-self.limit, self.limit) };
655 *y = if y.is_nan() { 0.0 } else { y.clamp(-self.limit, self.limit) };
656 cmd
657 }
658}
659
660fn coord_clamp_limit(w: u32, h: u32) -> f64 {
664 (f64::from(w) + f64::from(h)).mul_add(4.0, 4096.0)
665}
666
667pub fn agg_fill_path(
668 pixmap: &mut AzulPixmap,
669 path: &mut dyn VertexSource,
670 color: &Rgba8,
671 rule: FillingRule,
672) {
673 agg_fill_path_clipped(pixmap, path, color, rule, None);
674}
675
676#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[allow(clippy::similar_names)] pub fn agg_fill_path_clipped(
685 pixmap: &mut AzulPixmap,
686 path: &mut dyn VertexSource,
687 color: &Rgba8,
688 rule: FillingRule,
689 clip: Option<AzRect>,
690) {
691 let w = pixmap.width;
692 let h = pixmap.height;
693 let stride = (w * 4) as i32;
694 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
695 let mut pf = PixfmtRgba32::new(&mut ra);
696 let mut rb = RendererBase::new(pf);
697 if let Some(c) = clip {
698 if c.width <= 0.0 || c.height <= 0.0 {
703 return;
704 }
705 rb.clip_box_i(
706 c.x as i32,
707 c.y as i32,
708 (c.x + c.width) as i32 - 1,
709 (c.y + c.height) as i32 - 1,
710 );
711 }
712 let mut ras = RasterizerScanlineAa::new();
713 ras.filling_rule(rule);
714 let (clip_x0, clip_y0, clip_x1, clip_y1) = clip.map_or_else(
720 || (0.0, 0.0, f64::from(w), f64::from(h)),
721 |c| (
722 f64::from(c.x).max(0.0),
723 f64::from(c.y).max(0.0),
724 f64::from(c.x + c.width).min(f64::from(w)),
725 f64::from(c.y + c.height).min(f64::from(h)),
726 ),
727 );
728 ras.clip_box(clip_x0, clip_y0, clip_x1, clip_y1);
729 let mut clamped = ClampVertexSource { inner: path, limit: coord_clamp_limit(w, h) };
733 ras.add_path(&mut clamped, 0);
734 let mut sl = ScanlineU8::new();
735 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, color);
736}
737
738#[allow(clippy::trivially_copy_pass_by_ref)] fn agg_fill_transformed_path(
740 pixmap: &mut AzulPixmap,
741 path: &mut PathStorage,
742 color: &Rgba8,
743 rule: FillingRule,
744 transform: &TransAffine,
745) {
746 agg_fill_transformed_path_clipped(pixmap, path, color, rule, transform, None);
747}
748
749#[allow(clippy::trivially_copy_pass_by_ref)] fn agg_fill_transformed_path_clipped(
751 pixmap: &mut AzulPixmap,
752 path: &mut PathStorage,
753 color: &Rgba8,
754 rule: FillingRule,
755 transform: &TransAffine,
756 clip: Option<AzRect>,
757) {
758 if transform.is_identity(IDENTITY_EPSILON_F64) {
759 agg_fill_path_clipped(pixmap, path, color, rule, clip);
760 } else {
761 let mut transformed = ConvTransform::new(path, *transform);
762 agg_fill_path_clipped(pixmap, &mut transformed, color, rule, clip);
763 }
764}
765
766fn agg_fill_gradient<G: GradientFunction>(
771 pixmap: &mut AzulPixmap,
772 path: &mut dyn VertexSource,
773 lut: &GradientLut,
774 gradient_fn: G,
775 transform: TransAffine,
776 d1: f64,
777 d2: f64,
778) {
779 agg_fill_gradient_clipped(pixmap, path, lut, gradient_fn, transform, d1, d2, None);
780}
781
782#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[allow(clippy::similar_names)] pub fn agg_fill_gradient_clipped<G: GradientFunction>(
785 pixmap: &mut AzulPixmap,
786 path: &mut dyn VertexSource,
787 lut: &GradientLut,
788 gradient_fn: G,
789 transform: TransAffine,
790 d1: f64,
791 d2: f64,
792 clip: Option<AzRect>,
793) {
794 let w = pixmap.width;
795 let h = pixmap.height;
796 let stride = (w * 4) as i32;
797 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
798 let mut pf = PixfmtRgba32::new(&mut ra);
799 let mut rb = RendererBase::new(pf);
800 if let Some(c) = clip {
801 if c.width <= 0.0 || c.height <= 0.0 {
804 return;
805 }
806 rb.clip_box_i(
807 c.x as i32,
808 c.y as i32,
809 (c.x + c.width) as i32 - 1,
810 (c.y + c.height) as i32 - 1,
811 );
812 }
813 let mut ras = RasterizerScanlineAa::new();
814 ras.filling_rule(FillingRule::NonZero);
815 let (clip_x0, clip_y0, clip_x1, clip_y1) = clip.map_or_else(
816 || (0.0, 0.0, f64::from(w), f64::from(h)),
817 |c| (
818 f64::from(c.x).max(0.0),
819 f64::from(c.y).max(0.0),
820 f64::from(c.x + c.width).min(f64::from(w)),
821 f64::from(c.y + c.height).min(f64::from(h)),
822 ),
823 );
824 ras.clip_box(clip_x0, clip_y0, clip_x1, clip_y1);
825 let mut clamped = ClampVertexSource { inner: path, limit: coord_clamp_limit(w, h) };
826 ras.add_path(&mut clamped, 0);
827 let mut sl = ScanlineU8::new();
828
829 let interp = SpanInterpolatorLinear::new(transform);
830 let clamp_d = |d: f64| if d.is_nan() { 0.0 } else { d.clamp(-8192.0, 8192.0) };
837 let mut sg = SpanGradient::new(interp, gradient_fn, lut, clamp_d(d1), clamp_d(d2));
838 let mut alloc = SpanAllocator::<Rgba8>::new();
839 render_scanlines_aa(&mut ras, &mut sl, &mut rb, &mut alloc, &mut sg);
840}
841
842#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] pub fn blit_buffer(dst: &mut AzulPixmap, src: &[u8], src_w: u32, src_h: u32, dx: i32, dy: i32) {
849 let dw = dst.width as i32;
850 let dh = dst.height as i32;
851
852 for py in 0..src_h as i32 {
853 let ty = dy.saturating_add(py);
855 if ty < 0 || ty >= dh {
856 continue;
857 }
858 for px in 0..src_w as i32 {
859 let tx = dx.saturating_add(px);
860 if tx < 0 || tx >= dw {
861 continue;
862 }
863
864 let si = ((py as u32 * src_w + px as u32) * 4) as usize;
865 let di = ((ty as u32 * dst.width + tx as u32) * 4) as usize;
866
867 if si + 3 >= src.len() || di + 3 >= dst.data.len() {
868 continue;
869 }
870
871 let sa = u32::from(src[si + 3]);
872 if sa == 0 {
873 continue;
874 }
875 if sa == 255 {
876 dst.data[di] = src[si];
877 dst.data[di + 1] = src[si + 1];
878 dst.data[di + 2] = src[si + 2];
879 dst.data[di + 3] = 255;
880 } else {
881 let inv_sa = 255 - sa;
883 dst.data[di] =
884 ((u32::from(src[si]) + u32::from(dst.data[di]) * inv_sa / 255).min(255)) as u8;
885 dst.data[di + 1] =
886 ((u32::from(src[si + 1]) + u32::from(dst.data[di + 1]) * inv_sa / 255).min(255)) as u8;
887 dst.data[di + 2] =
888 ((u32::from(src[si + 2]) + u32::from(dst.data[di + 2]) * inv_sa / 255).min(255)) as u8;
889 dst.data[di + 3] = ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
890 }
891 }
892 }
893}
894
895#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[must_use] pub fn snapshot_region(pixmap: &AzulPixmap, x: i32, y: i32, w: u32, h: u32) -> Vec<u8> {
902 let pw = pixmap.width as i32;
903 let ph = pixmap.height as i32;
904 let mut snap = vec![0u8; (w as usize) * (h as usize) * 4];
905
906 for py in 0..h as i32 {
907 let sy = y.saturating_add(py);
909 if sy < 0 || sy >= ph {
910 continue;
911 }
912 for px in 0..w as i32 {
913 let sx = x.saturating_add(px);
914 if sx < 0 || sx >= pw {
915 continue;
916 }
917 let si = ((sy as u32 * pixmap.width + sx as u32) * 4) as usize;
918 let di = ((py as u32 * w + px as u32) * 4) as usize;
919 if si + 3 < pixmap.data.len() && di + 3 < snap.len() {
920 snap[di] = pixmap.data[si];
921 snap[di + 1] = pixmap.data[si + 1];
922 snap[di + 2] = pixmap.data[si + 2];
923 snap[di + 3] = pixmap.data[si + 3];
924 }
925 }
926 }
927 snap
928}
929
930#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
938pub fn write_region(dst: &mut AzulPixmap, src: &[u8], w: u32, h: u32, x: i32, y: i32) {
939 let dw = dst.width as i32;
940 let dh = dst.height as i32;
941 for py in 0..h as i32 {
942 let dy = y.saturating_add(py);
944 if dy < 0 || dy >= dh {
945 continue;
946 }
947 for px in 0..w as i32 {
948 let dx = x.saturating_add(px);
949 if dx < 0 || dx >= dw {
950 continue;
951 }
952 let si = ((py as u32 * w + px as u32) * 4) as usize;
953 let di = ((dy as u32 * dst.width + dx as u32) * 4) as usize;
954 if si + 3 < src.len() && di + 3 < dst.data.len() {
955 dst.data[di] = src[si];
956 dst.data[di + 1] = src[si + 1];
957 dst.data[di + 2] = src[si + 2];
958 dst.data[di + 3] = src[si + 3];
959 }
960 }
961 }
962}
963
964#[must_use] pub fn union_rect(a: &LogicalRect, b: &LogicalRect) -> LogicalRect {
965 let x = a.origin.x.min(b.origin.x);
966 let y = a.origin.y.min(b.origin.y);
967 let right = (a.origin.x + a.size.width).max(b.origin.x + b.size.width);
968 let bottom = (a.origin.y + a.size.height).max(b.origin.y + b.size.height);
969 LogicalRect {
970 origin: LogicalPosition { x, y },
971 size: LogicalSize {
972 width: right - x,
973 height: bottom - y,
974 },
975 }
976}
977
978#[must_use] pub fn logical_rect_to_az_rect(bounds: &LogicalRect, dpi_factor: f32) -> Option<AzRect> {
979 let x = bounds.origin.x * dpi_factor;
980 let y = bounds.origin.y * dpi_factor;
981 let width = bounds.size.width * dpi_factor;
982 let height = bounds.size.height * dpi_factor;
983
984 AzRect::from_xywh(x, y, width, height)
985}
986
987#[cfg(test)]
988#[allow(clippy::many_single_char_names, clippy::float_cmp)]
989mod autotest_generated {
990 use agg_rust::span_gradient::GradientX;
991
992 use super::*;
993
994 const WHITE: [u8; 4] = [255, 255, 255, 255];
999 const CLEAR: [u8; 4] = [0, 0, 0, 0];
1000
1001 fn pm(w: u32, h: u32) -> AzulPixmap {
1002 AzulPixmap::new(w, h).expect("AzulPixmap::new failed for a valid size")
1003 }
1004
1005 fn filled(w: u32, h: u32, c: [u8; 4]) -> AzulPixmap {
1006 let mut p = pm(w, h);
1007 p.fill(c[0], c[1], c[2], c[3]);
1008 p
1009 }
1010
1011 fn marked(w: u32, h: u32) -> AzulPixmap {
1013 let mut p = pm(w, h);
1014 for y in 0..h {
1015 for x in 0..w {
1016 let idx = u8::try_from(y * w + x).expect("marker fits in u8");
1017 set(&mut p, x, y, [idx, 0, 0, 255]);
1018 }
1019 }
1020 p
1021 }
1022
1023 fn zero_sized() -> AzulPixmap {
1025 let mut p = pm(2, 2);
1026 p.resize_reuse(0, 0, 0, 0, 0, 0);
1027 p
1028 }
1029
1030 fn painted_count(p: &AzulPixmap) -> usize {
1032 p.data()
1033 .chunks_exact(4)
1034 .filter(|c| c[0] != 255 || c[1] != 255 || c[2] != 255 || c[3] != 255)
1035 .count()
1036 }
1037
1038 fn get(p: &AzulPixmap, x: u32, y: u32) -> [u8; 4] {
1039 let i = ((y * p.width() + x) * 4) as usize;
1040 let d = p.data();
1041 [d[i], d[i + 1], d[i + 2], d[i + 3]]
1042 }
1043
1044 fn set(p: &mut AzulPixmap, x: u32, y: u32, c: [u8; 4]) {
1045 let i = ((y * p.width() + x) * 4) as usize;
1046 p.data_mut()[i..i + 4].copy_from_slice(&c);
1047 }
1048
1049 fn lrect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
1050 LogicalRect {
1051 origin: LogicalPosition { x, y },
1052 size: LogicalSize {
1053 width: w,
1054 height: h,
1055 },
1056 }
1057 }
1058
1059 fn approx(a: f32, b: f32) -> bool {
1060 (a - b).abs() < 1e-4
1061 }
1062
1063 fn rect_path(x: f64, y: f64, w: f64, h: f64) -> PathStorage {
1064 let mut p = PathStorage::new();
1065 p.move_to(x, y);
1066 p.line_to(x + w, y);
1067 p.line_to(x + w, y + h);
1068 p.line_to(x, y + h);
1069 p.close_polygon(0);
1070 p
1071 }
1072
1073 fn red() -> Rgba8 {
1074 Rgba8::new(255, 0, 0, 255)
1075 }
1076
1077 fn two_stop_lut() -> GradientLut {
1078 let mut lut = GradientLut::new(256);
1079 lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
1080 lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
1081 lut.build_lut();
1082 lut
1083 }
1084
1085 fn encode_custom_png(w: u32, h: u32, ct: png::ColorType, data: &[u8]) -> Vec<u8> {
1088 let mut buf = Vec::new();
1089 {
1090 let mut enc = png::Encoder::new(&mut buf, w, h);
1091 enc.set_color(ct);
1092 enc.set_depth(png::BitDepth::Eight);
1093 let mut wr = enc.write_header().expect("test PNG header");
1094 wr.write_image_data(data).expect("test PNG data");
1095 }
1096 buf
1097 }
1098
1099 fn temp_path(tag: &str) -> std::path::PathBuf {
1100 std::env::temp_dir().join(format!(
1101 "azul_autotest_pixmap_{}_{tag}.bin",
1102 std::process::id()
1103 ))
1104 }
1105
1106 #[test]
1111 fn rect_intersection_overlap_is_the_common_area() {
1112 let a = lrect(0.0, 0.0, 10.0, 10.0);
1113 let b = lrect(5.0, 5.0, 10.0, 10.0);
1114 let i = rect_intersection(&a, &b).expect("rects overlap");
1115 assert!(approx(i.origin.x, 5.0));
1116 assert!(approx(i.origin.y, 5.0));
1117 assert!(approx(i.size.width, 5.0));
1118 assert!(approx(i.size.height, 5.0));
1119 }
1120
1121 #[test]
1122 fn rect_intersection_is_commutative() {
1123 let a = lrect(-3.0, 2.0, 8.0, 4.0);
1124 let b = lrect(1.0, 1.0, 9.0, 9.0);
1125 let ab = rect_intersection(&a, &b).expect("overlap");
1126 let ba = rect_intersection(&b, &a).expect("overlap");
1127 assert!(approx(ab.origin.x, ba.origin.x));
1128 assert!(approx(ab.origin.y, ba.origin.y));
1129 assert!(approx(ab.size.width, ba.size.width));
1130 assert!(approx(ab.size.height, ba.size.height));
1131 }
1132
1133 #[test]
1134 fn rect_intersection_zero_sized_rect_is_none() {
1135 let a = lrect(0.0, 0.0, 0.0, 0.0);
1136 let b = lrect(0.0, 0.0, 10.0, 10.0);
1137 assert!(rect_intersection(&a, &b).is_none());
1139 assert!(rect_intersection(&b, &a).is_none());
1140 }
1141
1142 #[test]
1143 fn rect_intersection_touching_edges_is_none() {
1144 let a = lrect(0.0, 0.0, 5.0, 5.0);
1146 let b = lrect(5.0, 0.0, 5.0, 5.0);
1147 assert!(rect_intersection(&a, &b).is_none());
1148 }
1149
1150 #[test]
1151 fn rect_intersection_disjoint_is_none() {
1152 let a = lrect(0.0, 0.0, 1.0, 1.0);
1153 let b = lrect(100.0, 100.0, 1.0, 1.0);
1154 assert!(rect_intersection(&a, &b).is_none());
1155 }
1156
1157 #[test]
1158 fn rect_intersection_negative_coordinates() {
1159 let a = lrect(-10.0, -10.0, 5.0, 5.0);
1160 let b = lrect(-7.0, -7.0, 5.0, 5.0);
1161 let i = rect_intersection(&a, &b).expect("overlap in negative quadrant");
1162 assert!(approx(i.origin.x, -7.0));
1163 assert!(approx(i.origin.y, -7.0));
1164 assert!(approx(i.size.width, 2.0));
1165 assert!(approx(i.size.height, 2.0));
1166 }
1167
1168 #[test]
1169 fn rect_intersection_result_never_exceeds_either_input() {
1170 let a = lrect(-1.0, -1.0, 3.0, 100.0);
1171 let b = lrect(0.0, 0.0, 100.0, 3.0);
1172 let i = rect_intersection(&a, &b).expect("overlap");
1173 assert!(i.size.width <= a.size.width && i.size.width <= b.size.width);
1174 assert!(i.size.height <= a.size.height && i.size.height <= b.size.height);
1175 }
1176
1177 #[test]
1178 fn rect_intersection_f32_max_does_not_panic() {
1179 let a = lrect(0.0, 0.0, f32::MAX, f32::MAX);
1180 let b = lrect(0.0, 0.0, f32::MAX, f32::MAX);
1181 let i = rect_intersection(&a, &b).expect("both cover the same huge area");
1182 assert!(i.size.width > 0.0 && i.size.height > 0.0);
1183 }
1184
1185 #[test]
1186 fn rect_intersection_nan_does_not_panic_and_stays_finite() {
1187 let nan = lrect(f32::NAN, f32::NAN, f32::NAN, f32::NAN);
1188 let ok = lrect(0.0, 0.0, 10.0, 10.0);
1189 for r in [
1192 rect_intersection(&nan, &ok),
1193 rect_intersection(&ok, &nan),
1194 rect_intersection(&nan, &nan),
1195 ]
1196 .into_iter()
1197 .flatten()
1198 {
1199 assert!(!r.size.width.is_nan(), "NaN width leaked out: {r:?}");
1200 assert!(!r.size.height.is_nan(), "NaN height leaked out: {r:?}");
1201 assert!(r.size.width >= 0.0 && r.size.height >= 0.0);
1202 }
1203 }
1204
1205 #[test]
1206 fn rect_intersection_infinite_size_does_not_panic() {
1207 let inf = lrect(0.0, 0.0, f32::INFINITY, f32::INFINITY);
1208 let ok = lrect(2.0, 2.0, 4.0, 4.0);
1209 let i = rect_intersection(&inf, &ok).expect("infinite rect contains the finite one");
1210 assert!(approx(i.size.width, 4.0));
1211 assert!(approx(i.size.height, 4.0));
1212 }
1213
1214 #[test]
1219 fn union_rect_covers_both_inputs() {
1220 let a = lrect(0.0, 0.0, 2.0, 2.0);
1221 let b = lrect(8.0, 8.0, 2.0, 2.0);
1222 let u = union_rect(&a, &b);
1223 assert!(approx(u.origin.x, 0.0));
1224 assert!(approx(u.origin.y, 0.0));
1225 assert!(approx(u.size.width, 10.0));
1226 assert!(approx(u.size.height, 10.0));
1227 }
1228
1229 #[test]
1230 fn union_rect_with_self_is_identity() {
1231 let a = lrect(3.0, 4.0, 5.0, 6.0);
1232 let u = union_rect(&a, &a);
1233 assert!(approx(u.origin.x, 3.0) && approx(u.origin.y, 4.0));
1234 assert!(approx(u.size.width, 5.0) && approx(u.size.height, 6.0));
1235 }
1236
1237 #[test]
1238 fn union_rect_negative_origins() {
1239 let a = lrect(-5.0, -5.0, 1.0, 1.0);
1240 let b = lrect(5.0, 5.0, 1.0, 1.0);
1241 let u = union_rect(&a, &b);
1242 assert!(approx(u.origin.x, -5.0));
1243 assert!(approx(u.size.width, 11.0));
1244 assert!(approx(u.size.height, 11.0));
1245 }
1246
1247 #[test]
1248 fn union_rect_zero_size_still_extends_bounds() {
1249 let a = lrect(0.0, 0.0, 1.0, 1.0);
1251 let b = lrect(20.0, 20.0, 0.0, 0.0);
1252 let u = union_rect(&a, &b);
1253 assert!(approx(u.size.width, 20.0));
1254 assert!(approx(u.size.height, 20.0));
1255 }
1256
1257 #[test]
1258 fn union_rect_never_shrinks_below_its_inputs() {
1259 let a = lrect(1.0, 1.0, 4.0, 4.0);
1260 let b = lrect(2.0, 2.0, 1.0, 1.0); let u = union_rect(&a, &b);
1262 assert!(u.size.width >= a.size.width);
1263 assert!(u.size.height >= a.size.height);
1264 }
1265
1266 #[test]
1267 fn union_rect_infinite_inputs_do_not_panic() {
1268 let a = lrect(0.0, 0.0, f32::INFINITY, f32::INFINITY);
1269 let b = lrect(1.0, 1.0, 1.0, 1.0);
1270 let u = union_rect(&a, &b);
1271 assert!(u.size.width.is_infinite());
1272 assert!(u.size.height.is_infinite());
1273 }
1274
1275 #[test]
1280 fn logical_rect_to_az_rect_scales_by_dpi() {
1281 let r = lrect(1.0, 2.0, 3.0, 4.0);
1282 let a = logical_rect_to_az_rect(&r, 2.0).expect("positive size");
1283 assert!(approx(a.x, 2.0));
1284 assert!(approx(a.y, 4.0));
1285 assert!(approx(a.width, 6.0));
1286 assert!(approx(a.height, 8.0));
1287 }
1288
1289 #[test]
1290 fn logical_rect_to_az_rect_zero_dpi_is_none() {
1291 let r = lrect(1.0, 2.0, 3.0, 4.0);
1293 assert!(logical_rect_to_az_rect(&r, 0.0).is_none());
1294 }
1295
1296 #[test]
1297 fn logical_rect_to_az_rect_negative_dpi_is_none() {
1298 let r = lrect(1.0, 2.0, 3.0, 4.0);
1299 assert!(logical_rect_to_az_rect(&r, -1.0).is_none());
1300 }
1301
1302 #[test]
1303 fn logical_rect_to_az_rect_zero_size_is_none() {
1304 assert!(logical_rect_to_az_rect(&lrect(0.0, 0.0, 0.0, 10.0), 1.0).is_none());
1305 assert!(logical_rect_to_az_rect(&lrect(0.0, 0.0, 10.0, 0.0), 1.0).is_none());
1306 }
1307
1308 #[test]
1309 fn logical_rect_to_az_rect_nan_dpi_is_none() {
1310 let r = lrect(1.0, 2.0, 3.0, 4.0);
1311 assert!(logical_rect_to_az_rect(&r, f32::NAN).is_none());
1312 }
1313
1314 #[test]
1315 fn logical_rect_to_az_rect_infinite_dpi_is_none() {
1316 let r = lrect(1.0, 2.0, 3.0, 4.0);
1317 assert!(logical_rect_to_az_rect(&r, f32::INFINITY).is_none());
1318 assert!(logical_rect_to_az_rect(&r, f32::NEG_INFINITY).is_none());
1319 }
1320
1321 #[test]
1322 fn logical_rect_to_az_rect_overflow_to_inf_is_none() {
1323 let r = lrect(0.0, 0.0, f32::MAX, f32::MAX);
1325 assert!(logical_rect_to_az_rect(&r, 2.0).is_none());
1326 }
1327
1328 #[test]
1329 fn logical_rect_to_az_rect_nan_bounds_is_none() {
1330 let r = lrect(f32::NAN, 0.0, 10.0, 10.0);
1331 assert!(logical_rect_to_az_rect(&r, 1.0).is_none());
1332 }
1333
1334 #[test]
1335 fn az_rect_from_xywh_rejects_nonpositive_size() {
1336 assert!(AzRect::from_xywh(0.0, 0.0, 0.0, 1.0).is_none());
1337 assert!(AzRect::from_xywh(0.0, 0.0, 1.0, 0.0).is_none());
1338 assert!(AzRect::from_xywh(0.0, 0.0, -1.0, 1.0).is_none());
1339 assert!(AzRect::from_xywh(0.0, 0.0, 1.0, -1.0).is_none());
1340 }
1341
1342 #[test]
1343 fn az_rect_from_xywh_rejects_nonfinite() {
1344 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
1345 assert!(AzRect::from_xywh(bad, 0.0, 1.0, 1.0).is_none(), "x={bad}");
1346 assert!(AzRect::from_xywh(0.0, bad, 1.0, 1.0).is_none(), "y={bad}");
1347 assert!(AzRect::from_xywh(0.0, 0.0, bad, 1.0).is_none(), "w={bad}");
1348 assert!(AzRect::from_xywh(0.0, 0.0, 1.0, bad).is_none(), "h={bad}");
1349 }
1350 }
1351
1352 #[test]
1353 fn az_rect_from_xywh_keeps_fields_verbatim() {
1354 let r = AzRect::from_xywh(-3.5, 7.25, 1.5, 2.5).expect("valid");
1355 assert!(approx(r.x, -3.5));
1356 assert!(approx(r.y, 7.25));
1357 assert!(approx(r.width, 1.5));
1358 assert!(approx(r.height, 2.5));
1359 }
1360
1361 #[test]
1362 fn az_rect_from_xywh_smallest_positive_size_is_accepted() {
1363 assert!(AzRect::from_xywh(0.0, 0.0, f32::MIN_POSITIVE, f32::MIN_POSITIVE).is_some());
1364 }
1365
1366 #[test]
1371 fn az_rect_clip_contained_returns_self() {
1372 let inner = AzRect::from_xywh(2.0, 2.0, 2.0, 2.0).expect("valid");
1373 let outer = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).expect("valid");
1374 let c = inner.clip(&outer).expect("inner is fully inside outer");
1375 assert!(approx(c.x, 2.0) && approx(c.y, 2.0));
1376 assert!(approx(c.width, 2.0) && approx(c.height, 2.0));
1377 }
1378
1379 #[test]
1380 fn az_rect_clip_partial_overlap() {
1381 let a = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).expect("valid");
1382 let b = AzRect::from_xywh(5.0, 5.0, 10.0, 10.0).expect("valid");
1383 let c = a.clip(&b).expect("overlap");
1384 assert!(approx(c.x, 5.0) && approx(c.width, 5.0));
1385 }
1386
1387 #[test]
1388 fn az_rect_clip_disjoint_is_none() {
1389 let a = AzRect::from_xywh(0.0, 0.0, 1.0, 1.0).expect("valid");
1390 let b = AzRect::from_xywh(50.0, 50.0, 1.0, 1.0).expect("valid");
1391 assert!(a.clip(&b).is_none());
1392 }
1393
1394 #[test]
1395 fn az_rect_clip_touching_edge_is_none() {
1396 let a = AzRect::from_xywh(0.0, 0.0, 5.0, 5.0).expect("valid");
1397 let b = AzRect::from_xywh(5.0, 0.0, 5.0, 5.0).expect("valid");
1398 assert!(a.clip(&b).is_none());
1399 }
1400
1401 #[test]
1402 fn az_rect_clip_huge_rect_does_not_panic() {
1403 let a = AzRect::from_xywh(0.0, 0.0, f32::MAX, f32::MAX).expect("valid");
1404 let b = AzRect::from_xywh(1.0, 1.0, 2.0, 2.0).expect("valid");
1405 let c = a.clip(&b).expect("b is inside a");
1406 assert!(approx(c.width, 2.0) && approx(c.height, 2.0));
1407 }
1408
1409 #[test]
1414 fn intersect_clips_none_none_is_none() {
1415 assert!(intersect_clips(None, None).is_none());
1416 }
1417
1418 #[test]
1419 fn intersect_clips_none_current_adopts_new() {
1420 let new = AzRect::from_xywh(1.0, 2.0, 3.0, 4.0).expect("valid");
1421 let r = intersect_clips(None, Some(new)).expect("adopts new");
1422 assert!(approx(r.x, 1.0) && approx(r.width, 3.0));
1423 }
1424
1425 #[test]
1426 fn intersect_clips_none_new_keeps_current() {
1427 let cur = AzRect::from_xywh(1.0, 2.0, 3.0, 4.0).expect("valid");
1428 let r = intersect_clips(Some(cur), None).expect("keeps current");
1429 assert!(approx(r.x, 1.0) && approx(r.width, 3.0));
1430 }
1431
1432 #[test]
1433 fn intersect_clips_nested_shrinks_to_inner() {
1434 let outer = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).expect("valid");
1435 let inner = AzRect::from_xywh(2.0, 2.0, 3.0, 3.0).expect("valid");
1436 let r = intersect_clips(Some(outer), Some(inner)).expect("overlap");
1437 assert!(approx(r.x, 2.0) && approx(r.y, 2.0));
1438 assert!(approx(r.width, 3.0) && approx(r.height, 3.0));
1439 }
1440
1441 #[test]
1442 fn intersect_clips_never_grows() {
1443 let a = AzRect::from_xywh(0.0, 0.0, 10.0, 4.0).expect("valid");
1444 let b = AzRect::from_xywh(3.0, 0.0, 10.0, 10.0).expect("valid");
1445 let r = intersect_clips(Some(a), Some(b)).expect("overlap");
1446 assert!(r.width <= a.width && r.width <= b.width);
1447 assert!(r.height <= a.height && r.height <= b.height);
1448 }
1449
1450 #[test]
1451 fn intersect_clips_disjoint_is_some_zero_area_not_none() {
1452 let a = AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).expect("valid");
1455 let b = AzRect::from_xywh(10.0, 10.0, 4.0, 4.0).expect("valid");
1456 let r = intersect_clips(Some(a), Some(b)).expect("must stay Some, not unclipped");
1457 assert!(approx(r.width, 0.0), "empty clip must have zero width");
1458 assert!(approx(r.height, 0.0), "empty clip must have zero height");
1459 }
1460
1461 #[test]
1462 fn intersect_clips_never_yields_negative_extent() {
1463 let a = AzRect::from_xywh(0.0, 0.0, 1.0, 1.0).expect("valid");
1464 let b = AzRect::from_xywh(100.0, 100.0, 1.0, 1.0).expect("valid");
1465 let r = intersect_clips(Some(a), Some(b)).expect("some");
1466 assert!(r.width >= 0.0 && r.height >= 0.0);
1467 }
1468
1469 #[test]
1470 fn intersect_clips_with_self_is_idempotent() {
1471 let a = AzRect::from_xywh(2.0, 3.0, 4.0, 5.0).expect("valid");
1472 let r = intersect_clips(Some(a), Some(a)).expect("some");
1473 assert!(approx(r.x, 2.0) && approx(r.y, 3.0));
1474 assert!(approx(r.width, 4.0) && approx(r.height, 5.0));
1475 }
1476
1477 #[test]
1478 fn intersect_clips_huge_rects_do_not_panic() {
1479 let a = AzRect::from_xywh(0.0, 0.0, f32::MAX, f32::MAX).expect("valid");
1480 let b = AzRect::from_xywh(-1.0e30, -1.0e30, f32::MAX, f32::MAX).expect("valid");
1481 let r = intersect_clips(Some(a), Some(b)).expect("some");
1482 assert!(!r.width.is_nan() && !r.height.is_nan());
1483 assert!(r.width >= 0.0 && r.height >= 0.0);
1484 }
1485
1486 #[test]
1491 fn pixmap_new_zero_dimension_is_none() {
1492 assert!(AzulPixmap::new(0, 0).is_none());
1493 assert!(AzulPixmap::new(0, 16).is_none());
1494 assert!(AzulPixmap::new(16, 0).is_none());
1495 }
1496
1497 #[test]
1498 fn pixmap_new_invariants_hold() {
1499 let p = pm(3, 5);
1500 assert_eq!(p.width(), 3);
1501 assert_eq!(p.height(), 5);
1502 assert_eq!(p.data().len(), 3 * 5 * 4);
1503 assert!(
1504 p.data().iter().all(|&b| b == 255),
1505 "new() is documented to be opaque white"
1506 );
1507 }
1508
1509 #[test]
1510 fn pixmap_new_1x1_is_a_single_white_pixel() {
1511 let p = pm(1, 1);
1512 assert_eq!(p.data(), &WHITE);
1513 }
1514
1515 #[test]
1516 fn pixmap_new_absurd_dimensions_return_none_instead_of_aborting() {
1517 assert!(AzulPixmap::new(u32::MAX, u32::MAX).is_none());
1520 }
1521
1522 #[test]
1523 fn pixmap_getters_on_zero_sized_instance_do_not_panic() {
1524 let p = zero_sized();
1525 assert_eq!(p.width(), 0);
1526 assert_eq!(p.height(), 0);
1527 assert!(p.data().is_empty());
1528 }
1529
1530 #[test]
1531 fn data_mut_writes_are_visible_through_data() {
1532 let mut p = pm(2, 1);
1533 p.data_mut()[0] = 7;
1534 assert_eq!(p.data()[0], 7);
1535 assert_eq!(p.data().len(), 8);
1536 }
1537
1538 #[test]
1539 fn data_mut_on_zero_sized_instance_is_empty_not_panic() {
1540 let mut p = zero_sized();
1541 assert!(p.data_mut().is_empty());
1542 }
1543
1544 #[test]
1545 fn clone_pixmap_is_a_deep_copy() {
1546 let src = filled(2, 2, [1, 2, 3, 4]);
1547 let mut cloned = src.clone_pixmap();
1548 assert_eq!(cloned.width(), src.width());
1549 assert_eq!(cloned.height(), src.height());
1550 assert_eq!(cloned.data(), src.data());
1551
1552 set(&mut cloned, 0, 0, [9, 9, 9, 9]);
1553 assert_eq!(get(&src, 0, 0), [1, 2, 3, 4], "clone must not alias source");
1554 }
1555
1556 #[test]
1557 fn clone_pixmap_of_zero_sized_instance_does_not_panic() {
1558 let p = zero_sized();
1559 let c = p.clone_pixmap();
1560 assert_eq!(c.width(), 0);
1561 assert!(c.data().is_empty());
1562 }
1563
1564 #[test]
1569 fn fill_sets_every_channel_of_every_pixel() {
1570 let p = filled(4, 3, [10, 20, 30, 40]);
1571 for y in 0..3 {
1572 for x in 0..4 {
1573 assert_eq!(get(&p, x, y), [10, 20, 30, 40]);
1574 }
1575 }
1576 }
1577
1578 #[test]
1579 fn fill_with_u8_extremes() {
1580 let p = filled(2, 2, [0, 0, 0, 0]);
1581 assert!(p.data().iter().all(|&b| b == 0));
1582 let p = filled(2, 2, [255, 255, 255, 255]);
1583 assert!(p.data().iter().all(|&b| b == 255));
1584 }
1585
1586 #[test]
1587 fn fill_on_zero_sized_pixmap_does_not_panic() {
1588 let mut p = zero_sized();
1589 p.fill(1, 2, 3, 4);
1590 assert!(p.data().is_empty());
1591 }
1592
1593 #[test]
1598 fn fill_rect_fills_exactly_the_requested_box() {
1599 let mut p = filled(5, 5, CLEAR);
1600 p.fill_rect(1, 1, 2, 2, 1, 2, 3, 4);
1601 assert_eq!(get(&p, 1, 1), [1, 2, 3, 4]);
1602 assert_eq!(get(&p, 2, 2), [1, 2, 3, 4]);
1603 assert_eq!(get(&p, 0, 0), CLEAR, "outside the box must be untouched");
1604 assert_eq!(get(&p, 3, 3), CLEAR, "x1/y1 are exclusive");
1605 }
1606
1607 #[test]
1608 fn fill_rect_zero_size_is_a_noop() {
1609 let mut p = filled(4, 4, CLEAR);
1610 p.fill_rect(1, 1, 0, 0, 255, 0, 0, 255);
1611 assert!(p.data().iter().all(|&b| b == 0));
1612 }
1613
1614 #[test]
1615 fn fill_rect_negative_origin_clips_to_the_pixmap() {
1616 let mut p = filled(4, 4, CLEAR);
1617 p.fill_rect(-2, -2, 4, 4, 9, 9, 9, 9);
1618 assert_eq!(get(&p, 0, 0), [9, 9, 9, 9]);
1620 assert_eq!(get(&p, 1, 1), [9, 9, 9, 9]);
1621 assert_eq!(get(&p, 2, 2), CLEAR);
1622 }
1623
1624 #[test]
1625 fn fill_rect_fully_offscreen_is_a_noop() {
1626 let mut p = filled(4, 4, CLEAR);
1627 p.fill_rect(100, 100, 10, 10, 255, 0, 0, 255);
1628 p.fill_rect(-100, -100, 10, 10, 255, 0, 0, 255);
1629 assert!(p.data().iter().all(|&b| b == 0));
1630 }
1631
1632 #[test]
1633 fn fill_rect_negative_width_does_not_panic() {
1634 let mut p = filled(8, 8, CLEAR);
1637 p.fill_rect(3, 0, -5, 2, 255, 0, 0, 255);
1638 assert!(
1639 p.data().iter().all(|&b| b == 0),
1640 "a negative-width rect must paint nothing"
1641 );
1642 }
1643
1644 #[test]
1645 fn fill_rect_negative_height_is_a_noop() {
1646 let mut p = filled(8, 8, CLEAR);
1647 p.fill_rect(0, 3, 2, -5, 255, 0, 0, 255);
1648 assert!(p.data().iter().all(|&b| b == 0));
1649 }
1650
1651 #[test]
1652 fn fill_rect_i32_extremes_do_not_panic() {
1653 let mut p = filled(4, 4, CLEAR);
1654 p.fill_rect(i32::MIN, i32::MIN, i32::MAX, i32::MAX, 1, 1, 1, 1);
1655 p.fill_rect(i32::MAX, i32::MAX, i32::MAX, i32::MAX, 2, 2, 2, 2);
1656 p.fill_rect(0, 0, i32::MAX, i32::MAX, 3, 3, 3, 3);
1657 assert_eq!(get(&p, 0, 0), [3, 3, 3, 3]);
1659 assert_eq!(get(&p, 3, 3), [3, 3, 3, 3]);
1660 }
1661
1662 #[test]
1663 fn fill_rect_on_zero_sized_pixmap_does_not_panic() {
1664 let mut p = zero_sized();
1665 p.fill_rect(0, 0, 10, 10, 1, 2, 3, 4);
1666 assert!(p.data().is_empty());
1667 }
1668
1669 #[test]
1674 fn resize_grow_only_rejects_shrinking() {
1675 let mut p = filled(4, 4, [1, 2, 3, 4]);
1676 assert!(p.resize_grow_only(2, 4, 0, 0, 0, 0).is_none());
1677 assert!(p.resize_grow_only(4, 2, 0, 0, 0, 0).is_none());
1678 assert!(p.resize_grow_only(2, 2, 0, 0, 0, 0).is_none());
1679 assert!(p.resize_grow_only(8, 2, 0, 0, 0, 0).is_none());
1681 assert_eq!(p.width(), 4, "a rejected resize must not mutate");
1682 assert_eq!(p.height(), 4);
1683 assert_eq!(p.data().len(), 4 * 4 * 4);
1684 }
1685
1686 #[test]
1687 fn resize_grow_only_same_dimensions_is_a_noop_some() {
1688 let mut p = filled(3, 3, [7, 7, 7, 7]);
1689 assert!(p.resize_grow_only(3, 3, 0, 0, 0, 0).is_some());
1690 assert_eq!(p.width(), 3);
1691 assert!(p.data().iter().all(|&b| b == 7));
1692 }
1693
1694 #[test]
1695 fn resize_grow_only_preserves_topleft_and_fills_the_new_strips() {
1696 let mut p = marked(2, 2);
1697 p.resize_grow_only(4, 4, 9, 8, 7, 6).expect("growing is allowed");
1698 assert_eq!(p.width(), 4);
1699 assert_eq!(p.height(), 4);
1700 assert_eq!(p.data().len(), 4 * 4 * 4);
1701 assert_eq!(get(&p, 0, 0), [0, 0, 0, 255]);
1703 assert_eq!(get(&p, 1, 0), [1, 0, 0, 255]);
1704 assert_eq!(get(&p, 0, 1), [2, 0, 0, 255]);
1705 assert_eq!(get(&p, 1, 1), [3, 0, 0, 255]);
1706 assert_eq!(get(&p, 3, 0), [9, 8, 7, 6]);
1708 assert_eq!(get(&p, 0, 3), [9, 8, 7, 6]);
1709 assert_eq!(get(&p, 3, 3), [9, 8, 7, 6]);
1710 }
1711
1712 #[test]
1713 fn resize_grow_only_grow_one_axis_only() {
1714 let mut p = marked(2, 2);
1715 p.resize_grow_only(2, 4, 1, 1, 1, 1).expect("height-only growth");
1716 assert_eq!(p.width(), 2);
1717 assert_eq!(p.height(), 4);
1718 assert_eq!(get(&p, 1, 1), [3, 0, 0, 255]);
1719 assert_eq!(get(&p, 0, 3), [1, 1, 1, 1]);
1720 }
1721
1722 #[test]
1723 fn resize_grow_only_from_zero_sized_pixmap_does_not_panic() {
1724 let mut p = zero_sized();
1725 p.resize_grow_only(2, 2, 5, 5, 5, 5)
1726 .expect("0x0 -> 2x2 is a growth");
1727 assert_eq!(p.width(), 2);
1728 assert_eq!(p.data().len(), 16);
1729 assert_eq!(get(&p, 0, 0), [5, 5, 5, 5]);
1730 }
1731
1732 #[test]
1737 fn resize_reuse_same_dimensions_is_a_noop() {
1738 let mut p = filled(3, 3, [4, 4, 4, 4]);
1739 p.resize_reuse(3, 3, 0, 0, 0, 0);
1740 assert!(p.data().iter().all(|&b| b == 4));
1741 }
1742
1743 #[test]
1744 fn resize_reuse_grow_preserves_overlap_and_fills_the_rest() {
1745 let mut p = marked(2, 2);
1746 p.resize_reuse(4, 3, 1, 2, 3, 4);
1747 assert_eq!(p.width(), 4);
1748 assert_eq!(p.height(), 3);
1749 assert_eq!(p.data().len(), 4 * 3 * 4);
1750 assert_eq!(get(&p, 0, 0), [0, 0, 0, 255]);
1751 assert_eq!(get(&p, 1, 1), [3, 0, 0, 255]);
1752 assert_eq!(get(&p, 3, 0), [1, 2, 3, 4]);
1753 assert_eq!(get(&p, 0, 2), [1, 2, 3, 4]);
1754 }
1755
1756 #[test]
1757 fn resize_reuse_shrink_crops_to_the_topleft() {
1758 let mut p = marked(4, 4);
1759 p.resize_reuse(2, 2, 0, 0, 0, 0);
1760 assert_eq!(p.width(), 2);
1761 assert_eq!(p.height(), 2);
1762 assert_eq!(p.data().len(), 2 * 2 * 4);
1763 assert_eq!(get(&p, 0, 0), [0, 0, 0, 255]);
1765 assert_eq!(get(&p, 1, 0), [1, 0, 0, 255]);
1766 assert_eq!(get(&p, 0, 1), [4, 0, 0, 255]);
1767 assert_eq!(get(&p, 1, 1), [5, 0, 0, 255]);
1768 }
1769
1770 #[test]
1771 fn resize_reuse_shrink_width_only_keeps_rows_aligned() {
1772 let mut p = marked(4, 2);
1773 p.resize_reuse(2, 2, 0, 0, 0, 0);
1774 assert_eq!(get(&p, 0, 1), [4, 0, 0, 255], "row 1 must not be smeared");
1775 assert_eq!(get(&p, 1, 1), [5, 0, 0, 255]);
1776 }
1777
1778 #[test]
1779 fn resize_reuse_to_zero_yields_an_empty_buffer() {
1780 let mut p = filled(4, 4, [1, 1, 1, 1]);
1781 p.resize_reuse(0, 0, 2, 2, 2, 2);
1782 assert_eq!(p.width(), 0);
1783 assert_eq!(p.height(), 0);
1784 assert!(p.data().is_empty());
1785 }
1786
1787 #[test]
1788 fn resize_reuse_from_zero_sized_pixmap_does_not_panic() {
1789 let mut p = zero_sized();
1790 p.resize_reuse(2, 2, 3, 3, 3, 3);
1791 assert_eq!(p.width(), 2);
1792 assert_eq!(get(&p, 0, 0), [3, 3, 3, 3]);
1793 }
1794
1795 #[test]
1796 fn resize_reuse_data_len_always_matches_the_new_dimensions() {
1797 let mut p = marked(3, 3);
1798 for (w, h) in [(1u32, 1u32), (5, 2), (2, 5), (7, 7), (1, 9)] {
1799 p.resize_reuse(w, h, 0, 0, 0, 0);
1800 assert_eq!(p.width(), w);
1801 assert_eq!(p.height(), h);
1802 assert_eq!(p.data().len(), (w as usize) * (h as usize) * 4);
1803 }
1804 }
1805
1806 #[test]
1811 fn png_round_trip_preserves_dimensions_and_pixels() {
1812 let src = marked(5, 3);
1813 let bytes = src.encode_png().expect("encode");
1814 let back = AzulPixmap::decode_png(&bytes).expect("decode");
1815 assert_eq!(back.width(), src.width());
1816 assert_eq!(back.height(), src.height());
1817 assert_eq!(back.data(), src.data());
1818 }
1819
1820 #[test]
1821 fn png_round_trip_1x1() {
1822 let mut src = pm(1, 1);
1823 set(&mut src, 0, 0, [1, 2, 3, 4]);
1824 let bytes = src.encode_png().expect("encode");
1825 let back = AzulPixmap::decode_png(&bytes).expect("decode");
1826 assert_eq!(back.data(), &[1, 2, 3, 4]);
1827 }
1828
1829 #[test]
1830 fn png_round_trip_preserves_full_alpha_range() {
1831 let mut src = pm(4, 1);
1832 set(&mut src, 0, 0, [0, 0, 0, 0]);
1833 set(&mut src, 1, 0, [255, 255, 255, 255]);
1834 set(&mut src, 2, 0, [255, 0, 0, 1]);
1835 set(&mut src, 3, 0, [0, 255, 0, 254]);
1836 let bytes = src.encode_png().expect("encode");
1837 let back = AzulPixmap::decode_png(&bytes).expect("decode");
1838 assert_eq!(back.data(), src.data(), "PNG must not premultiply alpha");
1839 }
1840
1841 #[test]
1842 fn encode_png_starts_with_the_png_signature() {
1843 let bytes = pm(2, 2).encode_png().expect("encode");
1844 assert_eq!(&bytes[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]);
1845 }
1846
1847 #[test]
1848 fn encode_png_of_a_zero_sized_pixmap_is_err_not_panic() {
1849 let p = zero_sized();
1850 let e = p.encode_png().expect_err("PNG forbids zero dimensions");
1851 assert!(e.contains("PNG header error"), "unexpected message: {e}");
1852 }
1853
1854 #[test]
1855 fn decode_png_empty_input_is_err() {
1856 assert!(AzulPixmap::decode_png(&[]).is_err());
1857 }
1858
1859 #[test]
1860 fn decode_png_whitespace_only_is_err() {
1861 assert!(AzulPixmap::decode_png(b" ").is_err());
1862 assert!(AzulPixmap::decode_png(b"\t\n\r\n").is_err());
1863 }
1864
1865 #[test]
1866 fn decode_png_garbage_is_err() {
1867 assert!(AzulPixmap::decode_png(b"not a png at all").is_err());
1868 assert!(AzulPixmap::decode_png(&[0x00, 0x01, 0x02, 0x03]).is_err());
1869 }
1870
1871 #[test]
1872 fn decode_png_invalid_utf8_bytes_are_err() {
1873 assert!(AzulPixmap::decode_png(&[0xFF, 0xFE, 0x00]).is_err());
1874 assert!(AzulPixmap::decode_png(&[0xC0, 0x80, 0xED, 0xA0, 0x80]).is_err());
1875 }
1876
1877 #[test]
1878 fn decode_png_unicode_text_is_err() {
1879 assert!(AzulPixmap::decode_png("\u{1F600} héllo n\u{0303}".as_bytes()).is_err());
1880 }
1881
1882 #[test]
1883 fn decode_png_boundary_number_strings_are_err() {
1884 for s in ["0", "-0", "9223372036854775807", "NaN", "inf", "-inf", "1e999"] {
1885 assert!(
1886 AzulPixmap::decode_png(s.as_bytes()).is_err(),
1887 "{s:?} is not a PNG"
1888 );
1889 }
1890 }
1891
1892 #[test]
1893 fn decode_png_extremely_long_garbage_is_err_and_terminates() {
1894 let junk = vec![0x41u8; 1_000_000];
1895 assert!(AzulPixmap::decode_png(&junk).is_err());
1896 }
1897
1898 #[test]
1899 fn decode_png_deeply_nested_brackets_is_err() {
1900 let mut nested = vec![b'['; 10_000];
1901 nested.extend(std::iter::repeat_n(b']', 10_000));
1902 assert!(AzulPixmap::decode_png(&nested).is_err());
1903 }
1904
1905 #[test]
1906 fn decode_png_signature_without_chunks_is_err() {
1907 let sig = [0x89u8, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
1908 assert!(AzulPixmap::decode_png(&sig).is_err());
1909 }
1910
1911 #[test]
1912 fn decode_png_truncated_valid_png_is_err() {
1913 let full = pm(4, 4).encode_png().expect("encode");
1914 for cut in [9, full.len() / 2, full.len() - 1] {
1915 assert!(
1916 AzulPixmap::decode_png(&full[..cut]).is_err(),
1917 "a PNG truncated to {cut} bytes must not decode"
1918 );
1919 }
1920 }
1921
1922 #[test]
1923 fn decode_png_leading_junk_is_rejected() {
1924 let full = pm(2, 2).encode_png().expect("encode");
1925 let mut with_junk = b"garbage".to_vec();
1926 with_junk.extend_from_slice(&full);
1927 assert!(
1928 AzulPixmap::decode_png(&with_junk).is_err(),
1929 "the decoder must not scan forward for a signature"
1930 );
1931 }
1932
1933 #[test]
1934 fn decode_png_trailing_junk_after_iend_still_decodes() {
1935 let src = marked(2, 2);
1936 let mut bytes = src.encode_png().expect("encode");
1937 bytes.extend_from_slice(b"garbage;after;iend");
1938 let back = AzulPixmap::decode_png(&bytes).expect("bytes past IEND are outside the stream");
1939 assert_eq!(back.data(), src.data());
1940 }
1941
1942 #[test]
1943 fn decode_png_rgb_expands_to_opaque_rgba() {
1944 let bytes = encode_custom_png(2, 1, png::ColorType::Rgb, &[10, 20, 30, 40, 50, 60]);
1945 let p = AzulPixmap::decode_png(&bytes).expect("RGB is supported");
1946 assert_eq!(p.width(), 2);
1947 assert_eq!(p.height(), 1);
1948 assert_eq!(p.data(), &[10, 20, 30, 255, 40, 50, 60, 255]);
1949 }
1950
1951 #[test]
1952 fn decode_png_grayscale_expands_to_rgba() {
1953 let bytes = encode_custom_png(2, 1, png::ColorType::Grayscale, &[7, 9]);
1954 let p = AzulPixmap::decode_png(&bytes).expect("grayscale is supported");
1955 assert_eq!(p.data(), &[7, 7, 7, 255, 9, 9, 9, 255]);
1956 }
1957
1958 #[test]
1959 fn decode_png_unsupported_color_type_is_err_not_panic() {
1960 let bytes = encode_custom_png(1, 1, png::ColorType::GrayscaleAlpha, &[7, 128]);
1961 let e = AzulPixmap::decode_png(&bytes).expect_err("gray+alpha is not handled");
1962 assert!(
1963 e.contains("Unsupported PNG color type"),
1964 "unexpected message: {e}"
1965 );
1966 }
1967
1968 fn diff_result(diff_count: u64, total_pixels: u64, dimensions_match: bool) -> PixelDiffResult {
1973 PixelDiffResult {
1974 diff_count,
1975 total_pixels,
1976 max_delta: 0,
1977 dimensions_match,
1978 ref_width: 1,
1979 ref_height: 1,
1980 test_width: 1,
1981 test_height: 1,
1982 }
1983 }
1984
1985 #[test]
1986 fn is_match_is_true_only_when_dimensions_match_and_no_pixel_differs() {
1987 assert!(diff_result(0, 100, true).is_match());
1988 assert!(!diff_result(1, 100, true).is_match());
1989 assert!(!diff_result(0, 100, false).is_match());
1990 assert!(!diff_result(u64::MAX, 100, true).is_match());
1991 }
1992
1993 #[test]
1994 fn is_match_on_an_empty_comparison_is_deterministic() {
1995 assert!(diff_result(0, 0, true).is_match());
1997 assert!(!diff_result(0, 0, false).is_match());
1998 }
1999
2000 #[test]
2001 fn diff_ratio_of_zero_total_is_zero_not_nan() {
2002 let r = diff_result(0, 0, true).diff_ratio();
2003 assert!(r.is_finite() && r == 0.0, "0/0 must not become NaN");
2004 }
2005
2006 #[test]
2007 fn diff_ratio_basic_values() {
2008 assert!((diff_result(0, 100, true).diff_ratio() - 0.0).abs() < 1e-12);
2009 assert!((diff_result(50, 100, true).diff_ratio() - 0.5).abs() < 1e-12);
2010 assert!((diff_result(100, 100, true).diff_ratio() - 1.0).abs() < 1e-12);
2011 }
2012
2013 #[test]
2014 fn diff_ratio_at_u64_max_stays_finite() {
2015 let r = diff_result(u64::MAX, u64::MAX, true).diff_ratio();
2016 assert!(r.is_finite(), "u64::MAX/u64::MAX must not overflow to inf");
2017 assert!((r - 1.0).abs() < 1e-9);
2018 }
2019
2020 #[test]
2021 fn diff_ratio_on_a_dimension_mismatch_is_zero_yet_not_a_match() {
2022 let r = diff_result(0, 0, false);
2025 assert!((r.diff_ratio() - 0.0).abs() < 1e-12);
2026 assert!(!r.is_match());
2027 }
2028
2029 #[test]
2034 fn pixel_diff_identical_images_match() {
2035 let a = filled(4, 4, [1, 2, 3, 4]);
2036 let b = filled(4, 4, [1, 2, 3, 4]);
2037 let r = pixel_diff(&a, &b, 0);
2038 assert!(r.is_match());
2039 assert_eq!(r.diff_count, 0);
2040 assert_eq!(r.max_delta, 0);
2041 assert_eq!(r.total_pixels, 16);
2042 }
2043
2044 #[test]
2045 fn pixel_diff_threshold_is_exclusive_at_the_boundary() {
2046 let a = filled(2, 2, [10, 10, 10, 255]);
2047 let b = filled(2, 2, [13, 10, 10, 255]); assert!(
2049 pixel_diff(&a, &b, 3).is_match(),
2050 "delta == threshold is within tolerance"
2051 );
2052 assert!(
2053 !pixel_diff(&a, &b, 2).is_match(),
2054 "delta > threshold must be reported"
2055 );
2056 assert_eq!(pixel_diff(&a, &b, 2).diff_count, 4);
2057 assert_eq!(pixel_diff(&a, &b, 3).max_delta, 3, "max_delta ignores the threshold");
2058 }
2059
2060 #[test]
2061 fn pixel_diff_threshold_zero_catches_a_single_bit() {
2062 let a = filled(2, 2, [0, 0, 0, 0]);
2063 let mut b = filled(2, 2, [0, 0, 0, 0]);
2064 set(&mut b, 1, 1, [0, 0, 0, 1]);
2065 let r = pixel_diff(&a, &b, 0);
2066 assert!(!r.is_match());
2067 assert_eq!(r.diff_count, 1);
2068 assert_eq!(r.max_delta, 1);
2069 }
2070
2071 #[test]
2072 fn pixel_diff_threshold_255_always_matches() {
2073 let a = filled(3, 3, [0, 0, 0, 0]);
2075 let b = filled(3, 3, [255, 255, 255, 255]);
2076 let r = pixel_diff(&a, &b, 255);
2077 assert!(r.is_match(), "threshold 255 tolerates every possible delta");
2078 assert_eq!(r.diff_count, 0);
2079 assert_eq!(r.max_delta, 255);
2080 }
2081
2082 #[test]
2083 fn pixel_diff_max_delta_is_unsigned_and_direction_independent() {
2084 let a = filled(1, 1, [0, 0, 0, 0]);
2085 let b = filled(1, 1, [255, 0, 0, 0]);
2086 assert_eq!(pixel_diff(&a, &b, 0).max_delta, 255, "no wraparound");
2087 assert_eq!(
2088 pixel_diff(&b, &a, 0).max_delta,
2089 255,
2090 "reference/test order must not change |delta|"
2091 );
2092 }
2093
2094 #[test]
2095 fn pixel_diff_all_pixels_different_ratio_is_one() {
2096 let a = filled(4, 4, [0, 0, 0, 0]);
2097 let b = filled(4, 4, [255, 255, 255, 255]);
2098 let r = pixel_diff(&a, &b, 0);
2099 assert_eq!(r.diff_count, r.total_pixels);
2100 assert!((r.diff_ratio() - 1.0).abs() < 1e-12);
2101 }
2102
2103 #[test]
2104 fn pixel_diff_dimension_mismatch_reports_both_sizes_and_no_match() {
2105 let a = filled(4, 4, CLEAR);
2106 let b = filled(2, 8, CLEAR);
2107 let r = pixel_diff(&a, &b, 0);
2108 assert!(!r.is_match());
2109 assert!(!r.dimensions_match);
2110 assert_eq!(r.total_pixels, 0);
2111 assert_eq!(r.diff_count, 0);
2112 assert_eq!((r.ref_width, r.ref_height), (4, 4));
2113 assert_eq!((r.test_width, r.test_height), (2, 8));
2114 }
2115
2116 #[test]
2117 fn pixel_diff_ratio_is_always_within_0_and_1() {
2118 let a = marked(4, 4);
2119 let b = filled(4, 4, [0, 0, 0, 0]);
2120 for t in [0u8, 1, 127, 254, 255] {
2121 let r = pixel_diff(&a, &b, t);
2122 let ratio = r.diff_ratio();
2123 assert!((0.0..=1.0).contains(&ratio), "ratio {ratio} out of range at t={t}");
2124 assert!(r.diff_count <= r.total_pixels);
2125 }
2126 }
2127
2128 #[test]
2129 fn pixel_diff_on_zero_sized_pixmaps_does_not_panic() {
2130 let a = zero_sized();
2131 let b = zero_sized();
2132 let r = pixel_diff(&a, &b, 0);
2133 assert!(r.is_match());
2134 assert_eq!(r.total_pixels, 0);
2135 assert!((r.diff_ratio() - 0.0).abs() < 1e-12);
2136 }
2137
2138 #[test]
2143 fn compare_against_reference_missing_file_is_err() {
2144 let p = pm(2, 2);
2145 let e = compare_against_reference(&p, "/nonexistent/azul/does_not_exist.png", 0)
2146 .expect_err("missing file");
2147 assert!(e.contains("Cannot read reference image"), "got: {e}");
2148 }
2149
2150 #[test]
2151 fn compare_against_reference_empty_path_is_err() {
2152 let p = pm(2, 2);
2153 assert!(compare_against_reference(&p, "", 0).is_err());
2154 }
2155
2156 #[test]
2157 fn compare_against_reference_whitespace_path_is_err() {
2158 let p = pm(2, 2);
2159 assert!(compare_against_reference(&p, " ", 0).is_err());
2160 assert!(compare_against_reference(&p, "\t\n", 0).is_err());
2161 }
2162
2163 #[test]
2164 fn compare_against_reference_nul_byte_path_is_err_not_panic() {
2165 let p = pm(2, 2);
2166 assert!(compare_against_reference(&p, "bad\0path.png", 0).is_err());
2167 }
2168
2169 #[test]
2170 fn compare_against_reference_unicode_path_is_err() {
2171 let p = pm(2, 2);
2172 assert!(compare_against_reference(&p, "/tmp/\u{1F600}/nope.png", 0).is_err());
2173 }
2174
2175 #[test]
2176 fn compare_against_reference_extremely_long_path_is_err_not_panic() {
2177 let p = pm(2, 2);
2178 let long = format!("/tmp/{}.png", "a".repeat(100_000));
2179 assert!(compare_against_reference(&p, &long, 0).is_err());
2180 }
2181
2182 #[test]
2183 fn compare_against_reference_boundary_number_paths_are_err() {
2184 let p = pm(2, 2);
2185 for s in ["0", "-0", "NaN", "inf", "9223372036854775807"] {
2186 assert!(compare_against_reference(&p, s, 0).is_err(), "{s:?}");
2187 }
2188 }
2189
2190 #[test]
2191 fn compare_against_reference_non_png_file_is_err() {
2192 let path = temp_path("not_a_png");
2193 std::fs::write(&path, b"definitely not a png").expect("write temp file");
2194 let p = pm(2, 2);
2195 let res = compare_against_reference(&p, path.to_str().expect("utf8 path"), 0);
2196 let _ = std::fs::remove_file(&path);
2197 let e = res.expect_err("a non-PNG file must not decode");
2198 assert!(e.contains("PNG decode error"), "got: {e}");
2199 }
2200
2201 #[test]
2202 fn compare_against_reference_valid_png_matches_itself() {
2203 let src = marked(3, 2);
2204 let path = temp_path("valid_ref");
2205 std::fs::write(&path, src.encode_png().expect("encode")).expect("write temp file");
2206 let res = compare_against_reference(&src, path.to_str().expect("utf8 path"), 0);
2207 let _ = std::fs::remove_file(&path);
2208 let r = res.expect("a freshly written reference must decode");
2209 assert!(r.is_match(), "an image must match its own PNG: {r:?}");
2210 assert_eq!(r.total_pixels, 6);
2211 }
2212
2213 #[test]
2214 fn compare_against_reference_size_mismatch_is_ok_but_not_a_match() {
2215 let src = marked(3, 2);
2216 let path = temp_path("size_mismatch");
2217 std::fs::write(&path, src.encode_png().expect("encode")).expect("write temp file");
2218 let other = pm(4, 4);
2219 let res = compare_against_reference(&other, path.to_str().expect("utf8 path"), 0);
2220 let _ = std::fs::remove_file(&path);
2221 let r = res.expect("decoding succeeds; only the comparison fails");
2222 assert!(!r.is_match());
2223 assert!(!r.dimensions_match);
2224 }
2225
2226 #[test]
2231 fn blit_pixmap_full_opacity_opaque_src_overwrites_dst() {
2232 let src = filled(2, 2, [10, 20, 30, 255]);
2233 let mut dst = filled(4, 4, CLEAR);
2234 blit_pixmap(&src, &mut dst, 1, 1, 1.0);
2235 assert_eq!(get(&dst, 1, 1), [10, 20, 30, 255]);
2236 assert_eq!(get(&dst, 2, 2), [10, 20, 30, 255]);
2237 assert_eq!(get(&dst, 0, 0), CLEAR);
2238 assert_eq!(get(&dst, 3, 3), CLEAR);
2239 }
2240
2241 #[test]
2242 fn blit_pixmap_zero_opacity_leaves_dst_untouched() {
2243 let src = filled(2, 2, [10, 20, 30, 255]);
2244 let mut dst = filled(2, 2, WHITE);
2245 blit_pixmap(&src, &mut dst, 0, 0, 0.0);
2246 assert!(dst.data().iter().all(|&b| b == 255));
2247 }
2248
2249 #[test]
2250 fn blit_pixmap_transparent_src_leaves_dst_untouched() {
2251 let src = filled(2, 2, [10, 20, 30, 0]);
2252 let mut dst = filled(2, 2, WHITE);
2253 blit_pixmap(&src, &mut dst, 0, 0, 1.0);
2254 assert!(dst.data().iter().all(|&b| b == 255));
2255 }
2256
2257 #[test]
2258 fn blit_pixmap_nan_opacity_does_not_panic_and_blits_nothing() {
2259 let src = filled(2, 2, [10, 20, 30, 255]);
2261 let mut dst = filled(2, 2, WHITE);
2262 blit_pixmap(&src, &mut dst, 0, 0, f32::NAN);
2263 assert!(
2264 dst.data().iter().all(|&b| b == 255),
2265 "a NaN opacity must not paint anything"
2266 );
2267 }
2268
2269 #[test]
2270 fn blit_pixmap_infinite_opacity_clamps_to_fully_opaque() {
2271 let src = filled(1, 1, [10, 20, 30, 255]);
2272 let mut dst = filled(1, 1, WHITE);
2273 blit_pixmap(&src, &mut dst, 0, 0, f32::INFINITY);
2274 assert_eq!(get(&dst, 0, 0), [10, 20, 30, 255]);
2275 }
2276
2277 #[test]
2278 fn blit_pixmap_negative_infinite_opacity_clamps_to_transparent() {
2279 let src = filled(1, 1, [10, 20, 30, 255]);
2280 let mut dst = filled(1, 1, WHITE);
2281 blit_pixmap(&src, &mut dst, 0, 0, f32::NEG_INFINITY);
2282 assert_eq!(get(&dst, 0, 0), WHITE);
2283 }
2284
2285 #[test]
2286 fn blit_pixmap_opacity_above_one_clamps_to_one() {
2287 let src = filled(1, 1, [10, 20, 30, 255]);
2288 let mut a = filled(1, 1, CLEAR);
2289 let mut b = filled(1, 1, CLEAR);
2290 blit_pixmap(&src, &mut a, 0, 0, 1.0);
2291 blit_pixmap(&src, &mut b, 0, 0, 1.0e30);
2292 assert_eq!(a.data(), b.data(), "opacity is clamped to [0, 1]");
2293 }
2294
2295 #[test]
2296 fn blit_pixmap_half_alpha_blend_is_exact() {
2297 let src = filled(1, 1, [200, 100, 50, 128]);
2298 let mut dst = filled(1, 1, CLEAR);
2299 blit_pixmap(&src, &mut dst, 0, 0, 1.0);
2300 assert_eq!(get(&dst, 0, 0), [100, 50, 25, 128]);
2302 }
2303
2304 #[test]
2305 fn blit_pixmap_negative_position_clips_to_dst() {
2306 let src = marked(2, 2);
2307 let mut dst = filled(4, 4, CLEAR);
2308 blit_pixmap(&src, &mut dst, -1, -1, 1.0);
2309 assert_eq!(get(&dst, 0, 0), [3, 0, 0, 255]);
2311 assert_eq!(get(&dst, 1, 1), CLEAR);
2312 }
2313
2314 #[test]
2315 fn blit_pixmap_fully_offscreen_is_a_noop() {
2316 let src = filled(2, 2, [1, 2, 3, 255]);
2317 let mut dst = filled(4, 4, CLEAR);
2318 blit_pixmap(&src, &mut dst, 100, 100, 1.0);
2319 blit_pixmap(&src, &mut dst, -100, -100, 1.0);
2320 assert!(dst.data().iter().all(|&b| b == 0));
2321 }
2322
2323 #[test]
2324 fn blit_pixmap_into_smaller_dst_clips_instead_of_panicking() {
2325 let src = filled(8, 8, [1, 2, 3, 255]);
2326 let mut dst = filled(2, 2, CLEAR);
2327 blit_pixmap(&src, &mut dst, 0, 0, 1.0);
2328 assert_eq!(get(&dst, 1, 1), [1, 2, 3, 255]);
2329 }
2330
2331 #[test]
2332 fn blit_pixmap_extreme_positions_do_not_panic() {
2333 let src = filled(2, 2, [1, 2, 3, 255]);
2336 let mut dst = filled(4, 4, CLEAR);
2337 blit_pixmap(&src, &mut dst, i32::MAX, 0, 1.0);
2338 blit_pixmap(&src, &mut dst, 0, i32::MAX, 1.0);
2339 blit_pixmap(&src, &mut dst, i32::MIN, i32::MIN, 1.0);
2340 assert!(dst.data().iter().all(|&b| b == 0));
2341 }
2342
2343 #[test]
2344 fn blit_pixmap_zero_sized_src_is_a_noop() {
2345 let src = zero_sized();
2346 let mut dst = filled(2, 2, WHITE);
2347 blit_pixmap(&src, &mut dst, 0, 0, 1.0);
2348 assert!(dst.data().iter().all(|&b| b == 255));
2349 }
2350
2351 #[test]
2356 fn shift_pixbuf_zero_delta_is_a_noop() {
2357 let mut p = marked(3, 3);
2358 let before = p.data().to_vec();
2359 shift_pixbuf(&mut p, 0, 0);
2360 assert_eq!(p.data(), &before[..]);
2361 }
2362
2363 #[test]
2364 fn shift_pixbuf_right_moves_columns_and_clears_the_left() {
2365 let mut p = marked(3, 3);
2366 shift_pixbuf(&mut p, 1, 0);
2367 assert_eq!(get(&p, 0, 0), CLEAR, "the exposed column must be cleared");
2368 assert_eq!(get(&p, 1, 0), [0, 0, 0, 255]);
2369 assert_eq!(get(&p, 2, 0), [1, 0, 0, 255]);
2370 assert_eq!(get(&p, 1, 2), [6, 0, 0, 255]);
2371 }
2372
2373 #[test]
2374 fn shift_pixbuf_left_moves_columns_and_clears_the_right() {
2375 let mut p = marked(3, 3);
2376 shift_pixbuf(&mut p, -1, 0);
2377 assert_eq!(get(&p, 0, 0), [1, 0, 0, 255]);
2378 assert_eq!(get(&p, 1, 0), [2, 0, 0, 255]);
2379 assert_eq!(get(&p, 2, 0), CLEAR);
2380 assert_eq!(get(&p, 0, 2), [7, 0, 0, 255]);
2381 }
2382
2383 #[test]
2384 fn shift_pixbuf_down_moves_rows_and_clears_the_top() {
2385 let mut p = marked(3, 3);
2386 shift_pixbuf(&mut p, 0, 1);
2387 assert_eq!(get(&p, 0, 0), CLEAR);
2388 assert_eq!(get(&p, 0, 1), [0, 0, 0, 255]);
2389 assert_eq!(get(&p, 1, 1), [1, 0, 0, 255]);
2390 assert_eq!(get(&p, 0, 2), [3, 0, 0, 255]);
2391 }
2392
2393 #[test]
2394 fn shift_pixbuf_up_moves_rows_and_clears_the_bottom() {
2395 let mut p = marked(3, 3);
2396 shift_pixbuf(&mut p, 0, -1);
2397 assert_eq!(get(&p, 0, 0), [3, 0, 0, 255]);
2398 assert_eq!(get(&p, 0, 1), [6, 0, 0, 255]);
2399 assert_eq!(get(&p, 0, 2), CLEAR);
2400 assert_eq!(get(&p, 2, 2), CLEAR);
2401 }
2402
2403 #[test]
2404 fn shift_pixbuf_diagonal_composes_both_axes() {
2405 let mut p = marked(3, 3);
2406 shift_pixbuf(&mut p, 1, 1);
2407 assert_eq!(get(&p, 1, 1), [0, 0, 0, 255]);
2408 assert_eq!(get(&p, 2, 2), [4, 0, 0, 255]);
2409 assert_eq!(get(&p, 0, 0), CLEAR);
2410 assert_eq!(get(&p, 2, 0), CLEAR);
2411 assert_eq!(get(&p, 0, 2), CLEAR);
2412 }
2413
2414 #[test]
2415 fn shift_pixbuf_by_exactly_the_size_clears_everything() {
2416 let mut p = marked(3, 3);
2417 shift_pixbuf(&mut p, 3, 0);
2418 assert!(p.data().iter().all(|&b| b == 0));
2419
2420 let mut p = marked(3, 3);
2421 shift_pixbuf(&mut p, 0, -3);
2422 assert!(p.data().iter().all(|&b| b == 0));
2423 }
2424
2425 #[test]
2426 fn shift_pixbuf_beyond_the_size_clears_everything() {
2427 let mut p = marked(3, 3);
2428 shift_pixbuf(&mut p, 100, 100);
2429 assert!(p.data().iter().all(|&b| b == 0));
2430 }
2431
2432 #[test]
2433 fn shift_pixbuf_i32_max_clears_everything() {
2434 let mut p = marked(3, 3);
2435 shift_pixbuf(&mut p, i32::MAX, i32::MAX);
2436 assert!(p.data().iter().all(|&b| b == 0));
2437 }
2438
2439 #[test]
2440 fn shift_pixbuf_i32_min_does_not_panic() {
2441 let mut p = marked(3, 3);
2444 shift_pixbuf(&mut p, i32::MIN, 0);
2445 assert!(p.data().iter().all(|&b| b == 0));
2446 }
2447
2448 #[test]
2449 fn shift_pixbuf_i32_min_dy_does_not_panic() {
2450 let mut p = marked(3, 3);
2451 shift_pixbuf(&mut p, 0, i32::MIN);
2452 assert!(p.data().iter().all(|&b| b == 0));
2453 }
2454
2455 #[test]
2456 fn shift_pixbuf_on_zero_sized_pixmap_does_not_panic() {
2457 let mut p = zero_sized();
2458 shift_pixbuf(&mut p, 1, 1);
2459 assert!(p.data().is_empty());
2460 }
2461
2462 #[test]
2463 fn shift_pixbuf_preserves_the_buffer_length() {
2464 let mut p = marked(4, 4);
2465 for (dx, dy) in [(1, 0), (-1, 0), (0, 1), (0, -1), (3, 3), (-3, -3)] {
2466 shift_pixbuf(&mut p, dx, dy);
2467 assert_eq!(p.data().len(), 4 * 4 * 4);
2468 }
2469 }
2470
2471 #[test]
2476 fn blit_buffer_opaque_src_overwrites_dst() {
2477 let src = vec![10u8, 20, 30, 255, 40, 50, 60, 255];
2478 let mut dst = filled(4, 1, CLEAR);
2479 blit_buffer(&mut dst, &src, 2, 1, 1, 0);
2480 assert_eq!(get(&dst, 0, 0), CLEAR);
2481 assert_eq!(get(&dst, 1, 0), [10, 20, 30, 255]);
2482 assert_eq!(get(&dst, 2, 0), [40, 50, 60, 255]);
2483 assert_eq!(get(&dst, 3, 0), CLEAR);
2484 }
2485
2486 #[test]
2487 fn blit_buffer_transparent_src_is_a_noop() {
2488 let src = vec![10u8, 20, 30, 0];
2489 let mut dst = filled(1, 1, WHITE);
2490 blit_buffer(&mut dst, &src, 1, 1, 0, 0);
2491 assert_eq!(get(&dst, 0, 0), WHITE);
2492 }
2493
2494 #[test]
2495 fn blit_buffer_premultiplied_half_alpha_blend_is_exact() {
2496 let src = vec![100u8, 50, 25, 128];
2498 let mut dst = filled(1, 1, CLEAR);
2499 blit_buffer(&mut dst, &src, 1, 1, 0, 0);
2500 assert_eq!(get(&dst, 0, 0), [100, 50, 25, 128]);
2501 }
2502
2503 #[test]
2504 fn blit_buffer_premultiplied_blend_saturates_instead_of_wrapping() {
2505 let src = vec![255u8, 255, 255, 128];
2508 let mut dst = filled(1, 1, WHITE);
2509 blit_buffer(&mut dst, &src, 1, 1, 0, 0);
2510 assert_eq!(get(&dst, 0, 0), WHITE);
2511 }
2512
2513 #[test]
2514 fn blit_buffer_negative_offset_clips() {
2515 let src = vec![1u8, 1, 1, 255, 2, 2, 2, 255, 3, 3, 3, 255, 4, 4, 4, 255];
2516 let mut dst = filled(2, 2, CLEAR);
2517 blit_buffer(&mut dst, &src, 2, 2, -1, -1);
2518 assert_eq!(get(&dst, 0, 0), [4, 4, 4, 255]);
2520 assert_eq!(get(&dst, 1, 1), CLEAR);
2521 }
2522
2523 #[test]
2524 fn blit_buffer_fully_offscreen_is_a_noop() {
2525 let src = vec![9u8, 9, 9, 255];
2526 let mut dst = filled(2, 2, CLEAR);
2527 blit_buffer(&mut dst, &src, 1, 1, 50, 50);
2528 blit_buffer(&mut dst, &src, 1, 1, -50, -50);
2529 assert!(dst.data().iter().all(|&b| b == 0));
2530 }
2531
2532 #[test]
2533 fn blit_buffer_src_shorter_than_its_declared_size_is_skipped_not_panic() {
2534 let src = vec![7u8, 7, 7, 255, 8, 8, 8, 255];
2536 let mut dst = filled(4, 4, CLEAR);
2537 blit_buffer(&mut dst, &src, 4, 4, 0, 0);
2538 assert_eq!(get(&dst, 0, 0), [7, 7, 7, 255]);
2539 assert_eq!(get(&dst, 1, 0), [8, 8, 8, 255]);
2540 assert_eq!(get(&dst, 2, 0), CLEAR);
2541 assert_eq!(get(&dst, 3, 3), CLEAR);
2542 }
2543
2544 #[test]
2545 fn blit_buffer_empty_src_is_a_noop() {
2546 let mut dst = filled(2, 2, WHITE);
2547 blit_buffer(&mut dst, &[], 2, 2, 0, 0);
2548 assert!(dst.data().iter().all(|&b| b == 255));
2549 }
2550
2551 #[test]
2552 fn blit_buffer_zero_src_dimensions_are_a_noop() {
2553 let src = vec![9u8, 9, 9, 255];
2554 let mut dst = filled(2, 2, WHITE);
2555 blit_buffer(&mut dst, &src, 0, 0, 0, 0);
2556 blit_buffer(&mut dst, &src, 0, 1, 0, 0);
2557 blit_buffer(&mut dst, &src, 1, 0, 0, 0);
2558 assert!(dst.data().iter().all(|&b| b == 255));
2559 }
2560
2561 #[test]
2562 fn blit_buffer_u32_max_src_dimensions_are_a_noop() {
2563 let src = vec![9u8, 9, 9, 255];
2565 let mut dst = filled(2, 2, WHITE);
2566 blit_buffer(&mut dst, &src, u32::MAX, u32::MAX, 0, 0);
2567 assert!(dst.data().iter().all(|&b| b == 255));
2568 }
2569
2570 #[test]
2571 fn blit_buffer_extreme_offsets_do_not_panic() {
2572 let src = vec![9u8; 2 * 2 * 4];
2573 let mut dst = filled(4, 4, CLEAR);
2574 blit_buffer(&mut dst, &src, 2, 2, i32::MAX, 0);
2575 blit_buffer(&mut dst, &src, 2, 2, 0, i32::MAX);
2576 blit_buffer(&mut dst, &src, 2, 2, i32::MIN, i32::MIN);
2577 assert!(dst.data().iter().all(|&b| b == 0));
2578 }
2579
2580 #[test]
2585 fn snapshot_region_copies_the_requested_box() {
2586 let p = marked(4, 4);
2587 let snap = snapshot_region(&p, 1, 1, 2, 2);
2588 assert_eq!(snap.len(), 2 * 2 * 4);
2589 assert_eq!(&snap[0..4], &[5, 0, 0, 255]); assert_eq!(&snap[4..8], &[6, 0, 0, 255]); assert_eq!(&snap[8..12], &[9, 0, 0, 255]); assert_eq!(&snap[12..16], &[10, 0, 0, 255]); }
2594
2595 #[test]
2596 fn snapshot_region_zero_size_is_an_empty_vec() {
2597 let p = marked(4, 4);
2598 assert!(snapshot_region(&p, 0, 0, 0, 0).is_empty());
2599 assert!(snapshot_region(&p, 0, 0, 4, 0).is_empty());
2600 assert!(snapshot_region(&p, 0, 0, 0, 4).is_empty());
2601 }
2602
2603 #[test]
2604 fn snapshot_region_out_of_bounds_pixels_are_zero_filled() {
2605 let p = filled(2, 2, WHITE);
2606 let snap = snapshot_region(&p, 1, 1, 2, 2);
2607 assert_eq!(snap.len(), 16);
2608 assert_eq!(&snap[0..4], &WHITE, "only (1,1) is inside the pixmap");
2609 assert_eq!(&snap[4..8], &CLEAR);
2610 assert_eq!(&snap[8..12], &CLEAR);
2611 assert_eq!(&snap[12..16], &CLEAR);
2612 }
2613
2614 #[test]
2615 fn snapshot_region_negative_origin_is_partially_zero_filled() {
2616 let p = marked(2, 2);
2617 let snap = snapshot_region(&p, -1, -1, 2, 2);
2618 assert_eq!(&snap[0..4], &CLEAR);
2619 assert_eq!(&snap[4..8], &CLEAR);
2620 assert_eq!(&snap[8..12], &CLEAR);
2621 assert_eq!(&snap[12..16], &[0, 0, 0, 255], "pixel (0,0) of the source");
2622 }
2623
2624 #[test]
2625 fn snapshot_region_fully_offscreen_is_all_zeroes() {
2626 let p = filled(4, 4, WHITE);
2627 let snap = snapshot_region(&p, 100, 100, 2, 2);
2628 assert!(snap.iter().all(|&b| b == 0));
2629 }
2630
2631 #[test]
2632 fn snapshot_region_extreme_origin_does_not_panic() {
2633 let p = filled(4, 4, WHITE);
2636 let snap = snapshot_region(&p, i32::MAX, 0, 2, 2);
2637 assert!(snap.iter().all(|&b| b == 0));
2638 let snap = snapshot_region(&p, 0, i32::MAX, 2, 2);
2639 assert!(snap.iter().all(|&b| b == 0));
2640 let snap = snapshot_region(&p, i32::MIN, i32::MIN, 2, 2);
2641 assert!(snap.iter().all(|&b| b == 0));
2642 }
2643
2644 #[test]
2645 fn write_region_overwrites_without_blending() {
2646 let src = vec![0u8; 4];
2648 let mut dst = filled(2, 2, WHITE);
2649 write_region(&mut dst, &src, 1, 1, 0, 0);
2650 assert_eq!(get(&dst, 0, 0), CLEAR, "write_region is a direct copy");
2651 assert_eq!(get(&dst, 1, 1), WHITE);
2652 }
2653
2654 #[test]
2655 fn write_region_places_pixels_at_the_offset() {
2656 let src = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
2657 let mut dst = filled(4, 1, CLEAR);
2658 write_region(&mut dst, &src, 2, 1, 2, 0);
2659 assert_eq!(get(&dst, 2, 0), [1, 2, 3, 4]);
2660 assert_eq!(get(&dst, 3, 0), [5, 6, 7, 8]);
2661 assert_eq!(get(&dst, 0, 0), CLEAR);
2662 }
2663
2664 #[test]
2665 fn write_region_out_of_bounds_pixels_are_skipped() {
2666 let src = vec![9u8; 2 * 2 * 4];
2667 let mut dst = filled(2, 2, CLEAR);
2668 write_region(&mut dst, &src, 2, 2, 1, 1);
2669 assert_eq!(get(&dst, 1, 1), [9, 9, 9, 9]);
2670 assert_eq!(get(&dst, 0, 0), CLEAR);
2671 }
2672
2673 #[test]
2674 fn write_region_negative_coords_clip() {
2675 let src = vec![1u8, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4];
2676 let mut dst = filled(2, 2, CLEAR);
2677 write_region(&mut dst, &src, 2, 2, -1, -1);
2678 assert_eq!(get(&dst, 0, 0), [4, 4, 4, 4]);
2679 assert_eq!(get(&dst, 1, 1), CLEAR);
2680 }
2681
2682 #[test]
2683 fn write_region_short_src_is_skipped_not_panic() {
2684 let src = vec![7u8, 7, 7, 7];
2685 let mut dst = filled(4, 4, CLEAR);
2686 write_region(&mut dst, &src, 4, 4, 0, 0);
2687 assert_eq!(get(&dst, 0, 0), [7, 7, 7, 7]);
2688 assert_eq!(get(&dst, 1, 0), CLEAR);
2689 }
2690
2691 #[test]
2692 fn write_region_zero_dimensions_are_a_noop() {
2693 let src = vec![9u8; 16];
2694 let mut dst = filled(2, 2, WHITE);
2695 write_region(&mut dst, &src, 0, 0, 0, 0);
2696 assert!(dst.data().iter().all(|&b| b == 255));
2697 }
2698
2699 #[test]
2700 fn write_region_u32_max_dimensions_are_a_noop() {
2701 let src = vec![9u8; 16];
2702 let mut dst = filled(2, 2, WHITE);
2703 write_region(&mut dst, &src, u32::MAX, u32::MAX, 0, 0);
2704 assert!(dst.data().iter().all(|&b| b == 255));
2705 }
2706
2707 #[test]
2708 fn write_region_extreme_coords_do_not_panic() {
2709 let src = vec![9u8; 2 * 2 * 4];
2710 let mut dst = filled(4, 4, CLEAR);
2711 write_region(&mut dst, &src, 2, 2, i32::MAX, 0);
2712 write_region(&mut dst, &src, 2, 2, 0, i32::MAX);
2713 write_region(&mut dst, &src, 2, 2, i32::MIN, i32::MIN);
2714 assert!(dst.data().iter().all(|&b| b == 0));
2715 }
2716
2717 #[test]
2718 fn snapshot_region_then_write_region_round_trips() {
2719 let mut p = marked(6, 6);
2720 let snap = snapshot_region(&p, 1, 1, 3, 3);
2721 let expected = p.data().to_vec();
2722 p.fill_rect(1, 1, 3, 3, 0, 0, 0, 0); assert_eq!(get(&p, 2, 2), CLEAR);
2724 write_region(&mut p, &snap, 3, 3, 1, 1); assert_eq!(p.data(), &expected[..], "write_region must invert snapshot_region");
2726 }
2727
2728 #[test]
2733 fn agg_fill_path_paints_the_path_interior_only() {
2734 let mut p = filled(10, 10, WHITE);
2735 let mut path = rect_path(2.0, 2.0, 6.0, 6.0);
2736 agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
2737 assert_eq!(get(&p, 5, 5), [255, 0, 0, 255], "interior is fully covered");
2738 assert_eq!(get(&p, 0, 0), WHITE, "outside the path is untouched");
2739 assert_eq!(get(&p, 9, 9), WHITE);
2740 }
2741
2742 #[test]
2743 fn agg_fill_path_empty_path_paints_nothing() {
2744 let mut p = filled(8, 8, WHITE);
2745 let mut path = PathStorage::new();
2746 agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
2747 assert!(p.data().iter().all(|&b| b == 255));
2748 }
2749
2750 #[test]
2751 fn agg_fill_path_on_a_1x1_pixmap_does_not_panic() {
2752 let mut p = filled(1, 1, WHITE);
2753 let mut path = rect_path(0.0, 0.0, 1.0, 1.0);
2754 agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
2755 assert_eq!(get(&p, 0, 0), [255, 0, 0, 255]);
2756 }
2757
2758 #[test]
2759 fn agg_fill_path_offscreen_path_paints_nothing() {
2760 let mut p = filled(8, 8, WHITE);
2761 let mut path = rect_path(100.0, 100.0, 10.0, 10.0);
2762 agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
2763 assert!(p.data().iter().all(|&b| b == 255));
2764 }
2765
2766 #[test]
2767 fn agg_fill_path_nan_coordinates_do_not_panic() {
2768 let mut p = filled(8, 8, WHITE);
2769 let mut path = PathStorage::new();
2770 path.move_to(f64::NAN, f64::NAN);
2771 path.line_to(4.0, f64::NAN);
2772 path.line_to(f64::NAN, 4.0);
2773 path.close_polygon(0);
2774 agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
2775 assert_eq!(p.data().len(), 8 * 8 * 4, "the buffer must stay intact");
2776 }
2777
2778 #[test]
2779 fn agg_fill_path_huge_coordinates_do_not_panic() {
2780 let mut p = filled(8, 8, WHITE);
2781 let mut path = rect_path(-1.0e30, -1.0e30, 2.0e30, 2.0e30);
2782 agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
2783 assert_eq!(p.data().len(), 8 * 8 * 4);
2784 }
2785
2786 #[test]
2787 fn agg_fill_path_infinite_coordinates_do_not_panic() {
2788 let mut p = filled(8, 8, WHITE);
2789 let mut path = PathStorage::new();
2790 path.move_to(f64::NEG_INFINITY, f64::NEG_INFINITY);
2791 path.line_to(f64::INFINITY, f64::NEG_INFINITY);
2792 path.line_to(f64::INFINITY, f64::INFINITY);
2793 path.close_polygon(0);
2794 agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
2795 assert_eq!(p.data().len(), 8 * 8 * 4);
2796 }
2797
2798 #[test]
2799 fn agg_fill_path_clipped_none_clip_equals_unclipped() {
2800 let mut a = filled(10, 10, WHITE);
2801 let mut b = filled(10, 10, WHITE);
2802 let mut pa = rect_path(1.0, 1.0, 5.0, 5.0);
2803 let mut pb = rect_path(1.0, 1.0, 5.0, 5.0);
2804 agg_fill_path(&mut a, &mut pa, &red(), FillingRule::NonZero);
2805 agg_fill_path_clipped(&mut b, &mut pb, &red(), FillingRule::NonZero, None);
2806 assert_eq!(a.data(), b.data());
2807 }
2808
2809 #[test]
2810 fn agg_fill_path_clipped_restricts_output_to_the_clip_box() {
2811 let mut p = filled(10, 10, WHITE);
2812 let mut path = rect_path(0.0, 0.0, 10.0, 10.0); let clip = AzRect::from_xywh(2.0, 2.0, 3.0, 3.0).expect("valid");
2814 agg_fill_path_clipped(&mut p, &mut path, &red(), FillingRule::NonZero, Some(clip));
2815 assert_eq!(get(&p, 3, 3), [255, 0, 0, 255], "inside the clip");
2816 assert_eq!(get(&p, 6, 6), WHITE, "outside the clip");
2817 assert_eq!(get(&p, 0, 0), WHITE);
2818 }
2819
2820 #[test]
2821 fn agg_fill_path_clipped_offscreen_clip_paints_nothing() {
2822 let mut p = filled(10, 10, WHITE);
2823 let mut path = rect_path(0.0, 0.0, 10.0, 10.0);
2824 let clip = AzRect::from_xywh(100.0, 100.0, 5.0, 5.0).expect("valid");
2825 agg_fill_path_clipped(&mut p, &mut path, &red(), FillingRule::NonZero, Some(clip));
2826 assert!(
2827 p.data().iter().all(|&b| b == 255),
2828 "a clip box outside the buffer must reject everything"
2829 );
2830 }
2831
2832 #[test]
2833 fn agg_fill_path_clipped_empty_clip_paints_nothing() {
2834 let outer = AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).expect("valid");
2837 let inner = AzRect::from_xywh(10.0, 10.0, 4.0, 4.0).expect("valid");
2838 let empty = intersect_clips(Some(outer), Some(inner)).expect("stays Some");
2839 assert!(approx(empty.width, 0.0) && approx(empty.height, 0.0));
2840
2841 let mut p = filled(20, 20, WHITE);
2842 let mut path = rect_path(0.0, 0.0, 20.0, 20.0);
2843 agg_fill_path_clipped(&mut p, &mut path, &red(), FillingRule::NonZero, Some(empty));
2844 let painted = painted_count(&p);
2845 assert_eq!(painted, 0, "an empty clip leaked {painted} painted pixel(s)");
2846 }
2847
2848 #[test]
2849 fn agg_fill_path_evenodd_leaves_the_hole_of_a_donut_unpainted() {
2850 let mut p = filled(12, 12, WHITE);
2851 let mut path = PathStorage::new();
2852 path.move_to(1.0, 1.0);
2854 path.line_to(11.0, 1.0);
2855 path.line_to(11.0, 11.0);
2856 path.line_to(1.0, 11.0);
2857 path.close_polygon(0);
2858 path.move_to(4.0, 4.0);
2860 path.line_to(8.0, 4.0);
2861 path.line_to(8.0, 8.0);
2862 path.line_to(4.0, 8.0);
2863 path.close_polygon(0);
2864 agg_fill_path(&mut p, &mut path, &red(), FillingRule::EvenOdd);
2865 assert_eq!(get(&p, 2, 2), [255, 0, 0, 255], "the ring is painted");
2866 assert_eq!(get(&p, 6, 6), WHITE, "the hole is not");
2867 }
2868
2869 #[test]
2874 fn agg_fill_transformed_path_identity_matches_the_untransformed_fill() {
2875 let mut a = filled(10, 10, WHITE);
2876 let mut b = filled(10, 10, WHITE);
2877 let mut pa = rect_path(2.0, 2.0, 4.0, 4.0);
2878 let mut pb = rect_path(2.0, 2.0, 4.0, 4.0);
2879 agg_fill_path(&mut a, &mut pa, &red(), FillingRule::NonZero);
2880 agg_fill_transformed_path(&mut b, &mut pb, &red(), FillingRule::NonZero, &TransAffine::new());
2881 assert_eq!(a.data(), b.data(), "an identity transform must be a no-op");
2882 }
2883
2884 #[test]
2885 fn agg_fill_transformed_path_translation_moves_the_output() {
2886 let mut p = filled(12, 12, WHITE);
2887 let mut path = rect_path(0.0, 0.0, 3.0, 3.0);
2888 let t = TransAffine::new_translation(6.0, 0.0);
2889 agg_fill_transformed_path(&mut p, &mut path, &red(), FillingRule::NonZero, &t);
2890 assert_eq!(get(&p, 7, 1), [255, 0, 0, 255], "moved right by 6");
2891 assert_eq!(get(&p, 1, 1), WHITE, "the original position is empty");
2892 }
2893
2894 #[test]
2895 fn agg_fill_transformed_path_zero_scale_does_not_panic() {
2896 let mut p = filled(8, 8, WHITE);
2897 let mut path = rect_path(1.0, 1.0, 4.0, 4.0);
2898 let t = TransAffine::new_scaling(0.0, 0.0);
2899 agg_fill_transformed_path(&mut p, &mut path, &red(), FillingRule::NonZero, &t);
2900 assert!(
2901 p.data().iter().all(|&b| b == 255),
2902 "a degenerate transform collapses the path to a point"
2903 );
2904 }
2905
2906 #[test]
2907 fn agg_fill_transformed_path_nan_transform_does_not_panic() {
2908 let mut p = filled(8, 8, WHITE);
2909 let mut path = rect_path(1.0, 1.0, 4.0, 4.0);
2910 let t = TransAffine::new_scaling(f64::NAN, 1.0);
2911 agg_fill_transformed_path(&mut p, &mut path, &red(), FillingRule::NonZero, &t);
2912 assert_eq!(p.data().len(), 8 * 8 * 4);
2913 }
2914
2915 #[test]
2916 fn agg_fill_transformed_path_clipped_applies_the_clip_after_the_transform() {
2917 let mut p = filled(12, 12, WHITE);
2918 let mut path = rect_path(0.0, 0.0, 3.0, 3.0);
2919 let t = TransAffine::new_translation(6.0, 0.0);
2920 let clip = AzRect::from_xywh(0.0, 0.0, 3.0, 3.0).expect("valid");
2922 agg_fill_transformed_path_clipped(
2923 &mut p,
2924 &mut path,
2925 &red(),
2926 FillingRule::NonZero,
2927 &t,
2928 Some(clip),
2929 );
2930 assert!(
2931 p.data().iter().all(|&b| b == 255),
2932 "the translated path lies outside the clip box"
2933 );
2934 }
2935
2936 #[test]
2937 fn agg_fill_transformed_path_clipped_empty_clip_paints_nothing() {
2938 let outer = AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).expect("valid");
2939 let inner = AzRect::from_xywh(10.0, 10.0, 4.0, 4.0).expect("valid");
2940 let empty = intersect_clips(Some(outer), Some(inner)).expect("stays Some");
2941
2942 let mut p = filled(20, 20, WHITE);
2943 let mut path = rect_path(0.0, 0.0, 20.0, 20.0);
2944 agg_fill_transformed_path_clipped(
2945 &mut p,
2946 &mut path,
2947 &red(),
2948 FillingRule::NonZero,
2949 &TransAffine::new(),
2950 Some(empty),
2951 );
2952 assert!(
2953 p.data().iter().all(|&b| b == 255),
2954 "an empty clip must clip everything, even on the identity fast path"
2955 );
2956 }
2957
2958 #[test]
2963 fn agg_fill_gradient_paints_the_path() {
2964 let mut p = filled(10, 10, WHITE);
2965 let mut path = rect_path(0.0, 0.0, 10.0, 10.0);
2966 let lut = two_stop_lut();
2967 agg_fill_gradient(
2968 &mut p,
2969 &mut path,
2970 &lut,
2971 GradientX,
2972 TransAffine::new(),
2973 0.0,
2974 10.0,
2975 );
2976 assert_ne!(get(&p, 5, 5), WHITE, "the gradient must paint something");
2977 assert_eq!(get(&p, 5, 5)[3], 255, "opaque stops produce opaque pixels");
2978 }
2979
2980 #[test]
2981 fn agg_fill_gradient_empty_path_paints_nothing() {
2982 let mut p = filled(8, 8, WHITE);
2983 let mut path = PathStorage::new();
2984 let lut = two_stop_lut();
2985 agg_fill_gradient(
2986 &mut p,
2987 &mut path,
2988 &lut,
2989 GradientX,
2990 TransAffine::new(),
2991 0.0,
2992 8.0,
2993 );
2994 assert!(p.data().iter().all(|&b| b == 255));
2995 }
2996
2997 #[test]
2998 fn agg_fill_gradient_zero_length_d1_eq_d2_does_not_panic() {
2999 let mut p = filled(8, 8, WHITE);
3001 let mut path = rect_path(0.0, 0.0, 8.0, 8.0);
3002 let lut = two_stop_lut();
3003 agg_fill_gradient_clipped(
3004 &mut p,
3005 &mut path,
3006 &lut,
3007 GradientX,
3008 TransAffine::new(),
3009 5.0,
3010 5.0,
3011 None,
3012 );
3013 assert_eq!(p.data().len(), 8 * 8 * 4);
3014 }
3015
3016 #[test]
3017 fn agg_fill_gradient_reversed_d2_lt_d1_does_not_panic() {
3018 let mut p = filled(8, 8, WHITE);
3019 let mut path = rect_path(0.0, 0.0, 8.0, 8.0);
3020 let lut = two_stop_lut();
3021 agg_fill_gradient_clipped(
3022 &mut p,
3023 &mut path,
3024 &lut,
3025 GradientX,
3026 TransAffine::new(),
3027 8.0,
3028 0.0,
3029 None,
3030 );
3031 assert_eq!(p.data().len(), 8 * 8 * 4);
3032 }
3033
3034 #[test]
3035 fn agg_fill_gradient_nan_and_inf_distances_do_not_panic() {
3036 let lut = two_stop_lut();
3037 for (d1, d2) in [
3038 (f64::NAN, f64::NAN),
3039 (0.0, f64::NAN),
3040 (f64::NEG_INFINITY, f64::INFINITY),
3041 (0.0, f64::MAX),
3042 (f64::MIN, f64::MAX),
3043 ] {
3044 let mut p = filled(8, 8, WHITE);
3045 let mut path = rect_path(0.0, 0.0, 8.0, 8.0);
3046 agg_fill_gradient_clipped(
3047 &mut p,
3048 &mut path,
3049 &lut,
3050 GradientX,
3051 TransAffine::new(),
3052 d1,
3053 d2,
3054 None,
3055 );
3056 assert_eq!(p.data().len(), 8 * 8 * 4, "d1={d1}, d2={d2}");
3057 }
3058 }
3059
3060 #[test]
3061 fn agg_fill_gradient_nan_transform_does_not_panic() {
3062 let mut p = filled(8, 8, WHITE);
3063 let mut path = rect_path(0.0, 0.0, 8.0, 8.0);
3064 let lut = two_stop_lut();
3065 agg_fill_gradient_clipped(
3066 &mut p,
3067 &mut path,
3068 &lut,
3069 GradientX,
3070 TransAffine::new_scaling(f64::NAN, f64::NAN),
3071 0.0,
3072 8.0,
3073 None,
3074 );
3075 assert_eq!(p.data().len(), 8 * 8 * 4);
3076 }
3077
3078 #[test]
3079 fn agg_fill_gradient_clipped_restricts_output_to_the_clip_box() {
3080 let mut p = filled(10, 10, WHITE);
3081 let mut path = rect_path(0.0, 0.0, 10.0, 10.0);
3082 let lut = two_stop_lut();
3083 let clip = AzRect::from_xywh(0.0, 0.0, 3.0, 3.0).expect("valid");
3084 agg_fill_gradient_clipped(
3085 &mut p,
3086 &mut path,
3087 &lut,
3088 GradientX,
3089 TransAffine::new(),
3090 0.0,
3091 10.0,
3092 Some(clip),
3093 );
3094 assert_ne!(get(&p, 1, 1), WHITE, "inside the clip");
3095 assert_eq!(get(&p, 8, 8), WHITE, "outside the clip");
3096 }
3097
3098 #[test]
3099 fn agg_fill_gradient_clipped_empty_clip_paints_nothing() {
3100 let outer = AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).expect("valid");
3101 let inner = AzRect::from_xywh(10.0, 10.0, 4.0, 4.0).expect("valid");
3102 let empty = intersect_clips(Some(outer), Some(inner)).expect("stays Some");
3103
3104 let mut p = filled(20, 20, WHITE);
3105 let mut path = rect_path(0.0, 0.0, 20.0, 20.0);
3106 let lut = two_stop_lut();
3107 agg_fill_gradient_clipped(
3108 &mut p,
3109 &mut path,
3110 &lut,
3111 GradientX,
3112 TransAffine::new(),
3113 0.0,
3114 20.0,
3115 Some(empty),
3116 );
3117 let painted = painted_count(&p);
3118 assert_eq!(painted, 0, "an empty clip leaked {painted} painted pixel(s)");
3119 }
3120
3121 #[test]
3122 fn agg_fill_gradient_on_a_1x1_pixmap_does_not_panic() {
3123 let mut p = filled(1, 1, WHITE);
3124 let mut path = rect_path(0.0, 0.0, 1.0, 1.0);
3125 let lut = two_stop_lut();
3126 agg_fill_gradient(&mut p, &mut path, &lut, GradientX, TransAffine::new(), 0.0, 1.0);
3127 assert_eq!(p.data().len(), 4);
3128 }
3129}
3130