1#![warn(missing_docs)]
46#![allow(
47 clippy::cast_possible_truncation,
48 clippy::cast_precision_loss,
49 clippy::cast_sign_loss,
50 clippy::cast_lossless,
51 clippy::must_use_candidate,
52 clippy::missing_errors_doc,
53 clippy::missing_panics_doc,
54 clippy::unused_self,
55 clippy::doc_markdown,
56 clippy::too_many_lines,
57 clippy::too_many_arguments,
58 dead_code,
59 clippy::similar_names,
60 clippy::many_single_char_names,
61 clippy::unnested_or_patterns,
62 unused_imports,
63 unused_variables,
64 clippy::unnecessary_wraps,
65 clippy::redundant_pattern_matching,
66 clippy::pedantic,
67 clippy::approx_constant,
68 clippy::builtin_type_shadow
69)]
70
71pub mod burn_in;
72pub mod error;
73pub mod font;
74pub mod format_convert;
75pub mod format_converter;
76pub mod overlay;
77pub mod parser;
78pub mod renderer;
79pub mod soft_shadow;
80pub mod style;
81pub mod sub_style;
82pub mod text;
83pub mod timing;
84pub mod timing_adjuster;
85
86pub mod cea;
88pub mod convert;
89
90pub mod accessibility;
92pub mod segmentation;
93pub mod translation;
94
95pub mod line_break;
97pub mod spell_check;
98pub mod timing_adjust;
99
100pub mod cue_parser;
102pub mod subtitle_merge;
103pub mod subtitle_validator;
104
105pub mod overlap_detect;
107pub mod reading_speed;
108pub mod subtitle_style_ext;
109
110pub mod cue_point;
112pub mod subtitle_export;
113pub mod subtitle_index;
114
115pub mod cue_timing;
117pub mod position_calc;
118pub mod subtitle_diff;
119
120pub mod subtitle_sanitize;
122pub mod subtitle_search;
123pub mod subtitle_stats;
124
125pub mod forced_subtitle;
127
128pub mod subtitle_alignment;
130
131pub mod ttml_v2;
133
134pub mod cea708;
136
137pub mod ass_override;
141
142pub mod cue_positioning;
144
145pub mod eia608_realtime;
148
149pub mod fallback_fonts;
151
152pub mod glyph_atlas;
154
155pub mod karaoke_engine;
157
158pub mod live_caption;
160
161pub mod position;
163
164pub mod search;
166
167pub mod sign_language;
169
170pub mod ssa_style_cache;
172
173pub mod style_inherit;
175
176pub mod subtitle_chapters;
178
179pub mod subtitle_ocr;
181
182pub mod teletext;
184
185pub mod teletext_subtitle;
187
188pub mod wcag_validator;
190
191pub use cea708::{CaptionWindow, Dtvcc708Command, Dtvcc708Decoder, Dtvcc708Packet};
193pub use error::{SubtitleError, SubtitleResult};
194pub use font::{Font, GlyphCache};
195pub use overlay::overlay_subtitle;
196pub use renderer::{DirtyRect, IncrementalSubtitleRenderer, SubtitleRenderer};
197pub use style::{Alignment, Animation, Color, OutlineStyle, Position, ShadowStyle, SubtitleStyle};
198pub use text::{BidiLevel, TextLayout, TextLayoutEngine};
199pub use ttml_v2::{SubtitleEntry, TtmlParser, TtmlRegion, TtmlSpan, TtmlStyle};
200
201#[derive(Clone, Debug)]
203pub struct Subtitle {
204 pub id: Option<String>,
206 pub start_time: i64,
208 pub end_time: i64,
210 pub text: String,
212 pub style: Option<SubtitleStyle>,
214 pub position: Option<Position>,
216 pub animations: Vec<Animation>,
218}
219
220impl Subtitle {
221 #[must_use]
223 pub fn new(start_time: i64, end_time: i64, text: String) -> Self {
224 Self {
225 id: None,
226 start_time,
227 end_time,
228 text,
229 style: None,
230 position: None,
231 animations: Vec::new(),
232 }
233 }
234
235 #[must_use]
237 pub fn with_id(mut self, id: impl Into<String>) -> Self {
238 self.id = Some(id.into());
239 self
240 }
241
242 #[must_use]
244 pub fn is_active(&self, timestamp_ms: i64) -> bool {
245 timestamp_ms >= self.start_time && timestamp_ms < self.end_time
246 }
247
248 #[must_use]
250 pub fn duration(&self) -> i64 {
251 self.end_time - self.start_time
252 }
253
254 pub fn with_animation(mut self, animation: Animation) -> Self {
256 self.animations.push(animation);
257 self
258 }
259
260 #[must_use]
262 pub fn with_position(mut self, position: Position) -> Self {
263 self.position = Some(position);
264 self
265 }
266
267 #[must_use]
269 pub fn with_style(mut self, style: SubtitleStyle) -> Self {
270 self.style = Some(style);
271 self
272 }
273}
274
275pub struct SrtParser;
289
290impl SrtParser {
291 pub fn parse(text: &str) -> SubtitleResult<Vec<Subtitle>> {
297 parser::srt::parse_srt(text)
298 }
299}
300
301pub struct AssParser;
303
304impl AssParser {
305 pub fn parse(text: &str) -> SubtitleResult<Vec<Subtitle>> {
311 let file = parser::ssa::parse_ass(text)?;
312 Ok(file.events)
313 }
314}
315
316pub struct WebVttParser;
318
319impl WebVttParser {
320 pub fn parse(text: &str) -> SubtitleResult<Vec<Subtitle>> {
326 parser::webvtt::parse_webvtt(text)
327 }
328}
329
330#[cfg(test)]
331mod subtitle_api_tests {
332 use super::*;
333
334 const SAMPLE_SRT: &str = "1\n00:00:01,000 --> 00:00:04,000\nHello, world!\n\n2\n00:00:05,000 --> 00:00:08,000\nSecond subtitle.\n\n";
335
336 #[test]
337 fn test_subtitle_id_field() {
338 let sub = Subtitle::new(0, 1000, "test".to_string()).with_id("42");
339 assert_eq!(sub.id, Some("42".to_string()));
340 }
341
342 #[test]
343 fn test_subtitle_new_has_no_id() {
344 let sub = Subtitle::new(0, 1000, "test".to_string());
345 assert!(sub.id.is_none());
346 }
347
348 #[test]
349 fn test_srt_parser_basic() {
350 let subs = SrtParser::parse(SAMPLE_SRT).expect("should succeed in test");
351 assert_eq!(subs.len(), 2);
352 assert_eq!(subs[0].text, "Hello, world!");
353 assert_eq!(subs[0].start_time, 1000);
354 assert_eq!(subs[0].end_time, 4000);
355 }
356
357 #[test]
358 fn test_srt_parser_second_entry() {
359 let subs = SrtParser::parse(SAMPLE_SRT).expect("should succeed in test");
360 assert_eq!(subs[1].text, "Second subtitle.");
361 assert_eq!(subs[1].start_time, 5000);
362 assert_eq!(subs[1].end_time, 8000);
363 }
364
365 #[test]
366 fn test_webvtt_parser_basic() {
367 let vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:04.000\nHello VTT!\n\n";
368 let subs = WebVttParser::parse(vtt).expect("should succeed in test");
369 assert!(!subs.is_empty());
370 assert_eq!(subs[0].text, "Hello VTT!");
371 }
372
373 #[test]
374 fn test_webvtt_parser_timing() {
375 let vtt = "WEBVTT\n\n00:00:05.500 --> 00:00:09.000\nTimed cue.\n\n";
376 let subs = WebVttParser::parse(vtt).expect("should succeed in test");
377 assert_eq!(subs[0].start_time, 5500);
378 assert_eq!(subs[0].end_time, 9000);
379 }
380
381 #[test]
382 fn test_subtitle_is_active() {
383 let sub = Subtitle::new(1000, 4000, "test".to_string());
384 assert!(sub.is_active(2000));
385 assert!(!sub.is_active(500));
386 assert!(!sub.is_active(5000));
387 }
388
389 #[test]
390 fn test_subtitle_duration() {
391 let sub = Subtitle::new(1000, 4000, "test".to_string());
392 assert_eq!(sub.duration(), 3000);
393 }
394
395 #[test]
396 fn test_ass_parser_basic() {
397 let ass = "[Script Info]\nScriptType: v4.00+\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Arial,48,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:01.00,0:00:04.00,Default,,0,0,0,,Hello ASS!\n\n";
398 let result = AssParser::parse(ass);
399 assert!(result.is_ok());
400 let subs = result.expect("should succeed in test");
401 assert!(!subs.is_empty());
402 }
403}