Skip to main content

alpha2_text/
lib.rs

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