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;
41const LETTER_SPACING_RANGE: std::ops::RangeInclusive<f32> = -0.5..=1.0;
43
44#[derive(Clone, Debug)]
46pub struct TextFont {
47 pub family: String,
49 pub bytes: Arc<[u8]>,
51 pub face_index: u32,
53}
54
55#[derive(Clone, Debug, PartialEq)]
57pub struct TextRasterRequest {
58 pub text: String,
60 pub font_size: f32,
62 pub max_width: f32,
64 pub line_height: f32,
68 pub align: TextAlign,
70 pub color: [u8; 4],
75 pub emoji_policy: EmojiPolicy,
77 pub content_version: u64,
79 pub outline: Option<TextOutline>,
81 pub background: Option<TextBackground>,
83 pub font_family: Option<String>,
87 pub letter_spacing: f32,
93 pub font_weight: u16,
98}
99
100#[derive(Copy, Clone, Debug, PartialEq)]
107pub struct TextOutline {
108 pub color: [u8; 4],
110 pub width: f32,
112}
113
114#[derive(Copy, Clone, Debug, PartialEq)]
118pub struct TextBackground {
119 pub color: [u8; 4],
121 pub padding: f32,
123}
124
125#[derive(Clone, Debug, PartialEq)]
127pub struct TextRasterResult {
128 pub descriptor: TextTexture,
130 pub pixels: HostTexturePixels,
132}
133
134#[derive(Clone, Debug, PartialEq)]
140pub struct TextMeasure {
141 pub box_width: u32,
144 pub box_height: u32,
147 pub decoration_margin: u32,
149 pub content: [f32; 4],
154 pub ink: Option<[f32; 4]>,
161 pub line_count: u32,
163 pub first_baseline: f32,
166 pub last_baseline: f32,
169}
170
171#[derive(Clone, Debug)]
175pub struct ShapedText {
176 paragraphs: Vec<Vec<Cluster>>,
178 font_size: f32,
179 line_height: f32,
180 line_advance: f32,
181}
182
183#[derive(Copy, Clone, Debug, PartialEq)]
185pub struct LineMetrics {
186 pub line_count: u32,
188 pub widest: f32,
190 pub height: f32,
193 pub first_baseline: f32,
196 pub last_baseline: f32,
198}
199
200impl ShapedText {
201 #[must_use]
204 pub fn min_content_width(&self) -> f32 {
205 let mut widest = 0.0_f32;
206 for clusters in &self.paragraphs {
207 let mut start = 0;
208 for index in 1..=clusters.len() {
209 if index == clusters.len() || clusters[index].break_before {
210 widest = widest.max(advance(&clusters[start..index]));
211 start = index;
212 }
213 }
214 }
215 widest
216 }
217
218 #[must_use]
221 pub fn max_content_width(&self) -> f32 {
222 self.paragraphs
223 .iter()
224 .map(|clusters| advance(clusters))
225 .fold(0.0, f32::max)
226 }
227
228 #[must_use]
231 pub fn lines_at(&self, max_width: f32) -> LineMetrics {
232 let (mut count, mut kept, mut widest) = (0_u32, 1_u32, 0.0_f32);
233 for clusters in &self.paragraphs {
234 for range in break_lines(clusters, max_width) {
235 count = count.saturating_add(1);
236 if !range.is_empty() {
237 kept = count;
238 }
239 widest = widest.max(advance(&clusters[range]));
240 }
241 }
242 let [first_baseline, last_baseline] =
243 baselines(kept, self.font_size, self.line_height, self.line_advance);
244 LineMetrics {
245 line_count: kept,
246 widest,
247 height: box_height(kept, self.line_advance, self.font_size),
248 first_baseline,
249 last_baseline,
250 }
251 }
252}
253
254fn advance(clusters: &[Cluster]) -> f32 {
258 let Some(last) = clusters.iter().rposition(|cluster| !cluster.hangs) else {
259 return 0.0;
260 };
261 clusters[..=last]
262 .iter()
263 .fold(0.0, |width, cluster| width + cluster.width)
264 - clusters[last].spacing
265}
266
267#[must_use]
271pub fn half_leading(font_size: f32, line_height: f32) -> f32 {
272 (font_size * line_height - font_size).max(0.0) * 0.5
273}
274
275#[allow(clippy::cast_precision_loss)]
279fn baselines(line_count: u32, font_size: f32, line_height: f32, line_advance: f32) -> [f32; 2] {
280 let (leading, ascent) = (half_leading(font_size, line_height), font_size * 0.8);
281 let last = line_count.saturating_sub(1) as f32;
282 [leading + ascent, last * line_advance + leading + ascent]
283}
284
285#[allow(clippy::cast_precision_loss)]
288fn box_height(line_count: u32, line_advance: f32, font_size: f32) -> f32 {
289 line_count.saturating_sub(1) as f32 * line_advance + line_advance.max(font_size)
290}
291
292#[derive(Clone, Debug, Eq, PartialEq)]
294#[non_exhaustive]
295pub enum TextRasterError {
296 NoFonts,
298 InvalidFont(String),
300 InvalidRequest,
302 TooLarge,
304}
305
306impl fmt::Display for TextRasterError {
307 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
308 match self {
309 Self::NoFonts => formatter.write_str("text rasterizer requires at least one font"),
310 Self::InvalidFont(error) => write!(formatter, "invalid text font: {error}"),
311 Self::InvalidRequest => formatter.write_str("invalid text raster request"),
312 Self::TooLarge => formatter.write_str("text raster result is too large"),
313 }
314 }
315}
316
317impl std::error::Error for TextRasterError {}
318
319#[derive(Clone, Debug)]
320struct LoadedFont {
321 source: TextFont,
322 family: Option<String>,
324 weight: u16,
326 wght: Option<(f32, f32, f32)>,
328}
329
330const WGHT: ttf_parser::Tag = ttf_parser::Tag::from_bytes(b"wght");
331
332impl LoadedFont {
333 fn parse(source: TextFont) -> Result<Self, TextRasterError> {
337 let face = ttf_parser::Face::parse(&source.bytes, source.face_index)
338 .map_err(|error| TextRasterError::InvalidFont(error.to_string()))?;
339 let weight = face.weight().to_number().clamp(1, 1000);
340 let wght = face
341 .variation_axes()
342 .into_iter()
343 .find(|axis| axis.tag == WGHT)
344 .map(|axis| (axis.min_value, axis.def_value, axis.max_value));
345 let family = font_family(&source.bytes, source.face_index);
346 Ok(Self {
347 source,
348 family,
349 weight,
350 wght,
351 })
352 }
353
354 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
357 fn weights(&self) -> (u16, u16) {
358 self.wght
359 .map_or((self.weight, self.weight), |(min, _, max)| {
360 let whole = |value: f32| value.clamp(1.0, 1000.0) as u16;
361 (whole(min.ceil()), whole(max.floor()).max(whole(min.ceil())))
362 })
363 }
364}
365
366fn weight_order(weights: &[(u16, u16)], desired: u16) -> Vec<usize> {
374 let key = |&(min, max): &(u16, u16)| -> (u8, u16) {
375 if (min..=max).contains(&desired) {
376 return (0, 0);
377 }
378 let weight = if desired < min { min } else { max };
379 let (above, distance) = (weight > desired, weight.abs_diff(desired));
380 if (400..=500).contains(&desired) {
381 match (above, weight <= 500) {
382 (true, true) => (1, distance),
383 (false, _) => (2, distance),
384 (true, false) => (3, distance),
385 }
386 } else if (desired < 400) != above {
387 (1, distance)
388 } else {
389 (2, distance)
390 }
391 };
392 let mut order = (0..weights.len()).collect::<Vec<_>>();
393 order.sort_by_key(|&index| key(&weights[index]));
394 order
395}
396
397pub fn check_font(bytes: &[u8], face_index: u32) -> Result<(), TextRasterError> {
405 ttf_parser::Face::parse(bytes, face_index)
406 .map(|_| ())
407 .map_err(|error| TextRasterError::InvalidFont(error.to_string()))
408}
409
410#[must_use]
414pub fn face_count(bytes: &[u8]) -> u32 {
415 ttf_parser::fonts_in_collection(bytes).unwrap_or(1)
416}
417
418#[must_use]
423pub fn font_family(bytes: &[u8], face_index: u32) -> Option<String> {
424 use rustybuzz::ttf_parser::{Language, name_id};
425 let face = rustybuzz::ttf_parser::Face::parse(bytes, face_index).ok()?;
426 let names = face.names();
427 [name_id::TYPOGRAPHIC_FAMILY, name_id::FAMILY]
428 .into_iter()
429 .find_map(|id| {
430 let decoded = || {
431 names
432 .into_iter()
433 .filter(move |name| name.name_id == id)
434 .filter_map(|name| {
435 let text = name.to_string()?;
436 (!text.trim().is_empty()).then_some((name.language(), text))
437 })
438 };
439 decoded()
440 .find(|(language, _)| *language == Language::English_UnitedStates)
441 .or_else(|| decoded().next())
442 .map(|(_, text)| text)
443 })
444}
445
446pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
449
450const CACHE_ENTRY_OVERHEAD: usize = 256;
453
454#[derive(Debug)]
461pub struct TextRasterizer {
462 fonts: Vec<LoadedFont>,
463 cache: ResultCache,
464}
465
466#[derive(Debug)]
469struct ResultCache {
470 entries: HashMap<u64, CacheEntry>,
471 by_use: BTreeMap<u64, u64>,
473 next_use: u64,
474 bytes: usize,
475 budget: usize,
476}
477
478#[derive(Debug)]
479struct CacheEntry {
480 result: Arc<TextRasterResult>,
481 last_use: u64,
482 bytes: usize,
483}
484
485impl ResultCache {
486 fn new(budget: usize) -> Self {
487 Self {
488 entries: HashMap::new(),
489 by_use: BTreeMap::new(),
490 next_use: 0,
491 bytes: 0,
492 budget,
493 }
494 }
495
496 fn get(&mut self, version: u64) -> Option<Arc<TextRasterResult>> {
497 let entry = self.entries.get_mut(&version)?;
498 self.by_use.remove(&entry.last_use);
499 entry.last_use = self.next_use;
500 self.by_use.insert(self.next_use, version);
501 self.next_use += 1;
502 Some(Arc::clone(&entry.result))
503 }
504
505 fn insert(&mut self, version: u64, result: &Arc<TextRasterResult>) {
508 self.remove(version);
509 let bytes = result
510 .pixels
511 .rgba8
512 .len()
513 .saturating_add(CACHE_ENTRY_OVERHEAD);
514 if bytes > self.budget {
515 return;
516 }
517 self.bytes += bytes;
518 self.entries.insert(
519 version,
520 CacheEntry {
521 result: Arc::clone(result),
522 last_use: self.next_use,
523 bytes,
524 },
525 );
526 self.by_use.insert(self.next_use, version);
527 self.next_use += 1;
528 self.evict();
529 }
530
531 fn remove(&mut self, version: u64) {
532 if let Some(entry) = self.entries.remove(&version) {
533 self.by_use.remove(&entry.last_use);
534 self.bytes -= entry.bytes;
535 }
536 }
537
538 fn clear(&mut self) {
539 self.entries.clear();
540 self.by_use.clear();
541 self.bytes = 0;
542 }
543
544 fn set_budget(&mut self, budget: usize) {
545 self.budget = budget;
546 self.evict();
547 }
548
549 fn evict(&mut self) {
550 while self.bytes > self.budget {
551 let Some((_, version)) = self.by_use.pop_first() else {
552 break;
553 };
554 if let Some(entry) = self.entries.remove(&version) {
555 self.bytes -= entry.bytes;
556 }
557 }
558 }
559}
560
561impl TextRasterizer {
562 #[must_use]
564 pub fn empty() -> Self {
565 Self {
566 fonts: Vec::new(),
567 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
568 }
569 }
570
571 pub fn new(fonts: Vec<TextFont>) -> Result<Self, TextRasterError> {
576 if fonts.is_empty() {
577 return Err(TextRasterError::NoFonts);
578 }
579 let loaded = fonts
580 .into_iter()
581 .map(LoadedFont::parse)
582 .collect::<Result<Vec<_>, _>>()?;
583 Ok(Self {
584 fonts: loaded,
585 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
586 })
587 }
588
589 pub fn add_font(&mut self, source: TextFont) -> Result<(), TextRasterError> {
597 self.fonts.push(LoadedFont::parse(source)?);
598 self.cache.clear();
599 Ok(())
600 }
601
602 #[must_use]
604 pub fn font_count(&self) -> usize {
605 self.fonts.len()
606 }
607
608 pub fn rasterize(
614 &mut self,
615 request: &TextRasterRequest,
616 ) -> Result<Arc<TextRasterResult>, TextRasterError> {
617 validate_request(request)?;
618 if self.fonts.is_empty() {
619 return Err(TextRasterError::NoFonts);
620 }
621 if let Some(cached) = self.cache.get(request.content_version) {
622 return Ok(cached);
623 }
624 let result = Arc::new(self.rasterize_uncached(request)?);
625 self.cache.insert(request.content_version, &result);
626 Ok(result)
627 }
628
629 pub fn invalidate(&mut self, content_version: u64) {
631 self.cache.remove(content_version);
632 }
633
634 pub fn set_cache_budget(&mut self, bytes: usize) {
639 self.cache.set_budget(bytes);
640 }
641
642 #[must_use]
644 pub fn cache_bytes(&self) -> usize {
645 self.cache.bytes
646 }
647
648 pub fn clear(&mut self) {
650 self.cache.clear();
651 }
652
653 #[must_use]
655 pub fn cache_len(&self) -> usize {
656 self.cache.entries.len()
657 }
658
659 #[allow(clippy::cast_precision_loss)]
666 pub fn measure(&self, request: &TextRasterRequest) -> Result<TextMeasure, TextRasterError> {
667 validate_request(request)?;
668 if self.fonts.is_empty() {
669 return Err(TextRasterError::NoFonts);
670 }
671 let faces = Faces::new(&self.fonts, request.font_weight)?;
672 let layout = self.lay_out(&faces, request)?;
673 let mut ink = None;
674 layout.each_glyph(
675 &faces,
676 request,
677 &mut HashMap::new(),
678 |drawing, origin, scale, _| {
679 if let Some([x_min, y_min, x_max, y_max]) = drawing.ink_bounds() {
680 include(
681 &mut ink,
682 [
683 origin[0] + x_min * scale,
684 origin[1] - y_max * scale,
685 origin[0] + x_max * scale,
686 origin[1] - y_min * scale,
687 ],
688 );
689 }
690 },
691 );
692 let band = request.outline.map_or(0.0, |outline| outline.width + 1.0);
695 ink = ink.map(|bounds| grown(bounds, band));
696 let padding = request.background.map(|background| background.padding);
697 let mut content = None;
698 for (index, line) in layout.lines.iter().enumerate() {
699 let left = layout.line_offset(line, request.align);
700 let top = layout.line_top(index);
701 include(&mut content, [left, 0.0, left + line.width, layout.height]);
702 if let (Some(padding), true) = (padding, line.width > 0.0) {
703 include(
704 &mut ink,
705 grown(
706 [left, top, left + line.width, top + request.font_size],
707 padding,
708 ),
709 );
710 }
711 }
712 let content = content.unwrap_or([0.0, 0.0, 0.0, layout.height]);
713 let last = layout.lines.len().saturating_sub(1);
714 Ok(TextMeasure {
715 first_baseline: layout.line_top(0) + layout.ascent,
716 last_baseline: layout.line_top(last) + layout.ascent,
717 box_width: layout.box_width,
718 box_height: layout.box_height,
719 decoration_margin: request.decoration_margin(),
720 content: grown(content, request.decoration_margin() as f32),
721 ink,
722 line_count: layout.line_count,
723 })
724 }
725
726 pub fn shape(&self, request: &TextRasterRequest) -> Result<ShapedText, TextRasterError> {
736 validate_request(&TextRasterRequest {
737 text: String::new(),
738 max_width: 0.0,
739 content_version: 1,
740 outline: None,
741 background: None,
742 ..request.clone()
743 })?;
744 if request.text.len() > MAX_TEXT_BYTES {
745 return Err(TextRasterError::InvalidRequest);
746 }
747 if self.fonts.is_empty() {
748 return Err(TextRasterError::NoFonts);
749 }
750 let faces = Faces::new(&self.fonts, request.font_weight)?;
751 let order = self.font_order(request.font_family.as_deref(), request.font_weight);
752 let paragraphs = normalize(&request.text, request.emoji_policy)
753 .split('\n')
754 .map(|paragraph| {
755 let bidi = ParagraphBidiInfo::new(paragraph, None);
756 let glyphs = shape_paragraph(
757 &faces,
758 paragraph,
759 &bidi,
760 request.font_size,
761 &order,
762 request.emoji_policy,
763 request.letter_spacing,
764 );
765 clusters(&glyphs, paragraph, request.letter_spacing)
766 })
767 .collect();
768 Ok(ShapedText {
769 paragraphs,
770 font_size: request.font_size,
771 line_height: request.line_height,
772 line_advance: request.font_size * request.line_height,
773 })
774 }
775
776 #[allow(clippy::cast_precision_loss)]
778 fn lay_out(
779 &self,
780 faces: &Faces<'_>,
781 request: &TextRasterRequest,
782 ) -> Result<Layout, TextRasterError> {
783 let line_advance = request.font_size * request.line_height;
784 let order = self.font_order(request.font_family.as_deref(), request.font_weight);
785 let mut lines = Vec::new();
786 let mut kept = 1;
789 for paragraph in normalize(&request.text, request.emoji_policy).split('\n') {
790 for line in layout_paragraph(
791 faces,
792 paragraph,
793 request.font_size,
794 request.max_width,
795 &order,
796 request.emoji_policy,
797 request.letter_spacing,
798 ) {
799 lines.push(line);
800 if lines.last().is_some_and(|line| line.has_glyphs) {
801 kept = lines.len();
802 }
803 }
804 }
805 lines.truncate(kept);
806 let line_count = u32::try_from(lines.len()).map_err(|_| TextRasterError::TooLarge)?;
807 let width = if request.max_width > 0.0 {
808 request.max_width
809 } else {
810 lines.iter().map(|line| line.width).fold(0.0, f32::max)
811 }
812 .max(1.0);
813 let height = box_height(line_count, line_advance, request.font_size).max(1.0);
814 Ok(Layout {
815 box_width: checked_dimension(width)?,
816 box_height: checked_dimension(height)?,
817 lines,
818 line_count,
819 line_advance,
820 half_leading: half_leading(request.font_size, request.line_height),
821 ascent: request.font_size * 0.8,
822 descent: request.font_size * 0.2,
823 width,
824 height,
825 })
826 }
827
828 #[allow(
829 clippy::cast_possible_truncation,
830 clippy::cast_precision_loss,
831 clippy::too_many_lines
832 )]
833 fn rasterize_uncached(
834 &self,
835 request: &TextRasterRequest,
836 ) -> Result<TextRasterResult, TextRasterError> {
837 let faces = Faces::new(&self.fonts, request.font_weight)?;
838 let layout = self.lay_out(&faces, request)?;
839 let (box_width, box_height) = (layout.box_width, layout.box_height);
840 let mut drawings = HashMap::<(usize, u16), Drawing>::new();
841 let mut placed = Vec::new();
842 let (mut overflow_x, mut overflow_y) = (0_i64, 0_i64);
844 let (mut ink_ascent, mut ink_descent) = (layout.ascent, layout.descent);
845 layout.each_glyph(
846 &faces,
847 request,
848 &mut drawings,
849 |drawing, origin, scale, glyph| {
850 let Some(bounds) = drawing.bounds() else {
851 return;
852 };
853 let pixels = PixelBox::of(bounds, origin, scale);
854 overflow_x = overflow_x
855 .max(pixels.left.saturating_neg())
856 .max(pixels.right.saturating_sub(i64::from(box_width)));
857 overflow_y = overflow_y
858 .max(pixels.top.saturating_neg())
859 .max(pixels.bottom.saturating_sub(i64::from(box_height)));
860 ink_ascent = ink_ascent.max(bounds[3] * scale + glyph.y_offset);
861 ink_descent = ink_descent.max(-(bounds[1] * scale + glyph.y_offset));
862 placed.push(((glyph.font, glyph.glyph_index), origin, scale, pixels));
863 },
864 );
865 let (outline, padding) = decoration_extents(request);
868 let margin = |overflow: i64| -> Result<u32, TextRasterError> {
869 u32::try_from(overflow)
870 .map_err(|_| TextRasterError::TooLarge)?
871 .checked_add(outline)
872 .map(|margin| margin.max(outline.max(padding)))
873 .ok_or(TextRasterError::TooLarge)
874 };
875 let (margin_x, margin_y) = (margin(overflow_x)?, margin(overflow_y)?);
876 let width = margin_x
877 .checked_mul(2)
878 .and_then(|margins| box_width.checked_add(margins))
879 .ok_or(TextRasterError::TooLarge)?;
880 let height = margin_y
881 .checked_mul(2)
882 .and_then(|margins| box_height.checked_add(margins))
883 .ok_or(TextRasterError::TooLarge)?;
884 if width > 16_384 || height > 16_384 {
885 return Err(TextRasterError::TooLarge);
886 }
887 let pixel_count = usize::try_from(width)
888 .ok()
889 .and_then(|width| {
890 usize::try_from(height)
891 .ok()
892 .and_then(|height| width.checked_mul(height))
893 })
894 .ok_or(TextRasterError::TooLarge)?;
895 let mut coverage = vec![0_u8; pixel_count];
896 let mut colors: Option<Vec<u8>> = None;
898 let mut line_boxes = Vec::with_capacity(layout.lines.len());
899 for (index, line) in layout.lines.iter().enumerate() {
900 if line.width > 0.0 {
901 line_boxes.push((
902 layout.line_offset(line, request.align),
903 line.width,
904 layout.line_top(index),
905 ));
906 }
907 }
908 for (key, origin, scale, pixels) in placed {
909 let (origin_x, origin_y) = (
910 pixels.left + i64::from(margin_x),
911 pixels.top + i64::from(margin_y),
912 );
913 let glyph_width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
914 match &drawings[&key] {
915 Drawing::Outline(outline) => place_glyph(
916 &mut coverage,
917 width,
918 height,
919 origin_x,
920 origin_y,
921 glyph_width,
922 &outline.draw(origin, scale, pixels),
923 ),
924 drawing => place_color_glyph(
925 colors.get_or_insert_with(|| vec![0; pixel_count * 4]),
926 width,
927 height,
928 origin_x,
929 origin_y,
930 glyph_width,
931 &drawing.draw_color(origin, scale, pixels),
932 ),
933 }
934 }
935 let rgba8 = compose(
936 request,
937 &coverage,
938 colors.as_deref(),
939 width,
940 [margin_x, margin_y],
941 &line_boxes,
942 );
943 let texture = HostTexture {
944 content_version: request.content_version,
945 width,
946 height,
947 premultiplication: Premultiplication::Premultiplied,
948 color_space: RasterColorSpace::Srgb,
949 };
950 Ok(TextRasterResult {
951 descriptor: TextTexture {
952 texture: texture.clone(),
953 layout_x: margin_x as f32,
954 layout_y: margin_y as f32,
955 layout_width: layout.width,
956 layout_height: layout.height,
957 first_baseline: layout.half_leading + layout.ascent,
958 ascent: ink_ascent,
959 descent: ink_descent,
960 line_count: layout.line_count,
961 },
962 pixels: HostTexturePixels { texture, rgba8 },
963 })
964 }
965
966 fn font_order(&self, family: Option<&str>, weight: u16) -> Vec<usize> {
971 let mut groups: Vec<(Option<String>, Vec<usize>)> = Vec::new();
974 for (index, font) in self.fonts.iter().enumerate() {
975 let name = font.family.as_deref().map(str::to_ascii_lowercase);
976 match groups
977 .iter_mut()
978 .find(|(held, _)| name.is_some() && *held == name)
979 {
980 Some((_, faces)) => faces.push(index),
981 None => groups.push((name, vec![index])),
982 }
983 }
984 let wanted = family.map(str::to_ascii_lowercase);
985 if let Some(at) = groups
986 .iter()
987 .position(|(name, _)| wanted.is_some() && *name == wanted)
988 {
989 let preferred = groups.remove(at);
990 groups.insert(0, preferred);
991 }
992 groups
993 .into_iter()
994 .flat_map(|(_, faces)| {
995 let weights = faces
996 .iter()
997 .map(|&index| self.fonts[index].weights())
998 .collect::<Vec<_>>();
999 weight_order(&weights, weight)
1000 .into_iter()
1001 .map(move |at| faces[at])
1002 })
1003 .collect()
1004 }
1005}
1006
1007struct Faces<'a> {
1010 parsed: Vec<ttf_parser::Face<'a>>,
1011 shapers: Vec<OnceCell<rustybuzz::Face<'a>>>,
1012 color: Vec<bool>,
1014}
1015
1016impl<'a> Faces<'a> {
1017 fn new(fonts: &'a [LoadedFont], weight: u16) -> Result<Self, TextRasterError> {
1020 let parsed = fonts
1021 .iter()
1022 .map(|font| {
1023 let mut face = ttf_parser::Face::parse(&font.source.bytes, font.source.face_index)
1024 .ok()
1025 .filter(|face| face.units_per_em() > 0)
1026 .ok_or_else(|| TextRasterError::InvalidFont(font.source.family.clone()))?;
1027 if let Some((min, _, max)) = font.wght {
1028 face.set_variation(WGHT, f32::from(weight).clamp(min, max));
1029 }
1030 Ok(face)
1031 })
1032 .collect::<Result<Vec<_>, _>>()?;
1033 Ok(Self {
1034 shapers: parsed.iter().map(|_| OnceCell::new()).collect(),
1035 color: parsed
1036 .iter()
1037 .map(|face| {
1038 let tables = face.tables();
1039 tables.colr.is_some() || tables.cbdt.is_some() || tables.sbix.is_some()
1040 })
1041 .collect(),
1042 parsed,
1043 })
1044 }
1045
1046 fn shaper(&self, font: usize) -> &rustybuzz::Face<'a> {
1048 self.shapers[font].get_or_init(|| rustybuzz::Face::from_face(self.parsed[font].clone()))
1049 }
1050
1051 #[allow(clippy::cast_precision_loss)]
1052 fn units_per_em(&self, font: usize) -> f32 {
1053 f32::from(self.parsed[font].units_per_em())
1054 }
1055
1056 fn has(&self, font: usize, character: char) -> bool {
1058 self.parsed[font]
1059 .glyph_index(character)
1060 .is_some_and(|glyph| glyph.0 != 0)
1061 }
1062
1063 fn covers(&self, font: usize, grapheme: &str) -> bool {
1065 grapheme
1066 .chars()
1067 .filter(|&character| !is_default_ignorable(character))
1068 .all(|character| self.has(font, character))
1069 }
1070}
1071
1072fn normalize(text: &str, policy: EmojiPolicy) -> String {
1079 let mut normalized = String::with_capacity(text.len());
1080 for grapheme in text.graphemes(true) {
1081 if policy == EmojiPolicy::Replace && is_emoji(grapheme) {
1082 normalized.push('\u{fffd}');
1083 } else if grapheme == "\r\n" {
1084 normalized.push('\n');
1085 } else {
1086 normalized.extend(grapheme.chars().map(|character| match character {
1087 '\r'
1088 | '\u{b}'
1089 | '\u{c}'
1090 | '\u{1c}'..='\u{1e}'
1091 | '\u{85}'
1092 | '\u{2028}'
1093 | '\u{2029}' => '\n',
1094 '\t' => ' ',
1095 other => other,
1096 }));
1097 }
1098 }
1099 normalized
1100}
1101
1102fn is_emoji(grapheme: &str) -> bool {
1108 if grapheme.contains('\u{fe0e}') {
1109 return false;
1110 }
1111 let selected = grapheme.contains('\u{fe0f}');
1112 grapheme.chars().any(|character| {
1113 in_table(unicode_tables::EMOJI_PRESENTATION, character)
1114 || (selected
1115 && (character == '\u{20e3}'
1116 || in_table(unicode_tables::EXTENDED_PICTOGRAPHIC, character)))
1117 })
1118}
1119
1120fn is_cursive(text: &str) -> bool {
1124 text.chars()
1125 .map(|character| character.script())
1126 .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown))
1127 .is_some_and(|script| {
1128 matches!(
1129 script,
1130 Script::Adlam
1131 | Script::Arabic
1132 | Script::Hanifi_Rohingya
1133 | Script::Mandaic
1134 | Script::Mongolian
1135 | Script::Nko
1136 | Script::Phags_Pa
1137 | Script::Syriac
1138 )
1139 })
1140}
1141
1142fn is_default_ignorable(character: char) -> bool {
1143 in_table(unicode_tables::DEFAULT_IGNORABLE, character)
1144}
1145
1146fn in_table(table: &[(u32, u32)], character: char) -> bool {
1147 let code = u32::from(character);
1148 table
1149 .binary_search_by(|&(first, last)| {
1150 if last < code {
1151 Ordering::Less
1152 } else if first > code {
1153 Ordering::Greater
1154 } else {
1155 Ordering::Equal
1156 }
1157 })
1158 .is_ok()
1159}
1160
1161fn hangs(character: char) -> bool {
1165 character.is_whitespace()
1166 && !matches!(character, '\u{a0}' | '\u{2007}' | '\u{202f}' | '\u{1680}')
1167}
1168
1169fn is_hangul(character: char) -> bool {
1170 matches!(
1171 character,
1172 '\u{1100}'..='\u{11ff}'
1173 | '\u{3130}'..='\u{318f}'
1174 | '\u{a960}'..='\u{a97f}'
1175 | '\u{ac00}'..='\u{d7a3}'
1176 | '\u{d7b0}'..='\u{d7ff}'
1177 )
1178}
1179
1180struct Layout {
1183 lines: Vec<LaidLine>,
1184 line_count: u32,
1185 line_advance: f32,
1187 half_leading: f32,
1189 ascent: f32,
1191 descent: f32,
1192 width: f32,
1195 height: f32,
1196 box_width: u32,
1198 box_height: u32,
1199}
1200
1201impl Layout {
1202 fn line_offset(&self, line: &LaidLine, align: TextAlign) -> f32 {
1204 match (align, line.rtl) {
1205 (TextAlign::Center, _) => (self.width - line.width) * 0.5,
1206 (TextAlign::Start, false) | (TextAlign::End, true) => 0.0,
1207 (TextAlign::End, false) | (TextAlign::Start, true) => self.width - line.width,
1208 }
1209 }
1210
1211 #[allow(clippy::cast_precision_loss)]
1213 fn line_top(&self, index: usize) -> f32 {
1214 index as f32 * self.line_advance + self.half_leading
1215 }
1216
1217 #[allow(clippy::cast_precision_loss)]
1220 fn each_glyph(
1221 &self,
1222 faces: &Faces<'_>,
1223 request: &TextRasterRequest,
1224 drawings: &mut HashMap<(usize, u16), Drawing>,
1225 mut place: impl FnMut(&Drawing, [f32; 2], f32, &PositionedGlyph),
1226 ) {
1227 for (index, line) in self.lines.iter().enumerate() {
1228 let offset = self.line_offset(line, request.align);
1229 let baseline = self.line_top(index) + self.ascent;
1230 for glyph in &line.glyphs {
1231 let drawing = drawings
1232 .entry((glyph.font, glyph.glyph_index))
1233 .or_insert_with(|| Drawing::of(faces, glyph.font, glyph.glyph_index, request));
1234 let scale = request.font_size / faces.units_per_em(glyph.font);
1235 let origin = [glyph.x + glyph.x_offset + offset, baseline - glyph.y_offset];
1236 place(drawing, origin, scale, glyph);
1237 }
1238 }
1239 }
1240}
1241
1242fn include(bounds: &mut Option<[f32; 4]>, [left, top, right, bottom]: [f32; 4]) {
1244 let held = bounds.get_or_insert([left, top, right, bottom]);
1245 *held = [
1246 held[0].min(left),
1247 held[1].min(top),
1248 held[2].max(right),
1249 held[3].max(bottom),
1250 ];
1251}
1252
1253fn grown([left, top, right, bottom]: [f32; 4], by: f32) -> [f32; 4] {
1255 [left - by, top - by, right + by, bottom + by]
1256}
1257
1258struct LaidLine {
1260 glyphs: Vec<PositionedGlyph>,
1262 width: f32,
1264 rtl: bool,
1266 has_glyphs: bool,
1268}
1269
1270#[allow(clippy::too_many_arguments)]
1274fn layout_paragraph(
1275 faces: &Faces<'_>,
1276 paragraph: &str,
1277 font_size: f32,
1278 max_width: f32,
1279 order: &[usize],
1280 policy: EmojiPolicy,
1281 letter_spacing: f32,
1282) -> Vec<LaidLine> {
1283 let bidi = ParagraphBidiInfo::new(paragraph, None);
1284 let rtl = bidi.paragraph_level.is_rtl();
1285 let glyphs = shape_paragraph(
1286 faces,
1287 paragraph,
1288 &bidi,
1289 font_size,
1290 order,
1291 policy,
1292 letter_spacing,
1293 );
1294 let clusters = clusters(&glyphs, paragraph, letter_spacing);
1295 break_lines(&clusters, max_width)
1296 .into_iter()
1297 .map(|range| {
1298 let has_glyphs = !range.is_empty();
1299 let mut end = range.end;
1301 while end > range.start && clusters[end - 1].hangs {
1302 end -= 1;
1303 }
1304 let mut line = LaidLine {
1305 glyphs: Vec::new(),
1306 width: 0.0,
1307 rtl,
1308 has_glyphs,
1309 };
1310 if end == range.start {
1311 return line;
1312 }
1313 let bytes = clusters[range.start].start
1314 ..clusters
1315 .get(range.end)
1316 .map_or(paragraph.len(), |next| next.start);
1317 let levels = bidi.reordered_levels(bytes);
1318 let mut cluster_of = vec![0; clusters[end - 1].glyphs.end];
1321 for (index, cluster) in clusters[range.start..end].iter().enumerate() {
1322 cluster_of[cluster.glyphs.clone()].fill(range.start + index);
1323 }
1324 let mut x = 0.0_f32;
1325 let mut previous: Option<usize> = None;
1326 for index in visual_order(
1327 clusters[range.start].glyphs.start..clusters[end - 1].glyphs.end,
1328 |index| levels[glyphs[index].cluster].number(),
1329 ) {
1330 let glyph = &glyphs[index];
1331 let cluster = cluster_of[index];
1332 if let Some(before) = previous.filter(|&before| before != cluster) {
1333 x += clusters[before].spacing;
1334 }
1335 previous = Some(cluster);
1336 line.glyphs.push(PositionedGlyph {
1337 font: glyph.font,
1338 glyph_index: glyph.glyph_index,
1339 x,
1340 x_offset: glyph.x_offset,
1341 y_offset: glyph.y_offset,
1342 });
1343 x += glyph.advance;
1344 }
1345 line.width = x;
1346 line
1347 })
1348 .collect()
1349}
1350
1351fn visual_order(range: Range<usize>, level: impl Fn(usize) -> u8) -> Vec<usize> {
1356 let mut order = range.collect::<Vec<_>>();
1357 let levels = order.iter().map(|&index| level(index)).collect::<Vec<_>>();
1358 let highest = levels.iter().copied().max().unwrap_or(0);
1359 let Some(lowest_odd) = levels.iter().copied().filter(|level| level % 2 == 1).min() else {
1360 return order;
1361 };
1362 let mut current = levels;
1363 for at_least in (lowest_odd..=highest).rev() {
1364 let mut start = 0;
1365 while start < order.len() {
1366 if current[start] < at_least {
1367 start += 1;
1368 continue;
1369 }
1370 let end = (start..order.len())
1371 .find(|&index| current[index] < at_least)
1372 .unwrap_or(order.len());
1373 order[start..end].reverse();
1374 current[start..end].reverse();
1375 start = end;
1376 }
1377 }
1378 order
1379}
1380
1381#[derive(Clone, Debug, PartialEq)]
1384struct Run {
1385 range: Range<usize>,
1386 font: usize,
1387 script: Option<Script>,
1388 rtl: bool,
1389}
1390
1391fn itemize(
1402 faces: &Faces<'_>,
1403 paragraph: &str,
1404 bidi: &ParagraphBidiInfo<'_>,
1405 order: &[usize],
1406 policy: EmojiPolicy,
1407) -> Vec<Run> {
1408 let first = order.first().copied().unwrap_or(0);
1409 let (color, text): (Vec<usize>, Vec<usize>) =
1410 order.iter().partition(|&&font| faces.color[font]);
1411 let color_first = color.iter().chain(&text).copied().collect::<Vec<_>>();
1412 let text_first = text.iter().chain(&color).copied().collect::<Vec<_>>();
1413 let fits = |font: usize, color_wanted: bool| {
1415 faces.color[font] == color_wanted
1416 || if color_wanted {
1417 color.is_empty()
1418 } else {
1419 text.is_empty()
1420 }
1421 };
1422 let mut graphemes = Vec::<(usize, usize, Option<Script>, bool)>::new();
1423 let mut current = None;
1424 for (start, grapheme) in paragraph.grapheme_indices(true) {
1425 let script = grapheme
1426 .chars()
1427 .map(|character| character.script())
1428 .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown));
1429 let wants_color = if grapheme.contains('\u{fe0e}') {
1431 Some(false)
1432 } else if is_emoji(grapheme) {
1433 Some(policy == EmojiPolicy::Color)
1434 } else {
1435 None
1436 };
1437 let order = match wants_color {
1438 Some(true) => color_first.as_slice(),
1439 Some(false) => text_first.as_slice(),
1440 None => order,
1441 };
1442 let stays = current.filter(|&font| {
1443 script.is_none()
1444 && wants_color.is_none_or(|wanted| fits(font, wanted))
1445 && faces.covers(font, grapheme)
1446 });
1447 let font = stays
1448 .or_else(|| {
1449 order
1450 .iter()
1451 .copied()
1452 .find(|&font| faces.covers(font, grapheme))
1453 })
1454 .or_else(|| {
1455 let base = grapheme
1456 .chars()
1457 .find(|&character| !is_default_ignorable(character))?;
1458 order.iter().copied().find(|&font| faces.has(font, base))
1459 })
1460 .or(current)
1461 .unwrap_or(first);
1462 current = Some(font);
1463 graphemes.push((start, font, script, bidi.levels[start].is_rtl()));
1464 }
1465 let leading = graphemes.iter().find_map(|grapheme| grapheme.2);
1466 let mut before = leading;
1467 for grapheme in &mut graphemes {
1468 match grapheme.2 {
1469 Some(script) => before = Some(script),
1470 None => grapheme.2 = before,
1471 }
1472 }
1473 let mut runs = Vec::<Run>::new();
1474 for (index, &(start, font, script, rtl)) in graphemes.iter().enumerate() {
1475 let end = graphemes
1476 .get(index + 1)
1477 .map_or(paragraph.len(), |next| next.0);
1478 match runs.last_mut() {
1479 Some(run) if run.font == font && run.script == script && run.rtl == rtl => {
1480 run.range.end = end;
1481 }
1482 _ => runs.push(Run {
1483 range: start..end,
1484 font,
1485 script,
1486 rtl,
1487 }),
1488 }
1489 }
1490 runs
1491}
1492
1493fn shaping_script(script: Script) -> Option<rustybuzz::Script> {
1495 let code: [u8; 4] = script.short_name().as_bytes().try_into().ok()?;
1496 rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&code))
1497}
1498
1499fn shape_paragraph(
1502 faces: &Faces<'_>,
1503 paragraph: &str,
1504 bidi: &ParagraphBidiInfo<'_>,
1505 font_size: f32,
1506 order: &[usize],
1507 policy: EmojiPolicy,
1508 letter_spacing: f32,
1509) -> Vec<ShapedGlyph> {
1510 let features = if letter_spacing == 0.0 {
1512 Vec::new()
1513 } else {
1514 [b"liga", b"clig", b"dlig", b"hlig"]
1515 .map(|tag| rustybuzz::Feature::new(ttf_parser::Tag::from_bytes(tag), 0, ..))
1516 .to_vec()
1517 };
1518 let mut glyphs = Vec::new();
1519 for run in itemize(faces, paragraph, bidi, order, policy) {
1520 let face = faces.shaper(run.font);
1521 let units = faces.units_per_em(run.font);
1522 let mut buffer = UnicodeBuffer::new();
1523 buffer.push_str(¶graph[run.range.clone()]);
1524 buffer.set_pre_context(¶graph[..run.range.start]);
1525 buffer.set_post_context(¶graph[run.range.end..]);
1526 buffer.set_direction(if run.rtl {
1527 Direction::RightToLeft
1528 } else {
1529 Direction::LeftToRight
1530 });
1531 match run.script.and_then(shaping_script) {
1532 Some(script) => buffer.set_script(script),
1533 None => buffer.guess_segment_properties(),
1534 }
1535 let shaped = rustybuzz::shape(face, &features, buffer);
1536 let first = glyphs.len();
1537 for (info, position) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) {
1538 #[allow(clippy::cast_precision_loss)]
1539 glyphs.push(ShapedGlyph {
1540 font: run.font,
1541 glyph_index: u16::try_from(info.glyph_id)
1544 .ok()
1545 .filter(|&glyph| glyph < face.number_of_glyphs())
1546 .unwrap_or(0),
1547 cluster: run.range.start + usize::try_from(info.cluster).unwrap_or(0),
1548 advance: position.x_advance as f32 * font_size / units,
1549 x_offset: position.x_offset as f32 * font_size / units,
1550 y_offset: position.y_offset as f32 * font_size / units,
1551 });
1552 }
1553 if run.rtl {
1556 glyphs[first..].reverse();
1557 }
1558 }
1559 glyphs
1560}
1561
1562#[derive(Copy, Clone, Debug)]
1563struct ShapedGlyph {
1564 font: usize,
1565 glyph_index: u16,
1566 cluster: usize,
1568 advance: f32,
1569 x_offset: f32,
1572 y_offset: f32,
1573}
1574
1575#[derive(Clone, Debug, PartialEq)]
1577struct Cluster {
1578 glyphs: Range<usize>,
1579 start: usize,
1581 width: f32,
1583 spacing: f32,
1585 hangs: bool,
1587 break_before: bool,
1589}
1590
1591fn clusters(glyphs: &[ShapedGlyph], paragraph: &str, letter_spacing: f32) -> Vec<Cluster> {
1596 let mut opportunities = unicode_linebreak::linebreaks(paragraph)
1597 .filter(|&(at, kind)| {
1598 kind == BreakOpportunity::Allowed
1599 && !(paragraph[..at].chars().next_back().is_some_and(is_hangul)
1600 && paragraph[at..].chars().next().is_some_and(is_hangul))
1601 })
1602 .map(|(at, _)| at)
1603 .peekable();
1604 let mut clusters = Vec::<Cluster>::new();
1605 for (index, glyph) in glyphs.iter().enumerate() {
1606 match clusters.last_mut() {
1607 Some(cluster) if cluster.start == glyph.cluster => {
1608 cluster.glyphs.end = index + 1;
1609 cluster.width += glyph.advance;
1610 }
1611 _ => clusters.push(Cluster {
1612 glyphs: index..index + 1,
1613 start: glyph.cluster,
1614 width: glyph.advance,
1615 spacing: 0.0,
1616 hangs: false,
1617 break_before: false,
1618 }),
1619 }
1620 }
1621 for index in 0..clusters.len() {
1622 let start = clusters[index].start;
1623 let end = clusters
1624 .get(index + 1)
1625 .map(|next| next.start)
1626 .filter(|&end| end > start)
1627 .unwrap_or_else(|| {
1628 paragraph[start..]
1629 .chars()
1630 .next()
1631 .map_or(start, |character| start + character.len_utf8())
1632 });
1633 clusters[index].hangs = paragraph[start..end].chars().all(hangs);
1634 if letter_spacing != 0.0 && !is_cursive(¶graph[start..end]) {
1635 clusters[index].spacing = letter_spacing;
1636 clusters[index].width += letter_spacing;
1637 }
1638 let mut found = false;
1639 while let Some(&at) = opportunities.peek() {
1640 if at > start {
1641 break;
1642 }
1643 opportunities.next();
1644 found = true;
1645 }
1646 clusters[index].break_before = index > 0 && found;
1647 }
1648 clusters
1649}
1650
1651fn break_lines(clusters: &[Cluster], max_width: f32) -> Vec<Range<usize>> {
1658 let mut lines = Vec::new();
1659 let mut start = 0;
1660 let mut x = 0.0_f32;
1661 let mut opportunity: Option<(usize, f32)> = None;
1663 for (index, cluster) in clusters.iter().enumerate() {
1664 if cluster.break_before && index > start {
1665 opportunity = Some((index, x));
1666 }
1667 let fits = |x: f32| x + cluster.width - cluster.spacing <= max_width;
1669 if max_width > 0.0 && !cluster.hangs && index > start && !fits(x) {
1670 let (end, width) = opportunity.unwrap_or((index, x));
1671 lines.push(start..end);
1672 start = end;
1673 x -= width;
1674 opportunity = None;
1675 if index > start && !fits(x) {
1678 lines.push(start..index);
1679 start = index;
1680 x = 0.0;
1681 }
1682 }
1683 x += cluster.width;
1684 }
1685 lines.push(start..clusters.len());
1686 lines
1687}
1688
1689#[derive(Copy, Clone, Debug)]
1690struct PositionedGlyph {
1691 font: usize,
1692 glyph_index: u16,
1693 x: f32,
1695 x_offset: f32,
1697 y_offset: f32,
1698}
1699
1700enum Drawing {
1703 Outline(Outline),
1704 Painting(color::Painting),
1705 Bitmap(color::Bitmap),
1706}
1707
1708impl Drawing {
1709 fn of(faces: &Faces<'_>, font: usize, glyph: u16, request: &TextRasterRequest) -> Self {
1713 let face = &faces.parsed[font];
1714 if request.emoji_policy == EmojiPolicy::Color && faces.color[font] {
1715 let [red, green, blue, _] = request.color;
1716 if let Some(painting) = color::Painting::of(face, glyph, [red, green, blue, 255]) {
1717 return Self::Painting(painting);
1718 }
1719 if let Some(bitmap) = color::Bitmap::of(face, glyph, request.font_size) {
1720 return Self::Bitmap(bitmap);
1721 }
1722 }
1723 Self::Outline(Outline::of(face, glyph))
1724 }
1725
1726 fn bounds(&self) -> Option<[f32; 4]> {
1729 match self {
1730 Self::Outline(outline) => outline.bounds,
1731 Self::Painting(painting) => painting.bounds(),
1732 Self::Bitmap(bitmap) => Some(bitmap.bounds()),
1733 }
1734 }
1735
1736 fn ink_bounds(&self) -> Option<[f32; 4]> {
1741 match self {
1742 Self::Outline(outline) => outline.curve_bounds(),
1743 Self::Painting(painting) => painting.bounds(),
1744 Self::Bitmap(bitmap) => bitmap.ink_bounds(),
1745 }
1746 }
1747
1748 fn draw_color(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1751 match self {
1752 Self::Outline(_) => Vec::new(),
1753 Self::Painting(painting) => painting.draw(origin, scale, pixels),
1754 Self::Bitmap(bitmap) => bitmap.draw(origin, scale, pixels),
1755 }
1756 }
1757}
1758
1759#[derive(Default)]
1761struct Outline {
1762 segments: Vec<Segment>,
1763 bounds: Option<[f32; 4]>,
1766}
1767
1768enum Segment {
1769 Line([f32; 2], [f32; 2]),
1770 Quad([f32; 2], [f32; 2], [f32; 2]),
1771 Cubic([f32; 2], [f32; 2], [f32; 2], [f32; 2]),
1772}
1773
1774#[derive(Copy, Clone, Debug)]
1777struct PixelBox {
1778 left: i64,
1779 top: i64,
1780 right: i64,
1781 bottom: i64,
1782}
1783
1784impl PixelBox {
1785 #[allow(clippy::cast_possible_truncation)]
1788 fn of(bounds: [f32; 4], origin: [f32; 2], scale: f32) -> Self {
1789 let [x_min, y_min, x_max, y_max] = bounds;
1790 Self {
1793 left: (origin[0] + x_min * scale).floor() as i64,
1794 top: (origin[1] - y_max * scale).floor() as i64,
1795 right: (origin[0] + x_max * scale).ceil() as i64,
1796 bottom: (origin[1] - y_min * scale).ceil() as i64,
1797 }
1798 }
1799}
1800
1801#[cfg(test)]
1802thread_local! {
1803 static OUTLINES_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1805}
1806
1807impl Outline {
1808 fn of(face: &ttf_parser::Face<'_>, glyph: u16) -> Self {
1809 #[cfg(test)]
1810 OUTLINES_READ.with(|read| read.set(read.get() + 1));
1811 let mut sink = OutlineSink::default();
1812 face.outline_glyph(GlyphId(glyph), &mut sink);
1813 sink.close();
1814 sink.outline
1815 }
1816
1817 #[allow(
1820 clippy::cast_possible_truncation,
1821 clippy::cast_sign_loss,
1822 clippy::cast_precision_loss
1823 )]
1824 fn draw(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1825 let width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
1826 let height = usize::try_from(pixels.bottom - pixels.top).unwrap_or(0);
1827 let (left, top) = (pixels.left as f32, pixels.top as f32);
1828 let raster = self.raster(width, height, |[x, y]| {
1829 point(origin[0] + x * scale - left, (origin[1] - y * scale) - top)
1830 });
1831 let mut alpha = vec![0_u8; width * height];
1832 raster.for_each_pixel(|index, coverage| {
1833 alpha[index] = (coverage.min(1.0) * 255.0).round() as u8;
1834 });
1835 alpha
1836 }
1837}
1838
1839impl Outline {
1840 fn curve_bounds(&self) -> Option<[f32; 4]> {
1844 let mut bounds = None;
1845 let mut hold = |[x, y]: [f32; 2]| include(&mut bounds, [x, y, x, y]);
1846 for segment in &self.segments {
1847 match *segment {
1848 Segment::Line(from, to) => {
1849 hold(from);
1850 hold(to);
1851 }
1852 Segment::Quad(from, control, to) => {
1853 hold(from);
1854 hold(to);
1855 for axis in 0..2 {
1856 let curvature = from[axis] - 2.0 * control[axis] + to[axis];
1858 let t = (from[axis] - control[axis]) / curvature;
1859 if t > 0.0 && t < 1.0 {
1860 hold(std::array::from_fn(|axis| {
1861 let u = 1.0 - t;
1862 u * u * from[axis] + 2.0 * u * t * control[axis] + t * t * to[axis]
1863 }));
1864 }
1865 }
1866 }
1867 Segment::Cubic(from, first, second, to) => {
1868 hold(from);
1869 hold(to);
1870 for axis in 0..2 {
1871 let [p0, p1, p2, p3] = [from[axis], first[axis], second[axis], to[axis]];
1872 for t in quadratic_roots(
1873 -p0 + 3.0 * (p1 - p2) + p3,
1874 2.0 * (p0 - 2.0 * p1 + p2),
1875 p1 - p0,
1876 )
1877 .into_iter()
1878 .flatten()
1879 .filter(|t| *t > 0.0 && *t < 1.0)
1880 {
1881 hold(std::array::from_fn(|axis| {
1882 let u = 1.0 - t;
1883 u * u * u * from[axis]
1884 + 3.0 * u * u * t * first[axis]
1885 + 3.0 * u * t * t * second[axis]
1886 + t * t * t * to[axis]
1887 }));
1888 }
1889 }
1890 }
1891 }
1892 }
1893 bounds
1894 }
1895
1896 fn raster(&self, width: usize, height: usize, at: impl Fn([f32; 2]) -> Point) -> Rasterizer {
1899 let mut raster = Rasterizer::new(width, height);
1900 for segment in &self.segments {
1901 match *segment {
1902 Segment::Line(from, to) => raster.draw_line(at(from), at(to)),
1903 Segment::Quad(from, control, to) => raster.draw_quad(at(from), at(control), at(to)),
1904 Segment::Cubic(from, first, second, to) => {
1905 raster.draw_cubic(at(from), at(first), at(second), at(to));
1906 }
1907 }
1908 }
1909 raster
1910 }
1911}
1912
1913fn quadratic_roots(a: f32, b: f32, c: f32) -> [Option<f32>; 2] {
1916 let finite = |t: f32| t.is_finite().then_some(t);
1917 if a.abs() <= f32::EPSILON * (b.abs() + c.abs()) {
1918 return [finite(-c / b), None];
1919 }
1920 let discriminant = b * b - 4.0 * a * c;
1921 if discriminant < 0.0 {
1922 return [None, None];
1923 }
1924 let root = discriminant.sqrt();
1925 [
1926 finite((-b + root) / (2.0 * a)),
1927 finite((-b - root) / (2.0 * a)),
1928 ]
1929}
1930
1931#[derive(Default)]
1933struct OutlineSink {
1934 outline: Outline,
1935 start: [f32; 2],
1936 pen: [f32; 2],
1937}
1938
1939impl OutlineSink {
1940 fn include(&mut self, points: &[[f32; 2]]) {
1941 for &[x, y] in points {
1942 let bounds = self.outline.bounds.get_or_insert([x, y, x, y]);
1943 *bounds = [
1944 bounds[0].min(x),
1945 bounds[1].min(y),
1946 bounds[2].max(x),
1947 bounds[3].max(y),
1948 ];
1949 }
1950 }
1951}
1952
1953impl OutlineBuilder for OutlineSink {
1954 fn move_to(&mut self, x: f32, y: f32) {
1955 self.close();
1956 self.start = [x, y];
1957 self.pen = [x, y];
1958 }
1959
1960 fn line_to(&mut self, x: f32, y: f32) {
1961 let to = [x, y];
1962 self.include(&[self.pen, to]);
1963 self.outline.segments.push(Segment::Line(self.pen, to));
1964 self.pen = to;
1965 }
1966
1967 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1968 let (control, to) = ([x1, y1], [x, y]);
1969 self.include(&[self.pen, control, to]);
1970 self.outline
1971 .segments
1972 .push(Segment::Quad(self.pen, control, to));
1973 self.pen = to;
1974 }
1975
1976 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1977 let (first, second, to) = ([x1, y1], [x2, y2], [x, y]);
1978 self.include(&[self.pen, first, second, to]);
1979 self.outline
1980 .segments
1981 .push(Segment::Cubic(self.pen, first, second, to));
1982 self.pen = to;
1983 }
1984
1985 fn close(&mut self) {
1986 if self.pen.map(f32::to_bits) != self.start.map(f32::to_bits) {
1989 self.line_to(self.start[0], self.start[1]);
1990 }
1991 }
1992}
1993
1994fn validate_request(request: &TextRasterRequest) -> Result<(), TextRasterError> {
1995 if request.font_size.is_finite()
1996 && request.font_size > 0.0
1997 && request.font_size <= MAX_FONT_SIZE
1998 && request.text.len() <= MAX_TEXT_BYTES
1999 && request.max_width.is_finite()
2000 && request.max_width <= 16_384.0
2001 && request.line_height.is_finite()
2002 && request.line_height > 0.0
2003 && request.line_height <= MAX_LINE_HEIGHT
2004 && request.letter_spacing.is_finite()
2005 && request.letter_spacing >= LETTER_SPACING_RANGE.start() * request.font_size
2006 && request.letter_spacing <= LETTER_SPACING_RANGE.end() * request.font_size
2007 && (1..=1000).contains(&request.font_weight)
2008 && request.content_version > 0
2009 && request.outline.is_none_or(|outline| {
2010 outline.width.is_finite() && outline.width > 0.0 && outline.width <= MAX_OUTLINE_WIDTH
2011 })
2012 && request.background.is_none_or(|background| {
2013 background.padding.is_finite()
2014 && background.padding >= 0.0
2015 && background.padding <= MAX_BACKGROUND_PADDING
2016 })
2017 {
2018 Ok(())
2019 } else {
2020 Err(TextRasterError::InvalidRequest)
2021 }
2022}
2023
2024fn checked_dimension(value: f32) -> Result<u32, TextRasterError> {
2025 if value.is_finite() && value > 0.0 && value.ceil() <= 16_384.0 {
2026 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2027 Ok(value.ceil() as u32)
2028 } else {
2029 Err(TextRasterError::TooLarge)
2030 }
2031}
2032
2033fn place_glyph(
2037 mask: &mut [u8],
2038 width: u32,
2039 height: u32,
2040 origin_x: i64,
2041 origin_y: i64,
2042 glyph_width: usize,
2043 alpha: &[u8],
2044) {
2045 for (index, coverage) in alpha.iter().copied().enumerate() {
2046 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
2047 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
2048 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) {
2049 continue;
2050 }
2051 let pixel =
2052 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
2053 let slot = &mut mask[usize::try_from(pixel).unwrap_or(0)];
2054 *slot = (*slot).max(coverage);
2055 }
2056}
2057
2058fn place_color_glyph(
2061 colors: &mut [u8],
2062 width: u32,
2063 height: u32,
2064 origin_x: i64,
2065 origin_y: i64,
2066 glyph_width: usize,
2067 rgba8: &[u8],
2068) {
2069 for (index, source) in rgba8.as_chunks::<4>().0.iter().enumerate() {
2070 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
2071 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
2072 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) || source[3] == 0 {
2073 continue;
2074 }
2075 let pixel =
2076 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
2077 let slot = &mut colors[usize::try_from(pixel).unwrap_or(0) * 4..][..4];
2078 let blended = over(
2079 std::array::from_fn(|channel| u32::from(source[channel])),
2080 std::array::from_fn(|channel| u32::from(slot[channel])),
2081 );
2082 for (byte, value) in slot.iter_mut().zip(blended) {
2083 *byte = u8::try_from(value.min(255)).unwrap_or(u8::MAX);
2084 }
2085 }
2086}
2087
2088impl TextRasterRequest {
2089 #[must_use]
2094 pub fn decoration_margin(&self) -> u32 {
2095 let (outline, padding) = decoration_extents(self);
2096 outline.max(padding)
2097 }
2098}
2099
2100fn decoration_extents(request: &TextRasterRequest) -> (u32, u32) {
2103 let outline = request
2104 .outline
2105 .map_or(0, |outline| disc_extent(outline.width));
2106 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2107 let padding = request
2108 .background
2109 .map_or(0, |background| background.padding.ceil() as u32);
2110 (outline, padding)
2111}
2112
2113#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2115fn disc_extent(width: f32) -> u32 {
2116 (width + 0.5).ceil() as u32
2117}
2118
2119fn premultiplied(color: [u8; 4], coverage: u8) -> [u32; 4] {
2123 let alpha = u32::from(coverage) * u32::from(color[3]) / 255;
2124 [
2125 u32::from(color[0]) * alpha / 255,
2126 u32::from(color[1]) * alpha / 255,
2127 u32::from(color[2]) * alpha / 255,
2128 alpha,
2129 ]
2130}
2131
2132fn over(source: [u32; 4], destination: [u32; 4]) -> [u32; 4] {
2134 let keep = 255 - source[3];
2135 std::array::from_fn(|channel| source[channel] + (destination[channel] * keep + 127) / 255)
2136}
2137
2138#[allow(
2144 clippy::cast_possible_truncation,
2145 clippy::cast_sign_loss,
2146 clippy::cast_precision_loss
2147)]
2148fn compose(
2149 request: &TextRasterRequest,
2150 coverage: &[u8],
2151 colors: Option<&[u8]>,
2152 width: u32,
2153 margin: [u32; 2],
2154 line_boxes: &[(f32, f32, f32)],
2155) -> Vec<u8> {
2156 let outline = request.outline.map(|outline| {
2157 let ink = colors.map(|colors| {
2158 coverage
2159 .iter()
2160 .zip(colors.as_chunks::<4>().0)
2161 .map(|(&text, color)| text.max(color[3]))
2162 .collect::<Vec<_>>()
2163 });
2164 (
2165 outline.color,
2166 dilate(
2167 ink.as_deref().unwrap_or(coverage),
2168 width,
2169 u32::try_from(coverage.len() / width.max(1) as usize).unwrap_or(0),
2170 outline.width,
2171 ),
2172 )
2173 });
2174 let background = request.background.map(|background| {
2175 let [margin_x, margin_y] = margin.map(|margin| margin as f32);
2176 let padding = background.padding;
2177 let boxes = line_boxes
2178 .iter()
2179 .map(|&(left, line_width, top)| {
2180 (
2181 margin_x + left - padding,
2182 margin_x + left + line_width + padding,
2183 margin_y + top - padding,
2184 margin_y + top + request.font_size + padding,
2185 )
2186 })
2187 .collect::<Vec<_>>();
2188 (background.color, boxes)
2189 });
2190 let mut rgba8 = vec![0_u8; coverage.len() * 4];
2191 let (mut first, mut last) = (0, 0);
2194 for (index, text) in coverage.iter().copied().enumerate() {
2195 let mut pixel = [0_u32; 4];
2196 if let Some((color, boxes)) = &background {
2197 let x = (index % width as usize) as f32;
2198 let y = (index / width as usize) as f32;
2199 if index % width as usize == 0 {
2200 while first < boxes.len() && boxes[first].3 <= y {
2201 first += 1;
2202 }
2203 last = last.max(first);
2204 while last < boxes.len() && boxes[last].2 < y + 1.0 {
2205 last += 1;
2206 }
2207 }
2208 let covered = boxes[first..last]
2209 .iter()
2210 .map(|&(left, right, top, bottom)| {
2211 let across = (right.min(x + 1.0) - left.max(x)).clamp(0.0, 1.0);
2212 let down = (bottom.min(y + 1.0) - top.max(y)).clamp(0.0, 1.0);
2213 across * down
2214 })
2215 .fold(0.0_f32, f32::max);
2216 pixel = premultiplied(*color, (covered * 255.0).round() as u8);
2217 }
2218 if let Some((color, band)) = &outline {
2219 pixel = over(premultiplied(*color, band[index]), pixel);
2220 }
2221 pixel = over(premultiplied(request.color, text), pixel);
2222 if let Some(colors) = colors {
2223 let color = &colors[index * 4..][..4];
2224 if color[3] > 0 {
2225 let opacity = u32::from(request.color[3]);
2226 pixel = over(
2227 std::array::from_fn(|channel| u32::from(color[channel]) * opacity / 255),
2228 pixel,
2229 );
2230 }
2231 }
2232 for (channel, value) in pixel.iter().enumerate() {
2233 rgba8[index * 4 + channel] = (*value).min(255) as u8;
2234 }
2235 }
2236 rgba8
2237}
2238
2239#[allow(
2250 clippy::cast_possible_truncation,
2251 clippy::cast_sign_loss,
2252 clippy::cast_precision_loss,
2253 clippy::cast_possible_wrap
2254)]
2255fn dilate(coverage: &[u8], width: u32, height: u32, radius: f32) -> Vec<u8> {
2256 let (width, height) = (width as usize, height as usize);
2257 let mut band = vec![0_u8; coverage.len()];
2258 let Some(ink) = ink_bounds(coverage, width, height) else {
2259 return band;
2260 };
2261 let disc = Disc::new(radius);
2262 let extent = disc.extent;
2263 let reach = Bounds {
2264 left: ink.left.saturating_sub(extent),
2265 top: ink.top.saturating_sub(extent),
2266 right: (ink.right + extent).min(width),
2267 bottom: (ink.bottom + extent).min(height),
2268 };
2269 let span = reach.right - reach.left;
2272 let mut window = WindowMax::default();
2273 let mut maxima = vec![0_u8; span];
2274 for (half, offsets) in &disc.runs {
2275 for source_y in ink.top..ink.bottom {
2276 let row = &coverage[source_y * width..(source_y + 1) * width];
2277 window.of(row, reach.left, span, *half, &mut maxima);
2278 for &offset in offsets {
2279 let Some(y) = source_y.checked_add_signed(-offset) else {
2281 continue;
2282 };
2283 if y < reach.top || y >= reach.bottom {
2284 continue;
2285 }
2286 let out = &mut band[y * width + reach.left..y * width + reach.right];
2287 for (pixel, highest) in out.iter_mut().zip(&maxima) {
2288 *pixel = (*pixel).max(*highest);
2289 }
2290 }
2291 }
2292 }
2293 for &(dx, dy, weight) in &disc.rim {
2294 for source_y in ink.top..ink.bottom {
2295 let Some(y) = source_y.checked_add_signed(-dy) else {
2296 continue;
2297 };
2298 if y >= height {
2299 continue;
2300 }
2301 let first = ink.left.max(usize::try_from(dx).unwrap_or(0));
2304 let last = ink
2305 .right
2306 .min(usize::try_from(width as isize + dx.min(0)).unwrap_or(0));
2307 if first >= last {
2308 continue;
2309 }
2310 let source = &coverage[source_y * width + first..source_y * width + last];
2311 let start = y * width + (first as isize - dx) as usize;
2312 let out = &mut band[start..start + source.len()];
2313 for (pixel, value) in out.iter_mut().zip(source) {
2314 *pixel = (*pixel).max((u32::from(*value) * weight / 255) as u8);
2315 }
2316 }
2317 }
2318 band
2319}
2320
2321#[derive(Copy, Clone, Debug, Eq, PartialEq)]
2323struct Bounds {
2324 left: usize,
2325 top: usize,
2326 right: usize,
2327 bottom: usize,
2328}
2329
2330fn ink_bounds(coverage: &[u8], width: usize, height: usize) -> Option<Bounds> {
2332 let mut bounds: Option<Bounds> = None;
2333 for y in 0..height {
2334 let row = &coverage[y * width..(y + 1) * width];
2335 let Some(left) = row.iter().position(|&value| value > 0) else {
2336 continue;
2337 };
2338 let right = row.iter().rposition(|&value| value > 0).unwrap_or(left) + 1;
2339 bounds = Some(bounds.map_or(
2340 Bounds {
2341 left,
2342 top: y,
2343 right,
2344 bottom: y + 1,
2345 },
2346 |bounds| Bounds {
2347 left: bounds.left.min(left),
2348 top: bounds.top,
2349 right: bounds.right.max(right),
2350 bottom: y + 1,
2351 },
2352 ));
2353 }
2354 bounds
2355}
2356
2357struct Disc {
2360 extent: usize,
2362 runs: Vec<(usize, Vec<isize>)>,
2365 rim: Vec<(isize, isize, u32)>,
2367}
2368
2369impl Disc {
2370 #[allow(
2373 clippy::cast_possible_truncation,
2374 clippy::cast_sign_loss,
2375 clippy::cast_precision_loss,
2376 clippy::cast_possible_wrap
2377 )]
2378 fn new(radius: f32) -> Self {
2379 let extent = disc_extent(radius) as isize;
2380 let mut runs: Vec<(usize, Vec<isize>)> = Vec::new();
2381 let mut rim = Vec::new();
2382 for dy in -extent..=extent {
2383 let mut half = None;
2384 for dx in -extent..=extent {
2385 let distance = ((dx * dx + dy * dy) as f32).sqrt();
2386 let weight = ((radius + 0.5 - distance).clamp(0.0, 1.0) * 255.0).round() as u32;
2387 match weight {
2388 0 => {}
2389 255 => half = Some(dx.unsigned_abs()),
2392 _ => rim.push((dx, dy, weight)),
2393 }
2394 }
2395 if let Some(half) = half {
2396 match runs.iter_mut().find(|(run, _)| *run == half) {
2397 Some((_, offsets)) => offsets.push(dy),
2398 None => runs.push((half, vec![dy])),
2399 }
2400 }
2401 }
2402 Self {
2403 extent: extent as usize,
2404 runs,
2405 rim,
2406 }
2407 }
2408}
2409
2410#[derive(Default)]
2413struct WindowMax {
2414 padded: Vec<u8>,
2415 from_start: Vec<u8>,
2416 to_end: Vec<u8>,
2417}
2418
2419impl WindowMax {
2420 fn of(&mut self, row: &[u8], left: usize, count: usize, half: usize, out: &mut [u8]) {
2423 let window = 2 * half + 1;
2424 let length = count + 2 * half;
2425 self.padded.clear();
2426 self.padded.resize(length, 0);
2427 let first_column = left.saturating_sub(half);
2429 let last_column = (left + count + half).min(row.len());
2430 if first_column < last_column {
2431 let at = first_column + half - left;
2432 self.padded[at..at + last_column - first_column]
2433 .copy_from_slice(&row[first_column..last_column]);
2434 }
2435 self.from_start.clear();
2436 self.from_start.resize(length, 0);
2437 self.to_end.clear();
2438 self.to_end.resize(length, 0);
2439 for block in (0..length).step_by(window) {
2440 let end = (block + window).min(length);
2441 let mut highest = 0;
2442 for index in block..end {
2443 highest = highest.max(self.padded[index]);
2444 self.from_start[index] = highest;
2445 }
2446 highest = 0;
2447 for index in (block..end).rev() {
2448 highest = highest.max(self.padded[index]);
2449 self.to_end[index] = highest;
2450 }
2451 }
2452 for (index, value) in out[..count].iter_mut().enumerate() {
2453 *value = self.to_end[index].max(self.from_start[index + window - 1]);
2454 }
2455 }
2456}
2457
2458#[cfg(test)]
2459mod tests;