1use std::cell::OnceCell;
14use std::cmp::Ordering;
15use std::collections::{BTreeMap, HashMap};
16use std::fmt;
17use std::ops::Range;
18use std::sync::Arc;
19
20use ab_glyph_rasterizer::{Rasterizer, point};
21use alpha2_media::{
22 EmojiPolicy, HostTexture, HostTexturePixels, Premultiplication, RasterColorSpace, TextAlign,
23 TextTexture,
24};
25use rustybuzz::ttf_parser::{self, GlyphId, OutlineBuilder};
26use rustybuzz::{Direction, UnicodeBuffer};
27use unicode_bidi::ParagraphBidiInfo;
28use unicode_linebreak::BreakOpportunity;
29use unicode_script::{Script, UnicodeScript};
30use unicode_segmentation::UnicodeSegmentation;
31
32mod unicode_tables;
33
34const MAX_TEXT_BYTES: usize = 64 * 1024;
35const MAX_FONT_SIZE: f32 = 2_048.0;
36const MAX_LINE_HEIGHT: f32 = 10.0;
37const MAX_OUTLINE_WIDTH: f32 = 64.0;
38const MAX_BACKGROUND_PADDING: f32 = 256.0;
39
40#[derive(Clone, Debug)]
42pub struct TextFont {
43 pub family: String,
45 pub bytes: Arc<[u8]>,
47 pub face_index: u32,
49}
50
51#[derive(Clone, Debug, PartialEq)]
53pub struct TextRasterRequest {
54 pub text: String,
56 pub font_size: f32,
58 pub max_width: f32,
60 pub line_height: f32,
62 pub align: TextAlign,
64 pub color: [u8; 4],
67 pub emoji_policy: EmojiPolicy,
69 pub content_version: u64,
71 pub outline: Option<TextOutline>,
73 pub background: Option<TextBackground>,
75 pub font_family: Option<String>,
79}
80
81#[derive(Copy, Clone, Debug, PartialEq)]
88pub struct TextOutline {
89 pub color: [u8; 4],
91 pub width: f32,
93}
94
95#[derive(Copy, Clone, Debug, PartialEq)]
98pub struct TextBackground {
99 pub color: [u8; 4],
101 pub padding: f32,
103}
104
105#[derive(Clone, Debug, PartialEq)]
107pub struct TextRasterResult {
108 pub descriptor: TextTexture,
110 pub pixels: HostTexturePixels,
112}
113
114#[derive(Clone, Debug, Eq, PartialEq)]
116#[non_exhaustive]
117pub enum TextRasterError {
118 NoFonts,
120 InvalidFont(String),
122 InvalidRequest,
124 TooLarge,
126}
127
128impl fmt::Display for TextRasterError {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::NoFonts => formatter.write_str("text rasterizer requires at least one font"),
132 Self::InvalidFont(error) => write!(formatter, "invalid text font: {error}"),
133 Self::InvalidRequest => formatter.write_str("invalid text raster request"),
134 Self::TooLarge => formatter.write_str("text raster result is too large"),
135 }
136 }
137}
138
139impl std::error::Error for TextRasterError {}
140
141#[derive(Clone, Debug)]
142struct LoadedFont {
143 source: TextFont,
144 family: Option<String>,
146}
147
148impl LoadedFont {
149 fn parse(source: TextFont) -> Result<Self, TextRasterError> {
153 check_font(&source.bytes, source.face_index)?;
154 let family = font_family(&source.bytes, source.face_index);
155 Ok(Self { source, family })
156 }
157}
158
159pub fn check_font(bytes: &[u8], face_index: u32) -> Result<(), TextRasterError> {
167 ttf_parser::Face::parse(bytes, face_index)
168 .map(|_| ())
169 .map_err(|error| TextRasterError::InvalidFont(error.to_string()))
170}
171
172#[must_use]
177pub fn font_family(bytes: &[u8], face_index: u32) -> Option<String> {
178 use rustybuzz::ttf_parser::{Language, name_id};
179 let face = rustybuzz::ttf_parser::Face::parse(bytes, face_index).ok()?;
180 let names = face.names();
181 [name_id::TYPOGRAPHIC_FAMILY, name_id::FAMILY]
182 .into_iter()
183 .find_map(|id| {
184 let decoded = || {
185 names
186 .into_iter()
187 .filter(move |name| name.name_id == id)
188 .filter_map(|name| {
189 let text = name.to_string()?;
190 (!text.trim().is_empty()).then_some((name.language(), text))
191 })
192 };
193 decoded()
194 .find(|(language, _)| *language == Language::English_UnitedStates)
195 .or_else(|| decoded().next())
196 .map(|(_, text)| text)
197 })
198}
199
200pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
203
204const CACHE_ENTRY_OVERHEAD: usize = 256;
207
208#[derive(Debug)]
215pub struct TextRasterizer {
216 fonts: Vec<LoadedFont>,
217 cache: ResultCache,
218}
219
220#[derive(Debug)]
223struct ResultCache {
224 entries: HashMap<u64, CacheEntry>,
225 by_use: BTreeMap<u64, u64>,
227 next_use: u64,
228 bytes: usize,
229 budget: usize,
230}
231
232#[derive(Debug)]
233struct CacheEntry {
234 result: Arc<TextRasterResult>,
235 last_use: u64,
236 bytes: usize,
237}
238
239impl ResultCache {
240 fn new(budget: usize) -> Self {
241 Self {
242 entries: HashMap::new(),
243 by_use: BTreeMap::new(),
244 next_use: 0,
245 bytes: 0,
246 budget,
247 }
248 }
249
250 fn get(&mut self, version: u64) -> Option<Arc<TextRasterResult>> {
251 let entry = self.entries.get_mut(&version)?;
252 self.by_use.remove(&entry.last_use);
253 entry.last_use = self.next_use;
254 self.by_use.insert(self.next_use, version);
255 self.next_use += 1;
256 Some(Arc::clone(&entry.result))
257 }
258
259 fn insert(&mut self, version: u64, result: &Arc<TextRasterResult>) {
262 self.remove(version);
263 let bytes = result
264 .pixels
265 .rgba8
266 .len()
267 .saturating_add(CACHE_ENTRY_OVERHEAD);
268 if bytes > self.budget {
269 return;
270 }
271 self.bytes += bytes;
272 self.entries.insert(
273 version,
274 CacheEntry {
275 result: Arc::clone(result),
276 last_use: self.next_use,
277 bytes,
278 },
279 );
280 self.by_use.insert(self.next_use, version);
281 self.next_use += 1;
282 self.evict();
283 }
284
285 fn remove(&mut self, version: u64) {
286 if let Some(entry) = self.entries.remove(&version) {
287 self.by_use.remove(&entry.last_use);
288 self.bytes -= entry.bytes;
289 }
290 }
291
292 fn clear(&mut self) {
293 self.entries.clear();
294 self.by_use.clear();
295 self.bytes = 0;
296 }
297
298 fn set_budget(&mut self, budget: usize) {
299 self.budget = budget;
300 self.evict();
301 }
302
303 fn evict(&mut self) {
304 while self.bytes > self.budget {
305 let Some((_, version)) = self.by_use.pop_first() else {
306 break;
307 };
308 if let Some(entry) = self.entries.remove(&version) {
309 self.bytes -= entry.bytes;
310 }
311 }
312 }
313}
314
315impl TextRasterizer {
316 #[must_use]
318 pub fn empty() -> Self {
319 Self {
320 fonts: Vec::new(),
321 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
322 }
323 }
324
325 pub fn new(fonts: Vec<TextFont>) -> Result<Self, TextRasterError> {
330 if fonts.is_empty() {
331 return Err(TextRasterError::NoFonts);
332 }
333 let loaded = fonts
334 .into_iter()
335 .map(LoadedFont::parse)
336 .collect::<Result<Vec<_>, _>>()?;
337 Ok(Self {
338 fonts: loaded,
339 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
340 })
341 }
342
343 pub fn add_font(&mut self, source: TextFont) -> Result<(), TextRasterError> {
351 self.fonts.push(LoadedFont::parse(source)?);
352 self.cache.clear();
353 Ok(())
354 }
355
356 #[must_use]
358 pub fn font_count(&self) -> usize {
359 self.fonts.len()
360 }
361
362 pub fn rasterize(
368 &mut self,
369 request: &TextRasterRequest,
370 ) -> Result<Arc<TextRasterResult>, TextRasterError> {
371 validate_request(request)?;
372 if self.fonts.is_empty() {
373 return Err(TextRasterError::NoFonts);
374 }
375 if let Some(cached) = self.cache.get(request.content_version) {
376 return Ok(cached);
377 }
378 let result = Arc::new(self.rasterize_uncached(request)?);
379 self.cache.insert(request.content_version, &result);
380 Ok(result)
381 }
382
383 pub fn invalidate(&mut self, content_version: u64) {
385 self.cache.remove(content_version);
386 }
387
388 pub fn set_cache_budget(&mut self, bytes: usize) {
393 self.cache.set_budget(bytes);
394 }
395
396 #[must_use]
398 pub fn cache_bytes(&self) -> usize {
399 self.cache.bytes
400 }
401
402 pub fn clear(&mut self) {
404 self.cache.clear();
405 }
406
407 #[must_use]
409 pub fn cache_len(&self) -> usize {
410 self.cache.entries.len()
411 }
412
413 #[allow(
414 clippy::cast_possible_truncation,
415 clippy::cast_precision_loss,
416 clippy::too_many_lines
417 )]
418 fn rasterize_uncached(
419 &self,
420 request: &TextRasterRequest,
421 ) -> Result<TextRasterResult, TextRasterError> {
422 let line_advance = request.font_size * request.line_height;
423 let ascent = request.font_size * 0.8;
424 let descent = request.font_size * 0.2;
425 let faces = Faces::new(&self.fonts)?;
426 let order = self.font_order(request.font_family.as_deref());
427 let mut lines = Vec::new();
428 let mut kept = 1;
431 for paragraph in normalize(&request.text, request.emoji_policy).split('\n') {
432 for line in layout_paragraph(
433 &faces,
434 paragraph,
435 request.font_size,
436 request.max_width,
437 &order,
438 ) {
439 lines.push(line);
440 if lines.last().is_some_and(|line| line.has_glyphs) {
441 kept = lines.len();
442 }
443 }
444 }
445 lines.truncate(kept);
446 let line_count = u32::try_from(lines.len()).map_err(|_| TextRasterError::TooLarge)?;
447 let layout_width = if request.max_width > 0.0 {
448 request.max_width
449 } else {
450 lines.iter().map(|line| line.width).fold(0.0, f32::max)
451 }
452 .max(1.0);
453 let layout_height =
455 ((line_count - 1) as f32 * line_advance + line_advance.max(request.font_size)).max(1.0);
456 let box_width = checked_dimension(layout_width)?;
457 let box_height = checked_dimension(layout_height)?;
458 let line_offset = |line: &LaidLine| match (request.align, line.rtl) {
460 (TextAlign::Center, _) => (layout_width - line.width) * 0.5,
461 (TextAlign::Start, false) | (TextAlign::End, true) => 0.0,
462 (TextAlign::End, false) | (TextAlign::Start, true) => layout_width - line.width,
463 };
464 let mut outlines = HashMap::<(usize, u16), Outline>::new();
465 let mut placed = Vec::new();
466 let (mut overflow_x, mut overflow_y) = (0_i64, 0_i64);
468 let (mut ink_ascent, mut ink_descent) = (ascent, descent);
469 for (index, line) in lines.iter().enumerate() {
470 let offset = line_offset(line);
471 let baseline = index as f32 * line_advance + ascent;
472 for glyph in &line.glyphs {
473 let outline = outlines
474 .entry((glyph.font, glyph.glyph_index))
475 .or_insert_with(|| Outline::of(&faces.parsed[glyph.font], glyph.glyph_index));
476 let Some(bounds) = outline.bounds else {
477 continue;
478 };
479 let scale = request.font_size / faces.units_per_em(glyph.font);
480 let origin = [glyph.x + glyph.x_offset + offset, baseline - glyph.y_offset];
481 let pixels = PixelBox::of(bounds, origin, scale);
482 overflow_x = overflow_x
483 .max(pixels.left.saturating_neg())
484 .max(pixels.right.saturating_sub(i64::from(box_width)));
485 overflow_y = overflow_y
486 .max(pixels.top.saturating_neg())
487 .max(pixels.bottom.saturating_sub(i64::from(box_height)));
488 ink_ascent = ink_ascent.max(bounds[3] * scale + glyph.y_offset);
489 ink_descent = ink_descent.max(-(bounds[1] * scale + glyph.y_offset));
490 placed.push(((glyph.font, glyph.glyph_index), origin, scale, pixels));
491 }
492 }
493 let (outline, padding) = decoration_extents(request);
496 let margin = |overflow: i64| -> Result<u32, TextRasterError> {
497 u32::try_from(overflow)
498 .map_err(|_| TextRasterError::TooLarge)?
499 .checked_add(outline)
500 .map(|margin| margin.max(outline.max(padding)))
501 .ok_or(TextRasterError::TooLarge)
502 };
503 let (margin_x, margin_y) = (margin(overflow_x)?, margin(overflow_y)?);
504 let width = margin_x
505 .checked_mul(2)
506 .and_then(|margins| box_width.checked_add(margins))
507 .ok_or(TextRasterError::TooLarge)?;
508 let height = margin_y
509 .checked_mul(2)
510 .and_then(|margins| box_height.checked_add(margins))
511 .ok_or(TextRasterError::TooLarge)?;
512 if width > 16_384 || height > 16_384 {
513 return Err(TextRasterError::TooLarge);
514 }
515 let pixel_count = usize::try_from(width)
516 .ok()
517 .and_then(|width| {
518 usize::try_from(height)
519 .ok()
520 .and_then(|height| width.checked_mul(height))
521 })
522 .ok_or(TextRasterError::TooLarge)?;
523 let mut coverage = vec![0_u8; pixel_count];
524 let mut line_boxes = Vec::with_capacity(lines.len());
525 for (index, line) in lines.iter().enumerate() {
526 if line.width > 0.0 {
527 line_boxes.push((line_offset(line), line.width, index as f32 * line_advance));
528 }
529 }
530 for (key, origin, scale, pixels) in placed {
531 let alpha = outlines[&key].draw(origin, scale, pixels);
532 place_glyph(
533 &mut coverage,
534 width,
535 height,
536 pixels.left + i64::from(margin_x),
537 pixels.top + i64::from(margin_y),
538 usize::try_from(pixels.right - pixels.left).unwrap_or(0),
539 &alpha,
540 );
541 }
542 let rgba8 = compose(request, &coverage, width, [margin_x, margin_y], &line_boxes);
543 let texture = HostTexture {
544 content_version: request.content_version,
545 width,
546 height,
547 premultiplication: Premultiplication::Premultiplied,
548 color_space: RasterColorSpace::Srgb,
549 };
550 Ok(TextRasterResult {
551 descriptor: TextTexture {
552 texture: texture.clone(),
553 layout_x: margin_x as f32,
554 layout_y: margin_y as f32,
555 layout_width,
556 layout_height,
557 first_baseline: ascent,
558 ascent: ink_ascent,
559 descent: ink_descent,
560 line_count,
561 },
562 pixels: HostTexturePixels { texture, rgba8 },
563 })
564 }
565
566 fn font_order(&self, family: Option<&str>) -> Vec<usize> {
569 let preferred = |font: &LoadedFont| {
570 family.is_some_and(|wanted| {
571 font.family
572 .as_deref()
573 .is_some_and(|name| name.eq_ignore_ascii_case(wanted))
574 })
575 };
576 let (mut order, rest): (Vec<usize>, Vec<usize>) =
577 (0..self.fonts.len()).partition(|&index| preferred(&self.fonts[index]));
578 order.extend(rest);
579 order
580 }
581}
582
583struct Faces<'a> {
586 parsed: Vec<ttf_parser::Face<'a>>,
587 shapers: Vec<OnceCell<rustybuzz::Face<'a>>>,
588}
589
590impl<'a> Faces<'a> {
591 fn new(fonts: &'a [LoadedFont]) -> Result<Self, TextRasterError> {
592 let parsed = fonts
593 .iter()
594 .map(|font| {
595 ttf_parser::Face::parse(&font.source.bytes, font.source.face_index)
596 .ok()
597 .filter(|face| face.units_per_em() > 0)
598 .ok_or_else(|| TextRasterError::InvalidFont(font.source.family.clone()))
599 })
600 .collect::<Result<Vec<_>, _>>()?;
601 Ok(Self {
602 shapers: parsed.iter().map(|_| OnceCell::new()).collect(),
603 parsed,
604 })
605 }
606
607 fn shaper(&self, font: usize) -> &rustybuzz::Face<'a> {
608 self.shapers[font].get_or_init(|| rustybuzz::Face::from_face(self.parsed[font].clone()))
609 }
610
611 #[allow(clippy::cast_precision_loss)]
612 fn units_per_em(&self, font: usize) -> f32 {
613 f32::from(self.parsed[font].units_per_em())
614 }
615
616 fn has(&self, font: usize, character: char) -> bool {
618 self.parsed[font]
619 .glyph_index(character)
620 .is_some_and(|glyph| glyph.0 != 0)
621 }
622
623 fn covers(&self, font: usize, grapheme: &str) -> bool {
625 grapheme
626 .chars()
627 .filter(|&character| !is_default_ignorable(character))
628 .all(|character| self.has(font, character))
629 }
630}
631
632fn normalize(text: &str, policy: EmojiPolicy) -> String {
639 let mut normalized = String::with_capacity(text.len());
640 for grapheme in text.graphemes(true) {
641 if policy == EmojiPolicy::Replace && is_emoji(grapheme) {
642 normalized.push('\u{fffd}');
643 } else if grapheme == "\r\n" {
644 normalized.push('\n');
645 } else {
646 normalized.extend(grapheme.chars().map(|character| match character {
647 '\r'
648 | '\u{b}'
649 | '\u{c}'
650 | '\u{1c}'..='\u{1e}'
651 | '\u{85}'
652 | '\u{2028}'
653 | '\u{2029}' => '\n',
654 '\t' => ' ',
655 other => other,
656 }));
657 }
658 }
659 normalized
660}
661
662fn is_emoji(grapheme: &str) -> bool {
668 if grapheme.contains('\u{fe0e}') {
669 return false;
670 }
671 let selected = grapheme.contains('\u{fe0f}');
672 grapheme.chars().any(|character| {
673 in_table(unicode_tables::EMOJI_PRESENTATION, character)
674 || (selected
675 && (character == '\u{20e3}'
676 || in_table(unicode_tables::EXTENDED_PICTOGRAPHIC, character)))
677 })
678}
679
680fn is_default_ignorable(character: char) -> bool {
681 in_table(unicode_tables::DEFAULT_IGNORABLE, character)
682}
683
684fn in_table(table: &[(u32, u32)], character: char) -> bool {
685 let code = u32::from(character);
686 table
687 .binary_search_by(|&(first, last)| {
688 if last < code {
689 Ordering::Less
690 } else if first > code {
691 Ordering::Greater
692 } else {
693 Ordering::Equal
694 }
695 })
696 .is_ok()
697}
698
699fn hangs(character: char) -> bool {
703 character.is_whitespace()
704 && !matches!(character, '\u{a0}' | '\u{2007}' | '\u{202f}' | '\u{1680}')
705}
706
707fn is_hangul(character: char) -> bool {
708 matches!(
709 character,
710 '\u{1100}'..='\u{11ff}'
711 | '\u{3130}'..='\u{318f}'
712 | '\u{a960}'..='\u{a97f}'
713 | '\u{ac00}'..='\u{d7a3}'
714 | '\u{d7b0}'..='\u{d7ff}'
715 )
716}
717
718struct LaidLine {
720 glyphs: Vec<PositionedGlyph>,
722 width: f32,
724 rtl: bool,
726 has_glyphs: bool,
728}
729
730fn layout_paragraph(
734 faces: &Faces<'_>,
735 paragraph: &str,
736 font_size: f32,
737 max_width: f32,
738 order: &[usize],
739) -> Vec<LaidLine> {
740 let bidi = ParagraphBidiInfo::new(paragraph, None);
741 let rtl = bidi.paragraph_level.is_rtl();
742 let glyphs = shape_paragraph(faces, paragraph, &bidi, font_size, order);
743 let clusters = clusters(&glyphs, paragraph);
744 break_lines(&clusters, max_width)
745 .into_iter()
746 .map(|range| {
747 let has_glyphs = !range.is_empty();
748 let mut end = range.end;
750 while end > range.start && clusters[end - 1].hangs {
751 end -= 1;
752 }
753 let mut line = LaidLine {
754 glyphs: Vec::new(),
755 width: 0.0,
756 rtl,
757 has_glyphs,
758 };
759 if end == range.start {
760 return line;
761 }
762 let bytes = clusters[range.start].start
763 ..clusters
764 .get(range.end)
765 .map_or(paragraph.len(), |next| next.start);
766 let levels = bidi.reordered_levels(bytes);
767 let mut x = 0.0_f32;
768 for index in visual_order(
769 clusters[range.start].glyphs.start..clusters[end - 1].glyphs.end,
770 |index| levels[glyphs[index].cluster].number(),
771 ) {
772 let glyph = &glyphs[index];
773 line.glyphs.push(PositionedGlyph {
774 font: glyph.font,
775 glyph_index: glyph.glyph_index,
776 x,
777 x_offset: glyph.x_offset,
778 y_offset: glyph.y_offset,
779 });
780 x += glyph.advance;
781 }
782 line.width = x;
783 line
784 })
785 .collect()
786}
787
788fn visual_order(range: Range<usize>, level: impl Fn(usize) -> u8) -> Vec<usize> {
793 let mut order = range.collect::<Vec<_>>();
794 let levels = order.iter().map(|&index| level(index)).collect::<Vec<_>>();
795 let highest = levels.iter().copied().max().unwrap_or(0);
796 let Some(lowest_odd) = levels.iter().copied().filter(|level| level % 2 == 1).min() else {
797 return order;
798 };
799 let mut current = levels;
800 for at_least in (lowest_odd..=highest).rev() {
801 let mut start = 0;
802 while start < order.len() {
803 if current[start] < at_least {
804 start += 1;
805 continue;
806 }
807 let end = (start..order.len())
808 .find(|&index| current[index] < at_least)
809 .unwrap_or(order.len());
810 order[start..end].reverse();
811 current[start..end].reverse();
812 start = end;
813 }
814 }
815 order
816}
817
818#[derive(Clone, Debug, PartialEq)]
821struct Run {
822 range: Range<usize>,
823 font: usize,
824 script: Option<Script>,
825 rtl: bool,
826}
827
828fn itemize(
835 faces: &Faces<'_>,
836 paragraph: &str,
837 bidi: &ParagraphBidiInfo<'_>,
838 order: &[usize],
839) -> Vec<Run> {
840 let first = order.first().copied().unwrap_or(0);
841 let mut graphemes = Vec::<(usize, usize, Option<Script>, bool)>::new();
842 let mut current = None;
843 for (start, grapheme) in paragraph.grapheme_indices(true) {
844 let script = grapheme
845 .chars()
846 .map(|character| character.script())
847 .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown));
848 let stays = current.filter(|&font| script.is_none() && faces.covers(font, grapheme));
849 let font = stays
850 .or_else(|| {
851 order
852 .iter()
853 .copied()
854 .find(|&font| faces.covers(font, grapheme))
855 })
856 .or_else(|| {
857 let base = grapheme
858 .chars()
859 .find(|&character| !is_default_ignorable(character))?;
860 order.iter().copied().find(|&font| faces.has(font, base))
861 })
862 .or(current)
863 .unwrap_or(first);
864 current = Some(font);
865 graphemes.push((start, font, script, bidi.levels[start].is_rtl()));
866 }
867 let leading = graphemes.iter().find_map(|grapheme| grapheme.2);
868 let mut before = leading;
869 for grapheme in &mut graphemes {
870 match grapheme.2 {
871 Some(script) => before = Some(script),
872 None => grapheme.2 = before,
873 }
874 }
875 let mut runs = Vec::<Run>::new();
876 for (index, &(start, font, script, rtl)) in graphemes.iter().enumerate() {
877 let end = graphemes
878 .get(index + 1)
879 .map_or(paragraph.len(), |next| next.0);
880 match runs.last_mut() {
881 Some(run) if run.font == font && run.script == script && run.rtl == rtl => {
882 run.range.end = end;
883 }
884 _ => runs.push(Run {
885 range: start..end,
886 font,
887 script,
888 rtl,
889 }),
890 }
891 }
892 runs
893}
894
895fn shaping_script(script: Script) -> Option<rustybuzz::Script> {
897 let code: [u8; 4] = script.short_name().as_bytes().try_into().ok()?;
898 rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&code))
899}
900
901fn shape_paragraph(
904 faces: &Faces<'_>,
905 paragraph: &str,
906 bidi: &ParagraphBidiInfo<'_>,
907 font_size: f32,
908 order: &[usize],
909) -> Vec<ShapedGlyph> {
910 let mut glyphs = Vec::new();
911 for run in itemize(faces, paragraph, bidi, order) {
912 let face = faces.shaper(run.font);
913 let units = faces.units_per_em(run.font);
914 let mut buffer = UnicodeBuffer::new();
915 buffer.push_str(¶graph[run.range.clone()]);
916 buffer.set_pre_context(¶graph[..run.range.start]);
917 buffer.set_post_context(¶graph[run.range.end..]);
918 buffer.set_direction(if run.rtl {
919 Direction::RightToLeft
920 } else {
921 Direction::LeftToRight
922 });
923 match run.script.and_then(shaping_script) {
924 Some(script) => buffer.set_script(script),
925 None => buffer.guess_segment_properties(),
926 }
927 let shaped = rustybuzz::shape(face, &[], buffer);
928 let first = glyphs.len();
929 for (info, position) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) {
930 #[allow(clippy::cast_precision_loss)]
931 glyphs.push(ShapedGlyph {
932 font: run.font,
933 glyph_index: u16::try_from(info.glyph_id)
936 .ok()
937 .filter(|&glyph| glyph < face.number_of_glyphs())
938 .unwrap_or(0),
939 cluster: run.range.start + usize::try_from(info.cluster).unwrap_or(0),
940 advance: position.x_advance as f32 * font_size / units,
941 x_offset: position.x_offset as f32 * font_size / units,
942 y_offset: position.y_offset as f32 * font_size / units,
943 });
944 }
945 if run.rtl {
948 glyphs[first..].reverse();
949 }
950 }
951 glyphs
952}
953
954#[derive(Copy, Clone, Debug)]
955struct ShapedGlyph {
956 font: usize,
957 glyph_index: u16,
958 cluster: usize,
960 advance: f32,
961 x_offset: f32,
964 y_offset: f32,
965}
966
967#[derive(Clone, Debug, PartialEq)]
969struct Cluster {
970 glyphs: Range<usize>,
971 start: usize,
973 width: f32,
974 hangs: bool,
976 break_before: bool,
978}
979
980fn clusters(glyphs: &[ShapedGlyph], paragraph: &str) -> Vec<Cluster> {
984 let mut opportunities = unicode_linebreak::linebreaks(paragraph)
985 .filter(|&(at, kind)| {
986 kind == BreakOpportunity::Allowed
987 && !(paragraph[..at].chars().next_back().is_some_and(is_hangul)
988 && paragraph[at..].chars().next().is_some_and(is_hangul))
989 })
990 .map(|(at, _)| at)
991 .peekable();
992 let mut clusters = Vec::<Cluster>::new();
993 for (index, glyph) in glyphs.iter().enumerate() {
994 match clusters.last_mut() {
995 Some(cluster) if cluster.start == glyph.cluster => {
996 cluster.glyphs.end = index + 1;
997 cluster.width += glyph.advance;
998 }
999 _ => clusters.push(Cluster {
1000 glyphs: index..index + 1,
1001 start: glyph.cluster,
1002 width: glyph.advance,
1003 hangs: false,
1004 break_before: false,
1005 }),
1006 }
1007 }
1008 for index in 0..clusters.len() {
1009 let start = clusters[index].start;
1010 let end = clusters
1011 .get(index + 1)
1012 .map(|next| next.start)
1013 .filter(|&end| end > start)
1014 .unwrap_or_else(|| {
1015 paragraph[start..]
1016 .chars()
1017 .next()
1018 .map_or(start, |character| start + character.len_utf8())
1019 });
1020 clusters[index].hangs = paragraph[start..end].chars().all(hangs);
1021 let mut found = false;
1022 while let Some(&at) = opportunities.peek() {
1023 if at > start {
1024 break;
1025 }
1026 opportunities.next();
1027 found = true;
1028 }
1029 clusters[index].break_before = index > 0 && found;
1030 }
1031 clusters
1032}
1033
1034fn break_lines(clusters: &[Cluster], max_width: f32) -> Vec<Range<usize>> {
1041 let mut lines = Vec::new();
1042 let mut start = 0;
1043 let mut x = 0.0_f32;
1044 let mut opportunity: Option<(usize, f32)> = None;
1046 for (index, cluster) in clusters.iter().enumerate() {
1047 if cluster.break_before && index > start {
1048 opportunity = Some((index, x));
1049 }
1050 if max_width > 0.0 && !cluster.hangs && index > start && x + cluster.width > max_width {
1051 let (end, width) = opportunity.unwrap_or((index, x));
1052 lines.push(start..end);
1053 start = end;
1054 x -= width;
1055 opportunity = None;
1056 if index > start && x + cluster.width > max_width {
1059 lines.push(start..index);
1060 start = index;
1061 x = 0.0;
1062 }
1063 }
1064 x += cluster.width;
1065 }
1066 lines.push(start..clusters.len());
1067 lines
1068}
1069
1070#[derive(Copy, Clone, Debug)]
1071struct PositionedGlyph {
1072 font: usize,
1073 glyph_index: u16,
1074 x: f32,
1076 x_offset: f32,
1078 y_offset: f32,
1079}
1080
1081#[derive(Default)]
1083struct Outline {
1084 segments: Vec<Segment>,
1085 bounds: Option<[f32; 4]>,
1088}
1089
1090enum Segment {
1091 Line([f32; 2], [f32; 2]),
1092 Quad([f32; 2], [f32; 2], [f32; 2]),
1093 Cubic([f32; 2], [f32; 2], [f32; 2], [f32; 2]),
1094}
1095
1096#[derive(Copy, Clone, Debug)]
1099struct PixelBox {
1100 left: i64,
1101 top: i64,
1102 right: i64,
1103 bottom: i64,
1104}
1105
1106impl PixelBox {
1107 #[allow(clippy::cast_possible_truncation)]
1110 fn of(bounds: [f32; 4], origin: [f32; 2], scale: f32) -> Self {
1111 let [x_min, y_min, x_max, y_max] = bounds;
1112 Self {
1115 left: (origin[0] + x_min * scale).floor() as i64,
1116 top: (origin[1] - y_max * scale).floor() as i64,
1117 right: (origin[0] + x_max * scale).ceil() as i64,
1118 bottom: (origin[1] - y_min * scale).ceil() as i64,
1119 }
1120 }
1121}
1122
1123#[cfg(test)]
1124thread_local! {
1125 static OUTLINES_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1127}
1128
1129impl Outline {
1130 fn of(face: &ttf_parser::Face<'_>, glyph: u16) -> Self {
1131 #[cfg(test)]
1132 OUTLINES_READ.with(|read| read.set(read.get() + 1));
1133 let mut sink = OutlineSink::default();
1134 face.outline_glyph(GlyphId(glyph), &mut sink);
1135 sink.close();
1136 sink.outline
1137 }
1138
1139 #[allow(
1142 clippy::cast_possible_truncation,
1143 clippy::cast_sign_loss,
1144 clippy::cast_precision_loss
1145 )]
1146 fn draw(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1147 let width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
1148 let height = usize::try_from(pixels.bottom - pixels.top).unwrap_or(0);
1149 let mut raster = Rasterizer::new(width, height);
1150 let (left, top) = (pixels.left as f32, pixels.top as f32);
1151 let at =
1152 |[x, y]: [f32; 2]| point(origin[0] + x * scale - left, (origin[1] - y * scale) - top);
1153 for segment in &self.segments {
1154 match *segment {
1155 Segment::Line(from, to) => raster.draw_line(at(from), at(to)),
1156 Segment::Quad(from, control, to) => raster.draw_quad(at(from), at(control), at(to)),
1157 Segment::Cubic(from, first, second, to) => {
1158 raster.draw_cubic(at(from), at(first), at(second), at(to));
1159 }
1160 }
1161 }
1162 let mut alpha = vec![0_u8; width * height];
1163 raster.for_each_pixel(|index, coverage| {
1164 alpha[index] = (coverage.min(1.0) * 255.0).round() as u8;
1165 });
1166 alpha
1167 }
1168}
1169
1170#[derive(Default)]
1172struct OutlineSink {
1173 outline: Outline,
1174 start: [f32; 2],
1175 pen: [f32; 2],
1176}
1177
1178impl OutlineSink {
1179 fn include(&mut self, points: &[[f32; 2]]) {
1180 for &[x, y] in points {
1181 let bounds = self.outline.bounds.get_or_insert([x, y, x, y]);
1182 *bounds = [
1183 bounds[0].min(x),
1184 bounds[1].min(y),
1185 bounds[2].max(x),
1186 bounds[3].max(y),
1187 ];
1188 }
1189 }
1190}
1191
1192impl OutlineBuilder for OutlineSink {
1193 fn move_to(&mut self, x: f32, y: f32) {
1194 self.close();
1195 self.start = [x, y];
1196 self.pen = [x, y];
1197 }
1198
1199 fn line_to(&mut self, x: f32, y: f32) {
1200 let to = [x, y];
1201 self.include(&[self.pen, to]);
1202 self.outline.segments.push(Segment::Line(self.pen, to));
1203 self.pen = to;
1204 }
1205
1206 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1207 let (control, to) = ([x1, y1], [x, y]);
1208 self.include(&[self.pen, control, to]);
1209 self.outline
1210 .segments
1211 .push(Segment::Quad(self.pen, control, to));
1212 self.pen = to;
1213 }
1214
1215 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1216 let (first, second, to) = ([x1, y1], [x2, y2], [x, y]);
1217 self.include(&[self.pen, first, second, to]);
1218 self.outline
1219 .segments
1220 .push(Segment::Cubic(self.pen, first, second, to));
1221 self.pen = to;
1222 }
1223
1224 fn close(&mut self) {
1225 if self.pen.map(f32::to_bits) != self.start.map(f32::to_bits) {
1228 self.line_to(self.start[0], self.start[1]);
1229 }
1230 }
1231}
1232
1233fn validate_request(request: &TextRasterRequest) -> Result<(), TextRasterError> {
1234 if request.font_size.is_finite()
1235 && request.font_size > 0.0
1236 && request.font_size <= MAX_FONT_SIZE
1237 && request.text.len() <= MAX_TEXT_BYTES
1238 && request.max_width.is_finite()
1239 && request.max_width <= 16_384.0
1240 && request.line_height.is_finite()
1241 && request.line_height > 0.0
1242 && request.line_height <= MAX_LINE_HEIGHT
1243 && request.content_version > 0
1244 && request.outline.is_none_or(|outline| {
1245 outline.width.is_finite() && outline.width > 0.0 && outline.width <= MAX_OUTLINE_WIDTH
1246 })
1247 && request.background.is_none_or(|background| {
1248 background.padding.is_finite()
1249 && background.padding >= 0.0
1250 && background.padding <= MAX_BACKGROUND_PADDING
1251 })
1252 {
1253 Ok(())
1254 } else {
1255 Err(TextRasterError::InvalidRequest)
1256 }
1257}
1258
1259fn checked_dimension(value: f32) -> Result<u32, TextRasterError> {
1260 if value.is_finite() && value > 0.0 && value.ceil() <= 16_384.0 {
1261 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1262 Ok(value.ceil() as u32)
1263 } else {
1264 Err(TextRasterError::TooLarge)
1265 }
1266}
1267
1268fn place_glyph(
1272 mask: &mut [u8],
1273 width: u32,
1274 height: u32,
1275 origin_x: i64,
1276 origin_y: i64,
1277 glyph_width: usize,
1278 alpha: &[u8],
1279) {
1280 for (index, coverage) in alpha.iter().copied().enumerate() {
1281 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
1282 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
1283 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) {
1284 continue;
1285 }
1286 let pixel =
1287 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
1288 let slot = &mut mask[usize::try_from(pixel).unwrap_or(0)];
1289 *slot = (*slot).max(coverage);
1290 }
1291}
1292
1293impl TextRasterRequest {
1294 #[must_use]
1299 pub fn decoration_margin(&self) -> u32 {
1300 let (outline, padding) = decoration_extents(self);
1301 outline.max(padding)
1302 }
1303}
1304
1305fn decoration_extents(request: &TextRasterRequest) -> (u32, u32) {
1308 let outline = request
1309 .outline
1310 .map_or(0, |outline| disc_extent(outline.width));
1311 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1312 let padding = request
1313 .background
1314 .map_or(0, |background| background.padding.ceil() as u32);
1315 (outline, padding)
1316}
1317
1318#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1320fn disc_extent(width: f32) -> u32 {
1321 (width + 0.5).ceil() as u32
1322}
1323
1324fn premultiplied(color: [u8; 4], coverage: u8) -> [u32; 4] {
1328 let alpha = u32::from(coverage) * u32::from(color[3]) / 255;
1329 [
1330 u32::from(color[0]) * alpha / 255,
1331 u32::from(color[1]) * alpha / 255,
1332 u32::from(color[2]) * alpha / 255,
1333 alpha,
1334 ]
1335}
1336
1337fn over(source: [u32; 4], destination: [u32; 4]) -> [u32; 4] {
1339 let keep = 255 - source[3];
1340 std::array::from_fn(|channel| source[channel] + (destination[channel] * keep + 127) / 255)
1341}
1342
1343#[allow(
1347 clippy::cast_possible_truncation,
1348 clippy::cast_sign_loss,
1349 clippy::cast_precision_loss
1350)]
1351fn compose(
1352 request: &TextRasterRequest,
1353 coverage: &[u8],
1354 width: u32,
1355 margin: [u32; 2],
1356 line_boxes: &[(f32, f32, f32)],
1357) -> Vec<u8> {
1358 let outline = request.outline.map(|outline| {
1359 (
1360 outline.color,
1361 dilate(
1362 coverage,
1363 width,
1364 u32::try_from(coverage.len() / width.max(1) as usize).unwrap_or(0),
1365 outline.width,
1366 ),
1367 )
1368 });
1369 let background = request.background.map(|background| {
1370 let [margin_x, margin_y] = margin.map(|margin| margin as f32);
1371 let padding = background.padding;
1372 let boxes = line_boxes
1373 .iter()
1374 .map(|&(left, line_width, top)| {
1375 (
1376 margin_x + left - padding,
1377 margin_x + left + line_width + padding,
1378 margin_y + top - padding,
1379 margin_y + top + request.font_size + padding,
1380 )
1381 })
1382 .collect::<Vec<_>>();
1383 (background.color, boxes)
1384 });
1385 let mut rgba8 = vec![0_u8; coverage.len() * 4];
1386 let (mut first, mut last) = (0, 0);
1389 for (index, text) in coverage.iter().copied().enumerate() {
1390 let mut pixel = [0_u32; 4];
1391 if let Some((color, boxes)) = &background {
1392 let x = (index % width as usize) as f32;
1393 let y = (index / width as usize) as f32;
1394 if index % width as usize == 0 {
1395 while first < boxes.len() && boxes[first].3 <= y {
1396 first += 1;
1397 }
1398 last = last.max(first);
1399 while last < boxes.len() && boxes[last].2 < y + 1.0 {
1400 last += 1;
1401 }
1402 }
1403 let covered = boxes[first..last]
1404 .iter()
1405 .map(|&(left, right, top, bottom)| {
1406 let across = (right.min(x + 1.0) - left.max(x)).clamp(0.0, 1.0);
1407 let down = (bottom.min(y + 1.0) - top.max(y)).clamp(0.0, 1.0);
1408 across * down
1409 })
1410 .fold(0.0_f32, f32::max);
1411 pixel = premultiplied(*color, (covered * 255.0).round() as u8);
1412 }
1413 if let Some((color, band)) = &outline {
1414 pixel = over(premultiplied(*color, band[index]), pixel);
1415 }
1416 pixel = over(premultiplied(request.color, text), pixel);
1417 for (channel, value) in pixel.iter().enumerate() {
1418 rgba8[index * 4 + channel] = (*value).min(255) as u8;
1419 }
1420 }
1421 rgba8
1422}
1423
1424#[allow(
1435 clippy::cast_possible_truncation,
1436 clippy::cast_sign_loss,
1437 clippy::cast_precision_loss,
1438 clippy::cast_possible_wrap
1439)]
1440fn dilate(coverage: &[u8], width: u32, height: u32, radius: f32) -> Vec<u8> {
1441 let (width, height) = (width as usize, height as usize);
1442 let mut band = vec![0_u8; coverage.len()];
1443 let Some(ink) = ink_bounds(coverage, width, height) else {
1444 return band;
1445 };
1446 let disc = Disc::new(radius);
1447 let extent = disc.extent;
1448 let reach = Bounds {
1449 left: ink.left.saturating_sub(extent),
1450 top: ink.top.saturating_sub(extent),
1451 right: (ink.right + extent).min(width),
1452 bottom: (ink.bottom + extent).min(height),
1453 };
1454 let span = reach.right - reach.left;
1457 let mut window = WindowMax::default();
1458 let mut maxima = vec![0_u8; span];
1459 for (half, offsets) in &disc.runs {
1460 for source_y in ink.top..ink.bottom {
1461 let row = &coverage[source_y * width..(source_y + 1) * width];
1462 window.of(row, reach.left, span, *half, &mut maxima);
1463 for &offset in offsets {
1464 let Some(y) = source_y.checked_add_signed(-offset) else {
1466 continue;
1467 };
1468 if y < reach.top || y >= reach.bottom {
1469 continue;
1470 }
1471 let out = &mut band[y * width + reach.left..y * width + reach.right];
1472 for (pixel, highest) in out.iter_mut().zip(&maxima) {
1473 *pixel = (*pixel).max(*highest);
1474 }
1475 }
1476 }
1477 }
1478 for &(dx, dy, weight) in &disc.rim {
1479 for source_y in ink.top..ink.bottom {
1480 let Some(y) = source_y.checked_add_signed(-dy) else {
1481 continue;
1482 };
1483 if y >= height {
1484 continue;
1485 }
1486 let first = ink.left.max(usize::try_from(dx).unwrap_or(0));
1489 let last = ink
1490 .right
1491 .min(usize::try_from(width as isize + dx.min(0)).unwrap_or(0));
1492 if first >= last {
1493 continue;
1494 }
1495 let source = &coverage[source_y * width + first..source_y * width + last];
1496 let start = y * width + (first as isize - dx) as usize;
1497 let out = &mut band[start..start + source.len()];
1498 for (pixel, value) in out.iter_mut().zip(source) {
1499 *pixel = (*pixel).max((u32::from(*value) * weight / 255) as u8);
1500 }
1501 }
1502 }
1503 band
1504}
1505
1506#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1508struct Bounds {
1509 left: usize,
1510 top: usize,
1511 right: usize,
1512 bottom: usize,
1513}
1514
1515fn ink_bounds(coverage: &[u8], width: usize, height: usize) -> Option<Bounds> {
1517 let mut bounds: Option<Bounds> = None;
1518 for y in 0..height {
1519 let row = &coverage[y * width..(y + 1) * width];
1520 let Some(left) = row.iter().position(|&value| value > 0) else {
1521 continue;
1522 };
1523 let right = row.iter().rposition(|&value| value > 0).unwrap_or(left) + 1;
1524 bounds = Some(bounds.map_or(
1525 Bounds {
1526 left,
1527 top: y,
1528 right,
1529 bottom: y + 1,
1530 },
1531 |bounds| Bounds {
1532 left: bounds.left.min(left),
1533 top: bounds.top,
1534 right: bounds.right.max(right),
1535 bottom: y + 1,
1536 },
1537 ));
1538 }
1539 bounds
1540}
1541
1542struct Disc {
1545 extent: usize,
1547 runs: Vec<(usize, Vec<isize>)>,
1550 rim: Vec<(isize, isize, u32)>,
1552}
1553
1554impl Disc {
1555 #[allow(
1558 clippy::cast_possible_truncation,
1559 clippy::cast_sign_loss,
1560 clippy::cast_precision_loss,
1561 clippy::cast_possible_wrap
1562 )]
1563 fn new(radius: f32) -> Self {
1564 let extent = disc_extent(radius) as isize;
1565 let mut runs: Vec<(usize, Vec<isize>)> = Vec::new();
1566 let mut rim = Vec::new();
1567 for dy in -extent..=extent {
1568 let mut half = None;
1569 for dx in -extent..=extent {
1570 let distance = ((dx * dx + dy * dy) as f32).sqrt();
1571 let weight = ((radius + 0.5 - distance).clamp(0.0, 1.0) * 255.0).round() as u32;
1572 match weight {
1573 0 => {}
1574 255 => half = Some(dx.unsigned_abs()),
1577 _ => rim.push((dx, dy, weight)),
1578 }
1579 }
1580 if let Some(half) = half {
1581 match runs.iter_mut().find(|(run, _)| *run == half) {
1582 Some((_, offsets)) => offsets.push(dy),
1583 None => runs.push((half, vec![dy])),
1584 }
1585 }
1586 }
1587 Self {
1588 extent: extent as usize,
1589 runs,
1590 rim,
1591 }
1592 }
1593}
1594
1595#[derive(Default)]
1598struct WindowMax {
1599 padded: Vec<u8>,
1600 from_start: Vec<u8>,
1601 to_end: Vec<u8>,
1602}
1603
1604impl WindowMax {
1605 fn of(&mut self, row: &[u8], left: usize, count: usize, half: usize, out: &mut [u8]) {
1608 let window = 2 * half + 1;
1609 let length = count + 2 * half;
1610 self.padded.clear();
1611 self.padded.resize(length, 0);
1612 let first_column = left.saturating_sub(half);
1614 let last_column = (left + count + half).min(row.len());
1615 if first_column < last_column {
1616 let at = first_column + half - left;
1617 self.padded[at..at + last_column - first_column]
1618 .copy_from_slice(&row[first_column..last_column]);
1619 }
1620 self.from_start.clear();
1621 self.from_start.resize(length, 0);
1622 self.to_end.clear();
1623 self.to_end.resize(length, 0);
1624 for block in (0..length).step_by(window) {
1625 let end = (block + window).min(length);
1626 let mut highest = 0;
1627 for index in block..end {
1628 highest = highest.max(self.padded[index]);
1629 self.from_start[index] = highest;
1630 }
1631 highest = 0;
1632 for index in (block..end).rev() {
1633 highest = highest.max(self.padded[index]);
1634 self.to_end[index] = highest;
1635 }
1636 }
1637 for (index, value) in out[..count].iter_mut().enumerate() {
1638 *value = self.to_end[index].max(self.from_start[index + window - 1]);
1639 }
1640 }
1641}
1642
1643#[cfg(test)]
1644mod tests;