From 07faba35b97f2c9104adc80e0708b1f3fce3bf2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 22 Jun 2019 14:15:55 +0200 Subject: [PATCH] fixed some logic and added autoscaling option --- Cargo.lock | 2 +- Cargo.toml | 3 +- src/main.rs | 400 ++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 314 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7392fa..0aee2fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,7 +264,7 @@ dependencies = [ [[package]] name = "srtune" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index e6b3bde..ce4ac3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,10 @@ [package] name = "srtune" -version = "0.1.0" +version = "0.2.0" authors = ["Ondřej Hruška "] edition = "2018" [dependencies] -#srt = "0.1.0" clap = "2.33.0" log = "0.4" failure = "0.1.5" diff --git a/src/main.rs b/src/main.rs index 07f63da..f05cac3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,9 @@ use std::io; use std::str::FromStr; use std::convert::TryFrom; use std::io::BufRead; -use std::ops::{Add, Mul, MulAssign, AddAssign}; +use std::ops::{Add, Mul, MulAssign, AddAssign, SubAssign, Sub}; use std::fmt::{self, Display}; +use serde::export::fmt::Debug; const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"]; const SPAMMY_LIBS: [&str; 5] = ["tokio_reactor", "hyper", "reqwest", "mio", "want"]; @@ -22,7 +23,7 @@ fn main() { .version(env!("CARGO_PKG_VERSION")) .arg(clap::Arg::with_name("input") .value_name("INFILE") - .help("Input file, -- for stdin"), + .help("Input file, leave out for stdin"), ) .arg(clap::Arg::with_name("output") .short("o") @@ -30,29 +31,52 @@ fn main() { .value_name("OUTFILE") .help("Output file, defaults to stdout"), ) - .arg(clap::Arg::with_name("from") + .arg(clap::Arg::with_name("fromtime") .short("f") - .long("from") - .value_name("FROM") - .help("Index of the first affected entry, defaults to 0. With '--renumber', the original numbers are used."), + .long("from-time") + .value_name("TIME") + .help("Time of the first affected entry, defaults to 0. Use seconds, M:S or \ + H:M:S, decimals are supported. Uses the original times before any shifts or \ + scaling."), + ) + .arg(clap::Arg::with_name("fromindex") + .short("F") + .long("from-index") + .value_name("INDEX") + .help("Time of the first affected entry, defaults to 0. Use seconds, M:S or \ + H:M:S, decimals are supported. Uses the original times before any shifts or \ + scaling."), ) .arg(clap::Arg::with_name("move") .short("m") .long("move") - .value_name("SECS") - .help("Time shift, accepts positive or negative float"), + .value_name("TIME") + .help("Move subtitles in time. Use seconds, M:S or H:M:S, decimals and minus \ + are supported."), ) .arg(clap::Arg::with_name("scale") .short("s") .long("scale") .value_name("RATIO") - .help("Scale subtitle times and durations to compensate for bitrate differences.\nIf a start time was given, the scaling is relative to this point, otherwise to the first subtitle in the file."), + .help("Scale subtitle times and durations to compensate for bitrate \ + differences. 1 means identity, 1.1 makes all times 10% longer. If a start \ + time was given, the scaling is relative to this point, otherwise to the first \ + subtitle in the file. Has no effect if '--autoscale' is used."), + ) + .arg(clap::Arg::with_name("autoscale") + .short("S") + .long("autoscale") + .value_name("SUBTIME=VIDEOTIME") + .help("Calculate scaling based on a perceived difference. The scaling is \ + related to the first subtitle, so ensure it is aligned properly with '--move'."), ) .arg(clap::Arg::with_name("durscale") .short("d") .long("durscale") .value_name("RATIO") - .help("Scale durations, can be combined with '--scale'"), + .help("Scale durations, can be combined with '--scale' or '--autoscale'. The \ + given value will always be multiplied by the absolute time scale. 1 means \ + identity, 1.1 makes all times 10% longer."), ) .arg(clap::Arg::with_name("renumber") .short("r") @@ -93,12 +117,62 @@ fn main() { } builder.init(); + let from_time = match argv.value_of("fromtime") { + Some(s) => { + SubDuration::try_from(s).expect("Bad --from-time format").as_instant() + } + None => SubInstant(0f32) + }; + + let from_index = match argv.value_of("fromindex") { + Some(s) => { + s.parse().expect("Bad --from-index format") + } + None => 0u32 + }; + + let shift = match argv.value_of("move") { + Some(s) => { + SubDuration::try_from(s).expect("Bad --move format") + } + None => SubDuration(0f32) + }; + + let scale = match argv.value_of("scale") { + Some(s) => { + s.parse().expect("Bad --scale format") + } + None => 1f32 + }; + + let durscale = match argv.value_of("durscale") { + Some(s) => { + s.parse().expect("Bad --durscale format") + } + None => 1f32 + }; // always also shrink durations + + let autoscale = match argv.value_of("autoscale") { + Some(s) => { + let halves : Vec<&str> = s.split("=").collect(); + if halves.len() != 2 { + panic!("Bad --autoscale format, should be SUBTIME=VIDEOTIME") + } + let (first, second) = (halves[0], halves[1]); + let subtime = SubDuration::try_from(first).expect("Bad --autoscale format").as_instant(); + let vidtime = SubDuration::try_from(second).expect("Bad --autoscale format").as_instant(); + + Some((subtime, vidtime)) + } + None => None + }; + let inf = argv.value_of("input"); let outf = argv.value_of("output"); let stdin = io::stdin(); let stdout = io::stdout(); - let mut iter: Box>> = match inf { + let mut lines_iterator: Box>> = match inf { None => { Box::new(stdin.lock().lines()) } @@ -108,7 +182,7 @@ fn main() { } }; - let mut out : Box = match outf { + let mut outfile: Box = match outf { None => { Box::new(stdout.lock()) } @@ -123,44 +197,41 @@ fn main() { } }; - let from_s = match argv.value_of("from") { - Some(s) => { - s.parse().expect("Bad --from format") - }, - None => 0u32 - }; - - let move_s = match argv.value_of("move") { - Some(s) => { - s.parse().expect("Bad --move format") - }, - None => 0f32 - }; - - let scale_s = match argv.value_of("scale") { - Some(s) => { - s.parse().expect("Bad --scale format") - }, - None => 1f32 - }; + let renumber = argv.is_present("renumber"); - let durscale_s = match argv.value_of("durscale") { - Some(s) => { - s.parse().expect("Bad --durscale format") - }, - None => 1f32 - } * scale_s; // always also shrink durations + transform_subtitles(&mut lines_iterator, &mut outfile, TransformOpts { + renumber, + autoscale, + durscale, + scale, + shift, + from_index, + from_time, + }); +} - let renumber = argv.is_present("renumber"); +#[derive(Debug)] +struct TransformOpts { + renumber: bool, + autoscale: Option<(SubInstant, SubInstant)>, + durscale: f32, + scale: f32, + shift: SubDuration, + from_index: u32, + from_time: SubInstant, +} - info!("Opts: from #{}, move {}s, scale {}x, durscale {}x", from_s, move_s, scale_s, durscale_s); +fn transform_subtitles<'a>(lines : &mut Box> + 'a>, + outfile : &mut Box, + mut opts : TransformOpts) { + debug!("Opts: {:#?}", opts); - let mut scale_start = SubTime(0f32); + let mut start_time = SubInstant(0f32); let mut first_found = false; - let mut renumber_i : u32 = 0; + let mut renumber_i: u32 = 0; - let mut text = vec![]; - while let Some(Ok(x)) = iter.next() { + let mut linebuf = vec![]; + while let Some(Ok(x)) = lines.next() { let mut x = x.trim(); if x.starts_with('\u{feff}') { debug!("Stripping BOM mark"); @@ -178,55 +249,75 @@ fn main() { match u32::from_str(x) { Ok(num) => { // println!("Entry {}", num); - let datesrow = iter.next().unwrap().unwrap(); + let datesrow = lines.next().unwrap().unwrap(); if datesrow.contains(" --> ") { let mut halves = datesrow.split(" --> "); let (first, second) = (halves.next().unwrap(), halves.next().unwrap()); - let start = SubTime::try_from(first).unwrap(); - let end = SubTime::try_from(second).unwrap(); + let start = SubInstant::try_from(first).unwrap(); + let end = SubInstant::try_from(second).unwrap(); - text.clear(); - while let Some(Ok(x)) = iter.next() { + linebuf.clear(); + while let Some(Ok(x)) = lines.next() { if x.is_empty() { break; // space between the entries } - text.push(x); + linebuf.push(x); } - let mut one = Subtitle { + let mut subtitle = Subtitle { num, start, dur: SubDuration(end.0 - start.0), - text: text.join("\n"), + text: linebuf.join("\n"), }; - if num >= from_s { + if start >= opts.from_time && num >= opts.from_index { if !first_found { - debug!("Scaling anchored at {} (#{}), start edits", start, num); - scale_start = start; + debug!("Scaling anchored at {} (#{}), editing starts", start, num); + debug!("Shifting by: {}", opts.shift); + + start_time = start; first_found = true; + + if let Some((mut subt, mut vidt)) = opts.autoscale { + debug!("Autoscale: VT {} -> ST {}", vidt, subt); + subt -= start_time; + vidt -= start_time + opts.shift; + if subt.0 <= 0f32 { + panic!("Error in autoscale, start time is negative or zero."); + } + if vidt.0 <= 0f32 { + panic!("Error in autoscale, end time is negative or zero."); + } + debug!(" relative to #{}, after \"move\": VT {} -> ST {}", num, vidt, subt); + opts.scale = vidt.0 / subt.0; + debug!("Resolved scale as {}", opts.scale); + } + + opts.durscale *= opts.scale; + debug!("Duration scaling is {}", opts.durscale); } - if scale_s != 1f32 { - one.start = one.start.scale(scale_start, scale_s); + if opts.scale != 1f32 { + subtitle.start = subtitle.start.scale(start_time, opts.scale); } - one.dur *= durscale_s; - one.start += move_s; + subtitle.dur *= opts.durscale; + subtitle.start += opts.shift; } - if one.start.0 < 0f32 { - warn!("Discarding negative time entry #{} @ {:.3}s", one.num, one.start.0); + if subtitle.start.0 < 0f32 { + warn!("Discarding negative time entry #{} @ {:.3}s", subtitle.num, subtitle.start.0); continue; } // advance numbering only for the really emitted entries - if renumber { + if opts.renumber { renumber_i += 1; - one.num = renumber_i; + subtitle.num = renumber_i; } - out.write(one.to_string().as_bytes()).expect("failed to write"); + outfile.write(subtitle.to_string().as_bytes()).expect("failed to write"); } } Err(e) => { @@ -239,20 +330,46 @@ fn main() { } } - out.flush().unwrap(); + outfile.flush().unwrap(); } -#[derive(Copy, Clone, Debug)] -struct SubTime(f32); +#[derive(Copy, Clone, PartialEq, PartialOrd)] +struct SubInstant(f32); -#[derive(Copy, Clone, Debug)] +impl Debug for SubInstant { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "Time({})", self) + } +} + +#[derive(Copy, Clone, PartialEq, PartialOrd)] struct SubDuration(f32); -impl Add for SubTime { - type Output = SubTime; +impl Debug for SubDuration { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "Duration({})", self) + } +} + +impl Display for SubDuration { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", SubInstant(self.0)) + } +} + +impl Add for SubInstant { + type Output = SubInstant; fn add(self, rhs: SubDuration) -> Self::Output { - SubTime(self.0 + rhs.0) + SubInstant(self.0 + rhs.0) + } +} + +impl Sub for SubInstant { + type Output = SubInstant; + + fn sub(self, rhs: SubDuration) -> Self::Output { + SubInstant(self.0 - rhs.0) } } @@ -276,36 +393,63 @@ impl AddAssign for SubDuration { } } -impl SubTime { +impl SubInstant { /// Scale by a factor with a custom start time - pub fn scale(&self, start : SubTime, factor : f32) -> SubTime { - SubTime(start.0 + (self.0 - start.0) * factor) + pub fn scale(&self, start: SubInstant, factor: f32) -> SubInstant { + SubInstant(start.0 + (self.0 - start.0) * factor) } } -impl AddAssign for SubTime { +impl AddAssign for SubInstant { fn add_assign(&mut self, rhs: f32) { self.0 += rhs; } } -impl Display for SubTime { +impl AddAssign for SubInstant { + fn add_assign(&mut self, rhs: SubInstant) { + self.0 += rhs.0; + } +} + +impl SubAssign for SubInstant { + fn sub_assign(&mut self, rhs: SubInstant) { + self.0 -= rhs.0; + } +} + +impl SubAssign for SubInstant { + fn sub_assign(&mut self, rhs: SubDuration) { + self.0 -= rhs.0; + } +} + +impl AddAssign for SubInstant { + fn add_assign(&mut self, rhs: SubDuration) { + self.0 += rhs.0; + } +} + +impl Display for SubInstant { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { // TODO optimize this - let mut secs = self.0; + let sign = self.0.signum(); + let mut secs = self.0.abs(); let hours = (secs / 3600f32).floor(); secs -= hours * 3600f32; let minutes = (secs / 60f32).floor(); secs -= minutes * 60f32; - let msecs = ((secs % 1f32)*1000f32).round(); - write!(f, "{:02}:{:02}:{:02},{:03}", hours, minutes, secs.floor(), msecs) + let msecs = ((secs % 1f32) * 1000f32).round(); + write!(f, "{}{:02}:{:02}:{:02},{:03}", + if sign.is_sign_negative() { "-" } else { "" }, + hours, minutes, secs.floor(), msecs) } } #[derive(Clone, Debug)] struct Subtitle { num: u32, - start: SubTime, + start: SubInstant, dur: SubDuration, text: String, } @@ -320,23 +464,103 @@ impl Display for Subtitle { } } -lazy_static! { - static ref DATE_RE: Regex = Regex::new(r"(\d+):(\d+):(\d+),(\d+)").unwrap(); -} -impl TryFrom<&str> for SubTime { +impl TryFrom<&str> for SubInstant { type Error = failure::Error; fn try_from(value: &str) -> Result { + lazy_static! { + static ref DATE_RE: Regex = Regex::new(r"^(-)?(?P\d+):(?P\d+):(?P\d+(:?[,.]\d+)?)$").unwrap(); + } + match DATE_RE.captures(value) { Some(caps) => { - Ok(SubTime(f32::from_str(caps.get(1).unwrap().as_str()).unwrap() * 3600f32 + - f32::from_str(caps.get(2).unwrap().as_str()).unwrap() * 60f32 + - f32::from_str(caps.get(3).unwrap().as_str()).unwrap() + - f32::from_str(caps.get(4).unwrap().as_str()).unwrap() * 0.001f32)) + let minus = if caps.get(1).is_some() { -1f32 } else { 1f32 }; + + let h = &caps["h"]; + let m = &caps["m"]; + let s = caps["s"].replace(",", "."); + + Ok(SubInstant(minus * (f32::from_str(h).unwrap() * 3600f32 + + f32::from_str(m).unwrap() * 60f32 + + f32::from_str(&s).unwrap()))) } None => Err(failure::err_msg("Error parsing time.")) } } } +impl SubDuration { + pub fn as_instant(&self) -> SubInstant { + SubInstant(self.0) + } +} + +impl TryFrom<&str> for SubDuration { + type Error = failure::Error; + + fn try_from(value: &str) -> Result { + lazy_static! { + static ref TIME_RE: Regex = Regex::new(r"^(-)?(?:(\d+):)?(?:(\d+):)?(\d+(?:[,.]\d+)?)$").unwrap(); + } + + match TIME_RE.captures(value) { + Some(caps) => { + let minus = if caps.get(1).is_some() { -1f32 } else { 1f32 }; + let a = caps.get(2).map(|m| m.as_str()).unwrap_or(""); + let b = caps.get(3).map(|m| m.as_str()).unwrap_or(""); + let s = caps.get(4).map(|m| m.as_str()).unwrap_or("0").replace(",", "."); + + let (h, m) = if b.is_empty() { + ("0", if a.is_empty() { "0" } else { a }) + } else { + (a, b) + }; + + Ok(SubDuration(minus * (f32::from_str(h).unwrap() * 3600f32 + + f32::from_str(m).unwrap() * 60f32 + + f32::from_str(&s).unwrap()))) + } + None => Err(failure::err_msg("Error parsing time: No match")) + } + } +} + +#[test] +fn test_parse_duration() { + // this is used for user input on the command line + let bad = SubDuration(-1f32); + assert_eq!(SubDuration(45678f32), SubDuration::try_from("45678").unwrap_or(bad), "integer secs"); + assert_eq!(SubDuration(1.23f32), SubDuration::try_from("1.23").unwrap_or(bad), "float secs with period"); + assert_eq!(SubDuration(-1.23f32), SubDuration::try_from("-1.23").unwrap_or(bad), "MINUS float secs with period"); + assert_eq!(SubDuration(1.23f32), SubDuration::try_from("1,23").unwrap_or(bad), "float secs with comma"); + assert_eq!(SubDuration(121.15f32), SubDuration::try_from("2:1.15").unwrap_or(bad), "m:s.frac"); + assert_eq!(SubDuration(121.15f32), SubDuration::try_from("2:01.15").unwrap_or(bad), "m:0s.frac"); + assert_eq!(SubDuration(121.15f32), SubDuration::try_from("02:01.15").unwrap_or(bad), "0m:0s.frac"); + assert_eq!(SubDuration(121.15f32), SubDuration::try_from("02:01,15").unwrap_or(bad), "0m:0s,frac"); + + assert_eq!(SubDuration(3721.15f32), SubDuration::try_from("1:02:01,15").unwrap_or(bad), "h:0m:0s,frac"); + assert_eq!(SubDuration(3721.15f32), SubDuration::try_from("1:02:01,15").unwrap_or(bad), "h:0m:0s.frac"); + assert_eq!(SubDuration(3721.15f32), SubDuration::try_from("01:02:01,15").unwrap_or(bad), "0h:0m:0s,frac"); + assert_eq!(SubDuration(-3721.15f32), SubDuration::try_from("-01:02:01,15").unwrap_or(bad), "-0h:0m:0s,frac"); +} + +#[test] +fn test_parse_instant() { + let bad = SubInstant(-1f32); + assert_eq!(SubInstant(1081.755f32), SubInstant::try_from("00:18:01,755").unwrap_or(bad)); + assert_eq!(SubInstant(1081.755f32), SubInstant::try_from("00:18:01.755").unwrap_or(bad)); + assert_eq!(SubInstant(1081.7f32), SubInstant::try_from("00:18:01.7").unwrap_or(bad)); + assert_eq!(SubInstant(1081.7f32), SubInstant::try_from("0:18:1.7").unwrap_or(bad)); + assert_eq!(SubInstant(0f32), SubInstant::try_from("00:00:00,000").unwrap_or(bad)); + assert_eq!(SubInstant(-3600f32), SubInstant::try_from("-01:00:00,000").unwrap_or(bad)); +} + +#[test] +fn test_stringify_instant() { + assert_eq!("00:18:01,755", SubInstant::try_from("00:18:01,755").unwrap().to_string()); + assert_eq!("-00:18:01,755", SubInstant::try_from("-00:18:01,755").unwrap().to_string()); + assert_eq!("-00:18:01,700", SubInstant::try_from("-00:18:01.7").unwrap().to_string()); + assert_eq!("00:00:00,000", SubInstant::try_from("00:00:00,000").unwrap().to_string()); + assert_eq!("-00:00:00,000", SubInstant::try_from("-00:00:00,000").unwrap().to_string()); +}