Skip to main content

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}