1#[cfg(feature = "rayon")]
29mod parallel;
30#[cfg(feature = "rayon")]
31pub use parallel::{PARALLEL_FILL_MIN_HEIGHT, eo_fill_parallel, fill_parallel};
32
33use crate::bitmap::{AaBuf, Bitmap, BitmapBand};
34use crate::clip::{Clip, ClipResult};
35use crate::path::Path;
36use crate::pipe::{self, PipeSrc, PipeState};
37use crate::scanner::XPathScanner;
38use crate::scanner::iter::ScanIterator;
39use crate::simd;
40use crate::types::AA_SIZE;
41use crate::xpath::XPath;
42use color::Pixel;
43
44pub(super) const AA_GAMMA: [u8; (AA_SIZE * AA_SIZE + 1) as usize] = [
47 0, 4, 11, 21, 32, 45, 59, 74, 90, 108, 126, 145, 166, 187, 209, 231, 255,
48];
49
50#[expect(
52 clippy::too_many_arguments,
53 reason = "mirrors SplashFillWithPattern API; all params necessary"
54)]
55pub fn fill<P: Pixel>(
56 bitmap: &mut Bitmap<P>,
57 clip: &Clip,
58 path: &Path,
59 pipe: &PipeState<'_>,
60 src: &PipeSrc<'_>,
61 matrix: &[f64; 6],
62 flatness: f64,
63 vector_antialias: bool,
64) {
65 fill_impl::<P>(
66 bitmap,
67 clip,
68 path,
69 pipe,
70 src,
71 matrix,
72 flatness,
73 vector_antialias,
74 false,
75 );
76}
77
78#[expect(
80 clippy::too_many_arguments,
81 reason = "mirrors SplashFillWithPattern API; all params necessary"
82)]
83pub fn eo_fill<P: Pixel>(
84 bitmap: &mut Bitmap<P>,
85 clip: &Clip,
86 path: &Path,
87 pipe: &PipeState<'_>,
88 src: &PipeSrc<'_>,
89 matrix: &[f64; 6],
90 flatness: f64,
91 vector_antialias: bool,
92) {
93 fill_impl::<P>(
94 bitmap,
95 clip,
96 path,
97 pipe,
98 src,
99 matrix,
100 flatness,
101 vector_antialias,
102 true,
103 );
104}
105
106#[expect(
107 clippy::too_many_arguments,
108 reason = "mirrors SplashFillWithPattern API; all params necessary"
109)]
110pub(super) fn fill_impl<P: Pixel>(
111 bitmap: &mut Bitmap<P>,
112 clip: &Clip,
113 path: &Path,
114 pipe: &PipeState<'_>,
115 src: &PipeSrc<'_>,
116 matrix: &[f64; 6],
117 flatness: f64,
118 vector_antialias: bool,
119 eo: bool,
120) {
121 if path.pts.is_empty() {
122 return;
123 }
124
125 let mut xpath = XPath::new(path, matrix, flatness, true);
126
127 let (y_min_clip, y_max_clip) = if vector_antialias {
129 xpath.aa_scale();
130 let y_min = clip
131 .y_min_i
132 .checked_mul(AA_SIZE)
133 .expect("AA y_lo overflows i32: clip.y_min_i is unreasonably large");
134 let y_max = clip
135 .y_max_i
136 .checked_add(1)
137 .and_then(|v| v.checked_mul(AA_SIZE))
138 .map(|v| v - 1)
139 .expect("AA y_hi overflows i32: clip.y_max_i is unreasonably large");
140 (y_min, y_max)
141 } else {
142 (clip.y_min_i, clip.y_max_i)
143 };
144
145 let scanner = XPathScanner::new(&xpath, eo, y_min_clip, y_max_clip);
146
147 if scanner.is_empty() {
148 return;
149 }
150
151 if bitmap.width == 0 {
152 return;
153 }
154
155 let (x_min_i, y_min_i, x_max_i, y_max_i) = if vector_antialias {
157 (
158 scanner.x_min / AA_SIZE,
159 scanner.y_min / AA_SIZE,
160 scanner.x_max / AA_SIZE,
161 scanner.y_max / AA_SIZE,
162 )
163 } else {
164 (scanner.x_min, scanner.y_min, scanner.x_max, scanner.y_max)
165 };
166
167 let clip_res = clip.test_rect(x_min_i, y_min_i, x_max_i, y_max_i);
168 if clip_res == ClipResult::AllOutside {
169 return;
170 }
171
172 if vector_antialias {
173 let bitmap_width = bitmap.width as usize;
174 let mut aa_buf = AaBuf::new(bitmap_width);
175
176 for aa_y in scanner.y_min..=scanner.y_max {
177 let y = aa_y / AA_SIZE;
178 #[expect(clippy::cast_sign_loss, reason = "aa_y % AA_SIZE is in 0..AA_SIZE ≥ 0")]
181 let aa_row = (aa_y % AA_SIZE) as usize;
182
183 let mut x0 = scanner.x_min / AA_SIZE;
185 let mut x1 = scanner.x_max / AA_SIZE;
186
187 scanner.render_aa_line(&mut aa_buf, &mut x0, &mut x1, aa_y);
188
189 if clip_res != ClipResult::AllInside {
190 clip.clip_aa_line(&mut aa_buf, &mut x0, &mut x1, aa_y);
191 }
192
193 if aa_row == AA_SIZE as usize - 1 {
195 #[expect(
196 clippy::cast_sign_loss,
197 reason = "y = aa_y / AA_SIZE ≥ 0 since scanner.y_min ≥ 0"
198 )]
199 if x0 <= x1 && y >= 0 && (y as u32) < bitmap.height {
200 draw_aa_line::<P>(bitmap, pipe, src, &aa_buf, x0, x1, y);
201 }
202 aa_buf.clear();
203 }
204 }
205 } else {
206 #[expect(
207 clippy::cast_possible_wrap,
208 reason = "bitmap.width ≤ i32::MAX in practice; zero checked above scanner.is_empty()"
209 )]
210 let width_i = bitmap.width as i32;
211
212 for y in scanner.nonempty_rows() {
215 #[expect(clippy::cast_sign_loss, reason = "cast after y < 0 guard")]
216 if y < 0 || (y as u32) >= bitmap.height {
217 continue;
218 }
219 for (x0, x1) in ScanIterator::new(&scanner, y) {
220 let (mut sx0, mut sx1) = (x0, x1);
221 let inner_clip = if clip_res == ClipResult::AllInside {
222 sx0 = sx0.max(0);
224 sx1 = sx1.min(width_i - 1);
225 true
226 } else {
227 sx0 = sx0.max(clip.x_min_i);
228 sx1 = sx1.min(clip.x_max_i);
229 clip.test_span(sx0, sx1, y) == ClipResult::AllInside
230 };
231
232 if sx0 > sx1 {
233 continue;
234 }
235
236 if inner_clip {
237 draw_span::<P, _>(bitmap, pipe, src, sx0, sx1, y);
238 } else {
239 draw_span_clipped::<P, _>(bitmap, clip, pipe, src, sx0, sx1, y);
240 }
241 }
242 }
243 }
244}
245
246pub(super) trait RowSink<P: Pixel> {
254 fn row_and_alpha_mut(&mut self, y: u32) -> (&mut [u8], Option<&mut [u8]>);
262}
263
264impl<P: Pixel> RowSink<P> for Bitmap<P> {
265 #[inline]
266 fn row_and_alpha_mut(&mut self, y: u32) -> (&mut [u8], Option<&mut [u8]>) {
267 self.row_and_alpha_mut(y)
268 }
269}
270
271impl<P: Pixel> RowSink<P> for BitmapBand<'_, P> {
272 #[inline]
273 fn row_and_alpha_mut(&mut self, y: u32) -> (&mut [u8], Option<&mut [u8]>) {
274 self.row_and_alpha_mut(y)
275 }
276}
277
278pub(super) fn draw_span<P: Pixel, S: RowSink<P>>(
282 sink: &mut S,
283 pipe: &PipeState<'_>,
284 src: &PipeSrc<'_>,
285 x0: i32,
286 x1: i32,
287 y: i32,
288) {
289 debug_assert!(x0 <= x1, "draw_span: x0={x0} > x1={x1}");
290 debug_assert!(
291 x0 >= 0,
292 "draw_span: x0={x0} is negative (caller must clamp before calling)"
293 );
294 debug_assert!(y >= 0, "draw_span: y={y} is negative");
295 #[expect(clippy::cast_sign_loss, reason = "y >= 0 asserted above")]
296 let y_u = y as u32;
297 #[expect(clippy::cast_sign_loss, reason = "x0 >= 0 asserted above")]
298 let byte_off = x0 as usize * P::BYTES;
299 #[expect(clippy::cast_sign_loss, reason = "x1 >= x0 >= 0 asserted above")]
300 let byte_end = (x1 as usize + 1) * P::BYTES;
301 #[expect(clippy::cast_sign_loss, reason = "x0 >= 0, x1 >= x0 asserted above")]
302 let alpha_range = x0 as usize..=x1 as usize;
303
304 let (row, alpha) = sink.row_and_alpha_mut(y_u);
305 let dst_pixels = &mut row[byte_off..byte_end];
306 let dst_alpha = alpha.map(|a| &mut a[alpha_range]);
307
308 pipe::render_span::<P>(pipe, src, dst_pixels, dst_alpha, None, x0, x1, y);
309}
310
311pub(super) fn draw_span_clipped<P: Pixel, S: RowSink<P>>(
313 sink: &mut S,
314 clip: &Clip,
315 pipe: &PipeState<'_>,
316 src: &PipeSrc<'_>,
317 x0: i32,
318 x1: i32,
319 y: i32,
320) {
321 let mut run_start: Option<i32> = None;
324
325 for x in x0..=x1 {
326 if clip.test(x, y) {
327 if run_start.is_none() {
328 run_start = Some(x);
329 }
330 } else if let Some(rs) = run_start.take() {
331 draw_span(sink, pipe, src, rs, x - 1, y);
332 }
333 }
334 if let Some(rs) = run_start {
335 draw_span(sink, pipe, src, rs, x1, y);
336 }
337}
338
339fn draw_aa_line<P: Pixel>(
345 bitmap: &mut Bitmap<P>,
346 pipe: &PipeState<'_>,
347 src: &PipeSrc<'_>,
348 aa_buf: &AaBuf,
349 x0: i32,
350 x1: i32,
351 y: i32,
352) {
353 debug_assert!(x0 >= 0, "draw_aa_line: x0={x0} is negative");
354 debug_assert!(x0 <= x1, "draw_aa_line: x0={x0} > x1={x1}");
355 debug_assert!(y >= 0, "draw_aa_line: y={y} is negative");
356
357 #[expect(clippy::cast_sign_loss, reason = "x0 >= 0: asserted above")]
358 let x0_usize = x0 as usize;
359 #[expect(clippy::cast_sign_loss, reason = "x1 >= x0 >= 0")]
360 let count = (x1 - x0 + 1) as usize;
361
362 let rows = [
366 aa_buf.row_slice(0),
367 aa_buf.row_slice(1),
368 aa_buf.row_slice(2),
369 aa_buf.row_slice(3),
370 ];
371 let mut shape = vec![0u8; count];
372 simd::aa_coverage_span(rows, x0_usize, &mut shape);
373
374 let mut any_nonzero = false;
376 for s in &mut shape {
377 let t = *s as usize;
378 if t > 0 {
379 *s = AA_GAMMA[t];
380 any_nonzero = true;
381 }
382 }
383
384 if !any_nonzero {
385 return;
386 }
387
388 #[expect(clippy::cast_sign_loss, reason = "y >= 0")]
389 let y_u = y as u32;
390 let byte_off = x0_usize * P::BYTES;
391 #[expect(clippy::cast_sign_loss, reason = "x1 >= x0 >= 0")]
392 let byte_end = (x1 as usize + 1) * P::BYTES;
393 #[expect(clippy::cast_sign_loss, reason = "x1 >= x0 >= 0")]
394 let alpha_range = x0_usize..=x1 as usize;
395
396 let (row, alpha) = bitmap.row_and_alpha_mut(y_u);
397 let dst_pixels = &mut row[byte_off..byte_end];
398 let dst_alpha = alpha.map(|a| &mut a[alpha_range]);
399
400 pipe::render_span::<P>(pipe, src, dst_pixels, dst_alpha, Some(&shape), x0, x1, y);
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use crate::bitmap::Bitmap;
407 use crate::path::PathBuilder;
408 use crate::pipe::PipeSrc;
409 use crate::testutil::{identity_matrix, make_clip, rect_path, simple_pipe};
410 use color::Rgb8;
411
412 #[test]
413 fn fill_rect_paints_solid() {
414 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
415 let clip = make_clip(8, 8);
416 let pipe = simple_pipe();
417 let color = [200u8, 100, 50];
418 let src = PipeSrc::Solid(&color);
419 let path = rect_path(1.0, 1.0, 5.0, 5.0);
423
424 fill::<Rgb8>(
425 &mut bmp,
426 &clip,
427 &path,
428 &pipe,
429 &src,
430 &identity_matrix(),
431 1.0,
432 false,
433 );
434
435 for y in 2..5u32 {
437 let row = bmp.row(y);
438 for (x, px) in row.iter().enumerate().skip(1).take(5) {
439 assert_eq!(px.r, 200, "y={y} x={x} R");
440 assert_eq!(px.g, 100, "y={y} x={x} G");
441 assert_eq!(px.b, 50, "y={y} x={x} B");
442 }
443 }
444
445 assert_eq!(bmp.row(0)[0].r, 0, "row 0 should be untouched");
447 assert_eq!(bmp.row(1)[0].r, 0, "top edge row should be untouched");
448 assert_eq!(bmp.row(2)[0].r, 0, "x=0 should be untouched");
449 }
450
451 #[test]
452 fn fill_empty_path_is_noop() {
453 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
454 let clip = make_clip(8, 8);
455 let pipe = simple_pipe();
456 let color = [255u8, 0, 0];
457 let src = PipeSrc::Solid(&color);
458 let path = PathBuilder::new().build(); fill::<Rgb8>(
461 &mut bmp,
462 &clip,
463 &path,
464 &pipe,
465 &src,
466 &identity_matrix(),
467 1.0,
468 false,
469 );
470
471 assert_eq!(bmp.row(0)[0].r, 0);
473 }
474
475 #[test]
476 fn eo_fill_donut_leaves_interior_clear() {
477 let mut bmp: Bitmap<Rgb8> = Bitmap::new(10, 10, 4, false);
479 let clip = make_clip(10, 10);
480 let pipe = simple_pipe();
481 let color = [255u8, 0, 0];
482 let src = PipeSrc::Solid(&color);
483
484 let mut b = PathBuilder::new();
486 b.move_to(1.0, 1.0).unwrap();
487 b.line_to(8.0, 1.0).unwrap();
488 b.line_to(8.0, 8.0).unwrap();
489 b.line_to(1.0, 8.0).unwrap();
490 b.close(true).unwrap();
491 b.move_to(3.0, 3.0).unwrap();
492 b.line_to(6.0, 3.0).unwrap();
493 b.line_to(6.0, 6.0).unwrap();
494 b.line_to(3.0, 6.0).unwrap();
495 b.close(true).unwrap();
496 let path = b.build();
497
498 eo_fill::<Rgb8>(
499 &mut bmp,
500 &clip,
501 &path,
502 &pipe,
503 &src,
504 &identity_matrix(),
505 1.0,
506 false,
507 );
508
509 assert_eq!(bmp.row(4)[4].r, 0, "interior should be clear with EO rule");
511 assert_eq!(bmp.row(2)[2].r, 255, "outer band should be painted");
513 }
514
515 #[test]
516 fn aa_gamma_table_correct() {
517 let max_idx = AA_GAMMA.len() - 1; #[expect(
522 clippy::cast_precision_loss,
523 reason = "AA_GAMMA.len() ≤ 65 in practice; f64 represents it exactly"
524 )]
525 let divisor = max_idx as f64;
526 for (i, &actual) in AA_GAMMA.iter().enumerate() {
527 #[expect(
530 clippy::cast_possible_truncation,
531 clippy::cast_sign_loss,
532 clippy::cast_precision_loss,
533 reason = "value is f64::round() of a [0,255]-bounded expression"
534 )]
535 let expected = ((i as f64 / divisor).powf(1.5) * 255.0).round() as u8;
536 assert_eq!(actual, expected, "AA_GAMMA[{i}]: expected {expected}");
537 }
538 }
539
540 #[test]
541 fn scanner_produces_spans_for_rect() {
542 use crate::scanner::XPathScanner;
543 use crate::scanner::iter::ScanIterator;
544 use crate::xpath::XPath;
545
546 let path = rect_path(1.0, 1.0, 5.0, 5.0);
547 let xpath = XPath::new(&path, &identity_matrix(), 1.0, true);
548 let scanner = XPathScanner::new(&xpath, false, 0, 7);
549 assert!(
552 ScanIterator::new(&scanner, 2).next().is_some(),
553 "no spans at y=2"
554 );
555 assert!(
556 ScanIterator::new(&scanner, 3).next().is_some(),
557 "no spans at y=3"
558 );
559 assert!(
560 ScanIterator::new(&scanner, 4).next().is_some(),
561 "no spans at y=4"
562 );
563 }
564
565 #[cfg(feature = "rayon")]
568 mod parallel {
569 use super::*;
570 use crate::fill::{eo_fill_parallel, fill_parallel};
571
572 #[test]
575 fn fill_parallel_matches_sequential() {
576 const W: u32 = 64;
577 const H: u32 = 512;
578
579 let mut seq: Bitmap<Rgb8> = Bitmap::new(W, H, 1, false);
580 let mut par: Bitmap<Rgb8> = Bitmap::new(W, H, 1, false);
581
582 let clip = make_clip(W, H);
583 let pipe = simple_pipe();
584 let color = [77u8, 155, 211];
585 let src = PipeSrc::Solid(&color);
586 let path = rect_path(4.0, 4.0, 60.0, 508.0);
587 let matrix = identity_matrix();
588
589 fill::<Rgb8>(&mut seq, &clip, &path, &pipe, &src, &matrix, 1.0, false);
590 fill_parallel::<Rgb8>(&mut par, &clip, &path, &pipe, &src, &matrix, 1.0, false, 4);
591
592 assert_eq!(
593 seq.data(),
594 par.data(),
595 "parallel fill output differs from sequential"
596 );
597 }
598
599 #[test]
601 fn fill_parallel_single_band_is_sequential() {
602 const W: u32 = 32;
603 const H: u32 = 512;
604
605 let mut seq: Bitmap<Rgb8> = Bitmap::new(W, H, 1, false);
606 let mut par: Bitmap<Rgb8> = Bitmap::new(W, H, 1, false);
607
608 let clip = make_clip(W, H);
609 let pipe = simple_pipe();
610 let color = [33u8, 66, 99];
611 let src = PipeSrc::Solid(&color);
612 let path = rect_path(2.0, 2.0, 30.0, 510.0);
613 let matrix = identity_matrix();
614
615 fill::<Rgb8>(&mut seq, &clip, &path, &pipe, &src, &matrix, 1.0, false);
616 fill_parallel::<Rgb8>(&mut par, &clip, &path, &pipe, &src, &matrix, 1.0, false, 1);
618
619 assert_eq!(
620 seq.data(),
621 par.data(),
622 "single-band parallel fill output differs from sequential"
623 );
624 }
625
626 #[test]
628 fn eo_fill_parallel_matches_sequential() {
629 const W: u32 = 64;
630 const H: u32 = 512;
631
632 let mut seq: Bitmap<Rgb8> = Bitmap::new(W, H, 1, false);
633 let mut par: Bitmap<Rgb8> = Bitmap::new(W, H, 1, false);
634
635 let clip = make_clip(W, H);
636 let pipe = simple_pipe();
637 let color = [200u8, 100, 50];
638 let src = PipeSrc::Solid(&color);
639
640 let mut b = PathBuilder::new();
642 b.move_to(4.0, 4.0).unwrap();
643 b.line_to(60.0, 4.0).unwrap();
644 b.line_to(60.0, 508.0).unwrap();
645 b.line_to(4.0, 508.0).unwrap();
646 b.close(true).unwrap();
647 b.move_to(16.0, 16.0).unwrap();
648 b.line_to(48.0, 16.0).unwrap();
649 b.line_to(48.0, 496.0).unwrap();
650 b.line_to(16.0, 496.0).unwrap();
651 b.close(true).unwrap();
652 let path = b.build();
653 let matrix = identity_matrix();
654
655 eo_fill::<Rgb8>(&mut seq, &clip, &path, &pipe, &src, &matrix, 1.0, false);
656 eo_fill_parallel::<Rgb8>(&mut par, &clip, &path, &pipe, &src, &matrix, 1.0, false, 4);
657
658 assert_eq!(
659 seq.data(),
660 par.data(),
661 "parallel eo_fill output differs from sequential"
662 );
663 }
664 }
665}