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/// The letter spacing a request takes, in font sizes.
42const LETTER_SPACING_RANGE: std::ops::RangeInclusive<f32> = -0.5..=1.0;
43
44/// One caller-owned font registered in fallback order.
45#[derive(Clone, Debug)]
46pub struct TextFont {
47    /// Stable family name used in cache keys and diagnostics.
48    pub family: String,
49    /// OpenType font bytes.
50    pub bytes: Arc<[u8]>,
51    /// Face index for font collections.
52    pub face_index: u32,
53}
54
55/// One text layout request.
56#[derive(Clone, Debug, PartialEq)]
57pub struct TextRasterRequest {
58    /// UTF-8 text shaped by the selected font fallback stack.
59    pub text: String,
60    /// Logical font size in pixels.
61    pub font_size: f32,
62    /// Maximum layout width. Non-positive values disable wrapping.
63    pub max_width: f32,
64    /// Baseline-to-baseline distance in font sizes. Each line's em box,
65    /// 0.8 of a font size above its baseline and 0.2 below, is centred in
66    /// its line when the line is taller than an em.
67    pub line_height: f32,
68    /// Per-line horizontal alignment.
69    pub align: TextAlign,
70    /// Straight (not premultiplied), sRGB-encoded RGBA color. The output is
71    /// premultiplied by glyph coverage and this alpha. A color glyph keeps
72    /// its own colors, but for the parts its font draws in the text's
73    /// color, and takes this alpha as its opacity.
74    pub color: [u8; 4],
75    /// Whether color glyphs are drawn in color, and which fonts emoji take.
76    pub emoji_policy: EmojiPolicy,
77    /// Caller-managed version. Change it whenever content/style/font inputs change.
78    pub content_version: u64,
79    /// A band around the glyphs, drawn behind them.
80    pub outline: Option<TextOutline>,
81    /// A box behind each line, drawn behind the outline.
82    pub background: Option<TextBackground>,
83    /// The family drawn first, as [`font_family`] reads it, matched without
84    /// regard to ASCII case; the rest of the stack fills in what it lacks.
85    /// `None`, or a family no font has, keeps the fallback order.
86    pub font_family: Option<String>,
87    /// Space added after each typographic character unit (grapheme
88    /// cluster), in pixels, as CSS `letter-spacing` adds it: none after a
89    /// line's last, and none after a character of a cursive script (Arabic,
90    /// say), whose joins it would break. Not zero, it turns optional
91    /// ligatures off. In `[-0.5, 1]` font sizes.
92    pub letter_spacing: f32,
93    /// The weight, in `[1, 1000]`, a family's faces are matched by (CSS
94    /// Fonts 4 weight matching), and a variable face's `wght` is set to,
95    /// clamped to its axis. No face is emboldened: a weight a family lacks
96    /// draws its nearest face.
97    pub font_weight: u16,
98}
99
100/// A band of `width` pixels around every glyph, behind it, in a straight
101/// (not premultiplied), sRGB-encoded RGBA color.
102///
103/// The band is the glyph coverage dilated by a disc: each pixel takes the
104/// highest coverage within `width` of it, weighted down over the disc's last
105/// pixel so the band's edge is antialiased.
106#[derive(Copy, Clone, Debug, PartialEq)]
107pub struct TextOutline {
108    /// Straight, sRGB-encoded RGBA.
109    pub color: [u8; 4],
110    /// Radius of the band in pixels, in `(0, 64]`.
111    pub width: f32,
112}
113
114/// A box behind each non-empty line: the line's em box, as wide as the
115/// line's advance and one font size tall, grown by `padding` pixels on
116/// every side. Boxes that meet merge.
117#[derive(Copy, Clone, Debug, PartialEq)]
118pub struct TextBackground {
119    /// Straight, sRGB-encoded RGBA.
120    pub color: [u8; 4],
121    /// Space between the text and the box edge, in `[0, 256]` pixels.
122    pub padding: f32,
123}
124
125/// Rasterized pixels and their text-specific layout descriptor.
126#[derive(Clone, Debug, PartialEq)]
127pub struct TextRasterResult {
128    /// Layout facts sharing the generic compositor texture contract.
129    pub descriptor: TextTexture,
130    /// Tightly packed premultiplied RGBA8 pixels.
131    pub pixels: HostTexturePixels,
132}
133
134/// Where a request's text lies, without drawing it
135/// ([`TextRasterizer::measure`]): rectangles as `[left, top, right, bottom]`
136/// in pixels from the layout box's top-left corner, y down. The texture
137/// [`TextRasterizer::rasterize`] draws for the request holds that corner at
138/// ([`TextTexture::layout_x`], [`TextTexture::layout_y`]).
139#[derive(Clone, Debug, PartialEq)]
140pub struct TextMeasure {
141    /// The layout box's width in whole pixels: the width the request wraps
142    /// at, else its widest line, rounded up.
143    pub box_width: u32,
144    /// The layout box's height in whole pixels: every line's em box, rounded
145    /// up.
146    pub box_height: u32,
147    /// [`TextRasterRequest::decoration_margin`].
148    pub decoration_margin: u32,
149    /// From the start of the line that starts farthest left to the end of
150    /// the one that ends farthest right, the box's height, grown by the
151    /// decoration margin as the box is. It moves only as the lines' advances
152    /// do, not as glyphs with more or less ink are typed.
153    pub content: [f32; 4],
154    /// What the text draws: each glyph's outline (its curves, not their
155    /// control points) or color form, the outline's band around them, and
156    /// the background's boxes. Every pixel the raster gives any alpha meets
157    /// it, and it reaches at most a pixel past them, or past a COLR glyph's
158    /// outlines and clip boxes, which may hold a little more than it paints.
159    /// `None` when the text draws nothing.
160    pub ink: Option<[f32; 4]>,
161    /// Number of laid-out lines.
162    pub line_count: u32,
163    /// The first line's alphabetic baseline, in pixels from the layout box's
164    /// top.
165    pub first_baseline: f32,
166    /// The last line's alphabetic baseline, in pixels from the layout box's
167    /// top.
168    pub last_baseline: f32,
169}
170
171/// A request's text shaped once, for layout: its lines at any wrapping
172/// width, broken as [`TextRasterizer::rasterize`] breaks them, without
173/// shaping again ([`TextRasterizer::shape`]).
174#[derive(Clone, Debug)]
175pub struct ShapedText {
176    /// Each paragraph's clusters, in logical order.
177    paragraphs: Vec<Vec<Cluster>>,
178    font_size: f32,
179    line_height: f32,
180    line_advance: f32,
181}
182
183/// Lines of a shaped text at one wrapping width ([`ShapedText::lines_at`]).
184#[derive(Copy, Clone, Debug, PartialEq)]
185pub struct LineMetrics {
186    /// Number of lines, as [`TextMeasure::line_count`] counts them.
187    pub line_count: u32,
188    /// The widest line's advance, trailing spaces left out.
189    pub widest: f32,
190    /// Every line's em box, unrounded: the rasterizer's box height before it
191    /// is rounded up.
192    pub height: f32,
193    /// The first line's alphabetic baseline, in pixels from the box's top, as
194    /// [`TextMeasure::first_baseline`].
195    pub first_baseline: f32,
196    /// The last line's alphabetic baseline, in pixels from the box's top.
197    pub last_baseline: f32,
198}
199
200impl ShapedText {
201    /// The widest run between line-break opportunities, trailing spaces
202    /// left out (CSS min-content).
203    #[must_use]
204    pub fn min_content_width(&self) -> f32 {
205        let mut widest = 0.0_f32;
206        for clusters in &self.paragraphs {
207            let mut start = 0;
208            for index in 1..=clusters.len() {
209                if index == clusters.len() || clusters[index].break_before {
210                    widest = widest.max(advance(&clusters[start..index]));
211                    start = index;
212                }
213            }
214        }
215        widest
216    }
217
218    /// The widest paragraph unwrapped, trailing spaces left out (CSS
219    /// max-content).
220    #[must_use]
221    pub fn max_content_width(&self) -> f32 {
222        self.paragraphs
223            .iter()
224            .map(|clusters| advance(clusters))
225            .fold(0.0, f32::max)
226    }
227
228    /// The lines at `max_width`, as [`TextRasterizer::rasterize`] breaks
229    /// them; a non-positive width does not wrap.
230    #[must_use]
231    pub fn lines_at(&self, max_width: f32) -> LineMetrics {
232        let (mut count, mut kept, mut widest) = (0_u32, 1_u32, 0.0_f32);
233        for clusters in &self.paragraphs {
234            for range in break_lines(clusters, max_width) {
235                count = count.saturating_add(1);
236                if !range.is_empty() {
237                    kept = count;
238                }
239                widest = widest.max(advance(&clusters[range]));
240            }
241        }
242        let [first_baseline, last_baseline] =
243            baselines(kept, self.font_size, self.line_height, self.line_advance);
244        LineMetrics {
245            line_count: kept,
246            widest,
247            height: box_height(kept, self.line_advance, self.font_size),
248            first_baseline,
249            last_baseline,
250        }
251    }
252}
253
254/// The pen advance over `clusters` in logical order, as [`break_lines`]
255/// sums it, up to the last that does not hang, without the last's letter
256/// spacing: a line ends at its last character, not past its spacing.
257fn advance(clusters: &[Cluster]) -> f32 {
258    let Some(last) = clusters.iter().rposition(|cluster| !cluster.hangs) else {
259        return 0.0;
260    };
261    clusters[..=last]
262        .iter()
263        .fold(0.0, |width, cluster| width + cluster.width)
264        - clusters[last].spacing
265}
266
267/// Space above a line's em box in its line: half the leading when lines are
268/// farther apart than an em (CSS half-leading), else none, so the box still
269/// holds every em box.
270#[must_use]
271pub fn half_leading(font_size: f32, line_height: f32) -> f32 {
272    (font_size * line_height - font_size).max(0.0) * 0.5
273}
274
275/// The first and last of `line_count` lines' baselines, from the box's top:
276/// each line's em box top plus its nominal ascent, 0.8 of a font size, summed
277/// as [`Layout`] places them.
278#[allow(clippy::cast_precision_loss)]
279fn baselines(line_count: u32, font_size: f32, line_height: f32, line_advance: f32) -> [f32; 2] {
280    let (leading, ascent) = (half_leading(font_size, line_height), font_size * 0.8);
281    let last = line_count.saturating_sub(1) as f32;
282    [leading + ascent, last * line_advance + leading + ascent]
283}
284
285/// Every line's nominal em box: the last one's too when lines are closer
286/// than an em.
287#[allow(clippy::cast_precision_loss)]
288fn box_height(line_count: u32, line_advance: f32, font_size: f32) -> f32 {
289    line_count.saturating_sub(1) as f32 * line_advance + line_advance.max(font_size)
290}
291
292/// Typed text-host failure.
293#[derive(Clone, Debug, Eq, PartialEq)]
294#[non_exhaustive]
295pub enum TextRasterError {
296    /// No usable fonts were registered.
297    NoFonts,
298    /// OpenType parsing failed.
299    InvalidFont(String),
300    /// Request values are non-finite or outside the supported range.
301    InvalidRequest,
302    /// Computed dimensions cannot be represented safely.
303    TooLarge,
304}
305
306impl fmt::Display for TextRasterError {
307    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
308        match self {
309            Self::NoFonts => formatter.write_str("text rasterizer requires at least one font"),
310            Self::InvalidFont(error) => write!(formatter, "invalid text font: {error}"),
311            Self::InvalidRequest => formatter.write_str("invalid text raster request"),
312            Self::TooLarge => formatter.write_str("text raster result is too large"),
313        }
314    }
315}
316
317impl std::error::Error for TextRasterError {}
318
319#[derive(Clone, Debug)]
320struct LoadedFont {
321    source: TextFont,
322    /// The family its name table gives, if any.
323    family: Option<String>,
324    /// OS/2 usWeightClass in [1, 1000]; 400 without the table.
325    weight: u16,
326    /// The fvar `wght` axis (min, default, max), for a variable face.
327    wght: Option<(f32, f32, f32)>,
328}
329
330const WGHT: ttf_parser::Tag = ttf_parser::Tag::from_bytes(b"wght");
331
332impl LoadedFont {
333    /// Checks that the bytes hold the face and reads its family and weight.
334    /// Nothing else is read until a request draws with it: a glyph's outline
335    /// when the glyph is placed.
336    fn parse(source: TextFont) -> Result<Self, TextRasterError> {
337        let face = ttf_parser::Face::parse(&source.bytes, source.face_index)
338            .map_err(|error| TextRasterError::InvalidFont(error.to_string()))?;
339        let weight = face.weight().to_number().clamp(1, 1000);
340        let wght = face
341            .variation_axes()
342            .into_iter()
343            .find(|axis| axis.tag == WGHT)
344            .map(|axis| (axis.min_value, axis.def_value, axis.max_value));
345        let family = font_family(&source.bytes, source.face_index);
346        Ok(Self {
347            source,
348            family,
349            weight,
350            wght,
351        })
352    }
353
354    /// The weights it draws, `[min, max]`: its axis's, rounded inward to
355    /// whole weights, for a variable face; its weight for a static one.
356    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
357    fn weights(&self) -> (u16, u16) {
358        self.wght
359            .map_or((self.weight, self.weight), |(min, _, max)| {
360                let whole = |value: f32| value.clamp(1.0, 1000.0) as u16;
361                (whole(min.ceil()), whole(max.floor()).max(whole(min.ceil())))
362            })
363    }
364}
365
366/// The faces of one family in the order a request for `desired` tries them,
367/// as CSS Fonts 4 (section 5.2) matches weights, ties in the order given.
368/// `weights` holds each face's range, `[w, w]` for a static face: one that
369/// holds `desired` matches exactly; otherwise its nearest end is its weight.
370/// From 400 to 500 the faces up to 500 come first, nearest first, then those
371/// below, nearest first, then those above 500; below 400 those below, then
372/// those above; above 500 those above, then those below.
373fn weight_order(weights: &[(u16, u16)], desired: u16) -> Vec<usize> {
374    let key = |&(min, max): &(u16, u16)| -> (u8, u16) {
375        if (min..=max).contains(&desired) {
376            return (0, 0);
377        }
378        let weight = if desired < min { min } else { max };
379        let (above, distance) = (weight > desired, weight.abs_diff(desired));
380        if (400..=500).contains(&desired) {
381            match (above, weight <= 500) {
382                (true, true) => (1, distance),
383                (false, _) => (2, distance),
384                (true, false) => (3, distance),
385            }
386        } else if (desired < 400) != above {
387            (1, distance)
388        } else {
389            (2, distance)
390        }
391    };
392    let mut order = (0..weights.len()).collect::<Vec<_>>();
393    order.sort_by_key(|&index| key(&weights[index]));
394    order
395}
396
397/// Checks that `bytes` hold an OpenType face at `face_index`, as adding the
398/// font to a rasterizer checks it, without keeping anything: a host can
399/// refuse a file at import that the rasterizer would refuse later.
400///
401/// # Errors
402/// Returns [`TextRasterError::InvalidFont`] with the parser's reason (a WOFF
403/// or WOFF2 container's unknown magic, say).
404pub fn check_font(bytes: &[u8], face_index: u32) -> Result<(), TextRasterError> {
405    ttf_parser::Face::parse(bytes, face_index)
406        .map(|_| ())
407        .map_err(|error| TextRasterError::InvalidFont(error.to_string()))
408}
409
410/// How many faces `bytes` hold: a font collection's (`.ttc`, `.otc`) count,
411/// else 1. A host registers each of them to draw every weight a collection
412/// holds.
413#[must_use]
414pub fn face_count(bytes: &[u8]) -> u32 {
415    ttf_parser::fonts_in_collection(bytes).unwrap_or(1)
416}
417
418/// The family a font's name table gives the face at `face_index`: its
419/// typographic family (name ID 16), else its family (name ID 1), in US
420/// English where the table has it, else in the first entry that decodes.
421/// `None` for bytes that do not parse or name no family.
422#[must_use]
423pub fn font_family(bytes: &[u8], face_index: u32) -> Option<String> {
424    use rustybuzz::ttf_parser::{Language, name_id};
425    let face = rustybuzz::ttf_parser::Face::parse(bytes, face_index).ok()?;
426    let names = face.names();
427    [name_id::TYPOGRAPHIC_FAMILY, name_id::FAMILY]
428        .into_iter()
429        .find_map(|id| {
430            let decoded = || {
431                names
432                    .into_iter()
433                    .filter(move |name| name.name_id == id)
434                    .filter_map(|name| {
435                        let text = name.to_string()?;
436                        (!text.trim().is_empty()).then_some((name.language(), text))
437                    })
438            };
439            decoded()
440                .find(|(language, _)| *language == Language::English_UnitedStates)
441                .or_else(|| decoded().next())
442                .map(|(_, text)| text)
443        })
444}
445
446/// The pixel bytes a [`TextRasterizer`] keeps cached by default: room for a
447/// few full-frame 4K titles, or hundreds of caption-sized ones.
448pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
449
450/// What a cached result costs beyond its pixels, so that results with few or
451/// no pixels still count toward the budget and their number stays bounded.
452const CACHE_ENTRY_OVERHEAD: usize = 256;
453
454/// Deterministic text host with version-key result caching.
455///
456/// Results are cached by content version, least recently used first out,
457/// within a byte budget ([`DEFAULT_TEXT_CACHE_BYTES`] unless
458/// [`Self::set_cache_budget`] changes it). A cached result is shared, not
459/// copied: a request whose version is cached returns the same [`Arc`].
460#[derive(Debug)]
461pub struct TextRasterizer {
462    fonts: Vec<LoadedFont>,
463    cache: ResultCache,
464}
465
466/// Rasterized results by content version, within a byte budget, evicting the
467/// least recently used.
468#[derive(Debug)]
469struct ResultCache {
470    entries: HashMap<u64, CacheEntry>,
471    /// Content versions by last use, oldest first.
472    by_use: BTreeMap<u64, u64>,
473    next_use: u64,
474    bytes: usize,
475    budget: usize,
476}
477
478#[derive(Debug)]
479struct CacheEntry {
480    result: Arc<TextRasterResult>,
481    last_use: u64,
482    bytes: usize,
483}
484
485impl ResultCache {
486    fn new(budget: usize) -> Self {
487        Self {
488            entries: HashMap::new(),
489            by_use: BTreeMap::new(),
490            next_use: 0,
491            bytes: 0,
492            budget,
493        }
494    }
495
496    fn get(&mut self, version: u64) -> Option<Arc<TextRasterResult>> {
497        let entry = self.entries.get_mut(&version)?;
498        self.by_use.remove(&entry.last_use);
499        entry.last_use = self.next_use;
500        self.by_use.insert(self.next_use, version);
501        self.next_use += 1;
502        Some(Arc::clone(&entry.result))
503    }
504
505    /// Keeps `result` under `version` if it fits the budget, evicting the
506    /// least recently used results to make room.
507    fn insert(&mut self, version: u64, result: &Arc<TextRasterResult>) {
508        self.remove(version);
509        let bytes = result
510            .pixels
511            .rgba8
512            .len()
513            .saturating_add(CACHE_ENTRY_OVERHEAD);
514        if bytes > self.budget {
515            return;
516        }
517        self.bytes += bytes;
518        self.entries.insert(
519            version,
520            CacheEntry {
521                result: Arc::clone(result),
522                last_use: self.next_use,
523                bytes,
524            },
525        );
526        self.by_use.insert(self.next_use, version);
527        self.next_use += 1;
528        self.evict();
529    }
530
531    fn remove(&mut self, version: u64) {
532        if let Some(entry) = self.entries.remove(&version) {
533            self.by_use.remove(&entry.last_use);
534            self.bytes -= entry.bytes;
535        }
536    }
537
538    fn clear(&mut self) {
539        self.entries.clear();
540        self.by_use.clear();
541        self.bytes = 0;
542    }
543
544    fn set_budget(&mut self, budget: usize) {
545        self.budget = budget;
546        self.evict();
547    }
548
549    fn evict(&mut self) {
550        while self.bytes > self.budget {
551            let Some((_, version)) = self.by_use.pop_first() else {
552                break;
553            };
554            if let Some(entry) = self.entries.remove(&version) {
555                self.bytes -= entry.bytes;
556            }
557        }
558    }
559}
560
561impl TextRasterizer {
562    /// A rasterizer with no fonts yet; add them with [`Self::add_font`].
563    #[must_use]
564    pub fn empty() -> Self {
565        Self {
566            fonts: Vec::new(),
567            cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
568        }
569    }
570
571    /// Takes a fallback-ordered OpenType stack.
572    ///
573    /// # Errors
574    /// Returns [`TextRasterError::NoFonts`] or the first malformed font.
575    pub fn new(fonts: Vec<TextFont>) -> Result<Self, TextRasterError> {
576        if fonts.is_empty() {
577            return Err(TextRasterError::NoFonts);
578        }
579        let loaded = fonts
580            .into_iter()
581            .map(LoadedFont::parse)
582            .collect::<Result<Vec<_>, _>>()?;
583        Ok(Self {
584            fonts: loaded,
585            cache: ResultCache::new(DEFAULT_TEXT_CACHE_BYTES),
586        })
587    }
588
589    /// Appends a font to the end of the fallback stack.
590    ///
591    /// Fonts change what every request draws, so the result cache is
592    /// cleared.
593    ///
594    /// # Errors
595    /// Returns [`TextRasterError::InvalidFont`] for malformed font bytes.
596    pub fn add_font(&mut self, source: TextFont) -> Result<(), TextRasterError> {
597        self.fonts.push(LoadedFont::parse(source)?);
598        self.cache.clear();
599        Ok(())
600    }
601
602    /// Number of fonts in the fallback stack.
603    #[must_use]
604    pub fn font_count(&self) -> usize {
605        self.fonts.len()
606    }
607
608    /// Shapes, lays out, and rasterizes one request, or returns the cached
609    /// result of its exact version, shared rather than copied.
610    ///
611    /// # Errors
612    /// Returns a typed validation, font, or size error.
613    pub fn rasterize(
614        &mut self,
615        request: &TextRasterRequest,
616    ) -> Result<Arc<TextRasterResult>, TextRasterError> {
617        validate_request(request)?;
618        if self.fonts.is_empty() {
619            return Err(TextRasterError::NoFonts);
620        }
621        if let Some(cached) = self.cache.get(request.content_version) {
622            return Ok(cached);
623        }
624        let result = Arc::new(self.rasterize_uncached(request)?);
625        self.cache.insert(request.content_version, &result);
626        Ok(result)
627    }
628
629    /// Drops a single host version after content or font invalidation.
630    pub fn invalidate(&mut self, content_version: u64) {
631        self.cache.remove(content_version);
632    }
633
634    /// Sets how many bytes of results the cache keeps, evicting the least
635    /// recently used down to it. Each result counts its pixels and a small
636    /// fixed overhead; a result larger than the budget is returned but not
637    /// kept. Zero keeps nothing.
638    pub fn set_cache_budget(&mut self, bytes: usize) {
639        self.cache.set_budget(bytes);
640    }
641
642    /// Bytes the cached results count against the budget.
643    #[must_use]
644    pub fn cache_bytes(&self) -> usize {
645        self.cache.bytes
646    }
647
648    /// Drops every rasterized result while retaining parsed fonts.
649    pub fn clear(&mut self) {
650        self.cache.clear();
651    }
652
653    /// Number of versioned raster results currently retained.
654    #[must_use]
655    pub fn cache_len(&self) -> usize {
656        self.cache.entries.len()
657    }
658
659    /// Where `request`'s text lies, laid out as [`Self::rasterize`] lays it
660    /// out, without drawing it or touching the result cache.
661    ///
662    /// # Errors
663    /// Returns the validation, font, or size error [`Self::rasterize`]
664    /// returns for the same request.
665    #[allow(clippy::cast_precision_loss)]
666    pub fn measure(&self, request: &TextRasterRequest) -> Result<TextMeasure, TextRasterError> {
667        validate_request(request)?;
668        if self.fonts.is_empty() {
669            return Err(TextRasterError::NoFonts);
670        }
671        let faces = Faces::new(&self.fonts, request.font_weight)?;
672        let layout = self.lay_out(&faces, request)?;
673        let mut ink = None;
674        layout.each_glyph(
675            &faces,
676            request,
677            &mut HashMap::new(),
678            |drawing, origin, scale, _| {
679                if let Some([x_min, y_min, x_max, y_max]) = drawing.ink_bounds() {
680                    include(
681                        &mut ink,
682                        [
683                            origin[0] + x_min * scale,
684                            origin[1] - y_max * scale,
685                            origin[0] + x_max * scale,
686                            origin[1] - y_min * scale,
687                        ],
688                    );
689                }
690            },
691        );
692        // The band reaches past each pixel with ink by the disc's radius and one more pixel for
693        // the rim taps it weights in part: see `Disc::new`.
694        let band = request.outline.map_or(0.0, |outline| outline.width + 1.0);
695        ink = ink.map(|bounds| grown(bounds, band));
696        let padding = request.background.map(|background| background.padding);
697        let mut content = None;
698        for (index, line) in layout.lines.iter().enumerate() {
699            let left = layout.line_offset(line, request.align);
700            let top = layout.line_top(index);
701            include(&mut content, [left, 0.0, left + line.width, layout.height]);
702            if let (Some(padding), true) = (padding, line.width > 0.0) {
703                include(
704                    &mut ink,
705                    grown(
706                        [left, top, left + line.width, top + request.font_size],
707                        padding,
708                    ),
709                );
710            }
711        }
712        let content = content.unwrap_or([0.0, 0.0, 0.0, layout.height]);
713        let last = layout.lines.len().saturating_sub(1);
714        Ok(TextMeasure {
715            first_baseline: layout.line_top(0) + layout.ascent,
716            last_baseline: layout.line_top(last) + layout.ascent,
717            box_width: layout.box_width,
718            box_height: layout.box_height,
719            decoration_margin: request.decoration_margin(),
720            content: grown(content, request.decoration_margin() as f32),
721            ink,
722            line_count: layout.line_count,
723        })
724    }
725
726    /// Shapes `request`'s text as [`Self::rasterize`] would, for layout: its
727    /// lines at any wrapping width come from the result without shaping
728    /// again. The request's `max_width`, color, outline, background and
729    /// content version are ignored.
730    ///
731    /// # Errors
732    /// Returns [`TextRasterError::NoFonts`] without fonts, and
733    /// [`TextRasterError::InvalidRequest`] for a size, line height or text
734    /// [`Self::rasterize`] rejects.
735    pub fn shape(&self, request: &TextRasterRequest) -> Result<ShapedText, TextRasterError> {
736        validate_request(&TextRasterRequest {
737            text: String::new(),
738            max_width: 0.0,
739            content_version: 1,
740            outline: None,
741            background: None,
742            ..request.clone()
743        })?;
744        if request.text.len() > MAX_TEXT_BYTES {
745            return Err(TextRasterError::InvalidRequest);
746        }
747        if self.fonts.is_empty() {
748            return Err(TextRasterError::NoFonts);
749        }
750        let faces = Faces::new(&self.fonts, request.font_weight)?;
751        let order = self.font_order(request.font_family.as_deref(), request.font_weight);
752        let paragraphs = normalize(&request.text, request.emoji_policy)
753            .split('\n')
754            .map(|paragraph| {
755                let bidi = ParagraphBidiInfo::new(paragraph, None);
756                let glyphs = shape_paragraph(
757                    &faces,
758                    paragraph,
759                    &bidi,
760                    request.font_size,
761                    &order,
762                    request.emoji_policy,
763                    request.letter_spacing,
764                );
765                clusters(&glyphs, paragraph, request.letter_spacing)
766            })
767            .collect();
768        Ok(ShapedText {
769            paragraphs,
770            font_size: request.font_size,
771            line_height: request.line_height,
772            line_advance: request.font_size * request.line_height,
773        })
774    }
775
776    /// `request`'s lines, broken and placed in its layout box.
777    #[allow(clippy::cast_precision_loss)]
778    fn lay_out(
779        &self,
780        faces: &Faces<'_>,
781        request: &TextRasterRequest,
782    ) -> Result<Layout, TextRasterError> {
783        let line_advance = request.font_size * request.line_height;
784        let order = self.font_order(request.font_family.as_deref(), request.font_weight);
785        let mut lines = Vec::new();
786        // Lines up to the last with glyphs stay, however little ink they have; blank lines after it
787        // take no room.
788        let mut kept = 1;
789        for paragraph in normalize(&request.text, request.emoji_policy).split('\n') {
790            for line in layout_paragraph(
791                faces,
792                paragraph,
793                request.font_size,
794                request.max_width,
795                &order,
796                request.emoji_policy,
797                request.letter_spacing,
798            ) {
799                lines.push(line);
800                if lines.last().is_some_and(|line| line.has_glyphs) {
801                    kept = lines.len();
802                }
803            }
804        }
805        lines.truncate(kept);
806        let line_count = u32::try_from(lines.len()).map_err(|_| TextRasterError::TooLarge)?;
807        let width = if request.max_width > 0.0 {
808            request.max_width
809        } else {
810            lines.iter().map(|line| line.width).fold(0.0, f32::max)
811        }
812        .max(1.0);
813        let height = box_height(line_count, line_advance, request.font_size).max(1.0);
814        Ok(Layout {
815            box_width: checked_dimension(width)?,
816            box_height: checked_dimension(height)?,
817            lines,
818            line_count,
819            line_advance,
820            half_leading: half_leading(request.font_size, request.line_height),
821            ascent: request.font_size * 0.8,
822            descent: request.font_size * 0.2,
823            width,
824            height,
825        })
826    }
827
828    #[allow(
829        clippy::cast_possible_truncation,
830        clippy::cast_precision_loss,
831        clippy::too_many_lines
832    )]
833    fn rasterize_uncached(
834        &self,
835        request: &TextRasterRequest,
836    ) -> Result<TextRasterResult, TextRasterError> {
837        let faces = Faces::new(&self.fonts, request.font_weight)?;
838        let layout = self.lay_out(&faces, request)?;
839        let (box_width, box_height) = (layout.box_width, layout.box_height);
840        let mut drawings = HashMap::<(usize, u16), Drawing>::new();
841        let mut placed = Vec::new();
842        // Ink past the box on each axis, and the ink's own reach above and below its baselines.
843        let (mut overflow_x, mut overflow_y) = (0_i64, 0_i64);
844        let (mut ink_ascent, mut ink_descent) = (layout.ascent, layout.descent);
845        layout.each_glyph(
846            &faces,
847            request,
848            &mut drawings,
849            |drawing, origin, scale, glyph| {
850                let Some(bounds) = drawing.bounds() else {
851                    return;
852                };
853                let pixels = PixelBox::of(bounds, origin, scale);
854                overflow_x = overflow_x
855                    .max(pixels.left.saturating_neg())
856                    .max(pixels.right.saturating_sub(i64::from(box_width)));
857                overflow_y = overflow_y
858                    .max(pixels.top.saturating_neg())
859                    .max(pixels.bottom.saturating_sub(i64::from(box_height)));
860                ink_ascent = ink_ascent.max(bounds[3] * scale + glyph.y_offset);
861                ink_descent = ink_descent.max(-(bounds[1] * scale + glyph.y_offset));
862                placed.push(((glyph.font, glyph.glyph_index), origin, scale, pixels));
863            },
864        );
865        // The texture is the box plus a margin, equal on both ends of an axis so the box stays
866        // centred, with room for decorations and for outlined ink past the box.
867        let (outline, padding) = decoration_extents(request);
868        let margin = |overflow: i64| -> Result<u32, TextRasterError> {
869            u32::try_from(overflow)
870                .map_err(|_| TextRasterError::TooLarge)?
871                .checked_add(outline)
872                .map(|margin| margin.max(outline.max(padding)))
873                .ok_or(TextRasterError::TooLarge)
874        };
875        let (margin_x, margin_y) = (margin(overflow_x)?, margin(overflow_y)?);
876        let width = margin_x
877            .checked_mul(2)
878            .and_then(|margins| box_width.checked_add(margins))
879            .ok_or(TextRasterError::TooLarge)?;
880        let height = margin_y
881            .checked_mul(2)
882            .and_then(|margins| box_height.checked_add(margins))
883            .ok_or(TextRasterError::TooLarge)?;
884        if width > 16_384 || height > 16_384 {
885            return Err(TextRasterError::TooLarge);
886        }
887        let pixel_count = usize::try_from(width)
888            .ok()
889            .and_then(|width| {
890                usize::try_from(height)
891                    .ok()
892                    .and_then(|height| width.checked_mul(height))
893            })
894            .ok_or(TextRasterError::TooLarge)?;
895        let mut coverage = vec![0_u8; pixel_count];
896        // Color glyphs, premultiplied RGBA, only once one is placed.
897        let mut colors: Option<Vec<u8>> = None;
898        let mut line_boxes = Vec::with_capacity(layout.lines.len());
899        for (index, line) in layout.lines.iter().enumerate() {
900            if line.width > 0.0 {
901                line_boxes.push((
902                    layout.line_offset(line, request.align),
903                    line.width,
904                    layout.line_top(index),
905                ));
906            }
907        }
908        for (key, origin, scale, pixels) in placed {
909            let (origin_x, origin_y) = (
910                pixels.left + i64::from(margin_x),
911                pixels.top + i64::from(margin_y),
912            );
913            let glyph_width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
914            match &drawings[&key] {
915                Drawing::Outline(outline) => place_glyph(
916                    &mut coverage,
917                    width,
918                    height,
919                    origin_x,
920                    origin_y,
921                    glyph_width,
922                    &outline.draw(origin, scale, pixels),
923                ),
924                drawing => place_color_glyph(
925                    colors.get_or_insert_with(|| vec![0; pixel_count * 4]),
926                    width,
927                    height,
928                    origin_x,
929                    origin_y,
930                    glyph_width,
931                    &drawing.draw_color(origin, scale, pixels),
932                ),
933            }
934        }
935        let rgba8 = compose(
936            request,
937            &coverage,
938            colors.as_deref(),
939            width,
940            [margin_x, margin_y],
941            &line_boxes,
942        );
943        let texture = HostTexture {
944            content_version: request.content_version,
945            width,
946            height,
947            premultiplication: Premultiplication::Premultiplied,
948            color_space: RasterColorSpace::Srgb,
949        };
950        Ok(TextRasterResult {
951            descriptor: TextTexture {
952                texture: texture.clone(),
953                layout_x: margin_x as f32,
954                layout_y: margin_y as f32,
955                layout_width: layout.width,
956                layout_height: layout.height,
957                first_baseline: layout.half_leading + layout.ascent,
958                ascent: ink_ascent,
959                descent: ink_descent,
960                line_count: layout.line_count,
961            },
962            pixels: HostTexturePixels { texture, rgba8 },
963        })
964    }
965
966    /// Font indices in the order a request tries them: families in
967    /// fallback order (the order their first faces were registered),
968    /// `family` first; inside a family, faces by CSS Fonts 4's weight
969    /// matching for `weight` ([`weight_order`]), ties in registration order.
970    fn font_order(&self, family: Option<&str>, weight: u16) -> Vec<usize> {
971        // Faces grouped by family, matched without regard to ASCII case; a face without one is a
972        // family of its own.
973        let mut groups: Vec<(Option<String>, Vec<usize>)> = Vec::new();
974        for (index, font) in self.fonts.iter().enumerate() {
975            let name = font.family.as_deref().map(str::to_ascii_lowercase);
976            match groups
977                .iter_mut()
978                .find(|(held, _)| name.is_some() && *held == name)
979            {
980                Some((_, faces)) => faces.push(index),
981                None => groups.push((name, vec![index])),
982            }
983        }
984        let wanted = family.map(str::to_ascii_lowercase);
985        if let Some(at) = groups
986            .iter()
987            .position(|(name, _)| wanted.is_some() && *name == wanted)
988        {
989            let preferred = groups.remove(at);
990            groups.insert(0, preferred);
991        }
992        groups
993            .into_iter()
994            .flat_map(|(_, faces)| {
995                let weights = faces
996                    .iter()
997                    .map(|&index| self.fonts[index].weights())
998                    .collect::<Vec<_>>();
999                weight_order(&weights, weight)
1000                    .into_iter()
1001                    .map(move |at| faces[at])
1002            })
1003            .collect()
1004    }
1005}
1006
1007/// The stack's faces for one request, each parsed once; a face's shaping
1008/// tables only when a run shapes with it.
1009struct Faces<'a> {
1010    parsed: Vec<ttf_parser::Face<'a>>,
1011    shapers: Vec<OnceCell<rustybuzz::Face<'a>>>,
1012    /// Whether each face has color glyphs (a COLR, CBDT, or sbix table).
1013    color: Vec<bool>,
1014}
1015
1016impl<'a> Faces<'a> {
1017    /// The faces at `weight`: a variable face at that `wght`, clamped to its
1018    /// axis, so one request draws one instance of each face.
1019    fn new(fonts: &'a [LoadedFont], weight: u16) -> Result<Self, TextRasterError> {
1020        let parsed = fonts
1021            .iter()
1022            .map(|font| {
1023                let mut face = ttf_parser::Face::parse(&font.source.bytes, font.source.face_index)
1024                    .ok()
1025                    .filter(|face| face.units_per_em() > 0)
1026                    .ok_or_else(|| TextRasterError::InvalidFont(font.source.family.clone()))?;
1027                if let Some((min, _, max)) = font.wght {
1028                    face.set_variation(WGHT, f32::from(weight).clamp(min, max));
1029                }
1030                Ok(face)
1031            })
1032            .collect::<Result<Vec<_>, _>>()?;
1033        Ok(Self {
1034            shapers: parsed.iter().map(|_| OnceCell::new()).collect(),
1035            color: parsed
1036                .iter()
1037                .map(|face| {
1038                    let tables = face.tables();
1039                    tables.colr.is_some() || tables.cbdt.is_some() || tables.sbix.is_some()
1040                })
1041                .collect(),
1042            parsed,
1043        })
1044    }
1045
1046    /// The face's shaper, at the variation the parsed face is set to.
1047    fn shaper(&self, font: usize) -> &rustybuzz::Face<'a> {
1048        self.shapers[font].get_or_init(|| rustybuzz::Face::from_face(self.parsed[font].clone()))
1049    }
1050
1051    #[allow(clippy::cast_precision_loss)]
1052    fn units_per_em(&self, font: usize) -> f32 {
1053        f32::from(self.parsed[font].units_per_em())
1054    }
1055
1056    /// Whether the font maps `character` to a glyph of its own.
1057    fn has(&self, font: usize, character: char) -> bool {
1058        self.parsed[font]
1059            .glyph_index(character)
1060            .is_some_and(|glyph| glyph.0 != 0)
1061    }
1062
1063    /// Whether the font has every character of `grapheme` that is drawn at all.
1064    fn covers(&self, font: usize, grapheme: &str) -> bool {
1065        grapheme
1066            .chars()
1067            .filter(|&character| !is_default_ignorable(character))
1068            .all(|character| self.has(font, character))
1069    }
1070}
1071
1072/// The text as layout reads it: under [`EmojiPolicy::Replace`] each emoji
1073/// grapheme one replacement character, every line or paragraph separator a
1074/// `\n`, and a tab a space (a caption has no tab stops).
1075///
1076/// U+2028 LINE SEPARATOR, which UAX #9 does not count as a paragraph end,
1077/// ends one here too, so every line break is one path.
1078fn normalize(text: &str, policy: EmojiPolicy) -> String {
1079    let mut normalized = String::with_capacity(text.len());
1080    for grapheme in text.graphemes(true) {
1081        if policy == EmojiPolicy::Replace && is_emoji(grapheme) {
1082            normalized.push('\u{fffd}');
1083        } else if grapheme == "\r\n" {
1084            normalized.push('\n');
1085        } else {
1086            normalized.extend(grapheme.chars().map(|character| match character {
1087                '\r'
1088                | '\u{b}'
1089                | '\u{c}'
1090                | '\u{1c}'..='\u{1e}'
1091                | '\u{85}'
1092                | '\u{2028}'
1093                | '\u{2029}' => '\n',
1094                '\t' => ' ',
1095                other => other,
1096            }));
1097        }
1098    }
1099    normalized
1100}
1101
1102/// A grapheme shown as an emoji (UTS #51), as `OffscreenTextRasterizer`
1103/// reads one too: unless U+FE0E asks for text, one holding an
1104/// `Emoji_Presentation` character (regional indicators and skin tones
1105/// included), or U+FE0F with a pictograph or a keycap. A symbol shown as
1106/// text by default, such as (c) or a sun, is not one: a text font draws it.
1107fn is_emoji(grapheme: &str) -> bool {
1108    if grapheme.contains('\u{fe0e}') {
1109        return false;
1110    }
1111    let selected = grapheme.contains('\u{fe0f}');
1112    grapheme.chars().any(|character| {
1113        in_table(unicode_tables::EMOJI_PRESENTATION, character)
1114            || (selected
1115                && (character == '\u{20e3}'
1116                    || in_table(unicode_tables::EXTENDED_PICTOGRAPHIC, character)))
1117    })
1118}
1119
1120/// Whether `text` is of a cursive script, which takes no letter spacing
1121/// (CSS Text 3's list: Adlam, Arabic, Hanifi Rohingya, Mandaic, Mongolian,
1122/// N'Ko, Phags-pa, Syriac), by its first character of a script of its own.
1123fn is_cursive(text: &str) -> bool {
1124    text.chars()
1125        .map(|character| character.script())
1126        .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown))
1127        .is_some_and(|script| {
1128            matches!(
1129                script,
1130                Script::Adlam
1131                    | Script::Arabic
1132                    | Script::Hanifi_Rohingya
1133                    | Script::Mandaic
1134                    | Script::Mongolian
1135                    | Script::Nko
1136                    | Script::Phags_Pa
1137                    | Script::Syriac
1138            )
1139        })
1140}
1141
1142fn is_default_ignorable(character: char) -> bool {
1143    in_table(unicode_tables::DEFAULT_IGNORABLE, character)
1144}
1145
1146fn in_table(table: &[(u32, u32)], character: char) -> bool {
1147    let code = u32::from(character);
1148    table
1149        .binary_search_by(|&(first, last)| {
1150            if last < code {
1151                Ordering::Less
1152            } else if first > code {
1153                Ordering::Greater
1154            } else {
1155                Ordering::Equal
1156            }
1157        })
1158        .is_ok()
1159}
1160
1161/// A space a line may end in: it takes no width there, hanging past the
1162/// end. Not the no-break spaces, which hold their neighbours together, nor
1163/// the Ogham space mark, which is drawn.
1164fn hangs(character: char) -> bool {
1165    character.is_whitespace()
1166        && !matches!(character, '\u{a0}' | '\u{2007}' | '\u{202f}' | '\u{1680}')
1167}
1168
1169fn is_hangul(character: char) -> bool {
1170    matches!(
1171        character,
1172        '\u{1100}'..='\u{11ff}'
1173            | '\u{3130}'..='\u{318f}'
1174            | '\u{a960}'..='\u{a97f}'
1175            | '\u{ac00}'..='\u{d7a3}'
1176            | '\u{d7b0}'..='\u{d7ff}'
1177    )
1178}
1179
1180/// A request's lines, broken and placed: pixels from its layout box's
1181/// top-left corner, y down.
1182struct Layout {
1183    lines: Vec<LaidLine>,
1184    line_count: u32,
1185    /// Baseline to baseline.
1186    line_advance: f32,
1187    /// Space above each line's em box in its line ([`half_leading`]).
1188    half_leading: f32,
1189    /// The nominal ascent and descent lines are spaced by.
1190    ascent: f32,
1191    descent: f32,
1192    /// The layout box: as wide as the request wraps at, else its widest
1193    /// line, and every line's em box tall.
1194    width: f32,
1195    height: f32,
1196    /// The box rounded up to whole pixels, as the texture holds it.
1197    box_width: u32,
1198    box_height: u32,
1199}
1200
1201impl Layout {
1202    /// Where `line` starts in the box: Start and End are the paragraph's own sides.
1203    fn line_offset(&self, line: &LaidLine, align: TextAlign) -> f32 {
1204        match (align, line.rtl) {
1205            (TextAlign::Center, _) => (self.width - line.width) * 0.5,
1206            (TextAlign::Start, false) | (TextAlign::End, true) => 0.0,
1207            (TextAlign::End, false) | (TextAlign::Start, true) => self.width - line.width,
1208        }
1209    }
1210
1211    /// The top of line `index`'s em box, from the box's top.
1212    #[allow(clippy::cast_precision_loss)]
1213    fn line_top(&self, index: usize) -> f32 {
1214        index as f32 * self.line_advance + self.half_leading
1215    }
1216
1217    /// Calls `place` with each glyph's drawing, read once a glyph into
1218    /// `drawings`, its origin in the box, and its pixels a font unit.
1219    #[allow(clippy::cast_precision_loss)]
1220    fn each_glyph(
1221        &self,
1222        faces: &Faces<'_>,
1223        request: &TextRasterRequest,
1224        drawings: &mut HashMap<(usize, u16), Drawing>,
1225        mut place: impl FnMut(&Drawing, [f32; 2], f32, &PositionedGlyph),
1226    ) {
1227        for (index, line) in self.lines.iter().enumerate() {
1228            let offset = self.line_offset(line, request.align);
1229            let baseline = self.line_top(index) + self.ascent;
1230            for glyph in &line.glyphs {
1231                let drawing = drawings
1232                    .entry((glyph.font, glyph.glyph_index))
1233                    .or_insert_with(|| Drawing::of(faces, glyph.font, glyph.glyph_index, request));
1234                let scale = request.font_size / faces.units_per_em(glyph.font);
1235                let origin = [glyph.x + glyph.x_offset + offset, baseline - glyph.y_offset];
1236                place(drawing, origin, scale, glyph);
1237            }
1238        }
1239    }
1240}
1241
1242/// Grows `bounds` to hold `[left, top, right, bottom]`.
1243fn include(bounds: &mut Option<[f32; 4]>, [left, top, right, bottom]: [f32; 4]) {
1244    let held = bounds.get_or_insert([left, top, right, bottom]);
1245    *held = [
1246        held[0].min(left),
1247        held[1].min(top),
1248        held[2].max(right),
1249        held[3].max(bottom),
1250    ];
1251}
1252
1253/// `[left, top, right, bottom]` grown by `by` on every side.
1254fn grown([left, top, right, bottom]: [f32; 4], by: f32) -> [f32; 4] {
1255    [left - by, top - by, right + by, bottom + by]
1256}
1257
1258/// One line of a paragraph, laid out.
1259struct LaidLine {
1260    /// Glyphs left to right, pens from the line's start.
1261    glyphs: Vec<PositionedGlyph>,
1262    /// Width of what it draws: its trailing spaces hang past it.
1263    width: f32,
1264    /// The paragraph runs right to left.
1265    rtl: bool,
1266    /// It has glyphs, even if none with ink.
1267    has_glyphs: bool,
1268}
1269
1270/// Lays out one paragraph: shaped run by run in logical order, broken into
1271/// lines no wider than `max_width` (none when it is not positive), and each
1272/// line put in visual order.
1273#[allow(clippy::too_many_arguments)]
1274fn layout_paragraph(
1275    faces: &Faces<'_>,
1276    paragraph: &str,
1277    font_size: f32,
1278    max_width: f32,
1279    order: &[usize],
1280    policy: EmojiPolicy,
1281    letter_spacing: f32,
1282) -> Vec<LaidLine> {
1283    let bidi = ParagraphBidiInfo::new(paragraph, None);
1284    let rtl = bidi.paragraph_level.is_rtl();
1285    let glyphs = shape_paragraph(
1286        faces,
1287        paragraph,
1288        &bidi,
1289        font_size,
1290        order,
1291        policy,
1292        letter_spacing,
1293    );
1294    let clusters = clusters(&glyphs, paragraph, letter_spacing);
1295    break_lines(&clusters, max_width)
1296        .into_iter()
1297        .map(|range| {
1298            let has_glyphs = !range.is_empty();
1299            // Trailing spaces hang past the end: no width, nothing drawn.
1300            let mut end = range.end;
1301            while end > range.start && clusters[end - 1].hangs {
1302                end -= 1;
1303            }
1304            let mut line = LaidLine {
1305                glyphs: Vec::new(),
1306                width: 0.0,
1307                rtl,
1308                has_glyphs,
1309            };
1310            if end == range.start {
1311                return line;
1312            }
1313            let bytes = clusters[range.start].start
1314                ..clusters
1315                    .get(range.end)
1316                    .map_or(paragraph.len(), |next| next.start);
1317            let levels = bidi.reordered_levels(bytes);
1318            // Each glyph's cluster, for the spacing a cluster takes after it in visual order: a gap
1319            // between neighbours, so none after the line's last on either side.
1320            let mut cluster_of = vec![0; clusters[end - 1].glyphs.end];
1321            for (index, cluster) in clusters[range.start..end].iter().enumerate() {
1322                cluster_of[cluster.glyphs.clone()].fill(range.start + index);
1323            }
1324            let mut x = 0.0_f32;
1325            let mut previous: Option<usize> = None;
1326            for index in visual_order(
1327                clusters[range.start].glyphs.start..clusters[end - 1].glyphs.end,
1328                |index| levels[glyphs[index].cluster].number(),
1329            ) {
1330                let glyph = &glyphs[index];
1331                let cluster = cluster_of[index];
1332                if let Some(before) = previous.filter(|&before| before != cluster) {
1333                    x += clusters[before].spacing;
1334                }
1335                previous = Some(cluster);
1336                line.glyphs.push(PositionedGlyph {
1337                    font: glyph.font,
1338                    glyph_index: glyph.glyph_index,
1339                    x,
1340                    x_offset: glyph.x_offset,
1341                    y_offset: glyph.y_offset,
1342                });
1343                x += glyph.advance;
1344            }
1345            line.width = x;
1346            line
1347        })
1348        .collect()
1349}
1350
1351/// The glyphs of `range`, in logical order, in visual order (UAX #9 L2):
1352/// from the highest level down to the lowest odd one, every run at that
1353/// level or above reversed. A right-to-left run comes back to the order its
1354/// shaper gave.
1355fn visual_order(range: Range<usize>, level: impl Fn(usize) -> u8) -> Vec<usize> {
1356    let mut order = range.collect::<Vec<_>>();
1357    let levels = order.iter().map(|&index| level(index)).collect::<Vec<_>>();
1358    let highest = levels.iter().copied().max().unwrap_or(0);
1359    let Some(lowest_odd) = levels.iter().copied().filter(|level| level % 2 == 1).min() else {
1360        return order;
1361    };
1362    let mut current = levels;
1363    for at_least in (lowest_odd..=highest).rev() {
1364        let mut start = 0;
1365        while start < order.len() {
1366            if current[start] < at_least {
1367                start += 1;
1368                continue;
1369            }
1370            let end = (start..order.len())
1371                .find(|&index| current[index] < at_least)
1372                .unwrap_or(order.len());
1373            order[start..end].reverse();
1374            current[start..end].reverse();
1375            start = end;
1376        }
1377    }
1378    order
1379}
1380
1381/// One stretch of a paragraph shaped as a unit: one direction, one
1382/// script, one font.
1383#[derive(Clone, Debug, PartialEq)]
1384struct Run {
1385    range: Range<usize>,
1386    font: usize,
1387    script: Option<Script>,
1388    rtl: bool,
1389}
1390
1391/// Splits a paragraph into runs. Each grapheme takes the first font in
1392/// `order` with all it draws; one of only common characters (spaces, digits,
1393/// punctuation) stays in the font before it when that has them, and one no
1394/// font has takes the font of its base character, else the font before it.
1395/// A grapheme shown as an emoji tries the color fonts first under
1396/// [`EmojiPolicy::Color`] and the others first otherwise, as one asking for
1397/// text (U+FE0E) does, and stays only in a font of the kind it tries first
1398/// when the stack has one.
1399/// A grapheme of common characters takes the script before it, or after it
1400/// at the paragraph's start.
1401fn itemize(
1402    faces: &Faces<'_>,
1403    paragraph: &str,
1404    bidi: &ParagraphBidiInfo<'_>,
1405    order: &[usize],
1406    policy: EmojiPolicy,
1407) -> Vec<Run> {
1408    let first = order.first().copied().unwrap_or(0);
1409    let (color, text): (Vec<usize>, Vec<usize>) =
1410        order.iter().partition(|&&font| faces.color[font]);
1411    let color_first = color.iter().chain(&text).copied().collect::<Vec<_>>();
1412    let text_first = text.iter().chain(&color).copied().collect::<Vec<_>>();
1413    // Whether a font is of the kind a grapheme wants, or the stack has none of that kind.
1414    let fits = |font: usize, color_wanted: bool| {
1415        faces.color[font] == color_wanted
1416            || if color_wanted {
1417                color.is_empty()
1418            } else {
1419                text.is_empty()
1420            }
1421    };
1422    let mut graphemes = Vec::<(usize, usize, Option<Script>, bool)>::new();
1423    let mut current = None;
1424    for (start, grapheme) in paragraph.grapheme_indices(true) {
1425        let script = grapheme
1426            .chars()
1427            .map(|character| character.script())
1428            .find(|script| !matches!(script, Script::Common | Script::Inherited | Script::Unknown));
1429        // Whether the grapheme wants a color font, when it wants one kind.
1430        let wants_color = if grapheme.contains('\u{fe0e}') {
1431            Some(false)
1432        } else if is_emoji(grapheme) {
1433            Some(policy == EmojiPolicy::Color)
1434        } else {
1435            None
1436        };
1437        let order = match wants_color {
1438            Some(true) => color_first.as_slice(),
1439            Some(false) => text_first.as_slice(),
1440            None => order,
1441        };
1442        let stays = current.filter(|&font| {
1443            script.is_none()
1444                && wants_color.is_none_or(|wanted| fits(font, wanted))
1445                && faces.covers(font, grapheme)
1446        });
1447        let font = stays
1448            .or_else(|| {
1449                order
1450                    .iter()
1451                    .copied()
1452                    .find(|&font| faces.covers(font, grapheme))
1453            })
1454            .or_else(|| {
1455                let base = grapheme
1456                    .chars()
1457                    .find(|&character| !is_default_ignorable(character))?;
1458                order.iter().copied().find(|&font| faces.has(font, base))
1459            })
1460            .or(current)
1461            .unwrap_or(first);
1462        current = Some(font);
1463        graphemes.push((start, font, script, bidi.levels[start].is_rtl()));
1464    }
1465    let leading = graphemes.iter().find_map(|grapheme| grapheme.2);
1466    let mut before = leading;
1467    for grapheme in &mut graphemes {
1468        match grapheme.2 {
1469            Some(script) => before = Some(script),
1470            None => grapheme.2 = before,
1471        }
1472    }
1473    let mut runs = Vec::<Run>::new();
1474    for (index, &(start, font, script, rtl)) in graphemes.iter().enumerate() {
1475        let end = graphemes
1476            .get(index + 1)
1477            .map_or(paragraph.len(), |next| next.0);
1478        match runs.last_mut() {
1479            Some(run) if run.font == font && run.script == script && run.rtl == rtl => {
1480                run.range.end = end;
1481            }
1482            _ => runs.push(Run {
1483                range: start..end,
1484                font,
1485                script,
1486                rtl,
1487            }),
1488        }
1489    }
1490    runs
1491}
1492
1493/// The rustybuzz script of a Unicode one, by its ISO 15924 code.
1494fn shaping_script(script: Script) -> Option<rustybuzz::Script> {
1495    let code: [u8; 4] = script.short_name().as_bytes().try_into().ok()?;
1496    rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&code))
1497}
1498
1499/// Shapes a paragraph run by run, each in its own direction and script
1500/// with the text around it as context, into glyphs in logical order.
1501fn shape_paragraph(
1502    faces: &Faces<'_>,
1503    paragraph: &str,
1504    bidi: &ParagraphBidiInfo<'_>,
1505    font_size: f32,
1506    order: &[usize],
1507    policy: EmojiPolicy,
1508    letter_spacing: f32,
1509) -> Vec<ShapedGlyph> {
1510    // CSS Text 3: spaced text takes no optional ligatures; required ones and cursive joins stay.
1511    let features = if letter_spacing == 0.0 {
1512        Vec::new()
1513    } else {
1514        [b"liga", b"clig", b"dlig", b"hlig"]
1515            .map(|tag| rustybuzz::Feature::new(ttf_parser::Tag::from_bytes(tag), 0, ..))
1516            .to_vec()
1517    };
1518    let mut glyphs = Vec::new();
1519    for run in itemize(faces, paragraph, bidi, order, policy) {
1520        let face = faces.shaper(run.font);
1521        let units = faces.units_per_em(run.font);
1522        let mut buffer = UnicodeBuffer::new();
1523        buffer.push_str(&paragraph[run.range.clone()]);
1524        buffer.set_pre_context(&paragraph[..run.range.start]);
1525        buffer.set_post_context(&paragraph[run.range.end..]);
1526        buffer.set_direction(if run.rtl {
1527            Direction::RightToLeft
1528        } else {
1529            Direction::LeftToRight
1530        });
1531        match run.script.and_then(shaping_script) {
1532            Some(script) => buffer.set_script(script),
1533            None => buffer.guess_segment_properties(),
1534        }
1535        let shaped = rustybuzz::shape(face, &features, buffer);
1536        let first = glyphs.len();
1537        for (info, position) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) {
1538            #[allow(clippy::cast_precision_loss)]
1539            glyphs.push(ShapedGlyph {
1540                font: run.font,
1541                // A font's own substitutions (an AAT `morx` table) may name a glyph it does not
1542                // have; that draws as `.notdef`.
1543                glyph_index: u16::try_from(info.glyph_id)
1544                    .ok()
1545                    .filter(|&glyph| glyph < face.number_of_glyphs())
1546                    .unwrap_or(0),
1547                cluster: run.range.start + usize::try_from(info.cluster).unwrap_or(0),
1548                advance: position.x_advance as f32 * font_size / units,
1549                x_offset: position.x_offset as f32 * font_size / units,
1550                y_offset: position.y_offset as f32 * font_size / units,
1551            });
1552        }
1553        // The shaper gives a right-to-left run in visual order; the paragraph is broken in logical
1554        // order.
1555        if run.rtl {
1556            glyphs[first..].reverse();
1557        }
1558    }
1559    glyphs
1560}
1561
1562#[derive(Copy, Clone, Debug)]
1563struct ShapedGlyph {
1564    font: usize,
1565    glyph_index: u16,
1566    /// Byte offset in the paragraph of the text it draws.
1567    cluster: usize,
1568    advance: f32,
1569    /// Where the shaper places the glyph off its pen position, in pixels, y
1570    /// up: a mark on its base's anchor, for one.
1571    x_offset: f32,
1572    y_offset: f32,
1573}
1574
1575/// The glyphs of one cluster: what a line never splits.
1576#[derive(Clone, Debug, PartialEq)]
1577struct Cluster {
1578    glyphs: Range<usize>,
1579    /// Byte offset in the paragraph of its text.
1580    start: usize,
1581    /// Its advance, its letter spacing included.
1582    width: f32,
1583    /// The letter spacing it takes after itself, in its width.
1584    spacing: f32,
1585    /// All it draws is spaces that hang at a line's end.
1586    hangs: bool,
1587    /// UAX #14 lets a line break before it.
1588    break_before: bool,
1589}
1590
1591/// Groups logical-order glyphs into clusters, with where UAX #14 lets a
1592/// line break: except between two Hangul characters, since Korean breaks
1593/// between words (the tailoring UAX #14 gives for it). Each cluster but one
1594/// of a cursive script takes `letter_spacing` after itself.
1595fn clusters(glyphs: &[ShapedGlyph], paragraph: &str, letter_spacing: f32) -> Vec<Cluster> {
1596    let mut opportunities = unicode_linebreak::linebreaks(paragraph)
1597        .filter(|&(at, kind)| {
1598            kind == BreakOpportunity::Allowed
1599                && !(paragraph[..at].chars().next_back().is_some_and(is_hangul)
1600                    && paragraph[at..].chars().next().is_some_and(is_hangul))
1601        })
1602        .map(|(at, _)| at)
1603        .peekable();
1604    let mut clusters = Vec::<Cluster>::new();
1605    for (index, glyph) in glyphs.iter().enumerate() {
1606        match clusters.last_mut() {
1607            Some(cluster) if cluster.start == glyph.cluster => {
1608                cluster.glyphs.end = index + 1;
1609                cluster.width += glyph.advance;
1610            }
1611            _ => clusters.push(Cluster {
1612                glyphs: index..index + 1,
1613                start: glyph.cluster,
1614                width: glyph.advance,
1615                spacing: 0.0,
1616                hangs: false,
1617                break_before: false,
1618            }),
1619        }
1620    }
1621    for index in 0..clusters.len() {
1622        let start = clusters[index].start;
1623        let end = clusters
1624            .get(index + 1)
1625            .map(|next| next.start)
1626            .filter(|&end| end > start)
1627            .unwrap_or_else(|| {
1628                paragraph[start..]
1629                    .chars()
1630                    .next()
1631                    .map_or(start, |character| start + character.len_utf8())
1632            });
1633        clusters[index].hangs = paragraph[start..end].chars().all(hangs);
1634        if letter_spacing != 0.0 && !is_cursive(&paragraph[start..end]) {
1635            clusters[index].spacing = letter_spacing;
1636            clusters[index].width += letter_spacing;
1637        }
1638        let mut found = false;
1639        while let Some(&at) = opportunities.peek() {
1640            if at > start {
1641                break;
1642            }
1643            opportunities.next();
1644            found = true;
1645        }
1646        clusters[index].break_before = index > 0 && found;
1647    }
1648    clusters
1649}
1650
1651/// Splits a paragraph's clusters into lines no wider than `max_width` (none
1652/// when it is not positive), as cluster ranges.
1653///
1654/// A line breaks at its last opportunity that keeps it within the width;
1655/// what is still wider breaks between clusters. Spaces never push a line
1656/// over: they hang past its end.
1657fn break_lines(clusters: &[Cluster], max_width: f32) -> Vec<Range<usize>> {
1658    let mut lines = Vec::new();
1659    let mut start = 0;
1660    let mut x = 0.0_f32;
1661    // The latest cluster a line may start at, and the pen position there.
1662    let mut opportunity: Option<(usize, f32)> = None;
1663    for (index, cluster) in clusters.iter().enumerate() {
1664        if cluster.break_before && index > start {
1665            opportunity = Some((index, x));
1666        }
1667        // A line that ends at this cluster ends before its spacing.
1668        let fits = |x: f32| x + cluster.width - cluster.spacing <= max_width;
1669        if max_width > 0.0 && !cluster.hangs && index > start && !fits(x) {
1670            let (end, width) = opportunity.unwrap_or((index, x));
1671            lines.push(start..end);
1672            start = end;
1673            x -= width;
1674            opportunity = None;
1675            // What moved down with this cluster is wider than a line: it breaks again before the
1676            // cluster.
1677            if index > start && !fits(x) {
1678                lines.push(start..index);
1679                start = index;
1680                x = 0.0;
1681            }
1682        }
1683        x += cluster.width;
1684    }
1685    lines.push(start..clusters.len());
1686    lines
1687}
1688
1689#[derive(Copy, Clone, Debug)]
1690struct PositionedGlyph {
1691    font: usize,
1692    glyph_index: u16,
1693    /// Pen position from the line's start.
1694    x: f32,
1695    /// The shaper's placement of the glyph off its pen position, in pixels, y up.
1696    x_offset: f32,
1697    y_offset: f32,
1698}
1699
1700/// How a glyph is drawn: its outline, as coverage the text's color fills,
1701/// or under [`EmojiPolicy::Color`] its color form when the font has one.
1702enum Drawing {
1703    Outline(Outline),
1704    Painting(color::Painting),
1705    Bitmap(color::Bitmap),
1706}
1707
1708impl Drawing {
1709    /// The glyph's COLR painting, else its bitmap, else its outline. The
1710    /// painting takes the text's color, opaque, where it asks for it: the
1711    /// text's alpha applies to the whole glyph when it is composed.
1712    fn of(faces: &Faces<'_>, font: usize, glyph: u16, request: &TextRasterRequest) -> Self {
1713        let face = &faces.parsed[font];
1714        if request.emoji_policy == EmojiPolicy::Color && faces.color[font] {
1715            let [red, green, blue, _] = request.color;
1716            if let Some(painting) = color::Painting::of(face, glyph, [red, green, blue, 255]) {
1717                return Self::Painting(painting);
1718            }
1719            if let Some(bitmap) = color::Bitmap::of(face, glyph, request.font_size) {
1720                return Self::Bitmap(bitmap);
1721            }
1722        }
1723        Self::Outline(Outline::of(face, glyph))
1724    }
1725
1726    /// `[x_min, y_min, x_max, y_max]` in font units of all it may draw;
1727    /// `None` when it draws nothing.
1728    fn bounds(&self) -> Option<[f32; 4]> {
1729        match self {
1730            Self::Outline(outline) => outline.bounds,
1731            Self::Painting(painting) => painting.bounds(),
1732            Self::Bitmap(bitmap) => Some(bitmap.bounds()),
1733        }
1734    }
1735
1736    /// `[x_min, y_min, x_max, y_max]` in font units of what it draws: an
1737    /// outline's curves themselves, not their control points, a bitmap's
1738    /// pixels with alpha, or a painting's bounds; `None` when it draws
1739    /// nothing.
1740    fn ink_bounds(&self) -> Option<[f32; 4]> {
1741        match self {
1742            Self::Outline(outline) => outline.curve_bounds(),
1743            Self::Painting(painting) => painting.bounds(),
1744            Self::Bitmap(bitmap) => bitmap.ink_bounds(),
1745        }
1746    }
1747
1748    /// A color glyph over `pixels`, premultiplied RGBA8; an outline draws
1749    /// nothing here.
1750    fn draw_color(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1751        match self {
1752            Self::Outline(_) => Vec::new(),
1753            Self::Painting(painting) => painting.draw(origin, scale, pixels),
1754            Self::Bitmap(bitmap) => bitmap.draw(origin, scale, pixels),
1755        }
1756    }
1757}
1758
1759/// A glyph's outline in font units, y up, as the font draws it.
1760#[derive(Default)]
1761struct Outline {
1762    segments: Vec<Segment>,
1763    /// `[x_min, y_min, x_max, y_max]` of every point, on the curves and off
1764    /// them, so holding the curves; `None` when it draws nothing.
1765    bounds: Option<[f32; 4]>,
1766}
1767
1768enum Segment {
1769    Line([f32; 2], [f32; 2]),
1770    Quad([f32; 2], [f32; 2], [f32; 2]),
1771    Cubic([f32; 2], [f32; 2], [f32; 2], [f32; 2]),
1772}
1773
1774/// The pixels a glyph's outline reaches, right and bottom exclusive, in
1775/// the layout box, y down.
1776#[derive(Copy, Clone, Debug)]
1777struct PixelBox {
1778    left: i64,
1779    top: i64,
1780    right: i64,
1781    bottom: i64,
1782}
1783
1784impl PixelBox {
1785    /// Where outline `bounds` land with the glyph's origin at `origin`
1786    /// (layout pixels, y down) at `scale` pixels a font unit.
1787    #[allow(clippy::cast_possible_truncation)]
1788    fn of(bounds: [f32; 4], origin: [f32; 2], scale: f32) -> Self {
1789        let [x_min, y_min, x_max, y_max] = bounds;
1790        // Each edge the same expression `Outline::draw` maps a point by, so every point lands
1791        // inside.
1792        Self {
1793            left: (origin[0] + x_min * scale).floor() as i64,
1794            top: (origin[1] - y_max * scale).floor() as i64,
1795            right: (origin[0] + x_max * scale).ceil() as i64,
1796            bottom: (origin[1] - y_min * scale).ceil() as i64,
1797        }
1798    }
1799}
1800
1801#[cfg(test)]
1802thread_local! {
1803    /// How many glyph outlines this thread has read from a face.
1804    static OUTLINES_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1805}
1806
1807impl Outline {
1808    fn of(face: &ttf_parser::Face<'_>, glyph: u16) -> Self {
1809        #[cfg(test)]
1810        OUTLINES_READ.with(|read| read.set(read.get() + 1));
1811        let mut sink = OutlineSink::default();
1812        face.outline_glyph(GlyphId(glyph), &mut sink);
1813        sink.close();
1814        sink.outline
1815    }
1816
1817    /// The glyph's coverage over `pixels`, row by row, with its origin at
1818    /// `origin` at `scale`.
1819    #[allow(
1820        clippy::cast_possible_truncation,
1821        clippy::cast_sign_loss,
1822        clippy::cast_precision_loss
1823    )]
1824    fn draw(&self, origin: [f32; 2], scale: f32, pixels: PixelBox) -> Vec<u8> {
1825        let width = usize::try_from(pixels.right - pixels.left).unwrap_or(0);
1826        let height = usize::try_from(pixels.bottom - pixels.top).unwrap_or(0);
1827        let (left, top) = (pixels.left as f32, pixels.top as f32);
1828        let raster = self.raster(width, height, |[x, y]| {
1829            point(origin[0] + x * scale - left, (origin[1] - y * scale) - top)
1830        });
1831        let mut alpha = vec![0_u8; width * height];
1832        raster.for_each_pixel(|index, coverage| {
1833            alpha[index] = (coverage.min(1.0) * 255.0).round() as u8;
1834        });
1835        alpha
1836    }
1837}
1838
1839impl Outline {
1840    /// `[x_min, y_min, x_max, y_max]` of the curves themselves: their ends
1841    /// and where they turn along an axis. A curve stays within its control
1842    /// points but seldom reaches them, so this is tighter than `bounds`.
1843    fn curve_bounds(&self) -> Option<[f32; 4]> {
1844        let mut bounds = None;
1845        let mut hold = |[x, y]: [f32; 2]| include(&mut bounds, [x, y, x, y]);
1846        for segment in &self.segments {
1847            match *segment {
1848                Segment::Line(from, to) => {
1849                    hold(from);
1850                    hold(to);
1851                }
1852                Segment::Quad(from, control, to) => {
1853                    hold(from);
1854                    hold(to);
1855                    for axis in 0..2 {
1856                        // Where the derivative, linear in t, is zero.
1857                        let curvature = from[axis] - 2.0 * control[axis] + to[axis];
1858                        let t = (from[axis] - control[axis]) / curvature;
1859                        if t > 0.0 && t < 1.0 {
1860                            hold(std::array::from_fn(|axis| {
1861                                let u = 1.0 - t;
1862                                u * u * from[axis] + 2.0 * u * t * control[axis] + t * t * to[axis]
1863                            }));
1864                        }
1865                    }
1866                }
1867                Segment::Cubic(from, first, second, to) => {
1868                    hold(from);
1869                    hold(to);
1870                    for axis in 0..2 {
1871                        let [p0, p1, p2, p3] = [from[axis], first[axis], second[axis], to[axis]];
1872                        for t in quadratic_roots(
1873                            -p0 + 3.0 * (p1 - p2) + p3,
1874                            2.0 * (p0 - 2.0 * p1 + p2),
1875                            p1 - p0,
1876                        )
1877                        .into_iter()
1878                        .flatten()
1879                        .filter(|t| *t > 0.0 && *t < 1.0)
1880                        {
1881                            hold(std::array::from_fn(|axis| {
1882                                let u = 1.0 - t;
1883                                u * u * u * from[axis]
1884                                    + 3.0 * u * u * t * first[axis]
1885                                    + 3.0 * u * t * t * second[axis]
1886                                    + t * t * t * to[axis]
1887                            }));
1888                        }
1889                    }
1890                }
1891            }
1892        }
1893        bounds
1894    }
1895
1896    /// The outline drawn into a `width` by `height` raster, each point where
1897    /// `at` puts it.
1898    fn raster(&self, width: usize, height: usize, at: impl Fn([f32; 2]) -> Point) -> Rasterizer {
1899        let mut raster = Rasterizer::new(width, height);
1900        for segment in &self.segments {
1901            match *segment {
1902                Segment::Line(from, to) => raster.draw_line(at(from), at(to)),
1903                Segment::Quad(from, control, to) => raster.draw_quad(at(from), at(control), at(to)),
1904                Segment::Cubic(from, first, second, to) => {
1905                    raster.draw_cubic(at(from), at(first), at(second), at(to));
1906                }
1907            }
1908        }
1909        raster
1910    }
1911}
1912
1913/// The real roots of `a t^2 + b t + c`: a cubic's turning points, where its
1914/// derivative is zero. A non-finite one, from a zero divisor, is no root.
1915fn quadratic_roots(a: f32, b: f32, c: f32) -> [Option<f32>; 2] {
1916    let finite = |t: f32| t.is_finite().then_some(t);
1917    if a.abs() <= f32::EPSILON * (b.abs() + c.abs()) {
1918        return [finite(-c / b), None];
1919    }
1920    let discriminant = b * b - 4.0 * a * c;
1921    if discriminant < 0.0 {
1922        return [None, None];
1923    }
1924    let root = discriminant.sqrt();
1925    [
1926        finite((-b + root) / (2.0 * a)),
1927        finite((-b - root) / (2.0 * a)),
1928    ]
1929}
1930
1931/// Collects an outline from ttf-parser, closing every contour.
1932#[derive(Default)]
1933struct OutlineSink {
1934    outline: Outline,
1935    start: [f32; 2],
1936    pen: [f32; 2],
1937}
1938
1939impl OutlineSink {
1940    fn include(&mut self, points: &[[f32; 2]]) {
1941        for &[x, y] in points {
1942            let bounds = self.outline.bounds.get_or_insert([x, y, x, y]);
1943            *bounds = [
1944                bounds[0].min(x),
1945                bounds[1].min(y),
1946                bounds[2].max(x),
1947                bounds[3].max(y),
1948            ];
1949        }
1950    }
1951}
1952
1953impl OutlineBuilder for OutlineSink {
1954    fn move_to(&mut self, x: f32, y: f32) {
1955        self.close();
1956        self.start = [x, y];
1957        self.pen = [x, y];
1958    }
1959
1960    fn line_to(&mut self, x: f32, y: f32) {
1961        let to = [x, y];
1962        self.include(&[self.pen, to]);
1963        self.outline.segments.push(Segment::Line(self.pen, to));
1964        self.pen = to;
1965    }
1966
1967    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1968        let (control, to) = ([x1, y1], [x, y]);
1969        self.include(&[self.pen, control, to]);
1970        self.outline
1971            .segments
1972            .push(Segment::Quad(self.pen, control, to));
1973        self.pen = to;
1974    }
1975
1976    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1977        let (first, second, to) = ([x1, y1], [x2, y2], [x, y]);
1978        self.include(&[self.pen, first, second, to]);
1979        self.outline
1980            .segments
1981            .push(Segment::Cubic(self.pen, first, second, to));
1982        self.pen = to;
1983    }
1984
1985    fn close(&mut self) {
1986        // Exactly where it began: a contour that came back by its own last point needs no closing
1987        // line.
1988        if self.pen.map(f32::to_bits) != self.start.map(f32::to_bits) {
1989            self.line_to(self.start[0], self.start[1]);
1990        }
1991    }
1992}
1993
1994fn validate_request(request: &TextRasterRequest) -> Result<(), TextRasterError> {
1995    if request.font_size.is_finite()
1996        && request.font_size > 0.0
1997        && request.font_size <= MAX_FONT_SIZE
1998        && request.text.len() <= MAX_TEXT_BYTES
1999        && request.max_width.is_finite()
2000        && request.max_width <= 16_384.0
2001        && request.line_height.is_finite()
2002        && request.line_height > 0.0
2003        && request.line_height <= MAX_LINE_HEIGHT
2004        && request.letter_spacing.is_finite()
2005        && request.letter_spacing >= LETTER_SPACING_RANGE.start() * request.font_size
2006        && request.letter_spacing <= LETTER_SPACING_RANGE.end() * request.font_size
2007        && (1..=1000).contains(&request.font_weight)
2008        && request.content_version > 0
2009        && request.outline.is_none_or(|outline| {
2010            outline.width.is_finite() && outline.width > 0.0 && outline.width <= MAX_OUTLINE_WIDTH
2011        })
2012        && request.background.is_none_or(|background| {
2013            background.padding.is_finite()
2014                && background.padding >= 0.0
2015                && background.padding <= MAX_BACKGROUND_PADDING
2016        })
2017    {
2018        Ok(())
2019    } else {
2020        Err(TextRasterError::InvalidRequest)
2021    }
2022}
2023
2024fn checked_dimension(value: f32) -> Result<u32, TextRasterError> {
2025    if value.is_finite() && value > 0.0 && value.ceil() <= 16_384.0 {
2026        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2027        Ok(value.ceil() as u32)
2028    } else {
2029        Err(TextRasterError::TooLarge)
2030    }
2031}
2032
2033/// Adds a glyph's coverage to the mask: where glyphs overlap, each pixel
2034/// keeps the most ink either gives it, so a glyph's empty corners never
2035/// erase its neighbour.
2036fn place_glyph(
2037    mask: &mut [u8],
2038    width: u32,
2039    height: u32,
2040    origin_x: i64,
2041    origin_y: i64,
2042    glyph_width: usize,
2043    alpha: &[u8],
2044) {
2045    for (index, coverage) in alpha.iter().copied().enumerate() {
2046        let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
2047        let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
2048        if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) {
2049            continue;
2050        }
2051        let pixel =
2052            u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
2053        let slot = &mut mask[usize::try_from(pixel).unwrap_or(0)];
2054        *slot = (*slot).max(coverage);
2055    }
2056}
2057
2058/// Adds a color glyph to the color layer, premultiplied RGBA8: where
2059/// glyphs overlap, the later one goes over.
2060fn place_color_glyph(
2061    colors: &mut [u8],
2062    width: u32,
2063    height: u32,
2064    origin_x: i64,
2065    origin_y: i64,
2066    glyph_width: usize,
2067    rgba8: &[u8],
2068) {
2069    for (index, source) in rgba8.as_chunks::<4>().0.iter().enumerate() {
2070        let x = origin_x + i64::try_from(index % glyph_width.max(1)).unwrap_or(0);
2071        let y = origin_y + i64::try_from(index / glyph_width.max(1)).unwrap_or(0);
2072        if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) || source[3] == 0 {
2073            continue;
2074        }
2075        let pixel =
2076            u64::try_from(y).unwrap_or(0) * u64::from(width) + u64::try_from(x).unwrap_or(0);
2077        let slot = &mut colors[usize::try_from(pixel).unwrap_or(0) * 4..][..4];
2078        let blended = over(
2079            std::array::from_fn(|channel| u32::from(source[channel])),
2080            std::array::from_fn(|channel| u32::from(slot[channel])),
2081        );
2082        for (byte, value) in slot.iter_mut().zip(blended) {
2083            *byte = u8::try_from(value.min(255)).unwrap_or(u8::MAX);
2084        }
2085    }
2086}
2087
2088impl TextRasterRequest {
2089    /// Pixels the decorations reach past the layout box on every side when
2090    /// no ink does: the outline's disc and the background's padding, whole.
2091    /// The texture's margin ([`TextTexture::layout_x`]) is this plus room
2092    /// for ink past the box.
2093    #[must_use]
2094    pub fn decoration_margin(&self) -> u32 {
2095        let (outline, padding) = decoration_extents(self);
2096        outline.max(padding)
2097    }
2098}
2099
2100/// Pixels each decoration reaches past what it surrounds, whole: the
2101/// outline's disc past the ink, the background's padding past the box.
2102fn decoration_extents(request: &TextRasterRequest) -> (u32, u32) {
2103    let outline = request
2104        .outline
2105        .map_or(0, |outline| disc_extent(outline.width));
2106    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2107    let padding = request
2108        .background
2109        .map_or(0, |background| background.padding.ceil() as u32);
2110    (outline, padding)
2111}
2112
2113/// Integer radius of the disc an outline of `width` dilates by.
2114#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2115fn disc_extent(width: f32) -> u32 {
2116    (width + 0.5).ceil() as u32
2117}
2118
2119/// `color` at `coverage`, premultiplied: `(c * alpha, alpha)` with
2120/// `alpha = coverage * a`, each product truncated to a byte. The pinned
2121/// rasters depend on this truncation.
2122fn premultiplied(color: [u8; 4], coverage: u8) -> [u32; 4] {
2123    let alpha = u32::from(coverage) * u32::from(color[3]) / 255;
2124    [
2125        u32::from(color[0]) * alpha / 255,
2126        u32::from(color[1]) * alpha / 255,
2127        u32::from(color[2]) * alpha / 255,
2128        alpha,
2129    ]
2130}
2131
2132/// `source` over `destination`, premultiplied, rounded to the nearest byte.
2133fn over(source: [u32; 4], destination: [u32; 4]) -> [u32; 4] {
2134    let keep = 255 - source[3];
2135    std::array::from_fn(|channel| source[channel] + (destination[channel] * keep + 127) / 255)
2136}
2137
2138/// The text over its outline over its line boxes, premultiplied RGBA8, all
2139/// in integers so every target gives the same bytes. Undecorated text is its
2140/// coverage in its color alone, with the color glyphs (`colors`,
2141/// premultiplied RGBA8) over it at the color's alpha. The outline surrounds
2142/// both: its band dilates the coverage and the color glyphs' alpha.
2143#[allow(
2144    clippy::cast_possible_truncation,
2145    clippy::cast_sign_loss,
2146    clippy::cast_precision_loss
2147)]
2148fn compose(
2149    request: &TextRasterRequest,
2150    coverage: &[u8],
2151    colors: Option<&[u8]>,
2152    width: u32,
2153    margin: [u32; 2],
2154    line_boxes: &[(f32, f32, f32)],
2155) -> Vec<u8> {
2156    let outline = request.outline.map(|outline| {
2157        let ink = colors.map(|colors| {
2158            coverage
2159                .iter()
2160                .zip(colors.as_chunks::<4>().0)
2161                .map(|(&text, color)| text.max(color[3]))
2162                .collect::<Vec<_>>()
2163        });
2164        (
2165            outline.color,
2166            dilate(
2167                ink.as_deref().unwrap_or(coverage),
2168                width,
2169                u32::try_from(coverage.len() / width.max(1) as usize).unwrap_or(0),
2170                outline.width,
2171            ),
2172        )
2173    });
2174    let background = request.background.map(|background| {
2175        let [margin_x, margin_y] = margin.map(|margin| margin as f32);
2176        let padding = background.padding;
2177        let boxes = line_boxes
2178            .iter()
2179            .map(|&(left, line_width, top)| {
2180                (
2181                    margin_x + left - padding,
2182                    margin_x + left + line_width + padding,
2183                    margin_y + top - padding,
2184                    margin_y + top + request.font_size + padding,
2185                )
2186            })
2187            .collect::<Vec<_>>();
2188        (background.color, boxes)
2189    });
2190    let mut rgba8 = vec![0_u8; coverage.len() * 4];
2191    // The boxes that may meet the current row, `first..last`; boxes come in line order, so the
2192    // window only moves down.
2193    let (mut first, mut last) = (0, 0);
2194    for (index, text) in coverage.iter().copied().enumerate() {
2195        let mut pixel = [0_u32; 4];
2196        if let Some((color, boxes)) = &background {
2197            let x = (index % width as usize) as f32;
2198            let y = (index / width as usize) as f32;
2199            if index % width as usize == 0 {
2200                while first < boxes.len() && boxes[first].3 <= y {
2201                    first += 1;
2202                }
2203                last = last.max(first);
2204                while last < boxes.len() && boxes[last].2 < y + 1.0 {
2205                    last += 1;
2206                }
2207            }
2208            let covered = boxes[first..last]
2209                .iter()
2210                .map(|&(left, right, top, bottom)| {
2211                    let across = (right.min(x + 1.0) - left.max(x)).clamp(0.0, 1.0);
2212                    let down = (bottom.min(y + 1.0) - top.max(y)).clamp(0.0, 1.0);
2213                    across * down
2214                })
2215                .fold(0.0_f32, f32::max);
2216            pixel = premultiplied(*color, (covered * 255.0).round() as u8);
2217        }
2218        if let Some((color, band)) = &outline {
2219            pixel = over(premultiplied(*color, band[index]), pixel);
2220        }
2221        pixel = over(premultiplied(request.color, text), pixel);
2222        if let Some(colors) = colors {
2223            let color = &colors[index * 4..][..4];
2224            if color[3] > 0 {
2225                let opacity = u32::from(request.color[3]);
2226                pixel = over(
2227                    std::array::from_fn(|channel| u32::from(color[channel]) * opacity / 255),
2228                    pixel,
2229                );
2230            }
2231        }
2232        for (channel, value) in pixel.iter().enumerate() {
2233            rgba8[index * 4 + channel] = (*value).min(255) as u8;
2234        }
2235    }
2236    rgba8
2237}
2238
2239/// The coverage dilated by a disc of radius `width`: each pixel takes the
2240/// highest coverage within the disc, weighted by how much of the disc's edge
2241/// pixel lies inside it.
2242///
2243/// The work follows the ink, not the texture: only the ink's bounds grown by
2244/// the disc can be reached. The disc's fully weighted pixels are, row by row,
2245/// one run centred on the column, so they are a sliding-window maximum
2246/// (van Herk/Gil-Werman) of a coverage row; only the antialiased rim is
2247/// visited tap by tap. Every pixel is the same integer maximum a tap-by-tap
2248/// pass computes, so the band is byte for byte the same.
2249#[allow(
2250    clippy::cast_possible_truncation,
2251    clippy::cast_sign_loss,
2252    clippy::cast_precision_loss,
2253    clippy::cast_possible_wrap
2254)]
2255fn dilate(coverage: &[u8], width: u32, height: u32, radius: f32) -> Vec<u8> {
2256    let (width, height) = (width as usize, height as usize);
2257    let mut band = vec![0_u8; coverage.len()];
2258    let Some(ink) = ink_bounds(coverage, width, height) else {
2259        return band;
2260    };
2261    let disc = Disc::new(radius);
2262    let extent = disc.extent;
2263    let reach = Bounds {
2264        left: ink.left.saturating_sub(extent),
2265        top: ink.top.saturating_sub(extent),
2266        right: (ink.right + extent).min(width),
2267        bottom: (ink.bottom + extent).min(height),
2268    };
2269    // Each run half-width once per coverage row, laid onto every output row whose disc has that run
2270    // there.
2271    let span = reach.right - reach.left;
2272    let mut window = WindowMax::default();
2273    let mut maxima = vec![0_u8; span];
2274    for (half, offsets) in &disc.runs {
2275        for source_y in ink.top..ink.bottom {
2276            let row = &coverage[source_y * width..(source_y + 1) * width];
2277            window.of(row, reach.left, span, *half, &mut maxima);
2278            for &offset in offsets {
2279                // The tap `offset` rows below an output row reads this row.
2280                let Some(y) = source_y.checked_add_signed(-offset) else {
2281                    continue;
2282                };
2283                if y < reach.top || y >= reach.bottom {
2284                    continue;
2285                }
2286                let out = &mut band[y * width + reach.left..y * width + reach.right];
2287                for (pixel, highest) in out.iter_mut().zip(&maxima) {
2288                    *pixel = (*pixel).max(*highest);
2289                }
2290            }
2291        }
2292    }
2293    for &(dx, dy, weight) in &disc.rim {
2294        for source_y in ink.top..ink.bottom {
2295            let Some(y) = source_y.checked_add_signed(-dy) else {
2296                continue;
2297            };
2298            if y >= height {
2299                continue;
2300            }
2301            // Output columns x read x + dx; keep both within the texture and the source within the
2302            // ink.
2303            let first = ink.left.max(usize::try_from(dx).unwrap_or(0));
2304            let last = ink
2305                .right
2306                .min(usize::try_from(width as isize + dx.min(0)).unwrap_or(0));
2307            if first >= last {
2308                continue;
2309            }
2310            let source = &coverage[source_y * width + first..source_y * width + last];
2311            let start = y * width + (first as isize - dx) as usize;
2312            let out = &mut band[start..start + source.len()];
2313            for (pixel, value) in out.iter_mut().zip(source) {
2314                *pixel = (*pixel).max((u32::from(*value) * weight / 255) as u8);
2315            }
2316        }
2317    }
2318    band
2319}
2320
2321/// A rectangle of pixels, right and bottom exclusive.
2322#[derive(Copy, Clone, Debug, Eq, PartialEq)]
2323struct Bounds {
2324    left: usize,
2325    top: usize,
2326    right: usize,
2327    bottom: usize,
2328}
2329
2330/// The smallest rectangle holding every pixel with coverage, if any has.
2331fn ink_bounds(coverage: &[u8], width: usize, height: usize) -> Option<Bounds> {
2332    let mut bounds: Option<Bounds> = None;
2333    for y in 0..height {
2334        let row = &coverage[y * width..(y + 1) * width];
2335        let Some(left) = row.iter().position(|&value| value > 0) else {
2336            continue;
2337        };
2338        let right = row.iter().rposition(|&value| value > 0).unwrap_or(left) + 1;
2339        bounds = Some(bounds.map_or(
2340            Bounds {
2341                left,
2342                top: y,
2343                right,
2344                bottom: y + 1,
2345            },
2346            |bounds| Bounds {
2347                left: bounds.left.min(left),
2348                top: bounds.top,
2349                right: bounds.right.max(right),
2350                bottom: y + 1,
2351            },
2352        ));
2353    }
2354    bounds
2355}
2356
2357/// The taps of a disc of some radius, split into what a dilation reads as
2358/// runs and what it reads tap by tap.
2359struct Disc {
2360    /// Integer radius of the square holding the disc.
2361    extent: usize,
2362    /// Run half-widths with the row offsets whose fully weighted taps are
2363    /// exactly the columns `-half..=half`.
2364    runs: Vec<(usize, Vec<isize>)>,
2365    /// `(dx, dy, weight)` of each tap weighted below 255 and above 0.
2366    rim: Vec<(isize, isize, u32)>,
2367}
2368
2369impl Disc {
2370    /// The disc of `radius`: a tap's weight is how much of it lies within
2371    /// `radius + 0.5` of the centre, in 255ths, rounded.
2372    #[allow(
2373        clippy::cast_possible_truncation,
2374        clippy::cast_sign_loss,
2375        clippy::cast_precision_loss,
2376        clippy::cast_possible_wrap
2377    )]
2378    fn new(radius: f32) -> Self {
2379        let extent = disc_extent(radius) as isize;
2380        let mut runs: Vec<(usize, Vec<isize>)> = Vec::new();
2381        let mut rim = Vec::new();
2382        for dy in -extent..=extent {
2383            let mut half = None;
2384            for dx in -extent..=extent {
2385                let distance = ((dx * dx + dy * dy) as f32).sqrt();
2386                let weight = ((radius + 0.5 - distance).clamp(0.0, 1.0) * 255.0).round() as u32;
2387                match weight {
2388                    0 => {}
2389                    // Weight falls with distance, so a row's full taps are one run around its
2390                    // centre.
2391                    255 => half = Some(dx.unsigned_abs()),
2392                    _ => rim.push((dx, dy, weight)),
2393                }
2394            }
2395            if let Some(half) = half {
2396                match runs.iter_mut().find(|(run, _)| *run == half) {
2397                    Some((_, offsets)) => offsets.push(dy),
2398                    None => runs.push((half, vec![dy])),
2399                }
2400            }
2401        }
2402        Self {
2403            extent: extent as usize,
2404            runs,
2405            rim,
2406        }
2407    }
2408}
2409
2410/// Scratch for sliding-window maxima of a row (van Herk/Gil-Werman): the
2411/// maxima from each block's start and to each block's end.
2412#[derive(Default)]
2413struct WindowMax {
2414    padded: Vec<u8>,
2415    from_start: Vec<u8>,
2416    to_end: Vec<u8>,
2417}
2418
2419impl WindowMax {
2420    /// Into `out[i]`, the highest value of `row` over columns
2421    /// `left + i - half ..= left + i + half`, reading 0 off the row.
2422    fn of(&mut self, row: &[u8], left: usize, count: usize, half: usize, out: &mut [u8]) {
2423        let window = 2 * half + 1;
2424        let length = count + 2 * half;
2425        self.padded.clear();
2426        self.padded.resize(length, 0);
2427        // Padded index i is column left - half + i.
2428        let first_column = left.saturating_sub(half);
2429        let last_column = (left + count + half).min(row.len());
2430        if first_column < last_column {
2431            let at = first_column + half - left;
2432            self.padded[at..at + last_column - first_column]
2433                .copy_from_slice(&row[first_column..last_column]);
2434        }
2435        self.from_start.clear();
2436        self.from_start.resize(length, 0);
2437        self.to_end.clear();
2438        self.to_end.resize(length, 0);
2439        for block in (0..length).step_by(window) {
2440            let end = (block + window).min(length);
2441            let mut highest = 0;
2442            for index in block..end {
2443                highest = highest.max(self.padded[index]);
2444                self.from_start[index] = highest;
2445            }
2446            highest = 0;
2447            for index in (block..end).rev() {
2448                highest = highest.max(self.padded[index]);
2449                self.to_end[index] = highest;
2450            }
2451        }
2452        for (index, value) in out[..count].iter_mut().enumerate() {
2453            *value = self.to_end[index].max(self.from_start[index + window - 1]);
2454        }
2455    }
2456}
2457
2458#[cfg(test)]
2459mod tests;