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}