Skip to main content

alpha2_engine/
text.rs

1//! Text rendering: caption tracks' cues and text generators, rasterized by
2//! the portable text host.
3//!
4//! The engine draws text itself, from font bytes its host registers once,
5//! so a browser preview and a native export of one project, given the same
6//! fonts, draw the same text pixels. A host keeps one [`TextTextures`]
7//! beside its frame provider, calls [`TextTextures::prepare`] for each frame
8//! before rendering it, and answers [`crate::FrameProvider::text`] from it.
9//! A caption is composited as a layer between tracks; a text generator's
10//! texture is placed on the transparent picture the generator draws. A
11//! layout generator is resolved here too, with the same fonts, and its
12//! texts rasterized beside the others ([`crate::layout`]).
13
14use std::collections::HashMap;
15use std::fmt;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18
19use alpha2_eval::{FrameDesc, GeneratorPlan, LayoutPlan, StepOp};
20use alpha2_media::EmojiPolicy;
21use alpha2_model::{Generator, Rgba, TextAlign, TextStyle};
22use alpha2_render::{RenderCacheKey, RenderError, RenderSize, Renderer, SourceView, UploadTexture};
23use alpha2_text::{ShapedText, TextFont, TextRasterError, TextRasterRequest, TextRasterizer};
24
25use crate::layout::PreparedLayout;
26
27/// The high 64 bits of a text's result cache key, before its font set is
28/// mixed in: font sets are counted from 1 and stay far below this, so those
29/// bits are never zero, as a host's source keys ([`RenderCacheKey::source`])
30/// leave them, and the two never meet.
31const TEXT_CACHE_DOMAIN: u64 = 0x6361_7074_696f_6e73;
32
33/// The next font set's identity: each store, and each font a store adds,
34/// takes one, so no two font sets that might draw text differently in one
35/// process share a text cache key.
36static NEXT_FONT_SET: AtomicU64 = AtomicU64::new(1);
37
38/// How many rasterized texts a host keeps between frames; the current
39/// frame's are kept however many it shows.
40const RETAINED_TEXTS: usize = 16;
41
42/// How many resolved layouts a host keeps between frames; the current
43/// frame's are kept however many it shows.
44const RETAINED_LAYOUTS: usize = 8;
45
46/// How many shaped texts a host keeps for layout, least recently used
47/// first out.
48const RETAINED_SHAPED: usize = 256;
49
50/// The largest font, outline, and padding the text host draws, in pixels.
51pub(crate) const MAX_FONT_PIXELS: f32 = 2_048.0;
52pub(crate) const MAX_OUTLINE_PIXELS: f32 = 64.0;
53const MAX_PADDING_PIXELS: f32 = 256.0;
54
55/// One prepared text: its texture, pixel size, and where its layout box
56/// lies in it.
57#[derive(Clone, Copy, Debug)]
58pub struct TextTexture<'a> {
59    /// Premultiplied, sRGB-encoded RGBA, as every source texture.
60    pub texture: SourceView<'a>,
61    /// Width in pixels.
62    pub width: u32,
63    /// Height in pixels.
64    pub height: u32,
65    /// The margin, in pixels, the same on both sides of each axis, that the
66    /// texture adds around the text and its decorations for ink reaching
67    /// past them: an accent above the first line, say. Placement leaves it
68    /// out, so such ink never moves the text.
69    pub margin: [u32; 2],
70    /// The key the renderer's result cache holds results drawn from this
71    /// texture under, or `None` to keep every frame showing it out of that
72    /// cache. It is the provider's promise that the texture's pixels depend
73    /// on nothing it leaves out, as [`crate::FrameProvider::source_key`] is
74    /// for a source: [`TextTextures`] keys each text by its [`text_key`] and
75    /// the fonts it was drawn with.
76    pub cache_key: Option<RenderCacheKey>,
77}
78
79/// Where a text lies, as fractions of the canvas, `(0, 0)` top-left to
80/// `(1, 1)` bottom-right, as its style's position is: on the picture a text
81/// generator draws, before its clip's transform and effects, and on the
82/// canvas for a caption. [`measure_text`] measures it.
83#[derive(Copy, Clone, Debug, PartialEq)]
84pub struct TextBounds {
85    /// The box the style's anchor and position place: the wrapping width
86    /// wide, every line's em box tall, and the whole pixels of outline and
87    /// background padding around that.
88    pub layout: CanvasRect,
89    /// The lines' advances: from the start of the line that starts farthest
90    /// left to the end of the one that ends farthest right, as tall as the
91    /// layout, with the same decorations around it. It moves only as the
92    /// advances do, not as glyphs with more or less ink are typed.
93    pub content: CanvasRect,
94    /// What the text draws: its glyphs, their outline, and the background's
95    /// boxes, as [`alpha2_text::TextMeasure::ink`] bounds them at the measured
96    /// size. Every pixel the text gives any alpha there meets it, give or
97    /// take the pixel sampling the texture spreads it by. `None` when the text
98    /// draws nothing.
99    pub ink: Option<CanvasRect>,
100    /// Number of laid-out lines.
101    pub line_count: u32,
102}
103
104/// A rectangle as fractions of the canvas, y down.
105#[derive(Copy, Clone, Debug, PartialEq)]
106pub struct CanvasRect {
107    /// Left edge, a fraction of the canvas width.
108    pub left: f32,
109    /// Top edge, a fraction of the canvas height.
110    pub top: f32,
111    /// Right edge, a fraction of the canvas width.
112    pub right: f32,
113    /// Bottom edge, a fraction of the canvas height.
114    pub bottom: f32,
115}
116
117/// Why text could not be prepared.
118#[derive(Clone, Debug, Eq, PartialEq)]
119#[non_exhaustive]
120pub enum TextError {
121    /// The text host refused: no fonts registered, a malformed font, or a
122    /// text too large to rasterize.
123    Raster(TextRasterError),
124    /// The rasterized text could not be uploaded.
125    Render(RenderError),
126}
127
128impl fmt::Display for TextError {
129    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::Raster(TextRasterError::NoFonts) => {
132                formatter.write_str("the project shows text, but no fonts were registered")
133            }
134            Self::Raster(error) => write!(formatter, "text failed: {error}"),
135            Self::Render(error) => write!(formatter, "text upload failed: {error}"),
136        }
137    }
138}
139
140impl std::error::Error for TextError {}
141
142struct TextEntry {
143    texture: UploadTexture,
144    width: u32,
145    height: u32,
146    margin: [u32; 2],
147    /// Where the layout box's top-left corner lies in the texture.
148    corner: [u32; 2],
149    last_used: u64,
150}
151
152/// A layout resolved at one size, with the requests of its texts, to
153/// rasterize again one the text store has let go.
154struct LayoutEntry {
155    prepared: Arc<PreparedLayout>,
156    requests: Vec<(u64, TextRasterRequest)>,
157    last_used: u64,
158}
159
160/// A text shaped for layout, and when it was last asked for.
161struct ShapedEntry {
162    shaped: Arc<ShapedText>,
163    last_used: u64,
164}
165
166/// Fonts and the texts rasterized with them, keyed by content and render
167/// size.
168pub struct TextTextures {
169    rasterizer: TextRasterizer,
170    /// The identity of the fonts the texts are drawn with, from
171    /// [`NEXT_FONT_SET`].
172    font_set: u64,
173    entries: HashMap<u64, TextEntry>,
174    /// Resolved layouts by [`layout_key`].
175    layouts: HashMap<u128, LayoutEntry>,
176    /// Texts shaped for layout, by [`shaped_key`].
177    shaped: HashMap<u64, ShapedEntry>,
178    /// How many layouts have been resolved: a static layout resolves once.
179    resolves: u64,
180    frame: u64,
181}
182
183impl fmt::Debug for TextTextures {
184    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185        formatter
186            .debug_struct("TextTextures")
187            .field("fonts", &self.rasterizer.font_count())
188            .field("texts", &self.entries.len())
189            .field("layouts", &self.layouts.len())
190            .finish_non_exhaustive()
191    }
192}
193
194impl Default for TextTextures {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200impl TextTextures {
201    /// No fonts and no texts.
202    #[must_use]
203    pub fn new() -> Self {
204        Self {
205            rasterizer: TextRasterizer::empty(),
206            font_set: NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed),
207            entries: HashMap::new(),
208            layouts: HashMap::new(),
209            shaped: HashMap::new(),
210            resolves: 0,
211            frame: 0,
212        }
213    }
214
215    /// Appends an OpenType font to the fallback stack text draws with. Texts
216    /// already rasterized are dropped.
217    ///
218    /// # Errors
219    /// Returns [`TextError::Raster`] for malformed font bytes.
220    pub fn add_font(
221        &mut self,
222        family: impl Into<String>,
223        bytes: impl Into<Arc<[u8]>>,
224        face_index: u32,
225    ) -> Result<(), TextError> {
226        self.rasterizer
227            .add_font(TextFont {
228                family: family.into(),
229                bytes: bytes.into(),
230                face_index,
231            })
232            .map_err(TextError::Raster)?;
233        self.entries.clear();
234        self.layouts.clear();
235        self.shaped.clear();
236        // The same text may now draw other glyphs.
237        self.font_set = NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed);
238        Ok(())
239    }
240
241    /// Fonts in the fallback stack.
242    #[must_use]
243    pub fn font_count(&self) -> usize {
244        self.rasterizer.font_count()
245    }
246
247    /// Rasterizes and uploads every text `frame` shows at `size` that is not
248    /// already held: its captions and the text generators its clips draw.
249    /// Resolves every layout it draws that is not already held, and
250    /// rasterizes its texts. Forgets the least recently shown beyond a small
251    /// number.
252    ///
253    /// # Errors
254    /// Returns [`TextError`] when a text cannot be rasterized or uploaded;
255    /// with no fonts registered, only if the frame shows one.
256    pub fn prepare(
257        &mut self,
258        renderer: &Renderer,
259        frame: &FrameDesc,
260        size: RenderSize,
261    ) -> Result<(), TextError> {
262        self.frame += 1;
263        for (text, style) in frame_texts(frame) {
264            let key = text_key(text, style, size);
265            if let Some(entry) = self.entries.get_mut(&key) {
266                entry.last_used = self.frame;
267                continue;
268            }
269            let request = text_request(text, style, size, key);
270            self.rasterize(renderer, key, &request)?;
271        }
272        for (plan, time) in frame_layouts(frame) {
273            self.prepare_layout(renderer, plan, time, size)?;
274        }
275        while self.layouts.len() > RETAINED_LAYOUTS {
276            let Some(oldest) = self
277                .layouts
278                .iter()
279                .filter(|(_, entry)| entry.last_used < self.frame)
280                .min_by_key(|(_, entry)| entry.last_used)
281                .map(|(key, _)| *key)
282            else {
283                break;
284            };
285            self.layouts.remove(&oldest);
286        }
287        while self.entries.len() > RETAINED_TEXTS {
288            let Some(oldest) = self
289                .entries
290                .iter()
291                .filter(|(_, entry)| entry.last_used < self.frame)
292                .min_by_key(|(_, entry)| entry.last_used)
293                .map(|(key, _)| *key)
294            else {
295                break;
296            };
297            self.entries.remove(&oldest);
298        }
299        Ok(())
300    }
301
302    /// Rasterizes `request` and uploads it as the text with `key`.
303    fn rasterize(
304        &mut self,
305        renderer: &Renderer,
306        key: u64,
307        request: &TextRasterRequest,
308    ) -> Result<(), TextError> {
309        let raster = self
310            .rasterizer
311            .rasterize(request)
312            .map_err(TextError::Raster)?;
313        // Each text is rasterized once per key; keep no copy.
314        self.rasterizer.invalidate(key);
315        let texture_size = &raster.descriptor.texture;
316        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
317        let corner =
318            [raster.descriptor.layout_x, raster.descriptor.layout_y].map(|corner| corner as u32);
319        // The anchor places the decorated text; the margin, which holds only ink reaching past
320        // it, takes no part in placement.
321        let margin = corner.map(|corner| corner.saturating_sub(request.decoration_margin()));
322        let texture = renderer
323            .upload_rgba8(
324                RenderSize::new(texture_size.width, texture_size.height)
325                    .map_err(TextError::Render)?,
326                &raster.pixels.rgba8,
327            )
328            .map_err(TextError::Render)?;
329        self.entries.insert(
330            key,
331            TextEntry {
332                texture,
333                width: texture_size.width,
334                height: texture_size.height,
335                margin,
336                corner,
337                last_used: self.frame,
338            },
339        );
340        Ok(())
341    }
342
343    /// Resolves `plan` at picture time `time` and `size`, and rasterizes its
344    /// texts, unless that layout is already held: then only texts the store
345    /// let go are rasterized again.
346    fn prepare_layout(
347        &mut self,
348        renderer: &Renderer,
349        plan: &LayoutPlan,
350        time: alpha2_time::MediaTime,
351        size: RenderSize,
352    ) -> Result<(), TextError> {
353        let evaluated = plan.evaluate(time);
354        let key = layout_key(evaluated.key, size);
355        if let Some(entry) = self.layouts.get_mut(&key) {
356            entry.last_used = self.frame;
357            let requests = entry.requests.clone();
358            for (text, request) in &requests {
359                match self.entries.get_mut(text) {
360                    Some(held) => held.last_used = self.frame,
361                    None => self.rasterize(renderer, *text, request)?,
362                }
363            }
364            return Ok(());
365        }
366        self.resolves += 1;
367        let frame = self.frame;
368        let shaped =
369            crate::layout::shape_texts(&evaluated, size, |request| self.shape(request, frame))?;
370        let resolved = crate::layout::resolve(&evaluated, size, &shaped);
371        let (texts, requests) = crate::layout::layout_texts(&evaluated, &resolved, size);
372        let mut boxes = Vec::with_capacity(texts.len());
373        for text in &texts {
374            boxes.push(match text {
375                None => None,
376                Some(text_key) => {
377                    let request = &requests
378                        .iter()
379                        .find(|(key, _)| key == text_key)
380                        .expect("a request per text")
381                        .1;
382                    match self.entries.get_mut(text_key) {
383                        Some(held) => held.last_used = self.frame,
384                        None => self.rasterize(renderer, *text_key, request)?,
385                    }
386                    let entry = &self.entries[text_key];
387                    Some(crate::layout::paint::TextBox {
388                        corner: entry.corner,
389                        size: [entry.width, entry.height],
390                    })
391                }
392            });
393        }
394        let items = crate::layout::paint::paint_list(&evaluated, &resolved, size, &boxes);
395        self.layouts.insert(
396            key,
397            LayoutEntry {
398                prepared: Arc::new(PreparedLayout { items, texts }),
399                requests,
400                last_used: self.frame,
401            },
402        );
403        Ok(())
404    }
405
406    /// `request`'s text shaped for layout, from the cache when it holds it.
407    fn shape(
408        &mut self,
409        request: &TextRasterRequest,
410        frame: u64,
411    ) -> Result<Arc<ShapedText>, TextRasterError> {
412        let key = shaped_key(request);
413        if let Some(entry) = self.shaped.get_mut(&key) {
414            entry.last_used = frame;
415            return Ok(Arc::clone(&entry.shaped));
416        }
417        let shaped = Arc::new(self.rasterizer.shape(request)?);
418        if self.shaped.len() >= RETAINED_SHAPED
419            && let Some(oldest) = self
420                .shaped
421                .iter()
422                .min_by_key(|(_, entry)| entry.last_used)
423                .map(|(key, _)| *key)
424        {
425            self.shaped.remove(&oldest);
426        }
427        self.shaped.insert(
428            key,
429            ShapedEntry {
430                shaped: Arc::clone(&shaped),
431                last_used: frame,
432            },
433        );
434        Ok(shaped)
435    }
436
437    /// How many layouts have been resolved, held or not.
438    #[cfg(test)]
439    #[must_use]
440    pub(crate) const fn layout_resolves(&self) -> u64 {
441        self.resolves
442    }
443
444    /// The prepared layout with `key`, as [`layout_key`] names it.
445    #[must_use]
446    pub fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
447        self.layouts
448            .get(&key)
449            .map(|entry| Arc::clone(&entry.prepared))
450    }
451
452    /// Where each node of `root` lies on a picture of `size` at picture
453    /// time `time`, its pixels those of a `canvas` canvas, laid out with
454    /// these fonts: [`crate::measure_layout`].
455    ///
456    /// # Errors
457    /// Returns [`TextError::Raster`] as [`Self::prepare`] would for its
458    /// texts.
459    pub fn measure_layout(
460        &self,
461        root: &alpha2_model::LayoutNode,
462        time: alpha2_time::MediaTime,
463        size: RenderSize,
464        canvas: (u32, u32),
465    ) -> Result<crate::LayoutBounds, TextError> {
466        crate::measure_layout(&self.rasterizer, root, time, size, canvas)
467    }
468
469    /// [`Self::prepare`] for the frame of `project` at `time`, rendered at
470    /// `size`; nothing to do, and no evaluation, when the project draws no
471    /// text ([`crate::EngineProject::draws_text`]).
472    ///
473    /// # Errors
474    /// Returns [`crate::EngineError::Evaluate`] or
475    /// [`crate::EngineError::Text`].
476    pub fn prepare_at(
477        &mut self,
478        renderer: &Renderer,
479        project: &crate::EngineProject,
480        time: alpha2_time::MediaTime,
481        size: RenderSize,
482    ) -> Result<(), crate::EngineError> {
483        if !project.draws_text() {
484            return Ok(());
485        }
486        let frame = project.evaluate(time)?;
487        self.prepare(renderer, &frame, size)
488            .map_err(crate::EngineError::Text)
489    }
490
491    /// Where `text` in `style` lies on a canvas of `size`, drawn with these
492    /// fonts: [`measure_text`].
493    ///
494    /// # Errors
495    /// Returns [`TextError::Raster`] as [`Self::prepare`] would for the text.
496    pub fn measure(
497        &self,
498        text: &str,
499        style: &TextStyle,
500        size: RenderSize,
501    ) -> Result<TextBounds, TextError> {
502        measure_text(&self.rasterizer, text, style, size)
503    }
504
505    /// The prepared text with `key`, as [`text_key`] names it.
506    #[must_use]
507    pub fn get(&self, key: u64) -> Option<TextTexture<'_>> {
508        self.entries.get(&key).map(|entry| TextTexture {
509            texture: SourceView::from(&entry.texture),
510            width: entry.width,
511            height: entry.height,
512            margin: entry.margin,
513            cache_key: Some(self.cache_key(key)),
514        })
515    }
516
517    /// The result cache key of the text with `key`, drawn with these fonts:
518    /// the font set in the high half, beside the text domain, and the text
519    /// in the low half, so two keys are equal only for the same text drawn
520    /// with the same fonts.
521    fn cache_key(&self, key: u64) -> RenderCacheKey {
522        let high = TEXT_CACHE_DOMAIN ^ self.font_set;
523        RenderCacheKey::from_value((u128::from(high) << 64) | u128::from(key))
524    }
525}
526
527/// Every text `frame` may draw, with its style: each caption, then each text
528/// generator of a clip it draws, the clip's own source or a step of its
529/// compositions. A text drawn twice is named twice.
530pub(crate) fn frame_texts<'f>(
531    frame: &'f FrameDesc<'_>,
532) -> impl Iterator<Item = (&'f str, &'f TextStyle)> {
533    let captions = frame
534        .captions
535        .iter()
536        .map(|caption| (caption.text, caption.style));
537    let generators = frame
538        .visual
539        .iter()
540        .filter(|clip| frame.draws(clip))
541        .flat_map(|clip| {
542            let steps = clip
543                .composition
544                .iter()
545                .chain(&clip.source_composition)
546                .flat_map(|composition| &composition.plan().steps)
547                .filter_map(|step| match &step.op {
548                    StepOp::Generator { generator, .. } => Some(generator),
549                    _ => None,
550                });
551            clip.generator.into_iter().chain(steps)
552        })
553        .filter_map(|generator| match generator {
554            Generator::Text { text, style } => Some((text.as_str(), style)),
555            _ => None,
556        });
557    captions.chain(generators)
558}
559
560/// A host's frame provider together with its text store: frames come from
561/// the one and text from the other.
562pub struct FramesWithText<'a> {
563    frames: &'a dyn crate::FrameProvider,
564    texts: &'a TextTextures,
565}
566
567impl<'a> FramesWithText<'a> {
568    /// `frames` with the texts `texts` holds.
569    #[must_use]
570    pub fn new(frames: &'a dyn crate::FrameProvider, texts: &'a TextTextures) -> Self {
571        Self { frames, texts }
572    }
573}
574
575impl crate::FrameProvider for FramesWithText<'_> {
576    fn texture_for(
577        &self,
578        source: &alpha2_media::SourceId,
579        time: alpha2_time::MediaTime,
580    ) -> Option<SourceView<'_>> {
581        self.frames.texture_for(source, time)
582    }
583
584    fn pending_source_work(
585        &self,
586        source: &alpha2_media::SourceId,
587        time: alpha2_time::MediaTime,
588    ) -> Option<&alpha2_render::PendingSourceWork> {
589        self.frames.pending_source_work(source, time)
590    }
591
592    fn source_key(
593        &self,
594        source: &alpha2_media::SourceId,
595        time: alpha2_time::MediaTime,
596    ) -> Option<alpha2_render::RenderCacheKey> {
597        self.frames.source_key(source, time)
598    }
599
600    fn text(&self, key: u64) -> Option<TextTexture<'_>> {
601        self.texts.get(key)
602    }
603
604    fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
605        self.texts.layout(key)
606    }
607}
608
609/// Every layout `frame` may draw, with its picture time: each drawn clip's
610/// layout source at its source time, then each layout step of its
611/// compositions at the step's time plus its start.
612pub(crate) fn frame_layouts<'f>(
613    frame: &'f FrameDesc<'_>,
614) -> impl Iterator<Item = (&'f LayoutPlan, alpha2_time::MediaTime)> {
615    frame
616        .visual
617        .iter()
618        .filter(|clip| frame.draws(clip))
619        .flat_map(|clip| {
620            let own = clip
621                .plan
622                .and_then(GeneratorPlan::layout)
623                .map(|plan| (plan, clip.source_time));
624            let steps = clip
625                .composition
626                .iter()
627                .chain(&clip.source_composition)
628                .flat_map(|composition| {
629                    let time = composition.time();
630                    composition
631                        .plan()
632                        .steps
633                        .iter()
634                        .filter_map(move |step| match &step.op {
635                            StepOp::Generator {
636                                plan: Some(plan),
637                                start,
638                                ..
639                            } => plan.layout().map(|plan| {
640                                (
641                                    plan,
642                                    alpha2_time::MediaTime(
643                                        step.time(time).ticks().saturating_add(start.ticks()),
644                                    ),
645                                )
646                            }),
647                            _ => None,
648                        })
649                });
650            own.into_iter().chain(steps)
651        })
652}
653
654/// A layout's identity at a render size: its evaluated key and the size.
655/// Equal keys lay out and draw alike with the same fonts.
656#[must_use]
657pub fn layout_key(evaluated: u128, size: RenderSize) -> u128 {
658    let mut hash = alpha2_geometry::StableHash::new();
659    hash.bytes(b"layout");
660    hash.bytes(&evaluated.to_le_bytes());
661    hash.u32(size.width);
662    hash.u32(size.height);
663    hash.finish()
664}
665
666/// A layout text's raster request as a text store key: FNV-1a over a domain
667/// byte and every field of the request. Never zero.
668pub(crate) fn layout_text_key(request: &TextRasterRequest) -> u64 {
669    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
670    let mut feed = |bytes: &[u8]| {
671        for byte in bytes {
672            hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
673        }
674    };
675    // Layout texts take keys apart from captions' and text generators'.
676    feed(&[0x4c]);
677    feed_request(&mut feed, request);
678    hash.max(1)
679}
680
681/// What a request is shaped from, as a shaped-text cache key.
682fn shaped_key(request: &TextRasterRequest) -> u64 {
683    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
684    let mut feed = |bytes: &[u8]| {
685        for byte in bytes {
686            hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
687        }
688    };
689    feed(request.text.as_bytes());
690    feed(&[0xff]);
691    feed(
692        request
693            .font_family
694            .as_deref()
695            .unwrap_or_default()
696            .as_bytes(),
697    );
698    feed(&[0xff, u8::from(request.font_family.is_some())]);
699    feed(&request.font_size.to_bits().to_le_bytes());
700    feed(&request.line_height.to_bits().to_le_bytes());
701    hash
702}
703
704fn feed_request(feed: &mut impl FnMut(&[u8]), request: &TextRasterRequest) {
705    feed(request.text.as_bytes());
706    feed(&[0xff]);
707    feed(
708        request
709            .font_family
710            .as_deref()
711            .unwrap_or_default()
712            .as_bytes(),
713    );
714    feed(&[0xff, u8::from(request.font_family.is_some())]);
715    for value in [request.font_size, request.max_width, request.line_height] {
716        feed(&value.to_bits().to_le_bytes());
717    }
718    feed(&request.color);
719    feed(&[match request.align {
720        alpha2_media::TextAlign::Start => 0,
721        alpha2_media::TextAlign::Center => 1,
722        alpha2_media::TextAlign::End => 2,
723    }]);
724    match request.outline {
725        Some(outline) => {
726            feed(&[1]);
727            feed(&outline.color);
728            feed(&outline.width.to_bits().to_le_bytes());
729        }
730        None => feed(&[0]),
731    }
732}
733
734/// A text's identity at a render size: the text, its style, and the size,
735/// hashed (FNV-1a). Never zero.
736#[must_use]
737pub fn text_key(text: &str, style: &TextStyle, size: RenderSize) -> u64 {
738    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
739    let mut feed = |bytes: &[u8]| {
740        for byte in bytes {
741            hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
742        }
743    };
744    feed(text.as_bytes());
745    feed(&[0xff]);
746    // Family names cannot hold 0xff (they are UTF-8), so the text and family stay apart.
747    feed(style.font_family.as_deref().unwrap_or_default().as_bytes());
748    feed(&[0xff, u8::from(style.font_family.is_some())]);
749    for value in [
750        style.font_size,
751        style.line_height,
752        style.max_width,
753        style.position.x,
754        style.position.y,
755        style.anchor.x,
756        style.anchor.y,
757    ] {
758        feed(&value.to_bits().to_le_bytes());
759    }
760    feed(&rgba8(style.color));
761    feed(&[match style.align {
762        TextAlign::Start => 0,
763        TextAlign::Center => 1,
764        TextAlign::End => 2,
765    }]);
766    match style.outline {
767        Some(outline) => {
768            feed(&[1]);
769            feed(&rgba8(outline.color));
770            feed(&outline.width.to_bits().to_le_bytes());
771        }
772        None => feed(&[0]),
773    }
774    match style.background {
775        Some(background) => {
776            feed(&[1]);
777            feed(&rgba8(background.color));
778            feed(&background.padding.to_bits().to_le_bytes());
779        }
780        None => feed(&[0]),
781    }
782    feed(&size.width.to_le_bytes());
783    feed(&size.height.to_le_bytes());
784    hash.max(1)
785}
786
787/// Where `text` in `style` lies on a canvas of `size` ([`TextBounds`]), laid
788/// out with `rasterizer`'s fonts as [`TextTextures::prepare`] lays it out and
789/// placed as a frame places it, without drawing it. Measured at the size a
790/// frame is drawn at, it agrees with that frame to the pixel; text laid out
791/// at another size can break its lines elsewhere.
792///
793/// # Errors
794/// Returns [`TextError::Raster`] when the text host refuses the text: no
795/// fonts, or a style out of range.
796#[allow(clippy::cast_precision_loss)]
797pub fn measure_text(
798    rasterizer: &TextRasterizer,
799    text: &str,
800    style: &TextStyle,
801    size: RenderSize,
802) -> Result<TextBounds, TextError> {
803    let request = text_request(text, style, size, text_key(text, style, size));
804    let measure = rasterizer.measure(&request).map_err(TextError::Raster)?;
805    let (width, height) = (size.width as f32, size.height as f32);
806    let margin = measure.decoration_margin as f32;
807    let (box_width, box_height) = (measure.box_width as f32, measure.box_height as f32);
808    // The layout box's top-left corner on the canvas: the anchor of the box with its decorations
809    // lies at the position, as `text_placement` places the texture.
810    let origin = [
811        style.position.x * width - style.anchor.x * (box_width + 2.0 * margin) + margin,
812        style.position.y * height - style.anchor.y * (box_height + 2.0 * margin) + margin,
813    ];
814    let rect = |[left, top, right, bottom]: [f32; 4]| CanvasRect {
815        left: (origin[0] + left) / width,
816        top: (origin[1] + top) / height,
817        right: (origin[0] + right) / width,
818        bottom: (origin[1] + bottom) / height,
819    };
820    Ok(TextBounds {
821        layout: rect([-margin, -margin, box_width + margin, box_height + margin]),
822        content: rect(measure.content),
823        ink: measure.ink.map(rect),
824        line_count: measure.line_count,
825    })
826}
827
828/// A straight, sRGB-encoded color as bytes.
829#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
830pub(crate) fn rgba8(color: Rgba) -> [u8; 4] {
831    [color.red, color.green, color.blue, color.alpha]
832        .map(|value| (value.clamp(0.0, 1.0) * 255.0).round() as u8)
833}
834
835/// The text host request for `text` at `size`: sizes scaled from canvas
836/// fractions to pixels, within what the text host draws.
837#[allow(clippy::cast_precision_loss)]
838fn text_request(text: &str, style: &TextStyle, size: RenderSize, key: u64) -> TextRasterRequest {
839    let font_size = (style.font_size * size.height as f32).clamp(1.0, MAX_FONT_PIXELS);
840    TextRasterRequest {
841        text: text.to_owned(),
842        font_size,
843        max_width: (style.max_width * size.width as f32).max(1.0),
844        line_height: style.line_height,
845        align: match style.align {
846            TextAlign::Start => alpha2_media::TextAlign::Start,
847            TextAlign::Center => alpha2_media::TextAlign::Center,
848            TextAlign::End => alpha2_media::TextAlign::End,
849        },
850        color: rgba8(style.color),
851        emoji_policy: EmojiPolicy::Color,
852        content_version: key,
853        outline: style.outline.map(|outline| alpha2_text::TextOutline {
854            color: rgba8(outline.color),
855            width: (outline.width * font_size).clamp(f32::MIN_POSITIVE, MAX_OUTLINE_PIXELS),
856        }),
857        background: style
858            .background
859            .map(|background| alpha2_text::TextBackground {
860                color: rgba8(background.color),
861                padding: (background.padding * font_size).clamp(0.0, MAX_PADDING_PIXELS),
862            }),
863        font_family: style.font_family.clone(),
864    }
865}
866
867#[cfg(test)]
868#[path = "text_tests.rs"]
869mod tests;