#![allow(dead_code)]
#![allow(clippy::cast_possible_truncation)]
use crate::Timecode;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DisplayConvention {
Smpte,
Ebu,
Film,
FeetFrames35mm,
FeetFrames16mm,
Milliseconds,
FrameNumber,
}
#[derive(Debug, Clone)]
pub struct DisplayOptions {
pub convention: DisplayConvention,
pub show_sign: bool,
pub zero_pad: bool,
pub custom_separator: Option<char>,
}
impl Default for DisplayOptions {
fn default() -> Self {
Self {
convention: DisplayConvention::Smpte,
show_sign: false,
zero_pad: true,
custom_separator: None,
}
}
}
impl DisplayOptions {
#[must_use]
pub fn smpte() -> Self {
Self {
convention: DisplayConvention::Smpte,
..Self::default()
}
}
#[must_use]
pub fn ebu() -> Self {
Self {
convention: DisplayConvention::Ebu,
..Self::default()
}
}
#[must_use]
pub fn film() -> Self {
Self {
convention: DisplayConvention::Film,
..Self::default()
}
}
#[must_use]
pub fn milliseconds() -> Self {
Self {
convention: DisplayConvention::Milliseconds,
..Self::default()
}
}
#[must_use]
pub fn frame_number() -> Self {
Self {
convention: DisplayConvention::FrameNumber,
..Self::default()
}
}
}
#[derive(Debug, Clone)]
pub struct FormattedTimecode {
pub text: String,
pub convention: DisplayConvention,
}
impl fmt::Display for FormattedTimecode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.text)
}
}
#[must_use]
pub fn format_timecode(tc: &Timecode, opts: &DisplayOptions) -> FormattedTimecode {
let text = match opts.convention {
DisplayConvention::Smpte => format_smpte(tc, opts),
DisplayConvention::Ebu => format_ebu(tc, opts),
DisplayConvention::Film => format_film(tc, opts),
DisplayConvention::FeetFrames35mm => format_feet_frames(tc, 16),
DisplayConvention::FeetFrames16mm => format_feet_frames(tc, 40),
DisplayConvention::Milliseconds => format_milliseconds(tc),
DisplayConvention::FrameNumber => format_frame_number(tc),
};
FormattedTimecode {
text,
convention: opts.convention,
}
}
fn format_smpte(tc: &Timecode, _opts: &DisplayOptions) -> String {
let sep = if tc.frame_rate.drop_frame { ';' } else { ':' };
format!(
"{:02}:{:02}:{:02}{}{:02}",
tc.hours, tc.minutes, tc.seconds, sep, tc.frames
)
}
fn format_ebu(tc: &Timecode, _opts: &DisplayOptions) -> String {
format!(
"{:02}:{:02}:{:02}:{:02}",
tc.hours, tc.minutes, tc.seconds, tc.frames
)
}
fn format_film(tc: &Timecode, _opts: &DisplayOptions) -> String {
format!(
"{:02}+{:02}:{:02}:{:02}",
tc.hours, tc.minutes, tc.seconds, tc.frames
)
}
fn format_feet_frames(tc: &Timecode, frames_per_foot: u64) -> String {
let total_frames = tc.to_frames();
let feet = total_frames / frames_per_foot;
let leftover = total_frames % frames_per_foot;
format!("{feet:04}+{leftover:02}")
}
fn format_milliseconds(tc: &Timecode) -> String {
let fps = crate::frame_rate_from_info(&tc.frame_rate).as_float();
let ms = if fps > 0.0 {
((tc.frames as f64 / fps) * 1000.0).round() as u32
} else {
0
};
format!(
"{:02}:{:02}:{:02}.{:03}",
tc.hours,
tc.minutes,
tc.seconds,
ms.min(999)
)
}
fn format_frame_number(tc: &Timecode) -> String {
format!("{}", tc.to_frames())
}
pub fn parse_display(
s: &str,
frame_rate: crate::FrameRate,
) -> Result<Timecode, crate::TimecodeError> {
Timecode::from_string(s, frame_rate).or_else(|_| {
let normalized = s.replacen('+', ":", 1);
Timecode::from_string(&normalized, frame_rate)
})
}
#[derive(Debug, Clone)]
pub struct ConventionComparison {
pub timecode: Timecode,
pub smpte: String,
pub ebu: String,
pub film: String,
pub ms: String,
pub frame: String,
}
impl ConventionComparison {
#[must_use]
pub fn build(tc: Timecode) -> Self {
let smpte = format_timecode(&tc, &DisplayOptions::smpte()).text;
let ebu = format_timecode(&tc, &DisplayOptions::ebu()).text;
let film = format_timecode(&tc, &DisplayOptions::film()).text;
let ms = format_timecode(&tc, &DisplayOptions::milliseconds()).text;
let frame = format_timecode(&tc, &DisplayOptions::frame_number()).text;
Self {
timecode: tc,
smpte,
ebu,
film,
ms,
frame,
}
}
#[must_use]
pub fn to_table_row(&self) -> String {
format!(
"| {:>13} | {:>13} | {:>13} | {:>12} | {:>8} |",
self.smpte, self.ebu, self.film, self.ms, self.frame
)
}
}
#[must_use]
pub fn comparison_table_header() -> String {
format!(
"| {:>13} | {:>13} | {:>13} | {:>12} | {:>8} |",
"SMPTE", "EBU", "Film", "Milliseconds", "Frames"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FrameRate;
fn tc25(h: u8, m: u8, s: u8, f: u8) -> Timecode {
Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid tc")
}
#[test]
fn test_smpte_ndf() {
let tc = tc25(1, 30, 0, 12);
let fmt = format_timecode(&tc, &DisplayOptions::smpte());
assert_eq!(fmt.text, "01:30:00:12");
}
#[test]
fn test_ebu_always_colon() {
let tc = tc25(0, 0, 0, 0);
let fmt = format_timecode(&tc, &DisplayOptions::ebu());
assert_eq!(fmt.text, "00:00:00:00");
}
#[test]
fn test_film_format() {
let tc = tc25(2, 15, 30, 5);
let fmt = format_timecode(&tc, &DisplayOptions::film());
assert_eq!(fmt.text, "02+15:30:05");
}
#[test]
fn test_milliseconds_format() {
let tc = tc25(0, 0, 0, 12);
let fmt = format_timecode(&tc, &DisplayOptions::milliseconds());
assert_eq!(fmt.text, "00:00:00.480");
}
#[test]
fn test_frame_number() {
let tc = tc25(1, 0, 0, 0);
let fmt = format_timecode(&tc, &DisplayOptions::frame_number());
assert_eq!(fmt.text, "90000");
}
#[test]
fn test_feet_frames_35mm() {
let tc = tc25(0, 0, 1, 7); let fmt = format_timecode(
&tc,
&DisplayOptions {
convention: DisplayConvention::FeetFrames35mm,
..DisplayOptions::default()
},
);
assert!(!fmt.text.is_empty());
}
#[test]
fn test_comparison_build() {
let tc = tc25(1, 0, 0, 0);
let comp = ConventionComparison::build(tc);
assert!(!comp.smpte.is_empty());
assert!(!comp.ebu.is_empty());
}
#[test]
fn test_parse_display_smpte() {
let parsed = parse_display("01:30:00:12", FrameRate::Fps25).expect("parse ok");
assert_eq!(parsed.hours, 1);
assert_eq!(parsed.minutes, 30);
assert_eq!(parsed.seconds, 0);
assert_eq!(parsed.frames, 12);
}
}