Skip to main content

mp4_edit/atom/leaf/stsd/
text.rs

1use bon::Builder;
2
3use crate::atom::util::ColorRgb;
4
5use super::StsdExtension;
6
7#[derive(Debug, Clone, Default, Builder)]
8pub struct TextSampleEntry {
9    #[builder(default)]
10    pub display_flags: DisplayFlags,
11    #[builder(default)]
12    pub text_justification: TextJustification,
13    #[builder(default)]
14    pub background_color: ColorRgb,
15    #[builder(default)]
16    pub default_text_box: TextBox,
17    #[builder(default)]
18    pub font_number: u16,
19    /// 0 = normal text
20    #[builder(default)]
21    pub font_face: FontFace,
22    #[builder(default)]
23    pub foreground_color: ColorRgb,
24    #[builder(into)]
25    pub font_name: String,
26    #[builder(default)]
27    pub extensions: Vec<StsdExtension>,
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct DisplayFlags {
32    /// Reflow the text instead of scaling when the track is scaled.
33    pub disable_auto_scale: bool, // 0x0002
34    /// Ignore the background color field in the text sample description and use the movie’s background color instead.
35    pub use_movie_background_color: bool, // 0x0008
36    /// Scroll the text until the last of the text is in view.
37    pub scroll_in: bool, // 0x0020
38    /// Scroll the text until the last of the text is gone.
39    pub scroll_out: bool, // 0x0040
40    /// Scroll the text horizontally when set; otherwise, scroll the text vertically.
41    pub horizontal_scroll: bool, // 0x0080
42    /// Scroll down (if scrolling vertically) or backward (if scrolling horizontally)
43    ///
44    /// **Note:** Horizontal scrolling also depends upon text justification.
45    pub reverse_scroll: bool, // 0x0100
46    /// Display new samples by scrolling out the old ones.
47    pub continuous_scroll: bool, // 0x0200
48    /// Display the text with a drop shadow.
49    pub drop_shadow: bool, // 0x1000
50    /// Use anti-aliasing when drawing text.
51    pub anti_alias: bool, // 0x2000
52    /// Do not display the background color, so that the text overlay background tracks.
53    pub key_text: bool, // 0x4000
54}
55
56#[derive(Debug, Clone, Default)]
57pub enum TextJustification {
58    #[default]
59    Left,
60    Centre,
61    Right,
62    Other(i32),
63}
64
65#[derive(Debug, Clone, Default)]
66pub struct TextBox {
67    pub top: u16,
68    pub left: u16,
69    pub bottom: u16,
70    pub right: u16,
71}
72
73#[derive(Debug, Clone, Default)]
74pub struct FontFace {
75    pub bold: bool,
76    pub italic: bool,
77    pub underline: bool,
78    pub outline: bool,
79    pub shadow: bool,
80    pub condense: bool,
81    pub extend: bool,
82}
83
84pub(super) mod serializer {
85    use super::{DisplayFlags, FontFace, TextBox, TextJustification, TextSampleEntry};
86    use crate::atom::{
87        stsd::extension::serializer::serialize_stsd_extensions,
88        util::serializer::{bits::Packer, color_rgb, pascal_string},
89    };
90
91    pub fn serialize_text_sample_entry(text: TextSampleEntry) -> Vec<u8> {
92        let mut data = Vec::new();
93
94        data.extend(display_flags(text.display_flags));
95        data.extend(text_justification(text.text_justification));
96        data.extend(color_rgb(text.background_color));
97        data.extend(text_box(text.default_text_box));
98        data.extend([0u8; 8]); // reserved
99        data.extend(text.font_number.to_be_bytes());
100        data.extend(font_face(text.font_face));
101        data.extend([0u8; 2]); // reserved
102        data.extend(color_rgb(text.foreground_color));
103        data.extend(pascal_string(text.font_name));
104        data.extend(serialize_stsd_extensions(text.extensions));
105
106        data
107    }
108
109    fn display_flags(d: DisplayFlags) -> [u8; 4] {
110        let mut packer = Packer::from(vec![0u8; 2]); // 2 leading empty bytes
111        packer.push_n::<1>(0); // 1 leading empty bit
112        packer.push_bool(d.key_text);
113        packer.push_bool(d.anti_alias);
114        packer.push_bool(d.drop_shadow);
115        packer.push_n::<2>(0); // 2 padding bits
116        packer.push_bool(d.continuous_scroll);
117        packer.push_bool(d.reverse_scroll);
118        packer.push_bool(d.horizontal_scroll);
119        packer.push_bool(d.scroll_out);
120        packer.push_bool(d.scroll_in);
121        packer.push_n::<1>(0); // 1 padding bit
122        packer.push_bool(d.use_movie_background_color);
123        packer.push_n::<1>(0); // 1 padding bit
124        packer.push_bool(d.disable_auto_scale);
125        packer.push_n::<1>(0); // 1 padding bit
126        Vec::from(packer)
127            .try_into()
128            .expect("display_flags is 4 bytes")
129    }
130
131    fn text_justification(j: TextJustification) -> [u8; 4] {
132        let value: i32 = match j {
133            TextJustification::Left => 0,
134            TextJustification::Centre => 1,
135            TextJustification::Right => -1,
136            TextJustification::Other(v) => v,
137        };
138        value.to_be_bytes()
139    }
140
141    fn text_box(b: TextBox) -> [u8; 8] {
142        let mut data = Vec::with_capacity(6);
143        data.extend(b.top.to_be_bytes());
144        data.extend(b.left.to_be_bytes());
145        data.extend(b.bottom.to_be_bytes());
146        data.extend(b.right.to_be_bytes());
147        data.try_into().expect("text_box is 8 bytes")
148    }
149
150    fn font_face(f: FontFace) -> [u8; 2] {
151        let mut packer = Packer::from(vec![0u8; 1]); // 1 leading byte
152        packer.push_n::<1>(0); // 1 leading bit
153        packer.push_bool(f.extend);
154        packer.push_bool(f.condense);
155        packer.push_bool(f.shadow);
156        packer.push_bool(f.outline);
157        packer.push_bool(f.underline);
158        packer.push_bool(f.italic);
159        packer.push_bool(f.bold);
160        Vec::from(packer).try_into().expect("font_face is 2 bytes")
161    }
162}
163
164pub(super) mod parser {
165    use winnow::{
166        binary::{be_i32, be_u16, bits},
167        combinator::seq,
168        error::{ContextError, ErrMode, StrContext},
169        ModalResult, Parser,
170    };
171
172    use crate::atom::{
173        stsd::{extension::parser::parse_stsd_extensions, SampleEntryData},
174        util::parser::{byte_array, color_rgb, pascal_string, Stream},
175    };
176
177    use super::*;
178
179    pub fn parse_text_sample_entry(input: &mut Stream<'_>) -> ModalResult<SampleEntryData> {
180        seq!(TextSampleEntry {
181            display_flags: bits::bits(display_flags).context(StrContext::Label("display_flags")),
182            text_justification: text_justification.context(StrContext::Label("text_justification")),
183            background_color: color_rgb.context(StrContext::Label("background_color")),
184            default_text_box: text_box.context(StrContext::Label("default_text_box")),
185            _: byte_array::<8>.context(StrContext::Label("reserved")),
186            font_number: be_u16.context(StrContext::Label("font_number")),
187            font_face: bits::bits(font_face).context(StrContext::Label("font_face")),
188            _: byte_array::<2>.context(StrContext::Label("reserved")),
189            foreground_color: color_rgb.context(StrContext::Label("foreground_color")),
190            font_name: pascal_string.context(StrContext::Label("text_name")),
191            extensions: parse_stsd_extensions.context(StrContext::Label("extensions")),
192        })
193        .map(SampleEntryData::Text)
194        .parse_next(input)
195    }
196
197    fn display_flags(input: &mut (Stream, usize)) -> ModalResult<DisplayFlags> {
198        use bits::bool;
199        seq!(DisplayFlags {
200            _: bits::take::<_, usize, _, ErrMode<ContextError>>(8usize).context(StrContext::Label("leading byte 1")),
201            _: bits::take::<_, usize, _, ErrMode<ContextError>>(8usize).context(StrContext::Label("leading byte 2")),
202            _: bits::take::<_, usize, _, ErrMode<ContextError>>(1usize).context(StrContext::Label("leading bit")),
203            key_text: bool.context(StrContext::Label("key_text")),
204            anti_alias: bool.context(StrContext::Label("anti_alias")),
205            drop_shadow: bool.context(StrContext::Label("drop_shadow")),
206            _: bits::take::<_, usize, _, ErrMode<ContextError>>(2usize).context(StrContext::Label("padding")),
207            continuous_scroll: bool.context(StrContext::Label("continuous_scroll")),
208            reverse_scroll: bool.context(StrContext::Label("reverse_scroll")),
209            horizontal_scroll: bool.context(StrContext::Label("horizontal_scroll")),
210            scroll_out: bool.context(StrContext::Label("scroll_out")),
211            scroll_in: bool.context(StrContext::Label("scroll_in")),
212            _: bits::take::<_, usize, _, ErrMode<ContextError>>(1usize).context(StrContext::Label("padding")),
213            use_movie_background_color: bool
214                .context(StrContext::Label("use_movie_background_color")),
215            _: bits::take::<_, usize, _, ErrMode<ContextError>>(1usize).context(StrContext::Label("padding")),
216            disable_auto_scale: bool.context(StrContext::Label("disable_auto_scale")),
217            _: bits::take::<_, usize, _, ErrMode<ContextError>>(1usize).context(StrContext::Label("padding")),
218        })
219        .parse_next(input)
220    }
221
222    fn text_justification(input: &mut Stream<'_>) -> ModalResult<TextJustification> {
223        let text_justification = be_i32.parse_next(input)?;
224        Ok(match text_justification {
225            0 => TextJustification::Left,
226            1 => TextJustification::Centre,
227            -1 => TextJustification::Right,
228            v => TextJustification::Other(v),
229        })
230    }
231
232    fn text_box(input: &mut Stream<'_>) -> ModalResult<TextBox> {
233        seq!(TextBox {
234            top: be_u16.context(StrContext::Label("top")),
235            left: be_u16.context(StrContext::Label("left")),
236            bottom: be_u16.context(StrContext::Label("bottom")),
237            right: be_u16.context(StrContext::Label("right")),
238        })
239        .parse_next(input)
240    }
241
242    fn font_face(input: &mut (Stream, usize)) -> ModalResult<FontFace> {
243        use bits::bool;
244        seq!(FontFace {
245            _: bits::take::<_, usize, _, ErrMode<ContextError>>(8usize).context(StrContext::Label("leading empty byte")),
246            _: bits::take::<_, usize, _, ErrMode<ContextError>>(1usize).context(StrContext::Label("leading empty bit")),
247            extend: bool.context(StrContext::Label("extend")),
248            condense: bool.context(StrContext::Label("condense")),
249            shadow: bool.context(StrContext::Label("shadow")),
250            outline: bool.context(StrContext::Label("outline")),
251            underline: bool.context(StrContext::Label("underline")),
252            italic: bool.context(StrContext::Label("italic")),
253            bold: bool.context(StrContext::Label("bold")),
254        })
255        .parse_next(input)
256    }
257}