danmu2ass 0.2.1

哔哩哔哩的 xml 文件转化为 ass 文件
Documentation
use crate::{CanvasConfig, DrawEffect, Drawable};
use anyhow::Result;
use std::io::{BufWriter, Write};
use std::{fmt, fs::File};

struct TimePoint {
    t: f64,
}
impl fmt::Display for TimePoint {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let secs = self.t.floor() as u32;
        let hour = secs / 3600;
        let minutes = (secs % 3600) / 60;

        let left = self.t - (hour * 3600) as f64 - (minutes * 60) as f64;

        write!(f, "{hour}:{minutes:02}:{left:05.2}")
    }
}

struct AssEffect {
    effect: DrawEffect,
}
impl fmt::Display for AssEffect {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.effect {
            DrawEffect::Move { start, end } => {
                let (x0, y0) = start;
                let (x1, y1) = end;
                write!(f, "\\move({x0}, {y0}, {x1}, {y1})")
            }
            DrawEffect::Fixed {} => {
                error!("应该不会出现固定弹幕的");
                fmt::Result::Err(fmt::Error)
            }
        }
    }
}

impl super::CanvasConfig {
    pub fn ass_styles(&self) -> Vec<String> {
        vec![
            // Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, \
            // Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, \
            // Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
            format!(
                "Style: Float,{font},{font_size},&H{a:02x}FFFFFF,&H00FFFFFF,&H{a:02x}000000,&H00000000,\
                {bold}, 0, 0, 0, 100, 100, 0.00, 0.00, 1, \
                {outline}, 0, 7, 0, 0, 0, 1",
                a = self.opacity,
                font = self.font,
                font_size = self.font_size,
                bold = self.bold,
                outline = self.outline,
            ),
            format!(
                "Style: Bottom,{font},{font_size},&H{a:02x}FFFFFF,&H00FFFFFF,&H{a:02x}000000,&H00000000,\
                {bold}, 0, 0, 0, 100, 100, 0.00, 0.00, 1, \
                {outline}, 0, 7, 0, 0, 0, 1",
                a = self.opacity,
                font = self.font,
                font_size = self.font_size,
                bold = self.bold,
                outline = self.outline,
            ),
            format!(
                "Style: Top,{font},{font_size},&H{a:02x}FFFFFF,&H00FFFFFF,&H{a:02x}000000,&H00000000,\
                {bold}, 0, 0, 0, 100, 100, 0.00, 0.00, 1, \
                {outline}, 0, 7, 0, 0, 0, 1",
                a = self.opacity,
                font = self.font,
                font_size = self.font_size,
                bold = self.bold,
                outline = self.outline,
            ),
        ]
    }
}

struct CanvasStyles(Vec<String>);
impl fmt::Display for CanvasStyles {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for style in &self.0 {
            writeln!(f, "{}", style)?;
        }
        Ok(())
    }
}

pub struct AssWriter {
    f: BufWriter<File>,
    title: String,
    canvas_config: CanvasConfig,
}

impl AssWriter {
    pub fn new(f: File, title: String, canvas_config: CanvasConfig) -> Result<Self> {
        let mut this = AssWriter {
            f: BufWriter::new(f),
            title,
            canvas_config,
        };

        this.init()?;

        Ok(this)
    }

    pub fn init(&mut self) -> Result<()> {
        write!(
            self.f,
            "\
            [Script Info]\n\
            ; Script generated by danmu2ass\n\
            Title: {title}\n\
            Script Updated By: danmu2ass (https://github.com/gwy15/danmu2ass)\n\
            ScriptType: v4.00+\n\
            PlayResX: {width}\n\
            PlayResY: {height}\n\
            Aspect Ratio: {width}:{height}\n\
            Collisions: Normal\n\
            WrapStyle: 2\n\
            ScaledBorderAndShadow: yes\n\
            YCbCr Matrix: TV.601\n\
            \n\
            \n\
            [V4+ Styles]\n\
            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\
            {styles}\
            \n\
            [Events]\n\
            Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n\
            ",
            title = self.title,
            width = self.canvas_config.width,
            height = self.canvas_config.height,
            styles = CanvasStyles(self.canvas_config.ass_styles()),
        )?;
        Ok(())
    }

    pub fn write(&mut self, drawable: Drawable) -> Result<()> {
        writeln!(
            self.f,
            // Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
            "Dialogue: 2,{start},{end},{style},,0,0,0,,{{{effect}\\c&H{b:02x}{g:02x}{r:02x}&}}{text}",
            start = TimePoint {
                t: drawable.danmu.timeline_s
            },
            end = TimePoint {
                t: drawable.danmu.timeline_s + drawable.duration
            },
            style = drawable.style_name,
            effect = AssEffect {
                effect: drawable.effect
            },
            b = drawable.danmu.rgb.2,
            g = drawable.danmu.rgb.1,
            r = drawable.danmu.rgb.0,
            text = drawable.danmu.content,
            // text = (0..drawable.danmu.content.chars().count()).map(|_| '晚').collect::<String>(),
        )?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn time_point_fmt() {
        assert_eq!(format!("{}", TimePoint { t: 0.0 }), "0:00:00.00");
        assert_eq!(format!("{}", TimePoint { t: 1.0 }), "0:00:01.00");
        assert_eq!(format!("{}", TimePoint { t: 60.0 }), "0:01:00.00");
        assert_eq!(format!("{}", TimePoint { t: 3600.0 }), "1:00:00.00");
        assert_eq!(format!("{}", TimePoint { t: 3600.0 + 60.0 }), "1:01:00.00");
        assert_eq!(
            format!(
                "{}",
                TimePoint {
                    t: 3600.0 + 60.0 + 1.0
                }
            ),
            "1:01:01.00"
        );
        assert_eq!(
            format!(
                "{}",
                TimePoint {
                    t: 3600.0 + 60.0 + 1.0 + 0.5
                }
            ),
            "1:01:01.50"
        );
        assert_eq!(
            format!(
                "{}",
                TimePoint {
                    t: 3600.0 + 1.0 + 0.01234
                }
            ),
            "1:00:01.01"
        );
    }
}