ff_format/text.rs
1//! Pure text/title layer spec types.
2//!
3//! [`TextSpec`] and [`TextStyle`] describe a text layer's content, position, and
4//! styling as plain values, with **no** `FFmpeg` dependency. A rendering primitive
5//! (in `ff-filter`) translates a [`TextSpec`] into a compositable frame; the model
6//! (in `avio`) uses it for first-class text clips. Neither concern leaks here.
7
8use std::path::PathBuf;
9
10use crate::color::Color;
11
12/// Where a text layer is anchored within its canvas.
13///
14/// The renderer places the text box at the anchor, then applies
15/// [`TextSpec::offset`] as a pixel nudge from it.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub enum Anchor {
19 /// Top-left corner.
20 TopLeft,
21 /// Top edge, horizontally centred.
22 TopCenter,
23 /// Top-right corner.
24 TopRight,
25 /// Left edge, vertically centred.
26 CenterLeft,
27 /// Centre of the canvas. The default.
28 #[default]
29 Center,
30 /// Right edge, vertically centred.
31 CenterRight,
32 /// Bottom-left corner.
33 BottomLeft,
34 /// Bottom edge, horizontally centred.
35 BottomCenter,
36 /// Bottom-right corner.
37 BottomRight,
38}
39
40/// Visual styling for a text layer.
41///
42/// Mirrors the typical draw-text options as plain values; opacity is carried by
43/// the alpha channel of [`color`](Self::color) (and [`box_color`](Self::box_color)).
44#[derive(Debug, Clone, PartialEq, Eq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct TextStyle {
47 /// Font size in points.
48 pub font_size: u32,
49 /// Fill color of the text (alpha = opacity).
50 pub color: Color,
51 /// Optional path to a `TrueType` font file. A default font is used when `None`.
52 pub font_file: Option<PathBuf>,
53 /// Optional background box color behind the text (alpha = box opacity). No box
54 /// when `None`.
55 pub box_color: Option<Color>,
56 /// Background box padding in pixels. Ignored when [`box_color`](Self::box_color)
57 /// is `None`.
58 pub box_border_width: u32,
59}
60
61impl Default for TextStyle {
62 fn default() -> Self {
63 Self {
64 font_size: 48,
65 color: Color::WHITE,
66 font_file: None,
67 box_color: None,
68 box_border_width: 0,
69 }
70 }
71}
72
73/// A text/title layer: the string, its placement, and its styling.
74///
75/// Size-agnostic: the canvas dimensions are supplied by the renderer, so the same
76/// spec can render at any resolution.
77#[derive(Debug, Clone, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct TextSpec {
80 /// The text to render (UTF-8).
81 pub text: String,
82 /// Where the text is anchored within the canvas.
83 pub anchor: Anchor,
84 /// Pixel offset `(dx, dy)` from the anchor (positive = right / down).
85 pub offset: (i32, i32),
86 /// Visual styling.
87 pub style: TextStyle,
88}
89
90impl TextSpec {
91 /// Creates a centred text spec with default styling.
92 #[must_use]
93 pub fn new(text: impl Into<String>) -> Self {
94 Self {
95 text: text.into(),
96 anchor: Anchor::Center,
97 offset: (0, 0),
98 style: TextStyle::default(),
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::{Anchor, TextSpec, TextStyle};
106 use crate::color::Color;
107
108 #[test]
109 fn anchor_default_should_be_center() {
110 assert_eq!(Anchor::default(), Anchor::Center);
111 }
112
113 #[test]
114 fn text_style_default_should_be_white_48() {
115 let s = TextStyle::default();
116 assert_eq!(s.font_size, 48);
117 assert_eq!(s.color, Color::WHITE);
118 assert!(s.font_file.is_none());
119 assert!(s.box_color.is_none());
120 }
121
122 #[test]
123 fn text_spec_new_should_center_with_default_style() {
124 let spec = TextSpec::new("Hello");
125 assert_eq!(spec.text, "Hello");
126 assert_eq!(spec.anchor, Anchor::Center);
127 assert_eq!(spec.offset, (0, 0));
128 assert_eq!(spec.style, TextStyle::default());
129 }
130}