1use crate::color::Color;
8use crate::css_parser::{CssParser, FontStyle, IconMapping};
9use crate::embedded;
10use crate::error::{IconFontError, Result};
11use ab_glyph::{Font, FontArc, GlyphId, PxScale};
12use rayon::prelude::*;
13use rustc_hash::FxHashMap;
14use std::path::Path;
15use std::sync::{Arc, RwLock};
16
17const PARALLEL_PIXEL_THRESHOLD: usize = 32 * 1024;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum HorizontalAnchor {
25 Left,
26 #[default]
27 Center,
28 Right,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum VerticalAnchor {
34 Top,
35 #[default]
36 Center,
37 Bottom,
38}
39
40#[derive(Debug, Clone)]
42pub struct RenderConfig {
43 pub canvas_width: u32,
45 pub canvas_height: u32,
47 pub icon_size: u32,
49 pub supersample_factor: u32,
51 pub icon_color: Color,
53 pub background_color: Color,
55 pub horizontal_anchor: HorizontalAnchor,
57 pub vertical_anchor: VerticalAnchor,
59 pub offset_x: i32,
61 pub offset_y: i32,
63 pub rotate: f64,
65}
66
67impl Default for RenderConfig {
68 fn default() -> Self {
69 Self {
70 canvas_width: 512,
71 canvas_height: 512,
72 icon_size: 486,
74 supersample_factor: 2,
75 icon_color: Color::black(),
76 background_color: Color::white(),
77 horizontal_anchor: HorizontalAnchor::Center,
78 vertical_anchor: VerticalAnchor::Center,
79 offset_x: 0,
80 offset_y: 0,
81 rotate: 0.0,
82 }
83 }
84}
85
86impl RenderConfig {
87 pub fn new() -> Self {
89 Self::default()
90 }
91
92 pub fn canvas_size(mut self, width: u32, height: u32) -> Self {
94 self.canvas_width = width;
95 self.canvas_height = height;
96 self
97 }
98
99 pub fn icon_size(mut self, size: u32) -> Self {
101 self.icon_size = size;
102 self
103 }
104
105 pub fn supersample(mut self, factor: u32) -> Self {
107 self.supersample_factor = factor.max(1);
108 self
109 }
110
111 pub fn icon_color(mut self, color: Color) -> Self {
113 self.icon_color = color;
114 self
115 }
116
117 pub fn background_color(mut self, color: Color) -> Self {
119 self.background_color = color;
120 self
121 }
122
123 pub fn anchor(mut self, horizontal: HorizontalAnchor, vertical: VerticalAnchor) -> Self {
125 self.horizontal_anchor = horizontal;
126 self.vertical_anchor = vertical;
127 self
128 }
129
130 pub fn offset(mut self, x: i32, y: i32) -> Self {
132 self.offset_x = x;
133 self.offset_y = y;
134 self
135 }
136
137 pub fn rotate(mut self, degrees: f64) -> Self {
158 self.rotate = degrees;
159 self
160 }
161
162 pub fn sanitize_icon_size(mut self) -> Self {
168 let smaller_dim = self.canvas_width.min(self.canvas_height);
169 if self.icon_size > smaller_dim {
170 self.icon_size = ((smaller_dim as f64) * 0.95) as u32;
172 self.icon_size = self.icon_size.max(1);
173 }
174 self
175 }
176}
177
178pub struct IconRenderer {
185 css_parser: CssParser,
187 font_solid: FontArc,
189 font_regular: FontArc,
191 font_brands: FontArc,
193 glyph_cache: RwLock<FxHashMap<GlyphCacheKey, Arc<GlyphMask>>>,
196}
197
198#[derive(Debug, Clone)]
199struct GlyphMask {
200 width: u32,
201 height: u32,
202 alpha: Vec<u8>,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
206struct GlyphCacheKey {
207 style: FontStyle,
208 codepoint: u32,
209 ss_icon_size: u32,
210}
211
212impl IconRenderer {
213 pub fn new() -> Result<Self> {
238 let css_parser = CssParser::parse(embedded::FONTAWESOME_CSS)?;
239 let font_solid = FontArc::try_from_slice(embedded::FONT_SOLID).map_err(|err| {
240 IconFontError::FontLoadError(format!("Failed to parse embedded fa-solid.otf: {}", err))
241 })?;
242 let font_regular = FontArc::try_from_slice(embedded::FONT_REGULAR).map_err(|err| {
243 IconFontError::FontLoadError(format!(
244 "Failed to parse embedded fa-regular.otf: {}",
245 err
246 ))
247 })?;
248 let font_brands = FontArc::try_from_slice(embedded::FONT_BRANDS).map_err(|err| {
249 IconFontError::FontLoadError(format!("Failed to parse embedded fa-brands.otf: {}", err))
250 })?;
251
252 Ok(Self {
253 css_parser,
254 font_solid,
255 font_regular,
256 font_brands,
257 glyph_cache: RwLock::new(FxHashMap::with_capacity_and_hasher(256, Default::default())),
258 })
259 }
260
261 pub fn from_path<P: AsRef<Path>>(assets_dir: P) -> Result<Self> {
291 let assets_dir = assets_dir.as_ref();
292
293 let font_solid = Self::load_font(assets_dir.join("fa-solid.otf"), "fa-solid.otf")?;
294 let font_regular = Self::load_font(assets_dir.join("fa-regular.otf"), "fa-regular.otf")?;
295 let font_brands = Self::load_font(assets_dir.join("fa-brands.otf"), "fa-brands.otf")?;
296
297 let css_path = assets_dir.join("fontawesome.css");
299 let css_content = std::fs::read_to_string(&css_path)
300 .map_err(|e| IconFontError::CssParseError(format!("Failed to read CSS file: {}", e)))?;
301 let css_parser = CssParser::parse(&css_content)?;
302
303 Ok(Self {
304 css_parser,
305 font_solid,
306 font_regular,
307 font_brands,
308 glyph_cache: RwLock::new(FxHashMap::with_capacity_and_hasher(256, Default::default())),
309 })
310 }
311
312 fn load_font<P: AsRef<Path>>(path: P, name: &str) -> Result<FontArc> {
314 let path = path.as_ref();
315 let data = std::fs::read(path).map_err(|e| {
316 IconFontError::FontLoadError(format!(
317 "Failed to read font file {}: {}",
318 path.display(),
319 e
320 ))
321 })?;
322 FontArc::try_from_vec(data).map_err(|e| {
323 IconFontError::FontLoadError(format!("Failed to parse font {}: {}", name, e))
324 })
325 }
326
327 fn get_font(&self, style: FontStyle) -> &FontArc {
329 match style {
330 FontStyle::Solid => &self.font_solid,
331 FontStyle::Regular => &self.font_regular,
332 FontStyle::Brands => &self.font_brands,
333 }
334 }
335
336 #[inline]
338 fn get_or_rasterized_glyph_mask(
339 &self,
340 style: FontStyle,
341 codepoint: char,
342 ss_icon_size: u32,
343 ) -> Option<Arc<GlyphMask>> {
344 let key = GlyphCacheKey {
345 style,
346 codepoint: codepoint as u32,
347 ss_icon_size,
348 };
349
350 if let Some(mask) = self
352 .glyph_cache
353 .read()
354 .expect("glyph cache rwlock poisoned")
355 .get(&key)
356 .map(Arc::clone)
357 {
358 return Some(mask);
359 }
360
361 let font = self.get_font(style);
362 let glyph_id = font.glyph_id(codepoint);
363 if glyph_id == GlyphId(0) {
364 return None;
365 }
366
367 let glyph = glyph_id.with_scale_and_position(
368 PxScale::from(ss_icon_size as f32),
369 ab_glyph::point(0.0, 0.0),
370 );
371 let outlined = font.outline_glyph(glyph)?;
372 let bounds = outlined.px_bounds();
373 let width = bounds.width() as u32;
374 let height = bounds.height() as u32;
375 if width == 0 || height == 0 {
376 return None;
377 }
378
379 let width_usize = width as usize;
380 let mut alpha = vec![0u8; width_usize * height as usize];
381 outlined.draw(|gx, gy, coverage| {
382 let idx = gy as usize * width_usize + gx as usize;
383 alpha[idx] = (coverage * 255.0) as u8;
384 });
385
386 let mask = Arc::new(GlyphMask {
387 width,
388 height,
389 alpha,
390 });
391 let mut cache = self
392 .glyph_cache
393 .write()
394 .expect("glyph cache rwlock poisoned");
395 if cache.len() >= 2048 {
396 cache.clear();
397 }
398 let cached = cache.entry(key).or_insert(mask);
399 Some(Arc::clone(cached))
400 }
401
402 pub fn has_icon(&self, name: &str) -> bool {
404 self.css_parser.has_icon(name)
405 }
406
407 pub fn icon_count(&self) -> usize {
409 self.css_parser.icon_count()
410 }
411
412 pub fn list_icons(&self) -> Vec<&str> {
414 self.css_parser.list_icons()
415 }
416
417 #[inline]
433 pub fn render(&self, icon_name: &str, config: &RenderConfig) -> Result<(u32, u32, Vec<u8>)> {
434 let mapping = self
436 .css_parser
437 .get_icon(icon_name)
438 .ok_or_else(|| IconFontError::IconNotFound(icon_name.to_string()))?;
439
440 self.render_mapping(mapping, config)
441 }
442
443 pub fn render_with_style(
455 &self,
456 icon_name: &str,
457 style: FontStyle,
458 config: &RenderConfig,
459 ) -> Result<(u32, u32, Vec<u8>)> {
460 let mapping = self
461 .css_parser
462 .get_icon_with_style(icon_name, style)
463 .ok_or_else(|| IconFontError::IconNotFound(icon_name.to_string()))?;
464
465 self.render_mapping(&mapping, config)
466 }
467
468 #[inline]
470 fn render_mapping(
471 &self,
472 mapping: &IconMapping,
473 config: &RenderConfig,
474 ) -> Result<(u32, u32, Vec<u8>)> {
475 let ss_factor = config.supersample_factor.max(1);
476
477 let ss_canvas_width = config.canvas_width * ss_factor;
478 let ss_canvas_height = config.canvas_height * ss_factor;
479 let mut ss_icon_size = config.icon_size * ss_factor;
480 let mut glyph_mask =
481 match self.get_or_rasterized_glyph_mask(mapping.style, mapping.codepoint, ss_icon_size)
482 {
483 Some(mask) => mask,
484 None => return Ok(self.create_background_only(config)),
485 };
486
487 while glyph_mask.width > ss_canvas_width || glyph_mask.height > ss_canvas_height {
490 let Some(next_size) = scaled_icon_size_to_fit_canvas(
491 ss_icon_size,
492 glyph_mask.width,
493 glyph_mask.height,
494 ss_canvas_width,
495 ss_canvas_height,
496 ) else {
497 break;
498 };
499
500 ss_icon_size = next_size;
501 glyph_mask = match self.get_or_rasterized_glyph_mask(
502 mapping.style,
503 mapping.codepoint,
504 ss_icon_size,
505 ) {
506 Some(mask) => mask,
507 None => return Ok(self.create_background_only(config)),
508 };
509 }
510
511 let glyph_width = glyph_mask.width;
512 let glyph_height = glyph_mask.height;
513
514 if glyph_width == 0 || glyph_height == 0 {
515 return Ok(self.create_background_only(config));
516 }
517
518 let needs_rotation = config.rotate.abs() > 0.001;
519
520 if needs_rotation {
521 let mut ss_canvas = self.create_canvas(ss_canvas_width, ss_canvas_height, config);
522 let (rotated_alpha, rotated_width, rotated_height) =
523 self.render_and_rotate_glyph(&glyph_mask, config.rotate);
524
525 let (x_pos, y_pos) = self.calculate_position(
526 ss_canvas_width,
527 ss_canvas_height,
528 rotated_width,
529 rotated_height,
530 config,
531 ss_factor,
532 );
533
534 self.composite_alpha_buffer(
535 &mut ss_canvas,
536 ss_canvas_width,
537 &rotated_alpha,
538 rotated_width,
539 rotated_height,
540 x_pos,
541 y_pos,
542 &config.icon_color,
543 );
544
545 let final_pixels = match ss_factor {
546 1 => ss_canvas,
547 _ => self.downsample(&ss_canvas, ss_canvas_width, ss_canvas_height, ss_factor),
548 };
549 return Ok((config.canvas_width, config.canvas_height, final_pixels));
550 }
551
552 let (x_pos, y_pos) = self.calculate_position(
553 ss_canvas_width,
554 ss_canvas_height,
555 glyph_width,
556 glyph_height,
557 config,
558 ss_factor,
559 );
560
561 let bg = config.background_color.to_rgba();
565 let icon_a = config.icon_color.a;
566 let can_fuse = bg == [255, 255, 255, 255] && icon_a == 255 && ss_factor == 1;
569
570 let ss_canvas = if can_fuse {
571 self.create_canvas_with_glyph(
572 ss_canvas_width,
573 ss_canvas_height,
574 &glyph_mask,
575 x_pos,
576 y_pos,
577 &config.icon_color,
578 )
579 } else {
580 let mut canvas = self.create_canvas(ss_canvas_width, ss_canvas_height, config);
581 self.composite_glyph_mask(
582 &mut canvas,
583 ss_canvas_width,
584 &glyph_mask,
585 x_pos,
586 y_pos,
587 &config.icon_color,
588 );
589 canvas
590 };
591
592 let final_pixels = match ss_factor {
593 1 => ss_canvas,
594 _ => self.downsample(&ss_canvas, ss_canvas_width, ss_canvas_height, ss_factor),
595 };
596
597 Ok((config.canvas_width, config.canvas_height, final_pixels))
598 }
599
600 fn render_and_rotate_glyph(&self, glyph_mask: &GlyphMask, degrees: f64) -> (Vec<u8>, u32, u32) {
602 let glyph_width = glyph_mask.width;
603 let glyph_height = glyph_mask.height;
604 let glyph_width_usize = glyph_width as usize;
605
606 let radians = (degrees as f32).to_radians();
607 let cos_angle = radians.cos();
608 let sin_angle = radians.sin();
609
610 let w = glyph_width as f32;
611 let h = glyph_height as f32;
612 let half_w = w / 2.0;
613 let half_h = h / 2.0;
614
615 let new_width = (w * cos_angle.abs() + h * sin_angle.abs()).ceil() as u32;
616 let new_height = (w * sin_angle.abs() + h * cos_angle.abs()).ceil() as u32;
617 let new_width = new_width.max(1);
618 let new_height = new_height.max(1);
619 let new_width_usize = new_width as usize;
620
621 let new_half_w = new_width as f32 / 2.0;
622 let new_half_h = new_height as f32 / 2.0;
623
624 let mut rotated_alpha = vec![0u8; (new_width * new_height) as usize];
625 let max_src_x = w - 1.0;
626 let max_src_y = h - 1.0;
627
628 rotated_alpha
630 .par_chunks_mut(new_width_usize)
631 .enumerate()
632 .for_each(|(out_y, row)| {
633 let dy = out_y as f32 - new_half_h;
634 let mut src_x = (-new_half_w) * cos_angle + dy * sin_angle + half_w;
635 let mut src_y = new_half_w * sin_angle + dy * cos_angle + half_h;
636
637 for pixel in row.iter_mut().take(new_width_usize) {
638 if src_x >= 0.0 && src_x < max_src_x && src_y >= 0.0 && src_y < max_src_y {
639 *pixel = bilinear_sample_alpha(
640 &glyph_mask.alpha,
641 glyph_width_usize,
642 src_x,
643 src_y,
644 );
645 }
646 src_x += cos_angle;
647 src_y -= sin_angle;
648 }
649 });
650
651 (rotated_alpha, new_width, new_height)
652 }
653
654 #[allow(clippy::too_many_arguments)]
658 fn composite_alpha_buffer(
659 &self,
660 canvas: &mut [u8],
661 canvas_width: u32,
662 alpha: &[u8],
663 buffer_width: u32,
664 buffer_height: u32,
665 x_offset: i32,
666 y_offset: i32,
667 color: &Color,
668 ) {
669 let canvas_width_i32 = canvas_width as i32;
670 let canvas_height_i32 = (canvas.len() / (canvas_width as usize * 4)) as i32;
671 let buffer_width_i32 = buffer_width as i32;
672 let buffer_height_i32 = buffer_height as i32;
673
674 let dst_start_x = x_offset.max(0);
675 let dst_start_y = y_offset.max(0);
676 let dst_end_x = (x_offset + buffer_width_i32).min(canvas_width_i32);
677 let dst_end_y = (y_offset + buffer_height_i32).min(canvas_height_i32);
678
679 if dst_start_x >= dst_end_x || dst_start_y >= dst_end_y {
680 return;
681 }
682
683 let canvas_stride = canvas_width as usize * 4;
684 let buffer_stride = buffer_width as usize;
685 let color_a = color.a as u32;
686 let is_opaque = color_a == 255;
687 let color_r = color.r;
688 let color_g = color.g;
689 let color_b = color.b;
690 let bx_start = (dst_start_x - x_offset) as usize;
691 let span = (dst_end_x - dst_start_x) as usize;
692 let px_start = dst_start_x as usize * 4;
693 let px_end = px_start + span * 4;
694 let row_count = (dst_end_y - dst_start_y) as usize;
695 let first_row = dst_start_y as usize;
696 let y_off = y_offset as usize;
697
698 let canvas_region_start = first_row * canvas_stride;
699 let canvas_region_end = (first_row + row_count) * canvas_stride;
700 let canvas_region = &mut canvas[canvas_region_start..canvas_region_end];
701
702 let total_pixels = row_count * span;
703 if total_pixels >= PARALLEL_PIXEL_THRESHOLD {
704 canvas_region
705 .par_chunks_mut(canvas_stride)
706 .enumerate()
707 .for_each(|(row_idx, canvas_row_full)| {
708 let by = first_row + row_idx - y_off;
709 let buffer_row = by * buffer_stride;
710 let alpha_slice = &alpha[buffer_row + bx_start..buffer_row + bx_start + span];
711 let canvas_row = &mut canvas_row_full[px_start..px_end];
712 composite_alpha_row(
713 canvas_row,
714 alpha_slice,
715 color_r,
716 color_g,
717 color_b,
718 color_a,
719 is_opaque,
720 );
721 });
722 } else {
723 for (row_idx, canvas_row_full) in canvas_region.chunks_mut(canvas_stride).enumerate() {
724 let by = first_row + row_idx - y_off;
725 let buffer_row = by * buffer_stride;
726 let alpha_slice = &alpha[buffer_row + bx_start..buffer_row + bx_start + span];
727 let canvas_row = &mut canvas_row_full[px_start..px_end];
728 composite_alpha_row(
729 canvas_row,
730 alpha_slice,
731 color_r,
732 color_g,
733 color_b,
734 color_a,
735 is_opaque,
736 );
737 }
738 }
739 }
740
741 #[inline]
747 fn create_canvas_with_glyph(
748 &self,
749 width: u32,
750 height: u32,
751 glyph_mask: &GlyphMask,
752 x_offset: i32,
753 y_offset: i32,
754 color: &Color,
755 ) -> Vec<u8> {
756 let total_bytes = (width * height) as usize * 4;
757 let mut canvas = vec![255u8; total_bytes];
758 let canvas_stride = width as usize * 4;
759
760 let glyph_w = glyph_mask.width as i32;
761 let glyph_h = glyph_mask.height as i32;
762 let canvas_w = width as i32;
763 let canvas_h = height as i32;
764
765 let min_gx = (-x_offset).max(0) as usize;
766 let min_gy = (-y_offset).max(0) as usize;
767 let max_gx = (canvas_w - x_offset).min(glyph_w) as usize;
768 let max_gy = (canvas_h - y_offset).min(glyph_h) as usize;
769
770 if min_gx >= max_gx || min_gy >= max_gy {
771 return canvas;
772 }
773
774 let glyph_stride = glyph_mask.width as usize;
775 let x_off = x_offset as usize;
776 let color_r = color.r;
777 let color_g = color.g;
778 let color_b = color.b;
779 let row_count = max_gy - min_gy;
780 let span = max_gx - min_gx;
781 let total_glyph_pixels = row_count * span;
782 let first_canvas_row = (y_offset as usize) + min_gy;
783
784 let dr = color_r as i32 - 255;
788 let dg = color_g as i32 - 255;
789 let db = color_b as i32 - 255;
790
791 let canvas_region_start = first_canvas_row * canvas_stride;
792 let canvas_region_end = (first_canvas_row + row_count) * canvas_stride;
793 let canvas_region = &mut canvas[canvas_region_start..canvas_region_end];
794
795 let blend_row = |canvas_row_full: &mut [u8], gy: usize| {
796 let glyph_row_start = gy * glyph_stride + min_gx;
797 let alpha_row = &glyph_mask.alpha[glyph_row_start..glyph_row_start + span];
798 let px_start = (x_off + min_gx) * 4;
799 let canvas_row = &mut canvas_row_full[px_start..px_start + span * 4];
800
801 for (i, &coverage) in alpha_row.iter().enumerate() {
802 if coverage == 0 {
803 continue;
804 }
805 let dst = i * 4;
806 if coverage == 255 {
807 canvas_row[dst] = color_r;
808 canvas_row[dst + 1] = color_g;
809 canvas_row[dst + 2] = color_b;
810 } else {
811 let c = coverage as i32;
812 canvas_row[dst] = div255((c * dr + 65025) as u32) as u8;
813 canvas_row[dst + 1] = div255((c * dg + 65025) as u32) as u8;
814 canvas_row[dst + 2] = div255((c * db + 65025) as u32) as u8;
815 }
816 }
817 };
818
819 if total_glyph_pixels >= PARALLEL_PIXEL_THRESHOLD {
820 canvas_region
821 .par_chunks_mut(canvas_stride)
822 .enumerate()
823 .for_each(|(row_idx, canvas_row_full)| {
824 blend_row(canvas_row_full, min_gy + row_idx);
825 });
826 } else {
827 for (row_idx, canvas_row_full) in canvas_region.chunks_mut(canvas_stride).enumerate() {
828 blend_row(canvas_row_full, min_gy + row_idx);
829 }
830 }
831
832 canvas
833 }
834
835 fn create_canvas(&self, width: u32, height: u32, config: &RenderConfig) -> Vec<u8> {
837 let bg = config.background_color.to_rgba();
838 let total_bytes = (width * height) as usize * 4;
839
840 if bg == [0, 0, 0, 0] {
841 return vec![0u8; total_bytes];
842 }
843
844 if bg[0] == bg[1] && bg[1] == bg[2] && bg[2] == bg[3] {
846 return vec![bg[0]; total_bytes];
847 }
848
849 let mut canvas = vec![0u8; total_bytes];
852 canvas[..4].copy_from_slice(&bg);
853 let mut filled = 4usize;
854 while filled < total_bytes {
855 let copy_len = filled.min(total_bytes - filled);
856 let (src, dst) = canvas.split_at_mut(filled);
857 dst[..copy_len].copy_from_slice(&src[..copy_len]);
858 filled += copy_len;
859 }
860 canvas
861 }
862
863 fn create_background_only(&self, config: &RenderConfig) -> (u32, u32, Vec<u8>) {
865 let pixels = self.create_canvas(config.canvas_width, config.canvas_height, config);
866 (config.canvas_width, config.canvas_height, pixels)
867 }
868
869 #[inline]
871 fn calculate_position(
872 &self,
873 canvas_width: u32,
874 canvas_height: u32,
875 glyph_width: u32,
876 glyph_height: u32,
877 config: &RenderConfig,
878 ss_factor: u32,
879 ) -> (i32, i32) {
880 let ss_offset_x = config.offset_x * ss_factor as i32;
881 let ss_offset_y = config.offset_y * ss_factor as i32;
882
883 let x = match config.horizontal_anchor {
884 HorizontalAnchor::Left => ss_offset_x,
885 HorizontalAnchor::Center => {
886 (canvas_width as i32 - glyph_width as i32) / 2 + ss_offset_x
887 }
888 HorizontalAnchor::Right => canvas_width as i32 - glyph_width as i32 + ss_offset_x,
889 };
890
891 let y = match config.vertical_anchor {
892 VerticalAnchor::Top => ss_offset_y,
893 VerticalAnchor::Center => {
894 (canvas_height as i32 - glyph_height as i32) / 2 + ss_offset_y
895 }
896 VerticalAnchor::Bottom => canvas_height as i32 - glyph_height as i32 + ss_offset_y,
897 };
898
899 (x, y)
900 }
901
902 fn composite_glyph_mask(
907 &self,
908 canvas: &mut [u8],
909 canvas_width: u32,
910 glyph_mask: &GlyphMask,
911 x_offset: i32,
912 y_offset: i32,
913 color: &Color,
914 ) {
915 let glyph_width = glyph_mask.width as i32;
916 let glyph_height = glyph_mask.height as i32;
917 let canvas_height_i32 = (canvas.len() / (canvas_width as usize * 4)) as i32;
918 let canvas_width_i32 = canvas_width as i32;
919 let canvas_stride = canvas_width as usize * 4;
920
921 let min_gx = (-x_offset).max(0) as usize;
922 let min_gy = (-y_offset).max(0);
923 let max_gx = (canvas_width_i32 - x_offset).min(glyph_width) as usize;
924 let max_gy = (canvas_height_i32 - y_offset).min(glyph_height);
925
926 if min_gx >= max_gx || min_gy >= max_gy {
927 return;
928 }
929
930 let glyph_width_usize = glyph_mask.width as usize;
931 let color_a = color.a as u32;
932 let color_r = color.r;
933 let color_g = color.g;
934 let color_b = color.b;
935 let is_opaque_color = color_a == 255;
936 let x_off = x_offset as usize;
937 let row_count = (max_gy - min_gy) as usize;
938 let first_canvas_row = (y_offset + min_gy) as usize;
939
940 let canvas_region_start = first_canvas_row * canvas_stride;
942 let canvas_region_end = (first_canvas_row + row_count) * canvas_stride;
943 let canvas_region = &mut canvas[canvas_region_start..canvas_region_end];
944
945 let total_pixels = row_count * (max_gx - min_gx);
947 if total_pixels >= PARALLEL_PIXEL_THRESHOLD {
948 canvas_region
949 .par_chunks_mut(canvas_stride)
950 .enumerate()
951 .for_each(|(row_idx, canvas_row_full)| {
952 let gy = min_gy as usize + row_idx;
953 let glyph_row = gy * glyph_width_usize;
954 let alpha_row = &glyph_mask.alpha[glyph_row + min_gx..glyph_row + max_gx];
955 let px_start = (x_off + min_gx) * 4;
956 let px_end = (x_off + max_gx) * 4;
957 let canvas_row = &mut canvas_row_full[px_start..px_end];
958 composite_alpha_row(
959 canvas_row,
960 alpha_row,
961 color_r,
962 color_g,
963 color_b,
964 color_a,
965 is_opaque_color,
966 );
967 });
968 } else {
969 for (row_idx, canvas_row_full) in canvas_region.chunks_mut(canvas_stride).enumerate() {
970 let gy = min_gy as usize + row_idx;
971 let glyph_row = gy * glyph_width_usize;
972 let alpha_row = &glyph_mask.alpha[glyph_row + min_gx..glyph_row + max_gx];
973 let px_start = (x_off + min_gx) * 4;
974 let px_end = (x_off + max_gx) * 4;
975 let canvas_row = &mut canvas_row_full[px_start..px_end];
976 composite_alpha_row(
977 canvas_row,
978 alpha_row,
979 color_r,
980 color_g,
981 color_b,
982 color_a,
983 is_opaque_color,
984 );
985 }
986 }
987 }
988
989 fn downsample(&self, ss_pixels: &[u8], ss_width: u32, ss_height: u32, factor: u32) -> Vec<u8> {
991 let out_width = ss_width / factor;
992 let out_height = ss_height / factor;
993 let out_size = (out_width * out_height) as usize;
994
995 match factor {
997 2 => return self.downsample_2x(ss_pixels, ss_width, out_width, out_size),
998 1 => return ss_pixels.to_vec(),
999 _ => {}
1000 }
1001
1002 let factor_usize = factor as usize;
1004 let factor_sq = factor * factor;
1005 let out_row_bytes = out_width as usize * 4;
1006 let ss_row_bytes = ss_width as usize * 4;
1007 let mut result = vec![0u8; out_size * 4];
1008
1009 result
1010 .par_chunks_mut(out_row_bytes)
1011 .enumerate()
1012 .for_each(|(out_y, row)| {
1013 let base_y = out_y * factor_usize;
1014
1015 for out_x in 0..out_width as usize {
1016 let mut r_sum: u32 = 0;
1017 let mut g_sum: u32 = 0;
1018 let mut b_sum: u32 = 0;
1019 let mut a_sum: u32 = 0;
1020
1021 let base_x = out_x * factor_usize;
1022
1023 for dy in 0..factor_usize {
1024 let src_row = (base_y + dy) * ss_row_bytes + base_x * 4;
1025 for dx in 0..factor_usize {
1026 let idx = src_row + dx * 4;
1027 r_sum += ss_pixels[idx] as u32;
1028 g_sum += ss_pixels[idx + 1] as u32;
1029 b_sum += ss_pixels[idx + 2] as u32;
1030 a_sum += ss_pixels[idx + 3] as u32;
1031 }
1032 }
1033
1034 let out_idx = out_x * 4;
1035 row[out_idx] = (r_sum / factor_sq) as u8;
1036 row[out_idx + 1] = (g_sum / factor_sq) as u8;
1037 row[out_idx + 2] = (b_sum / factor_sq) as u8;
1038 row[out_idx + 3] = (a_sum / factor_sq) as u8;
1039 }
1040 });
1041
1042 result
1043 }
1044
1045 fn downsample_2x(
1047 &self,
1048 ss_pixels: &[u8],
1049 ss_width: u32,
1050 out_width: u32,
1051 out_size: usize,
1052 ) -> Vec<u8> {
1053 let out_row_bytes = out_width as usize * 4;
1054 let ss_row_bytes = ss_width as usize * 4;
1055 let mut result = vec![0u8; out_size * 4];
1056
1057 result
1058 .par_chunks_mut(out_row_bytes)
1059 .enumerate()
1060 .for_each(|(out_y, row)| {
1061 let row0_start = out_y * 2 * ss_row_bytes;
1062 let row1_start = row0_start + ss_row_bytes;
1063 let row0 = &ss_pixels[row0_start..row0_start + ss_row_bytes];
1064 let row1 = &ss_pixels[row1_start..row1_start + ss_row_bytes];
1065
1066 let out_width_usize = out_width as usize;
1067 let simd_width = out_width_usize & !3;
1068 let mut out_x = 0usize;
1069
1070 while out_x < simd_width {
1071 let mut base = out_x * 8;
1072 for _ in 0..4 {
1073 let dst = out_x * 4;
1074 row[dst] = ((row0[base] as u16
1075 + row0[base + 4] as u16
1076 + row1[base] as u16
1077 + row1[base + 4] as u16)
1078 >> 2) as u8;
1079 row[dst + 1] = ((row0[base + 1] as u16
1080 + row0[base + 5] as u16
1081 + row1[base + 1] as u16
1082 + row1[base + 5] as u16)
1083 >> 2) as u8;
1084 row[dst + 2] = ((row0[base + 2] as u16
1085 + row0[base + 6] as u16
1086 + row1[base + 2] as u16
1087 + row1[base + 6] as u16)
1088 >> 2) as u8;
1089 row[dst + 3] = ((row0[base + 3] as u16
1090 + row0[base + 7] as u16
1091 + row1[base + 3] as u16
1092 + row1[base + 7] as u16)
1093 >> 2) as u8;
1094 out_x += 1;
1095 base += 8;
1096 }
1097 }
1098
1099 while out_x < out_width_usize {
1100 let base = out_x * 8;
1101 let dst = out_x * 4;
1102 row[dst] = ((row0[base] as u16
1103 + row0[base + 4] as u16
1104 + row1[base] as u16
1105 + row1[base + 4] as u16)
1106 >> 2) as u8;
1107 row[dst + 1] = ((row0[base + 1] as u16
1108 + row0[base + 5] as u16
1109 + row1[base + 1] as u16
1110 + row1[base + 5] as u16)
1111 >> 2) as u8;
1112 row[dst + 2] = ((row0[base + 2] as u16
1113 + row0[base + 6] as u16
1114 + row1[base + 2] as u16
1115 + row1[base + 6] as u16)
1116 >> 2) as u8;
1117 row[dst + 3] = ((row0[base + 3] as u16
1118 + row0[base + 7] as u16
1119 + row1[base + 3] as u16
1120 + row1[base + 7] as u16)
1121 >> 2) as u8;
1122 out_x += 1;
1123 }
1124 });
1125
1126 result
1127 }
1128}
1129
1130#[inline]
1135fn composite_alpha_row(
1136 canvas_row: &mut [u8],
1137 alpha_row: &[u8],
1138 color_r: u8,
1139 color_g: u8,
1140 color_b: u8,
1141 color_a: u32,
1142 is_opaque_color: bool,
1143) {
1144 if is_opaque_color {
1145 for (i, &coverage) in alpha_row.iter().enumerate() {
1146 if coverage == 0 {
1147 continue;
1148 }
1149 let dst_idx = i * 4;
1150 blend_rgba_over(
1151 canvas_row,
1152 dst_idx,
1153 color_r,
1154 color_g,
1155 color_b,
1156 coverage as u32,
1157 );
1158 }
1159 } else {
1160 for (i, &coverage) in alpha_row.iter().enumerate() {
1161 if coverage == 0 {
1162 continue;
1163 }
1164 let src_alpha = div255(coverage as u32 * color_a);
1165 if src_alpha == 0 {
1166 continue;
1167 }
1168 let dst_idx = i * 4;
1169 blend_rgba_over(canvas_row, dst_idx, color_r, color_g, color_b, src_alpha);
1170 }
1171 }
1172}
1173
1174#[inline]
1178fn scaled_icon_size_to_fit_canvas(
1179 ss_icon_size: u32,
1180 glyph_width: u32,
1181 glyph_height: u32,
1182 canvas_width: u32,
1183 canvas_height: u32,
1184) -> Option<u32> {
1185 if glyph_width <= canvas_width && glyph_height <= canvas_height {
1186 return None;
1187 }
1188
1189 let fit_x = canvas_width as f64 / glyph_width as f64;
1190 let fit_y = canvas_height as f64 / glyph_height as f64;
1191 let fit_scale = fit_x.min(fit_y) * 0.95;
1192 let scaled = ((ss_icon_size as f64) * fit_scale).floor().max(1.0) as u32;
1193
1194 if scaled < ss_icon_size {
1195 Some(scaled)
1196 } else {
1197 None
1198 }
1199}
1200
1201#[inline(always)]
1203fn div255(x: u32) -> u32 {
1204 (x + 128 + ((x + 128) >> 8)) >> 8
1205}
1206
1207#[inline(always)]
1209fn blend_rgba_over(canvas: &mut [u8], dst_idx: usize, src_r: u8, src_g: u8, src_b: u8, src_a: u32) {
1210 if src_a == 255 {
1211 canvas[dst_idx] = src_r;
1212 canvas[dst_idx + 1] = src_g;
1213 canvas[dst_idx + 2] = src_b;
1214 canvas[dst_idx + 3] = 255;
1215 return;
1216 }
1217 let inv_src_alpha = 255 - src_a;
1218 canvas[dst_idx] = div255(src_r as u32 * src_a + canvas[dst_idx] as u32 * inv_src_alpha) as u8;
1219 canvas[dst_idx + 1] =
1220 div255(src_g as u32 * src_a + canvas[dst_idx + 1] as u32 * inv_src_alpha) as u8;
1221 canvas[dst_idx + 2] =
1222 div255(src_b as u32 * src_a + canvas[dst_idx + 2] as u32 * inv_src_alpha) as u8;
1223 canvas[dst_idx + 3] = (src_a + div255(canvas[dst_idx + 3] as u32 * inv_src_alpha)) as u8;
1224}
1225
1226#[inline(always)]
1230fn bilinear_sample_alpha(buffer: &[u8], width: usize, x: f32, y: f32) -> u8 {
1231 let x0 = x as usize;
1232 let y0 = y as usize;
1233
1234 let fx = ((x - x0 as f32) * 256.0) as u32;
1236 let fy = ((y - y0 as f32) * 256.0) as u32;
1237 let inv_fx = 256 - fx;
1238 let inv_fy = 256 - fy;
1239
1240 let row0 = y0 * width + x0;
1241 let row1 = row0 + width;
1242
1243 let val = inv_fy * (inv_fx * buffer[row0] as u32 + fx * buffer[row0 + 1] as u32)
1245 + fy * (inv_fx * buffer[row1] as u32 + fx * buffer[row1 + 1] as u32);
1246
1247 ((val + 32768) >> 16) as u8
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252 use super::*;
1253
1254 #[test]
1255 fn test_render_config_builder() {
1256 let config = RenderConfig::new()
1257 .canvas_size(512, 512)
1258 .icon_size(256)
1259 .supersample(4)
1260 .icon_color(Color::rgb(255, 0, 0))
1261 .background_color(Color::transparent())
1262 .anchor(HorizontalAnchor::Left, VerticalAnchor::Top)
1263 .offset(10, 20)
1264 .rotate(45.0);
1265
1266 assert_eq!(config.canvas_width, 512);
1267 assert_eq!(config.canvas_height, 512);
1268 assert_eq!(config.icon_size, 256);
1269 assert_eq!(config.supersample_factor, 4);
1270 assert_eq!(config.icon_color, Color::rgb(255, 0, 0));
1271 assert!(config.background_color.is_transparent());
1272 assert_eq!(config.horizontal_anchor, HorizontalAnchor::Left);
1273 assert_eq!(config.vertical_anchor, VerticalAnchor::Top);
1274 assert_eq!(config.offset_x, 10);
1275 assert_eq!(config.offset_y, 20);
1276 assert!((config.rotate - 45.0).abs() < 0.001);
1277 }
1278
1279 #[test]
1280 fn test_render_config_rotate_default() {
1281 let config = RenderConfig::default();
1282 assert!((config.rotate - 0.0).abs() < 0.001);
1283 }
1284
1285 #[test]
1286 fn test_render_config_rotate_negative() {
1287 let config = RenderConfig::new().rotate(-90.0);
1288 assert!((config.rotate - (-90.0)).abs() < 0.001);
1289 }
1290
1291 #[test]
1292 fn test_scaled_icon_size_to_fit_canvas_no_change_when_fit() {
1293 let scaled = scaled_icon_size_to_fit_canvas(1024, 800, 700, 1024, 1024);
1294 assert_eq!(scaled, None);
1295 }
1296
1297 #[test]
1298 fn test_scaled_icon_size_to_fit_canvas_downscales_oversized_glyph() {
1299 let scaled = scaled_icon_size_to_fit_canvas(1000, 1200, 800, 1000, 1000);
1300 let scaled_value = scaled.expect("oversized glyph should trigger downscaling");
1301 assert!(scaled_value < 1000);
1302 }
1303}