Skip to main content

alpha2_text/
lib.rs

1//! Text shaping and rasterization for alpha2 hosts.
2//!
3//! The engine takes text as a host texture ([`TextTexture`]); this crate is
4//! one host for it, in pure Rust, from font bytes the caller supplies. Each
5//! paragraph is split by direction (UAX #9), script, and font, rustybuzz
6//! shapes each run, lines break where UAX #14 allows, and each glyph's
7//! outline is drawn by `ab_glyph_rasterizer` when it is placed, or its
8//! color form (a COLR painting or a CBDT or sbix bitmap) by `color`. It builds
9//! the same way natively and for wasm, and the same request with the same
10//! fonts gives the same pixels on every target, so a browser preview and a
11//! native export of one project draw identical text. `alpha2/text` exposes
12//! it to JavaScript as `WasmTextRasterizer`.
13
14use 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/// One caller-owned font registered in fallback order.
43#[derive(Clone, Debug)]
44pub struct TextFont {
45    /// Stable family name used in cache keys and diagnostics.
46    pub family: String,
47    /// OpenType font bytes.
48    pub bytes: Arc<[u8]>,
49    /// Face index for font collections.
50    pub face_index: u32,
51}
52
53/// One text layout request.
54#[derive(Clone, Debug, PartialEq)]
55pub struct TextRasterRequest {
56    /// UTF-8 text shaped by the selected font fallback stack.
57    pub text: String,
58    /// Logical font size in pixels.
59    pub font_size: f32,
60    /// Maximum layout width. Non-positive values disable wrapping.
61    pub max_width: f32,
62    /// Baseline-to-baseline distance in font-size multiples.
63    pub line_height: f32,
64    /// Per-line horizontal alignment.
65    pub align: TextAlign,
66    /// Straight (not premultiplied), sRGB-encoded RGBA color. The output is
67    /// premultiplied by glyph coverage and this alpha. A color glyph keeps
68    /// its own colors, but for the parts its font draws in the text's
69    /// color, and takes this alpha as its opacity.
70    pub color: [u8; 4],
71    /// Whether color glyphs are drawn in color, and which fonts emoji take.
72    pub emoji_policy: EmojiPolicy,
73    /// Caller-managed version. Change it whenever content/style/font inputs change.
74    pub content_version: u64,
75    /// A band around the glyphs, drawn behind them.
76    pub outline: Option<TextOutline>,
77    /// A box behind each line, drawn behind the outline.
78    pub background: Option<TextBackground>,
79    /// The family drawn first, as [`font_family`] reads it, matched without
80    /// regard to ASCII case; the rest of the stack fills in what it lacks.
81    /// `None`, or a family no font has, keeps the fallback order.
82    pub font_family: Option<String>,
83}
84
85/// A band of `width` pixels around every glyph, behind it, in a straight
86/// (not premultiplied), sRGB-encoded RGBA color.
87///
88/// The band is the glyph coverage dilated by a disc: each pixel takes the
89/// highest coverage within `width` of it, weighted down over the disc's last
90/// pixel so the band's edge is antialiased.
91#[derive(Copy, Clone, Debug, PartialEq)]
92pub struct TextOutline {
93    /// Straight, sRGB-encoded RGBA.
94    pub color: [u8; 4],
95    /// Radius of the band in pixels, in `(0, 64]`.
96    pub width: f32,
97}
98
99/// A box behind each non-empty line: the line's ink width and one font size
100/// tall, grown by `padding` pixels on every side. Boxes that meet merge.
101#[derive(Copy, Clone, Debug, PartialEq)]
102pub struct TextBackground {
103    /// Straight, sRGB-encoded RGBA.
104    pub color: [u8; 4],
105    /// Space between the text and the box edge, in `[0, 256]` pixels.
106    pub padding: f32,
107}
108
109/// Rasterized pixels and their text-specific layout descriptor.
110#[derive(Clone, Debug, PartialEq)]
111pub struct TextRasterResult {
112    /// Layout facts sharing the generic compositor texture contract.
113    pub descriptor: TextTexture,
114    /// Tightly packed premultiplied RGBA8 pixels.
115    pub pixels: HostTexturePixels,
116}
117
118/// Typed text-host failure.
119#[derive(Clone, Debug, Eq, PartialEq)]
120#[non_exhaustive]
121pub enum TextRasterError {
122    /// No usable fonts were registered.
123    NoFonts,
124    /// OpenType parsing failed.
125    InvalidFont(String),
126    /// Request values are non-finite or outside the supported range.
127    InvalidRequest,
128    /// Computed dimensions cannot be represented safely.
129    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    /// The family its name table gives, if any.
149    family: Option<String>,
150}
151
152impl LoadedFont {
153    /// Checks that the bytes hold the face and reads its family. Nothing
154    /// else is read until a request draws with it: a glyph's outline when
155    /// the glyph is placed.
156    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
163/// Checks that `bytes` hold an OpenType face at `face_index`, as adding the
164/// font to a rasterizer checks it, without keeping anything: a host can
165/// refuse a file at import that the rasterizer would refuse later.
166///
167/// # Errors
168/// Returns [`TextRasterError::InvalidFont`] with the parser's reason (a WOFF
169/// or WOFF2 container's unknown magic, say).
170pub 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/// The family a font's name table gives the face at `face_index`: its
177/// typographic family (name ID 16), else its family (name ID 1), in US
178/// English where the table has it, else in the first entry that decodes.
179/// `None` for bytes that do not parse or name no family.
180#[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
204/// The pixel bytes a [`TextRasterizer`] keeps cached by default: room for a
205/// few full-frame 4K titles, or hundreds of caption-sized ones.
206pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
207
208/// What a cached result costs beyond its pixels, so that results with few or
209/// no pixels still count toward the budget and their number stays bounded.
210const CACHE_ENTRY_OVERHEAD: usize = 256;
211
212/// Deterministic text host with version-key result caching.
213///
214/// Results are cached by content version, least recently used first out,
215/// within a byte budget ([`DEFAULT_TEXT_CACHE_BYTES`] unless
216/// [`Self::set_cache_budget`] changes it). A cached result is shared, not
217/// copied: a request whose version is cached returns the same [`Arc`].
218#[derive(Debug)]
219pub struct TextRasterizer {
220    fonts: Vec<LoadedFont>,
221    cache: ResultCache,
222}
223
224/// Rasterized results by content version, within a byte budget, evicting the
225/// least recently used.
226#[derive(Debug)]
227struct ResultCache {
228    entries: HashMap<u64, CacheEntry>,
229    /// Content versions by last use, oldest first.
230    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    /// Keeps `result` under `version` if it fits the budget, evicting the
264    /// least recently used results to make room.
265    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    /// A rasterizer with no fonts yet; add them with [`Self::add_font`].
321    #[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    /// Takes a fallback-ordered OpenType stack.
330    ///
331    /// # Errors
332    /// Returns [`TextRasterError::NoFonts`] or the first malformed font.
333    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    /// Appends a font to the end of the fallback stack.
348    ///
349    /// Fonts change what every request draws, so the result cache is
350    /// cleared.
351    ///
352    /// # Errors
353    /// Returns [`TextRasterError::InvalidFont`] for malformed font bytes.
354    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    /// Number of fonts in the fallback stack.
361    #[must_use]
362    pub fn font_count(&self) -> usize {
363        self.fonts.len()
364    }
365
366    /// Shapes, lays out, and rasterizes one request, or returns the cached
367    /// result of its exact version, shared rather than copied.
368    ///
369    /// # Errors
370    /// Returns a typed validation, font, or size error.
371    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    /// Drops a single host version after content or font invalidation.
388    pub fn invalidate(&mut self, content_version: u64) {
389        self.cache.remove(content_version);
390    }
391
392    /// Sets how many bytes of results the cache keeps, evicting the least
393    /// recently used down to it. Each result counts its pixels and a small
394    /// fixed overhead; a result larger than the budget is returned but not
395    /// kept. Zero keeps nothing.
396    pub fn set_cache_budget(&mut self, bytes: usize) {
397        self.cache.set_budget(bytes);
398    }
399
400    /// Bytes the cached results count against the budget.
401    #[must_use]
402    pub fn cache_bytes(&self) -> usize {
403        self.cache.bytes
404    }
405
406    /// Drops every rasterized result while retaining parsed fonts.
407    pub fn clear(&mut self) {
408        self.cache.clear();
409    }
410
411    /// Number of versioned raster results currently retained.
412    #[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        // Lines up to the last with glyphs stay, however little ink they have; blank lines after it
433        // take no room.
434        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        // Every line's nominal em box fits, the last one's too when lines are closer than an em.
459        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        // Where each line starts in the box: Start and End are the paragraph's own sides.
464        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        // Ink past the box on each axis, and the ink's own reach above and below its baselines.
472        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        // The texture is the box plus a margin, equal on both ends of an axis so the box stays
499        // centred, with room for decorations and for outlined ink past the box.
500        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        // Color glyphs, premultiplied RGBA, only once one is placed.
530        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    /// Font indices in the order a request tries them: the fonts of
596    /// `family` first, then the others, each group in fallback order.
597    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
612/// The stack's faces for one request, each parsed once; a face's shaping
613/// tables only when a run shapes with it.
614struct Faces<'a> {
615    parsed: Vec<ttf_parser::Face<'a>>,
616    shapers: Vec<OnceCell<rustybuzz::Face<'a>>>,
617    /// Whether each face has color glyphs (a COLR, CBDT, or sbix table).
618    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    /// Whether the font maps `character` to a glyph of its own.
655    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    /// Whether the font has every character of `grapheme` that is drawn at all.
662    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
670/// The text as layout reads it: under [`EmojiPolicy::Replace`] each emoji
671/// grapheme one replacement character, every line or paragraph separator a
672/// `\n`, and a tab a space (a caption has no tab stops).
673///
674/// U+2028 LINE SEPARATOR, which UAX #9 does not count as a paragraph end,
675/// ends one here too, so every line break is one path.
676fn 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
700/// A grapheme shown as an emoji (UTS #51), as `OffscreenTextRasterizer`
701/// reads one too: unless U+FE0E asks for text, one holding an
702/// `Emoji_Presentation` character (regional indicators and skin tones
703/// included), or U+FE0F with a pictograph or a keycap. A symbol shown as
704/// text by default, such as (c) or a sun, is not one: a text font draws it.
705fn 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
737/// A space a line may end in: it takes no width there, hanging past the
738/// end. Not the no-break spaces, which hold their neighbours together, nor
739/// the Ogham space mark, which is drawn.
740fn 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
756/// One line of a paragraph, laid out.
757struct LaidLine {
758    /// Glyphs left to right, pens from the line's start.
759    glyphs: Vec<PositionedGlyph>,
760    /// Width of what it draws: its trailing spaces hang past it.
761    width: f32,
762    /// The paragraph runs right to left.
763    rtl: bool,
764    /// It has glyphs, even if none with ink.
765    has_glyphs: bool,
766}
767
768/// Lays out one paragraph: shaped run by run in logical order, broken into
769/// lines no wider than `max_width` (none when it is not positive), and each
770/// line put in visual order.
771fn 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            // Trailing spaces hang past the end: no width, nothing drawn.
788            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
827/// The glyphs of `range`, in logical order, in visual order (UAX #9 L2):
828/// from the highest level down to the lowest odd one, every run at that
829/// level or above reversed. A right-to-left run comes back to the order its
830/// shaper gave.
831fn 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/// One stretch of a paragraph shaped as a unit: one direction, one
858/// script, one font.
859#[derive(Clone, Debug, PartialEq)]
860struct Run {
861    range: Range<usize>,
862    font: usize,
863    script: Option<Script>,
864    rtl: bool,
865}
866
867/// Splits a paragraph into runs. Each grapheme takes the first font in
868/// `order` with all it draws; one of only common characters (spaces, digits,
869/// punctuation) stays in the font before it when that has them, and one no
870/// font has takes the font of its base character, else the font before it.
871/// A grapheme shown as an emoji tries the color fonts first under
872/// [`EmojiPolicy::Color`] and the others first otherwise, as one asking for
873/// text (U+FE0E) does, and stays only in a font of the kind it tries first
874/// when the stack has one.
875/// A grapheme of common characters takes the script before it, or after it
876/// at the paragraph's start.
877fn 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    // Whether a font is of the kind a grapheme wants, or the stack has none of that kind.
890    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        // Whether the grapheme wants a color font, when it wants one kind.
906        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
969/// The rustybuzz script of a Unicode one, by its ISO 15924 code.
970fn 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
975/// Shapes a paragraph run by run, each in its own direction and script
976/// with the text around it as context, into glyphs in logical order.
977fn 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(&paragraph[run.range.clone()]);
991        buffer.set_pre_context(&paragraph[..run.range.start]);
992        buffer.set_post_context(&paragraph[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                // A font's own substitutions (an AAT `morx` table) may name a glyph it does not
1009                // have; that draws as `.notdef`.
1010                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        // The shaper gives a right-to-left run in visual order; the paragraph is broken in logical
1021        // order.
1022        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    /// Byte offset in the paragraph of the text it draws.
1034    cluster: usize,
1035    advance: f32,
1036    /// Where the shaper places the glyph off its pen position, in pixels, y
1037    /// up: a mark on its base's anchor, for one.
1038    x_offset: f32,
1039    y_offset: f32,
1040}
1041
1042/// The glyphs of one cluster: what a line never splits.
1043#[derive(Clone, Debug, PartialEq)]
1044struct Cluster {
1045    glyphs: Range<usize>,
1046    /// Byte offset in the paragraph of its text.
1047    start: usize,
1048    width: f32,
1049    /// All it draws is spaces that hang at a line's end.
1050    hangs: bool,
1051    /// UAX #14 lets a line break before it.
1052    break_before: bool,
1053}
1054
1055/// Groups logical-order glyphs into clusters, with where UAX #14 lets a
1056/// line break: except between two Hangul characters, since Korean breaks
1057/// between words (the tailoring UAX #14 gives for it).
1058fn 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
1109/// Splits a paragraph's clusters into lines no wider than `max_width` (none
1110/// when it is not positive), as cluster ranges.
1111///
1112/// A line breaks at its last opportunity that keeps it within the width;
1113/// what is still wider breaks between clusters. Spaces never push a line
1114/// over: they hang past its end.
1115fn 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    // The latest cluster a line may start at, and the pen position there.
1120    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            // What moved down with this cluster is wider than a line: it breaks again before the
1132            // cluster.
1133            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    /// Pen position from the line's start.
1150    x: f32,
1151    /// The shaper's placement of the glyph off its pen position, in pixels, y up.
1152    x_offset: f32,
1153    y_offset: f32,
1154}
1155
1156/// How a glyph is drawn: its outline, as coverage the text's color fills,
1157/// or under [`EmojiPolicy::Color`] its color form when the font has one.
1158enum Drawing {
1159    Outline(Outline),
1160    Painting(color::Painting),
1161    Bitmap(color::Bitmap),
1162}
1163
1164impl Drawing {
1165    /// The glyph's COLR painting, else its bitmap, else its outline. The
1166    /// painting takes the text's color, opaque, where it asks for it: the
1167    /// text's alpha applies to the whole glyph when it is composed.
1168    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    /// `[x_min, y_min, x_max, y_max]` in font units of all it may draw;
1183    /// `None` when it draws nothing.
1184    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    /// A color glyph over `pixels`, premultiplied RGBA8; an outline draws
1193    /// nothing here.
1194    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/// A glyph's outline in font units, y up, as the font draws it.
1204#[derive(Default)]
1205struct Outline {
1206    segments: Vec<Segment>,
1207    /// `[x_min, y_min, x_max, y_max]` of every point, on the curves and off
1208    /// them, so holding the curves; `None` when it draws nothing.
1209    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/// The pixels a glyph's outline reaches, right and bottom exclusive, in
1219/// the layout box, y down.
1220#[derive(Copy, Clone, Debug)]
1221struct PixelBox {
1222    left: i64,
1223    top: i64,
1224    right: i64,
1225    bottom: i64,
1226}
1227
1228impl PixelBox {
1229    /// Where outline `bounds` land with the glyph's origin at `origin`
1230    /// (layout pixels, y down) at `scale` pixels a font unit.
1231    #[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        // Each edge the same expression `Outline::draw` maps a point by, so every point lands
1235        // inside.
1236        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    /// How many glyph outlines this thread has read from a face.
1248    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    /// The glyph's coverage over `pixels`, row by row, with its origin at
1262    /// `origin` at `scale`.
1263    #[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    /// The outline drawn into a `width` by `height` raster, each point where
1285    /// `at` puts it.
1286    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/// Collects an outline from ttf-parser, closing every contour.
1302#[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        // Exactly where it began: a contour that came back by its own last point needs no closing
1357        // line.
1358        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
1399/// Adds a glyph's coverage to the mask: where glyphs overlap, each pixel
1400/// keeps the most ink either gives it, so a glyph's empty corners never
1401/// erase its neighbour.
1402fn 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
1424/// Adds a color glyph to the color layer, premultiplied RGBA8: where
1425/// glyphs overlap, the later one goes over.
1426fn 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    /// Pixels the decorations reach past the layout box on every side when
1456    /// no ink does: the outline's disc and the background's padding, whole.
1457    /// The texture's margin ([`TextTexture::layout_x`]) is this plus room
1458    /// for ink past the box.
1459    #[must_use]
1460    pub fn decoration_margin(&self) -> u32 {
1461        let (outline, padding) = decoration_extents(self);
1462        outline.max(padding)
1463    }
1464}
1465
1466/// Pixels each decoration reaches past what it surrounds, whole: the
1467/// outline's disc past the ink, the background's padding past the box.
1468fn 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/// Integer radius of the disc an outline of `width` dilates by.
1480#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1481fn disc_extent(width: f32) -> u32 {
1482    (width + 0.5).ceil() as u32
1483}
1484
1485/// `color` at `coverage`, premultiplied: `(c * alpha, alpha)` with
1486/// `alpha = coverage * a`, each product truncated to a byte. The pinned
1487/// rasters depend on this truncation.
1488fn 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
1498/// `source` over `destination`, premultiplied, rounded to the nearest byte.
1499fn 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/// The text over its outline over its line boxes, premultiplied RGBA8, all
1505/// in integers so every target gives the same bytes. Undecorated text is its
1506/// coverage in its color alone, with the color glyphs (`colors`,
1507/// premultiplied RGBA8) over it at the color's alpha. The outline surrounds
1508/// both: its band dilates the coverage and the color glyphs' alpha.
1509#[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    // The boxes that may meet the current row, `first..last`; boxes come in line order, so the
1558    // window only moves down.
1559    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/// The coverage dilated by a disc of radius `width`: each pixel takes the
1606/// highest coverage within the disc, weighted by how much of the disc's edge
1607/// pixel lies inside it.
1608///
1609/// The work follows the ink, not the texture: only the ink's bounds grown by
1610/// the disc can be reached. The disc's fully weighted pixels are, row by row,
1611/// one run centred on the column, so they are a sliding-window maximum
1612/// (van Herk/Gil-Werman) of a coverage row; only the antialiased rim is
1613/// visited tap by tap. Every pixel is the same integer maximum a tap-by-tap
1614/// pass computes, so the band is byte for byte the same.
1615#[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    // Each run half-width once per coverage row, laid onto every output row whose disc has that run
1636    // there.
1637    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                // The tap `offset` rows below an output row reads this row.
1646                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            // Output columns x read x + dx; keep both within the texture and the source within the
1668            // ink.
1669            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/// A rectangle of pixels, right and bottom exclusive.
1688#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1689struct Bounds {
1690    left: usize,
1691    top: usize,
1692    right: usize,
1693    bottom: usize,
1694}
1695
1696/// The smallest rectangle holding every pixel with coverage, if any has.
1697fn 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
1723/// The taps of a disc of some radius, split into what a dilation reads as
1724/// runs and what it reads tap by tap.
1725struct Disc {
1726    /// Integer radius of the square holding the disc.
1727    extent: usize,
1728    /// Run half-widths with the row offsets whose fully weighted taps are
1729    /// exactly the columns `-half..=half`.
1730    runs: Vec<(usize, Vec<isize>)>,
1731    /// `(dx, dy, weight)` of each tap weighted below 255 and above 0.
1732    rim: Vec<(isize, isize, u32)>,
1733}
1734
1735impl Disc {
1736    /// The disc of `radius`: a tap's weight is how much of it lies within
1737    /// `radius + 0.5` of the centre, in 255ths, rounded.
1738    #[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                    // Weight falls with distance, so a row's full taps are one run around its
1756                    // centre.
1757                    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/// Scratch for sliding-window maxima of a row (van Herk/Gil-Werman): the
1777/// maxima from each block's start and to each block's end.
1778#[derive(Default)]
1779struct WindowMax {
1780    padded: Vec<u8>,
1781    from_start: Vec<u8>,
1782    to_end: Vec<u8>,
1783}
1784
1785impl WindowMax {
1786    /// Into `out[i]`, the highest value of `row` over columns
1787    /// `left + i - half ..= left + i + half`, reading 0 off the row.
1788    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        // Padded index i is column left - half + i.
1794        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;