1use crate::bitmap::Bitmap;
13use crate::clip::{Clip, ClipResult};
14use crate::pipe::{self, PipeSrc, PipeState};
15use crate::simd;
16use color::Pixel;
17
18pub struct GlyphBitmap<'a> {
23 pub data: &'a [u8],
25 pub x: i32,
27 pub y: i32,
29 pub w: i32,
31 pub h: i32,
33 pub aa: bool,
36}
37
38impl GlyphBitmap<'_> {
39 #[must_use]
41 pub fn row_bytes(&self) -> usize {
42 let w = self.w.max(0);
43 if self.aa {
44 #[expect(clippy::cast_sign_loss, reason = "w = self.w.max(0) is non-negative")]
45 {
46 w as usize
47 }
48 } else {
49 #[expect(clippy::cast_sign_loss, reason = "w = self.w.max(0) is non-negative")]
52 {
53 (w.saturating_add(7) / 8) as usize
54 }
55 }
56 }
57}
58
59#[expect(
71 clippy::too_many_arguments,
72 reason = "mirrors Splash::fillGlyph2 API; all params necessary"
73)]
74pub fn blit_glyph<P: Pixel>(
75 bitmap: &mut Bitmap<P>,
76 clip: &Clip,
77 clip_all_inside: bool,
78 pipe: &PipeState<'_>,
79 src: &PipeSrc<'_>,
80 pen_x: i32,
81 pen_y: i32,
82 glyph: &GlyphBitmap<'_>,
83) {
84 let x_start_raw = pen_x - glyph.x;
86 let y_start_raw = pen_y - glyph.y;
87
88 #[expect(
90 clippy::cast_possible_wrap,
91 reason = "bitmap dims ≤ i32::MAX in practice"
92 )]
93 let bmp_w = bitmap.width as i32;
94 #[expect(
95 clippy::cast_possible_wrap,
96 reason = "bitmap dims ≤ i32::MAX in practice"
97 )]
98 let bmp_h = bitmap.height as i32;
99
100 let x_clip = x_start_raw.max(0);
101 let y_clip = y_start_raw.max(0);
102 let x_end = (x_start_raw + glyph.w).min(bmp_w);
103 let y_end = (y_start_raw + glyph.h).min(bmp_h);
104
105 if x_clip >= x_end || y_clip >= y_end {
106 return;
107 }
108
109 #[expect(
111 clippy::cast_sign_loss,
112 reason = "x_clip ≥ x_start_raw.max(0) so difference ≥ 0"
113 )]
114 let x_data_skip = (x_clip - x_start_raw) as usize;
115 #[expect(
116 clippy::cast_sign_loss,
117 reason = "y_clip ≥ y_start_raw.max(0) so difference ≥ 0"
118 )]
119 let y_data_skip = (y_clip - y_start_raw) as usize;
120 #[expect(clippy::cast_sign_loss, reason = "x_end > x_clip by guard above")]
121 let xx_limit = (x_end - x_clip) as usize;
122 #[expect(clippy::cast_sign_loss, reason = "y_end > y_clip by guard above")]
123 let yy_limit = (y_end - y_clip) as usize;
124 let row_bytes = glyph.row_bytes();
125
126 if glyph.aa {
127 blit_aa::<P>(
128 bitmap,
129 clip,
130 clip_all_inside,
131 pipe,
132 src,
133 glyph,
134 x_clip,
135 y_clip,
136 x_data_skip,
137 y_data_skip,
138 xx_limit,
139 yy_limit,
140 row_bytes,
141 );
142 } else {
143 blit_mono::<P>(
144 bitmap,
145 clip,
146 clip_all_inside,
147 pipe,
148 src,
149 glyph,
150 x_clip,
151 y_clip,
152 x_data_skip,
153 y_data_skip,
154 xx_limit,
155 yy_limit,
156 row_bytes,
157 );
158 }
159}
160
161#[expect(
163 clippy::too_many_arguments,
164 reason = "internal helper; all params necessary for this blit path"
165)]
166fn blit_aa<P: Pixel>(
167 bitmap: &mut Bitmap<P>,
168 clip: &Clip,
169 clip_all_inside: bool,
170 pipe: &PipeState<'_>,
171 src: &PipeSrc<'_>,
172 glyph: &GlyphBitmap<'_>,
173 x_start: i32,
174 y_start: i32,
175 x_data_skip: usize,
176 y_data_skip: usize,
177 xx_limit: usize,
178 yy_limit: usize,
179 row_bytes: usize,
180) {
181 let data = glyph.data;
182 debug_assert!(
186 data.len() >= (y_data_skip + yy_limit) * row_bytes,
187 "blit_aa: glyph data too short: len={} < (y_data_skip={y_data_skip} + yy_limit={yy_limit}) * row_bytes={row_bytes}",
188 data.len(),
189 );
190 let mut run_shape: Vec<u8> = Vec::new();
192
193 for yy in 0..yy_limit {
194 #[expect(
195 clippy::cast_possible_truncation,
196 reason = "yy < yy_limit ≤ bitmap.height ≤ i32::MAX"
197 )]
198 #[expect(clippy::cast_possible_wrap, reason = "yy < bitmap.height ≤ i32::MAX")]
199 let y = y_start + yy as i32;
200 let row_off = (y_data_skip + yy) * row_bytes + x_data_skip;
201
202 let mut run_start: Option<i32> = None;
203 run_shape.clear();
204
205 for xx in 0..xx_limit {
206 #[expect(
207 clippy::cast_possible_truncation,
208 reason = "xx < xx_limit ≤ bitmap.width ≤ i32::MAX"
209 )]
210 #[expect(clippy::cast_possible_wrap, reason = "xx < bitmap.width ≤ i32::MAX")]
211 let x = x_start + xx as i32;
212 let data_idx = row_off + xx;
213 let alpha = data.get(data_idx).copied().unwrap_or(0);
214 let inside_clip = clip_all_inside || clip.test(x, y);
215
216 if inside_clip && alpha != 0 {
217 if run_start.is_none() {
218 run_start = Some(x);
219 run_shape.clear();
220 }
221 run_shape.push(alpha);
222 } else if let Some(rs) = run_start.take() {
223 emit_aa_run::<P>(bitmap, pipe, src, rs, y, &run_shape);
224 }
225 }
226 if let Some(rs) = run_start.take() {
227 emit_aa_run::<P>(bitmap, pipe, src, rs, y, &run_shape);
228 }
229 }
230}
231
232#[expect(
234 clippy::too_many_arguments,
235 reason = "internal helper; all params necessary"
236)]
237#[expect(
238 clippy::too_many_lines,
239 reason = "function handles both SIMD and scalar mono-unpack paths; splitting further would obscure the logic"
240)]
241fn blit_mono<P: Pixel>(
242 bitmap: &mut Bitmap<P>,
243 clip: &Clip,
244 clip_all_inside: bool,
245 pipe: &PipeState<'_>,
246 src: &PipeSrc<'_>,
247 glyph: &GlyphBitmap<'_>,
248 x_start: i32,
249 y_start: i32,
250 x_data_skip: usize,
251 y_data_skip: usize,
252 xx_limit: usize,
253 yy_limit: usize,
254 row_bytes: usize,
255) {
256 let x_shift = x_data_skip % 8;
257 let data = glyph.data;
258
259 debug_assert!(
261 data.len() >= (y_data_skip + yy_limit) * row_bytes,
262 "blit_mono: glyph data too short: len={} < (y_data_skip={y_data_skip} + yy_limit={yy_limit}) * row_bytes={row_bytes}",
263 data.len(),
264 );
265
266 let mut expanded: Vec<u8> = vec![0u8; xx_limit];
269
270 for yy in 0..yy_limit {
271 #[expect(
272 clippy::cast_possible_truncation,
273 reason = "yy < yy_limit ≤ bitmap.height ≤ i32::MAX"
274 )]
275 #[expect(clippy::cast_possible_wrap, reason = "yy < bitmap.height ≤ i32::MAX")]
276 let y = y_start + yy as i32;
277 let row_off = (y_data_skip + yy) * row_bytes + x_data_skip / 8;
278
279 #[cfg(target_arch = "x86_64")]
283 let use_simd_unpack = x_shift == 0;
284 #[cfg(not(target_arch = "x86_64"))]
285 let use_simd_unpack = false;
286
287 if use_simd_unpack {
288 let packed_row = &data[row_off..];
289 simd::unpack_mono_row(packed_row, xx_limit, &mut expanded);
290
291 let mut run_start: Option<i32> = None;
292 for (xx, &cov) in expanded[..xx_limit].iter().enumerate() {
293 #[expect(
294 clippy::cast_possible_truncation,
295 reason = "xx < xx_limit ≤ bitmap.width ≤ i32::MAX"
296 )]
297 #[expect(clippy::cast_possible_wrap, reason = "xx < bitmap.width ≤ i32::MAX")]
298 let x = x_start + xx as i32;
299 let set = cov != 0;
300 let inside_clip = clip_all_inside || clip.test(x, y);
301
302 if set && inside_clip {
303 if run_start.is_none() {
304 run_start = Some(x);
305 }
306 } else if let Some(rs) = run_start.take() {
307 let rx1 = x - 1;
308 emit_solid_run::<P>(bitmap, pipe, src, rs, rx1, y);
309 }
310 }
311 if let Some(rs) = run_start.take() {
312 #[expect(
313 clippy::cast_possible_truncation,
314 reason = "xx_limit ≤ bitmap.width ≤ i32::MAX"
315 )]
316 #[expect(
317 clippy::cast_possible_wrap,
318 reason = "xx_limit < bitmap.width ≤ i32::MAX"
319 )]
320 let rx1 = x_start + xx_limit as i32 - 1;
321 emit_solid_run::<P>(bitmap, pipe, src, rs, rx1, y);
322 }
323 continue;
324 }
325
326 let mut run_start: Option<i32> = None;
327 let mut xx = 0usize;
328
329 while xx < xx_limit {
330 let byte_idx = row_off + xx / 8;
331
332 let alpha0 = if x_shift > 0 && xx + 8 < xx_limit {
334 let lo = data.get(byte_idx).copied().unwrap_or(0);
335 let hi = data.get(byte_idx + 1).copied().unwrap_or(0);
336 #[expect(clippy::cast_possible_truncation, reason = "shift result fits in u8")]
337 {
338 (u16::from(lo) << x_shift | u16::from(hi) >> (8 - x_shift)) as u8
339 }
340 } else {
341 data.get(byte_idx).copied().unwrap_or(0)
342 };
343
344 let bits_this_byte = (xx_limit - xx).min(8);
345 for bit in 0..bits_this_byte {
346 #[expect(
347 clippy::cast_possible_truncation,
348 reason = "xx + bit < xx_limit ≤ bitmap.width ≤ i32::MAX"
349 )]
350 #[expect(
351 clippy::cast_possible_wrap,
352 reason = "xx + bit < bitmap.width ≤ i32::MAX"
353 )]
354 let x = x_start + (xx + bit) as i32;
355 let set = (alpha0 >> (7 - bit)) & 1 != 0;
356 let inside_clip = clip_all_inside || clip.test(x, y);
357
358 if set && inside_clip {
359 if run_start.is_none() {
360 run_start = Some(x);
361 }
362 } else if let Some(rs) = run_start.take() {
363 let rx1 = x - 1;
364 emit_solid_run::<P>(bitmap, pipe, src, rs, rx1, y);
365 }
366 }
367 xx += bits_this_byte;
368 }
369 if let Some(rs) = run_start.take() {
370 #[expect(
371 clippy::cast_possible_truncation,
372 reason = "xx_limit ≤ bitmap.width ≤ i32::MAX"
373 )]
374 #[expect(
375 clippy::cast_possible_wrap,
376 reason = "xx_limit < bitmap.width ≤ i32::MAX"
377 )]
378 let rx1 = x_start + xx_limit as i32 - 1;
379 emit_solid_run::<P>(bitmap, pipe, src, rs, rx1, y);
380 }
381 }
382}
383
384fn emit_aa_run<P: Pixel>(
386 bitmap: &mut Bitmap<P>,
387 pipe: &PipeState<'_>,
388 src: &PipeSrc<'_>,
389 x0: i32,
390 y: i32,
391 shape: &[u8],
392) {
393 debug_assert!(!shape.is_empty());
394 #[expect(
396 clippy::cast_possible_truncation,
397 reason = "shape.len() ≤ bitmap.width ≤ i32::MAX"
398 )]
399 #[expect(
400 clippy::cast_possible_wrap,
401 reason = "shape.len() ≤ bitmap.width ≤ i32::MAX"
402 )]
403 let x1 = x0 + shape.len() as i32 - 1;
404 #[expect(clippy::cast_sign_loss, reason = "y ≥ 0 by construction")]
405 let y_u = y as u32;
406 #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0")]
407 let byte_off = x0 as usize * P::BYTES;
408 #[expect(clippy::cast_sign_loss, reason = "x1 ≥ x0 ≥ 0")]
409 let byte_end = (x1 as usize + 1) * P::BYTES;
410 #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0, x1 ≥ x0")]
411 let alpha_range = x0 as usize..=x1 as usize;
412
413 let (row, alpha) = bitmap.row_and_alpha_mut(y_u);
414 let dst_pixels = &mut row[byte_off..byte_end];
415 let dst_alpha = alpha.map(|a| &mut a[alpha_range]);
416
417 pipe::render_span::<P>(pipe, src, dst_pixels, dst_alpha, Some(shape), x0, x1, y);
418}
419
420fn emit_solid_run<P: Pixel>(
422 bitmap: &mut Bitmap<P>,
423 pipe: &PipeState<'_>,
424 src: &PipeSrc<'_>,
425 x0: i32,
426 x1: i32,
427 y: i32,
428) {
429 debug_assert!(x0 <= x1);
430 #[expect(clippy::cast_sign_loss, reason = "y ≥ 0 by construction")]
431 let y_u = y as u32;
432 #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0")]
433 let byte_off = x0 as usize * P::BYTES;
434 #[expect(clippy::cast_sign_loss, reason = "x1 ≥ x0 ≥ 0")]
435 let byte_end = (x1 as usize + 1) * P::BYTES;
436 #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0, x1 ≥ x0")]
437 let alpha_range = x0 as usize..=x1 as usize;
438
439 let (row, alpha) = bitmap.row_and_alpha_mut(y_u);
440 let dst_pixels = &mut row[byte_off..byte_end];
441 let dst_alpha = alpha.map(|a| &mut a[alpha_range]);
442
443 pipe::render_span::<P>(pipe, src, dst_pixels, dst_alpha, None, x0, x1, y);
444}
445
446pub fn fill_glyph<P: Pixel>(
451 bitmap: &mut Bitmap<P>,
452 clip: &Clip,
453 pipe: &PipeState<'_>,
454 src: &PipeSrc<'_>,
455 pen_x: i32,
456 pen_y: i32,
457 glyph: &GlyphBitmap<'_>,
458) -> ClipResult {
459 let x0 = pen_x - glyph.x;
460 let y0 = pen_y - glyph.y;
461 let x1 = x0 + glyph.w - 1;
462 let y1 = y0 + glyph.h - 1;
463
464 let clip_res = clip.test_rect(x0, y0, x1, y1);
465 if clip_res != ClipResult::AllOutside {
466 blit_glyph::<P>(
467 bitmap,
468 clip,
469 clip_res == ClipResult::AllInside,
470 pipe,
471 src,
472 pen_x,
473 pen_y,
474 glyph,
475 );
476 }
477 clip_res
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use crate::bitmap::Bitmap;
484 use crate::pipe::PipeSrc;
485 use crate::testutil::{make_clip, simple_pipe};
486 use color::Rgb8;
487
488 #[test]
489 fn blit_aa_glyph_paints_pixels() {
490 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
491 let clip = make_clip(8, 8);
492 let pipe = simple_pipe();
493 let color = [255u8, 0, 0];
494 let src = PipeSrc::Solid(&color);
495
496 let data = vec![255u8; 16];
498 let glyph = GlyphBitmap {
499 data: &data,
500 x: 0,
501 y: 0,
502 w: 4,
503 h: 4,
504 aa: true,
505 };
506
507 blit_glyph::<Rgb8>(&mut bmp, &clip, true, &pipe, &src, 2, 2, &glyph);
508
509 for y in 2..6u32 {
511 let row = bmp.row(y);
512 for (x, px) in row.iter().enumerate().skip(2).take(4) {
513 assert_eq!(px.r, 255, "y={y} x={x} R");
514 }
515 }
516 assert_eq!(bmp.row(1)[1].r, 0);
517 }
518
519 #[test]
520 fn blit_aa_glyph_zero_coverage_skips() {
521 let mut bmp: Bitmap<Rgb8> = Bitmap::new(4, 4, 4, false);
522 let clip = make_clip(4, 4);
523 let pipe = simple_pipe();
524 let color = [255u8, 0, 0];
525 let src = PipeSrc::Solid(&color);
526
527 let data = vec![0u8; 4];
528 let glyph = GlyphBitmap {
529 data: &data,
530 x: 0,
531 y: 0,
532 w: 2,
533 h: 2,
534 aa: true,
535 };
536
537 blit_glyph::<Rgb8>(&mut bmp, &clip, true, &pipe, &src, 0, 0, &glyph);
538
539 for y in 0..4u32 {
540 let row = bmp.row(y);
541 for px in row.iter().take(4) {
542 assert_eq!(px.r, 0, "should be unpainted");
543 }
544 }
545 }
546
547 #[test]
548 fn blit_mono_glyph_paints_set_bits() {
549 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 4, 4, false);
550 let clip = make_clip(8, 4);
551 let pipe = simple_pipe();
552 let color = [0u8, 255, 0];
553 let src = PipeSrc::Solid(&color);
554
555 let data = [0xFFu8, 0x00u8];
557 let glyph = GlyphBitmap {
558 data: &data,
559 x: 0,
560 y: 0,
561 w: 8,
562 h: 2,
563 aa: false,
564 };
565
566 blit_glyph::<Rgb8>(&mut bmp, &clip, true, &pipe, &src, 0, 0, &glyph);
567
568 let row0 = bmp.row(0);
569 for (x, px) in row0.iter().enumerate().take(8) {
570 assert_eq!(px.g, 255, "row 0 x={x} should be painted");
571 }
572 let row1 = bmp.row(1);
573 for (x, px) in row1.iter().enumerate().take(8) {
574 assert_eq!(px.g, 0, "row 1 x={x} should be clear");
575 }
576 }
577
578 #[test]
579 fn blit_glyph_clip_excludes_outside() {
580 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
581 let clip = Clip::new(2.0, 2.0, 5.0, 5.0, false);
582 let pipe = simple_pipe();
583 let color = [255u8, 0, 0];
584 let src = PipeSrc::Solid(&color);
585
586 let data = vec![255u8; 16];
587 let glyph = GlyphBitmap {
588 data: &data,
589 x: 0,
590 y: 0,
591 w: 4,
592 h: 4,
593 aa: true,
594 };
595
596 blit_glyph::<Rgb8>(&mut bmp, &clip, false, &pipe, &src, 0, 0, &glyph);
597
598 assert_eq!(bmp.row(0)[0].r, 0, "row 0 should be clipped");
599 assert_eq!(bmp.row(1)[0].r, 0, "row 1 should be clipped");
600 assert_eq!(bmp.row(2)[2].r, 255, "(2,2) should be painted");
601 }
602
603 #[test]
604 fn fill_glyph_returns_clip_result() {
605 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
606 let clip = make_clip(8, 8);
607 let pipe = simple_pipe();
608 let color = [128u8, 128, 128];
609 let src = PipeSrc::Solid(&color);
610
611 let data = vec![128u8; 4];
612 let glyph = GlyphBitmap {
613 data: &data,
614 x: 0,
615 y: 0,
616 w: 2,
617 h: 2,
618 aa: true,
619 };
620
621 let res = fill_glyph::<Rgb8>(&mut bmp, &clip, &pipe, &src, 1, 1, &glyph);
622 assert_eq!(res, ClipResult::AllInside);
623 }
624
625 #[test]
626 fn glyph_row_bytes_aa() {
627 let data = [];
628 let g = GlyphBitmap {
629 data: &data,
630 x: 0,
631 y: 0,
632 w: 7,
633 h: 1,
634 aa: true,
635 };
636 assert_eq!(g.row_bytes(), 7);
637 }
638
639 #[test]
640 fn glyph_row_bytes_mono() {
641 let data = [];
642 let g = GlyphBitmap {
643 data: &data,
644 x: 0,
645 y: 0,
646 w: 7,
647 h: 1,
648 aa: false,
649 };
650 assert_eq!(g.row_bytes(), 1); let g9 = GlyphBitmap {
652 data: &data,
653 x: 0,
654 y: 0,
655 w: 9,
656 h: 1,
657 aa: false,
658 };
659 assert_eq!(g9.row_bytes(), 2); }
661}