use std::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum Format {
Srt,
Vtt,
Ass,
}
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Hash)]
pub struct Time(pub i64);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Subtitles {
pub format: Format,
pub header: Header,
pub cues: Vec<Cue>,
pub events: Vec<AssEvent>,
pub warnings: Vec<Warning>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Cue {
pub index: Option<u32>,
pub id: Option<String>,
pub start: Time,
pub end: Time,
pub text: String,
pub settings: Option<String>,
pub ass: Option<AssFields>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AssFields {
pub kind: EventKind,
pub layer: i32,
pub marked: bool,
pub style: String,
pub name: String,
pub margin_l: i32,
pub margin_r: i32,
pub margin_v: i32,
pub effect: String,
}
impl Default for AssFields {
fn default() -> Self {
Self {
kind: EventKind::Dialogue,
layer: 0,
marked: false,
style: "Default".to_owned(),
name: String::new(),
margin_l: 0,
margin_r: 0,
margin_v: 0,
effect: String::new(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum EventKind {
Dialogue,
Comment,
Picture,
Sound,
Movie,
Command,
}
impl EventKind {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Dialogue => "Dialogue",
Self::Comment => "Comment",
Self::Picture => "Picture",
Self::Sound => "Sound",
Self::Movie => "Movie",
Self::Command => "Command",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AssEvent {
pub kind: EventKind,
pub layer: i32,
pub marked: bool,
pub start: Time,
pub end: Time,
pub style: String,
pub name: String,
pub margin_l: i32,
pub margin_r: i32,
pub margin_v: i32,
pub effect: String,
pub text: String,
pub event_index: usize,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Header {
pub vtt_signature: Option<String>,
pub vtt_header_lines: Vec<String>,
pub blocks: Vec<VttBlock>,
pub script_info: Vec<(String, String)>,
pub styles: Vec<Style>,
pub raw_sections: Vec<RawSection>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VttBlock {
pub kind: VttBlockKind,
pub raw: String,
pub cue_index: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum VttBlockKind {
Region,
Style,
Note,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawSection {
pub name: String,
pub lines: Vec<String>,
pub position: usize,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub struct AssNumber {
units: i64,
scale: u32,
}
impl AssNumber {
pub const fn from_integer(value: i64) -> Self {
Self {
units: value,
scale: 0,
}
}
pub(crate) fn parse(input: &str) -> Option<Self> {
let input = input.trim();
if input.is_empty() {
return None;
}
let (negative, rest) = match input.as_bytes()[0] {
b'-' => (true, &input[1..]),
b'+' => (false, &input[1..]),
_ => (false, input),
};
let (whole, fraction) = rest.split_once('.').unwrap_or((rest, ""));
if whole.is_empty()
|| !whole.bytes().all(|byte| byte.is_ascii_digit())
|| !fraction.bytes().all(|byte| byte.is_ascii_digit())
{
return None;
}
let scale = fraction.len() as u32;
let factor = 10_i64.checked_pow(scale)?;
let mut units = whole.parse::<i64>().ok()?.checked_mul(factor)?;
if !fraction.is_empty() {
units = units.checked_add(fraction.parse::<i64>().ok()?)?;
}
if negative {
units = units.checked_neg()?;
}
Some(Self { units, scale }.normalized())
}
fn normalized(mut self) -> Self {
while self.scale > 0 && self.units % 10 == 0 {
self.units /= 10;
self.scale -= 1;
}
self
}
}
impl fmt::Display for AssNumber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.scale == 0 {
return write!(f, "{}", self.units);
}
let negative = self.units < 0;
let magnitude = self.units.unsigned_abs();
let factor = 10_u64.pow(self.scale);
if negative {
f.write_str("-")?;
}
write!(
f,
"{}.{:0width$}",
magnitude / factor,
magnitude % factor,
width = self.scale as usize
)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub struct Colour(pub u32);
impl fmt::Display for Colour {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "&H{:08X}", self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Style {
pub name: String,
pub font_name: String,
pub font_size: AssNumber,
pub primary_colour: Colour,
pub secondary_colour: Colour,
pub outline_colour: Colour,
pub back_colour: Colour,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strike_out: bool,
pub scale_x: AssNumber,
pub scale_y: AssNumber,
pub spacing: AssNumber,
pub angle: AssNumber,
pub border_style: i32,
pub outline: AssNumber,
pub shadow: AssNumber,
pub alignment: i32,
pub margin_l: i32,
pub margin_r: i32,
pub margin_v: i32,
pub encoding: i32,
}
impl Default for Style {
fn default() -> Self {
Self {
name: "Default".to_owned(),
font_name: "Arial".to_owned(),
font_size: AssNumber::from_integer(20),
primary_colour: Colour(0x00ff_ffff),
secondary_colour: Colour(0x0000_00ff),
outline_colour: Colour(0),
back_colour: Colour(0),
bold: false,
italic: false,
underline: false,
strike_out: false,
scale_x: AssNumber::from_integer(100),
scale_y: AssNumber::from_integer(100),
spacing: AssNumber::from_integer(0),
angle: AssNumber::from_integer(0),
border_style: 1,
outline: AssNumber::from_integer(2),
shadow: AssNumber::from_integer(0),
alignment: 2,
margin_l: 10,
margin_r: 10,
margin_v: 10,
encoding: 1,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum Warning {
BomStripped,
MixedLineEndings,
NoCues,
ComponentOutOfRange {
line: usize,
},
EndBeforeStart {
cue: usize,
},
MissingIndex {
line: usize,
},
NonSequentialIndex {
line: usize,
expected: u32,
found: u32,
},
StrayText {
line: usize,
},
EmptyCue {
cue: usize,
},
DotMillis {
line: usize,
},
ShortMillis {
line: usize,
},
MissingHours {
line: usize,
},
MissingBlankLine {
line: usize,
},
UnknownTimingSuffix {
line: usize,
},
CommaMillis {
line: usize,
},
ShortHours {
line: usize,
},
UnknownSection {
line: usize,
name: String,
},
FieldCountMismatch {
line: usize,
expected: usize,
found: usize,
},
MissingFormatLine {
section: String,
},
AssFractionDigits {
line: usize,
},
UnknownStyle {
cue: usize,
style: String,
},
BadField {
line: usize,
field: String,
},
UpgradedToV4Plus,
MissingStyles,
FormatReordered,
TimePrecisionLost {
cue: usize,
},
LossyTag {
cue: usize,
tag: String,
},
DroppedSettings {
cue: usize,
},
DroppedId {
cue: usize,
},
DroppedBlock {
kind: VttBlockKind,
},
DroppedEvent {
index: usize,
},
}
impl Warning {
pub(crate) fn is_output_warning(&self) -> bool {
matches!(
self,
Self::TimePrecisionLost { .. }
| Self::LossyTag { .. }
| Self::DroppedSettings { .. }
| Self::DroppedId { .. }
| Self::DroppedBlock { .. }
| Self::DroppedEvent { .. }
)
}
}
#[derive(Debug)]
pub enum Error {
Utf16Input,
InvalidUtf8 {
offset: usize,
},
TimeOutOfRange {
line: usize,
},
NegativeTime {
line: usize,
},
InvalidTimestamp {
line: usize,
},
MissingArrow {
line: usize,
},
MissingHeader {
line: usize,
found: String,
},
NotAss {
line: usize,
},
Io(std::io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Utf16Input => f.write_str("UTF-16 input requires transcoding"),
Self::InvalidUtf8 { offset } => write!(f, "invalid UTF-8 at byte {offset}"),
Self::TimeOutOfRange { line } => write!(f, "time is out of range on line {line}"),
Self::NegativeTime { line } => write!(f, "negative time on line {line}"),
Self::InvalidTimestamp { line } => write!(f, "invalid timestamp on line {line}"),
Self::MissingArrow { line } => write!(f, "missing timing arrow on line {line}"),
Self::MissingHeader { line, found } => {
write!(f, "missing WebVTT header on line {line}: {found}")
}
Self::NotAss { line } => write!(f, "content before an ASS section on line {line}"),
Self::Io(error) => write!(f, "read failed: {error}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub enum LineEnding {
#[default]
Lf,
CrLf,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub struct ComposeOptions {
pub line_ending: LineEnding,
pub bom: bool,
}