Skip to main content

ass_renderer/pipeline/drawing/
mod.rs

1//! Drawing command processing module
2
3mod parse;
4mod spline;
5
6pub use parse::parse_draw_commands;
7use spline::spline_to_bezier;
8
9use crate::utils::RenderError;
10use tiny_skia::{Path, PathBuilder};
11
12#[cfg(feature = "nostd")]
13use alloc::vec::Vec;
14#[cfg(not(feature = "nostd"))]
15use std::vec::Vec;
16
17/// Drawing command types
18#[derive(Debug, Clone)]
19pub enum DrawCommand {
20    /// Move to position (m command)
21    MoveTo { x: f32, y: f32 },
22    /// Move without drawing (n command)
23    MoveToNoDraw { x: f32, y: f32 },
24    /// Line to position (l command)
25    LineTo { x: f32, y: f32 },
26    /// Cubic Bezier curve (b command)
27    BezierTo {
28        x1: f32,
29        y1: f32,
30        x2: f32,
31        y2: f32,
32        x3: f32,
33        y3: f32,
34    },
35    /// B-spline (s command - converted to bezier internally)
36    Spline { points: Vec<(f32, f32)> },
37    /// Extended B-spline (p command)
38    ExtendSpline { points: Vec<(f32, f32)> },
39    /// Close path (c command)
40    ClosePath,
41}
42
43/// Process ASS drawing commands into a path
44pub fn process_drawing_commands(commands: &str) -> Result<Option<Path>, RenderError> {
45    // Try to parse the commands, return None for invalid input
46    let draw_commands = match parse_draw_commands(commands) {
47        Ok(commands) => commands,
48        Err(_) => return Ok(None), // Invalid drawing commands return None
49    };
50    if draw_commands.is_empty() {
51        return Ok(None);
52    }
53
54    let mut builder = PathBuilder::new();
55    let mut _current_pos = (0.0, 0.0);
56
57    for cmd in draw_commands {
58        match cmd {
59            DrawCommand::MoveTo { x, y } => {
60                builder.move_to(x, y);
61                _current_pos = (x, y);
62            }
63            DrawCommand::MoveToNoDraw { x, y } => {
64                builder.move_to(x, y);
65                _current_pos = (x, y);
66            }
67            DrawCommand::LineTo { x, y } => {
68                builder.line_to(x, y);
69                _current_pos = (x, y);
70            }
71            DrawCommand::BezierTo {
72                x1,
73                y1,
74                x2,
75                y2,
76                x3,
77                y3,
78            } => {
79                builder.cubic_to(x1, y1, x2, y2, x3, y3);
80                _current_pos = (x3, y3);
81            }
82            DrawCommand::Spline { ref points } => {
83                // Convert B-spline to Bezier curves
84                if points.len() >= 3 {
85                    let beziers = spline_to_bezier(points, false);
86                    for (c1, c2, end) in beziers {
87                        builder.cubic_to(c1.0, c1.1, c2.0, c2.1, end.0, end.1);
88                        _current_pos = end;
89                    }
90                }
91            }
92            DrawCommand::ExtendSpline { ref points } => {
93                // Extended B-spline with additional control
94                if points.len() >= 3 {
95                    let beziers = spline_to_bezier(points, true);
96                    for (c1, c2, end) in beziers {
97                        builder.cubic_to(c1.0, c1.1, c2.0, c2.1, end.0, end.1);
98                        _current_pos = end;
99                    }
100                }
101            }
102            DrawCommand::ClosePath => {
103                builder.close();
104            }
105        }
106    }
107
108    Ok(builder.finish())
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn plain_drawing_parses() {
117        let path = process_drawing_commands("m 0 0 l 100 0 l 100 100 l 0 100")
118            .expect("ok")
119            .expect("some path");
120        let b = path.bounds();
121        assert!(b.width() > 50.0 && b.height() > 50.0);
122    }
123
124    #[test]
125    fn trailing_tag_does_not_discard_the_shape() {
126        // Real scripts append the drawing's closing tag to the `\p` text, e.g.
127        // `...l 0 100\p0}`. The backslash must end the drawing, not poison the
128        // last coordinate and throw the whole shape away (which rendered nothing
129        // for every elaborate sign — the gradient boxes, brushstrokes, etc.).
130        let path = process_drawing_commands("m 0 0 l 100 0 l 100 100 l 0 100\\p0}")
131            .expect("ok")
132            .expect("some path");
133        let b = path.bounds();
134        assert!(
135            b.width() > 50.0 && b.height() > 50.0,
136            "shape with a trailing \\p0 tag was discarded: bounds {b:?}"
137        );
138
139        // The same shape with and without the trailing tag must be identical.
140        let clean = process_drawing_commands("m 0 0 l 100 0 l 100 100 l 0 100")
141            .expect("ok")
142            .expect("some");
143        assert_eq!(path.len(), clean.len());
144    }
145}