rassa 0.3.0

Rust API and native shared-library target for the rassa ASS subtitle renderer.
Documentation
//! Safe Rust API and native `librassa.so` target for rassa.

pub use rassa_core::{
    ImagePlane, Margins, Point, RassaError, RassaResult, Rect, RendererConfig, RgbaColor, Size, ass,
};
pub use rassa_fonts::{AttachedFontProvider, FontAttachment, FontProvider, FontconfigProvider};
pub use rassa_parse::{
    ParsedAttachment, ParsedEvent, ParsedSpanStyle, ParsedStyle, ParsedTrack, parse_script_text,
};
pub use rassa_render::{PreparedFrame, RenderEngine, RenderSelection, default_renderer_config};

/// C ABI symbols exported by `librassa.so`.
pub mod capi {
    pub use rassa_capi::*;
}

/// Parsed ASS/SSA subtitle script for the safe Rust API.
#[derive(Clone, Debug, PartialEq)]
pub struct Script {
    track: ParsedTrack,
}

impl Script {
    /// Parse an ASS/SSA script from UTF-8 text.
    pub fn parse(text: &str) -> RassaResult<Self> {
        parse_script_text(text).map(|track| Self { track })
    }

    /// Wrap an already parsed track.
    pub fn from_track(track: ParsedTrack) -> Self {
        Self { track }
    }

    /// Borrow the underlying parsed track for advanced users.
    pub fn track(&self) -> &ParsedTrack {
        &self.track
    }

    /// Consume this script and return the underlying parsed track.
    pub fn into_track(self) -> ParsedTrack {
        self.track
    }

    /// ASS PlayRes as a `Size`.
    pub fn play_res(&self) -> Size {
        Size {
            width: self.track.play_res_x,
            height: self.track.play_res_y,
        }
    }

    /// Default renderer configuration derived from this script.
    pub fn default_config(&self) -> RendererConfig {
        default_renderer_config(&self.track)
    }
}

/// Rendered frame returned by the safe Rust API.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Frame {
    pub now_ms: i64,
    pub planes: Vec<ImagePlane>,
}

/// Safe Rust subtitle renderer facade.
#[derive(Default)]
pub struct Renderer {
    engine: RenderEngine,
}

impl Renderer {
    pub fn new() -> Self {
        Self::default()
    }

    /// Render using the default fontconfig-backed provider.
    pub fn render_frame(&self, script: &Script, now_ms: i64) -> RassaResult<Frame> {
        let provider = FontconfigProvider::new();
        self.render_frame_with_provider(script, &provider, now_ms)
    }

    /// Render using an explicit font provider.
    pub fn render_frame_with_provider<P: FontProvider>(
        &self,
        script: &Script,
        provider: &P,
        now_ms: i64,
    ) -> RassaResult<Frame> {
        Ok(Frame {
            now_ms,
            planes: self
                .engine
                .render_frame_with_provider(script.track(), provider, now_ms),
        })
    }

    /// Render using an explicit font provider and renderer config.
    pub fn render_frame_with_config<P: FontProvider>(
        &self,
        script: &Script,
        provider: &P,
        now_ms: i64,
        config: &RendererConfig,
    ) -> RassaResult<Frame> {
        Ok(Frame {
            now_ms,
            planes: self.engine.render_frame_with_provider_and_config(
                script.track(),
                provider,
                now_ms,
                config,
            ),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn safe_facade_parses_and_renders() {
        let script = Script::parse("[Script Info]\nPlayResX: 320\nPlayResY: 180\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0,0,0,,Rust ABI")
            .expect("script should parse");

        let frame = Renderer::new()
            .render_frame(&script, 500)
            .expect("frame should render");

        assert_eq!(
            script.play_res(),
            Size {
                width: 320,
                height: 180
            }
        );
        assert_eq!(frame.now_ms, 500);
        assert!(!frame.planes.is_empty());
    }
}