Skip to main content

media_pp/elements/source/compositor/
text_layer.rs

1//! [`TextLayer`] — backend-agnostic construction-time settings for a
2//! dynamic text layer, the text sibling of [`super::video_layer::VideoLayer`]
3//! — plus the glyph rasterization both backends that draw one share.
4//!
5//! Rasterizing is backend-agnostic by nature: `ab_glyph` turns a font and a
6//! string into per-pixel coverage, and what differs is only what each
7//! backend does with that coverage. D3D11 expands it into a straight-alpha
8//! BGRA texture for its blend state; CUDA hands it to a blend kernel as a
9//! mask with the color as a scalar.
10
11use crate::color::Color;
12
13/// A rasterized string: tightly packed per-pixel coverage, one byte each.
14///
15/// Coverage, not color: `TextLayer::color` is uniform over the whole layer,
16/// so carrying it per pixel would be three redundant bytes each. Each
17/// backend combines the two in whatever form it draws with.
18#[cfg(any(feature = "cuda", all(target_os = "windows", feature = "d3d11")))]
19pub(crate) struct TextMask {
20    pub(crate) width: u32,
21    pub(crate) height: u32,
22    /// `width * height` bytes, row-major, 0 = untouched by any glyph.
23    pub(crate) coverage: Vec<u8>,
24}
25
26/// Errors from rasterizing, which each backend maps into its own text-layer
27/// error type so a caller matching on one sees only that backend's enum.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[cfg(any(feature = "cuda", all(target_os = "windows", feature = "d3d11")))]
30pub(crate) enum TextRasterError {
31    TooLarge { width: u64, height: u64 },
32    AllocationFailed { bytes: usize },
33}
34
35/// A rasterized string this crate refuses to allocate for — a guard against
36/// a pathological font size turning one `set_text` into gigabytes.
37#[cfg(any(feature = "cuda", all(target_os = "windows", feature = "d3d11")))]
38pub(crate) const MAX_TEXT_PIXELS: usize = 16 * 1024 * 1024;
39
40/// Rasterizes `text` at `size_px` (pixel height) into tightly-bounding
41/// coverage. `None` for text with no drawable glyphs (empty, all-whitespace,
42/// or all-control).
43#[cfg(any(feature = "cuda", all(target_os = "windows", feature = "d3d11")))]
44pub(crate) fn rasterize_coverage(
45    font: &ab_glyph::FontArc,
46    size_px: f32,
47    text: &str,
48) -> Result<Option<TextMask>, TextRasterError> {
49    use ab_glyph::{Font, PxScale, ScaleFont, point};
50
51    use super::video_layer::MAX_DIMENSION;
52
53    let scaled = font.as_scaled(PxScale::from(size_px));
54    let mut glyphs = Vec::new();
55    let mut caret = point(0.0, scaled.ascent());
56    let mut last_id = None;
57    for c in text.chars() {
58        if c.is_control() {
59            continue;
60        }
61        let mut glyph = scaled.scaled_glyph(c);
62        if let Some(last_id) = last_id {
63            caret.x += scaled.kern(last_id, glyph.id);
64        }
65        glyph.position = caret;
66        caret.x += scaled.h_advance(glyph.id);
67        last_id = Some(glyph.id);
68        glyphs.push(glyph);
69    }
70    if glyphs.is_empty() {
71        return Ok(None);
72    }
73
74    let outlined: Vec<_> = glyphs
75        .into_iter()
76        .filter_map(|glyph| font.outline_glyph(glyph))
77        .collect();
78    if outlined.is_empty() {
79        return Ok(None);
80    }
81
82    let width_f = caret.x.ceil();
83    let height_f = scaled.height().ceil();
84    if !width_f.is_finite()
85        || !height_f.is_finite()
86        || width_f > MAX_DIMENSION as f32
87        || height_f > MAX_DIMENSION as f32
88    {
89        return Err(TextRasterError::TooLarge {
90            width: width_f.max(0.0) as u64,
91            height: height_f.max(0.0) as u64,
92        });
93    }
94    let width = width_f.max(1.0) as u32;
95    let height = height_f.max(1.0) as u32;
96    let pixel_count = (width as usize)
97        .checked_mul(height as usize)
98        .filter(|&count| count <= MAX_TEXT_PIXELS)
99        .ok_or(TextRasterError::TooLarge {
100            width: width.into(),
101            height: height.into(),
102        })?;
103    let mut coverage = Vec::new();
104    coverage
105        .try_reserve_exact(pixel_count)
106        .map_err(|_| TextRasterError::AllocationFailed { bytes: pixel_count })?;
107    coverage.resize(pixel_count, 0u8);
108    for outlined in outlined {
109        let bounds = outlined.px_bounds();
110        outlined.draw(|gx, gy, value| {
111            let px = bounds.min.x as i32 + gx as i32;
112            let py = bounds.min.y as i32 + gy as i32;
113            if px < 0 || py < 0 || px as u32 >= width || py as u32 >= height {
114                return;
115            }
116            let index = (py as u32 * width + px as u32) as usize;
117            let alpha = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
118            // Glyphs can overlap (kerning, accents); the strongest coverage
119            // wins rather than the last one drawn.
120            coverage[index] = coverage[index].max(alpha);
121        });
122    }
123    Ok(Some(TextMask {
124        width,
125        height,
126        coverage,
127    }))
128}
129
130/// Construction-time settings for one text layer, passed to
131/// `D3d11VideoCompositorHandle::add_text_layer` — the
132/// text sibling of [`super::video_layer::VideoLayer`], which `add_source`
133/// takes the same way. `font_data` (raw TTF/OTF bytes; this crate bundles
134/// no font of its own) has no sane default, so — mirroring
135/// [`super::video_layer::VideoLayer::new`], which takes the one field a
136/// caller must supply (`rect`) and defaults the rest — [`Self::new`] takes
137/// only `font_data` and defaults `font_size`/`color`/`x`/`y`, all freely
138/// reassignable before the call to `add_text_layer`.
139#[derive(Debug, Clone)]
140pub struct TextLayer {
141    /// Raw TrueType or OpenType font bytes owned by the layer.
142    pub font_data: Vec<u8>,
143    /// Pixel height of rendered glyphs (not a point size).
144    pub font_size: f32,
145    /// Initial glyph color, including alpha.
146    pub color: Color,
147    /// Initial top-left corner of the layer — like `VideoLayer::rect`'s
148    /// `x`/`y`, but with no `width`/`height` counterpart, since a text
149    /// layer's size is only known once something has actually rasterized
150    /// its content (e.g. `D3d11TextLayerHandle::set_text`).
151    pub x: i32,
152    /// Initial vertical offset of the layer's top edge, in output pixels.
153    pub y: i32,
154}
155
156impl TextLayer {
157    /// Creates a text layer with the supplied font bytes and default visual settings.
158    pub const fn new(font_data: Vec<u8>) -> Self {
159        Self {
160            font_data,
161            font_size: 32.0,
162            color: Color::WHITE,
163            x: 0,
164            y: 0,
165        }
166    }
167}