srv3_ttml/
lib.rs

1use hex_color::HexColor;
2use serde::{Deserialize, Serialize};
3use serde_repr::{Deserialize_repr, Serialize_repr};
4#[derive(Debug, Serialize, Deserialize)]
5#[serde(rename = "timedtext")]
6/// The TimedText struct is the root of the XML file.
7/// It contains the head and body of the XML file.
8pub struct TimedText {
9    #[serde(rename = "format")]
10    pub format: Option<String>,
11    pub head: Option<Head>,
12    pub body: Body,
13}
14impl TimedText {
15    pub fn from_str(s: &str) -> Result<Self, quick_xml::DeError> {
16        quick_xml::de::from_str(s)
17    }
18}
19
20#[derive(Debug, Serialize, Deserialize)]
21pub struct Head {
22    #[serde(rename = "pen")]
23    pub pen: Vec<Pen>,
24    #[serde(rename = "wp")]
25    pub wp: Vec<WindowPosition>,
26    #[serde(rename = "ws")]
27    pub ws: Vec<WindowStyle>,
28}
29
30#[derive(Debug, Serialize, Deserialize)]
31// The value names here are not really descriptive,
32// but there's no documentation to go off of.
33// So I'll be guessing what they mean and renaming the fields
34// in code here to describe what I think they are.
35//
36// tfw proprietary XML schema
37//
38// todo: to anyone who knows what these fields are,
39// please PLEASE let me know what they are.
40
41// serde: do not add fields that are None
42/// Pen is a struct that contains the style of the text...?
43pub struct Pen {
44    /// ID of pen
45    #[serde(rename = "@id")]
46    pub id: Option<u32>,
47
48    // --- Text styles ---
49    #[serde(rename = "@b")]
50    /// Toggle bold style
51    pub bold: Option<bool>,
52
53    #[serde(rename = "@i")]
54    /// Toggle italic style
55    pub italic: Option<bool>,
56
57    #[serde(rename = "@u")]
58    /// Toggle underline style
59    pub underline: Option<bool>,
60
61    /// Foreground color of the text
62    #[serde(rename = "@fc")]
63    pub foreground_color: Option<HexColor>,
64
65    /// Opacity of foreground color, has to be input separately
66    /// because it's a separate attribute in the XML
67    ///
68    /// If your Hex is RGBA your program should automatically separate the A value
69    /// into this attribute
70    #[serde(rename = "@fo")]
71    pub foreground_opacity: Option<u32>,
72
73    /// Background color of the text
74    #[serde(rename = "@bc")]
75    pub background_color: Option<HexColor>,
76
77    /// Opacity of background color, has to be input separately
78    /// because it's a separate attribute in the XML
79    ///
80    /// If your Hex is RGBA your program should automatically separate the A value
81    /// into this attribute
82    #[serde(rename = "@bo")]
83    pub background_opacity: Option<u32>,
84
85    /// Color of text outline/edge
86    #[serde(rename = "@ec")]
87    pub edge_color: Option<HexColor>,
88
89    /// Type of edge/outline of the text
90    #[serde(rename = "@et")]
91    pub edge_type: Option<u32>,
92
93    /// Text size
94    #[serde(rename = "@sz")]
95    pub font_size: Option<u32>,
96
97    #[serde(rename = "@fs")]
98    /// Font style, is an enum?
99    pub font_style: Option<FontStyle>,
100
101    #[serde(rename = "@rb")]
102    /// Ruby text
103    ///
104    /// Ruby text is a small annotation above or below the main text,
105    /// typically used in East Asian typography since
106    /// Chinese characters are logographic. Ruby text is used to clarify
107    /// pronounciation or meaning of these glyphs.
108    ///
109    /// Sometimes called "furigana" in Japanese and "bopomofo" in Chinese.
110    ///
111    /// They can also sometimes be used in English text to clarify references
112    /// to literary devices or other things, for example in the game Honkai Star Rail
113    /// which makes extensive use of ruby text to clarify references to the game's lore.
114    ///
115    /// This ruby field is an enum that specifies the type of ruby text.
116    ///
117    /// This can be:
118    /// - No ruby text
119    /// - Base
120    /// - Parentheses
121    /// - Before text
122    /// - After text
123    pub ruby: Option<RubyPart>,
124
125    #[serde(rename = "@hg")]
126    /// Packing of text
127    pub packing: Option<u32>,
128}
129
130#[derive(Debug, Serialize_repr, Deserialize_repr)]
131#[repr(u32)]
132pub enum RubyPart {
133    None = 0,
134    Base = 1,
135    Parenthesis = 2,
136    BeforeText = 4,
137    AfterText = 5,
138}
139
140#[derive(Debug, Serialize_repr, Deserialize_repr)]
141#[repr(u32)]
142pub enum FontStyle {
143    CourierNew = 1,
144    TimesNewRoman = 2,
145    LucidaConsole = 3,
146    ComicSans = 5,
147    MonotypeCorsiva = 6,
148    CarriosGothic = 7,
149    #[serde(other)]
150    Roboto = 0,
151}
152
153impl Default for FontStyle {
154    fn default() -> Self {
155        FontStyle::Roboto
156    }
157}
158
159#[derive(Debug, Serialize_repr, Deserialize_repr)]
160#[repr(u32)]
161pub enum AnchorPoint {
162    TopLeft = 0,
163    TopCenter = 1,
164    TopRight = 2,
165    MiddleLeft = 3,
166    Center = 4,
167    MiddleRight = 5,
168    BottomLeft = 6,
169    BottomCenter = 7,
170    BottomRight = 8,
171}
172
173#[derive(Debug, Serialize, Deserialize)]
174/// Window position of the text
175#[serde(rename = "wp")]
176pub struct WindowPosition {
177    /// ID of window position
178    #[serde(rename = "@id")]
179    pub id: u32,
180    #[serde(rename = "@ap")]
181    /// References to an anchor point.
182    /// Is an enum which specify which corner of the screen the text is anchored to.
183    pub anchor_point: Option<AnchorPoint>,
184
185    /// X position from anchor point
186    #[serde(rename = "@ah")]
187    pub anchor_horizontal: Option<i32>,
188
189    /// Y position from anchor point
190    #[serde(rename = "@av")]
191    pub anchor_vertical: Option<i32>,
192}
193
194#[derive(Debug, Serialize, Deserialize)]
195#[serde(rename = "ws")]
196pub struct WindowStyle {
197    #[serde(rename = "@id")]
198    pub id: u32,
199    #[serde(rename = "@ju")]
200    /// Reference to anchor point to justify text???
201    pub justify: Option<u32>,
202
203    // todo: Both of these should be an enum of 0-3 but I don't know what to name them
204    /// Pitch direction of text (vertical tilt)
205    #[serde(rename = "@pd")]
206    pub pitch_direction: Option<Rotation>,
207
208    /// Yaw (skew) direction of text (horizontal tilt)
209    #[serde(rename = "@sd")]
210    pub skew_direction: Option<Rotation>,
211}
212
213#[derive(Debug, Serialize_repr, Deserialize_repr)]
214#[repr(u32)]
215pub enum Rotation {
216    Zero = 0,
217    Ninety = 1,
218    OneEighty = 2,
219    TwoSeventy = 3,
220}
221
222#[derive(Debug, Serialize, Deserialize)]
223pub struct Body {
224    // todo: parse this properly into a list of enums or something
225    #[serde(rename = "$value")]
226    pub elements: Vec<BodyElement>,
227}
228
229// --- The body ---
230// Finally, the body of the XML
231// the actual data we want to really parse
232//
233// It's a bunch of <p> tags with some attributes and inner text, kinda like HTML
234#[derive(Debug, Serialize, Deserialize)]
235pub struct Paragraph {
236    // The actual text inside the <p> tag
237    // <p>text</p>
238    #[serde(rename = "$value")]
239    // Always treat the inner text as string
240    pub inner: Vec<BodyElement>,
241    /// Timestamp on when to display the caption
242    #[serde(rename = "@t")]
243    pub timestamp: u64,
244    /// Duration to display the caption for
245    #[serde(rename = "@d")]
246    pub duration: u64,
247
248    /// Reference to a window position ID
249    #[serde(rename = "@wp")]
250    pub window_position: Option<u32>,
251
252    /// Reference to a window style ID
253    #[serde(rename = "@ws")]
254    pub window_style: Option<u32>,
255}
256
257// todo: make the thing like HTML
258
259#[derive(Debug, Serialize, Deserialize)]
260pub struct Span {
261    #[serde(rename = "$value")]
262    pub inner: Option<Vec<BodyElement>>,
263    /// Reference to a pen style ID
264    #[serde(rename = "p")]
265    pub pen: Option<u32>,
266    // nested inside is more spans or something
267    // this is a recursive structure
268}
269#[derive(Debug, Serialize, Deserialize)]
270pub struct Br;
271#[derive(Debug, Serialize, Deserialize)]
272pub struct Div {
273    #[serde(rename = "$value")]
274    pub elements: Vec<BodyElement>,
275}
276
277#[derive(Debug, Serialize, Deserialize)]
278pub enum BodyElement {
279    #[serde(rename = "$text")]
280    Text(String),
281    #[serde(rename = "p")]
282    Paragraph(Paragraph),
283    #[serde(rename = "s")]
284    Span(Span),
285    #[serde(rename = "br")]
286    Br(Br),
287    #[serde(rename = "div")]
288    Div(Vec<Self>),
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    // TODO: So formatted files with <s> tags inside the <p> tags are not parsed correctly
296    // We want to treat them literally as <s> tags, not as a separate element
297    #[test]
298    fn test_parse_file() {
299        let file = include_str!("../test/aishite.srv3");
300        let parse = TimedText::from_str(file).unwrap();
301
302        println!("{:#?}", parse);
303    }
304
305    #[test]
306    fn test_parse_file_unformatted() {
307        let file = include_str!("../test/mesmerizer.srv3.xml");
308        let parse = TimedText::from_str(file).unwrap();
309
310        println!("{:#?}", parse);
311    }
312}