Skip to main content

alpha2_text/
animated.rs

1//! Per-cluster animation states (motion and color) for text animators.
2//!
3//! [`TextRasterizer::layout`] reports where each cluster lies so a caller can compute states;
4//! [`TextRasterizer::rasterize_animated`] draws with them. Tracking re-aligns a line but never
5//! re-wraps it.
6//!
7//! Outlines are transformed before rasterizing, so scaled or rotated glyphs stay sharp. Each
8//! cluster is drawn into its own tile and tiles combine by maximum coverage, taking color from the
9//! winning cluster; strokes work the same way. Color glyphs are drawn upright and resampled
10//! bilinearly in fixed point. Integer and `libm` arithmetic keep native and wasm output identical.
11
12use super::{
13    Drawing, Faces, HashMap, HostTexture, HostTexturePixels, Layout, PixelBox, PixelLook,
14    Premultiplication, RasterColorSpace, RequestLook, TextFill, TextRasterError, TextRasterRequest,
15    TextRasterResult, TextRasterizer, TextTexture, decoration_extents, dilate, disc_extent,
16    gradient, normalize_mapped, over, point, stroke_radius, validate_request,
17};
18use std::ops::Range;
19use std::sync::Arc;
20use unicode_segmentation::UnicodeSegmentation;
21
22/// Cluster and line positions, matching [`TextRasterizer::rasterize`].
23#[derive(Clone, Debug, PartialEq)]
24pub struct TextLayout {
25    /// Clusters (the unit of letter spacing) in logical order.
26    pub clusters: Vec<ClusterInfo>,
27    /// Lines, top to bottom.
28    pub lines: Vec<LineInfo>,
29    /// Font size in pixels.
30    pub font_size: f32,
31}
32
33/// One cluster of a [`TextLayout`].
34#[derive(Clone, Debug, PartialEq)]
35pub struct ClusterInfo {
36    /// Byte range in the request's text.
37    pub bytes: Range<usize>,
38    /// Line index.
39    pub line: u32,
40    /// Index of its UAX #29 word, counting only words with a letter or digit; `None` for spaces
41    /// and punctuation between words.
42    pub word: Option<u32>,
43    /// Whether it holds only white space.
44    pub is_space: bool,
45    /// Start of its advance in pixels from the layout box's left edge.
46    pub x: f32,
47    /// Advance in pixels; zero for a space hanging at a line end.
48    pub advance: f32,
49    /// Line baseline in pixels from the layout box's top.
50    pub baseline: f32,
51}
52
53/// One line of a [`TextLayout`].
54#[derive(Clone, Debug, PartialEq)]
55pub struct LineInfo {
56    /// Cluster indices, in logical order.
57    pub clusters: Range<usize>,
58    /// Start in pixels from the layout box's left edge.
59    pub left: f32,
60    /// Advance excluding trailing spaces.
61    pub width: f32,
62    /// Baseline in pixels from the layout box's top.
63    pub baseline: f32,
64}
65
66/// A color change `base * keep + add` in premultiplied linear light. Mixing toward a target by
67/// `w` is `keep = 1 - w`, `add = w * target`; mixes compose.
68#[derive(Copy, Clone, Debug, PartialEq)]
69pub struct ColorMix {
70    /// Factor on the base color, in `[0, 1]`.
71    pub keep: f32,
72    /// Premultiplied linear-light RGBA in `[0, 1]`.
73    pub add: [f32; 4],
74}
75
76/// Animation state of one cluster.
77#[derive(Copy, Clone, Debug, PartialEq)]
78pub struct ClusterState {
79    /// Translation in pixels, +y down.
80    pub offset: [f32; 2],
81    /// Non-negative scale about its center.
82    pub scale: [f32; 2],
83    /// Clockwise rotation about its center, in radians.
84    pub rotation: f32,
85    /// Multiplies fill and stroke coverage, in `[0, 1]`.
86    pub opacity: f32,
87    /// Fill color change; `None` keeps it.
88    pub fill: Option<ColorMix>,
89    /// Stroke color change; `None` keeps them.
90    pub stroke: Option<ColorMix>,
91    /// Extra space after the cluster in pixels, like letter spacing.
92    pub tracking: f32,
93}
94
95impl ClusterState {
96    /// No change.
97    pub const IDENTITY: Self = Self {
98        offset: [0.0, 0.0],
99        scale: [1.0, 1.0],
100        rotation: 0.0,
101        opacity: 1.0,
102        fill: None,
103        stroke: None,
104        tracking: 0.0,
105    };
106
107    /// Whether the state changes nothing.
108    #[must_use]
109    pub fn is_identity(&self) -> bool {
110        *self == Self::IDENTITY
111    }
112
113    fn is_valid(&self) -> bool {
114        let mix = |mix: &Option<ColorMix>| {
115            mix.is_none_or(|mix| {
116                (0.0..=1.0).contains(&mix.keep)
117                    && mix.add.iter().all(|value| (0.0..=1.0).contains(value))
118            })
119        };
120        self.offset.iter().all(|value| value.is_finite())
121            && self
122                .scale
123                .iter()
124                .all(|value| value.is_finite() && *value >= 0.0)
125            && self.rotation.is_finite()
126            && (0.0..=1.0).contains(&self.opacity)
127            && self.tracking.is_finite()
128            && mix(&self.fill)
129            && mix(&self.stroke)
130    }
131}
132
133/// An affine map of layout pixels: `[a, b, c, d, e, f]` sends `(x, y)` to
134/// `(a x + c y + e, b x + d y + f)`.
135type Affine = [f32; 6];
136
137fn apply(affine: Affine, [x, y]: [f32; 2]) -> [f32; 2] {
138    [
139        affine[0] * x + affine[2] * y + affine[4],
140        affine[1] * x + affine[3] * y + affine[5],
141    ]
142}
143
144/// Scales and rotates about `pivot`, then translates by `offset`.
145fn cluster_affine(state: &ClusterState, pivot: [f32; 2]) -> Affine {
146    let (sine, cosine) = (libm::sinf(state.rotation), libm::cosf(state.rotation));
147    let [sx, sy] = state.scale;
148    // Rotation clockwise on screen, y down: x' = x cos - y sin, y' = x sin + y cos.
149    let (a, b, c, d) = (cosine * sx, sine * sx, -sine * sy, cosine * sy);
150    let [px, py] = pivot;
151    [
152        a,
153        b,
154        c,
155        d,
156        px - a * px - c * py + state.offset[0],
157        py - b * px - d * py + state.offset[1],
158    ]
159}
160
161fn invert(affine: Affine) -> Option<Affine> {
162    let determinant = affine[0] * affine[3] - affine[1] * affine[2];
163    if determinant.abs() <= f32::MIN_POSITIVE {
164        return None;
165    }
166    let (a, b, c, d) = (
167        affine[3] / determinant,
168        -affine[1] / determinant,
169        -affine[2] / determinant,
170        affine[0] / determinant,
171    );
172    Some([
173        a,
174        b,
175        c,
176        d,
177        -(a * affine[4] + c * affine[5]),
178        -(b * affine[4] + d * affine[5]),
179    ])
180}
181
182/// One cluster's coverage and color glyphs over a pixel rectangle (right and bottom exclusive)
183/// that may extend past the layout box.
184struct Tile {
185    left: i64,
186    top: i64,
187    width: usize,
188    height: usize,
189    coverage: Vec<u8>,
190    /// Premultiplied RGBA8, once a color glyph is drawn into it.
191    colors: Option<Vec<u8>>,
192}
193
194impl Tile {
195    fn empty() -> Self {
196        Self {
197            left: 0,
198            top: 0,
199            width: 0,
200            height: 0,
201            coverage: Vec::new(),
202            colors: None,
203        }
204    }
205
206    fn grow(&mut self, bounds: PixelBox) {
207        if self.width == 0 || self.height == 0 {
208            *self = Self {
209                left: bounds.left,
210                top: bounds.top,
211                width: usize::try_from(bounds.right - bounds.left).unwrap_or(0),
212                height: usize::try_from(bounds.bottom - bounds.top).unwrap_or(0),
213                coverage: Vec::new(),
214                colors: None,
215            };
216            self.coverage = vec![0; self.width * self.height];
217            return;
218        }
219        let right = (self.left + i64::try_from(self.width).unwrap_or(0)).max(bounds.right);
220        let bottom = (self.top + i64::try_from(self.height).unwrap_or(0)).max(bounds.bottom);
221        let (left, top) = (self.left.min(bounds.left), self.top.min(bounds.top));
222        if left == self.left
223            && top == self.top
224            && right == self.left + i64::try_from(self.width).unwrap_or(0)
225            && bottom == self.top + i64::try_from(self.height).unwrap_or(0)
226        {
227            return;
228        }
229        let width = usize::try_from(right - left).unwrap_or(0);
230        let height = usize::try_from(bottom - top).unwrap_or(0);
231        let (dx, dy) = (
232            usize::try_from(self.left - left).unwrap_or(0),
233            usize::try_from(self.top - top).unwrap_or(0),
234        );
235        let mut coverage = vec![0; width * height];
236        let mut colors = self.colors.as_ref().map(|_| vec![0; width * height * 4]);
237        for row in 0..self.height {
238            let to = (row + dy) * width + dx;
239            coverage[to..to + self.width]
240                .copy_from_slice(&self.coverage[row * self.width..(row + 1) * self.width]);
241            if let (Some(colors), Some(held)) = (&mut colors, &self.colors) {
242                colors[to * 4..(to + self.width) * 4]
243                    .copy_from_slice(&held[row * self.width * 4..(row + 1) * self.width * 4]);
244            }
245        }
246        *self = Self {
247            left,
248            top,
249            width,
250            height,
251            coverage,
252            colors,
253        };
254    }
255
256    fn index(&self, x: i64, y: i64) -> Option<usize> {
257        let (x, y) = (
258            usize::try_from(x - self.left).ok()?,
259            usize::try_from(y - self.top).ok()?,
260        );
261        (x < self.width && y < self.height).then_some(y * self.width + x)
262    }
263
264    fn bounds(&self) -> Option<PixelBox> {
265        (self.width > 0 && self.height > 0).then(|| PixelBox {
266            left: self.left,
267            top: self.top,
268            right: self.left + i64::try_from(self.width).unwrap_or(0),
269            bottom: self.top + i64::try_from(self.height).unwrap_or(0),
270        })
271    }
272}
273
274/// Pixel box of a glyph's font-unit `bounds` after placement and `affine`.
275#[allow(clippy::cast_possible_truncation)]
276fn mapped_box(bounds: [f32; 4], origin: [f32; 2], scale: f32, affine: Affine) -> PixelBox {
277    let [x_min, y_min, x_max, y_max] = bounds;
278    let corners = [
279        [x_min, y_min],
280        [x_max, y_min],
281        [x_min, y_max],
282        [x_max, y_max],
283    ]
284    .map(|[x, y]| apply(affine, [origin[0] + x * scale, origin[1] - y * scale]));
285    let (mut left, mut top, mut right, mut bottom) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
286    for [x, y] in corners {
287        (left, top, right, bottom) = (left.min(x), top.min(y), right.max(x), bottom.max(y));
288    }
289    PixelBox {
290        left: left.floor() as i64,
291        top: top.floor() as i64,
292        right: right.ceil() as i64,
293        bottom: bottom.ceil() as i64,
294    }
295}
296
297/// Resamples an upright color glyph into `target` through `affine`, bilinear with 8-bit weights.
298#[allow(
299    clippy::cast_possible_truncation,
300    clippy::cast_possible_wrap,
301    clippy::cast_sign_loss,
302    clippy::cast_precision_loss,
303    clippy::many_single_char_names
304)]
305fn resample(rgba8: &[u8], source: PixelBox, target: PixelBox, inverse: Affine) -> Vec<u8> {
306    let source_width = usize::try_from(source.right - source.left).unwrap_or(0);
307    let source_height = usize::try_from(source.bottom - source.top).unwrap_or(0);
308    let width = usize::try_from(target.right - target.left).unwrap_or(0);
309    let height = usize::try_from(target.bottom - target.top).unwrap_or(0);
310    let mut out = vec![0_u8; width * height * 4];
311    let texel = |x: i64, y: i64| -> [u32; 4] {
312        if x < 0 || y < 0 || x >= source_width as i64 || y >= source_height as i64 {
313            return [0; 4];
314        }
315        let at = (y as usize * source_width + x as usize) * 4;
316        std::array::from_fn(|channel| u32::from(rgba8[at + channel]))
317    };
318    for row in 0..height {
319        for column in 0..width {
320            let [x, y] = apply(
321                inverse,
322                [
323                    (target.left + column as i64) as f32 + 0.5,
324                    (target.top + row as i64) as f32 + 0.5,
325                ],
326            );
327            // Texel space, with texel centres at whole numbers.
328            let (u, v) = (x - source.left as f32 - 0.5, y - source.top as f32 - 0.5);
329            let (u0, v0) = (u.floor(), v.floor());
330            let fx = ((u - u0) * 256.0) as u32;
331            let fy = ((v - v0) * 256.0) as u32;
332            let (u0, v0) = (u0 as i64, v0 as i64);
333            let [a, b, c, d] = [
334                texel(u0, v0),
335                texel(u0 + 1, v0),
336                texel(u0, v0 + 1),
337                texel(u0 + 1, v0 + 1),
338            ];
339            let at = (row * width + column) * 4;
340            for channel in 0..4 {
341                let top = a[channel] * (256 - fx) + b[channel] * fx;
342                let bottom = c[channel] * (256 - fx) + d[channel] * fx;
343                let value = (top * (256 - fy) + bottom * fy + 32_768) >> 16;
344                out[at + channel] = value.min(255) as u8;
345            }
346        }
347    }
348    out
349}
350
351fn mixed(color: [u8; 4], mix: &ColorMix) -> [u8; 4] {
352    let base = gradient::premultiplied_linear(color.map(|value| f32::from(value) / 255.0));
353    gradient::stored_rgba8(std::array::from_fn(|channel| {
354        base[channel] * mix.keep + mix.add[channel]
355    }))
356}
357
358/// Per-pixel color ownership: each pixel takes its colors from the cluster that gives it coverage.
359struct ClusterLook<'a> {
360    base: RequestLook<'a>,
361    states: &'a [ClusterState],
362    /// Owning cluster per pixel, `u32::MAX` for none.
363    fill_owner: Vec<u32>,
364    band_owner: Vec<Vec<u32>>,
365    /// The cluster whose color glyph was drawn last at each pixel.
366    color_owner: Vec<u32>,
367    solid: Vec<Option<[u8; 4]>>,
368    strokes: Vec<Vec<[u8; 4]>>,
369}
370
371impl PixelLook for ClusterLook<'_> {
372    fn fill(&self, index: usize, point: [f32; 2]) -> [u8; 4] {
373        let owner = self.fill_owner[index] as usize;
374        let Some(state) = self.states.get(owner) else {
375            return self.base.fill(index, point);
376        };
377        match (&self.base.gradient, state.fill) {
378            (Some(gradient), Some(mix)) => {
379                let base = gradient.linear_at(point);
380                gradient::stored_rgba8(std::array::from_fn(|channel| {
381                    base[channel] * mix.keep + mix.add[channel]
382                }))
383            }
384            (None, Some(_)) => self.solid[owner].unwrap_or(self.base.foreground),
385            (_, None) => self.base.fill(index, point),
386        }
387    }
388
389    fn band(&self, stroke: usize, index: usize) -> [u8; 4] {
390        let owner = self.band_owner[stroke][index] as usize;
391        self.strokes[stroke]
392            .get(owner)
393            .copied()
394            .unwrap_or_else(|| self.base.band(stroke, index))
395    }
396
397    /// Opacity of the owning cluster: fill, else color glyph, else innermost band.
398    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
399    fn fade(&self, index: usize) -> Option<u32> {
400        let owner = [self.fill_owner[index], self.color_owner[index]]
401            .into_iter()
402            .chain(self.band_owner.iter().map(|owners| owners[index]))
403            .find(|&owner| owner != u32::MAX)?;
404        let opacity = (self.states.get(owner as usize)?.opacity * 255.0).round() as u32;
405        (opacity < 255).then_some(opacity)
406    }
407}
408
409/// Writes `tile`'s coverage into `plane` where it is larger (ties go to the later cluster) and
410/// records `cluster` as the owner.
411fn place_max(
412    plane: (&mut [u8], &mut [u32], u32),
413    margin: [u32; 2],
414    tile: &Tile,
415    coverage: &[u8],
416    cluster: u32,
417) {
418    let (plane, owners, width) = plane;
419    let height = plane.len() / width.max(1) as usize;
420    for row in 0..tile.height {
421        let y = tile.top + i64::try_from(row).unwrap_or(0) + i64::from(margin[1]);
422        let Ok(y) = usize::try_from(y) else { continue };
423        if y >= height {
424            continue;
425        }
426        for column in 0..tile.width {
427            let value = coverage[row * tile.width + column];
428            if value == 0 {
429                continue;
430            }
431            let x = tile.left + i64::try_from(column).unwrap_or(0) + i64::from(margin[0]);
432            let Ok(x) = usize::try_from(x) else { continue };
433            if x >= width as usize {
434                continue;
435            }
436            let at = y * width as usize + x;
437            if value >= plane[at] {
438                plane[at] = value;
439                owners[at] = cluster;
440            }
441        }
442    }
443}
444
445impl TextRasterizer {
446    /// Lays out `request`'s clusters and lines as [`Self::rasterize`] would.
447    ///
448    /// # Errors
449    /// Returns the same errors as [`Self::rasterize`].
450    pub fn layout(&self, request: &TextRasterRequest) -> Result<TextLayout, TextRasterError> {
451        validate_request(request)?;
452        if self.fonts.is_empty() {
453            return Err(TextRasterError::NoFonts);
454        }
455        let faces = Faces::new(&self.fonts, request.font_weight)?;
456        let layout = self.lay_out(&faces, request)?;
457        Ok(text_layout(&layout, request))
458    }
459
460    /// [`Self::rasterize`] with one state per [`Self::layout`] cluster, in logical order. Identity
461    /// states match [`Self::rasterize`] exactly. Results are cached by content version, so give
462    /// each set of states its own version.
463    ///
464    /// # Errors
465    /// Returns [`TextRasterError::InvalidRequest`] when the state count or a value is out of
466    /// range, otherwise what [`Self::rasterize`] returns.
467    pub fn rasterize_animated(
468        &mut self,
469        request: &TextRasterRequest,
470        states: &[ClusterState],
471    ) -> Result<Arc<TextRasterResult>, TextRasterError> {
472        validate_request(request)?;
473        if self.fonts.is_empty() {
474            return Err(TextRasterError::NoFonts);
475        }
476        if !states.iter().all(ClusterState::is_valid) {
477            return Err(TextRasterError::InvalidRequest);
478        }
479        if states.iter().all(ClusterState::is_identity) {
480            let result = self.rasterize(request)?;
481            let clusters = self.layout(request)?.clusters.len();
482            return if states.len() == clusters {
483                Ok(result)
484            } else {
485                Err(TextRasterError::InvalidRequest)
486            };
487        }
488        if let Some(cached) = self.cache.get(request.content_version) {
489            return Ok(cached);
490        }
491        let mut tiles = std::mem::take(&mut self.tiles);
492        let result = self.rasterize_states(request, states, &mut tiles);
493        self.tiles = tiles;
494        let result = Arc::new(result?);
495        self.cache.insert(request.content_version, &result);
496        Ok(result)
497    }
498
499    #[allow(
500        clippy::cast_possible_truncation,
501        clippy::cast_possible_wrap,
502        clippy::cast_precision_loss,
503        clippy::cast_sign_loss,
504        clippy::too_many_lines
505    )]
506    fn rasterize_states(
507        &self,
508        request: &TextRasterRequest,
509        states: &[ClusterState],
510        cache: &mut TileCache,
511    ) -> Result<TextRasterResult, TextRasterError> {
512        let faces = Faces::new(&self.fonts, request.font_weight)?;
513        let layout = self.lay_out(&faces, request)?;
514        if states.len() != layout.clusters.len() {
515            return Err(TextRasterError::InvalidRequest);
516        }
517        let (box_width, box_height) = (layout.box_width, layout.box_height);
518        let ascent = layout.ascent;
519        let pivot_drop = request.font_size * 0.3;
520        let mut placed = vec![(Vec::<Placed>::new(), [0.0_f32; 6]); states.len()];
521        let mut drawings = HashMap::<(usize, u16), Drawing>::new();
522        let (mut ink_ascent, mut ink_descent) = (layout.ascent, layout.descent);
523        for (index, line) in layout.lines.iter().enumerate() {
524            let baseline = layout.line_top(index) + ascent;
525            // Tracking goes after each cluster except the line's last, in visual order.
526            let mut shifts = Vec::with_capacity(line.glyphs.len());
527            let mut shift = 0.0_f32;
528            let mut previous: Option<usize> = None;
529            for glyph in &line.glyphs {
530                if let Some(before) = previous.filter(|&before| before != glyph.cluster) {
531                    shift += states[before].tracking;
532                }
533                previous = Some(glyph.cluster);
534                shifts.push(shift);
535            }
536            let width = line.width + shift;
537            let offset = line_start(&layout, line.rtl, width, request);
538            // Pivot is the middle of the advance and of the em box.
539            let mut extents = HashMap::<usize, (f32, f32)>::new();
540            for (glyph, shift) in line.glyphs.iter().zip(&shifts) {
541                let left = glyph.x + shift + offset;
542                let extent = extents.entry(glyph.cluster).or_insert((left, left));
543                *extent = (extent.0.min(left), extent.1.max(left + glyph.advance));
544            }
545            for (glyph, shift) in line.glyphs.iter().zip(&shifts) {
546                let (left, right) = extents[&glyph.cluster];
547                let affine = cluster_affine(
548                    &states[glyph.cluster],
549                    [f32::midpoint(left, right), baseline - pivot_drop],
550                );
551                let drawing = drawings
552                    .entry((glyph.font, glyph.glyph_index))
553                    .or_insert_with(|| Drawing::of(&faces, glyph.font, glyph.glyph_index, request));
554                let Some(bounds) = drawing.bounds() else {
555                    continue;
556                };
557                let scale = request.font_size / faces.units_per_em(glyph.font);
558                ink_ascent = ink_ascent.max(bounds[3] * scale + glyph.y_offset);
559                ink_descent = ink_descent.max(-(bounds[1] * scale + glyph.y_offset));
560                let slot = &mut placed[glyph.cluster];
561                slot.1 = affine;
562                slot.0.push(Placed {
563                    key: (glyph.font, glyph.glyph_index),
564                    origin: [
565                        glyph.x + shift + glyph.x_offset + offset,
566                        baseline - glyph.y_offset,
567                    ],
568                    scale,
569                });
570            }
571        }
572        // Tiles depend only on the glyphs, the linear part of the map and the subpixel position,
573        // so they can be cached.
574        let look_key = look_key(request);
575        let mut tiles = Vec::with_capacity(states.len());
576        for (glyphs, affine) in &placed {
577            let Some(first) = glyphs.first() else {
578                tiles.push(None);
579                continue;
580            };
581            let [x, y] = apply(*affine, first.origin);
582            let whole = [x.floor(), y.floor()];
583            let local = [
584                affine[0],
585                affine[1],
586                affine[2],
587                affine[3],
588                affine[4] - whole[0],
589                affine[5] - whole[1],
590            ];
591            let key = cluster_key(look_key, glyphs, local);
592            let drawn = if let Some(drawn) = cache.get(key) {
593                drawn
594            } else {
595                let drawn = Arc::new(draw_cluster(request, &drawings, glyphs, local));
596                cache.insert(key, &drawn);
597                drawn
598            };
599            tiles.push(Some((drawn, [whole[0] as i64, whole[1] as i64])));
600        }
601        // Same symmetric margin as the static raster, with room for strokes.
602        let (outline, padding) = decoration_extents(request);
603        let (mut overflow_x, mut overflow_y) = (0_i64, 0_i64);
604        for bounds in tiles
605            .iter()
606            .flatten()
607            .filter_map(|(drawn, at)| drawn.tile.bounds().map(|bounds| bounds.moved(*at)))
608        {
609            overflow_x = overflow_x
610                .max(bounds.left.saturating_neg())
611                .max(bounds.right.saturating_sub(i64::from(box_width)));
612            overflow_y = overflow_y
613                .max(bounds.top.saturating_neg())
614                .max(bounds.bottom.saturating_sub(i64::from(box_height)));
615        }
616        let margin = |overflow: i64| -> Result<u32, TextRasterError> {
617            u32::try_from(overflow)
618                .map_err(|_| TextRasterError::TooLarge)?
619                .checked_add(outline)
620                .map(|margin| margin.max(outline.max(padding)))
621                .ok_or(TextRasterError::TooLarge)
622        };
623        let (margin_x, margin_y) = (margin(overflow_x)?, margin(overflow_y)?);
624        let width = margin_x
625            .checked_mul(2)
626            .and_then(|margins| box_width.checked_add(margins))
627            .ok_or(TextRasterError::TooLarge)?;
628        let height = margin_y
629            .checked_mul(2)
630            .and_then(|margins| box_height.checked_add(margins))
631            .ok_or(TextRasterError::TooLarge)?;
632        if width > 16_384 || height > 16_384 {
633            return Err(TextRasterError::TooLarge);
634        }
635        let pixel_count = width as usize * height as usize;
636        let margins = [margin_x, margin_y];
637        let mut coverage = vec![0_u8; pixel_count];
638        let mut fill_owner = vec![u32::MAX; pixel_count];
639        let mut color_owner = vec![u32::MAX; pixel_count];
640        let mut colors: Option<Vec<u8>> = None;
641        let mut bands = vec![vec![0_u8; pixel_count]; request.strokes.len()];
642        let mut band_owner = vec![vec![u32::MAX; pixel_count]; request.strokes.len()];
643        for (cluster, (drawn, at)) in tiles
644            .iter()
645            .enumerate()
646            .filter_map(|(cluster, tile)| tile.as_ref().map(|tile| (cluster, tile)))
647        {
648            let cluster = u32::try_from(cluster).map_err(|_| TextRasterError::TooLarge)?;
649            let tile = drawn.tile.moved(*at);
650            place_max(
651                (&mut coverage, &mut fill_owner, width),
652                margins,
653                &tile,
654                &drawn.tile.coverage,
655                cluster,
656            );
657            if let Some(held) = &drawn.tile.colors {
658                let plane = colors.get_or_insert_with(|| vec![0; pixel_count * 4]);
659                for row in 0..tile.height {
660                    for column in 0..tile.width {
661                        let source = &held[(row * tile.width + column) * 4..][..4];
662                        if source[3] == 0 {
663                            continue;
664                        }
665                        let x = tile.left + column as i64 + i64::from(margin_x);
666                        let y = tile.top + row as i64 + i64::from(margin_y);
667                        if x < 0 || y < 0 || x >= i64::from(width) || y >= i64::from(height) {
668                            continue;
669                        }
670                        let pixel = y as usize * width as usize + x as usize;
671                        color_owner[pixel] = cluster;
672                        let at = pixel * 4;
673                        let slot = &mut plane[at..at + 4];
674                        let blended = over(
675                            std::array::from_fn(|channel| u32::from(source[channel])),
676                            std::array::from_fn(|channel| u32::from(slot[channel])),
677                        );
678                        for (byte, value) in slot.iter_mut().zip(blended) {
679                            *byte = u8::try_from(value.min(255)).unwrap_or(u8::MAX);
680                        }
681                    }
682                }
683            }
684            let padded = drawn.padded.moved(*at);
685            for ((band, owners), dilated) in bands
686                .iter_mut()
687                .zip(band_owner.iter_mut())
688                .zip(&drawn.bands)
689            {
690                place_max((band, owners, width), margins, &padded, dilated, cluster);
691            }
692        }
693        let mut line_boxes = Vec::with_capacity(layout.lines.len());
694        for (index, line) in layout.lines.iter().enumerate() {
695            if line.width > 0.0 {
696                line_boxes.push((
697                    layout.line_offset(line, request.align),
698                    line.width,
699                    layout.line_top(index),
700                ));
701            }
702        }
703        let base = RequestLook::of(request, [layout.width, layout.height]);
704        let solid = match &request.fill {
705            TextFill::Solid(color) => states
706                .iter()
707                .map(|state| state.fill.map(|mix| mixed(*color, &mix)))
708                .collect(),
709            TextFill::Gradient(_) => vec![None; states.len()],
710        };
711        let strokes = request
712            .strokes
713            .iter()
714            .map(|stroke| {
715                states
716                    .iter()
717                    .map(|state| {
718                        state
719                            .stroke
720                            .map_or(stroke.color, |mix| mixed(stroke.color, &mix))
721                    })
722                    .collect()
723            })
724            .collect();
725        let look = ClusterLook {
726            base,
727            states,
728            fill_owner,
729            band_owner,
730            color_owner,
731            solid,
732            strokes,
733        };
734        let (rgba8, background) = super::compose(
735            request,
736            &coverage,
737            colors.as_deref(),
738            width,
739            margins,
740            &line_boxes,
741            &bands,
742            &look,
743        );
744        let texture = HostTexture {
745            content_version: request.content_version,
746            width,
747            height,
748            premultiplication: Premultiplication::Premultiplied,
749            color_space: RasterColorSpace::Srgb,
750        };
751        Ok(TextRasterResult {
752            descriptor: TextTexture {
753                texture: texture.clone(),
754                layout_x: margin_x as f32,
755                layout_y: margin_y as f32,
756                layout_width: layout.width,
757                layout_height: layout.height,
758                first_baseline: layout.half_leading + layout.ascent,
759                ascent: ink_ascent,
760                descent: ink_descent,
761                line_count: layout.line_count,
762            },
763            pixels: HostTexturePixels { texture, rgba8 },
764            background,
765        })
766    }
767}
768
769/// A glyph with its origin before the cluster's map and its pixels per font unit.
770#[derive(Clone, Debug)]
771struct Placed {
772    key: (usize, u16),
773    origin: [f32; 2],
774    scale: f32,
775}
776
777/// A cluster's tile and stroke bands, in a frame offset by whole pixels when placed.
778struct DrawnCluster {
779    tile: Tile,
780    padded: Tile,
781    bands: Vec<Vec<u8>>,
782}
783
784impl Tile {
785    fn moved(&self, [x, y]: [i64; 2]) -> Self {
786        Self {
787            left: self.left + x,
788            top: self.top + y,
789            width: self.width,
790            height: self.height,
791            coverage: Vec::new(),
792            colors: None,
793        }
794    }
795}
796
797impl PixelBox {
798    fn moved(self, [x, y]: [i64; 2]) -> Self {
799        Self {
800            left: self.left + x,
801            top: self.top + y,
802            right: self.right + x,
803            bottom: self.bottom + y,
804        }
805    }
806}
807
808/// LRU cache of drawn clusters, so held or settled characters are drawn once.
809#[derive(Debug, Default)]
810pub(crate) struct TileCache {
811    entries: HashMap<u64, (Arc<DrawnCluster>, u64)>,
812    next_use: u64,
813}
814
815impl std::fmt::Debug for DrawnCluster {
816    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
817        formatter
818            .debug_struct("DrawnCluster")
819            .field("width", &self.tile.width)
820            .field("height", &self.tile.height)
821            .finish_non_exhaustive()
822    }
823}
824
825const MAX_CACHED_CLUSTERS: usize = 4_096;
826
827impl TileCache {
828    fn get(&mut self, key: u64) -> Option<Arc<DrawnCluster>> {
829        self.next_use += 1;
830        let use_now = self.next_use;
831        self.entries.get_mut(&key).map(|(drawn, used)| {
832            *used = use_now;
833            Arc::clone(drawn)
834        })
835    }
836
837    fn insert(&mut self, key: u64, drawn: &Arc<DrawnCluster>) {
838        if self.entries.len() >= MAX_CACHED_CLUSTERS {
839            // Drop the older half so eviction stays rare.
840            let mut uses = self
841                .entries
842                .values()
843                .map(|(_, used)| *used)
844                .collect::<Vec<_>>();
845            uses.sort_unstable();
846            let cut = uses[uses.len() / 2];
847            self.entries.retain(|_, (_, used)| *used > cut);
848        }
849        self.entries.insert(key, (Arc::clone(drawn), self.next_use));
850    }
851
852    pub(crate) fn clear(&mut self) {
853        self.entries.clear();
854    }
855}
856
857/// FNV-1a over the request fields that affect cluster drawing besides glyphs and maps.
858fn look_key(request: &TextRasterRequest) -> u64 {
859    let mut hash = Fnv::default();
860    hash.feed(&request.font_weight.to_le_bytes());
861    hash.feed(&[request.emoji_policy as u8]);
862    hash.feed(&request.fill.foreground());
863    for stroke in &request.strokes {
864        hash.feed(&stroke.width.to_bits().to_le_bytes());
865    }
866    hash.0
867}
868
869fn cluster_key(look: u64, glyphs: &[Placed], affine: Affine) -> u64 {
870    let mut hash = Fnv(look);
871    let first = glyphs.first().map_or([0.0; 2], |glyph| glyph.origin);
872    for glyph in glyphs {
873        hash.feed(&glyph.key.0.to_le_bytes());
874        hash.feed(&glyph.key.1.to_le_bytes());
875        hash.feed(&glyph.scale.to_bits().to_le_bytes());
876        for value in [glyph.origin[0] - first[0], glyph.origin[1] - first[1]] {
877            hash.feed(&value.to_bits().to_le_bytes());
878        }
879    }
880    // Key the map relative to the first glyph, so a moved cluster hits the cache.
881    let [x, y] = apply(affine, first);
882    for value in [affine[0], affine[1], affine[2], affine[3], x, y] {
883        hash.feed(&value.to_bits().to_le_bytes());
884    }
885    hash.0
886}
887
888struct Fnv(u64);
889
890impl Default for Fnv {
891    fn default() -> Self {
892        Self(0xcbf2_9ce4_8422_2325)
893    }
894}
895
896impl Fnv {
897    fn feed(&mut self, bytes: &[u8]) {
898        for byte in bytes {
899            self.0 = (self.0 ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
900        }
901    }
902}
903
904#[allow(
905    clippy::cast_possible_truncation,
906    clippy::cast_possible_wrap,
907    clippy::cast_precision_loss,
908    clippy::cast_sign_loss,
909    clippy::too_many_lines
910)]
911fn draw_cluster(
912    request: &TextRasterRequest,
913    drawings: &HashMap<(usize, u16), Drawing>,
914    glyphs: &[Placed],
915    affine: Affine,
916) -> DrawnCluster {
917    let mut tile = Tile::empty();
918    for glyph in glyphs {
919        let drawing = &drawings[&glyph.key];
920        let Some(bounds) = drawing.bounds() else {
921            continue;
922        };
923        let (origin, scale) = (glyph.origin, glyph.scale);
924        let target = mapped_box(bounds, origin, scale, affine);
925        if target.right <= target.left || target.bottom <= target.top {
926            continue;
927        }
928        tile.grow(target);
929        let width = usize::try_from(target.right - target.left).unwrap_or(0);
930        match drawing {
931            Drawing::Outline(outline) => {
932                let height = usize::try_from(target.bottom - target.top).unwrap_or(0);
933                let (left, top) = (target.left as f32, target.top as f32);
934                let raster = outline.raster(width, height, |[x, y]| {
935                    let [x, y] = apply(affine, [origin[0] + x * scale, origin[1] - y * scale]);
936                    point(x - left, y - top)
937                });
938                raster.for_each_pixel(|at, coverage| {
939                    let value = (coverage.min(1.0) * 255.0).round() as u8;
940                    let x = target.left + (at % width) as i64;
941                    let y = target.top + (at / width) as i64;
942                    if let Some(slot) = tile.index(x, y) {
943                        tile.coverage[slot] = tile.coverage[slot].max(value);
944                    }
945                });
946            }
947            drawing => {
948                let Some(inverse) = invert(affine) else {
949                    continue;
950                };
951                let upright = PixelBox::of(bounds, origin, scale);
952                let rgba8 = resample(
953                    &drawing.draw_color(origin, scale, upright),
954                    upright,
955                    target,
956                    inverse,
957                );
958                let size = tile.width * tile.height * 4;
959                let slots = (0..rgba8.len() / 4)
960                    .map(|at| {
961                        tile.index(
962                            target.left + (at % width) as i64,
963                            target.top + (at / width) as i64,
964                        )
965                    })
966                    .collect::<Vec<_>>();
967                let colors = tile.colors.get_or_insert_with(|| vec![0; size]);
968                for (source, slot) in rgba8.as_chunks::<4>().0.iter().zip(slots) {
969                    let Some(slot) = slot.filter(|_| source[3] > 0) else {
970                        continue;
971                    };
972                    let held = &mut colors[slot * 4..slot * 4 + 4];
973                    let blended = over(
974                        source.map(u32::from),
975                        std::array::from_fn(|channel| u32::from(held[channel])),
976                    );
977                    for (byte, value) in held.iter_mut().zip(blended) {
978                        *byte = u8::try_from(value.min(255)).unwrap_or(u8::MAX);
979                    }
980                }
981            }
982        }
983    }
984    // Dilate the ink, including color glyph alpha, band by band.
985    let reach = stroke_radius(&request.strokes).map_or(0, disc_extent) as usize;
986    let padded = Tile {
987        left: tile.left - reach as i64,
988        top: tile.top - reach as i64,
989        width: tile.width + 2 * reach,
990        height: tile.height + 2 * reach,
991        coverage: Vec::new(),
992        colors: None,
993    };
994    let mut bands = Vec::with_capacity(request.strokes.len());
995    if !request.strokes.is_empty() && tile.width > 0 && tile.height > 0 {
996        let mut ink = vec![0_u8; padded.width * padded.height];
997        for row in 0..tile.height {
998            for column in 0..tile.width {
999                let at = row * tile.width + column;
1000                let alpha = tile.colors.as_ref().map_or(0, |held| held[at * 4 + 3]);
1001                ink[(row + reach) * padded.width + column + reach] = tile.coverage[at].max(alpha);
1002            }
1003        }
1004        let mut radius = 0.0;
1005        for stroke in &request.strokes {
1006            radius += stroke.width;
1007            bands.push(dilate(
1008                &ink,
1009                u32::try_from(padded.width).unwrap_or(u32::MAX),
1010                u32::try_from(padded.height).unwrap_or(u32::MAX),
1011                radius,
1012            ));
1013        }
1014    }
1015    DrawnCluster {
1016        tile,
1017        padded,
1018        bands,
1019    }
1020}
1021
1022/// Line start in the box, as [`Layout::line_offset`] computes it.
1023fn line_start(layout: &Layout, rtl: bool, width: f32, request: &TextRasterRequest) -> f32 {
1024    use alpha2_media::TextAlign;
1025    match (request.align, rtl) {
1026        (TextAlign::Center, _) => (layout.width - width) * 0.5,
1027        (TextAlign::Start, false) | (TextAlign::End, true) => 0.0,
1028        (TextAlign::End, false) | (TextAlign::Start, true) => layout.width - width,
1029    }
1030}
1031
1032fn text_layout(layout: &Layout, request: &TextRasterRequest) -> TextLayout {
1033    let mut map = Vec::new();
1034    normalize_mapped(&request.text, request.emoji_policy, Some(&mut map));
1035    let original = |at: usize| map.get(at).copied().unwrap_or(request.text.len());
1036    let words = request
1037        .text
1038        .split_word_bound_indices()
1039        .filter(|(_, word)| word.chars().any(char::is_alphanumeric))
1040        .map(|(start, word)| start..start + word.len())
1041        .collect::<Vec<_>>();
1042    let mut clusters = layout
1043        .clusters
1044        .iter()
1045        .map(|span| {
1046            let bytes = original(span.bytes.start)..original(span.bytes.end);
1047            let text = &request.text[bytes.clone()];
1048            let word = words
1049                .partition_point(|word| word.end <= bytes.start)
1050                .try_into()
1051                .ok()
1052                .filter(|&index: &u32| {
1053                    words
1054                        .get(index as usize)
1055                        .is_some_and(|word| word.contains(&bytes.start))
1056                });
1057            ClusterInfo {
1058                is_space: !text.is_empty() && text.chars().all(char::is_whitespace),
1059                bytes,
1060                line: u32::try_from(span.line).unwrap_or(u32::MAX),
1061                word,
1062                x: 0.0,
1063                advance: 0.0,
1064                baseline: 0.0,
1065            }
1066        })
1067        .collect::<Vec<_>>();
1068    let mut lines = Vec::with_capacity(layout.lines.len());
1069    let mut first_cluster = 0;
1070    for (index, line) in layout.lines.iter().enumerate() {
1071        let left = layout.line_offset(line, request.align);
1072        let baseline = layout.line_top(index) + layout.ascent;
1073        let line_number = u32::try_from(index).unwrap_or(u32::MAX);
1074        let last_cluster = clusters[first_cluster..]
1075            .iter()
1076            .position(|cluster| cluster.line != line_number)
1077            .map_or(clusters.len(), |count| first_cluster + count);
1078        let mut extents = HashMap::<usize, (f32, f32)>::new();
1079        for glyph in &line.glyphs {
1080            let start = glyph.x + left;
1081            let extent = extents.entry(glyph.cluster).or_insert((start, start));
1082            *extent = (extent.0.min(start), extent.1.max(start + glyph.advance));
1083        }
1084        for (cluster, info) in clusters[first_cluster..last_cluster].iter_mut().enumerate() {
1085            let (start, end) = extents
1086                .get(&(first_cluster + cluster))
1087                .copied()
1088                .unwrap_or((left + line.width, left + line.width));
1089            info.x = start;
1090            info.advance = end - start;
1091            info.baseline = baseline;
1092        }
1093        lines.push(LineInfo {
1094            clusters: first_cluster..last_cluster,
1095            left,
1096            width: line.width,
1097            baseline,
1098        });
1099        first_cluster = last_cluster;
1100    }
1101    TextLayout {
1102        clusters,
1103        lines,
1104        font_size: request.font_size,
1105    }
1106}