1use std::cell::OnceCell;
15use std::cmp::Ordering;
16use std::collections::{BTreeMap, HashMap};
17use std::fmt;
18use std::ops::Range;
19use std::sync::Arc;
20
21use ab_glyph_rasterizer::{Point, Rasterizer, point};
22use alpha2_media::{
23 EmojiPolicy, HostTexture, HostTexturePixels, Premultiplication, RasterColorSpace, TextAlign,
24 TextTexture,
25};
26use rustybuzz::ttf_parser::{self, GlyphId, OutlineBuilder};
27use rustybuzz::{Direction, UnicodeBuffer};
28use unicode_bidi::ParagraphBidiInfo;
29use unicode_linebreak::BreakOpportunity;
30use unicode_script::{Script, UnicodeScript};
31use unicode_segmentation::UnicodeSegmentation;
32
33mod color;
34mod unicode_tables;
35
36const MAX_TEXT_BYTES: usize = 64 * 1024;
37const MAX_FONT_SIZE: f32 = 2_048.0;
38const MAX_LINE_HEIGHT: f32 = 10.0;
39const MAX_OUTLINE_WIDTH: f32 = 64.0;
40const MAX_BACKGROUND_PADDING: f32 = 256.0;
41
42#[derive(Clone, Debug)]
44pub struct TextFont {
45 pub family: String,
47 pub bytes: Arc<[u8]>,
49 pub face_index: u32,
51}
52
53#[derive(Clone, Debug, PartialEq)]
55pub struct TextRasterRequest {
56 pub text: String,
58 pub font_size: f32,
60 pub max_width: f32,
62 pub line_height: f32,
64 pub align: TextAlign,
66 pub color: [u8; 4],
71 pub emoji_policy: EmojiPolicy,
73 pub content_version: u64,
75 pub outline: Option<TextOutline>,
77 pub background: Option<TextBackground>,
79 pub font_family: Option<String>,
83}
84
85#[derive(Copy, Clone, Debug, PartialEq)]
92pub struct TextOutline {
93 pub color: [u8; 4],
95 pub width: f32,
97}
98
99#[derive(Copy, Clone, Debug, PartialEq)]
102pub struct TextBackground {
103 pub color: [u8; 4],
105 pub padding: f32,
107}
108
109#[derive(Clone, Debug, PartialEq)]
111pub struct TextRasterResult {
112 pub descriptor: TextTexture,
114 pub pixels: HostTexturePixels,
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
120#[non_exhaustive]
121pub enum TextRasterError {
122 NoFonts,
124 InvalidFont(String),
126 InvalidRequest,
128 TooLarge,
130}
131
132impl fmt::Display for TextRasterError {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self {
135 Self::NoFonts => formatter.write_str("text rasterizer requires at least one font"),
136 Self::InvalidFont(error) => write!(formatter, "invalid text font: {error}"),
137 Self::InvalidRequest => formatter.write_str("invalid text raster request"),
138 Self::TooLarge => formatter.write_str("text raster result is too large"),
139 }
140 }
141}
142
143impl std::error::Error for TextRasterError {}
144
145#[derive(Clone, Debug)]
146struct LoadedFont {
147 source: TextFont,
148 family: Option<String>,
150}
151
152impl LoadedFont {
153 fn parse(source: TextFont) -> Result<Self, TextRasterError> {
157 check_font(&source.bytes, source.face_index)?;
158 let family = font_family(&source.bytes, source.face_index);
159 Ok(Self { source, family })
160 }
161}
162
163pub fn check_font(bytes: &[u8], face_index: u32) -> Result<(), TextRasterError> {
171 ttf_parser::Face::parse(bytes, face_index)
172 .map(|_| ())
173 .map_err(|error| TextRasterError::InvalidFont(error.to_string()))
174}
175
176#[must_use]
181pub fn font_family(bytes: &[u8], face_index: u32) -> Option<String> {
182 use rustybuzz::ttf_parser::{Language, name_id};
183 let face = rustybuzz::ttf_parser::Face::parse(bytes, face_index).ok()?;
184 let names = face.names();
185 [name_id::TYPOGRAPHIC_FAMILY, name_id::FAMILY]
186 .into_iter()
187 .find_map(|id| {
188 let decoded = || {
189 names
190 .into_iter()
191 .filter(move |name| name.name_id == id)
192 .filter_map(|name| {
193 let text = name.to_string()?;
194 (!text.trim().is_empty()).then_some((name.language(), text))
195 })
196 };
197 decoded()
198 .find(|(language, _)| *language == Language::English_UnitedStates)
199 .or_else(|| decoded().next())
200 .map(|(_, text)| text)
201 })
202}
203
204pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
207
208const CACHE_ENTRY_OVERHEAD: usize = 256;
211
212#[derive(Debug)]
219pub struct TextRasterizer {
220 fonts: Vec<LoadedFont>,
221 cache: ResultCache,
222}
223
224#[derive(Debug)]
227struct ResultCache {
228 entries: HashMap<u64, CacheEntry>,
229 by_use: BTreeMap<u64, u64>,
231 next_use: u64,
232 bytes: usize,
233 budget: usize,
234}
235
236#[derive(Debug)]
237struct CacheEntry {
238 result: Arc<TextRasterResult>,
239 last_use: u64,
240 bytes: usize,
241}
242
243impl ResultCache {
244 fn new(budget: usize) -> Self {
245 Self {
246 entries: HashMap::new(),
247 by_use: BTreeMap::new(),
248 next_use: 0,
249 bytes: 0,
250 budget,
251 }
252 }
253
254 fn get(&mut self, version: u64) -> Option<Arc<TextRasterResult>> {
255 let entry = self.entries.get_mut(&version)?;
256 self.by_use.remove(&entry.last_use);
257 entry.last_use = self.next_use;
258 self.by_use.insert(self.next_use, version);
259 self.next_use += 1;
260 Some(Arc::clone(&entry.result))
261 }
262
263 fn insert(&mut self, version: u64, result: &Arc<TextRasterResult>) {
266 self.remove(version);
267 let bytes = result
268 .pixels
269 .rgba8
270 .len()
271 .saturating_add(CACHE_ENTRY_OVERHEAD);
272 if bytes > self.budget {
273 return;
274 }
275 self.bytes += bytes;
276 self.entries.insert(
277 version,
278 CacheEntry {
279 result: Arc::clone(result),
280 last_use: self.next_use,
281 bytes,
282 },
283 );
284 self.by_use.insert(self.next_use, version);
285 self.next_use += 1;
286 self.evict();
287 }
288
289 fn remove(&mut self, version: u64) {
290 if let Some(entry) = self.entries.remove(&version) {
291 self.by_use.remove(&entry.last_use);
292 self.bytes -= entry.bytes;
293 }
294 }
295
296 fn clear(&mut self) {
297 self.entries.clear();
298 self.by_use.clear();
299 self.bytes = 0;
300 }
301
302 fn set_budget(&mut self, budget: usize) {
303 self.budget = budget;
304 self.evict();
305 }
306
307 fn evict(&mut self) {
308 while self.bytes > self.budget {
309 let Some((_, version)) = self.by_use.pop_first() else {
310 break;
311 };
312 if let Some(entry) = self.entries.remove(&version) {
313 self.bytes -= entry.bytes;
314 }
315 }
316 }
317}
318
319impl TextRasterizer {
320 #[must_use]
322 pub fn empty() -> Self {
323 Self {
324 fonts: Vec::new(),
325 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
326 }
327 }
328
329 pub fn new(fonts: Vec<TextFont>) -> Result<Self, TextRasterError> {
334 if fonts.is_empty() {
335 return Err(TextRasterError::NoFonts);
336 }
337 let loaded = fonts
338 .into_iter()
339 .map(LoadedFont::parse)
340 .collect::<Result<Vec<_>, _>>()?;
341 Ok(Self {
342 fonts: loaded,
343 cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
344 })
345 }
346
347 pub fn add_font(&mut self, source: TextFont) -> Result<(), TextRasterError> {
355 self.fonts.push(LoadedFont::parse(source)?);
356 self.cache.clear();
357 Ok(())
358 }
359
360 #[must_use]
362 pub fn font_count(&self) -> usize {
363 self.fonts.len()
364 }
365
366 pub fn rasterize(
372 &mut self,
373 request: &TextRasterRequest,
374 ) -> Result<Arc<TextRasterResult>, TextRasterError> {
375 validate_request(request)?;
376 if self.fonts.is_empty() {
377 return Err(TextRasterError::NoFonts);
378 }
379 if let Some(cached) = self.cache.get(request.content_version) {
380 return Ok(cached);
381 }
382 let result = Arc::new(self.rasterize_uncached(request)?);
383 self.cache.insert(request.content_version, &result);
384 Ok(result)
385 }
386
387 pub fn invalidate(&mut self, content_version: u64) {
389 self.cache.remove(content_version);
390 }
391
392 pub fn set_cache_budget(&mut self, bytes: usize) {
397 self.cache.set_budget(bytes);
398 }
399
400 #[must_use]
402 pub fn cache_bytes(&self) -> usize {
403 self.cache.bytes
404 }
405
406 pub fn clear(&mut self) {
408 self.cache.clear();
409 }
410
411 #[must_use]
413 pub fn cache_len(&self) -> usize {
414 self.cache.entries.len()
415 }
416
417 #[allow(
418 clippy::cast_possible_truncation,
419 clippy::cast_precision_loss,
420 clippy::too_many_lines
421 )]
422 fn rasterize_uncached(
423 &self,
424 request: &TextRasterRequest,
425 ) -> Result<TextRasterResult, TextRasterError> {
426 let line_advance = request.font_size * request.line_height;
427 let ascent = request.font_size * 0.8;
428 let descent = request.font_size * 0.2;
429 let faces = Faces::new(&self.fonts)?;
430 let order = self.font_order(request.font_family.as_deref());
431 let mut lines = Vec::new();
432 let mut kept = 1;
435 for paragraph in normalize(&request.text, request.emoji_policy).split('\n') {
436 for line in layout_paragraph(
437 &faces,
438 paragraph,
439 request.font_size,
440 request.max_width,
441 &order,
442 request.emoji_policy,
443 ) {
444 lines.push(line);
445 if lines.last().is_some_and(|line| line.has_glyphs) {
446 kept = lines.len();
447 }
448 }
449 }
450 lines.truncate(kept);
451 let line_count = u32::try_from(lines.len()).map_err(|_| TextRasterError::TooLarge)?;
452 let layout_width = if request.max_width > 0.0 {
453 request.max_width
454 } else {
455 lines.iter().map(|line| line.width).fold(0.0, f32::max)
456 }
457 .max(1.0);
458 let layout_height =
460 ((line_count - 1) as f32 * line_advance + line_advance.max(request.font_size)).max(1.0);
461 let box_width = checked_dimension(layout_width)?;
462 let box_height = checked_dimension(layout_height)?;
463 let line_offset = |line: &LaidLine| match (request.align, line.rtl) {
465 (TextAlign::Center, _) => (layout_width - line.width) * 0.5,
466 (TextAlign::Start, false) | (TextAlign::End, true) => 0.0,
467 (TextAlign::End, false) | (TextAlign::Start, true) => layout_width - line.width,
468 };
469 let mut drawings = HashMap::<(usize, u16), Drawing>::new();
470 let mut placed = Vec::new();
471 let (mut overflow_x, mut overflow_y) = (0_i64, 0_i64);
473 let (mut ink_ascent, mut ink_descent) = (ascent, descent);
474 for (index, line) in lines.iter().enumerate() {
475 let offset = line_offset(line);
476 let baseline = index as f32 * line_advance + ascent;
477 for glyph in &line.glyphs {
478 let drawing = drawings
479 .entry((glyph.font, glyph.glyph_index))
480 .or_insert_with(|| Drawing::of(&faces, glyph.font, glyph.glyph_index, request));
481 let Some(bounds) = drawing.bounds() else {
482 continue;
483 };
484 let scale = request.font_size / faces.units_per_em(glyph.font);
485 let origin = [glyph.x + glyph.x_offset + offset, baseline - glyph.y_offset];
486 let pixels = PixelBox::of(bounds, origin, scale);
487 overflow_x = overflow_x
488 .max(pixels.left.saturating_neg())
489 .max(pixels.right.saturating_sub(i64::from(box_width)));
490 overflow_y = overflow_y
491 .max(pixels.top.saturating_neg())
492 .max(pixels.bottom.saturating_sub(i64::from(box_height)));
493 ink_ascent = ink_ascent.max(bounds[3] * scale + glyph.y_offset);
494 ink_descent = ink_descent.max(-(bounds[1] * scale + glyph.y_offset));
495 placed.push(((glyph.font, glyph.glyph_index), origin, scale, pixels));
496 }
497 }
498 let (outline, padding) = decoration_extents(request);
501 let margin = |overflow: i64| -> Result<u32, TextRasterError> {
502 u32::try_from(overflow)
503 .map_err(|_| TextRasterError::TooLarge)?
504 .checked_add(outline)
505 .map(|margin| margin.max(outline.max(padding)))
506 .ok_or(TextRasterError::TooLarge)
507 };
508 let (margin_x, margin_y) = (margin(overflow_x)?, margin(overflow_y)?);
509 let width = margin_x
510 .checked_mul(2)
511 .and_then(|margins| box_width.checked_add(margins))
512 .ok_or(TextRasterError::TooLarge)?;
513 let height = margin_y
514 .checked_mul(2)
515 .and_then(|margins| box_height.checked_add(margins))
516 .ok_or(TextRasterError::TooLarge)?;
517 if width > 16_384 || height > 16_384 {
518 return Err(TextRasterError::TooLarge);
519 }
520 let pixel_count = usize::try_from(width)
521 .ok()
522 .and_then(|width| {
523 usize::try_from(height)
524 .ok()
525 .and_then(|height| width.checked_mul(height))
526 })
527 .ok_or(TextRasterError::TooLarge)?;
528 let mut coverage = vec![0_u8; pixel_count];
529 let mut colors: Option<Vec<u8>> = None;
531 let mut line_boxes = Vec::with_capacity(lines.len());
532 for (index, line) in lines.iter().enumerate() {
533 if line.width > 0.0 {
534 line_boxes.push((line_offset(line), line.width, index as f32 * line_advance));
535 }
536 }
537 for (key, origin, scale, pixels) in placed {
538 let (origin_x, origin_y) = (
539 pixels.left + i64::from(margin_x),
540 pixels.top + i64::from(margin_y),
541 );
542 let glyph_width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
543 match &drawings[&key] {
544 Drawing::Outline(outline) => place_glyph(
545 &mut coverage,
546 width,
547 height,
548 origin_x,
549 origin_y,
550 glyph_width,
551 &outline.draw(origin, scale, pixels),
552 ),
553 drawing => place_color_glyph(
554 colors.get_or_insert_with(|| vec![0; pixel_count * 4]),
555 width,
556 height,
557 origin_x,
558 origin_y,
559 glyph_width,
560 &drawing.draw_color(origin, scale, pixels),
561 ),
562 }
563 }
564 let rgba8 = compose(
565 request,
566 &coverage,
567 colors.as_deref(),
568 width,
569 [margin_x, margin_y],
570 &line_boxes,
571 );
572 let texture = HostTexture {
573 content_version: request.content_version,
574 width,
575 height,
576 premultiplication: Premultiplication::Premultiplied,
577 color_space: RasterColorSpace::Srgb,
578 };
579 Ok(TextRasterResult {
580 descriptor: TextTexture {
581 texture: texture.clone(),
582 layout_x: margin_x as f32,
583 layout_y: margin_y as f32,
584 layout_width,
585 layout_height,
586 first_baseline: ascent,
587 ascent: ink_ascent,
588 descent: ink_descent,
589 line_count,
590 },
591 pixels: HostTexturePixels { texture, rgba8 },
592 })
593 }
594
595 fn font_order(&self, family: Option<&str>) -> Vec<usize> {
598 let preferred = |font: &LoadedFont| {
599 family.is_some_and(|wanted| {
600 font.family
601 .as_deref()
602 .is_some_and(|name| name.eq_ignore_ascii_case(wanted))
603 })
604 };
605 let (mut order, rest): (Vec<usize>, Vec<usize>) =
606 (0..self.fonts.len()).partition(|&index| preferred(&self.fonts[index]));
607 order.extend(rest);
608 order
609 }
610}
611
612struct Faces<'a> {
615 parsed: Vec<ttf_parser::Face<'a>>,
616 shapers: Vec<OnceCell<rustybuzz::Face<'a>>>,
617 color: Vec<bool>,
619}
620
621impl<'a> Faces<'a> {
622 fn new(fonts: &'a [LoadedFont]) -> Result<Self, TextRasterError> {
623 let parsed = fonts
624 .iter()
625 .map(|font| {
626 ttf_parser::Face::parse(&font.source.bytes, font.source.face_index)
627 .ok()
628 .filter(|face| face.units_per_em() > 0)
629 .ok_or_else(|| TextRasterError::InvalidFont(font.source.family.clone()))
630 })
631 .collect::<Result<Vec<_>, _>>()?;
632 Ok(Self {
633 shapers: parsed.iter().map(|_| OnceCell::new()).collect(),
634 color: parsed
635 .iter()
636 .map(|face| {
637 let tables = face.tables();
638 tables.colr.is_some() || tables.cbdt.is_some() || tables.sbix.is_some()
639 })
640 .collect(),
641 parsed,
642 })
643 }
644
645 fn shaper(&self, font: usize) -> &rustybuzz::Face<'a> {
646 self.shapers[font].get_or_init(|| rustybuzz::Face::from_face(self.parsed[font].clone()))
647 }
648
649 #[allow(clippy::cast_precision_loss)]
650 fn units_per_em(&self, font: usize) -> f32 {
651 f32::from(self.parsed[font].units_per_em())
652 }
653
654 fn has(&self, font: usize, character: char) -> bool {
656 self.parsed[font]
657 .glyph_index(character)
658 .is_some_and(|glyph| glyph.0 != 0)
659 }
660
661 fn covers(&self, font: usize, grapheme: &str) -> bool {
663 grapheme
664 .chars()
665 .filter(|&character| !is_default_ignorable(character))
666 .all(|character| self.has(font, character))
667 }
668}
669
670fn normalize(text: &str, policy: EmojiPolicy) -> String {
677 let mut normalized = String::with_capacity(text.len());
678 for grapheme in text.graphemes(true) {
679 if policy == EmojiPolicy::Replace && is_emoji(grapheme) {
680 normalized.push('\u{fffd}');
681 } else if grapheme == "\r\n" {
682 normalized.push('\n');
683 } else {
684 normalized.extend(grapheme.chars().map(|character| match character {
685 '\r'
686 | '\u{b}'
687 | '\u{c}'
688 | '\u{1c}'..='\u{1e}'
689 | '\u{85}'
690 | '\u{2028}'
691 | '\u{2029}' => '\n',
692 '\t' => ' ',
693 other => other,
694 }));
695 }
696 }
697 normalized
698}
699
700fn is_emoji(grapheme: &str) -> bool {
706 if grapheme.contains('\u{fe0e}') {
707 return false;
708 }
709 let selected = grapheme.contains('\u{fe0f}');
710 grapheme.chars().any(|character| {
711 in_table(unicode_tables::EMOJI_PRESENTATION, character)
712 || (selected
713 && (character == '\u{20e3}'
714 || in_table(unicode_tables::EXTENDED_PICTOGRAPHIC, character)))
715 })
716}
717
718fn is_default_ignorable(character: char) -> bool {
719 in_table(unicode_tables::DEFAULT_IGNORABLE, character)
720}
721
722fn in_table(table: &[(u32, u32)], character: char) -> bool {
723 let code = u32::from(character);
724 table
725 .binary_search_by(|&(first, last)| {
726 if last < code {
727 Ordering::Less
728 } else if first > code {
729 Ordering::Greater
730 } else {
731 Ordering::Equal
732 }
733 })
734 .is_ok()
735}
736
737fn hangs(character: char) -> bool {
741 character.is_whitespace()
742 && !matches!(character, '\u{a0}' | '\u{2007}' | '\u{202f}' | '\u{1680}')
743}
744
745fn is_hangul(character: char) -> bool {
746 matches!(
747 character,
748 '\u{1100}'..='\u{11ff}'
749 | '\u{3130}'..='\u{318f}'
750 | '\u{a960}'..='\u{a97f}'
751 | '\u{ac00}'..='\u{d7a3}'
752 | '\u{d7b0}'..='\u{d7ff}'
753 )
754}
755
756struct LaidLine {
758 glyphs: Vec<PositionedGlyph>,
760 width: f32,
762 rtl: bool,
764 has_glyphs: bool,
766}
767
768fn layout_paragraph(
772 faces: &Faces<'_>,
773 paragraph: &str,
774 font_size: f32,
775 max_width: f32,
776 order: &[usize],
777 policy: EmojiPolicy,
778) -> Vec<LaidLine> {
779 let bidi = ParagraphBidiInfo::new(paragraph, None);
780 let rtl = bidi.paragraph_level.is_rtl();
781 let glyphs = shape_paragraph(faces, paragraph, &bidi, font_size, order, policy);
782 let clusters = clusters(&glyphs, paragraph);
783 break_lines(&clusters, max_width)
784 .into_iter()
785 .map(|range| {
786 let has_glyphs = !range.is_empty();
787 let mut end = range.end;
789 while end > range.start && clusters[end - 1].hangs {
790 end -= 1;
791 }
792 let mut line = LaidLine {
793 glyphs: Vec::new(),
794 width: 0.0,
795 rtl,
796 has_glyphs,
797 };
798 if end == range.start {
799 return line;
800 }
801 let bytes = clusters[range.start].start
802 ..clusters
803 .get(range.end)
804 .map_or(paragraph.len(), |next| next.start);
805 let levels = bidi.reordered_levels(bytes);
806 let mut x = 0.0_f32;
807 for index in visual_order(
808 clusters[range.start].glyphs.start..clusters[end - 1].glyphs.end,
809 |index| levels[glyphs[index].cluster].number(),
810 ) {
811 let glyph = &glyphs[index];
812 line.glyphs.push(PositionedGlyph {
813 font: glyph.font,
814 glyph_index: glyph.glyph_index,
815 x,
816 x_offset: glyph.x_offset,
817 y_offset: glyph.y_offset,
818 });
819 x += glyph.advance;
820 }
821 line.width = x;
822 line
823 })
824 .collect()
825}
826
827fn visual_order(range: Range<usize>, level: impl Fn(usize) -> u8) -> Vec<usize> {
832 let mut order = range.collect::<Vec<_>>();
833 let levels = order.iter().map(|&index| level(index)).collect::<Vec<_>>();
834 let highest = levels.iter().copied().max().unwrap_or(0);
835 let Some(lowest_odd) = levels.iter().copied().filter(|level| level % 2 == 1).min() else {
836 return order;
837 };
838 let mut current = levels;
839 for at_least in (lowest_odd..=highest).rev() {
840 let mut start = 0;
841 while start < order.len() {
842 if current[start] < at_least {
843 start += 1;
844 continue;
845 }
846 let end = (start..order.len())
847 .find(|&index| current[index] < at_least)
848 .unwrap_or(order.len());
849 order[start..end].reverse();
850 current[start..end].reverse();
851 start = end;
852 }
853 }
854 order
855}
856
857#[derive(Clone, Debug, PartialEq)]
860struct Run {
861 range: Range<usize>,
862 font: usize,
863 script: Option<Script>,
864 rtl: bool,
865}
866
867fn itemize(
878 faces: &Faces<'_>,
879 paragraph: &str,
880 bidi: &ParagraphBidiInfo<'_>,
881 order: &[usize],
882 policy: EmojiPolicy,
883) -> Vec<Run> {
884 let first = order.first().copied().unwrap_or(0);
885 let (color, text): (Vec<usize>, Vec<usize>) =
886 order.iter().partition(|&&font| faces.color[font]);
887 let color_first = color.iter().chain(&text).copied().collect::<Vec<_>>();
888 let text_first = text.iter().chain(&color).copied().collect::<Vec<_>>();
889 let fits = |font: usize, color_wanted: bool| {
891 faces.color[font] == color_wanted
892 || if color_wanted {
893 color.is_empty()
894 } else {
895 text.is_empty()
896 }
897 };
898 let mut graphemes = Vec::<(usize, usize, Option<Script>, bool)>::new();
899 let mut current = None;
900 for (start, grapheme) in paragraph.grapheme_indices(true) {
901 let script = grapheme
902 .chars()
903 .map(|character| character.script())
904 .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown));
905 let wants_color = if grapheme.contains('\u{fe0e}') {
907 Some(false)
908 } else if is_emoji(grapheme) {
909 Some(policy == EmojiPolicy::Color)
910 } else {
911 None
912 };
913 let order = match wants_color {
914 Some(true) => color_first.as_slice(),
915 Some(false) => text_first.as_slice(),
916 None => order,
917 };
918 let stays = current.filter(|&font| {
919 script.is_none()
920 && wants_color.is_none_or(|wanted| fits(font, wanted))
921 && faces.covers(font, grapheme)
922 });
923 let font = stays
924 .or_else(|| {
925 order
926 .iter()
927 .copied()
928 .find(|&font| faces.covers(font, grapheme))
929 })
930 .or_else(|| {
931 let base = grapheme
932 .chars()
933 .find(|&character| !is_default_ignorable(character))?;
934 order.iter().copied().find(|&font| faces.has(font, base))
935 })
936 .or(current)
937 .unwrap_or(first);
938 current = Some(font);
939 graphemes.push((start, font, script, bidi.levels[start].is_rtl()));
940 }
941 let leading = graphemes.iter().find_map(|grapheme| grapheme.2);
942 let mut before = leading;
943 for grapheme in &mut graphemes {
944 match grapheme.2 {
945 Some(script) => before = Some(script),
946 None => grapheme.2 = before,
947 }
948 }
949 let mut runs = Vec::<Run>::new();
950 for (index, &(start, font, script, rtl)) in graphemes.iter().enumerate() {
951 let end = graphemes
952 .get(index + 1)
953 .map_or(paragraph.len(), |next| next.0);
954 match runs.last_mut() {
955 Some(run) if run.font == font && run.script == script && run.rtl == rtl => {
956 run.range.end = end;
957 }
958 _ => runs.push(Run {
959 range: start..end,
960 font,
961 script,
962 rtl,
963 }),
964 }
965 }
966 runs
967}
968
969fn shaping_script(script: Script) -> Option<rustybuzz::Script> {
971 let code: [u8; 4] = script.short_name().as_bytes().try_into().ok()?;
972 rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&code))
973}
974
975fn shape_paragraph(
978 faces: &Faces<'_>,
979 paragraph: &str,
980 bidi: &ParagraphBidiInfo<'_>,
981 font_size: f32,
982 order: &[usize],
983 policy: EmojiPolicy,
984) -> Vec<ShapedGlyph> {
985 let mut glyphs = Vec::new();
986 for run in itemize(faces, paragraph, bidi, order, policy) {
987 let face = faces.shaper(run.font);
988 let units = faces.units_per_em(run.font);
989 let mut buffer = UnicodeBuffer::new();
990 buffer.push_str(¶graph[run.range.clone()]);
991 buffer.set_pre_context(¶graph[..run.range.start]);
992 buffer.set_post_context(¶graph[run.range.end..]);
993 buffer.set_direction(if run.rtl {
994 Direction::RightToLeft
995 } else {
996 Direction::LeftToRight
997 });
998 match run.script.and_then(shaping_script) {
999 Some(script) => buffer.set_script(script),
1000 None => buffer.guess_segment_properties(),
1001 }
1002 let shaped = rustybuzz::shape(face, &[], buffer);
1003 let first = glyphs.len();
1004 for (info, position) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) {
1005 #[allow(clippy::cast_precision_loss)]
1006 glyphs.push(ShapedGlyph {
1007 font: run.font,
1008 glyph_index: u16::try_from(info.glyph_id)
1011 .ok()
1012 .filter(|&glyph| glyph < face.number_of_glyphs())
1013 .unwrap_or(0),
1014 cluster: run.range.start + usize::try_from(info.cluster).unwrap_or(0),
1015 advance: position.x_advance as f32 * font_size / units,
1016 x_offset: position.x_offset as f32 * font_size / units,
1017 y_offset: position.y_offset as f32 * font_size / units,
1018 });
1019 }
1020 if run.rtl {
1023 glyphs[first..].reverse();
1024 }
1025 }
1026 glyphs
1027}
1028
1029#[derive(Copy, Clone, Debug)]
1030struct ShapedGlyph {
1031 font: usize,
1032 glyph_index: u16,
1033 cluster: usize,
1035 advance: f32,
1036 x_offset: f32,
1039 y_offset: f32,
1040}
1041
1042#[derive(Clone, Debug, PartialEq)]
1044struct Cluster {
1045 glyphs: Range<usize>,
1046 start: usize,
1048 width: f32,
1049 hangs: bool,
1051 break_before: bool,
1053}
1054
1055fn clusters(glyphs: &[ShapedGlyph], paragraph: &str) -> Vec<Cluster> {
1059 let mut opportunities = unicode_linebreak::linebreaks(paragraph)
1060 .filter(|&(at, kind)| {
1061 kind == BreakOpportunity::Allowed
1062 && !(paragraph[..at].chars().next_back().is_some_and(is_hangul)
1063 && paragraph[at..].chars().next().is_some_and(is_hangul))
1064 })
1065 .map(|(at, _)| at)
1066 .peekable();
1067 let mut clusters = Vec::<Cluster>::new();
1068 for (index, glyph) in glyphs.iter().enumerate() {
1069 match clusters.last_mut() {
1070 Some(cluster) if cluster.start == glyph.cluster => {
1071 cluster.glyphs.end = index + 1;
1072 cluster.width += glyph.advance;
1073 }
1074 _ => clusters.push(Cluster {
1075 glyphs: index..index + 1,
1076 start: glyph.cluster,
1077 width: glyph.advance,
1078 hangs: false,
1079 break_before: false,
1080 }),
1081 }
1082 }
1083 for index in 0..clusters.len() {
1084 let start = clusters[index].start;
1085 let end = clusters
1086 .get(index + 1)
1087 .map(|next| next.start)
1088 .filter(|&end| end > start)
1089 .unwrap_or_else(|| {
1090 paragraph[start..]
1091 .chars()
1092 .next()
1093 .map_or(start, |character| start + character.len_utf8())
1094 });
1095 clusters[index].hangs = paragraph[start..end].chars().all(hangs);
1096 let mut found = false;
1097 while let Some(&at) = opportunities.peek() {
1098 if at > start {
1099 break;
1100 }
1101 opportunities.next();
1102 found = true;
1103 }
1104 clusters[index].break_before = index > 0 && found;
1105 }
1106 clusters
1107}
1108
1109fn break_lines(clusters: &[Cluster], max_width: f32) -> Vec<Range<usize>> {
1116 let mut lines = Vec::new();
1117 let mut start = 0;
1118 let mut x = 0.0_f32;
1119 let mut opportunity: Option<(usize, f32)> = None;
1121 for (index, cluster) in clusters.iter().enumerate() {
1122 if cluster.break_before && index > start {
1123 opportunity = Some((index, x));
1124 }
1125 if max_width > 0.0 && !cluster.hangs && index > start && x + cluster.width > max_width {
1126 let (end, width) = opportunity.unwrap_or((index, x));
1127 lines.push(start..end);
1128 start = end;
1129 x -= width;
1130 opportunity = None;
1131 if index > start && x + cluster.width > max_width {
1134 lines.push(start..index);
1135 start = index;
1136 x = 0.0;
1137 }
1138 }
1139 x += cluster.width;
1140 }
1141 lines.push(start..clusters.len());
1142 lines
1143}
1144
1145#[derive(Copy, Clone, Debug)]
1146struct PositionedGlyph {
1147 font: usize,
1148 glyph_index: u16,
1149 x: f32,
1151 x_offset: f32,
1153 y_offset: f32,
1154}
1155
1156enum Drawing {
1159 Outline(Outline),
1160 Painting(color::Painting),
1161 Bitmap(color::Bitmap),
1162}
1163
1164impl Drawing {
1165 fn of(faces: &Faces<'_>, font: usize, glyph: u16, request: &TextRasterRequest) -> Self {
1169 let face = &faces.parsed[font];
1170 if request.emoji_policy == EmojiPolicy::Color && faces.color[font] {
1171 let [red, green, blue, _] = request.color;
1172 if let Some(painting) = color::Painting::of(face, glyph, [red, green, blue, 255]) {
1173 return Self::Painting(painting);
1174 }
1175 if let Some(bitmap) = color::Bitmap::of(face, glyph, request.font_size) {
1176 return Self::Bitmap(bitmap);
1177 }
1178 }
1179 Self::Outline(Outline::of(face, glyph))
1180 }
1181
1182 fn bounds(&self) -> Option<[f32; 4]> {
1185 match self {
1186 Self::Outline(outline) => outline.bounds,
1187 Self::Painting(painting) => painting.bounds(),
1188 Self::Bitmap(bitmap) => Some(bitmap.bounds()),
1189 }
1190 }
1191
1192 fn draw_color(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1195 match self {
1196 Self::Outline(_) => Vec::new(),
1197 Self::Painting(painting) => painting.draw(origin, scale, pixels),
1198 Self::Bitmap(bitmap) => bitmap.draw(origin, scale, pixels),
1199 }
1200 }
1201}
1202
1203#[derive(Default)]
1205struct Outline {
1206 segments: Vec<Segment>,
1207 bounds: Option<[f32; 4]>,
1210}
1211
1212enum Segment {
1213 Line([f32; 2], [f32; 2]),
1214 Quad([f32; 2], [f32; 2], [f32; 2]),
1215 Cubic([f32; 2], [f32; 2], [f32; 2], [f32; 2]),
1216}
1217
1218#[derive(Copy, Clone, Debug)]
1221struct PixelBox {
1222 left: i64,
1223 top: i64,
1224 right: i64,
1225 bottom: i64,
1226}
1227
1228impl PixelBox {
1229 #[allow(clippy::cast_possible_truncation)]
1232 fn of(bounds: [f32; 4], origin: [f32; 2], scale: f32) -> Self {
1233 let [x_min, y_min, x_max, y_max] = bounds;
1234 Self {
1237 left: (origin[0] + x_min * scale).floor() as i64,
1238 top: (origin[1] - y_max * scale).floor() as i64,
1239 right: (origin[0] + x_max * scale).ceil() as i64,
1240 bottom: (origin[1] - y_min * scale).ceil() as i64,
1241 }
1242 }
1243}
1244
1245#[cfg(test)]
1246thread_local! {
1247 static OUTLINES_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1249}
1250
1251impl Outline {
1252 fn of(face: &ttf_parser::Face<'_>, glyph: u16) -> Self {
1253 #[cfg(test)]
1254 OUTLINES_READ.with(|read| read.set(read.get() + 1));
1255 let mut sink = OutlineSink::default();
1256 face.outline_glyph(GlyphId(glyph), &mut sink);
1257 sink.close();
1258 sink.outline
1259 }
1260
1261 #[allow(
1264 clippy::cast_possible_truncation,
1265 clippy::cast_sign_loss,
1266 clippy::cast_precision_loss
1267 )]
1268 fn draw(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1269 let width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
1270 let height = usize::try_from(pixels.bottom - pixels.top).unwrap_or(0);
1271 let (left, top) = (pixels.left as f32, pixels.top as f32);
1272 let raster = self.raster(width, height, |[x, y]| {
1273 point(origin[0] + x * scale - left, (origin[1] - y * scale) - top)
1274 });
1275 let mut alpha = vec![0_u8; width * height];
1276 raster.for_each_pixel(|index, coverage| {
1277 alpha[index] = (coverage.min(1.0) * 255.0).round() as u8;
1278 });
1279 alpha
1280 }
1281}
1282
1283impl Outline {
1284 fn raster(&self, width: usize, height: usize, at: impl Fn([f32; 2]) -> Point) -> Rasterizer {
1287 let mut raster = Rasterizer::new(width, height);
1288 for segment in &self.segments {
1289 match *segment {
1290 Segment::Line(from, to) => raster.draw_line(at(from), at(to)),
1291 Segment::Quad(from, control, to) => raster.draw_quad(at(from), at(control), at(to)),
1292 Segment::Cubic(from, first, second, to) => {
1293 raster.draw_cubic(at(from), at(first), at(second), at(to));
1294 }
1295 }
1296 }
1297 raster
1298 }
1299}
1300
1301#[derive(Default)]
1303struct OutlineSink {
1304 outline: Outline,
1305 start: [f32; 2],
1306 pen: [f32; 2],
1307}
1308
1309impl OutlineSink {
1310 fn include(&mut self, points: &[[f32; 2]]) {
1311 for &[x, y] in points {
1312 let bounds = self.outline.bounds.get_or_insert([x, y, x, y]);
1313 *bounds = [
1314 bounds[0].min(x),
1315 bounds[1].min(y),
1316 bounds[2].max(x),
1317 bounds[3].max(y),
1318 ];
1319 }
1320 }
1321}
1322
1323impl OutlineBuilder for OutlineSink {
1324 fn move_to(&mut self, x: f32, y: f32) {
1325 self.close();
1326 self.start = [x, y];
1327 self.pen = [x, y];
1328 }
1329
1330 fn line_to(&mut self, x: f32, y: f32) {
1331 let to = [x, y];
1332 self.include(&[self.pen, to]);
1333 self.outline.segments.push(Segment::Line(self.pen, to));
1334 self.pen = to;
1335 }
1336
1337 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1338 let (control, to) = ([x1, y1], [x, y]);
1339 self.include(&[self.pen, control, to]);
1340 self.outline
1341 .segments
1342 .push(Segment::Quad(self.pen, control, to));
1343 self.pen = to;
1344 }
1345
1346 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1347 let (first, second, to) = ([x1, y1], [x2, y2], [x, y]);
1348 self.include(&[self.pen, first, second, to]);
1349 self.outline
1350 .segments
1351 .push(Segment::Cubic(self.pen, first, second, to));
1352 self.pen = to;
1353 }
1354
1355 fn close(&mut self) {
1356 if self.pen.map(f32::to_bits) != self.start.map(f32::to_bits) {
1359 self.line_to(self.start[0], self.start[1]);
1360 }
1361 }
1362}
1363
1364fn validate_request(request: &TextRasterRequest) -> Result<(), TextRasterError> {
1365 if request.font_size.is_finite()
1366 && request.font_size > 0.0
1367 && request.font_size <= MAX_FONT_SIZE
1368 && request.text.len() <= MAX_TEXT_BYTES
1369 && request.max_width.is_finite()
1370 && request.max_width <= 16_384.0
1371 && request.line_height.is_finite()
1372 && request.line_height > 0.0
1373 && request.line_height <= MAX_LINE_HEIGHT
1374 && request.content_version > 0
1375 && request.outline.is_none_or(|outline| {
1376 outline.width.is_finite() && outline.width > 0.0 && outline.width <= MAX_OUTLINE_WIDTH
1377 })
1378 && request.background.is_none_or(|background| {
1379 background.padding.is_finite()
1380 && background.padding >= 0.0
1381 && background.padding <= MAX_BACKGROUND_PADDING
1382 })
1383 {
1384 Ok(())
1385 } else {
1386 Err(TextRasterError::InvalidRequest)
1387 }
1388}
1389
1390fn checked_dimension(value: f32) -> Result<u32, TextRasterError> {
1391 if value.is_finite() && value > 0.0 && value.ceil() <= 16_384.0 {
1392 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1393 Ok(value.ceil() as u32)
1394 } else {
1395 Err(TextRasterError::TooLarge)
1396 }
1397}
1398
1399fn place_glyph(
1403 mask: &mut [u8],
1404 width: u32,
1405 height: u32,
1406 origin_x: i64,
1407 origin_y: i64,
1408 glyph_width: usize,
1409 alpha: &[u8],
1410) {
1411 for (index, coverage) in alpha.iter().copied().enumerate() {
1412 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
1413 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
1414 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) {
1415 continue;
1416 }
1417 let pixel =
1418 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
1419 let slot = &mut mask[usize::try_from(pixel).unwrap_or(0)];
1420 *slot = (*slot).max(coverage);
1421 }
1422}
1423
1424fn place_color_glyph(
1427 colors: &mut [u8],
1428 width: u32,
1429 height: u32,
1430 origin_x: i64,
1431 origin_y: i64,
1432 glyph_width: usize,
1433 rgba8: &[u8],
1434) {
1435 for (index, source) in rgba8.as_chunks::<4>().0.iter().enumerate() {
1436 let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
1437 let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
1438 if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) || source[3] == 0 {
1439 continue;
1440 }
1441 let pixel =
1442 u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
1443 let slot = &mut colors[usize::try_from(pixel).unwrap_or(0) * 4..][..4];
1444 let blended = over(
1445 std::array::from_fn(|channel| u32::from(source[channel])),
1446 std::array::from_fn(|channel| u32::from(slot[channel])),
1447 );
1448 for (byte, value) in slot.iter_mut().zip(blended) {
1449 *byte = u8::try_from(value.min(255)).unwrap_or(u8::MAX);
1450 }
1451 }
1452}
1453
1454impl TextRasterRequest {
1455 #[must_use]
1460 pub fn decoration_margin(&self) -> u32 {
1461 let (outline, padding) = decoration_extents(self);
1462 outline.max(padding)
1463 }
1464}
1465
1466fn decoration_extents(request: &TextRasterRequest) -> (u32, u32) {
1469 let outline = request
1470 .outline
1471 .map_or(0, |outline| disc_extent(outline.width));
1472 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1473 let padding = request
1474 .background
1475 .map_or(0, |background| background.padding.ceil() as u32);
1476 (outline, padding)
1477}
1478
1479#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1481fn disc_extent(width: f32) -> u32 {
1482 (width + 0.5).ceil() as u32
1483}
1484
1485fn premultiplied(color: [u8; 4], coverage: u8) -> [u32; 4] {
1489 let alpha = u32::from(coverage) * u32::from(color[3]) / 255;
1490 [
1491 u32::from(color[0]) * alpha / 255,
1492 u32::from(color[1]) * alpha / 255,
1493 u32::from(color[2]) * alpha / 255,
1494 alpha,
1495 ]
1496}
1497
1498fn over(source: [u32; 4], destination: [u32; 4]) -> [u32; 4] {
1500 let keep = 255 - source[3];
1501 std::array::from_fn(|channel| source[channel] + (destination[channel] * keep + 127) / 255)
1502}
1503
1504#[allow(
1510 clippy::cast_possible_truncation,
1511 clippy::cast_sign_loss,
1512 clippy::cast_precision_loss
1513)]
1514fn compose(
1515 request: &TextRasterRequest,
1516 coverage: &[u8],
1517 colors: Option<&[u8]>,
1518 width: u32,
1519 margin: [u32; 2],
1520 line_boxes: &[(f32, f32, f32)],
1521) -> Vec<u8> {
1522 let outline = request.outline.map(|outline| {
1523 let ink = colors.map(|colors| {
1524 coverage
1525 .iter()
1526 .zip(colors.as_chunks::<4>().0)
1527 .map(|(&text, color)| text.max(color[3]))
1528 .collect::<Vec<_>>()
1529 });
1530 (
1531 outline.color,
1532 dilate(
1533 ink.as_deref().unwrap_or(coverage),
1534 width,
1535 u32::try_from(coverage.len() / width.max(1) as usize).unwrap_or(0),
1536 outline.width,
1537 ),
1538 )
1539 });
1540 let background = request.background.map(|background| {
1541 let [margin_x, margin_y] = margin.map(|margin| margin as f32);
1542 let padding = background.padding;
1543 let boxes = line_boxes
1544 .iter()
1545 .map(|&(left, line_width, top)| {
1546 (
1547 margin_x + left - padding,
1548 margin_x + left + line_width + padding,
1549 margin_y + top - padding,
1550 margin_y + top + request.font_size + padding,
1551 )
1552 })
1553 .collect::<Vec<_>>();
1554 (background.color, boxes)
1555 });
1556 let mut rgba8 = vec![0_u8; coverage.len() * 4];
1557 let (mut first, mut last) = (0, 0);
1560 for (index, text) in coverage.iter().copied().enumerate() {
1561 let mut pixel = [0_u32; 4];
1562 if let Some((color, boxes)) = &background {
1563 let x = (index % width as usize) as f32;
1564 let y = (index / width as usize) as f32;
1565 if index % width as usize == 0 {
1566 while first < boxes.len() && boxes[first].3 <= y {
1567 first += 1;
1568 }
1569 last = last.max(first);
1570 while last < boxes.len() && boxes[last].2 < y + 1.0 {
1571 last += 1;
1572 }
1573 }
1574 let covered = boxes[first..last]
1575 .iter()
1576 .map(|&(left, right, top, bottom)| {
1577 let across = (right.min(x + 1.0) - left.max(x)).clamp(0.0, 1.0);
1578 let down = (bottom.min(y + 1.0) - top.max(y)).clamp(0.0, 1.0);
1579 across * down
1580 })
1581 .fold(0.0_f32, f32::max);
1582 pixel = premultiplied(*color, (covered * 255.0).round() as u8);
1583 }
1584 if let Some((color, band)) = &outline {
1585 pixel = over(premultiplied(*color, band[index]), pixel);
1586 }
1587 pixel = over(premultiplied(request.color, text), pixel);
1588 if let Some(colors) = colors {
1589 let color = &colors[index * 4..][..4];
1590 if color[3] > 0 {
1591 let opacity = u32::from(request.color[3]);
1592 pixel = over(
1593 std::array::from_fn(|channel| u32::from(color[channel]) * opacity / 255),
1594 pixel,
1595 );
1596 }
1597 }
1598 for (channel, value) in pixel.iter().enumerate() {
1599 rgba8[index * 4 + channel] = (*value).min(255) as u8;
1600 }
1601 }
1602 rgba8
1603}
1604
1605#[allow(
1616 clippy::cast_possible_truncation,
1617 clippy::cast_sign_loss,
1618 clippy::cast_precision_loss,
1619 clippy::cast_possible_wrap
1620)]
1621fn dilate(coverage: &[u8], width: u32, height: u32, radius: f32) -> Vec<u8> {
1622 let (width, height) = (width as usize, height as usize);
1623 let mut band = vec![0_u8; coverage.len()];
1624 let Some(ink) = ink_bounds(coverage, width, height) else {
1625 return band;
1626 };
1627 let disc = Disc::new(radius);
1628 let extent = disc.extent;
1629 let reach = Bounds {
1630 left: ink.left.saturating_sub(extent),
1631 top: ink.top.saturating_sub(extent),
1632 right: (ink.right + extent).min(width),
1633 bottom: (ink.bottom + extent).min(height),
1634 };
1635 let span = reach.right - reach.left;
1638 let mut window = WindowMax::default();
1639 let mut maxima = vec![0_u8; span];
1640 for (half, offsets) in &disc.runs {
1641 for source_y in ink.top..ink.bottom {
1642 let row = &coverage[source_y * width..(source_y + 1) * width];
1643 window.of(row, reach.left, span, *half, &mut maxima);
1644 for &offset in offsets {
1645 let Some(y) = source_y.checked_add_signed(-offset) else {
1647 continue;
1648 };
1649 if y < reach.top || y >= reach.bottom {
1650 continue;
1651 }
1652 let out = &mut band[y * width + reach.left..y * width + reach.right];
1653 for (pixel, highest) in out.iter_mut().zip(&maxima) {
1654 *pixel = (*pixel).max(*highest);
1655 }
1656 }
1657 }
1658 }
1659 for &(dx, dy, weight) in &disc.rim {
1660 for source_y in ink.top..ink.bottom {
1661 let Some(y) = source_y.checked_add_signed(-dy) else {
1662 continue;
1663 };
1664 if y >= height {
1665 continue;
1666 }
1667 let first = ink.left.max(usize::try_from(dx).unwrap_or(0));
1670 let last = ink
1671 .right
1672 .min(usize::try_from(width as isize + dx.min(0)).unwrap_or(0));
1673 if first >= last {
1674 continue;
1675 }
1676 let source = &coverage[source_y * width + first..source_y * width + last];
1677 let start = y * width + (first as isize - dx) as usize;
1678 let out = &mut band[start..start + source.len()];
1679 for (pixel, value) in out.iter_mut().zip(source) {
1680 *pixel = (*pixel).max((u32::from(*value) * weight / 255) as u8);
1681 }
1682 }
1683 }
1684 band
1685}
1686
1687#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1689struct Bounds {
1690 left: usize,
1691 top: usize,
1692 right: usize,
1693 bottom: usize,
1694}
1695
1696fn ink_bounds(coverage: &[u8], width: usize, height: usize) -> Option<Bounds> {
1698 let mut bounds: Option<Bounds> = None;
1699 for y in 0..height {
1700 let row = &coverage[y * width..(y + 1) * width];
1701 let Some(left) = row.iter().position(|&value| value > 0) else {
1702 continue;
1703 };
1704 let right = row.iter().rposition(|&value| value > 0).unwrap_or(left) + 1;
1705 bounds = Some(bounds.map_or(
1706 Bounds {
1707 left,
1708 top: y,
1709 right,
1710 bottom: y + 1,
1711 },
1712 |bounds| Bounds {
1713 left: bounds.left.min(left),
1714 top: bounds.top,
1715 right: bounds.right.max(right),
1716 bottom: y + 1,
1717 },
1718 ));
1719 }
1720 bounds
1721}
1722
1723struct Disc {
1726 extent: usize,
1728 runs: Vec<(usize, Vec<isize>)>,
1731 rim: Vec<(isize, isize, u32)>,
1733}
1734
1735impl Disc {
1736 #[allow(
1739 clippy::cast_possible_truncation,
1740 clippy::cast_sign_loss,
1741 clippy::cast_precision_loss,
1742 clippy::cast_possible_wrap
1743 )]
1744 fn new(radius: f32) -> Self {
1745 let extent = disc_extent(radius) as isize;
1746 let mut runs: Vec<(usize, Vec<isize>)> = Vec::new();
1747 let mut rim = Vec::new();
1748 for dy in -extent..=extent {
1749 let mut half = None;
1750 for dx in -extent..=extent {
1751 let distance = ((dx * dx + dy * dy) as f32).sqrt();
1752 let weight = ((radius + 0.5 - distance).clamp(0.0, 1.0) * 255.0).round() as u32;
1753 match weight {
1754 0 => {}
1755 255 => half = Some(dx.unsigned_abs()),
1758 _ => rim.push((dx, dy, weight)),
1759 }
1760 }
1761 if let Some(half) = half {
1762 match runs.iter_mut().find(|(run, _)| *run == half) {
1763 Some((_, offsets)) => offsets.push(dy),
1764 None => runs.push((half, vec![dy])),
1765 }
1766 }
1767 }
1768 Self {
1769 extent: extent as usize,
1770 runs,
1771 rim,
1772 }
1773 }
1774}
1775
1776#[derive(Default)]
1779struct WindowMax {
1780 padded: Vec<u8>,
1781 from_start: Vec<u8>,
1782 to_end: Vec<u8>,
1783}
1784
1785impl WindowMax {
1786 fn of(&mut self, row: &[u8], left: usize, count: usize, half: usize, out: &mut [u8]) {
1789 let window = 2 * half + 1;
1790 let length = count + 2 * half;
1791 self.padded.clear();
1792 self.padded.resize(length, 0);
1793 let first_column = left.saturating_sub(half);
1795 let last_column = (left + count + half).min(row.len());
1796 if first_column < last_column {
1797 let at = first_column + half - left;
1798 self.padded[at..at + last_column - first_column]
1799 .copy_from_slice(&row[first_column..last_column]);
1800 }
1801 self.from_start.clear();
1802 self.from_start.resize(length, 0);
1803 self.to_end.clear();
1804 self.to_end.resize(length, 0);
1805 for block in (0..length).step_by(window) {
1806 let end = (block + window).min(length);
1807 let mut highest = 0;
1808 for index in block..end {
1809 highest = highest.max(self.padded[index]);
1810 self.from_start[index] = highest;
1811 }
1812 highest = 0;
1813 for index in (block..end).rev() {
1814 highest = highest.max(self.padded[index]);
1815 self.to_end[index] = highest;
1816 }
1817 }
1818 for (index, value) in out[..count].iter_mut().enumerate() {
1819 *value = self.to_end[index].max(self.from_start[index + window - 1]);
1820 }
1821 }
1822}
1823
1824#[cfg(test)]
1825mod tests;