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