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)]
153pub struct ShapedText {
154 paragraphs: Vec<Vec<Cluster>>,
156 font_size: f32,
157 line_advance: f32,
158}
159
160#[derive(Copy, Clone, Debug, PartialEq)]
162pub struct LineMetrics {
163 pub line_count: u32,
165 pub widest: f32,
167 pub height: f32,
170}
171
172impl ShapedText {
173 #[must_use]
176 pub fn min_content_width(&self) -> f32 {
177 let mut widest = 0.0_f32;
178 for clusters in &self.paragraphs {
179 let mut start = 0;
180 for index in 1..=clusters.len() {
181 if index == clusters.len() || clusters[index].break_before {
182 widest = widest.max(advance(&clusters[start..index]));
183 start = index;
184 }
185 }
186 }
187 widest
188 }
189
190 #[must_use]
193 pub fn max_content_width(&self) -> f32 {
194 self.paragraphs
195 .iter()
196 .map(|clusters| advance(clusters))
197 .fold(0.0, f32::max)
198 }
199
200 #[must_use]
203 pub fn lines_at(&self, max_width: f32) -> LineMetrics {
204 let (mut count, mut kept, mut widest) = (0_u32, 1_u32, 0.0_f32);
205 for clusters in &self.paragraphs {
206 for range in break_lines(clusters, max_width) {
207 count = count.saturating_add(1);
208 if !range.is_empty() {
209 kept = count;
210 }
211 widest = widest.max(advance(&clusters[range]));
212 }
213 }
214 LineMetrics {
215 line_count: kept,
216 widest,
217 height: box_height(kept, self.line_advance, self.font_size),
218 }
219 }
220}
221
222fn advance(clusters: &[Cluster]) -> f32 {
225 let end = clusters
226 .iter()
227 .rposition(|cluster| !cluster.hangs)
228 .map_or(0, |last| last + 1);
229 clusters[..end]
230 .iter()
231 .fold(0.0, |width, cluster| width + cluster.width)
232}
233
234#[allow(clippy::cast_precision_loss)]
237fn box_height(line_count: u32, line_advance: f32, font_size: f32) -> f32 {
238 line_count.saturating_sub(1) as f32 * line_advance + line_advance.max(font_size)
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
243#[non_exhaustive]
244pub enum TextRasterError {
245 NoFonts,
247 InvalidFont(String),
249 InvalidRequest,
251 TooLarge,
253}
254
255impl fmt::Display for TextRasterError {
256 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257 match self {
258 Self::NoFonts => formatter.write_str("text rasterizer requires at least one font"),
259 Self::InvalidFont(error) => write!(formatter, "invalid text font: {error}"),
260 Self::InvalidRequest => formatter.write_str("invalid text raster request"),
261 Self::TooLarge => formatter.write_str("text raster result is too large"),
262 }
263 }
264}
265
266impl std::error::Error for TextRasterError {}
267
268#[derive(Clone, Debug)]
269struct LoadedFont {
270 source: TextFont,
271 family: Option<String>,
273}
274
275impl LoadedFont {
276 fn parse(source: TextFont) -> Result<Self, TextRasterError> {
280 check_font(&source.bytes, source.face_index)?;
281 let family = font_family(&source.bytes, source.face_index);
282 Ok(Self { source, family })
283 }
284}
285
286pub fn check_font(bytes: &[u8], face_index: u32) -> Result<(), TextRasterError> {
294 ttf_parser::Face::parse(bytes, face_index)
295 .map(|_| ())
296 .map_err(|error| TextRasterError::InvalidFont(error.to_string()))
297}
298
299#[must_use]
304pub fn font_family(bytes: &[u8], face_index: u32) -> Option<String> {
305 use rustybuzz::ttf_parser::{Language, name_id};
306 let face = rustybuzz::ttf_parser::Face::parse(bytes, face_index).ok()?;
307 let names = face.names();
308 [name_id::TYPOGRAPHIC_FAMILY, name_id::FAMILY]
309 .into_iter()
310 .find_map(|id| {
311 let decoded = || {
312 names
313 .into_iter()
314 .filter(move |name| name.name_id == id)
315 .filter_map(|name| {
316 let text = name.to_string()?;
317 (!text.trim().is_empty()).then_some((name.language(), text))
318 })
319 };
320 decoded()
321 .find(|(language, _)| *language == Language::English_UnitedStates)
322 .or_else(|| decoded().next())
323 .map(|(_, text)| text)
324 })
325}
326
327pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
330
331const CACHE_ENTRY_OVERHEAD: usize = 256;
334
335#[derive(Debug)]
342pub struct TextRasterizer {
343 fonts: Vec<LoadedFont>,
344 cache: ResultCache,
345}
346
347#[derive(Debug)]
350struct ResultCache {
351 entries: HashMap<u64, CacheEntry>,
352 by_use: BTreeMap<u64, u64>,
354 next_use: u64,
355 bytes: usize,
356 budget: usize,
357}
358
359#[derive(Debug)]
360struct CacheEntry {
361 result: Arc<TextRasterResult>,
362 last_use: u64,
363 bytes: usize,
364}
365
366impl ResultCache {
367 fn new(budget: usize) -> Self {
368 Self {
369 entries: HashMap::new(),
370 by_use: BTreeMap::new(),
371 next_use: 0,
372 bytes: 0,
373 budget,
374 }
375 }
376
377 fn get(&mut self, version: u64) -> Option<Arc<TextRasterResult>> {
378 let entry = self.entries.get_mut(&version)?;
379 self.by_use.remove(&entry.last_use);
380 entry.last_use = self.next_use;
381 self.by_use.insert(self.next_use, version);
382 self.next_use += 1;
383 Some(Arc::clone(&entry.result))
384 }
385
386 fn insert(&mut self, version: u64, result: &Arc<TextRasterResult>) {
389 self.remove(version);
390 let bytes = result
391 .pixels
392 .rgba8
393 .len()
394 .saturating_add(CACHE_ENTRY_OVERHEAD);
395 if bytes > self.budget {
396 return;
397 }
398 self.bytes += bytes;
399 self.entries.insert(
400 version,
401 CacheEntry {
402 result: Arc::clone(result),
403 last_use: self.next_use,
404 bytes,
405 },
406 );
407 self.by_use.insert(self.next_use, version);
408 self.next_use += 1;
409 self.evict();
410 }
411
412 fn remove(&mut self, version: u64) {
413 if let Some(entry) = self.entries.remove(&version) {
414 self.by_use.remove(&entry.last_use);
415 self.bytes -= entry.bytes;
416 }
417 }
418
419 fn clear(&mut self) {
420 self.entries.clear();
421 self.by_use.clear();
422 self.bytes = 0;
423 }
424
425 fn set_budget(&mut self, budget: usize) {
426 self.budget = budget;
427 self.evict();
428 }
429
430 fn evict(&mut self) {
431 while self.bytes > self.budget {
432 let Some((_, version)) = self.by_use.pop_first() else {
433 break;
434 };
435 if let Some(entry) = self.entries.remove(&version) {
436 self.bytes -= entry.bytes;
437 }
438 }
439 }
440}
441
442impl TextRasterizer {
443 #[must_use]
445 pub fn empty() -> Self {
446 Self {
447 fonts: Vec::new(),
448 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
449 }
450 }
451
452 pub fn new(fonts: Vec<TextFont>) -> Result<Self, TextRasterError> {
457 if fonts.is_empty() {
458 return Err(TextRasterError::NoFonts);
459 }
460 let loaded = fonts
461 .into_iter()
462 .map(LoadedFont::parse)
463 .collect::<Result<Vec<_>, _>>()?;
464 Ok(Self {
465 fonts: loaded,
466 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
467 })
468 }
469
470 pub fn add_font(&mut self, source: TextFont) -> Result<(), TextRasterError> {
478 self.fonts.push(LoadedFont::parse(source)?);
479 self.cache.clear();
480 Ok(())
481 }
482
483 #[must_use]
485 pub fn font_count(&self) -> usize {
486 self.fonts.len()
487 }
488
489 pub fn rasterize(
495 &mut self,
496 request: &TextRasterRequest,
497 ) -> Result<Arc<TextRasterResult>, TextRasterError> {
498 validate_request(request)?;
499 if self.fonts.is_empty() {
500 return Err(TextRasterError::NoFonts);
501 }
502 if let Some(cached) = self.cache.get(request.content_version) {
503 return Ok(cached);
504 }
505 let result = Arc::new(self.rasterize_uncached(request)?);
506 self.cache.insert(request.content_version, &result);
507 Ok(result)
508 }
509
510 pub fn invalidate(&mut self, content_version: u64) {
512 self.cache.remove(content_version);
513 }
514
515 pub fn set_cache_budget(&mut self, bytes: usize) {
520 self.cache.set_budget(bytes);
521 }
522
523 #[must_use]
525 pub fn cache_bytes(&self) -> usize {
526 self.cache.bytes
527 }
528
529 pub fn clear(&mut self) {
531 self.cache.clear();
532 }
533
534 #[must_use]
536 pub fn cache_len(&self) -> usize {
537 self.cache.entries.len()
538 }
539
540 #[allow(clippy::cast_precision_loss)]
547 pub fn measure(&self, request: &TextRasterRequest) -> Result<TextMeasure, TextRasterError> {
548 validate_request(request)?;
549 if self.fonts.is_empty() {
550 return Err(TextRasterError::NoFonts);
551 }
552 let faces = Faces::new(&self.fonts)?;
553 let layout = self.lay_out(&faces, request)?;
554 let mut ink = None;
555 layout.each_glyph(
556 &faces,
557 request,
558 &mut HashMap::new(),
559 |drawing, origin, scale, _| {
560 if let Some([x_min, y_min, x_max, y_max]) = drawing.ink_bounds() {
561 include(
562 &mut ink,
563 [
564 origin[0] + x_min * scale,
565 origin[1] - y_max * scale,
566 origin[0] + x_max * scale,
567 origin[1] - y_min * scale,
568 ],
569 );
570 }
571 },
572 );
573 let band = request.outline.map_or(0.0, |outline| outline.width + 1.0);
576 ink = ink.map(|bounds| grown(bounds, band));
577 let padding = request.background.map(|background| background.padding);
578 let mut content = None;
579 for (index, line) in layout.lines.iter().enumerate() {
580 let left = layout.line_offset(line, request.align);
581 let top = index as f32 * layout.line_advance;
582 include(&mut content, [left, 0.0, left + line.width, layout.height]);
583 if let (Some(padding), true) = (padding, line.width > 0.0) {
584 include(
585 &mut ink,
586 grown(
587 [left, top, left + line.width, top + request.font_size],
588 padding,
589 ),
590 );
591 }
592 }
593 let content = content.unwrap_or([0.0, 0.0, 0.0, layout.height]);
594 Ok(TextMeasure {
595 box_width: layout.box_width,
596 box_height: layout.box_height,
597 decoration_margin: request.decoration_margin(),
598 content: grown(content, request.decoration_margin() as f32),
599 ink,
600 line_count: layout.line_count,
601 })
602 }
603
604 pub fn shape(&self, request: &TextRasterRequest) -> Result<ShapedText, TextRasterError> {
614 validate_request(&TextRasterRequest {
615 text: String::new(),
616 max_width: 0.0,
617 content_version: 1,
618 outline: None,
619 background: None,
620 ..request.clone()
621 })?;
622 if request.text.len() > MAX_TEXT_BYTES {
623 return Err(TextRasterError::InvalidRequest);
624 }
625 if self.fonts.is_empty() {
626 return Err(TextRasterError::NoFonts);
627 }
628 let faces = Faces::new(&self.fonts)?;
629 let order = self.font_order(request.font_family.as_deref());
630 let paragraphs = normalize(&request.text, request.emoji_policy)
631 .split('\n')
632 .map(|paragraph| {
633 let bidi = ParagraphBidiInfo::new(paragraph, None);
634 let glyphs = shape_paragraph(
635 &faces,
636 paragraph,
637 &bidi,
638 request.font_size,
639 &order,
640 request.emoji_policy,
641 );
642 clusters(&glyphs, paragraph)
643 })
644 .collect();
645 Ok(ShapedText {
646 paragraphs,
647 font_size: request.font_size,
648 line_advance: request.font_size * request.line_height,
649 })
650 }
651
652 #[allow(clippy::cast_precision_loss)]
654 fn lay_out(
655 &self,
656 faces: &Faces<'_>,
657 request: &TextRasterRequest,
658 ) -> Result<Layout, TextRasterError> {
659 let line_advance = request.font_size * request.line_height;
660 let order = self.font_order(request.font_family.as_deref());
661 let mut lines = Vec::new();
662 let mut kept = 1;
665 for paragraph in normalize(&request.text, request.emoji_policy).split('\n') {
666 for line in layout_paragraph(
667 faces,
668 paragraph,
669 request.font_size,
670 request.max_width,
671 &order,
672 request.emoji_policy,
673 ) {
674 lines.push(line);
675 if lines.last().is_some_and(|line| line.has_glyphs) {
676 kept = lines.len();
677 }
678 }
679 }
680 lines.truncate(kept);
681 let line_count = u32::try_from(lines.len()).map_err(|_| TextRasterError::TooLarge)?;
682 let width = if request.max_width > 0.0 {
683 request.max_width
684 } else {
685 lines.iter().map(|line| line.width).fold(0.0, f32::max)
686 }
687 .max(1.0);
688 let height = box_height(line_count, line_advance, request.font_size).max(1.0);
689 Ok(Layout {
690 box_width: checked_dimension(width)?,
691 box_height: checked_dimension(height)?,
692 lines,
693 line_count,
694 line_advance,
695 ascent: request.font_size * 0.8,
696 descent: request.font_size * 0.2,
697 width,
698 height,
699 })
700 }
701
702 #[allow(
703 clippy::cast_possible_truncation,
704 clippy::cast_precision_loss,
705 clippy::too_many_lines
706 )]
707 fn rasterize_uncached(
708 &self,
709 request: &TextRasterRequest,
710 ) -> Result<TextRasterResult, TextRasterError> {
711 let faces = Faces::new(&self.fonts)?;
712 let layout = self.lay_out(&faces, request)?;
713 let (box_width, box_height) = (layout.box_width, layout.box_height);
714 let mut drawings = HashMap::<(usize, u16), Drawing>::new();
715 let mut placed = Vec::new();
716 let (mut overflow_x, mut overflow_y) = (0_i64, 0_i64);
718 let (mut ink_ascent, mut ink_descent) = (layout.ascent, layout.descent);
719 layout.each_glyph(
720 &faces,
721 request,
722 &mut drawings,
723 |drawing, origin, scale, glyph| {
724 let Some(bounds) = drawing.bounds() else {
725 return;
726 };
727 let pixels = PixelBox::of(bounds, origin, scale);
728 overflow_x = overflow_x
729 .max(pixels.left.saturating_neg())
730 .max(pixels.right.saturating_sub(i64::from(box_width)));
731 overflow_y = overflow_y
732 .max(pixels.top.saturating_neg())
733 .max(pixels.bottom.saturating_sub(i64::from(box_height)));
734 ink_ascent = ink_ascent.max(bounds[3] * scale + glyph.y_offset);
735 ink_descent = ink_descent.max(-(bounds[1] * scale + glyph.y_offset));
736 placed.push(((glyph.font, glyph.glyph_index), origin, scale, pixels));
737 },
738 );
739 let (outline, padding) = decoration_extents(request);
742 let margin = |overflow: i64| -> Result<u32, TextRasterError> {
743 u32::try_from(overflow)
744 .map_err(|_| TextRasterError::TooLarge)?
745 .checked_add(outline)
746 .map(|margin| margin.max(outline.max(padding)))
747 .ok_or(TextRasterError::TooLarge)
748 };
749 let (margin_x, margin_y) = (margin(overflow_x)?, margin(overflow_y)?);
750 let width = margin_x
751 .checked_mul(2)
752 .and_then(|margins| box_width.checked_add(margins))
753 .ok_or(TextRasterError::TooLarge)?;
754 let height = margin_y
755 .checked_mul(2)
756 .and_then(|margins| box_height.checked_add(margins))
757 .ok_or(TextRasterError::TooLarge)?;
758 if width > 16_384 || height > 16_384 {
759 return Err(TextRasterError::TooLarge);
760 }
761 let pixel_count = usize::try_from(width)
762 .ok()
763 .and_then(|width| {
764 usize::try_from(height)
765 .ok()
766 .and_then(|height| width.checked_mul(height))
767 })
768 .ok_or(TextRasterError::TooLarge)?;
769 let mut coverage = vec![0_u8; pixel_count];
770 let mut colors: Option<Vec<u8>> = None;
772 let mut line_boxes = Vec::with_capacity(layout.lines.len());
773 for (index, line) in layout.lines.iter().enumerate() {
774 if line.width > 0.0 {
775 line_boxes.push((
776 layout.line_offset(line, request.align),
777 line.width,
778 index as f32 * layout.line_advance,
779 ));
780 }
781 }
782 for (key, origin, scale, pixels) in placed {
783 let (origin_x, origin_y) = (
784 pixels.left + i64::from(margin_x),
785 pixels.top + i64::from(margin_y),
786 );
787 let glyph_width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
788 match &drawings[&key] {
789 Drawing::Outline(outline) => place_glyph(
790 &mut coverage,
791 width,
792 height,
793 origin_x,
794 origin_y,
795 glyph_width,
796 &outline.draw(origin, scale, pixels),
797 ),
798 drawing => place_color_glyph(
799 colors.get_or_insert_with(|| vec![0; pixel_count * 4]),
800 width,
801 height,
802 origin_x,
803 origin_y,
804 glyph_width,
805 &drawing.draw_color(origin, scale, pixels),
806 ),
807 }
808 }
809 let rgba8 = compose(
810 request,
811 &coverage,
812 colors.as_deref(),
813 width,
814 [margin_x, margin_y],
815 &line_boxes,
816 );
817 let texture = HostTexture {
818 content_version: request.content_version,
819 width,
820 height,
821 premultiplication: Premultiplication::Premultiplied,
822 color_space: RasterColorSpace::Srgb,
823 };
824 Ok(TextRasterResult {
825 descriptor: TextTexture {
826 texture: texture.clone(),
827 layout_x: margin_x as f32,
828 layout_y: margin_y as f32,
829 layout_width: layout.width,
830 layout_height: layout.height,
831 first_baseline: layout.ascent,
832 ascent: ink_ascent,
833 descent: ink_descent,
834 line_count: layout.line_count,
835 },
836 pixels: HostTexturePixels { texture, rgba8 },
837 })
838 }
839
840 fn font_order(&self, family: Option<&str>) -> Vec<usize> {
843 let preferred = |font: &LoadedFont| {
844 family.is_some_and(|wanted| {
845 font.family
846 .as_deref()
847 .is_some_and(|name| name.eq_ignore_ascii_case(wanted))
848 })
849 };
850 let (mut order, rest): (Vec<usize>, Vec<usize>) =
851 (0..self.fonts.len()).partition(|&index| preferred(&self.fonts[index]));
852 order.extend(rest);
853 order
854 }
855}
856
857struct Faces<'a> {
860 parsed: Vec<ttf_parser::Face<'a>>,
861 shapers: Vec<OnceCell<rustybuzz::Face<'a>>>,
862 color: Vec<bool>,
864}
865
866impl<'a> Faces<'a> {
867 fn new(fonts: &'a [LoadedFont]) -> Result<Self, TextRasterError> {
868 let parsed = fonts
869 .iter()
870 .map(|font| {
871 ttf_parser::Face::parse(&font.source.bytes, font.source.face_index)
872 .ok()
873 .filter(|face| face.units_per_em() > 0)
874 .ok_or_else(|| TextRasterError::InvalidFont(font.source.family.clone()))
875 })
876 .collect::<Result<Vec<_>, _>>()?;
877 Ok(Self {
878 shapers: parsed.iter().map(|_| OnceCell::new()).collect(),
879 color: parsed
880 .iter()
881 .map(|face| {
882 let tables = face.tables();
883 tables.colr.is_some() || tables.cbdt.is_some() || tables.sbix.is_some()
884 })
885 .collect(),
886 parsed,
887 })
888 }
889
890 fn shaper(&self, font: usize) -> &rustybuzz::Face<'a> {
891 self.shapers[font].get_or_init(|| rustybuzz::Face::from_face(self.parsed[font].clone()))
892 }
893
894 #[allow(clippy::cast_precision_loss)]
895 fn units_per_em(&self, font: usize) -> f32 {
896 f32::from(self.parsed[font].units_per_em())
897 }
898
899 fn has(&self, font: usize, character: char) -> bool {
901 self.parsed[font]
902 .glyph_index(character)
903 .is_some_and(|glyph| glyph.0 != 0)
904 }
905
906 fn covers(&self, font: usize, grapheme: &str) -> bool {
908 grapheme
909 .chars()
910 .filter(|&character| !is_default_ignorable(character))
911 .all(|character| self.has(font, character))
912 }
913}
914
915fn normalize(text: &str, policy: EmojiPolicy) -> String {
922 let mut normalized = String::with_capacity(text.len());
923 for grapheme in text.graphemes(true) {
924 if policy == EmojiPolicy::Replace && is_emoji(grapheme) {
925 normalized.push('\u{fffd}');
926 } else if grapheme == "\r\n" {
927 normalized.push('\n');
928 } else {
929 normalized.extend(grapheme.chars().map(|character| match character {
930 '\r'
931 | '\u{b}'
932 | '\u{c}'
933 | '\u{1c}'..='\u{1e}'
934 | '\u{85}'
935 | '\u{2028}'
936 | '\u{2029}' => '\n',
937 '\t' => ' ',
938 other => other,
939 }));
940 }
941 }
942 normalized
943}
944
945fn is_emoji(grapheme: &str) -> bool {
951 if grapheme.contains('\u{fe0e}') {
952 return false;
953 }
954 let selected = grapheme.contains('\u{fe0f}');
955 grapheme.chars().any(|character| {
956 in_table(unicode_tables::EMOJI_PRESENTATION, character)
957 || (selected
958 && (character == '\u{20e3}'
959 || in_table(unicode_tables::EXTENDED_PICTOGRAPHIC, character)))
960 })
961}
962
963fn is_default_ignorable(character: char) -> bool {
964 in_table(unicode_tables::DEFAULT_IGNORABLE, character)
965}
966
967fn in_table(table: &[(u32, u32)], character: char) -> bool {
968 let code = u32::from(character);
969 table
970 .binary_search_by(|&(first, last)| {
971 if last < code {
972 Ordering::Less
973 } else if first > code {
974 Ordering::Greater
975 } else {
976 Ordering::Equal
977 }
978 })
979 .is_ok()
980}
981
982fn hangs(character: char) -> bool {
986 character.is_whitespace()
987 && !matches!(character, '\u{a0}' | '\u{2007}' | '\u{202f}' | '\u{1680}')
988}
989
990fn is_hangul(character: char) -> bool {
991 matches!(
992 character,
993 '\u{1100}'..='\u{11ff}'
994 | '\u{3130}'..='\u{318f}'
995 | '\u{a960}'..='\u{a97f}'
996 | '\u{ac00}'..='\u{d7a3}'
997 | '\u{d7b0}'..='\u{d7ff}'
998 )
999}
1000
1001struct Layout {
1004 lines: Vec<LaidLine>,
1005 line_count: u32,
1006 line_advance: f32,
1008 ascent: f32,
1010 descent: f32,
1011 width: f32,
1014 height: f32,
1015 box_width: u32,
1017 box_height: u32,
1018}
1019
1020impl Layout {
1021 fn line_offset(&self, line: &LaidLine, align: TextAlign) -> f32 {
1023 match (align, line.rtl) {
1024 (TextAlign::Center, _) => (self.width - line.width) * 0.5,
1025 (TextAlign::Start, false) | (TextAlign::End, true) => 0.0,
1026 (TextAlign::End, false) | (TextAlign::Start, true) => self.width - line.width,
1027 }
1028 }
1029
1030 #[allow(clippy::cast_precision_loss)]
1033 fn each_glyph(
1034 &self,
1035 faces: &Faces<'_>,
1036 request: &TextRasterRequest,
1037 drawings: &mut HashMap<(usize, u16), Drawing>,
1038 mut place: impl FnMut(&Drawing, [f32; 2], f32, &PositionedGlyph),
1039 ) {
1040 for (index, line) in self.lines.iter().enumerate() {
1041 let offset = self.line_offset(line, request.align);
1042 let baseline = index as f32 * self.line_advance + self.ascent;
1043 for glyph in &line.glyphs {
1044 let drawing = drawings
1045 .entry((glyph.font, glyph.glyph_index))
1046 .or_insert_with(|| Drawing::of(faces, glyph.font, glyph.glyph_index, request));
1047 let scale = request.font_size / faces.units_per_em(glyph.font);
1048 let origin = [glyph.x + glyph.x_offset + offset, baseline - glyph.y_offset];
1049 place(drawing, origin, scale, glyph);
1050 }
1051 }
1052 }
1053}
1054
1055fn include(bounds: &mut Option<[f32; 4]>, [left, top, right, bottom]: [f32; 4]) {
1057 let held = bounds.get_or_insert([left, top, right, bottom]);
1058 *held = [
1059 held[0].min(left),
1060 held[1].min(top),
1061 held[2].max(right),
1062 held[3].max(bottom),
1063 ];
1064}
1065
1066fn grown([left, top, right, bottom]: [f32; 4], by: f32) -> [f32; 4] {
1068 [left - by, top - by, right + by, bottom + by]
1069}
1070
1071struct LaidLine {
1073 glyphs: Vec<PositionedGlyph>,
1075 width: f32,
1077 rtl: bool,
1079 has_glyphs: bool,
1081}
1082
1083fn layout_paragraph(
1087 faces: &Faces<'_>,
1088 paragraph: &str,
1089 font_size: f32,
1090 max_width: f32,
1091 order: &[usize],
1092 policy: EmojiPolicy,
1093) -> Vec<LaidLine> {
1094 let bidi = ParagraphBidiInfo::new(paragraph, None);
1095 let rtl = bidi.paragraph_level.is_rtl();
1096 let glyphs = shape_paragraph(faces, paragraph, &bidi, font_size, order, policy);
1097 let clusters = clusters(&glyphs, paragraph);
1098 break_lines(&clusters, max_width)
1099 .into_iter()
1100 .map(|range| {
1101 let has_glyphs = !range.is_empty();
1102 let mut end = range.end;
1104 while end > range.start && clusters[end - 1].hangs {
1105 end -= 1;
1106 }
1107 let mut line = LaidLine {
1108 glyphs: Vec::new(),
1109 width: 0.0,
1110 rtl,
1111 has_glyphs,
1112 };
1113 if end == range.start {
1114 return line;
1115 }
1116 let bytes = clusters[range.start].start
1117 ..clusters
1118 .get(range.end)
1119 .map_or(paragraph.len(), |next| next.start);
1120 let levels = bidi.reordered_levels(bytes);
1121 let mut x = 0.0_f32;
1122 for index in visual_order(
1123 clusters[range.start].glyphs.start..clusters[end - 1].glyphs.end,
1124 |index| levels[glyphs[index].cluster].number(),
1125 ) {
1126 let glyph = &glyphs[index];
1127 line.glyphs.push(PositionedGlyph {
1128 font: glyph.font,
1129 glyph_index: glyph.glyph_index,
1130 x,
1131 x_offset: glyph.x_offset,
1132 y_offset: glyph.y_offset,
1133 });
1134 x += glyph.advance;
1135 }
1136 line.width = x;
1137 line
1138 })
1139 .collect()
1140}
1141
1142fn visual_order(range: Range<usize>, level: impl Fn(usize) -> u8) -> Vec<usize> {
1147 let mut order = range.collect::<Vec<_>>();
1148 let levels = order.iter().map(|&index| level(index)).collect::<Vec<_>>();
1149 let highest = levels.iter().copied().max().unwrap_or(0);
1150 let Some(lowest_odd) = levels.iter().copied().filter(|level| level % 2 == 1).min() else {
1151 return order;
1152 };
1153 let mut current = levels;
1154 for at_least in (lowest_odd..=highest).rev() {
1155 let mut start = 0;
1156 while start < order.len() {
1157 if current[start] < at_least {
1158 start += 1;
1159 continue;
1160 }
1161 let end = (start..order.len())
1162 .find(|&index| current[index] < at_least)
1163 .unwrap_or(order.len());
1164 order[start..end].reverse();
1165 current[start..end].reverse();
1166 start = end;
1167 }
1168 }
1169 order
1170}
1171
1172#[derive(Clone, Debug, PartialEq)]
1175struct Run {
1176 range: Range<usize>,
1177 font: usize,
1178 script: Option<Script>,
1179 rtl: bool,
1180}
1181
1182fn itemize(
1193 faces: &Faces<'_>,
1194 paragraph: &str,
1195 bidi: &ParagraphBidiInfo<'_>,
1196 order: &[usize],
1197 policy: EmojiPolicy,
1198) -> Vec<Run> {
1199 let first = order.first().copied().unwrap_or(0);
1200 let (color, text): (Vec<usize>, Vec<usize>) =
1201 order.iter().partition(|&&font| faces.color[font]);
1202 let color_first = color.iter().chain(&text).copied().collect::<Vec<_>>();
1203 let text_first = text.iter().chain(&color).copied().collect::<Vec<_>>();
1204 let fits = |font: usize, color_wanted: bool| {
1206 faces.color[font] == color_wanted
1207 || if color_wanted {
1208 color.is_empty()
1209 } else {
1210 text.is_empty()
1211 }
1212 };
1213 let mut graphemes = Vec::<(usize, usize, Option<Script>, bool)>::new();
1214 let mut current = None;
1215 for (start, grapheme) in paragraph.grapheme_indices(true) {
1216 let script = grapheme
1217 .chars()
1218 .map(|character| character.script())
1219 .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown));
1220 let wants_color = if grapheme.contains('\u{fe0e}') {
1222 Some(false)
1223 } else if is_emoji(grapheme) {
1224 Some(policy == EmojiPolicy::Color)
1225 } else {
1226 None
1227 };
1228 let order = match wants_color {
1229 Some(true) => color_first.as_slice(),
1230 Some(false) => text_first.as_slice(),
1231 None => order,
1232 };
1233 let stays = current.filter(|&font| {
1234 script.is_none()
1235 && wants_color.is_none_or(|wanted| fits(font, wanted))
1236 && faces.covers(font, grapheme)
1237 });
1238 let font = stays
1239 .or_else(|| {
1240 order
1241 .iter()
1242 .copied()
1243 .find(|&font| faces.covers(font, grapheme))
1244 })
1245 .or_else(|| {
1246 let base = grapheme
1247 .chars()
1248 .find(|&character| !is_default_ignorable(character))?;
1249 order.iter().copied().find(|&font| faces.has(font, base))
1250 })
1251 .or(current)
1252 .unwrap_or(first);
1253 current = Some(font);
1254 graphemes.push((start, font, script, bidi.levels[start].is_rtl()));
1255 }
1256 let leading = graphemes.iter().find_map(|grapheme| grapheme.2);
1257 let mut before = leading;
1258 for grapheme in &mut graphemes {
1259 match grapheme.2 {
1260 Some(script) => before = Some(script),
1261 None => grapheme.2 = before,
1262 }
1263 }
1264 let mut runs = Vec::<Run>::new();
1265 for (index, &(start, font, script, rtl)) in graphemes.iter().enumerate() {
1266 let end = graphemes
1267 .get(index + 1)
1268 .map_or(paragraph.len(), |next| next.0);
1269 match runs.last_mut() {
1270 Some(run) if run.font == font && run.script == script && run.rtl == rtl => {
1271 run.range.end = end;
1272 }
1273 _ => runs.push(Run {
1274 range: start..end,
1275 font,
1276 script,
1277 rtl,
1278 }),
1279 }
1280 }
1281 runs
1282}
1283
1284fn shaping_script(script: Script) -> Option<rustybuzz::Script> {
1286 let code: [u8; 4] = script.short_name().as_bytes().try_into().ok()?;
1287 rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&code))
1288}
1289
1290fn shape_paragraph(
1293 faces: &Faces<'_>,
1294 paragraph: &str,
1295 bidi: &ParagraphBidiInfo<'_>,
1296 font_size: f32,
1297 order: &[usize],
1298 policy: EmojiPolicy,
1299) -> Vec<ShapedGlyph> {
1300 let mut glyphs = Vec::new();
1301 for run in itemize(faces, paragraph, bidi, order, policy) {
1302 let face = faces.shaper(run.font);
1303 let units = faces.units_per_em(run.font);
1304 let mut buffer = UnicodeBuffer::new();
1305 buffer.push_str(¶graph[run.range.clone()]);
1306 buffer.set_pre_context(¶graph[..run.range.start]);
1307 buffer.set_post_context(¶graph[run.range.end..]);
1308 buffer.set_direction(if run.rtl {
1309 Direction::RightToLeft
1310 } else {
1311 Direction::LeftToRight
1312 });
1313 match run.script.and_then(shaping_script) {
1314 Some(script) => buffer.set_script(script),
1315 None => buffer.guess_segment_properties(),
1316 }
1317 let shaped = rustybuzz::shape(face, &[], buffer);
1318 let first = glyphs.len();
1319 for (info, position) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) {
1320 #[allow(clippy::cast_precision_loss)]
1321 glyphs.push(ShapedGlyph {
1322 font: run.font,
1323 glyph_index: u16::try_from(info.glyph_id)
1326 .ok()
1327 .filter(|&glyph| glyph < face.number_of_glyphs())
1328 .unwrap_or(0),
1329 cluster: run.range.start + usize::try_from(info.cluster).unwrap_or(0),
1330 advance: position.x_advance as f32 * font_size / units,
1331 x_offset: position.x_offset as f32 * font_size / units,
1332 y_offset: position.y_offset as f32 * font_size / units,
1333 });
1334 }
1335 if run.rtl {
1338 glyphs[first..].reverse();
1339 }
1340 }
1341 glyphs
1342}
1343
1344#[derive(Copy, Clone, Debug)]
1345struct ShapedGlyph {
1346 font: usize,
1347 glyph_index: u16,
1348 cluster: usize,
1350 advance: f32,
1351 x_offset: f32,
1354 y_offset: f32,
1355}
1356
1357#[derive(Clone, Debug, PartialEq)]
1359struct Cluster {
1360 glyphs: Range<usize>,
1361 start: usize,
1363 width: f32,
1364 hangs: bool,
1366 break_before: bool,
1368}
1369
1370fn clusters(glyphs: &[ShapedGlyph], paragraph: &str) -> Vec<Cluster> {
1374 let mut opportunities = unicode_linebreak::linebreaks(paragraph)
1375 .filter(|&(at, kind)| {
1376 kind == BreakOpportunity::Allowed
1377 && !(paragraph[..at].chars().next_back().is_some_and(is_hangul)
1378 && paragraph[at..].chars().next().is_some_and(is_hangul))
1379 })
1380 .map(|(at, _)| at)
1381 .peekable();
1382 let mut clusters = Vec::<Cluster>::new();
1383 for (index, glyph) in glyphs.iter().enumerate() {
1384 match clusters.last_mut() {
1385 Some(cluster) if cluster.start == glyph.cluster => {
1386 cluster.glyphs.end = index + 1;
1387 cluster.width += glyph.advance;
1388 }
1389 _ => clusters.push(Cluster {
1390 glyphs: index..index + 1,
1391 start: glyph.cluster,
1392 width: glyph.advance,
1393 hangs: false,
1394 break_before: false,
1395 }),
1396 }
1397 }
1398 for index in 0..clusters.len() {
1399 let start = clusters[index].start;
1400 let end = clusters
1401 .get(index + 1)
1402 .map(|next| next.start)
1403 .filter(|&end| end > start)
1404 .unwrap_or_else(|| {
1405 paragraph[start..]
1406 .chars()
1407 .next()
1408 .map_or(start, |character| start + character.len_utf8())
1409 });
1410 clusters[index].hangs = paragraph[start..end].chars().all(hangs);
1411 let mut found = false;
1412 while let Some(&at) = opportunities.peek() {
1413 if at > start {
1414 break;
1415 }
1416 opportunities.next();
1417 found = true;
1418 }
1419 clusters[index].break_before = index > 0 && found;
1420 }
1421 clusters
1422}
1423
1424fn break_lines(clusters: &[Cluster], max_width: f32) -> Vec<Range<usize>> {
1431 let mut lines = Vec::new();
1432 let mut start = 0;
1433 let mut x = 0.0_f32;
1434 let mut opportunity: Option<(usize, f32)> = None;
1436 for (index, cluster) in clusters.iter().enumerate() {
1437 if cluster.break_before && index > start {
1438 opportunity = Some((index, x));
1439 }
1440 if max_width > 0.0 && !cluster.hangs && index > start && x + cluster.width > max_width {
1441 let (end, width) = opportunity.unwrap_or((index, x));
1442 lines.push(start..end);
1443 start = end;
1444 x -= width;
1445 opportunity = None;
1446 if index > start && x + cluster.width > max_width {
1449 lines.push(start..index);
1450 start = index;
1451 x = 0.0;
1452 }
1453 }
1454 x += cluster.width;
1455 }
1456 lines.push(start..clusters.len());
1457 lines
1458}
1459
1460#[derive(Copy, Clone, Debug)]
1461struct PositionedGlyph {
1462 font: usize,
1463 glyph_index: u16,
1464 x: f32,
1466 x_offset: f32,
1468 y_offset: f32,
1469}
1470
1471enum Drawing {
1474 Outline(Outline),
1475 Painting(color::Painting),
1476 Bitmap(color::Bitmap),
1477}
1478
1479impl Drawing {
1480 fn of(faces: &Faces<'_>, font: usize, glyph: u16, request: &TextRasterRequest) -> Self {
1484 let face = &faces.parsed[font];
1485 if request.emoji_policy == EmojiPolicy::Color && faces.color[font] {
1486 let [red, green, blue, _] = request.color;
1487 if let Some(painting) = color::Painting::of(face, glyph, [red, green, blue, 255]) {
1488 return Self::Painting(painting);
1489 }
1490 if let Some(bitmap) = color::Bitmap::of(face, glyph, request.font_size) {
1491 return Self::Bitmap(bitmap);
1492 }
1493 }
1494 Self::Outline(Outline::of(face, glyph))
1495 }
1496
1497 fn bounds(&self) -> Option<[f32; 4]> {
1500 match self {
1501 Self::Outline(outline) => outline.bounds,
1502 Self::Painting(painting) => painting.bounds(),
1503 Self::Bitmap(bitmap) => Some(bitmap.bounds()),
1504 }
1505 }
1506
1507 fn ink_bounds(&self) -> Option<[f32; 4]> {
1512 match self {
1513 Self::Outline(outline) => outline.curve_bounds(),
1514 Self::Painting(painting) => painting.bounds(),
1515 Self::Bitmap(bitmap) => bitmap.ink_bounds(),
1516 }
1517 }
1518
1519 fn draw_color(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1522 match self {
1523 Self::Outline(_) => Vec::new(),
1524 Self::Painting(painting) => painting.draw(origin, scale, pixels),
1525 Self::Bitmap(bitmap) => bitmap.draw(origin, scale, pixels),
1526 }
1527 }
1528}
1529
1530#[derive(Default)]
1532struct Outline {
1533 segments: Vec<Segment>,
1534 bounds: Option<[f32; 4]>,
1537}
1538
1539enum Segment {
1540 Line([f32; 2], [f32; 2]),
1541 Quad([f32; 2], [f32; 2], [f32; 2]),
1542 Cubic([f32; 2], [f32; 2], [f32; 2], [f32; 2]),
1543}
1544
1545#[derive(Copy, Clone, Debug)]
1548struct PixelBox {
1549 left: i64,
1550 top: i64,
1551 right: i64,
1552 bottom: i64,
1553}
1554
1555impl PixelBox {
1556 #[allow(clippy::cast_possible_truncation)]
1559 fn of(bounds: [f32; 4], origin: [f32; 2], scale: f32) -> Self {
1560 let [x_min, y_min, x_max, y_max] = bounds;
1561 Self {
1564 left: (origin[0] + x_min * scale).floor() as i64,
1565 top: (origin[1] - y_max * scale).floor() as i64,
1566 right: (origin[0] + x_max * scale).ceil() as i64,
1567 bottom: (origin[1] - y_min * scale).ceil() as i64,
1568 }
1569 }
1570}
1571
1572#[cfg(test)]
1573thread_local! {
1574 static OUTLINES_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1576}
1577
1578impl Outline {
1579 fn of(face: &ttf_parser::Face<'_>, glyph: u16) -> Self {
1580 #[cfg(test)]
1581 OUTLINES_READ.with(|read| read.set(read.get() + 1));
1582 let mut sink = OutlineSink::default();
1583 face.outline_glyph(GlyphId(glyph), &mut sink);
1584 sink.close();
1585 sink.outline
1586 }
1587
1588 #[allow(
1591 clippy::cast_possible_truncation,
1592 clippy::cast_sign_loss,
1593 clippy::cast_precision_loss
1594 )]
1595 fn draw(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1596 let width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
1597 let height = usize::try_from(pixels.bottom - pixels.top).unwrap_or(0);
1598 let (left, top) = (pixels.left as f32, pixels.top as f32);
1599 let raster = self.raster(width, height, |[x, y]| {
1600 point(origin[0] + x * scale - left, (origin[1] - y * scale) - top)
1601 });
1602 let mut alpha = vec![0_u8; width * height];
1603 raster.for_each_pixel(|index, coverage| {
1604 alpha[index] = (coverage.min(1.0) * 255.0).round() as u8;
1605 });
1606 alpha
1607 }
1608}
1609
1610impl Outline {
1611 fn curve_bounds(&self) -> Option<[f32; 4]> {
1615 let mut bounds = None;
1616 let mut hold = |[x, y]: [f32; 2]| include(&mut bounds, [x, y, x, y]);
1617 for segment in &self.segments {
1618 match *segment {
1619 Segment::Line(from, to) => {
1620 hold(from);
1621 hold(to);
1622 }
1623 Segment::Quad(from, control, to) => {
1624 hold(from);
1625 hold(to);
1626 for axis in 0..2 {
1627 let curvature = from[axis] - 2.0 * control[axis] + to[axis];
1629 let t = (from[axis] - control[axis]) / curvature;
1630 if t > 0.0 && t < 1.0 {
1631 hold(std::array::from_fn(|axis| {
1632 let u = 1.0 - t;
1633 u * u * from[axis] + 2.0 * u * t * control[axis] + t * t * to[axis]
1634 }));
1635 }
1636 }
1637 }
1638 Segment::Cubic(from, first, second, to) => {
1639 hold(from);
1640 hold(to);
1641 for axis in 0..2 {
1642 let [p0, p1, p2, p3] = [from[axis], first[axis], second[axis], to[axis]];
1643 for t in quadratic_roots(
1644 -p0 + 3.0 * (p1 - p2) + p3,
1645 2.0 * (p0 - 2.0 * p1 + p2),
1646 p1 - p0,
1647 )
1648 .into_iter()
1649 .flatten()
1650 .filter(|t| *t > 0.0 && *t < 1.0)
1651 {
1652 hold(std::array::from_fn(|axis| {
1653 let u = 1.0 - t;
1654 u * u * u * from[axis]
1655 + 3.0 * u * u * t * first[axis]
1656 + 3.0 * u * t * t * second[axis]
1657 + t * t * t * to[axis]
1658 }));
1659 }
1660 }
1661 }
1662 }
1663 }
1664 bounds
1665 }
1666
1667 fn raster(&self, width: usize, height: usize, at: impl Fn([f32; 2]) -> Point) -> Rasterizer {
1670 let mut raster = Rasterizer::new(width, height);
1671 for segment in &self.segments {
1672 match *segment {
1673 Segment::Line(from, to) => raster.draw_line(at(from), at(to)),
1674 Segment::Quad(from, control, to) => raster.draw_quad(at(from), at(control), at(to)),
1675 Segment::Cubic(from, first, second, to) => {
1676 raster.draw_cubic(at(from), at(first), at(second), at(to));
1677 }
1678 }
1679 }
1680 raster
1681 }
1682}
1683
1684fn quadratic_roots(a: f32, b: f32, c: f32) -> [Option<f32>; 2] {
1687 let finite = |t: f32| t.is_finite().then_some(t);
1688 if a.abs() <= f32::EPSILON * (b.abs() + c.abs()) {
1689 return [finite(-c / b), None];
1690 }
1691 let discriminant = b * b - 4.0 * a * c;
1692 if discriminant < 0.0 {
1693 return [None, None];
1694 }
1695 let root = discriminant.sqrt();
1696 [
1697 finite((-b + root) / (2.0 * a)),
1698 finite((-b - root) / (2.0 * a)),
1699 ]
1700}
1701
1702#[derive(Default)]
1704struct OutlineSink {
1705 outline: Outline,
1706 start: [f32; 2],
1707 pen: [f32; 2],
1708}
1709
1710impl OutlineSink {
1711 fn include(&mut self, points: &[[f32; 2]]) {
1712 for &[x, y] in points {
1713 let bounds = self.outline.bounds.get_or_insert([x, y, x, y]);
1714 *bounds = [
1715 bounds[0].min(x),
1716 bounds[1].min(y),
1717 bounds[2].max(x),
1718 bounds[3].max(y),
1719 ];
1720 }
1721 }
1722}
1723
1724impl OutlineBuilder for OutlineSink {
1725 fn move_to(&mut self, x: f32, y: f32) {
1726 self.close();
1727 self.start = [x, y];
1728 self.pen = [x, y];
1729 }
1730
1731 fn line_to(&mut self, x: f32, y: f32) {
1732 let to = [x, y];
1733 self.include(&[self.pen, to]);
1734 self.outline.segments.push(Segment::Line(self.pen, to));
1735 self.pen = to;
1736 }
1737
1738 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1739 let (control, to) = ([x1, y1], [x, y]);
1740 self.include(&[self.pen, control, to]);
1741 self.outline
1742 .segments
1743 .push(Segment::Quad(self.pen, control, to));
1744 self.pen = to;
1745 }
1746
1747 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1748 let (first, second, to) = ([x1, y1], [x2, y2], [x, y]);
1749 self.include(&[self.pen, first, second, to]);
1750 self.outline
1751 .segments
1752 .push(Segment::Cubic(self.pen, first, second, to));
1753 self.pen = to;
1754 }
1755
1756 fn close(&mut self) {
1757 if self.pen.map(f32::to_bits) != self.start.map(f32::to_bits) {
1760 self.line_to(self.start[0], self.start[1]);
1761 }
1762 }
1763}
1764
1765fn validate_request(request: &TextRasterRequest) -> Result<(), TextRasterError> {
1766 if request.font_size.is_finite()
1767 && request.font_size > 0.0
1768 && request.font_size <= MAX_FONT_SIZE
1769 && request.text.len() <= MAX_TEXT_BYTES
1770 && request.max_width.is_finite()
1771 && request.max_width <= 16_384.0
1772 && request.line_height.is_finite()
1773 && request.line_height > 0.0
1774 && request.line_height <= MAX_LINE_HEIGHT
1775 && request.content_version > 0
1776 && request.outline.is_none_or(|outline| {
1777 outline.width.is_finite() && outline.width > 0.0 && outline.width <= MAX_OUTLINE_WIDTH
1778 })
1779 && request.background.is_none_or(|background| {
1780 background.padding.is_finite()
1781 && background.padding >= 0.0
1782 && background.padding <= MAX_BACKGROUND_PADDING
1783 })
1784 {
1785 Ok(())
1786 } else {
1787 Err(TextRasterError::InvalidRequest)
1788 }
1789}
1790
1791fn checked_dimension(value: f32) -> Result<u32, TextRasterError> {
1792 if value.is_finite() && value > 0.0 && value.ceil() <= 16_384.0 {
1793 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1794 Ok(value.ceil() as u32)
1795 } else {
1796 Err(TextRasterError::TooLarge)
1797 }
1798}
1799
1800fn place_glyph(
1804 mask: &mut [u8],
1805 width: u32,
1806 height: u32,
1807 origin_x: i64,
1808 origin_y: i64,
1809 glyph_width: usize,
1810 alpha: &[u8],
1811) {
1812 for (index, coverage) in alpha.iter().copied().enumerate() {
1813 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
1814 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
1815 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) {
1816 continue;
1817 }
1818 let pixel =
1819 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
1820 let slot = &mut mask[usize::try_from(pixel).unwrap_or(0)];
1821 *slot = (*slot).max(coverage);
1822 }
1823}
1824
1825fn place_color_glyph(
1828 colors: &mut [u8],
1829 width: u32,
1830 height: u32,
1831 origin_x: i64,
1832 origin_y: i64,
1833 glyph_width: usize,
1834 rgba8: &[u8],
1835) {
1836 for (index, source) in rgba8.as_chunks::<4>().0.iter().enumerate() {
1837 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
1838 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
1839 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) || source[3] == 0 {
1840 continue;
1841 }
1842 let pixel =
1843 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
1844 let slot = &mut colors[usize::try_from(pixel).unwrap_or(0) * 4..][..4];
1845 let blended = over(
1846 std::array::from_fn(|channel| u32::from(source[channel])),
1847 std::array::from_fn(|channel| u32::from(slot[channel])),
1848 );
1849 for (byte, value) in slot.iter_mut().zip(blended) {
1850 *byte = u8::try_from(value.min(255)).unwrap_or(u8::MAX);
1851 }
1852 }
1853}
1854
1855impl TextRasterRequest {
1856 #[must_use]
1861 pub fn decoration_margin(&self) -> u32 {
1862 let (outline, padding) = decoration_extents(self);
1863 outline.max(padding)
1864 }
1865}
1866
1867fn decoration_extents(request: &TextRasterRequest) -> (u32, u32) {
1870 let outline = request
1871 .outline
1872 .map_or(0, |outline| disc_extent(outline.width));
1873 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1874 let padding = request
1875 .background
1876 .map_or(0, |background| background.padding.ceil() as u32);
1877 (outline, padding)
1878}
1879
1880#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1882fn disc_extent(width: f32) -> u32 {
1883 (width + 0.5).ceil() as u32
1884}
1885
1886fn premultiplied(color: [u8; 4], coverage: u8) -> [u32; 4] {
1890 let alpha = u32::from(coverage) * u32::from(color[3]) / 255;
1891 [
1892 u32::from(color[0]) * alpha / 255,
1893 u32::from(color[1]) * alpha / 255,
1894 u32::from(color[2]) * alpha / 255,
1895 alpha,
1896 ]
1897}
1898
1899fn over(source: [u32; 4], destination: [u32; 4]) -> [u32; 4] {
1901 let keep = 255 - source[3];
1902 std::array::from_fn(|channel| source[channel] + (destination[channel] * keep + 127) / 255)
1903}
1904
1905#[allow(
1911 clippy::cast_possible_truncation,
1912 clippy::cast_sign_loss,
1913 clippy::cast_precision_loss
1914)]
1915fn compose(
1916 request: &TextRasterRequest,
1917 coverage: &[u8],
1918 colors: Option<&[u8]>,
1919 width: u32,
1920 margin: [u32; 2],
1921 line_boxes: &[(f32, f32, f32)],
1922) -> Vec<u8> {
1923 let outline = request.outline.map(|outline| {
1924 let ink = colors.map(|colors| {
1925 coverage
1926 .iter()
1927 .zip(colors.as_chunks::<4>().0)
1928 .map(|(&text, color)| text.max(color[3]))
1929 .collect::<Vec<_>>()
1930 });
1931 (
1932 outline.color,
1933 dilate(
1934 ink.as_deref().unwrap_or(coverage),
1935 width,
1936 u32::try_from(coverage.len() / width.max(1) as usize).unwrap_or(0),
1937 outline.width,
1938 ),
1939 )
1940 });
1941 let background = request.background.map(|background| {
1942 let [margin_x, margin_y] = margin.map(|margin| margin as f32);
1943 let padding = background.padding;
1944 let boxes = line_boxes
1945 .iter()
1946 .map(|&(left, line_width, top)| {
1947 (
1948 margin_x + left - padding,
1949 margin_x + left + line_width + padding,
1950 margin_y + top - padding,
1951 margin_y + top + request.font_size + padding,
1952 )
1953 })
1954 .collect::<Vec<_>>();
1955 (background.color, boxes)
1956 });
1957 let mut rgba8 = vec![0_u8; coverage.len() * 4];
1958 let (mut first, mut last) = (0, 0);
1961 for (index, text) in coverage.iter().copied().enumerate() {
1962 let mut pixel = [0_u32; 4];
1963 if let Some((color, boxes)) = &background {
1964 let x = (index % width as usize) as f32;
1965 let y = (index / width as usize) as f32;
1966 if index % width as usize == 0 {
1967 while first < boxes.len() && boxes[first].3 <= y {
1968 first += 1;
1969 }
1970 last = last.max(first);
1971 while last < boxes.len() && boxes[last].2 < y + 1.0 {
1972 last += 1;
1973 }
1974 }
1975 let covered = boxes[first..last]
1976 .iter()
1977 .map(|&(left, right, top, bottom)| {
1978 let across = (right.min(x + 1.0) - left.max(x)).clamp(0.0, 1.0);
1979 let down = (bottom.min(y + 1.0) - top.max(y)).clamp(0.0, 1.0);
1980 across * down
1981 })
1982 .fold(0.0_f32, f32::max);
1983 pixel = premultiplied(*color, (covered * 255.0).round() as u8);
1984 }
1985 if let Some((color, band)) = &outline {
1986 pixel = over(premultiplied(*color, band[index]), pixel);
1987 }
1988 pixel = over(premultiplied(request.color, text), pixel);
1989 if let Some(colors) = colors {
1990 let color = &colors[index * 4..][..4];
1991 if color[3] > 0 {
1992 let opacity = u32::from(request.color[3]);
1993 pixel = over(
1994 std::array::from_fn(|channel| u32::from(color[channel]) * opacity / 255),
1995 pixel,
1996 );
1997 }
1998 }
1999 for (channel, value) in pixel.iter().enumerate() {
2000 rgba8[index * 4 + channel] = (*value).min(255) as u8;
2001 }
2002 }
2003 rgba8
2004}
2005
2006#[allow(
2017 clippy::cast_possible_truncation,
2018 clippy::cast_sign_loss,
2019 clippy::cast_precision_loss,
2020 clippy::cast_possible_wrap
2021)]
2022fn dilate(coverage: &[u8], width: u32, height: u32, radius: f32) -> Vec<u8> {
2023 let (width, height) = (width as usize, height as usize);
2024 let mut band = vec![0_u8; coverage.len()];
2025 let Some(ink) = ink_bounds(coverage, width, height) else {
2026 return band;
2027 };
2028 let disc = Disc::new(radius);
2029 let extent = disc.extent;
2030 let reach = Bounds {
2031 left: ink.left.saturating_sub(extent),
2032 top: ink.top.saturating_sub(extent),
2033 right: (ink.right + extent).min(width),
2034 bottom: (ink.bottom + extent).min(height),
2035 };
2036 let span = reach.right - reach.left;
2039 let mut window = WindowMax::default();
2040 let mut maxima = vec![0_u8; span];
2041 for (half, offsets) in &disc.runs {
2042 for source_y in ink.top..ink.bottom {
2043 let row = &coverage[source_y * width..(source_y + 1) * width];
2044 window.of(row, reach.left, span, *half, &mut maxima);
2045 for &offset in offsets {
2046 let Some(y) = source_y.checked_add_signed(-offset) else {
2048 continue;
2049 };
2050 if y < reach.top || y >= reach.bottom {
2051 continue;
2052 }
2053 let out = &mut band[y * width + reach.left..y * width + reach.right];
2054 for (pixel, highest) in out.iter_mut().zip(&maxima) {
2055 *pixel = (*pixel).max(*highest);
2056 }
2057 }
2058 }
2059 }
2060 for &(dx, dy, weight) in &disc.rim {
2061 for source_y in ink.top..ink.bottom {
2062 let Some(y) = source_y.checked_add_signed(-dy) else {
2063 continue;
2064 };
2065 if y >= height {
2066 continue;
2067 }
2068 let first = ink.left.max(usize::try_from(dx).unwrap_or(0));
2071 let last = ink
2072 .right
2073 .min(usize::try_from(width as isize + dx.min(0)).unwrap_or(0));
2074 if first >= last {
2075 continue;
2076 }
2077 let source = &coverage[source_y * width + first..source_y * width + last];
2078 let start = y * width + (first as isize - dx) as usize;
2079 let out = &mut band[start..start + source.len()];
2080 for (pixel, value) in out.iter_mut().zip(source) {
2081 *pixel = (*pixel).max((u32::from(*value) * weight / 255) as u8);
2082 }
2083 }
2084 }
2085 band
2086}
2087
2088#[derive(Copy, Clone, Debug, Eq, PartialEq)]
2090struct Bounds {
2091 left: usize,
2092 top: usize,
2093 right: usize,
2094 bottom: usize,
2095}
2096
2097fn ink_bounds(coverage: &[u8], width: usize, height: usize) -> Option<Bounds> {
2099 let mut bounds: Option<Bounds> = None;
2100 for y in 0..height {
2101 let row = &coverage[y * width..(y + 1) * width];
2102 let Some(left) = row.iter().position(|&value| value > 0) else {
2103 continue;
2104 };
2105 let right = row.iter().rposition(|&value| value > 0).unwrap_or(left) + 1;
2106 bounds = Some(bounds.map_or(
2107 Bounds {
2108 left,
2109 top: y,
2110 right,
2111 bottom: y + 1,
2112 },
2113 |bounds| Bounds {
2114 left: bounds.left.min(left),
2115 top: bounds.top,
2116 right: bounds.right.max(right),
2117 bottom: y + 1,
2118 },
2119 ));
2120 }
2121 bounds
2122}
2123
2124struct Disc {
2127 extent: usize,
2129 runs: Vec<(usize, Vec<isize>)>,
2132 rim: Vec<(isize, isize, u32)>,
2134}
2135
2136impl Disc {
2137 #[allow(
2140 clippy::cast_possible_truncation,
2141 clippy::cast_sign_loss,
2142 clippy::cast_precision_loss,
2143 clippy::cast_possible_wrap
2144 )]
2145 fn new(radius: f32) -> Self {
2146 let extent = disc_extent(radius) as isize;
2147 let mut runs: Vec<(usize, Vec<isize>)> = Vec::new();
2148 let mut rim = Vec::new();
2149 for dy in -extent..=extent {
2150 let mut half = None;
2151 for dx in -extent..=extent {
2152 let distance = ((dx * dx + dy * dy) as f32).sqrt();
2153 let weight = ((radius + 0.5 - distance).clamp(0.0, 1.0) * 255.0).round() as u32;
2154 match weight {
2155 0 => {}
2156 255 => half = Some(dx.unsigned_abs()),
2159 _ => rim.push((dx, dy, weight)),
2160 }
2161 }
2162 if let Some(half) = half {
2163 match runs.iter_mut().find(|(run, _)| *run == half) {
2164 Some((_, offsets)) => offsets.push(dy),
2165 None => runs.push((half, vec![dy])),
2166 }
2167 }
2168 }
2169 Self {
2170 extent: extent as usize,
2171 runs,
2172 rim,
2173 }
2174 }
2175}
2176
2177#[derive(Default)]
2180struct WindowMax {
2181 padded: Vec<u8>,
2182 from_start: Vec<u8>,
2183 to_end: Vec<u8>,
2184}
2185
2186impl WindowMax {
2187 fn of(&mut self, row: &[u8], left: usize, count: usize, half: usize, out: &mut [u8]) {
2190 let window = 2 * half + 1;
2191 let length = count + 2 * half;
2192 self.padded.clear();
2193 self.padded.resize(length, 0);
2194 let first_column = left.saturating_sub(half);
2196 let last_column = (left + count + half).min(row.len());
2197 if first_column < last_column {
2198 let at = first_column + half - left;
2199 self.padded[at..at + last_column - first_column]
2200 .copy_from_slice(&row[first_column..last_column]);
2201 }
2202 self.from_start.clear();
2203 self.from_start.resize(length, 0);
2204 self.to_end.clear();
2205 self.to_end.resize(length, 0);
2206 for block in (0..length).step_by(window) {
2207 let end = (block + window).min(length);
2208 let mut highest = 0;
2209 for index in block..end {
2210 highest = highest.max(self.padded[index]);
2211 self.from_start[index] = highest;
2212 }
2213 highest = 0;
2214 for index in (block..end).rev() {
2215 highest = highest.max(self.padded[index]);
2216 self.to_end[index] = highest;
2217 }
2218 }
2219 for (index, value) in out[..count].iter_mut().enumerate() {
2220 *value = self.to_end[index].max(self.from_start[index + window - 1]);
2221 }
2222 }
2223}
2224
2225#[cfg(test)]
2226mod tests;