1use std::cell::OnceCell;
15use std::cmp::Ordering;
16use std::collections::{BTreeMap, HashMap};
17use std::fmt;
18use std::ops::Range;
19use std::sync::Arc;
20
21use ab_glyph_rasterizer::{Point, Rasterizer, point};
22use alpha2_media::{
23 EmojiPolicy, HostTexture, HostTexturePixels, Premultiplication, RasterColorSpace, TextAlign,
24 TextTexture,
25};
26use rustybuzz::ttf_parser::{self, GlyphId, OutlineBuilder};
27use rustybuzz::{Direction, UnicodeBuffer};
28use unicode_bidi::ParagraphBidiInfo;
29use unicode_linebreak::BreakOpportunity;
30use unicode_script::{Script, UnicodeScript};
31use unicode_segmentation::UnicodeSegmentation;
32
33mod color;
34mod unicode_tables;
35
36const MAX_TEXT_BYTES: usize = 64 * 1024;
37const MAX_FONT_SIZE: f32 = 2_048.0;
38const MAX_LINE_HEIGHT: f32 = 10.0;
39const MAX_OUTLINE_WIDTH: f32 = 64.0;
40const MAX_BACKGROUND_PADDING: f32 = 256.0;
41
42#[derive(Clone, Debug)]
44pub struct TextFont {
45 pub family: String,
47 pub bytes: Arc<[u8]>,
49 pub face_index: u32,
51}
52
53#[derive(Clone, Debug, PartialEq)]
55pub struct TextRasterRequest {
56 pub text: String,
58 pub font_size: f32,
60 pub max_width: f32,
62 pub line_height: f32,
64 pub align: TextAlign,
66 pub color: [u8; 4],
71 pub emoji_policy: EmojiPolicy,
73 pub content_version: u64,
75 pub outline: Option<TextOutline>,
77 pub background: Option<TextBackground>,
79 pub font_family: Option<String>,
83}
84
85#[derive(Copy, Clone, Debug, PartialEq)]
92pub struct TextOutline {
93 pub color: [u8; 4],
95 pub width: f32,
97}
98
99#[derive(Copy, Clone, Debug, PartialEq)]
102pub struct TextBackground {
103 pub color: [u8; 4],
105 pub padding: f32,
107}
108
109#[derive(Clone, Debug, PartialEq)]
111pub struct TextRasterResult {
112 pub descriptor: TextTexture,
114 pub pixels: HostTexturePixels,
116}
117
118#[derive(Clone, Debug, PartialEq)]
124pub struct TextMeasure {
125 pub box_width: u32,
128 pub box_height: u32,
131 pub decoration_margin: u32,
133 pub content: [f32; 4],
138 pub ink: Option<[f32; 4]>,
145 pub line_count: u32,
147}
148
149#[derive(Clone, Debug, Eq, PartialEq)]
151#[non_exhaustive]
152pub enum TextRasterError {
153 NoFonts,
155 InvalidFont(String),
157 InvalidRequest,
159 TooLarge,
161}
162
163impl fmt::Display for TextRasterError {
164 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self {
166 Self::NoFonts => formatter.write_str("text rasterizer requires at least one font"),
167 Self::InvalidFont(error) => write!(formatter, "invalid text font: {error}"),
168 Self::InvalidRequest => formatter.write_str("invalid text raster request"),
169 Self::TooLarge => formatter.write_str("text raster result is too large"),
170 }
171 }
172}
173
174impl std::error::Error for TextRasterError {}
175
176#[derive(Clone, Debug)]
177struct LoadedFont {
178 source: TextFont,
179 family: Option<String>,
181}
182
183impl LoadedFont {
184 fn parse(source: TextFont) -> Result<Self, TextRasterError> {
188 check_font(&source.bytes, source.face_index)?;
189 let family = font_family(&source.bytes, source.face_index);
190 Ok(Self { source, family })
191 }
192}
193
194pub fn check_font(bytes: &[u8], face_index: u32) -> Result<(), TextRasterError> {
202 ttf_parser::Face::parse(bytes, face_index)
203 .map(|_| ())
204 .map_err(|error| TextRasterError::InvalidFont(error.to_string()))
205}
206
207#[must_use]
212pub fn font_family(bytes: &[u8], face_index: u32) -> Option<String> {
213 use rustybuzz::ttf_parser::{Language, name_id};
214 let face = rustybuzz::ttf_parser::Face::parse(bytes, face_index).ok()?;
215 let names = face.names();
216 [name_id::TYPOGRAPHIC_FAMILY, name_id::FAMILY]
217 .into_iter()
218 .find_map(|id| {
219 let decoded = || {
220 names
221 .into_iter()
222 .filter(move |name| name.name_id == id)
223 .filter_map(|name| {
224 let text = name.to_string()?;
225 (!text.trim().is_empty()).then_some((name.language(), text))
226 })
227 };
228 decoded()
229 .find(|(language, _)| *language == Language::English_UnitedStates)
230 .or_else(|| decoded().next())
231 .map(|(_, text)| text)
232 })
233}
234
235pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
238
239const CACHE_ENTRY_OVERHEAD: usize = 256;
242
243#[derive(Debug)]
250pub struct TextRasterizer {
251 fonts: Vec<LoadedFont>,
252 cache: ResultCache,
253}
254
255#[derive(Debug)]
258struct ResultCache {
259 entries: HashMap<u64, CacheEntry>,
260 by_use: BTreeMap<u64, u64>,
262 next_use: u64,
263 bytes: usize,
264 budget: usize,
265}
266
267#[derive(Debug)]
268struct CacheEntry {
269 result: Arc<TextRasterResult>,
270 last_use: u64,
271 bytes: usize,
272}
273
274impl ResultCache {
275 fn new(budget: usize) -> Self {
276 Self {
277 entries: HashMap::new(),
278 by_use: BTreeMap::new(),
279 next_use: 0,
280 bytes: 0,
281 budget,
282 }
283 }
284
285 fn get(&mut self, version: u64) -> Option<Arc<TextRasterResult>> {
286 let entry = self.entries.get_mut(&version)?;
287 self.by_use.remove(&entry.last_use);
288 entry.last_use = self.next_use;
289 self.by_use.insert(self.next_use, version);
290 self.next_use += 1;
291 Some(Arc::clone(&entry.result))
292 }
293
294 fn insert(&mut self, version: u64, result: &Arc<TextRasterResult>) {
297 self.remove(version);
298 let bytes = result
299 .pixels
300 .rgba8
301 .len()
302 .saturating_add(CACHE_ENTRY_OVERHEAD);
303 if bytes > self.budget {
304 return;
305 }
306 self.bytes += bytes;
307 self.entries.insert(
308 version,
309 CacheEntry {
310 result: Arc::clone(result),
311 last_use: self.next_use,
312 bytes,
313 },
314 );
315 self.by_use.insert(self.next_use, version);
316 self.next_use += 1;
317 self.evict();
318 }
319
320 fn remove(&mut self, version: u64) {
321 if let Some(entry) = self.entries.remove(&version) {
322 self.by_use.remove(&entry.last_use);
323 self.bytes -= entry.bytes;
324 }
325 }
326
327 fn clear(&mut self) {
328 self.entries.clear();
329 self.by_use.clear();
330 self.bytes = 0;
331 }
332
333 fn set_budget(&mut self, budget: usize) {
334 self.budget = budget;
335 self.evict();
336 }
337
338 fn evict(&mut self) {
339 while self.bytes > self.budget {
340 let Some((_, version)) = self.by_use.pop_first() else {
341 break;
342 };
343 if let Some(entry) = self.entries.remove(&version) {
344 self.bytes -= entry.bytes;
345 }
346 }
347 }
348}
349
350impl TextRasterizer {
351 #[must_use]
353 pub fn empty() -> Self {
354 Self {
355 fonts: Vec::new(),
356 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
357 }
358 }
359
360 pub fn new(fonts: Vec<TextFont>) -> Result<Self, TextRasterError> {
365 if fonts.is_empty() {
366 return Err(TextRasterError::NoFonts);
367 }
368 let loaded = fonts
369 .into_iter()
370 .map(LoadedFont::parse)
371 .collect::<Result<Vec<_>, _>>()?;
372 Ok(Self {
373 fonts: loaded,
374 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
375 })
376 }
377
378 pub fn add_font(&mut self, source: TextFont) -> Result<(), TextRasterError> {
386 self.fonts.push(LoadedFont::parse(source)?);
387 self.cache.clear();
388 Ok(())
389 }
390
391 #[must_use]
393 pub fn font_count(&self) -> usize {
394 self.fonts.len()
395 }
396
397 pub fn rasterize(
403 &mut self,
404 request: &TextRasterRequest,
405 ) -> Result<Arc<TextRasterResult>, TextRasterError> {
406 validate_request(request)?;
407 if self.fonts.is_empty() {
408 return Err(TextRasterError::NoFonts);
409 }
410 if let Some(cached) = self.cache.get(request.content_version) {
411 return Ok(cached);
412 }
413 let result = Arc::new(self.rasterize_uncached(request)?);
414 self.cache.insert(request.content_version, &result);
415 Ok(result)
416 }
417
418 pub fn invalidate(&mut self, content_version: u64) {
420 self.cache.remove(content_version);
421 }
422
423 pub fn set_cache_budget(&mut self, bytes: usize) {
428 self.cache.set_budget(bytes);
429 }
430
431 #[must_use]
433 pub fn cache_bytes(&self) -> usize {
434 self.cache.bytes
435 }
436
437 pub fn clear(&mut self) {
439 self.cache.clear();
440 }
441
442 #[must_use]
444 pub fn cache_len(&self) -> usize {
445 self.cache.entries.len()
446 }
447
448 #[allow(clippy::cast_precision_loss)]
455 pub fn measure(&self, request: &TextRasterRequest) -> Result<TextMeasure, TextRasterError> {
456 validate_request(request)?;
457 if self.fonts.is_empty() {
458 return Err(TextRasterError::NoFonts);
459 }
460 let faces = Faces::new(&self.fonts)?;
461 let layout = self.lay_out(&faces, request)?;
462 let mut ink = None;
463 layout.each_glyph(
464 &faces,
465 request,
466 &mut HashMap::new(),
467 |drawing, origin, scale, _| {
468 if let Some([x_min, y_min, x_max, y_max]) = drawing.ink_bounds() {
469 include(
470 &mut ink,
471 [
472 origin[0] + x_min * scale,
473 origin[1] - y_max * scale,
474 origin[0] + x_max * scale,
475 origin[1] - y_min * scale,
476 ],
477 );
478 }
479 },
480 );
481 let band = request.outline.map_or(0.0, |outline| outline.width + 1.0);
484 ink = ink.map(|bounds| grown(bounds, band));
485 let padding = request.background.map(|background| background.padding);
486 let mut content = None;
487 for (index, line) in layout.lines.iter().enumerate() {
488 let left = layout.line_offset(line, request.align);
489 let top = index as f32 * layout.line_advance;
490 include(&mut content, [left, 0.0, left + line.width, layout.height]);
491 if let (Some(padding), true) = (padding, line.width > 0.0) {
492 include(
493 &mut ink,
494 grown(
495 [left, top, left + line.width, top + request.font_size],
496 padding,
497 ),
498 );
499 }
500 }
501 let content = content.unwrap_or([0.0, 0.0, 0.0, layout.height]);
502 Ok(TextMeasure {
503 box_width: layout.box_width,
504 box_height: layout.box_height,
505 decoration_margin: request.decoration_margin(),
506 content: grown(content, request.decoration_margin() as f32),
507 ink,
508 line_count: layout.line_count,
509 })
510 }
511
512 #[allow(clippy::cast_precision_loss)]
514 fn lay_out(
515 &self,
516 faces: &Faces<'_>,
517 request: &TextRasterRequest,
518 ) -> Result<Layout, TextRasterError> {
519 let line_advance = request.font_size * request.line_height;
520 let order = self.font_order(request.font_family.as_deref());
521 let mut lines = Vec::new();
522 let mut kept = 1;
525 for paragraph in normalize(&request.text, request.emoji_policy).split('\n') {
526 for line in layout_paragraph(
527 faces,
528 paragraph,
529 request.font_size,
530 request.max_width,
531 &order,
532 request.emoji_policy,
533 ) {
534 lines.push(line);
535 if lines.last().is_some_and(|line| line.has_glyphs) {
536 kept = lines.len();
537 }
538 }
539 }
540 lines.truncate(kept);
541 let line_count = u32::try_from(lines.len()).map_err(|_| TextRasterError::TooLarge)?;
542 let width = if request.max_width > 0.0 {
543 request.max_width
544 } else {
545 lines.iter().map(|line| line.width).fold(0.0, f32::max)
546 }
547 .max(1.0);
548 let height =
550 ((line_count - 1) as f32 * line_advance + line_advance.max(request.font_size)).max(1.0);
551 Ok(Layout {
552 box_width: checked_dimension(width)?,
553 box_height: checked_dimension(height)?,
554 lines,
555 line_count,
556 line_advance,
557 ascent: request.font_size * 0.8,
558 descent: request.font_size * 0.2,
559 width,
560 height,
561 })
562 }
563
564 #[allow(
565 clippy::cast_possible_truncation,
566 clippy::cast_precision_loss,
567 clippy::too_many_lines
568 )]
569 fn rasterize_uncached(
570 &self,
571 request: &TextRasterRequest,
572 ) -> Result<TextRasterResult, TextRasterError> {
573 let faces = Faces::new(&self.fonts)?;
574 let layout = self.lay_out(&faces, request)?;
575 let (box_width, box_height) = (layout.box_width, layout.box_height);
576 let mut drawings = HashMap::<(usize, u16), Drawing>::new();
577 let mut placed = Vec::new();
578 let (mut overflow_x, mut overflow_y) = (0_i64, 0_i64);
580 let (mut ink_ascent, mut ink_descent) = (layout.ascent, layout.descent);
581 layout.each_glyph(
582 &faces,
583 request,
584 &mut drawings,
585 |drawing, origin, scale, glyph| {
586 let Some(bounds) = drawing.bounds() else {
587 return;
588 };
589 let pixels = PixelBox::of(bounds, origin, scale);
590 overflow_x = overflow_x
591 .max(pixels.left.saturating_neg())
592 .max(pixels.right.saturating_sub(i64::from(box_width)));
593 overflow_y = overflow_y
594 .max(pixels.top.saturating_neg())
595 .max(pixels.bottom.saturating_sub(i64::from(box_height)));
596 ink_ascent = ink_ascent.max(bounds[3] * scale + glyph.y_offset);
597 ink_descent = ink_descent.max(-(bounds[1] * scale + glyph.y_offset));
598 placed.push(((glyph.font, glyph.glyph_index), origin, scale, pixels));
599 },
600 );
601 let (outline, padding) = decoration_extents(request);
604 let margin = |overflow: i64| -> Result<u32, TextRasterError> {
605 u32::try_from(overflow)
606 .map_err(|_| TextRasterError::TooLarge)?
607 .checked_add(outline)
608 .map(|margin| margin.max(outline.max(padding)))
609 .ok_or(TextRasterError::TooLarge)
610 };
611 let (margin_x, margin_y) = (margin(overflow_x)?, margin(overflow_y)?);
612 let width = margin_x
613 .checked_mul(2)
614 .and_then(|margins| box_width.checked_add(margins))
615 .ok_or(TextRasterError::TooLarge)?;
616 let height = margin_y
617 .checked_mul(2)
618 .and_then(|margins| box_height.checked_add(margins))
619 .ok_or(TextRasterError::TooLarge)?;
620 if width > 16_384 || height > 16_384 {
621 return Err(TextRasterError::TooLarge);
622 }
623 let pixel_count = usize::try_from(width)
624 .ok()
625 .and_then(|width| {
626 usize::try_from(height)
627 .ok()
628 .and_then(|height| width.checked_mul(height))
629 })
630 .ok_or(TextRasterError::TooLarge)?;
631 let mut coverage = vec![0_u8; pixel_count];
632 let mut colors: Option<Vec<u8>> = None;
634 let mut line_boxes = Vec::with_capacity(layout.lines.len());
635 for (index, line) in layout.lines.iter().enumerate() {
636 if line.width > 0.0 {
637 line_boxes.push((
638 layout.line_offset(line, request.align),
639 line.width,
640 index as f32 * layout.line_advance,
641 ));
642 }
643 }
644 for (key, origin, scale, pixels) in placed {
645 let (origin_x, origin_y) = (
646 pixels.left + i64::from(margin_x),
647 pixels.top + i64::from(margin_y),
648 );
649 let glyph_width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
650 match &drawings[&key] {
651 Drawing::Outline(outline) => place_glyph(
652 &mut coverage,
653 width,
654 height,
655 origin_x,
656 origin_y,
657 glyph_width,
658 &outline.draw(origin, scale, pixels),
659 ),
660 drawing => place_color_glyph(
661 colors.get_or_insert_with(|| vec![0; pixel_count * 4]),
662 width,
663 height,
664 origin_x,
665 origin_y,
666 glyph_width,
667 &drawing.draw_color(origin, scale, pixels),
668 ),
669 }
670 }
671 let rgba8 = compose(
672 request,
673 &coverage,
674 colors.as_deref(),
675 width,
676 [margin_x, margin_y],
677 &line_boxes,
678 );
679 let texture = HostTexture {
680 content_version: request.content_version,
681 width,
682 height,
683 premultiplication: Premultiplication::Premultiplied,
684 color_space: RasterColorSpace::Srgb,
685 };
686 Ok(TextRasterResult {
687 descriptor: TextTexture {
688 texture: texture.clone(),
689 layout_x: margin_x as f32,
690 layout_y: margin_y as f32,
691 layout_width: layout.width,
692 layout_height: layout.height,
693 first_baseline: layout.ascent,
694 ascent: ink_ascent,
695 descent: ink_descent,
696 line_count: layout.line_count,
697 },
698 pixels: HostTexturePixels { texture, rgba8 },
699 })
700 }
701
702 fn font_order(&self, family: Option<&str>) -> Vec<usize> {
705 let preferred = |font: &LoadedFont| {
706 family.is_some_and(|wanted| {
707 font.family
708 .as_deref()
709 .is_some_and(|name| name.eq_ignore_ascii_case(wanted))
710 })
711 };
712 let (mut order, rest): (Vec<usize>, Vec<usize>) =
713 (0..self.fonts.len()).partition(|&index| preferred(&self.fonts[index]));
714 order.extend(rest);
715 order
716 }
717}
718
719struct Faces<'a> {
722 parsed: Vec<ttf_parser::Face<'a>>,
723 shapers: Vec<OnceCell<rustybuzz::Face<'a>>>,
724 color: Vec<bool>,
726}
727
728impl<'a> Faces<'a> {
729 fn new(fonts: &'a [LoadedFont]) -> Result<Self, TextRasterError> {
730 let parsed = fonts
731 .iter()
732 .map(|font| {
733 ttf_parser::Face::parse(&font.source.bytes, font.source.face_index)
734 .ok()
735 .filter(|face| face.units_per_em() > 0)
736 .ok_or_else(|| TextRasterError::InvalidFont(font.source.family.clone()))
737 })
738 .collect::<Result<Vec<_>, _>>()?;
739 Ok(Self {
740 shapers: parsed.iter().map(|_| OnceCell::new()).collect(),
741 color: parsed
742 .iter()
743 .map(|face| {
744 let tables = face.tables();
745 tables.colr.is_some() || tables.cbdt.is_some() || tables.sbix.is_some()
746 })
747 .collect(),
748 parsed,
749 })
750 }
751
752 fn shaper(&self, font: usize) -> &rustybuzz::Face<'a> {
753 self.shapers[font].get_or_init(|| rustybuzz::Face::from_face(self.parsed[font].clone()))
754 }
755
756 #[allow(clippy::cast_precision_loss)]
757 fn units_per_em(&self, font: usize) -> f32 {
758 f32::from(self.parsed[font].units_per_em())
759 }
760
761 fn has(&self, font: usize, character: char) -> bool {
763 self.parsed[font]
764 .glyph_index(character)
765 .is_some_and(|glyph| glyph.0 != 0)
766 }
767
768 fn covers(&self, font: usize, grapheme: &str) -> bool {
770 grapheme
771 .chars()
772 .filter(|&character| !is_default_ignorable(character))
773 .all(|character| self.has(font, character))
774 }
775}
776
777fn normalize(text: &str, policy: EmojiPolicy) -> String {
784 let mut normalized = String::with_capacity(text.len());
785 for grapheme in text.graphemes(true) {
786 if policy == EmojiPolicy::Replace && is_emoji(grapheme) {
787 normalized.push('\u{fffd}');
788 } else if grapheme == "\r\n" {
789 normalized.push('\n');
790 } else {
791 normalized.extend(grapheme.chars().map(|character| match character {
792 '\r'
793 | '\u{b}'
794 | '\u{c}'
795 | '\u{1c}'..='\u{1e}'
796 | '\u{85}'
797 | '\u{2028}'
798 | '\u{2029}' => '\n',
799 '\t' => ' ',
800 other => other,
801 }));
802 }
803 }
804 normalized
805}
806
807fn is_emoji(grapheme: &str) -> bool {
813 if grapheme.contains('\u{fe0e}') {
814 return false;
815 }
816 let selected = grapheme.contains('\u{fe0f}');
817 grapheme.chars().any(|character| {
818 in_table(unicode_tables::EMOJI_PRESENTATION, character)
819 || (selected
820 && (character == '\u{20e3}'
821 || in_table(unicode_tables::EXTENDED_PICTOGRAPHIC, character)))
822 })
823}
824
825fn is_default_ignorable(character: char) -> bool {
826 in_table(unicode_tables::DEFAULT_IGNORABLE, character)
827}
828
829fn in_table(table: &[(u32, u32)], character: char) -> bool {
830 let code = u32::from(character);
831 table
832 .binary_search_by(|&(first, last)| {
833 if last < code {
834 Ordering::Less
835 } else if first > code {
836 Ordering::Greater
837 } else {
838 Ordering::Equal
839 }
840 })
841 .is_ok()
842}
843
844fn hangs(character: char) -> bool {
848 character.is_whitespace()
849 && !matches!(character, '\u{a0}' | '\u{2007}' | '\u{202f}' | '\u{1680}')
850}
851
852fn is_hangul(character: char) -> bool {
853 matches!(
854 character,
855 '\u{1100}'..='\u{11ff}'
856 | '\u{3130}'..='\u{318f}'
857 | '\u{a960}'..='\u{a97f}'
858 | '\u{ac00}'..='\u{d7a3}'
859 | '\u{d7b0}'..='\u{d7ff}'
860 )
861}
862
863struct Layout {
866 lines: Vec<LaidLine>,
867 line_count: u32,
868 line_advance: f32,
870 ascent: f32,
872 descent: f32,
873 width: f32,
876 height: f32,
877 box_width: u32,
879 box_height: u32,
880}
881
882impl Layout {
883 fn line_offset(&self, line: &LaidLine, align: TextAlign) -> f32 {
885 match (align, line.rtl) {
886 (TextAlign::Center, _) => (self.width - line.width) * 0.5,
887 (TextAlign::Start, false) | (TextAlign::End, true) => 0.0,
888 (TextAlign::End, false) | (TextAlign::Start, true) => self.width - line.width,
889 }
890 }
891
892 #[allow(clippy::cast_precision_loss)]
895 fn each_glyph(
896 &self,
897 faces: &Faces<'_>,
898 request: &TextRasterRequest,
899 drawings: &mut HashMap<(usize, u16), Drawing>,
900 mut place: impl FnMut(&Drawing, [f32; 2], f32, &PositionedGlyph),
901 ) {
902 for (index, line) in self.lines.iter().enumerate() {
903 let offset = self.line_offset(line, request.align);
904 let baseline = index as f32 * self.line_advance + self.ascent;
905 for glyph in &line.glyphs {
906 let drawing = drawings
907 .entry((glyph.font, glyph.glyph_index))
908 .or_insert_with(|| Drawing::of(faces, glyph.font, glyph.glyph_index, request));
909 let scale = request.font_size / faces.units_per_em(glyph.font);
910 let origin = [glyph.x + glyph.x_offset + offset, baseline - glyph.y_offset];
911 place(drawing, origin, scale, glyph);
912 }
913 }
914 }
915}
916
917fn include(bounds: &mut Option<[f32; 4]>, [left, top, right, bottom]: [f32; 4]) {
919 let held = bounds.get_or_insert([left, top, right, bottom]);
920 *held = [
921 held[0].min(left),
922 held[1].min(top),
923 held[2].max(right),
924 held[3].max(bottom),
925 ];
926}
927
928fn grown([left, top, right, bottom]: [f32; 4], by: f32) -> [f32; 4] {
930 [left - by, top - by, right + by, bottom + by]
931}
932
933struct LaidLine {
935 glyphs: Vec<PositionedGlyph>,
937 width: f32,
939 rtl: bool,
941 has_glyphs: bool,
943}
944
945fn layout_paragraph(
949 faces: &Faces<'_>,
950 paragraph: &str,
951 font_size: f32,
952 max_width: f32,
953 order: &[usize],
954 policy: EmojiPolicy,
955) -> Vec<LaidLine> {
956 let bidi = ParagraphBidiInfo::new(paragraph, None);
957 let rtl = bidi.paragraph_level.is_rtl();
958 let glyphs = shape_paragraph(faces, paragraph, &bidi, font_size, order, policy);
959 let clusters = clusters(&glyphs, paragraph);
960 break_lines(&clusters, max_width)
961 .into_iter()
962 .map(|range| {
963 let has_glyphs = !range.is_empty();
964 let mut end = range.end;
966 while end > range.start && clusters[end - 1].hangs {
967 end -= 1;
968 }
969 let mut line = LaidLine {
970 glyphs: Vec::new(),
971 width: 0.0,
972 rtl,
973 has_glyphs,
974 };
975 if end == range.start {
976 return line;
977 }
978 let bytes = clusters[range.start].start
979 ..clusters
980 .get(range.end)
981 .map_or(paragraph.len(), |next| next.start);
982 let levels = bidi.reordered_levels(bytes);
983 let mut x = 0.0_f32;
984 for index in visual_order(
985 clusters[range.start].glyphs.start..clusters[end - 1].glyphs.end,
986 |index| levels[glyphs[index].cluster].number(),
987 ) {
988 let glyph = &glyphs[index];
989 line.glyphs.push(PositionedGlyph {
990 font: glyph.font,
991 glyph_index: glyph.glyph_index,
992 x,
993 x_offset: glyph.x_offset,
994 y_offset: glyph.y_offset,
995 });
996 x += glyph.advance;
997 }
998 line.width = x;
999 line
1000 })
1001 .collect()
1002}
1003
1004fn visual_order(range: Range<usize>, level: impl Fn(usize) -> u8) -> Vec<usize> {
1009 let mut order = range.collect::<Vec<_>>();
1010 let levels = order.iter().map(|&index| level(index)).collect::<Vec<_>>();
1011 let highest = levels.iter().copied().max().unwrap_or(0);
1012 let Some(lowest_odd) = levels.iter().copied().filter(|level| level % 2 == 1).min() else {
1013 return order;
1014 };
1015 let mut current = levels;
1016 for at_least in (lowest_odd..=highest).rev() {
1017 let mut start = 0;
1018 while start < order.len() {
1019 if current[start] < at_least {
1020 start += 1;
1021 continue;
1022 }
1023 let end = (start..order.len())
1024 .find(|&index| current[index] < at_least)
1025 .unwrap_or(order.len());
1026 order[start..end].reverse();
1027 current[start..end].reverse();
1028 start = end;
1029 }
1030 }
1031 order
1032}
1033
1034#[derive(Clone, Debug, PartialEq)]
1037struct Run {
1038 range: Range<usize>,
1039 font: usize,
1040 script: Option<Script>,
1041 rtl: bool,
1042}
1043
1044fn itemize(
1055 faces: &Faces<'_>,
1056 paragraph: &str,
1057 bidi: &ParagraphBidiInfo<'_>,
1058 order: &[usize],
1059 policy: EmojiPolicy,
1060) -> Vec<Run> {
1061 let first = order.first().copied().unwrap_or(0);
1062 let (color, text): (Vec<usize>, Vec<usize>) =
1063 order.iter().partition(|&&font| faces.color[font]);
1064 let color_first = color.iter().chain(&text).copied().collect::<Vec<_>>();
1065 let text_first = text.iter().chain(&color).copied().collect::<Vec<_>>();
1066 let fits = |font: usize, color_wanted: bool| {
1068 faces.color[font] == color_wanted
1069 || if color_wanted {
1070 color.is_empty()
1071 } else {
1072 text.is_empty()
1073 }
1074 };
1075 let mut graphemes = Vec::<(usize, usize, Option<Script>, bool)>::new();
1076 let mut current = None;
1077 for (start, grapheme) in paragraph.grapheme_indices(true) {
1078 let script = grapheme
1079 .chars()
1080 .map(|character| character.script())
1081 .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown));
1082 let wants_color = if grapheme.contains('\u{fe0e}') {
1084 Some(false)
1085 } else if is_emoji(grapheme) {
1086 Some(policy == EmojiPolicy::Color)
1087 } else {
1088 None
1089 };
1090 let order = match wants_color {
1091 Some(true) => color_first.as_slice(),
1092 Some(false) => text_first.as_slice(),
1093 None => order,
1094 };
1095 let stays = current.filter(|&font| {
1096 script.is_none()
1097 && wants_color.is_none_or(|wanted| fits(font, wanted))
1098 && faces.covers(font, grapheme)
1099 });
1100 let font = stays
1101 .or_else(|| {
1102 order
1103 .iter()
1104 .copied()
1105 .find(|&font| faces.covers(font, grapheme))
1106 })
1107 .or_else(|| {
1108 let base = grapheme
1109 .chars()
1110 .find(|&character| !is_default_ignorable(character))?;
1111 order.iter().copied().find(|&font| faces.has(font, base))
1112 })
1113 .or(current)
1114 .unwrap_or(first);
1115 current = Some(font);
1116 graphemes.push((start, font, script, bidi.levels[start].is_rtl()));
1117 }
1118 let leading = graphemes.iter().find_map(|grapheme| grapheme.2);
1119 let mut before = leading;
1120 for grapheme in &mut graphemes {
1121 match grapheme.2 {
1122 Some(script) => before = Some(script),
1123 None => grapheme.2 = before,
1124 }
1125 }
1126 let mut runs = Vec::<Run>::new();
1127 for (index, &(start, font, script, rtl)) in graphemes.iter().enumerate() {
1128 let end = graphemes
1129 .get(index + 1)
1130 .map_or(paragraph.len(), |next| next.0);
1131 match runs.last_mut() {
1132 Some(run) if run.font == font && run.script == script && run.rtl == rtl => {
1133 run.range.end = end;
1134 }
1135 _ => runs.push(Run {
1136 range: start..end,
1137 font,
1138 script,
1139 rtl,
1140 }),
1141 }
1142 }
1143 runs
1144}
1145
1146fn shaping_script(script: Script) -> Option<rustybuzz::Script> {
1148 let code: [u8; 4] = script.short_name().as_bytes().try_into().ok()?;
1149 rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&code))
1150}
1151
1152fn shape_paragraph(
1155 faces: &Faces<'_>,
1156 paragraph: &str,
1157 bidi: &ParagraphBidiInfo<'_>,
1158 font_size: f32,
1159 order: &[usize],
1160 policy: EmojiPolicy,
1161) -> Vec<ShapedGlyph> {
1162 let mut glyphs = Vec::new();
1163 for run in itemize(faces, paragraph, bidi, order, policy) {
1164 let face = faces.shaper(run.font);
1165 let units = faces.units_per_em(run.font);
1166 let mut buffer = UnicodeBuffer::new();
1167 buffer.push_str(¶graph[run.range.clone()]);
1168 buffer.set_pre_context(¶graph[..run.range.start]);
1169 buffer.set_post_context(¶graph[run.range.end..]);
1170 buffer.set_direction(if run.rtl {
1171 Direction::RightToLeft
1172 } else {
1173 Direction::LeftToRight
1174 });
1175 match run.script.and_then(shaping_script) {
1176 Some(script) => buffer.set_script(script),
1177 None => buffer.guess_segment_properties(),
1178 }
1179 let shaped = rustybuzz::shape(face, &[], buffer);
1180 let first = glyphs.len();
1181 for (info, position) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) {
1182 #[allow(clippy::cast_precision_loss)]
1183 glyphs.push(ShapedGlyph {
1184 font: run.font,
1185 glyph_index: u16::try_from(info.glyph_id)
1188 .ok()
1189 .filter(|&glyph| glyph < face.number_of_glyphs())
1190 .unwrap_or(0),
1191 cluster: run.range.start + usize::try_from(info.cluster).unwrap_or(0),
1192 advance: position.x_advance as f32 * font_size / units,
1193 x_offset: position.x_offset as f32 * font_size / units,
1194 y_offset: position.y_offset as f32 * font_size / units,
1195 });
1196 }
1197 if run.rtl {
1200 glyphs[first..].reverse();
1201 }
1202 }
1203 glyphs
1204}
1205
1206#[derive(Copy, Clone, Debug)]
1207struct ShapedGlyph {
1208 font: usize,
1209 glyph_index: u16,
1210 cluster: usize,
1212 advance: f32,
1213 x_offset: f32,
1216 y_offset: f32,
1217}
1218
1219#[derive(Clone, Debug, PartialEq)]
1221struct Cluster {
1222 glyphs: Range<usize>,
1223 start: usize,
1225 width: f32,
1226 hangs: bool,
1228 break_before: bool,
1230}
1231
1232fn clusters(glyphs: &[ShapedGlyph], paragraph: &str) -> Vec<Cluster> {
1236 let mut opportunities = unicode_linebreak::linebreaks(paragraph)
1237 .filter(|&(at, kind)| {
1238 kind == BreakOpportunity::Allowed
1239 && !(paragraph[..at].chars().next_back().is_some_and(is_hangul)
1240 && paragraph[at..].chars().next().is_some_and(is_hangul))
1241 })
1242 .map(|(at, _)| at)
1243 .peekable();
1244 let mut clusters = Vec::<Cluster>::new();
1245 for (index, glyph) in glyphs.iter().enumerate() {
1246 match clusters.last_mut() {
1247 Some(cluster) if cluster.start == glyph.cluster => {
1248 cluster.glyphs.end = index + 1;
1249 cluster.width += glyph.advance;
1250 }
1251 _ => clusters.push(Cluster {
1252 glyphs: index..index + 1,
1253 start: glyph.cluster,
1254 width: glyph.advance,
1255 hangs: false,
1256 break_before: false,
1257 }),
1258 }
1259 }
1260 for index in 0..clusters.len() {
1261 let start = clusters[index].start;
1262 let end = clusters
1263 .get(index + 1)
1264 .map(|next| next.start)
1265 .filter(|&end| end > start)
1266 .unwrap_or_else(|| {
1267 paragraph[start..]
1268 .chars()
1269 .next()
1270 .map_or(start, |character| start + character.len_utf8())
1271 });
1272 clusters[index].hangs = paragraph[start..end].chars().all(hangs);
1273 let mut found = false;
1274 while let Some(&at) = opportunities.peek() {
1275 if at > start {
1276 break;
1277 }
1278 opportunities.next();
1279 found = true;
1280 }
1281 clusters[index].break_before = index > 0 && found;
1282 }
1283 clusters
1284}
1285
1286fn break_lines(clusters: &[Cluster], max_width: f32) -> Vec<Range<usize>> {
1293 let mut lines = Vec::new();
1294 let mut start = 0;
1295 let mut x = 0.0_f32;
1296 let mut opportunity: Option<(usize, f32)> = None;
1298 for (index, cluster) in clusters.iter().enumerate() {
1299 if cluster.break_before && index > start {
1300 opportunity = Some((index, x));
1301 }
1302 if max_width > 0.0 && !cluster.hangs && index > start && x + cluster.width > max_width {
1303 let (end, width) = opportunity.unwrap_or((index, x));
1304 lines.push(start..end);
1305 start = end;
1306 x -= width;
1307 opportunity = None;
1308 if index > start && x + cluster.width > max_width {
1311 lines.push(start..index);
1312 start = index;
1313 x = 0.0;
1314 }
1315 }
1316 x += cluster.width;
1317 }
1318 lines.push(start..clusters.len());
1319 lines
1320}
1321
1322#[derive(Copy, Clone, Debug)]
1323struct PositionedGlyph {
1324 font: usize,
1325 glyph_index: u16,
1326 x: f32,
1328 x_offset: f32,
1330 y_offset: f32,
1331}
1332
1333enum Drawing {
1336 Outline(Outline),
1337 Painting(color::Painting),
1338 Bitmap(color::Bitmap),
1339}
1340
1341impl Drawing {
1342 fn of(faces: &Faces<'_>, font: usize, glyph: u16, request: &TextRasterRequest) -> Self {
1346 let face = &faces.parsed[font];
1347 if request.emoji_policy == EmojiPolicy::Color && faces.color[font] {
1348 let [red, green, blue, _] = request.color;
1349 if let Some(painting) = color::Painting::of(face, glyph, [red, green, blue, 255]) {
1350 return Self::Painting(painting);
1351 }
1352 if let Some(bitmap) = color::Bitmap::of(face, glyph, request.font_size) {
1353 return Self::Bitmap(bitmap);
1354 }
1355 }
1356 Self::Outline(Outline::of(face, glyph))
1357 }
1358
1359 fn bounds(&self) -> Option<[f32; 4]> {
1362 match self {
1363 Self::Outline(outline) => outline.bounds,
1364 Self::Painting(painting) => painting.bounds(),
1365 Self::Bitmap(bitmap) => Some(bitmap.bounds()),
1366 }
1367 }
1368
1369 fn ink_bounds(&self) -> Option<[f32; 4]> {
1374 match self {
1375 Self::Outline(outline) => outline.curve_bounds(),
1376 Self::Painting(painting) => painting.bounds(),
1377 Self::Bitmap(bitmap) => bitmap.ink_bounds(),
1378 }
1379 }
1380
1381 fn draw_color(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1384 match self {
1385 Self::Outline(_) => Vec::new(),
1386 Self::Painting(painting) => painting.draw(origin, scale, pixels),
1387 Self::Bitmap(bitmap) => bitmap.draw(origin, scale, pixels),
1388 }
1389 }
1390}
1391
1392#[derive(Default)]
1394struct Outline {
1395 segments: Vec<Segment>,
1396 bounds: Option<[f32; 4]>,
1399}
1400
1401enum Segment {
1402 Line([f32; 2], [f32; 2]),
1403 Quad([f32; 2], [f32; 2], [f32; 2]),
1404 Cubic([f32; 2], [f32; 2], [f32; 2], [f32; 2]),
1405}
1406
1407#[derive(Copy, Clone, Debug)]
1410struct PixelBox {
1411 left: i64,
1412 top: i64,
1413 right: i64,
1414 bottom: i64,
1415}
1416
1417impl PixelBox {
1418 #[allow(clippy::cast_possible_truncation)]
1421 fn of(bounds: [f32; 4], origin: [f32; 2], scale: f32) -> Self {
1422 let [x_min, y_min, x_max, y_max] = bounds;
1423 Self {
1426 left: (origin[0] + x_min * scale).floor() as i64,
1427 top: (origin[1] - y_max * scale).floor() as i64,
1428 right: (origin[0] + x_max * scale).ceil() as i64,
1429 bottom: (origin[1] - y_min * scale).ceil() as i64,
1430 }
1431 }
1432}
1433
1434#[cfg(test)]
1435thread_local! {
1436 static OUTLINES_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1438}
1439
1440impl Outline {
1441 fn of(face: &ttf_parser::Face<'_>, glyph: u16) -> Self {
1442 #[cfg(test)]
1443 OUTLINES_READ.with(|read| read.set(read.get() + 1));
1444 let mut sink = OutlineSink::default();
1445 face.outline_glyph(GlyphId(glyph), &mut sink);
1446 sink.close();
1447 sink.outline
1448 }
1449
1450 #[allow(
1453 clippy::cast_possible_truncation,
1454 clippy::cast_sign_loss,
1455 clippy::cast_precision_loss
1456 )]
1457 fn draw(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1458 let width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
1459 let height = usize::try_from(pixels.bottom - pixels.top).unwrap_or(0);
1460 let (left, top) = (pixels.left as f32, pixels.top as f32);
1461 let raster = self.raster(width, height, |[x, y]| {
1462 point(origin[0] + x * scale - left, (origin[1] - y * scale) - top)
1463 });
1464 let mut alpha = vec![0_u8; width * height];
1465 raster.for_each_pixel(|index, coverage| {
1466 alpha[index] = (coverage.min(1.0) * 255.0).round() as u8;
1467 });
1468 alpha
1469 }
1470}
1471
1472impl Outline {
1473 fn curve_bounds(&self) -> Option<[f32; 4]> {
1477 let mut bounds = None;
1478 let mut hold = |[x, y]: [f32; 2]| include(&mut bounds, [x, y, x, y]);
1479 for segment in &self.segments {
1480 match *segment {
1481 Segment::Line(from, to) => {
1482 hold(from);
1483 hold(to);
1484 }
1485 Segment::Quad(from, control, to) => {
1486 hold(from);
1487 hold(to);
1488 for axis in 0..2 {
1489 let curvature = from[axis] - 2.0 * control[axis] + to[axis];
1491 let t = (from[axis] - control[axis]) / curvature;
1492 if t > 0.0 && t < 1.0 {
1493 hold(std::array::from_fn(|axis| {
1494 let u = 1.0 - t;
1495 u * u * from[axis] + 2.0 * u * t * control[axis] + t * t * to[axis]
1496 }));
1497 }
1498 }
1499 }
1500 Segment::Cubic(from, first, second, to) => {
1501 hold(from);
1502 hold(to);
1503 for axis in 0..2 {
1504 let [p0, p1, p2, p3] = [from[axis], first[axis], second[axis], to[axis]];
1505 for t in quadratic_roots(
1506 -p0 + 3.0 * (p1 - p2) + p3,
1507 2.0 * (p0 - 2.0 * p1 + p2),
1508 p1 - p0,
1509 )
1510 .into_iter()
1511 .flatten()
1512 .filter(|t| *t > 0.0 && *t < 1.0)
1513 {
1514 hold(std::array::from_fn(|axis| {
1515 let u = 1.0 - t;
1516 u * u * u * from[axis]
1517 + 3.0 * u * u * t * first[axis]
1518 + 3.0 * u * t * t * second[axis]
1519 + t * t * t * to[axis]
1520 }));
1521 }
1522 }
1523 }
1524 }
1525 }
1526 bounds
1527 }
1528
1529 fn raster(&self, width: usize, height: usize, at: impl Fn([f32; 2]) -> Point) -> Rasterizer {
1532 let mut raster = Rasterizer::new(width, height);
1533 for segment in &self.segments {
1534 match *segment {
1535 Segment::Line(from, to) => raster.draw_line(at(from), at(to)),
1536 Segment::Quad(from, control, to) => raster.draw_quad(at(from), at(control), at(to)),
1537 Segment::Cubic(from, first, second, to) => {
1538 raster.draw_cubic(at(from), at(first), at(second), at(to));
1539 }
1540 }
1541 }
1542 raster
1543 }
1544}
1545
1546fn quadratic_roots(a: f32, b: f32, c: f32) -> [Option<f32>; 2] {
1549 let finite = |t: f32| t.is_finite().then_some(t);
1550 if a.abs() <= f32::EPSILON * (b.abs() + c.abs()) {
1551 return [finite(-c / b), None];
1552 }
1553 let discriminant = b * b - 4.0 * a * c;
1554 if discriminant < 0.0 {
1555 return [None, None];
1556 }
1557 let root = discriminant.sqrt();
1558 [
1559 finite((-b + root) / (2.0 * a)),
1560 finite((-b - root) / (2.0 * a)),
1561 ]
1562}
1563
1564#[derive(Default)]
1566struct OutlineSink {
1567 outline: Outline,
1568 start: [f32; 2],
1569 pen: [f32; 2],
1570}
1571
1572impl OutlineSink {
1573 fn include(&mut self, points: &[[f32; 2]]) {
1574 for &[x, y] in points {
1575 let bounds = self.outline.bounds.get_or_insert([x, y, x, y]);
1576 *bounds = [
1577 bounds[0].min(x),
1578 bounds[1].min(y),
1579 bounds[2].max(x),
1580 bounds[3].max(y),
1581 ];
1582 }
1583 }
1584}
1585
1586impl OutlineBuilder for OutlineSink {
1587 fn move_to(&mut self, x: f32, y: f32) {
1588 self.close();
1589 self.start = [x, y];
1590 self.pen = [x, y];
1591 }
1592
1593 fn line_to(&mut self, x: f32, y: f32) {
1594 let to = [x, y];
1595 self.include(&[self.pen, to]);
1596 self.outline.segments.push(Segment::Line(self.pen, to));
1597 self.pen = to;
1598 }
1599
1600 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1601 let (control, to) = ([x1, y1], [x, y]);
1602 self.include(&[self.pen, control, to]);
1603 self.outline
1604 .segments
1605 .push(Segment::Quad(self.pen, control, to));
1606 self.pen = to;
1607 }
1608
1609 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1610 let (first, second, to) = ([x1, y1], [x2, y2], [x, y]);
1611 self.include(&[self.pen, first, second, to]);
1612 self.outline
1613 .segments
1614 .push(Segment::Cubic(self.pen, first, second, to));
1615 self.pen = to;
1616 }
1617
1618 fn close(&mut self) {
1619 if self.pen.map(f32::to_bits) != self.start.map(f32::to_bits) {
1622 self.line_to(self.start[0], self.start[1]);
1623 }
1624 }
1625}
1626
1627fn validate_request(request: &TextRasterRequest) -> Result<(), TextRasterError> {
1628 if request.font_size.is_finite()
1629 && request.font_size > 0.0
1630 && request.font_size <= MAX_FONT_SIZE
1631 && request.text.len() <= MAX_TEXT_BYTES
1632 && request.max_width.is_finite()
1633 && request.max_width <= 16_384.0
1634 && request.line_height.is_finite()
1635 && request.line_height > 0.0
1636 && request.line_height <= MAX_LINE_HEIGHT
1637 && request.content_version > 0
1638 && request.outline.is_none_or(|outline| {
1639 outline.width.is_finite() && outline.width > 0.0 && outline.width <= MAX_OUTLINE_WIDTH
1640 })
1641 && request.background.is_none_or(|background| {
1642 background.padding.is_finite()
1643 && background.padding >= 0.0
1644 && background.padding <= MAX_BACKGROUND_PADDING
1645 })
1646 {
1647 Ok(())
1648 } else {
1649 Err(TextRasterError::InvalidRequest)
1650 }
1651}
1652
1653fn checked_dimension(value: f32) -> Result<u32, TextRasterError> {
1654 if value.is_finite() && value > 0.0 && value.ceil() <= 16_384.0 {
1655 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1656 Ok(value.ceil() as u32)
1657 } else {
1658 Err(TextRasterError::TooLarge)
1659 }
1660}
1661
1662fn place_glyph(
1666 mask: &mut [u8],
1667 width: u32,
1668 height: u32,
1669 origin_x: i64,
1670 origin_y: i64,
1671 glyph_width: usize,
1672 alpha: &[u8],
1673) {
1674 for (index, coverage) in alpha.iter().copied().enumerate() {
1675 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
1676 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
1677 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) {
1678 continue;
1679 }
1680 let pixel =
1681 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
1682 let slot = &mut mask[usize::try_from(pixel).unwrap_or(0)];
1683 *slot = (*slot).max(coverage);
1684 }
1685}
1686
1687fn place_color_glyph(
1690 colors: &mut [u8],
1691 width: u32,
1692 height: u32,
1693 origin_x: i64,
1694 origin_y: i64,
1695 glyph_width: usize,
1696 rgba8: &[u8],
1697) {
1698 for (index, source) in rgba8.as_chunks::<4>().0.iter().enumerate() {
1699 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
1700 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
1701 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) || source[3] == 0 {
1702 continue;
1703 }
1704 let pixel =
1705 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
1706 let slot = &mut colors[usize::try_from(pixel).unwrap_or(0) * 4..][..4];
1707 let blended = over(
1708 std::array::from_fn(|channel| u32::from(source[channel])),
1709 std::array::from_fn(|channel| u32::from(slot[channel])),
1710 );
1711 for (byte, value) in slot.iter_mut().zip(blended) {
1712 *byte = u8::try_from(value.min(255)).unwrap_or(u8::MAX);
1713 }
1714 }
1715}
1716
1717impl TextRasterRequest {
1718 #[must_use]
1723 pub fn decoration_margin(&self) -> u32 {
1724 let (outline, padding) = decoration_extents(self);
1725 outline.max(padding)
1726 }
1727}
1728
1729fn decoration_extents(request: &TextRasterRequest) -> (u32, u32) {
1732 let outline = request
1733 .outline
1734 .map_or(0, |outline| disc_extent(outline.width));
1735 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1736 let padding = request
1737 .background
1738 .map_or(0, |background| background.padding.ceil() as u32);
1739 (outline, padding)
1740}
1741
1742#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1744fn disc_extent(width: f32) -> u32 {
1745 (width + 0.5).ceil() as u32
1746}
1747
1748fn premultiplied(color: [u8; 4], coverage: u8) -> [u32; 4] {
1752 let alpha = u32::from(coverage) * u32::from(color[3]) / 255;
1753 [
1754 u32::from(color[0]) * alpha / 255,
1755 u32::from(color[1]) * alpha / 255,
1756 u32::from(color[2]) * alpha / 255,
1757 alpha,
1758 ]
1759}
1760
1761fn over(source: [u32; 4], destination: [u32; 4]) -> [u32; 4] {
1763 let keep = 255 - source[3];
1764 std::array::from_fn(|channel| source[channel] + (destination[channel] * keep + 127) / 255)
1765}
1766
1767#[allow(
1773 clippy::cast_possible_truncation,
1774 clippy::cast_sign_loss,
1775 clippy::cast_precision_loss
1776)]
1777fn compose(
1778 request: &TextRasterRequest,
1779 coverage: &[u8],
1780 colors: Option<&[u8]>,
1781 width: u32,
1782 margin: [u32; 2],
1783 line_boxes: &[(f32, f32, f32)],
1784) -> Vec<u8> {
1785 let outline = request.outline.map(|outline| {
1786 let ink = colors.map(|colors| {
1787 coverage
1788 .iter()
1789 .zip(colors.as_chunks::<4>().0)
1790 .map(|(&text, color)| text.max(color[3]))
1791 .collect::<Vec<_>>()
1792 });
1793 (
1794 outline.color,
1795 dilate(
1796 ink.as_deref().unwrap_or(coverage),
1797 width,
1798 u32::try_from(coverage.len() / width.max(1) as usize).unwrap_or(0),
1799 outline.width,
1800 ),
1801 )
1802 });
1803 let background = request.background.map(|background| {
1804 let [margin_x, margin_y] = margin.map(|margin| margin as f32);
1805 let padding = background.padding;
1806 let boxes = line_boxes
1807 .iter()
1808 .map(|&(left, line_width, top)| {
1809 (
1810 margin_x + left - padding,
1811 margin_x + left + line_width + padding,
1812 margin_y + top - padding,
1813 margin_y + top + request.font_size + padding,
1814 )
1815 })
1816 .collect::<Vec<_>>();
1817 (background.color, boxes)
1818 });
1819 let mut rgba8 = vec![0_u8; coverage.len() * 4];
1820 let (mut first, mut last) = (0, 0);
1823 for (index, text) in coverage.iter().copied().enumerate() {
1824 let mut pixel = [0_u32; 4];
1825 if let Some((color, boxes)) = &background {
1826 let x = (index % width as usize) as f32;
1827 let y = (index / width as usize) as f32;
1828 if index % width as usize == 0 {
1829 while first < boxes.len() && boxes[first].3 <= y {
1830 first += 1;
1831 }
1832 last = last.max(first);
1833 while last < boxes.len() && boxes[last].2 < y + 1.0 {
1834 last += 1;
1835 }
1836 }
1837 let covered = boxes[first..last]
1838 .iter()
1839 .map(|&(left, right, top, bottom)| {
1840 let across = (right.min(x + 1.0) - left.max(x)).clamp(0.0, 1.0);
1841 let down = (bottom.min(y + 1.0) - top.max(y)).clamp(0.0, 1.0);
1842 across * down
1843 })
1844 .fold(0.0_f32, f32::max);
1845 pixel = premultiplied(*color, (covered * 255.0).round() as u8);
1846 }
1847 if let Some((color, band)) = &outline {
1848 pixel = over(premultiplied(*color, band[index]), pixel);
1849 }
1850 pixel = over(premultiplied(request.color, text), pixel);
1851 if let Some(colors) = colors {
1852 let color = &colors[index * 4..][..4];
1853 if color[3] > 0 {
1854 let opacity = u32::from(request.color[3]);
1855 pixel = over(
1856 std::array::from_fn(|channel| u32::from(color[channel]) * opacity / 255),
1857 pixel,
1858 );
1859 }
1860 }
1861 for (channel, value) in pixel.iter().enumerate() {
1862 rgba8[index * 4 + channel] = (*value).min(255) as u8;
1863 }
1864 }
1865 rgba8
1866}
1867
1868#[allow(
1879 clippy::cast_possible_truncation,
1880 clippy::cast_sign_loss,
1881 clippy::cast_precision_loss,
1882 clippy::cast_possible_wrap
1883)]
1884fn dilate(coverage: &[u8], width: u32, height: u32, radius: f32) -> Vec<u8> {
1885 let (width, height) = (width as usize, height as usize);
1886 let mut band = vec![0_u8; coverage.len()];
1887 let Some(ink) = ink_bounds(coverage, width, height) else {
1888 return band;
1889 };
1890 let disc = Disc::new(radius);
1891 let extent = disc.extent;
1892 let reach = Bounds {
1893 left: ink.left.saturating_sub(extent),
1894 top: ink.top.saturating_sub(extent),
1895 right: (ink.right + extent).min(width),
1896 bottom: (ink.bottom + extent).min(height),
1897 };
1898 let span = reach.right - reach.left;
1901 let mut window = WindowMax::default();
1902 let mut maxima = vec![0_u8; span];
1903 for (half, offsets) in &disc.runs {
1904 for source_y in ink.top..ink.bottom {
1905 let row = &coverage[source_y * width..(source_y + 1) * width];
1906 window.of(row, reach.left, span, *half, &mut maxima);
1907 for &offset in offsets {
1908 let Some(y) = source_y.checked_add_signed(-offset) else {
1910 continue;
1911 };
1912 if y < reach.top || y >= reach.bottom {
1913 continue;
1914 }
1915 let out = &mut band[y * width + reach.left..y * width + reach.right];
1916 for (pixel, highest) in out.iter_mut().zip(&maxima) {
1917 *pixel = (*pixel).max(*highest);
1918 }
1919 }
1920 }
1921 }
1922 for &(dx, dy, weight) in &disc.rim {
1923 for source_y in ink.top..ink.bottom {
1924 let Some(y) = source_y.checked_add_signed(-dy) else {
1925 continue;
1926 };
1927 if y >= height {
1928 continue;
1929 }
1930 let first = ink.left.max(usize::try_from(dx).unwrap_or(0));
1933 let last = ink
1934 .right
1935 .min(usize::try_from(width as isize + dx.min(0)).unwrap_or(0));
1936 if first >= last {
1937 continue;
1938 }
1939 let source = &coverage[source_y * width + first..source_y * width + last];
1940 let start = y * width + (first as isize - dx) as usize;
1941 let out = &mut band[start..start + source.len()];
1942 for (pixel, value) in out.iter_mut().zip(source) {
1943 *pixel = (*pixel).max((u32::from(*value) * weight / 255) as u8);
1944 }
1945 }
1946 }
1947 band
1948}
1949
1950#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1952struct Bounds {
1953 left: usize,
1954 top: usize,
1955 right: usize,
1956 bottom: usize,
1957}
1958
1959fn ink_bounds(coverage: &[u8], width: usize, height: usize) -> Option<Bounds> {
1961 let mut bounds: Option<Bounds> = None;
1962 for y in 0..height {
1963 let row = &coverage[y * width..(y + 1) * width];
1964 let Some(left) = row.iter().position(|&value| value > 0) else {
1965 continue;
1966 };
1967 let right = row.iter().rposition(|&value| value > 0).unwrap_or(left) + 1;
1968 bounds = Some(bounds.map_or(
1969 Bounds {
1970 left,
1971 top: y,
1972 right,
1973 bottom: y + 1,
1974 },
1975 |bounds| Bounds {
1976 left: bounds.left.min(left),
1977 top: bounds.top,
1978 right: bounds.right.max(right),
1979 bottom: y + 1,
1980 },
1981 ));
1982 }
1983 bounds
1984}
1985
1986struct Disc {
1989 extent: usize,
1991 runs: Vec<(usize, Vec<isize>)>,
1994 rim: Vec<(isize, isize, u32)>,
1996}
1997
1998impl Disc {
1999 #[allow(
2002 clippy::cast_possible_truncation,
2003 clippy::cast_sign_loss,
2004 clippy::cast_precision_loss,
2005 clippy::cast_possible_wrap
2006 )]
2007 fn new(radius: f32) -> Self {
2008 let extent = disc_extent(radius) as isize;
2009 let mut runs: Vec<(usize, Vec<isize>)> = Vec::new();
2010 let mut rim = Vec::new();
2011 for dy in -extent..=extent {
2012 let mut half = None;
2013 for dx in -extent..=extent {
2014 let distance = ((dx * dx + dy * dy) as f32).sqrt();
2015 let weight = ((radius + 0.5 - distance).clamp(0.0, 1.0) * 255.0).round() as u32;
2016 match weight {
2017 0 => {}
2018 255 => half = Some(dx.unsigned_abs()),
2021 _ => rim.push((dx, dy, weight)),
2022 }
2023 }
2024 if let Some(half) = half {
2025 match runs.iter_mut().find(|(run, _)| *run == half) {
2026 Some((_, offsets)) => offsets.push(dy),
2027 None => runs.push((half, vec![dy])),
2028 }
2029 }
2030 }
2031 Self {
2032 extent: extent as usize,
2033 runs,
2034 rim,
2035 }
2036 }
2037}
2038
2039#[derive(Default)]
2042struct WindowMax {
2043 padded: Vec<u8>,
2044 from_start: Vec<u8>,
2045 to_end: Vec<u8>,
2046}
2047
2048impl WindowMax {
2049 fn of(&mut self, row: &[u8], left: usize, count: usize, half: usize, out: &mut [u8]) {
2052 let window = 2 * half + 1;
2053 let length = count + 2 * half;
2054 self.padded.clear();
2055 self.padded.resize(length, 0);
2056 let first_column = left.saturating_sub(half);
2058 let last_column = (left + count + half).min(row.len());
2059 if first_column < last_column {
2060 let at = first_column + half - left;
2061 self.padded[at..at + last_column - first_column]
2062 .copy_from_slice(&row[first_column..last_column]);
2063 }
2064 self.from_start.clear();
2065 self.from_start.resize(length, 0);
2066 self.to_end.clear();
2067 self.to_end.resize(length, 0);
2068 for block in (0..length).step_by(window) {
2069 let end = (block + window).min(length);
2070 let mut highest = 0;
2071 for index in block..end {
2072 highest = highest.max(self.padded[index]);
2073 self.from_start[index] = highest;
2074 }
2075 highest = 0;
2076 for index in (block..end).rev() {
2077 highest = highest.max(self.padded[index]);
2078 self.to_end[index] = highest;
2079 }
2080 }
2081 for (index, value) in out[..count].iter_mut().enumerate() {
2082 *value = self.to_end[index].max(self.from_start[index + window - 1]);
2083 }
2084 }
2085}
2086
2087#[cfg(test)]
2088mod tests;