ass_parser/lib.rs
1///! # AssParser
2///!
3///! [ass_parser] is a crate to parse .ass (Advanced SubStation Alpha) files. which is a subtitle file for creating and displaying subtitles in video files. It is widely used due to it's complex text formatting, positioning and styling. The Advanced SubStation Alpha is a successor
4///! to the SubStation Alpha .ssa file.
5///!
6///! ## Installation
7///!
8///! Add `ass_parser` as a dependency to your cargo.toml:
9///!
10///! ```shell
11///! cargo add ass_parser
12///! ```
13///! # Introduction
14///!
15///! AssParser is based on the principle of easy to read write and modify `.ass` files. This is the first version of `ass_parser`and now currently only have the features to modify `.ass` file.
16///!
17///! # Example
18///!
19/// Creating a simple `Advanced SubStation Alpha` `(.ass)` file with default values!
20///
21/// ```rust
22/// use ass_parser::{AssFile, ScriptInfo, V4Format, Events, AssFileOptions};
23/// use hex_color::HexColor;
24///
25/// fn main() {
26/// let mut ass_file = AssFile::new();
27/// let hexcolor = AssFileOptions::get_ass_color(HexColor::YELLOW);
28///
29/// ass_file.components.script
30/// .set_script(ScriptInfo::default());
31///
32/// ass_file.components.v4
33/// .set_v4(V4Format::default())
34/// .set_primarycolour(&hexcolor);
35///
36/// ass_file.components.events
37/// .set_events(Events::default());
38///
39/// AssFile::save_file(&ass_file, "new_subtitles.ass")
40///
41/// }
42///
43/// ```
44/// Here we create an .ass file with default values and When you open the .ass file you can see the
45/// following content.
46/// ```ass
47/// ScriptType: v4.00+
48/// PlayResX: 384
49/// PlayResY: 288
50/// ScaledBorderAndShadow: yes
51/// YCbCr Matrix: None
52///
53///
54/// [V4+ Styles]
55/// Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
56/// Style: Default,Arial,16,&H00ff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1
57///
58///
59/// [Events]
60/// Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
61/// Dialogue: 0,0:00:00.00,0:00:01.00,Default,,0,0,0,,Hello Friend
62/// ```
63///
64/// # Add Dialogues
65///
66/// ```rust
67/// use ass_parser::{AssFile, ScriptInfo, V4Format, Events, AssFileOptions, Dialogue};
68/// use ass_parser::IndexNotFound;
69/// use hex_color::HexColor;
70///
71/// fn main() -> Result<(), IndexNotFound>{
72/// let mut ass_file = AssFile::new();
73/// let hexcolor = AssFileOptions::get_ass_color(HexColor::YELLOW);
74///
75/// let first_dialogue = Dialogue::default()
76/// .set_text("Hello There!")
77/// .set_start("0:00:00.10")
78/// .set_end("0:00:00.50");
79///
80/// let second_dialogue = Dialogue::default()
81/// .set_text("Hello Friend!")
82/// .set_start("00:00.50")
83/// .set_end("00:00.58");
84///
85/// let third_dialogue = Dialogue::default()
86/// .set_text("Hello World!!")
87/// .set_start("0:00:00.58")
88/// .set_end("0:00:01.01");
89///
90/// let events = Events::new()
91/// .add_first_dialogue(first_dialogue)?
92/// .add_dialogue(second_dialogue)
93/// .add_dialogue(third_dialogue)
94/// .create();
95///
96///
97/// ass_file.components.script
98/// .set_script(ScriptInfo::default())
99/// .set_scripttype("FFMPEG");
100///
101/// ass_file.components.v4
102/// .set_v4(V4Format::default())
103/// .set_primarycolour(&hexcolor);
104///
105/// ass_file.components.events
106/// .set_events(events);
107///
108/// AssFile::save_file(&ass_file, "new_subtitles.ass");
109///
110/// Ok(())
111///
112/// }
113/// ```
114///
115/// # Add Colors to Subtitles.
116///
117/// You can add individual colors to each subtitles using the `.set_colour()` function. This
118/// function takes HexColor. Make sure that you are using rand + std features to generate random colors via rand out of the box.
119///
120/// ```rust
121/// use hex_color::HexColor;
122/// use ass_parser::Dialogue;
123/// use ass_parser::Events;
124///
125/// let random_color:HexColor = rand::random();
126///
127/// let dialogue = Dialogue::default()
128/// .set_text("Hello Friend!")
129/// .set_start("0:00:00.50")
130/// .set_end("0:00:00.58")
131/// .set_colour(random_color);
132///
133/// let events = Events::new()
134/// .add_first_dialogue(dialogue).expect("Unable to add Dialogue");
135/// ```
136///
137/// # Modify Existing ASS files.
138///
139/// Use the `from_file` function of AssFile to modify and change the contents or appearance.
140///
141/// ``` rust
142/// use ass_parser::{AssFile, Dialogue, AssFileOptions};
143/// use hex_color::HexColor;
144///
145/// fn main() -> Result<(), std::io::Error>{
146/// let mut ass_file = AssFile::from_file("./examples/subtitles.ass")?;
147/// let dialogue = Dialogue::default()
148/// .set_text("Hello Friend!");
149/// let primary_color = AssFileOptions::get_ass_color(HexColor::RED);
150///
151///
152/// ass_file.components.v4
153/// .set_primarycolour(&primary_color);
154///
155/// ass_file.components.events
156/// .add_dialogue(dialogue);
157///
158/// AssFile::save_file(&ass_file, "new_subtitles.ass");
159///
160/// Ok(())
161/// }
162/// ```
163/// # Read Contents of ASS Files
164///
165/// To retrieve the values of fields present in each dialogue. You can load a `.ass` file and then access each dialogue details.
166///
167/// ```rust
168/// use ass_parser::{AssFile, Dialogue};
169///
170/// let ass_file = AssFile::from_file("examples/subtitles.ass").expect("Unable to find file");
171/// let dialogues: Vec<Dialogue> = ass_file.events.get_dialogues();
172///
173/// for dialogue in dialogues {
174/// println!("layer: {:?}", &dialogue.get_layer());
175/// println!("name: {:?}", &dialogue.get_name());
176/// println!("end: {:?}", &dialogue.get_end());
177/// println!("start: {:?}", &dialogue.get_start());
178/// println!("text: {:?}", &dialogue.get_text());
179/// println!("marginl: {:?}", &dialogue.get_marginl());
180/// println!("marginr: {:?}", &dialogue.get_marginr());
181/// println!("marginv: {:?}", &dialogue.get_marginv());
182/// println!("style: {:?}", &dialogue.get_style());
183/// println!("effect: {:?}", &dialogue.get_effect());
184/// println!("colour: {:?}", &dialogue.get_colour());
185/// }
186/// ```
187///
188///
189/// # Added Support for SubRip files.
190///
191/// Now you can load `.srt` files and convert them to `.ass` files and even modify them on the
192/// process too. Here is an example from the `examples` directory.
193///
194/// In this example we load an SubRip file (`RapGod.srt`) and extract each subtitle from it and
195/// modify them by adding random colors to each subtitle. Then finally converting it to a `.ass`
196/// file and saving it.
197///
198/// ```rust
199/// use hex_color::HexColor;
200/// use ass_parser::{AssFile, AssFileOptions};
201/// use ass_parser::{ScriptInfo, V4Format, Events, Dialogue};
202/// use rand;
203///
204/// fn main() {
205/// let hexcolor = AssFileOptions::get_ass_color(HexColor::YELLOW);
206/// let srt_file = AssFile::from_srt("./examples/RapGod.srt");
207/// let mut ass_file = AssFile::new();
208/// let mut event = Events::default();
209///
210/// for srt_seg in srt_file.iter() {
211/// let start = &srt_seg.start;
212/// let end = &srt_seg.end;
213/// let text = &srt_seg.text;
214///
215/// let random_color:HexColor = rand::random();
216///
217/// let dialogue = Dialogue::default()
218/// .set_start(&start)
219/// .set_end(&end)
220/// .set_text(&text)
221/// .set_colour(random_color);
222///
223/// event.add_dialogue(dialogue);
224/// }
225///
226///
227/// ass_file.components.script
228/// .set_script(ScriptInfo::default());
229///
230///
231///
232/// ass_file.components.v4
233/// .set_v4(V4Format::default())
234/// .set_primarycolour(&hexcolor);
235/// ass_file.components.events
236/// .set_events(event);
237///
238/// AssFile::save_file(&ass_file, "new_subtitle.ass");
239/// }
240/// ```
241///
242///
243/// ## This will generate an ASS file which would be similiar to this
244///
245/// ```ass
246///ScriptType: FFMPEG
247///PlayResX: 384
248///PlayResY: 288
249///ScaledBorderAndShadow: yes
250///YCbCr Matrix: None
251///
252///
253///[V4+ Styles]
254///Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
255///Style: Default,Arial,16,&H0ffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1
256///
257///
258///[Events]
259///Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
260///Dialogue: 0,0:00:00.10,0:00:00.50,Default,,0,0,0,,Hello There!
261///Dialogue: 0,00:00.50,00:00.58,Default,,0,0,0,,Hello Friend!
262///Dialogue: 0,0:00:00.58,0:00:01.01,Default,,0,0,0,,Hello World!!
263/// ```
264/// # Events can also be created like this
265///
266///
267///```rust
268///use ass_parser::Dialogue;
269///use ass_parser::Events;
270///
271///let first_dialogue = Dialogue::default()
272/// .set_start("0:00:00.10")
273/// .set_end("0:00:00.50");
274///
275///let second_dialogue = Dialogue::default()
276/// .set_start("00:00.50")
277/// .set_end("00:00.58");
278///
279///let third_dialogue = Dialogue::default()
280/// .set_start("0:00:00.58")
281/// .set_end("0:00:01.01");
282///
283///let events = Events::new()
284/// .add_first_dialogue(first_dialogue).expect("Unable to add dialogue")
285/// .add_dialogue(second_dialogue)
286/// .add_dialogue(third_dialogue)
287/// .create();
288/// ```
289///
290/// You can burn this subtitle file to a video or use any video player to select a video file along
291/// with this subtitle file.
292///
293/// # Using [FFmpeg] to burn the video with the subtitles file.
294///
295/// You will first have to download and install [FFmpeg] on your system to try this. Once you have
296/// downloaded you can use the following command to burn the video file `video.avi` and the
297/// generated subtitle file `new_subtitles.ass` to a single output video file `output.avi`
298///
299/// ```shell
300/// ffmpeg -i video.avi -vf "ass=new_subtitles.ass" output.avi
301/// ```
302///!
303///! [FFmpeg]: https://www.ffmpeg.org/about.html
304///! [ass_parser]: https://github.com/Aavtic/ass_parser
305
306
307use hex_color::HexColor;
308use std::{fs, io::Read};
309use std::io::{Seek, Write};
310use std::ops::Deref;
311use std::fmt;
312use std::iter::Iterator;
313
314mod parser;
315
316type SrtData = parser::SrtContent;
317
318const SCRIPT_HEADER:&str = "[Script Info]";
319const SCRIPT_TYPE:&str = "ScriptType: ";
320const SCRIPT_PLAYRESX:&str = "PlayResX: ";
321const SCRIPT_PLAYRESY:&str = "PlayResY: ";
322const SCRIPT_SCALEDBORDERANDSHADOW:&str = "ScaledBorderAndShadow: ";
323const SCRIPT_YCBCR_MATRIX:&str = "YCbCr Matrix: ";
324const V4_HEADER:&str = "[V4+ Styles]";
325const V4_STYLE_HEAD:&str = "Style: ";
326const EVENTS_HEADER:&str = "[Events]";
327const EVENT_HEAD:&str = "Dialogue: ";
328
329
330type Result<T> = std::result::Result<T, IndexNotFound>;
331
332#[derive(Debug, Clone)]
333pub struct IndexNotFound;
334
335impl std::fmt::Display for IndexNotFound {
336 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
337 write!(f, "The Index is not found on Dialogues.")
338 }
339}
340
341
342/// The First part of any Advanced SubStation Alpha file is `Script Info`.
343/// This holds necessary information which include the version the resolution of subtitles etc of
344/// the `.ass` file.
345
346#[derive(Debug, PartialEq, Clone)]
347pub struct ScriptInfo {
348 scripttype: Option<String>,
349 playresx: Option<String>,
350 playresy: Option<String>,
351 scaledborderandshadow: Option<String>,
352 ycbcr_matrix: Option<String>,
353}
354
355impl ScriptInfo {
356 fn get_key_values(&self) -> Vec<[&str; 2]> {
357 let mut values = Vec::new();
358
359 if let Some(value) = &self.scripttype {
360 values.push([SCRIPT_TYPE, value])
361 }
362 if let Some(value) = &self.playresx {
363 values.push([SCRIPT_PLAYRESX, value])
364 }
365 if let Some(value) = &self.playresy{
366 values.push([SCRIPT_PLAYRESY, value])
367 }
368 if let Some(value) = &self.scaledborderandshadow {
369 values.push([SCRIPT_SCALEDBORDERANDSHADOW, value])
370 }
371 if let Some(value) = &self.ycbcr_matrix {
372 values.push([SCRIPT_YCBCR_MATRIX, value])
373 }
374 values
375 }
376}
377
378impl ScriptInfo {
379 fn new() -> Self {
380 Self {
381 scripttype: None,
382 playresx: None,
383 playresy: None,
384 scaledborderandshadow: None,
385 ycbcr_matrix: None,
386 }
387 }
388
389 pub fn set_script(&mut self, script: ScriptInfo) -> &mut ScriptInfo {
390 *self = script;
391 self
392 }
393}
394
395impl Default for ScriptInfo {
396 fn default() -> ScriptInfo {
397 ScriptInfo {
398 scripttype: Some("v4.00+".to_string()),
399 playresx: Some("384".to_string()),
400 playresy: Some("288".to_string()),
401 scaledborderandshadow: Some("yes".to_string()),
402 ycbcr_matrix: Some("None".to_string()),
403 }
404 }
405}
406
407impl ScriptInfo {
408 /// After creating the `AssFile` set the scripttype of the .ass file.
409 /// If you want to specify any, the default ScriptType from the original `.ass` file will be
410 /// used.
411 /// This is the SSA script format version eg. "V4.00". It is used by SSA to give a warning if
412 /// you are using a version of SSA older than the version that created the script.
413 /// ASS version is “V4.00+”.
414 pub fn set_scripttype(&mut self, value: &str) -> &mut Self {
415 self.scripttype = Some(value.to_string());
416 self
417 }
418 /// After creating the `AssFile` set the playresx of the .ass file.
419 ///
420 /// This is the height of the screen used by the script's author(s) when playing the script. SSA v4 will automatically select the nearest enabled setting, if you are using Directdraw playback.
421 ///
422 /// If you don't want to specify any, the default playresx from the original `.ass` file will be
423 /// used.
424 pub fn set_playresx(&mut self, value: &str) -> &mut Self {
425 self.playresx = Some(value.to_string());
426 self
427 }
428 /// After creating the `AssFile` set the playresy of the .ass file.
429 ///
430 /// This is the height of the screen used by the script's author(s) when playing the script. SSA v4 will automatically select the nearest enabled setting, if you are using Directdraw playback.
431 ///
432 /// If you want to specify any, the default playresy from the original `.ass` file will be
433 /// used.
434 ///
435 pub fn set_playresy(&mut self, value: &str) -> &mut Self {
436 self.playresy = Some(value.to_string());
437 self
438 }
439 /// After creating the `AssFile` set the scaledborderandshadow of the .ass file.
440 /// If you want to specify any, the default scaledborderandshadowfrom the original `.ass` file will be
441 /// used.
442 pub fn set_scaledborderandshadow(&mut self, value: &str) -> &mut Self {
443 self.scaledborderandshadow = Some(value.to_string());
444 self
445 }
446 /// After creating the `AssFile` set the ycbcr_matrix( of the .ass file.
447 /// If you want to specify any, the default ycbcr_matrix from the original `.ass` file will be
448 /// used.
449 pub fn set_ycbcr_matrix(&mut self, value: &str) -> &mut Self {
450 self.ycbcr_matrix = Some(value.to_string());
451 self
452 }
453}
454
455
456/// # V4Format
457///
458/// The Second part of any Advanced SubStation Alpha file is `V4Format`.
459/// This is the part which has fields separated by comma which specify the format, styling,
460/// encoding colors and many other important parts of the the `.ass` file.
461
462#[derive(Debug, PartialEq, Clone)]
463pub struct V4Format {
464 name: Option<String>,
465 fontname: Option<String>,
466 fontsize: Option<String>,
467 primarycolour: Option<String>,
468 secondarycolour: Option<String>,
469 outlinecolour: Option<String>,
470 backcolour: Option<String>,
471 bold: Option<String>,
472 italic: Option<String>,
473 underline: Option<String>,
474 strikeout: Option<String>,
475 scalex: Option<String>,
476 scaley: Option<String>,
477 spacing: Option<String>,
478 angle: Option<String>,
479 borderstyle: Option<String>,
480 outline: Option<String>,
481 shadow: Option<String>,
482 alignment: Option<String>,
483 marginl: Option<String>,
484 marginr: Option<String>,
485 marginv: Option<String>,
486 encoding: Option<String>,
487}
488
489impl V4Format {
490 fn new() -> V4Format {
491 Self {
492 name: None,
493 fontname: None,
494 fontsize: None,
495 primarycolour: None,
496 secondarycolour: None,
497 outlinecolour: None,
498 backcolour: None,
499 bold: None,
500 italic: None,
501 underline: None,
502 strikeout: None,
503 scalex: None,
504 scaley: None,
505 spacing: None,
506 angle: None,
507 borderstyle: None,
508 outline: None,
509 shadow: None,
510 alignment: None,
511 marginl: None,
512 marginr: None,
513 marginv: None,
514 encoding: None,
515 }
516 }
517}
518
519impl Default for V4Format {
520 /// V4 Set with the common '`Default`' Format for `Advanced SubStation Alpha`.
521 fn default() -> V4Format {
522 V4Format {
523 name: Some("Default".to_string()),
524 fontname: Some("Arial".to_string()),
525 fontsize: Some("16".to_string()),
526 primarycolour: Some("&Hffffff".to_string()),
527 secondarycolour: Some("&Hffffff".to_string()),
528 outlinecolour:Some("&H0".to_string()),
529 backcolour: Some("&H0".to_string()),
530 bold: Some("0".to_string()),
531 italic: Some("0".to_string()),
532 underline: Some("0".to_string()),
533 strikeout: Some("0".to_string()),
534 scalex: Some("100".to_string()),
535 scaley: Some("100".to_string()),
536 spacing: Some("0".to_string()),
537 angle:Some("0".to_string()),
538 borderstyle:Some("1".to_string()),
539 outline: Some("1".to_string()),
540 shadow: Some("0".to_string()),
541 alignment: Some("2".to_string()),
542 marginl:Some("10".to_string()),
543 marginr: Some("10".to_string()),
544 marginv: Some("10".to_string()),
545 encoding: Some("1".to_string()),
546 }
547 }
548}
549
550impl V4Format {
551 /// Set V4 from a V4 Struct.
552 pub fn set_v4(&mut self, v4: V4Format) -> &mut V4Format {
553 *self = v4;
554 self
555 }
556 fn get_array(&self) -> [&Option<String>; 23] {
557 [
558 &self.name,
559 &self.fontname,
560 &self.fontsize,
561 &self.primarycolour,
562 &self.secondarycolour,
563 &self.outlinecolour,
564 &self.backcolour,
565 &self.bold,
566 &self.italic,
567 &self.underline,
568 &self.strikeout,
569 &self.scalex,
570 &self.scaley,
571 &self.spacing,
572 &self.angle,
573 &self.borderstyle,
574 &self.outline,
575 &self.shadow,
576 &self.alignment,
577 &self.marginl,
578 &self.marginr,
579 &self.marginv,
580 &self.encoding,
581 ]
582 }
583
584}
585
586impl V4Format {
587 // Ik this looks crazy. but what do?
588 /// set the name for the V4 field.
589 /// The name of the Style. Case sensitive. Cannot include commas
590 pub fn set_name(&mut self,
591 value: &str) -> &mut Self{
592 self.name = Some(value.to_string());
593 self
594 }
595 /// set the fontname for the V4 field.
596 /// The fontname as used by Windows. Case-sensitive.
597 pub fn set_fontname(&mut self,
598 value: &str) -> &mut Self{
599 self.fontname = Some(value.to_string());
600 self
601 }
602 /// set the fontsize for the V4 field.
603 pub fn set_fontsize(&mut self,
604 value: &str) -> &mut Self{
605 self.fontsize = Some(value.to_string());
606 self
607 }
608 /// set the primarycolour for the V4 field.
609 /// ```rust
610 /// use ass_parser::{AssFile, ScriptInfo, V4Format, Events, AssFileOptions};
611 /// use hex_color::HexColor;
612 ///
613 /// fn main() {
614 /// let mut ass_file = AssFile::new();
615 /// let hexcolor = AssFileOptions::get_ass_color(HexColor::YELLOW);
616 ///
617 /// ass_file.components.script
618 /// .set_script(ScriptInfo::default());
619 ///
620 /// ass_file.components.v4
621 /// .set_v4(V4Format::default())
622 /// .set_primarycolour(&hexcolor);
623 ///
624 /// ass_file.components.events
625 /// .set_events(Events::default());
626 ///
627 /// AssFile::save_file(&ass_file, "new_subtitles.ass")
628 /// }
629 /// ```
630 pub fn set_primarycolour(&mut self,
631 value: &str) -> &mut Self{
632 self.primarycolour = Some(value.to_string());
633 self
634 }
635 /// set the secondarycolour for the V4 field.
636 /// ```rust
637 /// use ass_parser::{AssFile, ScriptInfo, V4Format, Events, AssFileOptions};
638 /// use hex_color::HexColor;
639 ///
640 /// fn main() {
641 /// let mut ass_file = AssFile::new();
642 /// let hexcolor = AssFileOptions::get_ass_color(HexColor::YELLOW);
643 ///
644 /// ass_file.components.script
645 /// .set_script(ScriptInfo::default());
646 ///
647 /// ass_file.components.v4
648 /// .set_v4(V4Format::default())
649 /// .set_secondarycolour(&hexcolor);
650 ///
651 /// ass_file.components.events
652 /// .set_events(Events::default());
653 ///
654 /// AssFile::save_file(&ass_file, "new_subtitles.ass")
655 /// }
656 /// ```
657 pub fn set_secondarycolour(&mut self,
658 value: &str) -> &mut Self{
659 self.secondarycolour = Some(value.to_string());
660 self
661 }
662 /// set the outlinecolour for the V4 field.
663 /// ```rust
664 /// use ass_parser::{AssFile, ScriptInfo, V4Format, Events, AssFileOptions};
665 /// use hex_color::HexColor;
666 ///
667 /// fn main() {
668 /// let mut ass_file = AssFile::new();
669 /// let hexcolor = AssFileOptions::get_ass_color(HexColor::YELLOW);
670 ///
671 /// ass_file.components.script
672 /// .set_script(ScriptInfo::default());
673 ///
674 /// ass_file.components.v4
675 /// .set_v4(V4Format::default())
676 /// .set_outlinecolour(&hexcolor);
677 ///
678 /// ass_file.components.events
679 /// .set_events(Events::default());
680 ///
681 /// AssFile::save_file(&ass_file, "new_subtitles.ass")
682 /// }
683 /// ```
684 pub fn set_outlinecolour(&mut self,
685 value: &str) -> &mut Self{
686 self.outlinecolour = Some(value.to_string());
687 self
688 }
689 /// set the backcolour for the V4 field.
690 /// ```rust
691 /// use ass_parser::{AssFile, ScriptInfo, V4Format, Events, AssFileOptions};
692 /// use hex_color::HexColor;
693 ///
694 /// fn main() {
695 /// let mut ass_file = AssFile::new();
696 /// let hexcolor = AssFileOptions::get_ass_color(HexColor::YELLOW);
697 ///
698 /// ass_file.components.script
699 /// .set_script(ScriptInfo::default());
700 ///
701 /// ass_file.components.v4
702 /// .set_v4(V4Format::default())
703 /// .set_backcolour(&hexcolor);
704 ///
705 /// ass_file.components.events
706 /// .set_events(Events::default());
707 ///
708 /// AssFile::save_file(&ass_file, "new_subtitles.ass")
709 /// }
710 /// ```
711 pub fn set_backcolour(&mut self,
712 value: &str) -> &mut Self{
713 self.backcolour = Some(value.to_string());
714 self
715 }
716 /// set the bold for the V4 field.
717 /// This defines whether text is bold (true) or not (false). -1 is True, 0 is False. This is independant of the Italic attribute - you can have have text which is both bold and italic
718 pub fn set_bold(&mut self,
719 value: &str) -> &mut Self{
720 self.bold = Some(value.to_string());
721 self
722 }
723 /// set the italic for the V4 field.
724 /// This defines whether text is italic (true) or not (false). -1 is True, 0 is False. This is independant of the bold attribute - you can have have text which is both bold and italic.
725 pub fn set_italic(&mut self,
726 value: &str) -> &mut Self{
727 self.italic = Some(value.to_string());
728 self
729 }
730 /// set the underline for the V4 field.
731 /// use either of [-1 or 0] where -1 is considered True and 0 is considered False.
732 pub fn set_underline(&mut self,
733 value: &str) -> &mut Self{
734 self.underline = Some(value.to_string());
735 self
736 }
737 /// set the strikeout for the V4 field.
738 /// use either of [-1 or 0] where -1 is considered True and 0 is considered False.
739 pub fn set_strikeout(&mut self,
740 value: &str) -> &mut Self{
741 self.strikeout = Some(value.to_string());
742 self
743 }
744 /// set the scalex for the V4 field.
745 /// ScaleX. Modifies the width of the font. [percent]
746 pub fn set_scalex(&mut self,
747 value: &str) -> &mut Self{
748 self.scalex = Some(value.to_string());
749 self
750 }
751 /// set the scaley for the V4 field.
752 /// ScaleX. Modifies the height of the font. [percent]
753 pub fn set_scaley(&mut self,
754 value: &str) -> &mut Self{
755 self.scaley = Some(value.to_string());
756 self
757 }
758 /// set the spacing for the V4 field.
759 /// Extra space between characters. [pixels]
760 pub fn set_spacing(&mut self,
761 value: &str) -> &mut Self{
762 self.spacing = Some(value.to_string());
763 self
764 }
765 /// set the angle for the V4 field.
766 /// The origin of the rotation is defined by the alignment. Can be a floating point number. [degrees]
767 pub fn set_angle(&mut self,
768 value: &str) -> &mut Self{
769 self.angle = Some(value.to_string());
770 self
771 }
772 /// set the borderstyle for the V4 field.
773 /// pass either 1 or 3. where 1=Outline + drop shadow, 3=Opaque box.
774 pub fn set_borderstyle(&mut self,
775 value: &str) -> &mut Self{
776 self.borderstyle = Some(value.to_string());
777 self
778 }
779 /// set the outline for the V4 field.
780 /// If BorderStyle is 1, then this specifies the width of the outline around the text, in pixels.
781 /// Values may be 0, 1, 2, 3 or 4.
782 pub fn set_outline(&mut self,
783 value: &str) -> &mut Self{
784 self.outline = Some(value.to_string());
785 self
786 }
787 /// set the shadow for the V4 field.
788 /// If BorderStyle is 1, then this specifies the depth of the drop shadow behind the text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is always used in addition to an outline.
789 pub fn set_shadow(&mut self,
790 value: &str) -> &mut Self{
791 self.shadow = Some(value.to_string());
792 self
793 }
794 /// set the alignment for the V4 field.
795 /// This sets how text is "justified" within the Left/Right onscreen margins, and also the vertical placing. Values may be 1=Left, 2=Centered, 3=Right. Add 4 to the value for a "Toptitle". Add 8 to the value for a "Midtitle".
796 /// eg. 5 = left-justified toptitle
797 pub fn set_alignment(&mut self,
798 value: &str) -> &mut Self{
799 self.alignment = Some(value.to_string());
800 self
801 }
802 /// set the marginl for the V4 field.
803 /// This defines the Left Margin in pixels. It is the distance from the left-hand edge of the screen.The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed.
804 pub fn set_marginl(&mut self,
805 value: &str) -> &mut Self{
806 self.marginl = Some(value.to_string());
807 self
808 }
809 /// set the marginr for the V4 field.
810 /// This defines the Right Margin in pixels. It is the distance from the right-hand edge of the screen. The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed.
811 pub fn set_marginr(&mut self,
812 value: &str) -> &mut Self{
813 self.marginr = Some(value.to_string());
814 self
815 }
816 /// set the marginv for the V4 field.
817 /// This defines the vertical Left Margin in pixels.
818 /// For a subtitle, it is the distance from the bottom of the screen.
819 /// For a toptitle, it is the distance from the top of the screen.
820 /// For a midtitle, the value is ignored - the text will be vertically centred.
821 pub fn set_marginv(&mut self,
822 value: &str) -> &mut Self{
823 self.marginv = Some(value.to_string());
824 self
825 }
826 /// set the encoding for the V4 field.
827 /// This specifies the font character set or encoding and on multi-lingual Windows installations it provides access to characters used in multiple than one languages. It is usually 0 (zero) for English (Western, ANSI) Windows.
828 fn set_encoding(&mut self, value: &str) -> &mut Self{
829 self.encoding = Some(value.to_string());
830 self
831 }
832}
833
834
835/// # Events
836/// In `Advanced SubStation Alpha` Events is the core part of the subtitle file.
837/// This contains Dialogues which can be subtitle text. and even Graphics.
838
839#[derive(Debug, PartialEq, Clone)]
840pub struct Events {
841 pub dialogues: Dialogues,
842}
843
844impl Events {
845 /// Returns a Clone of `Dialogues`
846 /// You can then use this to access fields of `Dialogue`.
847 /// ```rust
848 ///
849 /// use ass_parser::AssFile;
850 ///
851 /// let mut ass_file = ass_parser::AssFile::from_file("./examples/subtitles.ass").expect("error while reading file.");
852 /// let dialogues = ass_file.events.get_dialogues().clone();
853
854 /// for dialogue in dialogues {
855 /// println!("layer: {:?}", &dialogue.get_layer());
856 /// println!("name: {:?}", &dialogue.get_name());
857 /// println!("end: {:?}", &dialogue.get_end());
858 /// println!("start: {:?}", &dialogue.get_start());
859 /// println!("text: {:?}", &dialogue.get_text());
860 /// println!("marginl: {:?}", &dialogue.get_marginl());
861 /// println!("marginr: {:?}", &dialogue.get_marginr());
862 /// println!("marginv: {:?}", &dialogue.get_marginv());
863 /// println!("style: {:?}", &dialogue.get_style());
864 /// println!("effect: {:?}", &dialogue.get_effect());
865 /// println!("colour: {:?}", &dialogue.get_colour());
866 /// }
867 /// ```
868 pub fn get_dialogues(&self) -> Vec<Dialogue> {
869 return self.dialogues.dialogues.clone();
870 }
871}
872
873impl Events {
874 /// Create a new instance of Event.
875 /// This will have `None` for all the fields for EventFormat.
876 pub fn new() -> Events {
877 let dialogue = Dialogue::new();
878 Events {
879 dialogues:
880 Dialogues {
881 dialogues:
882 vec![
883 dialogue
884 ]
885 }
886 }
887 }
888
889 /// Create the final Event
890 /// This simply consumes the mutable self and returns self.
891 /// Call this function at the end of constructing an `Event`.
892 ///
893 /// # Example
894 /// ```rust
895 /// use ass_parser::Dialogue;
896 /// use ass_parser::Events;
897 ///
898 /// let dialogue = Dialogue::default();
899 /// let events = Events::new()
900 /// .add_first_dialogue(dialogue.clone().set_text("Hello There!")).unwrap()
901 /// .add_dialogue(dialogue.clone().set_text("Hello Friend!"))
902 /// .add_n_dialogue(1, dialogue.clone().set_text("Hello Friend :)")).unwrap()
903 /// .add_last_dialogue(dialogue.set_text("Bye Friend.")).unwrap()
904 /// .create();
905 /// ```
906
907 pub fn create(&mut self) -> Self {
908 self.clone()
909 }
910
911 /// Add a dialogue to the first of the `Events` Struct.
912 /// # Example
913 /// ```rust
914 /// use ass_parser::Dialogue;
915 /// use ass_parser::Events;
916 ///
917 /// let dialogue = Dialogue::default();
918 /// let events = Events::new()
919 /// .add_first_dialogue(dialogue.set_text("Hello There!")).unwrap();
920 /// ```
921 ///
922 pub fn add_first_dialogue(&mut self, dialogue: Dialogue) -> Result<&mut Self> {
923 match self.dialogues.dialogues.first_mut() {
924 Some(dlg) => {
925 *dlg = dialogue;
926 Ok(self)
927 },
928 None => {
929 Err(IndexNotFound)
930 }
931 }
932 }
933
934 /// Add a dialogue to the last of the `Events` Struct.
935 /// # Example
936 /// ```rust
937 /// use ass_parser::Dialogue;
938 /// use ass_parser::Events;
939 ///
940 /// let dialogue = Dialogue::default();
941 /// let events = Events::new()
942 /// .add_last_dialogue(dialogue.set_text("Hello There!")).unwrap();
943 /// ```
944 pub fn add_last_dialogue(&mut self, dialogue: Dialogue) -> Result<&mut Self> {
945 match self.dialogues.dialogues.last_mut() {
946 Some(dlg) => {
947 *dlg = dialogue;
948 Ok(self)
949 },
950 None => {
951 Err(IndexNotFound)
952 }
953 }
954 }
955
956 /// Add a dialogue to the nth position of the `Events` Struct.
957 /// # Example
958 /// ```rust
959 /// use ass_parser::Dialogue;
960 /// use ass_parser::Events;
961 ///
962 /// let dialogue = Dialogue::default();
963 /// let events = Events::new()
964 /// .add_n_dialogue(0, dialogue.set_text("Hello There!")).unwrap();
965 /// ```
966 pub fn add_n_dialogue(&mut self, n: usize, dialogue: Dialogue) -> Result<&mut Self> {
967 match self.dialogues.dialogues.get_mut(n) {
968 Some(dlg) => {
969 *dlg = dialogue;
970 Ok(self)
971 },
972 None => {
973 Err(IndexNotFound)
974 }
975 }
976 }
977
978 /// Add a dialogue to the end of the `Events` Struct.
979 /// # Example
980 /// ```rust
981 /// use ass_parser::Dialogue;
982 /// use ass_parser::Events;
983 ///
984 /// let dialogue = Dialogue::default();
985 /// let mut events = Events::new();
986 /// events.add_dialogue(dialogue.set_text("Hello There!"));
987 /// ```
988 pub fn add_dialogue(&mut self, dialogue: Dialogue) -> &mut Events {
989 self.dialogues.dialogues.push(dialogue);
990 self
991 }
992}
993
994impl Default for Events {
995 fn default() -> Events {
996 Events {
997 dialogues:
998 Dialogues {
999 dialogues: vec![
1000 Dialogue {
1001 event: EventFormat::default(),
1002 }
1003 ]
1004 }
1005 }
1006 }
1007}
1008
1009
1010impl Events {
1011 pub fn set_events(&mut self, events: Events) -> &mut Events {
1012 *self = events;
1013 self
1014 }
1015}
1016
1017/// # Dialogues
1018/// This stores each `Dialogue: ` field in an `Advanced SubStation File`
1019#[derive(Debug, PartialEq,Clone)]
1020pub struct Dialogues {
1021 pub dialogues: Vec<Dialogue>
1022}
1023
1024/// A single `Dialogue` which contain `event` which can be used to modify the state of a
1025/// `Dialogue`.
1026#[derive(Debug, PartialEq, Clone)]
1027pub struct Dialogue {
1028 event: EventFormat
1029}
1030
1031#[derive(Debug, PartialEq,Clone)]
1032struct EventFormat {
1033 layer: Option<String>,
1034 start: Option<String>,
1035 end: Option<String>,
1036 style: Option<String>,
1037 name: Option<String>,
1038 marginl: Option<String>,
1039 marginr: Option<String>,
1040 marginv: Option<String>,
1041 effect: Option<String>,
1042 text: Option<String>,
1043 color: Option<String>
1044}
1045
1046impl Default for EventFormat {
1047 fn default() -> EventFormat {
1048 EventFormat {
1049 layer: Some("0".to_string()),
1050 start: Some("0:00:00.00".to_string()),
1051 end: Some("0:00:00.00".to_string()),
1052 style: Some("Default".to_string()),
1053 name: Some("".to_string()),
1054 marginl: Some("0".to_string()),
1055 marginr: Some("0".to_string()),
1056 marginv: Some("0".to_string()),
1057 effect: Some("".to_string()),
1058 text: None,
1059 color: None,
1060 }
1061 }
1062}
1063
1064impl Dialogue {
1065 pub fn new() -> Self {
1066 Self {
1067 event: EventFormat {
1068 layer: None,
1069 start: None,
1070 end: None,
1071 style: None,
1072 name: None,
1073 marginl: None,
1074 marginr: None,
1075 marginv: None,
1076 effect: None,
1077 text: None,
1078 color: None,
1079 }
1080 }
1081 }
1082}
1083
1084impl Default for Dialogue {
1085 fn default() -> Dialogue {
1086 Dialogue {
1087 event: EventFormat::default()
1088 }
1089 }
1090}
1091
1092impl Dialogue {
1093 fn to_string(&self) -> String {
1094 let mut dialogue_string = String::new();
1095 dialogue_string.push_str(EVENT_HEAD);
1096 dialogue_string.push_str(&(self.event.layer.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1097 dialogue_string.push_str(&(self.event.start.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1098 dialogue_string.push_str(&(self.event.end.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1099 dialogue_string.push_str(&(self.event.style.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1100 dialogue_string.push_str(&(self.event.name.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1101 dialogue_string.push_str(&(self.event.marginl.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1102 dialogue_string.push_str(&(self.event.marginr.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1103 dialogue_string.push_str(&(self.event.marginv.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1104 dialogue_string.push_str(&(self.event.effect.as_ref().unwrap_or(&"".to_owned()).to_owned() + ","));
1105 dialogue_string.push_str(&(self.event.text.as_ref().unwrap_or(&"".to_owned()).to_owned() + "\n"));
1106
1107 return dialogue_string;
1108 }
1109}
1110
1111impl Dialogue {
1112 /// set the layer
1113 /// Layer (any integer)
1114 /// Subtitles having different layer number will be ignored during the collusion detection.
1115 /// Higher numbered layers will be drawn over the lower numbered.
1116 pub fn set_layer(mut self, value: &str) -> Self {
1117 self.event.layer = Some(value.to_string());
1118 self
1119 }
1120 /// set the start time of the subtitle.
1121 /// Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will appear onscreen. Note that there is a single digit for the hours!
1122 pub fn set_start(mut self, value: &str) -> Self {
1123 self.event.start = Some(value.to_string());
1124 self
1125 }
1126 /// set the end time of the subtitle.
1127 /// End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will disappear offscreen. Note that there is a single digit for the hours!
1128 pub fn set_end(mut self, value: &str) -> Self {
1129 self.event.end = Some(value.to_string());
1130 self
1131 }
1132 /// set the style.
1133 /// Style name. If it is "Default", then your own *Default style will be subtituted.
1134 ///However, the Default style used by the script author IS stored in the script even though SSA ignores it - so if you want to use it, the information is there - you could even change the Name in the Style definition line, so that it will appear in the list of "script" styles.
1135 pub fn set_style(mut self, value: &str) -> Self {
1136 self.event.style = Some(value.to_string());
1137 self
1138 }
1139 /// set name.
1140 /// Character name. This is the name of the character who speaks the dialogue. It is for information only, to make the script is easier to follow when editing/timing.
1141 pub fn set_name(mut self, value: &str) -> Self {
1142 self.event.name = Some(value.to_string());
1143 self
1144 }
1145 /// set the marginl
1146 /// 4-figure Left Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
1147 pub fn set_marginl(mut self, value: &str) -> Self {
1148 self.event.marginl = Some(value.to_string());
1149 self
1150 }
1151 /// set the marginr
1152 /// 4-figure Right Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
1153 pub fn set_marginr(mut self, value: &str) -> Self {
1154 self.event.marginr = Some(value.to_string());
1155 self
1156 }
1157 /// set the marginv
1158 /// 4-figure Bottom Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
1159 pub fn set_marginv(mut self, value: &str) -> Self {
1160 self.event.marginv = Some(value.to_string());
1161 self
1162 }
1163 /// set effects for the Dialogue object.
1164 /// Transition Effect. This is either empty, or contains information for one of the three transition effects implemented in SSA v4.x
1165 /// The effect names are case sensitive and must appear exactly as shown. The effect names do not have quote marks around them.
1166 /// "Karaoke" means that the text will be successively highlighted one word at a time.
1167 /// Karaoke as an effect type is obsolete.
1168 pub fn set_effect(mut self, value: &str) -> Self {
1169 self.event.effect = Some(value.to_string());
1170 self
1171 }
1172 /// set the text for the subtitle.
1173 /// Subtitle Text. This is the actual text which will be displayed as a subtitle onscreen. Everything after the 9th comma is treated as the subtitle text, so it can include commas.
1174 /// The text can include \n codes which is a line break, and can include Style Override control codes, which appear between braces { }.
1175 pub fn set_text(mut self, value: &str) -> Self {
1176 self.event.text = Some(value.to_string());
1177 self
1178 }
1179
1180 /// set the color of the subtitle.
1181 pub fn set_colour(mut self, color: HexColor) -> Self {
1182 let colour = AssFileOptions::get_ass_color_text(color);
1183 match &self.event.text {
1184 Some(text) => {
1185 let new_text = &(colour.clone() + text);
1186 // If you know a better way, please do create a pull request
1187 self = self.clone().set_text(new_text);
1188 self.event.color = Some(colour);
1189 self
1190 },
1191 None => {
1192 self.event.color = Some(colour.clone());
1193 self.set_text(&colour)
1194 }
1195 }
1196 }
1197}
1198
1199
1200impl Dialogue {
1201 /// get the layer of the subtitle
1202 pub fn get_layer(&self) -> Option<String> {
1203 return self.event.layer.clone();
1204 }
1205 /// get the start time of the `Dialogue`
1206 /// Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will appear onscreen. Note that there is a single digit for the hours!
1207 pub fn get_start(&self) -> Option<String> {
1208 return self.event.start.clone();
1209 }
1210 /// get the end time of the `Dialogue`.
1211 /// End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will disappear offscreen. Note that there is a single digit for the hours!
1212 pub fn get_end(&self) -> Option<String> {
1213 return self.event.end.clone();
1214 }
1215 /// get the style of the `Dialogue`.
1216 pub fn get_style(&self) -> Option<String> {
1217 return self.event.style.clone();
1218 }
1219 /// get the name of the `Dialogue`.
1220 /// Character name. This is the name of the character who speaks the dialogue. It is for information only, to make the script is easier to follow when editing/timing.
1221 pub fn get_name(&self) -> Option<String> {
1222 return self.event.name.clone();
1223 }
1224 /// get the marginl of the `Dialogue`.
1225 /// 4-figure Left Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
1226 pub fn get_marginl(&self) -> Option<String> {
1227 return self.event.marginl.clone();
1228 }
1229 /// get the marginr of the `Dialogue`.
1230 /// 4-figure Right Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
1231 pub fn get_marginr(&self) -> Option<String> {
1232 return self.event.marginr.clone();
1233 }
1234 /// get the marginv of the `Dialogue`.
1235 /// 4-figure Bottom Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
1236 pub fn get_marginv(&self) -> Option<String> {
1237 return self.event.marginv.clone();
1238 }
1239 /// get the effects for the Dialogue object.
1240 /// Transition Effect. This is either empty, or contains information for one of the three transition effects implemented in SSA v4.x
1241 /// The effect names are case sensitive and must appear exactly as shown. The effect names do not have quote marks around them.
1242 /// "Karaoke" means that the text will be successively highlighted one word at a time.
1243 /// Karaoke as an effect type is obsolete.
1244 pub fn get_effect(&self) -> Option<String> {
1245 return self.event.effect.clone();
1246 }
1247 /// get the text for the subtitle.
1248 /// Subtitle Text. This is the actual text which will be displayed as a subtitle onscreen. Everything after the 9th comma is treated as the subtitle text, so it can include commas.
1249 /// The text can include \n codes which is a line break, and can include Style Override control codes, which appear between braces { }.
1250 pub fn get_text(&self) -> Option<String> {
1251 return self.event.text.clone();
1252 }
1253
1254 /// get the color of the subtitle.
1255 pub fn get_colour(&self) -> Option<String> {
1256 return self.event.color.clone()
1257 }
1258}
1259
1260pub struct AssFileOptions{}
1261
1262/// `script`, `v4` and `event` are fields in `Components`
1263#[derive(Clone, PartialEq, Debug)]
1264pub struct Components {
1265 /// instance holding the scirpt field.
1266 pub script: ScriptInfo,
1267 /// instance holding the V4 field of.
1268 pub v4: V4Format,
1269 /// instance holding the Events field of.
1270 pub events: Events,
1271}
1272
1273
1274pub struct Srt {
1275 srt_data: SrtData,
1276}
1277
1278impl Srt {
1279 pub fn iter(&self) -> std::slice::Iter<'_, parser::SrtData> {
1280 let iterator = self.srt_data.iter();
1281 return iterator;
1282 }
1283}
1284
1285
1286/// # AssFile represents an instance of an existing `.ass` file.
1287/// The `AssFile::from_file function can be used to construct an `AssFile` from an existing `.ass
1288/// file`.
1289
1290#[derive(Clone, PartialEq,Debug)]
1291pub struct AssFile{
1292 _ass_file: String,
1293 /// Each components present in a `.ass` file.
1294 /// They are `script` `v4` and `events`.
1295 pub components: Components,
1296}
1297
1298impl Deref for AssFile {
1299 type Target = Components;
1300
1301 fn deref(&self) -> &Self::Target {
1302 &self.components
1303 }
1304}
1305
1306impl AssFile {
1307 pub fn new() -> AssFile {
1308 AssFile {
1309 _ass_file: String::new(),
1310 components: Components {
1311 script: ScriptInfo::new(),
1312 v4: V4Format::new(),
1313 events: Events::new(),
1314 }
1315 }
1316 }
1317
1318 /// Load Subtitles from a SubRip file.
1319 ///
1320 /// # Example
1321 /// ```rust
1322 /// use ass_parser::AssFile;
1323 ///
1324 /// let srt_file = AssFile::from_srt("./examples/RapGod.srt");
1325 ///
1326 /// for srt_seg in srt_file.iter() {
1327 /// let start = &srt_seg.start;
1328 /// let end = &srt_seg.end;
1329 /// let text = &srt_seg.text;
1330 ///
1331 /// println!("Start: {}\nEnd: {}\nText: {}", start, end, text);
1332 ///}
1333 ///```
1334 pub fn from_srt(filename: &str) -> Srt {
1335 let file_contents = get_contents(filename).unwrap();
1336 let srtdata = parser::SrtData::new();
1337 let srt = srtdata.parse_srt(file_contents);
1338
1339 Srt {
1340 srt_data: srt,
1341 }
1342 }
1343}
1344
1345struct Parser;
1346impl Parser {
1347 fn new() -> Parser {
1348 Parser
1349 }
1350
1351 fn stringify_script(&self, scriptinfo: Vec<[&str; 2]>) -> String {
1352 let mut contents = String::new();
1353
1354 for pair in scriptinfo {
1355 contents.push_str(&(pair[0].to_owned() + pair[1] + "\n"))
1356 }
1357 contents
1358 }
1359
1360 fn combine_components(&self, components: &Components) -> String {
1361 let components = components.clone();
1362 let script = components.script;
1363 let v4 = components.v4;
1364 let events = components.events;
1365 let scriptinfo = script.get_key_values();
1366
1367 let script_data = &self.stringify_script(scriptinfo);
1368 let v4_data = &self.plug_v4(v4);
1369 let event_data = &self.plug_events(events);
1370 let total_data = format!("{}\n\n{}\n\n{}", script_data, v4_data, event_data);
1371
1372 return total_data;
1373 }
1374
1375 fn _plug_script(&self, script_lines: Vec<String>, scriptinfo: ScriptInfo) -> String {
1376 let mut new_lines = script_lines.clone();
1377 let mut total_lines = String::new();
1378 let script_type = scriptinfo.scripttype.unwrap();
1379 let playresx = scriptinfo.playresx.unwrap();
1380 let playresy = scriptinfo.playresy.unwrap();
1381 let scaledborderandshadow = scriptinfo.scaledborderandshadow.unwrap();
1382 let ycbcr_matrix = scriptinfo.ycbcr_matrix.unwrap();
1383
1384
1385 for (i, line) in script_lines.iter().enumerate() {
1386 if line.starts_with(SCRIPT_TYPE) {
1387 new_lines[i] = line[..SCRIPT_TYPE.len()].to_owned() + &script_type + "\n";
1388 continue
1389 } else if line.starts_with(SCRIPT_PLAYRESX){
1390 new_lines[i] = line[..SCRIPT_PLAYRESX.len()].to_owned() + &playresx + "\n";
1391 continue
1392 } else if line.starts_with(SCRIPT_PLAYRESY){
1393 new_lines[i] = line[..SCRIPT_PLAYRESY.len()].to_owned() + &playresy + "\n";
1394 continue;
1395 } else if line.starts_with(SCRIPT_SCALEDBORDERANDSHADOW){
1396 new_lines[i] = line[..SCRIPT_SCALEDBORDERANDSHADOW.len()].to_owned() + &scaledborderandshadow + "\n";
1397 continue;
1398 } else if line.starts_with(SCRIPT_YCBCR_MATRIX){
1399 new_lines[i] = line[..SCRIPT_YCBCR_MATRIX.len()].to_owned() + &ycbcr_matrix + "\n";
1400 continue;
1401 }
1402 }
1403
1404 for line in new_lines {
1405 total_lines.push_str(line.as_str());
1406 }
1407
1408 return total_lines;
1409 }
1410
1411 fn plug_v4(&self, v4_info: V4Format) -> String {
1412 let array = v4_info.get_array();
1413 let mut values = Vec::new();
1414 let mut v4_lines = Vec::new();
1415 let mut total_v4 = String::new();
1416 v4_lines.push("[V4+ Styles]\n".to_string());
1417 v4_lines.push("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n".to_string());
1418 v4_lines.push(V4_STYLE_HEAD.to_string());
1419
1420 for (i, value) in array.into_iter().enumerate() {
1421 let style_val = value.clone().unwrap();
1422 if i < (array.len()-1) {
1423 values.push(style_val + ",");
1424 } else {
1425 values.push(style_val);
1426 }
1427 }
1428
1429 values.push("\n".to_string());
1430
1431 v4_lines.append(&mut values);
1432
1433 for line in v4_lines {
1434 total_v4.push_str(line.as_str());
1435 }
1436 return total_v4;
1437 }
1438
1439 fn plug_events(&self, event_info: Events) -> String {
1440 let mut lines = Vec::new();
1441 let mut total_events = String::new();
1442 let dialogues = event_info.dialogues.dialogues;
1443 lines.push(EVENTS_HEADER.to_string() + "\n");
1444 lines.push("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text".to_string() + "\n");
1445
1446 for dialogue in dialogues {
1447 let dialogue_line = dialogue.to_string();
1448 lines.push(dialogue_line);
1449 }
1450 for line in lines {
1451 total_events.push_str(line.as_str());
1452 }
1453
1454 return total_events;
1455 }
1456
1457 fn get_each_components(&self, file_contents: String) -> Components {
1458 let lines:Vec<&str> = file_contents.split("\n").collect();
1459 let script_lines = &self.get_info(&lines, SCRIPT_HEADER);
1460 let v4_lines = &self.get_info(&lines, V4_HEADER);
1461 let events_lines = &self.get_info(&lines, EVENTS_HEADER);
1462
1463 let script = self.parse_script(script_lines.to_vec()).unwrap();
1464 let v4 = self.parse_v4(v4_lines.to_vec()).unwrap();
1465 let events = self.parse_event(events_lines.to_vec()).unwrap();
1466
1467 Components {
1468 script,
1469 v4,
1470 events,
1471 }.clone()
1472 }
1473 fn parse_script(&self, script_lines: Vec<String>) -> Option<ScriptInfo> {
1474 let mut script_type: Option<String>= None;
1475 let mut script_playerresx: Option<String>= None;
1476 let mut script_playerresy: Option<String>= None;
1477 let mut script_scaledborderandshadow: Option<String>= None;
1478 let mut script_ycbcr_matrix: Option<String>= None;
1479
1480 for line in &script_lines {
1481 if line.starts_with(SCRIPT_TYPE) {
1482 script_type = Some(line[SCRIPT_TYPE.len()..].to_owned());
1483 continue
1484 } else if line.starts_with(SCRIPT_PLAYRESX){
1485 script_playerresx= Some(line[SCRIPT_PLAYRESX.len()..].to_owned());
1486 continue
1487 } else if line.starts_with(SCRIPT_PLAYRESY){
1488 script_playerresy= Some(line[SCRIPT_PLAYRESY.len()..].to_owned());
1489 continue;
1490 } else if line.starts_with(SCRIPT_SCALEDBORDERANDSHADOW){
1491 script_scaledborderandshadow = Some(line[SCRIPT_SCALEDBORDERANDSHADOW.len()..].to_owned());
1492 continue;
1493 } else if line.starts_with(SCRIPT_YCBCR_MATRIX){
1494 script_ycbcr_matrix = Some(line[SCRIPT_YCBCR_MATRIX.len()..].to_owned());
1495 continue;
1496 }
1497 }
1498 println!("{:?}, {:?}, {:?}, {:?} {:?}", script_type,
1499 script_playerresx,
1500 script_playerresy,
1501 script_scaledborderandshadow,
1502 script_ycbcr_matrix);
1503
1504
1505 let script_info: ScriptInfo = {
1506 let mut scriptinfo: &mut ScriptInfo = &mut ScriptInfo::new();
1507 if script_type.is_some() {
1508 scriptinfo = scriptinfo.set_scripttype(&script_type.unwrap());
1509 }
1510
1511 if script_playerresx.is_some() {
1512 scriptinfo = scriptinfo.set_playresx(&script_playerresx.unwrap());
1513 }
1514
1515 if script_playerresy.is_some() {
1516 scriptinfo = scriptinfo.set_playresy(&script_playerresy.unwrap());
1517 }
1518
1519 if script_scaledborderandshadow.is_some() {
1520 scriptinfo = scriptinfo.set_scaledborderandshadow(&script_scaledborderandshadow.unwrap())
1521 }
1522
1523 if script_ycbcr_matrix.is_some() {
1524 scriptinfo = scriptinfo.set_ycbcr_matrix(&script_ycbcr_matrix.unwrap())
1525 }
1526
1527 scriptinfo.clone()
1528 };
1529
1530
1531 // Error prone unwraps
1532 //let script_info = scriptinfo.
1533 // set_scripttype(&script_type.unwrap()).
1534 // set_playresx(&script_playerresx.unwrap()).
1535 // set_playresy(&script_playerresy.unwrap()).
1536 // set_scaledborderandshadow(&script_scaledborderandshadow.unwrap()).
1537 // set_ycbcr_matrix(&script_ycbcr_matrix.unwrap()).clone();
1538
1539 Some(script_info)
1540}
1541 fn parse_event(&self, event_lines: Vec<String>) -> Option<Events>{
1542 // let mut events = Vec::new();
1543 let mut raw_dialogues = Vec::new();
1544 let mut dialogues = Vec::new();
1545
1546 for line in event_lines {
1547 if line.starts_with(EVENT_HEAD) {
1548 raw_dialogues.push(line[EVENT_HEAD.len()..].to_string());
1549 }
1550 }
1551 for dialogue in &raw_dialogues {
1552 let splitted_dialogue: Vec<&str> = dialogue.split(',').collect();
1553 let dialogue = Dialogue::new().
1554 set_layer(splitted_dialogue[0]).
1555 set_start(splitted_dialogue[1]).
1556 set_end(splitted_dialogue[2]).
1557 set_style(splitted_dialogue[3]).
1558 set_name(splitted_dialogue[4]).
1559 set_marginl(splitted_dialogue[5]).
1560 set_marginr(splitted_dialogue[6]).
1561 set_marginv(splitted_dialogue[7]).
1562 set_effect(splitted_dialogue[8]).
1563 set_text(splitted_dialogue[9]);
1564
1565 dialogues.push(dialogue);
1566 }
1567
1568 let dialogues = Dialogues {
1569 dialogues,
1570 };
1571
1572 return Some(Events {
1573 dialogues,
1574 })
1575
1576
1577 }
1578 fn parse_v4(&self, v4_lines: Vec<String>) -> Option<V4Format>{
1579 let mut style_line: Option::<String> = None;
1580 for line in &v4_lines {
1581 if line.starts_with(V4_STYLE_HEAD) {
1582 style_line = Some(line[V4_STYLE_HEAD.len()..].to_string());
1583 break;
1584 }
1585 }
1586 if let Some(style_data) = style_line {
1587 let values: Vec<&str> = style_data.split(',').collect();
1588 println!("{:?}", values);
1589
1590 let v4format = V4Format::new().
1591 set_name(values[0]).
1592 set_fontname(values[1]).
1593 set_fontsize(values[2]).
1594 set_primarycolour(values[3]).
1595 set_secondarycolour(values[4]).
1596 set_outlinecolour(values[5]).
1597 set_backcolour(values[6]).
1598 set_bold(values[7]).
1599 set_italic(values[8]).
1600 set_underline(values[9]).
1601 set_strikeout(values[10]).
1602 set_scalex(values[11]).
1603 set_scaley(values[12]).
1604 set_spacing(values[13]).
1605 set_angle(values[14]).
1606 set_borderstyle(values[15]).
1607 set_outline(values[16]).
1608 set_shadow(values[17]).
1609 set_alignment(values[18]).
1610 set_marginl(values[19]).
1611 set_marginr(values[20]).
1612 set_marginv(values[22]).
1613 set_encoding(values[22]).clone();
1614
1615 return Some(v4format);
1616 } else {
1617 eprintln!("Unable to parse v4!");
1618 println!("{:?}", &v4_lines);
1619 return None
1620 }
1621//["Default", "Arial", "16", "&Hffffff", "&Hffffff", "&H0", "&H0", "0", "0", "0", "0", "100", "100", "0", "0", "1", "1", "0", "2", "10", "10", "10", "1"]
1622 }
1623 fn get_info(&self, lines: &Vec<&str>, header: &str) -> Vec<String> {
1624 let mut script_lines = Vec::new();
1625 let mut found_script_header = false;
1626 for line in lines {
1627 let line = if line.ends_with('\n') {
1628 &line[..line.len()-1]
1629 } else if line.ends_with('\r'){
1630 &line[..line.len()-1]
1631 }else if line.ends_with("\r\n"){
1632 &line[..line.len()-2]
1633 }else {
1634 line
1635 };
1636 if line == header{
1637 found_script_header = true;
1638 script_lines.push(line.to_string());
1639 script_lines.push("\n".to_string());
1640 continue
1641 }
1642 if found_script_header {
1643 if line.starts_with('[') {
1644 break;
1645 } else if line.starts_with(';') {
1646 continue;
1647 } else {
1648 script_lines.push(line.to_string());
1649 }
1650 } else {
1651 continue;
1652 }
1653 }
1654 return script_lines;
1655 }
1656}
1657
1658impl AssFile {
1659 /// Construct `AssFile` from an existing `.ass` file.
1660 ///
1661 /// # Example
1662 /// ```rust
1663 /// use ass_parser::AssFile;
1664 /// let mut ass_file = ass_parser::AssFile::from_file("./examples/subtitles.ass").expect("error while reading file.");
1665 /// ```
1666 pub fn from_file(filename: &str) -> std::result::Result<AssFile, std::io::Error> {
1667 let file_contents = get_contents(&filename);
1668 let parser = Parser::new();
1669 match file_contents {
1670 Ok(mut contents) => {
1671 while !contents.starts_with("[") {
1672 if contents.is_empty() {
1673 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "The given file is empty or malformed"));
1674 }
1675 contents.remove(0);
1676 }
1677 let components = parser.get_each_components(contents);
1678 Ok(
1679 Self{
1680 _ass_file: filename.to_string(),
1681 components,
1682 })
1683 },
1684 Err(e) => {
1685 return Err(e)
1686 }
1687 }
1688 }
1689
1690}
1691
1692impl AssFile {
1693 /// save an instance of `AssFile` to an `.ass` file.
1694 /// # Example
1695 /// ```rust
1696 /// use hex_color::HexColor;
1697 /// use ass_parser::{AssFile, V4Format, AssFileOptions};
1698
1699 ///
1700 /// fn main() -> Result<(), std::io::Error>{
1701 /// let mut ass_file = ass_parser::AssFile::from_file("./examples/subtitles.ass")?;
1702 /// ass_file.components.script
1703 /// .set_scripttype("v4.00+")
1704 /// .set_playresx("384")
1705 /// .set_playresy("288")
1706 /// .set_scaledborderandshadow("yes")
1707 /// .set_ycbcr_matrix("None");
1708 ///
1709 /// ass_file.components.v4.set_v4(V4Format::default());
1710 ///
1711 /// AssFile::save_file(&ass_file, "modified_subtitles.ass");
1712 /// Ok(())
1713 /// }
1714 /// ```
1715 pub fn save_file(file_components: &AssFile, filename: &str) {
1716 let parser = Parser::new();
1717 let components = &file_components.components;
1718
1719 let file_data = parser.combine_components(components);
1720 write_contents(filename, &file_data);
1721 }
1722}
1723
1724impl AssFileOptions {
1725}
1726
1727impl AssFileOptions{
1728 /// Get BB:GG:RR representation of colors in Hexadecimal form
1729 pub fn get_ass_color(color: HexColor) -> String {
1730 let red = color.r;
1731 let green = color.g;
1732 let blue = color.b;
1733
1734 let red_hex = format!("{:x}", red);
1735 let green_hex = format!("{:x}", green);
1736 let blue_hex = format!("{:x}", blue);
1737
1738 let reversed_hex_color = format!("{}{}{}", blue_hex, green_hex, red_hex);
1739
1740 // let mut ass_format_color = format!(r"\c&H{}&", reversed_hex_color);
1741 let ass_format_color = format!("&H{}", reversed_hex_color);
1742 // ass_format_color.push('}');
1743 // ass_format_color = "{".to_owned() + &ass_format_color;
1744
1745 return ass_format_color;
1746 }
1747
1748
1749 pub fn get_ass_color_text(color: HexColor) -> String {
1750 let red = color.r;
1751 let green = color.g;
1752 let blue = color.b;
1753
1754 let red_hex = format!("{:x}", red);
1755 let green_hex = format!("{:x}", green);
1756 let blue_hex = format!("{:x}", blue);
1757
1758 let reversed_hex_color = format!("{}{}{}", blue_hex, green_hex, red_hex);
1759
1760 let mut ass_format_color = format!(r"\c&H{}&", reversed_hex_color);
1761 ass_format_color.push('}');
1762 ass_format_color = "{".to_owned() + &ass_format_color;
1763
1764 return ass_format_color;
1765 }
1766
1767 fn _change_ass_subtitle_color(ass_file: &str, color: HexColor) -> std::result::Result<(), std::io::Error>{
1768 if !check_path_exists(ass_file){
1769 eprintln!("ERROR: File {} does not exist", ass_file);
1770 return Ok(());
1771 }
1772
1773 let mut file_data = String::new();
1774 let mut file_buffer = fs::File::open(ass_file)?;
1775 let ass_color = Self::get_ass_color(color);
1776 file_buffer.read_to_string(&mut file_data)?;
1777
1778 let lines:Vec<&str> = file_data.split("\r\n").collect();
1779 let mut subtitle_lines = Vec::new();
1780 let mut new_lines = Vec::new();
1781
1782 for line in lines {
1783 if line.starts_with("Dialogue:") {
1784 subtitle_lines.push(line);
1785 }
1786 }
1787
1788 for (_idx, line) in subtitle_lines.into_iter().enumerate() {
1789 let new_line = match line.rfind(",,") {
1790 Some(i) => {
1791 let mut new_line = String::new();
1792 new_line.push_str(&line[..i+2]);
1793 new_line.push_str(&ass_color);
1794 new_line.push_str(&line[i+2..]);
1795 new_line.push_str("\r\n");
1796 new_line
1797 },
1798 None => {
1799 eprintln!("Unable to find match in line: {}", line);
1800 line.to_string()
1801 }
1802 };
1803 new_lines.push(new_line);
1804 }
1805 for line in &new_lines{
1806 println!("{}", line);
1807 }
1808
1809 _write_dialogues(ass_file, new_lines);
1810
1811 Ok(())
1812 }
1813
1814}
1815
1816
1817//{\c&He3cb44&}
1818
1819fn check_path_exists(path: &str) -> bool {
1820 fs::metadata(path).is_ok()
1821}
1822
1823fn _write_dialogues(filename: &str, dialogues: Vec<String>) {
1824 if !check_path_exists(filename){
1825 eprintln!("ERROR: File {} does not exist", filename);
1826 return
1827 }
1828 let mut file = fs::OpenOptions::new().read(true).write(true).open(filename).unwrap();
1829 let mut contents = String::new();
1830 file.read_to_string(&mut contents).unwrap();
1831
1832 let dialogue_idx = contents.find("Dialogue: ").unwrap();
1833
1834 file.seek(std::io::SeekFrom::Start(dialogue_idx.try_into().unwrap())).unwrap();
1835
1836 for line in dialogues {
1837 file.write(line.as_bytes()).unwrap();
1838 }
1839}
1840
1841fn write_contents(filename: &str, contents: &str) {
1842 let mut file = fs::File::create(filename).unwrap();
1843 file.write(contents.as_bytes()).unwrap();
1844}
1845
1846fn get_contents(filename: &str) -> std::result::Result<String, std::io::Error>{
1847 if !check_path_exists(filename){
1848 return Err(std::io::ErrorKind::NotFound.into());
1849 }
1850 return fs::read_to_string(filename);
1851}
1852
1853
1854
1855
1856#[cfg(test)]
1857mod tests {
1858 use super::*;
1859
1860 #[test]
1861 fn test_file_contents() {
1862 use parser::SrtData;
1863 let file_contents = get_contents("examples/rapgod.srt").unwrap();
1864
1865 let srt_data = SrtData::new();
1866 let srt_content = srt_data.parse_srt(file_contents);
1867
1868 let test_srt_content = SrtData {
1869 index: "0".to_string(),
1870 start: "0:00:01.50".to_string(),
1871 end: "0:00:04.90".to_string(),
1872 text: "Look, I was gonna go easy on you and not to hurt your feelings ".to_string(),
1873 };
1874
1875 assert_eq!(test_srt_content, srt_content[0]);
1876 }
1877
1878 #[test]
1879 fn test_from_file_wrong() {
1880 let result = AssFile::from_file("asdfasdf").map_err(|e| e.kind());
1881 let expected = std::result::Result::Err(std::io::ErrorKind::NotFound);
1882
1883 assert_eq!(expected, result);
1884 }
1885}