1use std::cell::OnceCell;
9use std::cmp::Ordering;
10use std::collections::{BTreeMap, BTreeSet, HashMap};
11use std::fmt;
12use std::ops::Range;
13use std::sync::Arc;
14
15use ab_glyph_rasterizer::{Point, Rasterizer, point};
16use alpha2_media::{
17 EmojiPolicy, HostTexture, HostTexturePixels, Premultiplication, RasterColorSpace, TextAlign,
18 TextTexture,
19};
20use rustybuzz::ttf_parser::{self, GlyphId, OutlineBuilder};
21use rustybuzz::{Direction, UnicodeBuffer};
22use unicode_bidi::ParagraphBidiInfo;
23use unicode_linebreak::BreakOpportunity;
24use unicode_script::{Script, UnicodeScript};
25use unicode_segmentation::UnicodeSegmentation;
26
27mod animated;
28mod color;
29mod gradient;
30mod unicode_tables;
31
32pub use animated::{ClusterInfo, ClusterState, ColorMix, LineInfo, TextLayout};
33pub use gradient::{TextGradient, TextGradientKind, TextGradientStop, TextSpread};
34
35const MAX_TEXT_BYTES: usize = 64 * 1024;
36const MAX_FONT_SIZE: f32 = 2_048.0;
37const MAX_LINE_HEIGHT: f32 = 10.0;
38const MAX_STROKE_EXTENT: f32 = 64.0;
40const MAX_STROKES: usize = 4;
41const MAX_BACKGROUND_PADDING: f32 = 256.0;
42const LETTER_SPACING_RANGE: std::ops::RangeInclusive<f32> = -0.5..=1.0;
44
45#[derive(Clone, Debug)]
47pub struct TextFont {
48 pub family: String,
50 pub bytes: Arc<[u8]>,
52 pub face_index: u32,
54}
55
56#[derive(Clone, Debug, PartialEq)]
58pub struct TextRasterRequest {
59 pub text: String,
61 pub font_size: f32,
63 pub max_width: f32,
65 pub line_height: f32,
68 pub align: TextAlign,
70 pub fill: TextFill,
73 pub emoji_policy: EmojiPolicy,
75 pub content_version: u64,
77 pub strokes: Vec<TextStroke>,
80 pub background: Option<TextBackground>,
82 pub split_background: bool,
85 pub font_family: Option<String>,
89 pub letter_spacing: f32,
93 pub font_weight: u16,
96}
97
98#[derive(Clone, Debug, PartialEq)]
100pub enum TextFill {
101 Solid([u8; 4]),
103 Gradient(TextGradient),
105}
106
107impl TextFill {
108 #[must_use]
110 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
111 pub fn foreground(&self) -> [u8; 4] {
112 match self {
113 Self::Solid(color) => *color,
114 Self::Gradient(gradient) => gradient.stops.first().map_or([0; 4], |stop| {
115 stop.color
116 .map(|value| (value.clamp(0.0, 1.0) * 255.0).round() as u8)
117 }),
118 }
119 }
120
121 fn is_valid(&self) -> bool {
122 match self {
123 Self::Solid(_) => true,
124 Self::Gradient(gradient) => gradient.is_valid(),
125 }
126 }
127}
128
129#[derive(Copy, Clone, Debug, PartialEq)]
134pub struct TextStroke {
135 pub color: [u8; 4],
137 pub width: f32,
139}
140
141#[derive(Copy, Clone, Debug, PartialEq)]
144pub struct TextBackground {
145 pub color: [u8; 4],
147 pub padding: f32,
149}
150
151#[derive(Clone, Debug, PartialEq)]
153pub struct TextRasterResult {
154 pub descriptor: TextTexture,
156 pub pixels: HostTexturePixels,
158 pub background: Option<Vec<u8>>,
161}
162
163#[derive(Clone, Debug, PartialEq)]
167pub struct TextMeasure {
168 pub box_width: u32,
170 pub box_height: u32,
172 pub decoration_margin: u32,
174 pub content: [f32; 4],
177 pub ink: Option<[f32; 4]>,
180 pub line_count: u32,
182 pub first_baseline: f32,
184 pub last_baseline: f32,
186}
187
188#[derive(Clone, Debug)]
191pub struct ShapedText {
192 paragraphs: Vec<Vec<Cluster>>,
193 font_size: f32,
194 line_height: f32,
195 line_advance: f32,
196}
197
198#[derive(Copy, Clone, Debug, PartialEq)]
200pub struct LineMetrics {
201 pub line_count: u32,
203 pub widest: f32,
205 pub height: f32,
207 pub first_baseline: f32,
209 pub last_baseline: f32,
211}
212
213impl ShapedText {
214 #[must_use]
216 pub fn min_content_width(&self) -> f32 {
217 let mut widest = 0.0_f32;
218 for clusters in &self.paragraphs {
219 let mut start = 0;
220 for index in 1..=clusters.len() {
221 if index == clusters.len() || clusters[index].break_before {
222 widest = widest.max(advance(&clusters[start..index]));
223 start = index;
224 }
225 }
226 }
227 widest
228 }
229
230 #[must_use]
232 pub fn max_content_width(&self) -> f32 {
233 self.paragraphs
234 .iter()
235 .map(|clusters| advance(clusters))
236 .fold(0.0, f32::max)
237 }
238
239 #[must_use]
241 pub fn lines_at(&self, max_width: f32) -> LineMetrics {
242 let (mut count, mut kept, mut widest) = (0_u32, 1_u32, 0.0_f32);
243 for clusters in &self.paragraphs {
244 for range in break_lines(clusters, max_width) {
245 count = count.saturating_add(1);
246 if !range.is_empty() {
247 kept = count;
248 }
249 widest = widest.max(advance(&clusters[range]));
250 }
251 }
252 let [first_baseline, last_baseline] =
253 baselines(kept, self.font_size, self.line_height, self.line_advance);
254 LineMetrics {
255 line_count: kept,
256 widest,
257 height: box_height(kept, self.line_advance, self.font_size),
258 first_baseline,
259 last_baseline,
260 }
261 }
262}
263
264fn advance(clusters: &[Cluster]) -> f32 {
267 let Some(last) = clusters.iter().rposition(|cluster| !cluster.hangs) else {
268 return 0.0;
269 };
270 clusters[..=last]
271 .iter()
272 .fold(0.0, |width, cluster| width + cluster.width)
273 - clusters[last].spacing
274}
275
276#[must_use]
278pub fn half_leading(font_size: f32, line_height: f32) -> f32 {
279 (font_size * line_height - font_size).max(0.0) * 0.5
280}
281
282#[allow(clippy::cast_precision_loss)]
284fn baselines(line_count: u32, font_size: f32, line_height: f32, line_advance: f32) -> [f32; 2] {
285 let (leading, ascent) = (half_leading(font_size, line_height), font_size * 0.8);
286 let last = line_count.saturating_sub(1) as f32;
287 [leading + ascent, last * line_advance + leading + ascent]
288}
289
290#[allow(clippy::cast_precision_loss)]
292fn box_height(line_count: u32, line_advance: f32, font_size: f32) -> f32 {
293 line_count.saturating_sub(1) as f32 * line_advance + line_advance.max(font_size)
294}
295
296#[derive(Clone, Debug, Eq, PartialEq)]
298#[non_exhaustive]
299pub enum TextRasterError {
300 NoFonts,
302 InvalidFont(String),
304 InvalidRequest,
306 TooLarge,
308}
309
310impl fmt::Display for TextRasterError {
311 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
312 match self {
313 Self::NoFonts => formatter.write_str("text rasterizer requires at least one font"),
314 Self::InvalidFont(error) => write!(formatter, "invalid text font: {error}"),
315 Self::InvalidRequest => formatter.write_str("invalid text raster request"),
316 Self::TooLarge => formatter.write_str("text raster result is too large"),
317 }
318 }
319}
320
321impl std::error::Error for TextRasterError {}
322
323#[derive(Clone, Debug)]
324struct LoadedFont {
325 source: TextFont,
326 family: Option<String>,
328 aliases: Vec<String>,
330 weight: u16,
332 wght: Option<(f32, f32, f32)>,
334}
335
336const WGHT: ttf_parser::Tag = ttf_parser::Tag::from_bytes(b"wght");
337
338impl LoadedFont {
339 fn parse(source: TextFont) -> Result<Self, TextRasterError> {
341 let face = ttf_parser::Face::parse(&source.bytes, source.face_index)
342 .map_err(|error| TextRasterError::InvalidFont(error.to_string()))?;
343 let weight = face.weight().to_number().clamp(1, 1000);
344 let wght = face
345 .variation_axes()
346 .into_iter()
347 .find(|axis| axis.tag == WGHT)
348 .map(|axis| (axis.min_value, axis.def_value, axis.max_value));
349 let family = font_family(&source.bytes, source.face_index).map(|name| fold_family(&name));
350 let mut aliases = font_family_names(&source.bytes, source.face_index)
351 .iter()
352 .map(|name| fold_family(name))
353 .collect::<Vec<_>>();
354 aliases.extend(family.clone());
355 aliases.sort();
356 aliases.dedup();
357 Ok(Self {
358 source,
359 family,
360 aliases,
361 weight,
362 wght,
363 })
364 }
365
366 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
368 fn weights(&self) -> (u16, u16) {
369 self.wght
370 .map_or((self.weight, self.weight), |(min, _, max)| {
371 let whole = |value: f32| value.clamp(1.0, 1000.0) as u16;
372 (whole(min.ceil()), whole(max.floor()).max(whole(min.ceil())))
373 })
374 }
375}
376
377fn weight_order(weights: &[(u16, u16)], desired: u16) -> Vec<usize> {
380 let key = |&(min, max): &(u16, u16)| -> (u8, u16) {
381 if (min..=max).contains(&desired) {
382 return (0, 0);
383 }
384 let weight = if desired < min { min } else { max };
385 let (above, distance) = (weight > desired, weight.abs_diff(desired));
386 if (400..=500).contains(&desired) {
387 match (above, weight <= 500) {
388 (true, true) => (1, distance),
389 (false, _) => (2, distance),
390 (true, false) => (3, distance),
391 }
392 } else if (desired < 400) != above {
393 (1, distance)
394 } else {
395 (2, distance)
396 }
397 };
398 let mut order = (0..weights.len()).collect::<Vec<_>>();
399 order.sort_by_key(|&index| key(&weights[index]));
400 order
401}
402
403pub fn check_font(bytes: &[u8], face_index: u32) -> Result<(), TextRasterError> {
409 ttf_parser::Face::parse(bytes, face_index)
410 .map(|_| ())
411 .map_err(|error| TextRasterError::InvalidFont(error.to_string()))
412}
413
414#[must_use]
416pub fn face_count(bytes: &[u8]) -> u32 {
417 ttf_parser::fonts_in_collection(bytes).unwrap_or(1)
418}
419
420#[must_use]
423pub fn font_family(bytes: &[u8], face_index: u32) -> Option<String> {
424 use rustybuzz::ttf_parser::{Language, name_id};
425 let face = rustybuzz::ttf_parser::Face::parse(bytes, face_index).ok()?;
426 let names = face.names();
427 [name_id::TYPOGRAPHIC_FAMILY, name_id::FAMILY]
428 .into_iter()
429 .find_map(|id| {
430 let decoded = || {
431 names
432 .into_iter()
433 .filter(move |name| name.name_id == id)
434 .filter_map(|name| {
435 let text = name.to_string()?;
436 (!text.trim().is_empty()).then_some((name.language(), text))
437 })
438 };
439 decoded()
440 .find(|(language, _)| *language == Language::English_UnitedStates)
441 .or_else(|| decoded().next())
442 .map(|(_, text)| text)
443 })
444}
445
446#[must_use]
449pub fn font_family_names(bytes: &[u8], face_index: u32) -> Vec<String> {
450 use rustybuzz::ttf_parser::name_id;
451 let Ok(face) = rustybuzz::ttf_parser::Face::parse(bytes, face_index) else {
452 return Vec::new();
453 };
454 let mut names = Vec::new();
455 for name in face.names() {
456 if name.name_id != name_id::TYPOGRAPHIC_FAMILY && name.name_id != name_id::FAMILY {
457 continue;
458 }
459 if let Some(text) = name.to_string().map(|text| text.trim().to_owned())
460 && !text.is_empty()
461 && !names.contains(&text)
462 {
463 names.push(text);
464 }
465 }
466 names
467}
468
469#[must_use]
471pub fn fold_family(name: &str) -> String {
472 use unicode_normalization::UnicodeNormalization;
473 name.trim().nfc().collect::<String>().to_lowercase()
474}
475
476pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
478
479const CACHE_ENTRY_OVERHEAD: usize = 256;
481
482#[derive(Debug)]
487pub struct TextRasterizer {
488 fonts: Vec<LoadedFont>,
489 cache: ResultCache,
490 tiles: animated::TileCache,
491 unmatched: std::sync::Mutex<BTreeSet<String>>,
492}
493
494#[derive(Debug)]
495struct ResultCache {
496 entries: HashMap<u64, CacheEntry>,
497 by_use: BTreeMap<u64, u64>,
499 next_use: u64,
500 bytes: usize,
501 budget: usize,
502}
503
504#[derive(Debug)]
505struct CacheEntry {
506 result: Arc<TextRasterResult>,
507 last_use: u64,
508 bytes: usize,
509}
510
511impl ResultCache {
512 fn new(budget: usize) -> Self {
513 Self {
514 entries: HashMap::new(),
515 by_use: BTreeMap::new(),
516 next_use: 0,
517 bytes: 0,
518 budget,
519 }
520 }
521
522 fn get(&mut self, version: u64) -> Option<Arc<TextRasterResult>> {
523 let entry = self.entries.get_mut(&version)?;
524 self.by_use.remove(&entry.last_use);
525 entry.last_use = self.next_use;
526 self.by_use.insert(self.next_use, version);
527 self.next_use += 1;
528 Some(Arc::clone(&entry.result))
529 }
530
531 fn insert(&mut self, version: u64, result: &Arc<TextRasterResult>) {
532 self.remove(version);
533 let bytes = result
534 .pixels
535 .rgba8
536 .len()
537 .saturating_add(CACHE_ENTRY_OVERHEAD);
538 if bytes > self.budget {
539 return;
540 }
541 self.bytes += bytes;
542 self.entries.insert(
543 version,
544 CacheEntry {
545 result: Arc::clone(result),
546 last_use: self.next_use,
547 bytes,
548 },
549 );
550 self.by_use.insert(self.next_use, version);
551 self.next_use += 1;
552 self.evict();
553 }
554
555 fn remove(&mut self, version: u64) {
556 if let Some(entry) = self.entries.remove(&version) {
557 self.by_use.remove(&entry.last_use);
558 self.bytes -= entry.bytes;
559 }
560 }
561
562 fn clear(&mut self) {
563 self.entries.clear();
564 self.by_use.clear();
565 self.bytes = 0;
566 }
567
568 fn set_budget(&mut self, budget: usize) {
569 self.budget = budget;
570 self.evict();
571 }
572
573 fn evict(&mut self) {
574 while self.bytes > self.budget {
575 let Some((_, version)) = self.by_use.pop_first() else {
576 break;
577 };
578 if let Some(entry) = self.entries.remove(&version) {
579 self.bytes -= entry.bytes;
580 }
581 }
582 }
583}
584
585impl TextRasterizer {
586 #[must_use]
588 pub fn empty() -> Self {
589 Self {
590 fonts: Vec::new(),
591 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
592 tiles: animated::TileCache::default(),
593 unmatched: std::sync::Mutex::default(),
594 }
595 }
596
597 pub fn new(fonts: Vec<TextFont>) -> Result<Self, TextRasterError> {
602 if fonts.is_empty() {
603 return Err(TextRasterError::NoFonts);
604 }
605 let loaded = fonts
606 .into_iter()
607 .map(LoadedFont::parse)
608 .collect::<Result<Vec<_>, _>>()?;
609 Ok(Self {
610 fonts: loaded,
611 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
612 tiles: animated::TileCache::default(),
613 unmatched: std::sync::Mutex::default(),
614 })
615 }
616
617 pub fn add_font(&mut self, source: TextFont) -> Result<(), TextRasterError> {
624 self.fonts.push(LoadedFont::parse(source)?);
625 self.cache.clear();
626 self.tiles.clear();
627 Ok(())
628 }
629
630 #[must_use]
632 pub fn font_count(&self) -> usize {
633 self.fonts.len()
634 }
635
636 #[must_use]
639 pub fn unmatched_families(&self) -> Vec<String> {
640 self.unmatched
641 .lock()
642 .unwrap_or_else(std::sync::PoisonError::into_inner)
643 .iter()
644 .cloned()
645 .collect()
646 }
647
648 pub fn rasterize(
653 &mut self,
654 request: &TextRasterRequest,
655 ) -> Result<Arc<TextRasterResult>, TextRasterError> {
656 validate_request(request)?;
657 if self.fonts.is_empty() {
658 return Err(TextRasterError::NoFonts);
659 }
660 if let Some(cached) = self.cache.get(request.content_version) {
661 return Ok(cached);
662 }
663 let result = Arc::new(self.rasterize_uncached(request)?);
664 self.cache.insert(request.content_version, &result);
665 Ok(result)
666 }
667
668 pub fn invalidate(&mut self, content_version: u64) {
670 self.cache.remove(content_version);
671 }
672
673 pub fn set_cache_budget(&mut self, bytes: usize) {
676 self.cache.set_budget(bytes);
677 }
678
679 #[must_use]
681 pub fn cache_bytes(&self) -> usize {
682 self.cache.bytes
683 }
684
685 pub fn clear(&mut self) {
687 self.cache.clear();
688 self.tiles.clear();
689 }
690
691 #[must_use]
693 pub fn cache_len(&self) -> usize {
694 self.cache.entries.len()
695 }
696
697 #[allow(clippy::cast_precision_loss)]
702 pub fn measure(&self, request: &TextRasterRequest) -> Result<TextMeasure, TextRasterError> {
703 validate_request(request)?;
704 if self.fonts.is_empty() {
705 return Err(TextRasterError::NoFonts);
706 }
707 let faces = Faces::new(&self.fonts, request.font_weight)?;
708 let layout = self.lay_out(&faces, request)?;
709 let mut ink = None;
710 layout.each_glyph(
711 &faces,
712 request,
713 &mut HashMap::new(),
714 |drawing, origin, scale, _| {
715 if let Some([x_min, y_min, x_max, y_max]) = drawing.ink_bounds() {
716 include(
717 &mut ink,
718 [
719 origin[0] + x_min * scale,
720 origin[1] - y_max * scale,
721 origin[0] + x_max * scale,
722 origin[1] - y_min * scale,
723 ],
724 );
725 }
726 },
727 );
728 let band = stroke_radius(&request.strokes).map_or(0.0, |radius| radius + 1.0);
730 ink = ink.map(|bounds| grown(bounds, band));
731 let padding = request.background.map(|background| background.padding);
732 let mut content = None;
733 for (index, line) in layout.lines.iter().enumerate() {
734 let left = layout.line_offset(line, request.align);
735 let top = layout.line_top(index);
736 include(&mut content, [left, 0.0, left + line.width, layout.height]);
737 if let (Some(padding), true) = (padding, line.width > 0.0) {
738 include(
739 &mut ink,
740 grown(
741 [left, top, left + line.width, top + request.font_size],
742 padding,
743 ),
744 );
745 }
746 }
747 let content = content.unwrap_or([0.0, 0.0, 0.0, layout.height]);
748 let last = layout.lines.len().saturating_sub(1);
749 Ok(TextMeasure {
750 first_baseline: layout.line_top(0) + layout.ascent,
751 last_baseline: layout.line_top(last) + layout.ascent,
752 box_width: layout.box_width,
753 box_height: layout.box_height,
754 decoration_margin: request.decoration_margin(),
755 content: grown(content, request.decoration_margin() as f32),
756 ink,
757 line_count: layout.line_count,
758 })
759 }
760
761 pub fn shape(&self, request: &TextRasterRequest) -> Result<ShapedText, TextRasterError> {
768 validate_request(&TextRasterRequest {
769 text: String::new(),
770 max_width: 0.0,
771 content_version: 1,
772 fill: TextFill::Solid([0; 4]),
773 strokes: Vec::new(),
774 background: None,
775 ..request.clone()
776 })?;
777 if request.text.len() > MAX_TEXT_BYTES {
778 return Err(TextRasterError::InvalidRequest);
779 }
780 if self.fonts.is_empty() {
781 return Err(TextRasterError::NoFonts);
782 }
783 let faces = Faces::new(&self.fonts, request.font_weight)?;
784 let order = self.font_order(request.font_family.as_deref(), request.font_weight);
785 let paragraphs = normalize(&request.text, request.emoji_policy)
786 .split('\n')
787 .map(|paragraph| {
788 let bidi = ParagraphBidiInfo::new(paragraph, None);
789 let glyphs = shape_paragraph(
790 &faces,
791 paragraph,
792 &bidi,
793 request.font_size,
794 &order,
795 request.emoji_policy,
796 request.letter_spacing,
797 );
798 clusters(&glyphs, paragraph, request.letter_spacing)
799 })
800 .collect();
801 Ok(ShapedText {
802 paragraphs,
803 font_size: request.font_size,
804 line_height: request.line_height,
805 line_advance: request.font_size * request.line_height,
806 })
807 }
808
809 #[allow(clippy::cast_precision_loss)]
810 fn lay_out(
811 &self,
812 faces: &Faces<'_>,
813 request: &TextRasterRequest,
814 ) -> Result<Layout, TextRasterError> {
815 let line_advance = request.font_size * request.line_height;
816 let order = self.font_order(request.font_family.as_deref(), request.font_weight);
817 let mut lines = Vec::new();
818 let mut kept = 1;
820 let mut clusters = Vec::new();
821 let mut paragraph_start = 0;
822 for paragraph in normalize(&request.text, request.emoji_policy).split('\n') {
823 let first = clusters.len();
824 for line in layout_paragraph(
825 faces,
826 paragraph,
827 request.font_size,
828 request.max_width,
829 &order,
830 request.emoji_policy,
831 request.letter_spacing,
832 (first, lines.len()),
833 &mut clusters,
834 ) {
835 lines.push(line);
836 if lines.last().is_some_and(|line| line.has_glyphs) {
837 kept = lines.len();
838 }
839 }
840 for span in &mut clusters[first..] {
841 span.bytes = span.bytes.start + paragraph_start..span.bytes.end + paragraph_start;
842 }
843 paragraph_start += paragraph.len() + 1;
844 }
845 lines.truncate(kept);
846 let line_count = u32::try_from(lines.len()).map_err(|_| TextRasterError::TooLarge)?;
847 let width = if request.max_width > 0.0 {
848 request.max_width
849 } else {
850 lines.iter().map(|line| line.width).fold(0.0, f32::max)
851 }
852 .max(1.0);
853 let height = box_height(line_count, line_advance, request.font_size).max(1.0);
854 Ok(Layout {
855 box_width: checked_dimension(width)?,
856 box_height: checked_dimension(height)?,
857 lines,
858 clusters,
859 line_count,
860 line_advance,
861 half_leading: half_leading(request.font_size, request.line_height),
862 ascent: request.font_size * 0.8,
863 descent: request.font_size * 0.2,
864 width,
865 height,
866 })
867 }
868
869 #[allow(
870 clippy::cast_possible_truncation,
871 clippy::cast_precision_loss,
872 clippy::too_many_lines
873 )]
874 fn rasterize_uncached(
875 &self,
876 request: &TextRasterRequest,
877 ) -> Result<TextRasterResult, TextRasterError> {
878 let faces = Faces::new(&self.fonts, request.font_weight)?;
879 let layout = self.lay_out(&faces, request)?;
880 let (box_width, box_height) = (layout.box_width, layout.box_height);
881 let mut drawings = HashMap::<(usize, u16), Drawing>::new();
882 let mut placed = Vec::new();
883 let (mut overflow_x, mut overflow_y) = (0_i64, 0_i64);
884 let (mut ink_ascent, mut ink_descent) = (layout.ascent, layout.descent);
885 layout.each_glyph(
886 &faces,
887 request,
888 &mut drawings,
889 |drawing, origin, scale, glyph| {
890 let Some(bounds) = drawing.bounds() else {
891 return;
892 };
893 let pixels = PixelBox::of(bounds, origin, scale);
894 overflow_x = overflow_x
895 .max(pixels.left.saturating_neg())
896 .max(pixels.right.saturating_sub(i64::from(box_width)));
897 overflow_y = overflow_y
898 .max(pixels.top.saturating_neg())
899 .max(pixels.bottom.saturating_sub(i64::from(box_height)));
900 ink_ascent = ink_ascent.max(bounds[3] * scale + glyph.y_offset);
901 ink_descent = ink_descent.max(-(bounds[1] * scale + glyph.y_offset));
902 placed.push(((glyph.font, glyph.glyph_index), origin, scale, pixels));
903 },
904 );
905 let (outline, padding) = decoration_extents(request);
907 let margin = |overflow: i64| -> Result<u32, TextRasterError> {
908 u32::try_from(overflow)
909 .map_err(|_| TextRasterError::TooLarge)?
910 .checked_add(outline)
911 .map(|margin| margin.max(outline.max(padding)))
912 .ok_or(TextRasterError::TooLarge)
913 };
914 let (margin_x, margin_y) = (margin(overflow_x)?, margin(overflow_y)?);
915 let width = margin_x
916 .checked_mul(2)
917 .and_then(|margins| box_width.checked_add(margins))
918 .ok_or(TextRasterError::TooLarge)?;
919 let height = margin_y
920 .checked_mul(2)
921 .and_then(|margins| box_height.checked_add(margins))
922 .ok_or(TextRasterError::TooLarge)?;
923 if width > 16_384 || height > 16_384 {
924 return Err(TextRasterError::TooLarge);
925 }
926 let pixel_count = usize::try_from(width)
927 .ok()
928 .and_then(|width| {
929 usize::try_from(height)
930 .ok()
931 .and_then(|height| width.checked_mul(height))
932 })
933 .ok_or(TextRasterError::TooLarge)?;
934 let mut coverage = vec![0_u8; pixel_count];
935 let mut colors: Option<Vec<u8>> = None;
937 let mut line_boxes = Vec::with_capacity(layout.lines.len());
938 for (index, line) in layout.lines.iter().enumerate() {
939 if line.width > 0.0 {
940 line_boxes.push((
941 layout.line_offset(line, request.align),
942 line.width,
943 layout.line_top(index),
944 ));
945 }
946 }
947 for (key, origin, scale, pixels) in placed {
948 let (origin_x, origin_y) = (
949 pixels.left + i64::from(margin_x),
950 pixels.top + i64::from(margin_y),
951 );
952 let glyph_width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
953 match &drawings[&key] {
954 Drawing::Outline(outline) => place_glyph(
955 &mut coverage,
956 width,
957 height,
958 origin_x,
959 origin_y,
960 glyph_width,
961 &outline.draw(origin, scale, pixels),
962 ),
963 drawing => place_color_glyph(
964 colors.get_or_insert_with(|| vec![0; pixel_count * 4]),
965 width,
966 height,
967 origin_x,
968 origin_y,
969 glyph_width,
970 &drawing.draw_color(origin, scale, pixels),
971 ),
972 }
973 }
974 let bands = stroke_bands(request, &coverage, colors.as_deref(), width);
975 let (rgba8, background) = compose(
976 request,
977 &coverage,
978 colors.as_deref(),
979 width,
980 [margin_x, margin_y],
981 &line_boxes,
982 &bands,
983 &RequestLook::of(request, [layout.width, layout.height]),
984 );
985 let texture = HostTexture {
986 content_version: request.content_version,
987 width,
988 height,
989 premultiplication: Premultiplication::Premultiplied,
990 color_space: RasterColorSpace::Srgb,
991 };
992 Ok(TextRasterResult {
993 descriptor: TextTexture {
994 texture: texture.clone(),
995 layout_x: margin_x as f32,
996 layout_y: margin_y as f32,
997 layout_width: layout.width,
998 layout_height: layout.height,
999 first_baseline: layout.half_leading + layout.ascent,
1000 ascent: ink_ascent,
1001 descent: ink_descent,
1002 line_count: layout.line_count,
1003 },
1004 pixels: HostTexturePixels { texture, rgba8 },
1005 background,
1006 })
1007 }
1008
1009 fn font_order(&self, family: Option<&str>, weight: u16) -> Vec<usize> {
1012 let mut groups: Vec<(Option<&str>, Vec<usize>)> = Vec::new();
1014 for (index, font) in self.fonts.iter().enumerate() {
1015 let name = font.family.as_deref();
1016 match groups
1017 .iter_mut()
1018 .find(|(held, _)| name.is_some() && *held == name)
1019 {
1020 Some((_, faces)) => faces.push(index),
1021 None => groups.push((name, vec![index])),
1022 }
1023 }
1024 if let Some(requested) = family {
1025 let wanted = fold_family(requested);
1026 match groups.iter().position(|(_, faces)| {
1027 faces
1028 .iter()
1029 .any(|&index| self.fonts[index].aliases.contains(&wanted))
1030 }) {
1031 Some(at) => {
1032 let preferred = groups.remove(at);
1033 groups.insert(0, preferred);
1034 }
1035 None if !self.fonts.is_empty() => {
1036 self.unmatched
1037 .lock()
1038 .unwrap_or_else(std::sync::PoisonError::into_inner)
1039 .insert(requested.to_owned());
1040 }
1041 None => {}
1042 }
1043 }
1044 groups
1045 .into_iter()
1046 .flat_map(|(_, faces)| {
1047 let weights = faces
1048 .iter()
1049 .map(|&index| self.fonts[index].weights())
1050 .collect::<Vec<_>>();
1051 weight_order(&weights, weight)
1052 .into_iter()
1053 .map(move |at| faces[at])
1054 })
1055 .collect()
1056 }
1057}
1058
1059struct Faces<'a> {
1061 parsed: Vec<ttf_parser::Face<'a>>,
1062 shapers: Vec<OnceCell<rustybuzz::Face<'a>>>,
1063 color: Vec<bool>,
1065}
1066
1067impl<'a> Faces<'a> {
1068 fn new(fonts: &'a [LoadedFont], weight: u16) -> Result<Self, TextRasterError> {
1070 let parsed = fonts
1071 .iter()
1072 .map(|font| {
1073 let mut face = ttf_parser::Face::parse(&font.source.bytes, font.source.face_index)
1074 .ok()
1075 .filter(|face| face.units_per_em() > 0)
1076 .ok_or_else(|| TextRasterError::InvalidFont(font.source.family.clone()))?;
1077 if let Some((min, _, max)) = font.wght {
1078 face.set_variation(WGHT, f32::from(weight).clamp(min, max));
1079 }
1080 Ok(face)
1081 })
1082 .collect::<Result<Vec<_>, _>>()?;
1083 Ok(Self {
1084 shapers: parsed.iter().map(|_| OnceCell::new()).collect(),
1085 color: parsed
1086 .iter()
1087 .map(|face| {
1088 let tables = face.tables();
1089 tables.colr.is_some() || tables.cbdt.is_some() || tables.sbix.is_some()
1090 })
1091 .collect(),
1092 parsed,
1093 })
1094 }
1095
1096 fn shaper(&self, font: usize) -> &rustybuzz::Face<'a> {
1097 self.shapers[font].get_or_init(|| rustybuzz::Face::from_face(self.parsed[font].clone()))
1098 }
1099
1100 #[allow(clippy::cast_precision_loss)]
1101 fn units_per_em(&self, font: usize) -> f32 {
1102 f32::from(self.parsed[font].units_per_em())
1103 }
1104
1105 fn has(&self, font: usize, character: char) -> bool {
1106 self.parsed[font]
1107 .glyph_index(character)
1108 .is_some_and(|glyph| glyph.0 != 0)
1109 }
1110
1111 fn covers(&self, font: usize, grapheme: &str) -> bool {
1113 grapheme
1114 .chars()
1115 .filter(|&character| !is_default_ignorable(character))
1116 .all(|character| self.has(font, character))
1117 }
1118}
1119
1120fn normalize(text: &str, policy: EmojiPolicy) -> String {
1123 normalize_mapped(text, policy, None)
1124}
1125
1126fn normalize_mapped(text: &str, policy: EmojiPolicy, mut map: Option<&mut Vec<usize>>) -> String {
1129 let mut normalized = String::with_capacity(text.len());
1130 for (start, grapheme) in text.grapheme_indices(true) {
1131 let before = normalized.len();
1132 if policy == EmojiPolicy::Replace && is_emoji(grapheme) {
1133 normalized.push('\u{fffd}');
1134 } else if grapheme == "\r\n" {
1135 normalized.push('\n');
1136 } else {
1137 normalized.extend(grapheme.chars().map(|character| match character {
1138 '\r'
1139 | '\u{b}'
1140 | '\u{c}'
1141 | '\u{1c}'..='\u{1e}'
1142 | '\u{85}'
1143 | '\u{2028}'
1144 | '\u{2029}' => '\n',
1145 '\t' => ' ',
1146 other => other,
1147 }));
1148 }
1149 if let Some(map) = map.as_deref_mut() {
1150 let written = normalized.len() - before;
1152 if written == grapheme.len() {
1153 map.extend(start..start + written);
1154 } else {
1155 map.extend(std::iter::repeat_n(start, written));
1156 }
1157 }
1158 }
1159 if let Some(map) = map {
1160 map.push(text.len());
1161 }
1162 normalized
1163}
1164
1165fn is_emoji(grapheme: &str) -> bool {
1168 if grapheme.contains('\u{fe0e}') {
1169 return false;
1170 }
1171 let selected = grapheme.contains('\u{fe0f}');
1172 grapheme.chars().any(|character| {
1173 in_table(unicode_tables::EMOJI_PRESENTATION, character)
1174 || (selected
1175 && (character == '\u{20e3}'
1176 || in_table(unicode_tables::EXTENDED_PICTOGRAPHIC, character)))
1177 })
1178}
1179
1180fn is_cursive(text: &str) -> bool {
1182 text.chars()
1183 .map(|character| character.script())
1184 .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown))
1185 .is_some_and(|script| {
1186 matches!(
1187 script,
1188 Script::Adlam
1189 | Script::Arabic
1190 | Script::Hanifi_Rohingya
1191 | Script::Mandaic
1192 | Script::Mongolian
1193 | Script::Nko
1194 | Script::Phags_Pa
1195 | Script::Syriac
1196 )
1197 })
1198}
1199
1200fn is_default_ignorable(character: char) -> bool {
1201 in_table(unicode_tables::DEFAULT_IGNORABLE, character)
1202}
1203
1204fn in_table(table: &[(u32, u32)], character: char) -> bool {
1205 let code = u32::from(character);
1206 table
1207 .binary_search_by(|&(first, last)| {
1208 if last < code {
1209 Ordering::Less
1210 } else if first > code {
1211 Ordering::Greater
1212 } else {
1213 Ordering::Equal
1214 }
1215 })
1216 .is_ok()
1217}
1218
1219fn hangs(character: char) -> bool {
1221 character.is_whitespace()
1222 && !matches!(character, '\u{a0}' | '\u{2007}' | '\u{202f}' | '\u{1680}')
1223}
1224
1225fn is_hangul(character: char) -> bool {
1226 matches!(
1227 character,
1228 '\u{1100}'..='\u{11ff}'
1229 | '\u{3130}'..='\u{318f}'
1230 | '\u{a960}'..='\u{a97f}'
1231 | '\u{ac00}'..='\u{d7a3}'
1232 | '\u{d7b0}'..='\u{d7ff}'
1233 )
1234}
1235
1236struct Layout {
1238 lines: Vec<LaidLine>,
1239 clusters: Vec<ClusterSpan>,
1241 line_count: u32,
1242 line_advance: f32,
1243 half_leading: f32,
1245 ascent: f32,
1246 descent: f32,
1247 width: f32,
1249 height: f32,
1250 box_width: u32,
1251 box_height: u32,
1252}
1253
1254impl Layout {
1255 fn line_offset(&self, line: &LaidLine, align: TextAlign) -> f32 {
1257 match (align, line.rtl) {
1258 (TextAlign::Center, _) => (self.width - line.width) * 0.5,
1259 (TextAlign::Start, false) | (TextAlign::End, true) => 0.0,
1260 (TextAlign::End, false) | (TextAlign::Start, true) => self.width - line.width,
1261 }
1262 }
1263
1264 #[allow(clippy::cast_precision_loss)]
1265 fn line_top(&self, index: usize) -> f32 {
1266 index as f32 * self.line_advance + self.half_leading
1267 }
1268
1269 #[allow(clippy::cast_precision_loss)]
1272 fn each_glyph(
1273 &self,
1274 faces: &Faces<'_>,
1275 request: &TextRasterRequest,
1276 drawings: &mut HashMap<(usize, u16), Drawing>,
1277 mut place: impl FnMut(&Drawing, [f32; 2], f32, &PositionedGlyph),
1278 ) {
1279 for (index, line) in self.lines.iter().enumerate() {
1280 let offset = self.line_offset(line, request.align);
1281 let baseline = self.line_top(index) + self.ascent;
1282 for glyph in &line.glyphs {
1283 let drawing = drawings
1284 .entry((glyph.font, glyph.glyph_index))
1285 .or_insert_with(|| Drawing::of(faces, glyph.font, glyph.glyph_index, request));
1286 let scale = request.font_size / faces.units_per_em(glyph.font);
1287 let origin = [glyph.x + glyph.x_offset + offset, baseline - glyph.y_offset];
1288 place(drawing, origin, scale, glyph);
1289 }
1290 }
1291 }
1292}
1293
1294fn include(bounds: &mut Option<[f32; 4]>, [left, top, right, bottom]: [f32; 4]) {
1295 let held = bounds.get_or_insert([left, top, right, bottom]);
1296 *held = [
1297 held[0].min(left),
1298 held[1].min(top),
1299 held[2].max(right),
1300 held[3].max(bottom),
1301 ];
1302}
1303
1304fn grown([left, top, right, bottom]: [f32; 4], by: f32) -> [f32; 4] {
1305 [left - by, top - by, right + by, bottom + by]
1306}
1307
1308#[derive(Clone, Debug)]
1310struct ClusterSpan {
1311 bytes: Range<usize>,
1312 line: usize,
1313}
1314
1315struct LaidLine {
1316 glyphs: Vec<PositionedGlyph>,
1318 width: f32,
1320 rtl: bool,
1321 has_glyphs: bool,
1323}
1324
1325#[allow(clippy::too_many_arguments)]
1328fn layout_paragraph(
1329 faces: &Faces<'_>,
1330 paragraph: &str,
1331 font_size: f32,
1332 max_width: f32,
1333 order: &[usize],
1334 policy: EmojiPolicy,
1335 letter_spacing: f32,
1336 (cluster_base, line_base): (usize, usize),
1337 spans: &mut Vec<ClusterSpan>,
1338) -> Vec<LaidLine> {
1339 let bidi = ParagraphBidiInfo::new(paragraph, None);
1340 let rtl = bidi.paragraph_level.is_rtl();
1341 let glyphs = shape_paragraph(
1342 faces,
1343 paragraph,
1344 &bidi,
1345 font_size,
1346 order,
1347 policy,
1348 letter_spacing,
1349 );
1350 let clusters = clusters(&glyphs, paragraph, letter_spacing);
1351 let lines = break_lines(&clusters, max_width);
1352 for (line, range) in lines.iter().enumerate() {
1353 for index in range.clone() {
1354 let end = clusters
1355 .get(index + 1)
1356 .map_or(paragraph.len(), |next| next.start);
1357 spans.push(ClusterSpan {
1358 bytes: clusters[index].start..end.max(clusters[index].start),
1359 line: line_base + line,
1360 });
1361 }
1362 }
1363 lines
1364 .into_iter()
1365 .map(|range| {
1366 let has_glyphs = !range.is_empty();
1367 let mut end = range.end;
1369 while end > range.start && clusters[end - 1].hangs {
1370 end -= 1;
1371 }
1372 let mut line = LaidLine {
1373 glyphs: Vec::new(),
1374 width: 0.0,
1375 rtl,
1376 has_glyphs,
1377 };
1378 if end == range.start {
1379 return line;
1380 }
1381 let bytes = clusters[range.start].start
1382 ..clusters
1383 .get(range.end)
1384 .map_or(paragraph.len(), |next| next.start);
1385 let levels = bidi.reordered_levels(bytes);
1386 let mut cluster_of = vec![0; clusters[end - 1].glyphs.end];
1388 for (index, cluster) in clusters[range.start..end].iter().enumerate() {
1389 cluster_of[cluster.glyphs.clone()].fill(range.start + index);
1390 }
1391 let mut x = 0.0_f32;
1392 let mut previous: Option<usize> = None;
1393 for index in visual_order(
1394 clusters[range.start].glyphs.start..clusters[end - 1].glyphs.end,
1395 |index| levels[glyphs[index].cluster].number(),
1396 ) {
1397 let glyph = &glyphs[index];
1398 let cluster = cluster_of[index];
1399 if let Some(before) = previous.filter(|&before| before != cluster) {
1400 x += clusters[before].spacing;
1401 }
1402 previous = Some(cluster);
1403 line.glyphs.push(PositionedGlyph {
1404 font: glyph.font,
1405 glyph_index: glyph.glyph_index,
1406 cluster: cluster_base + cluster,
1407 advance: glyph.advance,
1408 x,
1409 x_offset: glyph.x_offset,
1410 y_offset: glyph.y_offset,
1411 });
1412 x += glyph.advance;
1413 }
1414 line.width = x;
1415 line
1416 })
1417 .collect()
1418}
1419
1420fn visual_order(range: Range<usize>, level: impl Fn(usize) -> u8) -> Vec<usize> {
1422 let mut order = range.collect::<Vec<_>>();
1423 let levels = order.iter().map(|&index| level(index)).collect::<Vec<_>>();
1424 let highest = levels.iter().copied().max().unwrap_or(0);
1425 let Some(lowest_odd) = levels.iter().copied().filter(|level| level % 2 == 1).min() else {
1426 return order;
1427 };
1428 let mut current = levels;
1429 for at_least in (lowest_odd..=highest).rev() {
1430 let mut start = 0;
1431 while start < order.len() {
1432 if current[start] < at_least {
1433 start += 1;
1434 continue;
1435 }
1436 let end = (start..order.len())
1437 .find(|&index| current[index] < at_least)
1438 .unwrap_or(order.len());
1439 order[start..end].reverse();
1440 current[start..end].reverse();
1441 start = end;
1442 }
1443 }
1444 order
1445}
1446
1447#[derive(Clone, Debug, PartialEq)]
1449struct Run {
1450 range: Range<usize>,
1451 font: usize,
1452 script: Option<Script>,
1453 rtl: bool,
1454}
1455
1456fn itemize(
1463 faces: &Faces<'_>,
1464 paragraph: &str,
1465 bidi: &ParagraphBidiInfo<'_>,
1466 order: &[usize],
1467 policy: EmojiPolicy,
1468) -> Vec<Run> {
1469 let first = order.first().copied().unwrap_or(0);
1470 let (color, text): (Vec<usize>, Vec<usize>) =
1471 order.iter().partition(|&&font| faces.color[font]);
1472 let color_first = color.iter().chain(&text).copied().collect::<Vec<_>>();
1473 let text_first = text.iter().chain(&color).copied().collect::<Vec<_>>();
1474 let fits = |font: usize, color_wanted: bool| {
1475 faces.color[font] == color_wanted
1476 || if color_wanted {
1477 color.is_empty()
1478 } else {
1479 text.is_empty()
1480 }
1481 };
1482 let mut graphemes = Vec::<(usize, usize, Option<Script>, bool)>::new();
1483 let mut current = None;
1484 for (start, grapheme) in paragraph.grapheme_indices(true) {
1485 let script = grapheme
1486 .chars()
1487 .map(|character| character.script())
1488 .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown));
1489 let wants_color = if grapheme.contains('\u{fe0e}') {
1490 Some(false)
1491 } else if is_emoji(grapheme) {
1492 Some(policy == EmojiPolicy::Color)
1493 } else {
1494 None
1495 };
1496 let order = match wants_color {
1497 Some(true) => color_first.as_slice(),
1498 Some(false) => text_first.as_slice(),
1499 None => order,
1500 };
1501 let stays = current.filter(|&font| {
1502 script.is_none()
1503 && wants_color.is_none_or(|wanted| fits(font, wanted))
1504 && faces.covers(font, grapheme)
1505 });
1506 let font = stays
1507 .or_else(|| {
1508 order
1509 .iter()
1510 .copied()
1511 .find(|&font| faces.covers(font, grapheme))
1512 })
1513 .or_else(|| {
1514 let base = grapheme
1515 .chars()
1516 .find(|&character| !is_default_ignorable(character))?;
1517 order.iter().copied().find(|&font| faces.has(font, base))
1518 })
1519 .or(current)
1520 .unwrap_or(first);
1521 current = Some(font);
1522 graphemes.push((start, font, script, bidi.levels[start].is_rtl()));
1523 }
1524 let leading = graphemes.iter().find_map(|grapheme| grapheme.2);
1525 let mut before = leading;
1526 for grapheme in &mut graphemes {
1527 match grapheme.2 {
1528 Some(script) => before = Some(script),
1529 None => grapheme.2 = before,
1530 }
1531 }
1532 let mut runs = Vec::<Run>::new();
1533 for (index, &(start, font, script, rtl)) in graphemes.iter().enumerate() {
1534 let end = graphemes
1535 .get(index + 1)
1536 .map_or(paragraph.len(), |next| next.0);
1537 match runs.last_mut() {
1538 Some(run) if run.font == font && run.script == script && run.rtl == rtl => {
1539 run.range.end = end;
1540 }
1541 _ => runs.push(Run {
1542 range: start..end,
1543 font,
1544 script,
1545 rtl,
1546 }),
1547 }
1548 }
1549 runs
1550}
1551
1552fn shaping_script(script: Script) -> Option<rustybuzz::Script> {
1553 let code: [u8; 4] = script.short_name().as_bytes().try_into().ok()?;
1554 rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&code))
1555}
1556
1557fn shape_paragraph(
1559 faces: &Faces<'_>,
1560 paragraph: &str,
1561 bidi: &ParagraphBidiInfo<'_>,
1562 font_size: f32,
1563 order: &[usize],
1564 policy: EmojiPolicy,
1565 letter_spacing: f32,
1566) -> Vec<ShapedGlyph> {
1567 let features = if letter_spacing == 0.0 {
1569 Vec::new()
1570 } else {
1571 [b"liga", b"clig", b"dlig", b"hlig"]
1572 .map(|tag| rustybuzz::Feature::new(ttf_parser::Tag::from_bytes(tag), 0, ..))
1573 .to_vec()
1574 };
1575 let mut glyphs = Vec::new();
1576 for run in itemize(faces, paragraph, bidi, order, policy) {
1577 let face = faces.shaper(run.font);
1578 let units = faces.units_per_em(run.font);
1579 let mut buffer = UnicodeBuffer::new();
1580 buffer.push_str(¶graph[run.range.clone()]);
1581 buffer.set_pre_context(¶graph[..run.range.start]);
1582 buffer.set_post_context(¶graph[run.range.end..]);
1583 buffer.set_direction(if run.rtl {
1584 Direction::RightToLeft
1585 } else {
1586 Direction::LeftToRight
1587 });
1588 match run.script.and_then(shaping_script) {
1589 Some(script) => buffer.set_script(script),
1590 None => buffer.guess_segment_properties(),
1591 }
1592 let shaped = rustybuzz::shape(face, &features, buffer);
1593 let first = glyphs.len();
1594 for (info, position) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) {
1595 #[allow(clippy::cast_precision_loss)]
1596 glyphs.push(ShapedGlyph {
1597 font: run.font,
1598 glyph_index: u16::try_from(info.glyph_id)
1600 .ok()
1601 .filter(|&glyph| glyph < face.number_of_glyphs())
1602 .unwrap_or(0),
1603 cluster: run.range.start + usize::try_from(info.cluster).unwrap_or(0),
1604 advance: position.x_advance as f32 * font_size / units,
1605 x_offset: position.x_offset as f32 * font_size / units,
1606 y_offset: position.y_offset as f32 * font_size / units,
1607 });
1608 }
1609 if run.rtl {
1611 glyphs[first..].reverse();
1612 }
1613 }
1614 glyphs
1615}
1616
1617#[derive(Copy, Clone, Debug)]
1618struct ShapedGlyph {
1619 font: usize,
1620 glyph_index: u16,
1621 cluster: usize,
1623 advance: f32,
1624 x_offset: f32,
1626 y_offset: f32,
1627}
1628
1629#[derive(Clone, Debug, PartialEq)]
1631struct Cluster {
1632 glyphs: Range<usize>,
1633 start: usize,
1635 width: f32,
1637 spacing: f32,
1639 hangs: bool,
1641 break_before: bool,
1643}
1644
1645fn clusters(glyphs: &[ShapedGlyph], paragraph: &str, letter_spacing: f32) -> Vec<Cluster> {
1648 let mut opportunities = unicode_linebreak::linebreaks(paragraph)
1649 .filter(|&(at, kind)| {
1650 kind == BreakOpportunity::Allowed
1651 && !(paragraph[..at].chars().next_back().is_some_and(is_hangul)
1652 && paragraph[at..].chars().next().is_some_and(is_hangul))
1653 })
1654 .map(|(at, _)| at)
1655 .peekable();
1656 let mut clusters = Vec::<Cluster>::new();
1657 for (index, glyph) in glyphs.iter().enumerate() {
1658 match clusters.last_mut() {
1659 Some(cluster) if cluster.start == glyph.cluster => {
1660 cluster.glyphs.end = index + 1;
1661 cluster.width += glyph.advance;
1662 }
1663 _ => clusters.push(Cluster {
1664 glyphs: index..index + 1,
1665 start: glyph.cluster,
1666 width: glyph.advance,
1667 spacing: 0.0,
1668 hangs: false,
1669 break_before: false,
1670 }),
1671 }
1672 }
1673 for index in 0..clusters.len() {
1674 let start = clusters[index].start;
1675 let end = clusters
1676 .get(index + 1)
1677 .map(|next| next.start)
1678 .filter(|&end| end > start)
1679 .unwrap_or_else(|| {
1680 paragraph[start..]
1681 .chars()
1682 .next()
1683 .map_or(start, |character| start + character.len_utf8())
1684 });
1685 clusters[index].hangs = paragraph[start..end].chars().all(hangs);
1686 if letter_spacing != 0.0 && !is_cursive(¶graph[start..end]) {
1687 clusters[index].spacing = letter_spacing;
1688 clusters[index].width += letter_spacing;
1689 }
1690 let mut found = false;
1691 while let Some(&at) = opportunities.peek() {
1692 if at > start {
1693 break;
1694 }
1695 opportunities.next();
1696 found = true;
1697 }
1698 clusters[index].break_before = index > 0 && found;
1699 }
1700 clusters
1701}
1702
1703fn break_lines(clusters: &[Cluster], max_width: f32) -> Vec<Range<usize>> {
1706 let mut lines = Vec::new();
1707 let mut start = 0;
1708 let mut x = 0.0_f32;
1709 let mut opportunity: Option<(usize, f32)> = None;
1711 for (index, cluster) in clusters.iter().enumerate() {
1712 if cluster.break_before && index > start {
1713 opportunity = Some((index, x));
1714 }
1715 let fits = |x: f32| x + cluster.width - cluster.spacing <= max_width;
1716 if max_width > 0.0 && !cluster.hangs && index > start && !fits(x) {
1717 let (end, width) = opportunity.unwrap_or((index, x));
1718 lines.push(start..end);
1719 start = end;
1720 x -= width;
1721 opportunity = None;
1722 if index > start && !fits(x) {
1724 lines.push(start..index);
1725 start = index;
1726 x = 0.0;
1727 }
1728 }
1729 x += cluster.width;
1730 }
1731 lines.push(start..clusters.len());
1732 lines
1733}
1734
1735#[derive(Copy, Clone, Debug)]
1736struct PositionedGlyph {
1737 font: usize,
1738 glyph_index: u16,
1739 cluster: usize,
1741 advance: f32,
1743 x: f32,
1745 x_offset: f32,
1747 y_offset: f32,
1748}
1749
1750enum Drawing {
1752 Outline(Outline),
1753 Painting(color::Painting),
1754 Bitmap(color::Bitmap),
1755}
1756
1757impl Drawing {
1758 fn of(faces: &Faces<'_>, font: usize, glyph: u16, request: &TextRasterRequest) -> Self {
1761 let face = &faces.parsed[font];
1762 if request.emoji_policy == EmojiPolicy::Color && faces.color[font] {
1763 let [red, green, blue, _] = request.fill.foreground();
1764 if let Some(painting) = color::Painting::of(face, glyph, [red, green, blue, 255]) {
1765 return Self::Painting(painting);
1766 }
1767 if let Some(bitmap) = color::Bitmap::of(face, glyph, request.font_size) {
1768 return Self::Bitmap(bitmap);
1769 }
1770 }
1771 Self::Outline(Outline::of(face, glyph))
1772 }
1773
1774 fn bounds(&self) -> Option<[f32; 4]> {
1776 match self {
1777 Self::Outline(outline) => outline.bounds,
1778 Self::Painting(painting) => painting.bounds(),
1779 Self::Bitmap(bitmap) => Some(bitmap.bounds()),
1780 }
1781 }
1782
1783 fn ink_bounds(&self) -> Option<[f32; 4]> {
1785 match self {
1786 Self::Outline(outline) => outline.curve_bounds(),
1787 Self::Painting(painting) => painting.bounds(),
1788 Self::Bitmap(bitmap) => bitmap.ink_bounds(),
1789 }
1790 }
1791
1792 fn draw_color(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1794 match self {
1795 Self::Outline(_) => Vec::new(),
1796 Self::Painting(painting) => painting.draw(origin, scale, pixels),
1797 Self::Bitmap(bitmap) => bitmap.draw(origin, scale, pixels),
1798 }
1799 }
1800}
1801
1802#[derive(Default)]
1804struct Outline {
1805 segments: Vec<Segment>,
1806 bounds: Option<[f32; 4]>,
1808}
1809
1810enum Segment {
1811 Line([f32; 2], [f32; 2]),
1812 Quad([f32; 2], [f32; 2], [f32; 2]),
1813 Cubic([f32; 2], [f32; 2], [f32; 2], [f32; 2]),
1814}
1815
1816#[derive(Copy, Clone, Debug)]
1818struct PixelBox {
1819 left: i64,
1820 top: i64,
1821 right: i64,
1822 bottom: i64,
1823}
1824
1825impl PixelBox {
1826 #[allow(clippy::cast_possible_truncation)]
1827 fn of(bounds: [f32; 4], origin: [f32; 2], scale: f32) -> Self {
1828 let [x_min, y_min, x_max, y_max] = bounds;
1829 Self {
1831 left: (origin[0] + x_min * scale).floor() as i64,
1832 top: (origin[1] - y_max * scale).floor() as i64,
1833 right: (origin[0] + x_max * scale).ceil() as i64,
1834 bottom: (origin[1] - y_min * scale).ceil() as i64,
1835 }
1836 }
1837}
1838
1839#[cfg(test)]
1840thread_local! {
1841 static OUTLINES_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1842}
1843
1844impl Outline {
1845 fn of(face: &ttf_parser::Face<'_>, glyph: u16) -> Self {
1846 #[cfg(test)]
1847 OUTLINES_READ.with(|read| read.set(read.get() + 1));
1848 let mut sink = OutlineSink::default();
1849 face.outline_glyph(GlyphId(glyph), &mut sink);
1850 sink.close();
1851 sink.outline
1852 }
1853
1854 #[allow(
1855 clippy::cast_possible_truncation,
1856 clippy::cast_sign_loss,
1857 clippy::cast_precision_loss
1858 )]
1859 fn draw(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1860 let width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
1861 let height = usize::try_from(pixels.bottom - pixels.top).unwrap_or(0);
1862 let (left, top) = (pixels.left as f32, pixels.top as f32);
1863 let raster = self.raster(width, height, |[x, y]| {
1864 point(origin[0] + x * scale - left, (origin[1] - y * scale) - top)
1865 });
1866 let mut alpha = vec![0_u8; width * height];
1867 raster.for_each_pixel(|index, coverage| {
1868 alpha[index] = (coverage.min(1.0) * 255.0).round() as u8;
1869 });
1870 alpha
1871 }
1872}
1873
1874impl Outline {
1875 fn curve_bounds(&self) -> Option<[f32; 4]> {
1877 let mut bounds = None;
1878 let mut hold = |[x, y]: [f32; 2]| include(&mut bounds, [x, y, x, y]);
1879 for segment in &self.segments {
1880 match *segment {
1881 Segment::Line(from, to) => {
1882 hold(from);
1883 hold(to);
1884 }
1885 Segment::Quad(from, control, to) => {
1886 hold(from);
1887 hold(to);
1888 for axis in 0..2 {
1889 let curvature = from[axis] - 2.0 * control[axis] + to[axis];
1890 let t = (from[axis] - control[axis]) / curvature;
1891 if t > 0.0 && t < 1.0 {
1892 hold(std::array::from_fn(|axis| {
1893 let u = 1.0 - t;
1894 u * u * from[axis] + 2.0 * u * t * control[axis] + t * t * to[axis]
1895 }));
1896 }
1897 }
1898 }
1899 Segment::Cubic(from, first, second, to) => {
1900 hold(from);
1901 hold(to);
1902 for axis in 0..2 {
1903 let [p0, p1, p2, p3] = [from[axis], first[axis], second[axis], to[axis]];
1904 for t in quadratic_roots(
1905 -p0 + 3.0 * (p1 - p2) + p3,
1906 2.0 * (p0 - 2.0 * p1 + p2),
1907 p1 - p0,
1908 )
1909 .into_iter()
1910 .flatten()
1911 .filter(|t| *t > 0.0 && *t < 1.0)
1912 {
1913 hold(std::array::from_fn(|axis| {
1914 let u = 1.0 - t;
1915 u * u * u * from[axis]
1916 + 3.0 * u * u * t * first[axis]
1917 + 3.0 * u * t * t * second[axis]
1918 + t * t * t * to[axis]
1919 }));
1920 }
1921 }
1922 }
1923 }
1924 }
1925 bounds
1926 }
1927
1928 fn raster(&self, width: usize, height: usize, at: impl Fn([f32; 2]) -> Point) -> Rasterizer {
1929 let mut raster = Rasterizer::new(width, height);
1930 for segment in &self.segments {
1931 match *segment {
1932 Segment::Line(from, to) => raster.draw_line(at(from), at(to)),
1933 Segment::Quad(from, control, to) => raster.draw_quad(at(from), at(control), at(to)),
1934 Segment::Cubic(from, first, second, to) => {
1935 raster.draw_cubic(at(from), at(first), at(second), at(to));
1936 }
1937 }
1938 }
1939 raster
1940 }
1941}
1942
1943fn quadratic_roots(a: f32, b: f32, c: f32) -> [Option<f32>; 2] {
1945 let finite = |t: f32| t.is_finite().then_some(t);
1946 if a.abs() <= f32::EPSILON * (b.abs() + c.abs()) {
1947 return [finite(-c / b), None];
1948 }
1949 let discriminant = b * b - 4.0 * a * c;
1950 if discriminant < 0.0 {
1951 return [None, None];
1952 }
1953 let root = discriminant.sqrt();
1954 [
1955 finite((-b + root) / (2.0 * a)),
1956 finite((-b - root) / (2.0 * a)),
1957 ]
1958}
1959
1960#[derive(Default)]
1961struct OutlineSink {
1962 outline: Outline,
1963 start: [f32; 2],
1964 pen: [f32; 2],
1965}
1966
1967impl OutlineSink {
1968 fn include(&mut self, points: &[[f32; 2]]) {
1969 for &[x, y] in points {
1970 let bounds = self.outline.bounds.get_or_insert([x, y, x, y]);
1971 *bounds = [
1972 bounds[0].min(x),
1973 bounds[1].min(y),
1974 bounds[2].max(x),
1975 bounds[3].max(y),
1976 ];
1977 }
1978 }
1979}
1980
1981impl OutlineBuilder for OutlineSink {
1982 fn move_to(&mut self, x: f32, y: f32) {
1983 self.close();
1984 self.start = [x, y];
1985 self.pen = [x, y];
1986 }
1987
1988 fn line_to(&mut self, x: f32, y: f32) {
1989 let to = [x, y];
1990 self.include(&[self.pen, to]);
1991 self.outline.segments.push(Segment::Line(self.pen, to));
1992 self.pen = to;
1993 }
1994
1995 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1996 let (control, to) = ([x1, y1], [x, y]);
1997 self.include(&[self.pen, control, to]);
1998 self.outline
1999 .segments
2000 .push(Segment::Quad(self.pen, control, to));
2001 self.pen = to;
2002 }
2003
2004 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
2005 let (first, second, to) = ([x1, y1], [x2, y2], [x, y]);
2006 self.include(&[self.pen, first, second, to]);
2007 self.outline
2008 .segments
2009 .push(Segment::Cubic(self.pen, first, second, to));
2010 self.pen = to;
2011 }
2012
2013 fn close(&mut self) {
2014 if self.pen.map(f32::to_bits) != self.start.map(f32::to_bits) {
2016 self.line_to(self.start[0], self.start[1]);
2017 }
2018 }
2019}
2020
2021fn validate_request(request: &TextRasterRequest) -> Result<(), TextRasterError> {
2022 if request.font_size.is_finite()
2023 && request.font_size > 0.0
2024 && request.font_size <= MAX_FONT_SIZE
2025 && request.text.len() <= MAX_TEXT_BYTES
2026 && request.max_width.is_finite()
2027 && request.max_width <= 16_384.0
2028 && request.line_height.is_finite()
2029 && request.line_height > 0.0
2030 && request.line_height <= MAX_LINE_HEIGHT
2031 && request.letter_spacing.is_finite()
2032 && request.letter_spacing >= LETTER_SPACING_RANGE.start() * request.font_size
2033 && request.letter_spacing <= LETTER_SPACING_RANGE.end() * request.font_size
2034 && (1..=1000).contains(&request.font_weight)
2035 && request.content_version > 0
2036 && request.fill.is_valid()
2037 && request.strokes.len() <= MAX_STROKES
2038 && request
2039 .strokes
2040 .iter()
2041 .all(|stroke| stroke.width.is_finite() && stroke.width > 0.0)
2042 && stroke_radius(&request.strokes).is_none_or(|radius| radius <= MAX_STROKE_EXTENT)
2043 && request.background.is_none_or(|background| {
2044 background.padding.is_finite()
2045 && background.padding >= 0.0
2046 && background.padding <= MAX_BACKGROUND_PADDING
2047 })
2048 {
2049 Ok(())
2050 } else {
2051 Err(TextRasterError::InvalidRequest)
2052 }
2053}
2054
2055fn checked_dimension(value: f32) -> Result<u32, TextRasterError> {
2056 if value.is_finite() && value > 0.0 && value.ceil() <= 16_384.0 {
2057 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2058 Ok(value.ceil() as u32)
2059 } else {
2060 Err(TextRasterError::TooLarge)
2061 }
2062}
2063
2064fn place_glyph(
2066 mask: &mut [u8],
2067 width: u32,
2068 height: u32,
2069 origin_x: i64,
2070 origin_y: i64,
2071 glyph_width: usize,
2072 alpha: &[u8],
2073) {
2074 for (index, coverage) in alpha.iter().copied().enumerate() {
2075 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
2076 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
2077 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) {
2078 continue;
2079 }
2080 let pixel =
2081 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
2082 let slot = &mut mask[usize::try_from(pixel).unwrap_or(0)];
2083 *slot = (*slot).max(coverage);
2084 }
2085}
2086
2087fn place_color_glyph(
2089 colors: &mut [u8],
2090 width: u32,
2091 height: u32,
2092 origin_x: i64,
2093 origin_y: i64,
2094 glyph_width: usize,
2095 rgba8: &[u8],
2096) {
2097 for (index, source) in rgba8.as_chunks::<4>().0.iter().enumerate() {
2098 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
2099 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
2100 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) || source[3] == 0 {
2101 continue;
2102 }
2103 let pixel =
2104 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
2105 let slot = &mut colors[usize::try_from(pixel).unwrap_or(0) * 4..][..4];
2106 let blended = over(
2107 std::array::from_fn(|channel| u32::from(source[channel])),
2108 std::array::from_fn(|channel| u32::from(slot[channel])),
2109 );
2110 for (byte, value) in slot.iter_mut().zip(blended) {
2111 *byte = u8::try_from(value.min(255)).unwrap_or(u8::MAX);
2112 }
2113 }
2114}
2115
2116impl TextRasterRequest {
2117 #[must_use]
2120 pub fn decoration_margin(&self) -> u32 {
2121 let (outline, padding) = decoration_extents(self);
2122 outline.max(padding)
2123 }
2124}
2125
2126fn decoration_extents(request: &TextRasterRequest) -> (u32, u32) {
2128 let outline = stroke_radius(&request.strokes).map_or(0, disc_extent);
2129 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2130 let padding = request
2131 .background
2132 .map_or(0, |background| background.padding.ceil() as u32);
2133 (outline, padding)
2134}
2135
2136fn stroke_radius(strokes: &[TextStroke]) -> Option<f32> {
2138 let mut radius = None;
2139 for stroke in strokes {
2140 radius = Some(radius.unwrap_or(0.0) + stroke.width);
2141 }
2142 radius
2143}
2144
2145#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2147fn disc_extent(width: f32) -> u32 {
2148 (width + 0.5).ceil() as u32
2149}
2150
2151fn premultiplied(color: [u8; 4], coverage: u8) -> [u32; 4] {
2154 let alpha = u32::from(coverage) * u32::from(color[3]) / 255;
2155 [
2156 u32::from(color[0]) * alpha / 255,
2157 u32::from(color[1]) * alpha / 255,
2158 u32::from(color[2]) * alpha / 255,
2159 alpha,
2160 ]
2161}
2162
2163fn over(source: [u32; 4], destination: [u32; 4]) -> [u32; 4] {
2164 let keep = 255 - source[3];
2165 std::array::from_fn(|channel| source[channel] + (destination[channel] * keep + 127) / 255)
2166}
2167
2168trait PixelLook {
2169 fn fill(&self, index: usize, point: [f32; 2]) -> [u8; 4];
2171 fn band(&self, stroke: usize, index: usize) -> [u8; 4];
2172 fn fade(&self, _index: usize) -> Option<u32> {
2174 None
2175 }
2176}
2177
2178struct RequestLook<'a> {
2179 foreground: [u8; 4],
2180 gradient: Option<gradient::PreparedGradient<'a>>,
2181 strokes: &'a [TextStroke],
2182}
2183
2184impl<'a> RequestLook<'a> {
2185 fn of(request: &'a TextRasterRequest, layout_size: [f32; 2]) -> Self {
2186 Self {
2187 foreground: request.fill.foreground(),
2188 gradient: match &request.fill {
2189 TextFill::Solid(_) => None,
2190 TextFill::Gradient(gradient) => Some(gradient.prepared(layout_size)),
2191 },
2192 strokes: &request.strokes,
2193 }
2194 }
2195}
2196
2197impl PixelLook for RequestLook<'_> {
2198 #[inline]
2199 fn fill(&self, _: usize, point: [f32; 2]) -> [u8; 4] {
2200 self.gradient
2201 .as_ref()
2202 .map_or(self.foreground, |gradient| gradient.rgba8_at(point))
2203 }
2204
2205 #[inline]
2206 fn band(&self, stroke: usize, _: usize) -> [u8; 4] {
2207 self.strokes[stroke].color
2208 }
2209}
2210
2211fn stroke_bands(
2213 request: &TextRasterRequest,
2214 coverage: &[u8],
2215 colors: Option<&[u8]>,
2216 width: u32,
2217) -> Vec<Vec<u8>> {
2218 if request.strokes.is_empty() {
2219 return Vec::new();
2220 }
2221 let height = u32::try_from(coverage.len() / width.max(1) as usize).unwrap_or(0);
2222 let ink = colors.map(|colors| {
2223 coverage
2224 .iter()
2225 .zip(colors.as_chunks::<4>().0)
2226 .map(|(&text, color)| text.max(color[3]))
2227 .collect::<Vec<_>>()
2228 });
2229 let ink = ink.as_deref().unwrap_or(coverage);
2230 let mut radius = 0.0;
2231 request
2232 .strokes
2233 .iter()
2234 .map(|stroke| {
2235 radius += stroke.width;
2236 dilate(ink, width, height, radius)
2237 })
2238 .collect()
2239}
2240
2241#[allow(
2245 clippy::cast_possible_truncation,
2246 clippy::cast_sign_loss,
2247 clippy::cast_precision_loss,
2248 clippy::too_many_arguments
2249)]
2250fn compose(
2251 request: &TextRasterRequest,
2252 coverage: &[u8],
2253 colors: Option<&[u8]>,
2254 width: u32,
2255 margin: [u32; 2],
2256 line_boxes: &[(f32, f32, f32)],
2257 bands: &[Vec<u8>],
2258 look: &impl PixelLook,
2259) -> (Vec<u8>, Option<Vec<u8>>) {
2260 let background = request.background.map(|background| {
2261 let [margin_x, margin_y] = margin.map(|margin| margin as f32);
2262 let padding = background.padding;
2263 let boxes = line_boxes
2264 .iter()
2265 .map(|&(left, line_width, top)| {
2266 (
2267 margin_x + left - padding,
2268 margin_x + left + line_width + padding,
2269 margin_y + top - padding,
2270 margin_y + top + request.font_size + padding,
2271 )
2272 })
2273 .collect::<Vec<_>>();
2274 (background.color, boxes)
2275 });
2276 let split = request.split_background && background.is_some();
2277 let mut split_plane = split.then(|| vec![0_u8; coverage.len() * 4]);
2278 let foreground = request.fill.foreground();
2279 let [margin_x, margin_y] = margin.map(|margin| margin as f32);
2280 let mut rgba8 = vec![0_u8; coverage.len() * 4];
2281 let (mut first, mut last) = (0, 0);
2283 for (index, text) in coverage.iter().copied().enumerate() {
2284 let mut pixel = [0_u32; 4];
2285 let x = (index % width as usize) as f32;
2286 let y = (index / width as usize) as f32;
2287 if let Some((color, boxes)) = &background {
2288 if index % width as usize == 0 {
2289 while first < boxes.len() && boxes[first].3 <= y {
2290 first += 1;
2291 }
2292 last = last.max(first);
2293 while last < boxes.len() && boxes[last].2 < y + 1.0 {
2294 last += 1;
2295 }
2296 }
2297 let covered = boxes[first..last]
2298 .iter()
2299 .map(|&(left, right, top, bottom)| {
2300 let across = (right.min(x + 1.0) - left.max(x)).clamp(0.0, 1.0);
2301 let down = (bottom.min(y + 1.0) - top.max(y)).clamp(0.0, 1.0);
2302 across * down
2303 })
2304 .fold(0.0_f32, f32::max);
2305 let boxed = premultiplied(*color, (covered * 255.0).round() as u8);
2306 match &mut split_plane {
2307 Some(plane) => {
2308 for (channel, value) in boxed.iter().enumerate() {
2309 plane[index * 4 + channel] = (*value).min(255) as u8;
2310 }
2311 }
2312 None => pixel = boxed,
2313 }
2314 }
2315 let fade = look.fade(index);
2317 let boxes = pixel;
2318 if fade.is_some() {
2319 pixel = [0; 4];
2320 }
2321 for (stroke, band) in bands.iter().enumerate().rev() {
2322 if band[index] > 0 {
2323 pixel = over(premultiplied(look.band(stroke, index), band[index]), pixel);
2324 }
2325 }
2326 if text > 0 {
2327 let fill = look.fill(index, [x + 0.5 - margin_x, y + 0.5 - margin_y]);
2328 pixel = over(premultiplied(fill, text), pixel);
2329 }
2330 if let Some(colors) = colors {
2331 let color = &colors[index * 4..][..4];
2332 if color[3] > 0 {
2333 let opacity = u32::from(foreground[3]);
2334 pixel = over(
2335 std::array::from_fn(|channel| u32::from(color[channel]) * opacity / 255),
2336 pixel,
2337 );
2338 }
2339 }
2340 if let Some(fade) = fade {
2341 pixel = over(pixel.map(|value| (value * fade + 127) / 255), boxes);
2342 }
2343 for (channel, value) in pixel.iter().enumerate() {
2344 rgba8[index * 4 + channel] = (*value).min(255) as u8;
2345 }
2346 }
2347 (rgba8, split_plane)
2348}
2349
2350#[allow(
2356 clippy::cast_possible_truncation,
2357 clippy::cast_sign_loss,
2358 clippy::cast_precision_loss,
2359 clippy::cast_possible_wrap
2360)]
2361fn dilate(coverage: &[u8], width: u32, height: u32, radius: f32) -> Vec<u8> {
2362 let (width, height) = (width as usize, height as usize);
2363 let mut band = vec![0_u8; coverage.len()];
2364 let Some(ink) = ink_bounds(coverage, width, height) else {
2365 return band;
2366 };
2367 let disc = Disc::new(radius);
2368 let extent = disc.extent;
2369 let reach = Bounds {
2370 left: ink.left.saturating_sub(extent),
2371 top: ink.top.saturating_sub(extent),
2372 right: (ink.right + extent).min(width),
2373 bottom: (ink.bottom + extent).min(height),
2374 };
2375 let span = reach.right - reach.left;
2377 let mut window = WindowMax::default();
2378 let mut maxima = vec![0_u8; span];
2379 for (half, offsets) in &disc.runs {
2380 for source_y in ink.top..ink.bottom {
2381 let row = &coverage[source_y * width..(source_y + 1) * width];
2382 window.of(row, reach.left, span, *half, &mut maxima);
2383 for &offset in offsets {
2384 let Some(y) = source_y.checked_add_signed(-offset) else {
2385 continue;
2386 };
2387 if y < reach.top || y >= reach.bottom {
2388 continue;
2389 }
2390 let out = &mut band[y * width + reach.left..y * width + reach.right];
2391 for (pixel, highest) in out.iter_mut().zip(&maxima) {
2392 *pixel = (*pixel).max(*highest);
2393 }
2394 }
2395 }
2396 }
2397 for &(dx, dy, weight) in &disc.rim {
2398 for source_y in ink.top..ink.bottom {
2399 let Some(y) = source_y.checked_add_signed(-dy) else {
2400 continue;
2401 };
2402 if y >= height {
2403 continue;
2404 }
2405 let first = ink.left.max(usize::try_from(dx).unwrap_or(0));
2407 let last = ink
2408 .right
2409 .min(usize::try_from(width as isize + dx.min(0)).unwrap_or(0));
2410 if first >= last {
2411 continue;
2412 }
2413 let source = &coverage[source_y * width + first..source_y * width + last];
2414 let start = y * width + (first as isize - dx) as usize;
2415 let out = &mut band[start..start + source.len()];
2416 for (pixel, value) in out.iter_mut().zip(source) {
2417 *pixel = (*pixel).max((u32::from(*value) * weight / 255) as u8);
2418 }
2419 }
2420 }
2421 band
2422}
2423
2424#[derive(Copy, Clone, Debug, Eq, PartialEq)]
2426struct Bounds {
2427 left: usize,
2428 top: usize,
2429 right: usize,
2430 bottom: usize,
2431}
2432
2433fn ink_bounds(coverage: &[u8], width: usize, height: usize) -> Option<Bounds> {
2434 let mut bounds: Option<Bounds> = None;
2435 for y in 0..height {
2436 let row = &coverage[y * width..(y + 1) * width];
2437 let Some(left) = row.iter().position(|&value| value > 0) else {
2438 continue;
2439 };
2440 let right = row.iter().rposition(|&value| value > 0).unwrap_or(left) + 1;
2441 bounds = Some(bounds.map_or(
2442 Bounds {
2443 left,
2444 top: y,
2445 right,
2446 bottom: y + 1,
2447 },
2448 |bounds| Bounds {
2449 left: bounds.left.min(left),
2450 top: bounds.top,
2451 right: bounds.right.max(right),
2452 bottom: y + 1,
2453 },
2454 ));
2455 }
2456 bounds
2457}
2458
2459struct Disc {
2461 extent: usize,
2462 runs: Vec<(usize, Vec<isize>)>,
2464 rim: Vec<(isize, isize, u32)>,
2466}
2467
2468impl Disc {
2469 #[allow(
2471 clippy::cast_possible_truncation,
2472 clippy::cast_sign_loss,
2473 clippy::cast_precision_loss,
2474 clippy::cast_possible_wrap
2475 )]
2476 fn new(radius: f32) -> Self {
2477 let extent = disc_extent(radius) as isize;
2478 let mut runs: Vec<(usize, Vec<isize>)> = Vec::new();
2479 let mut rim = Vec::new();
2480 for dy in -extent..=extent {
2481 let mut half = None;
2482 for dx in -extent..=extent {
2483 let distance = ((dx * dx + dy * dy) as f32).sqrt();
2484 let weight = ((radius + 0.5 - distance).clamp(0.0, 1.0) * 255.0).round() as u32;
2485 match weight {
2486 0 => {}
2487 255 => half = Some(dx.unsigned_abs()),
2489 _ => rim.push((dx, dy, weight)),
2490 }
2491 }
2492 if let Some(half) = half {
2493 match runs.iter_mut().find(|(run, _)| *run == half) {
2494 Some((_, offsets)) => offsets.push(dy),
2495 None => runs.push((half, vec![dy])),
2496 }
2497 }
2498 }
2499 Self {
2500 extent: extent as usize,
2501 runs,
2502 rim,
2503 }
2504 }
2505}
2506
2507#[derive(Default)]
2509struct WindowMax {
2510 padded: Vec<u8>,
2511 from_start: Vec<u8>,
2512 to_end: Vec<u8>,
2513}
2514
2515impl WindowMax {
2516 fn of(&mut self, row: &[u8], left: usize, count: usize, half: usize, out: &mut [u8]) {
2518 let window = 2 * half + 1;
2519 let length = count + 2 * half;
2520 self.padded.clear();
2521 self.padded.resize(length, 0);
2522 let first_column = left.saturating_sub(half);
2524 let last_column = (left + count + half).min(row.len());
2525 if first_column < last_column {
2526 let at = first_column + half - left;
2527 self.padded[at..at + last_column - first_column]
2528 .copy_from_slice(&row[first_column..last_column]);
2529 }
2530 self.from_start.clear();
2531 self.from_start.resize(length, 0);
2532 self.to_end.clear();
2533 self.to_end.resize(length, 0);
2534 for block in (0..length).step_by(window) {
2535 let end = (block + window).min(length);
2536 let mut highest = 0;
2537 for index in block..end {
2538 highest = highest.max(self.padded[index]);
2539 self.from_start[index] = highest;
2540 }
2541 highest = 0;
2542 for index in (block..end).rev() {
2543 highest = highest.max(self.padded[index]);
2544 self.to_end[index] = highest;
2545 }
2546 }
2547 for (index, value) in out[..count].iter_mut().enumerate() {
2548 *value = self.to_end[index].max(self.from_start[index + window - 1]);
2549 }
2550 }
2551}
2552
2553#[cfg(test)]
2554mod tests;