ass-renderer 0.1.2

High-performance ASS subtitle renderer with modular backends
Documentation
//! Drawing command string parsing

#[cfg(feature = "nostd")]
use alloc::{
    format,
    string::{String, ToString},
    vec::Vec,
};
#[cfg(not(feature = "nostd"))]
use std::{
    string::{String, ToString},
    vec::Vec,
};

use crate::utils::RenderError;

use super::DrawCommand;

/// Parse drawing commands from string
pub fn parse_draw_commands(text: &str) -> Result<Vec<DrawCommand>, RenderError> {
    let mut commands = Vec::new();
    let tokens = tokenize_drawing_commands(text);
    let mut i = 0;

    while i < tokens.len() {
        match tokens[i].as_str() {
            "m" => {
                // Move to - can have multiple coordinate pairs
                i += 1;
                if i + 1 < tokens.len() {
                    // First move is always MoveTo
                    let x = parse_coord(&tokens[i])?;
                    let y = parse_coord(&tokens[i + 1])?;
                    commands.push(DrawCommand::MoveTo { x, y });
                    i += 2;

                    // Additional coordinate pairs become LineTo
                    while i + 1 < tokens.len() && !is_command(&tokens[i]) {
                        let x = parse_coord(&tokens[i])?;
                        let y = parse_coord(&tokens[i + 1])?;
                        commands.push(DrawCommand::LineTo { x, y });
                        i += 2;
                    }
                } else {
                    return Err(RenderError::InvalidDrawCommand(
                        "Incomplete move command".to_string(),
                    ));
                }
            }
            "n" => {
                // Move to without drawing - can have multiple coordinate pairs
                i += 1;
                while i + 1 < tokens.len() && !is_command(&tokens[i]) {
                    let x = parse_coord(&tokens[i])?;
                    let y = parse_coord(&tokens[i + 1])?;
                    commands.push(DrawCommand::MoveToNoDraw { x, y });
                    i += 2;
                }

                if commands.is_empty()
                    || !matches!(commands.last(), Some(DrawCommand::MoveToNoDraw { .. }))
                {
                    return Err(RenderError::InvalidDrawCommand(
                        "Incomplete move command".to_string(),
                    ));
                }
            }
            "l" => {
                // Line to - can have multiple coordinate pairs
                i += 1;
                while i + 1 < tokens.len() && !is_command(&tokens[i]) {
                    let x = parse_coord(&tokens[i])?;
                    let y = parse_coord(&tokens[i + 1])?;
                    commands.push(DrawCommand::LineTo { x, y });
                    i += 2;
                }

                if commands.is_empty()
                    || !matches!(commands.last(), Some(DrawCommand::LineTo { .. }))
                {
                    return Err(RenderError::InvalidDrawCommand(
                        "Incomplete line command".to_string(),
                    ));
                }
            }
            "b" => {
                // Bezier curve - can have multiple sets of 6 coordinates
                i += 1;
                while i + 5 < tokens.len() && !is_command(&tokens[i]) {
                    let x1 = parse_coord(&tokens[i])?;
                    let y1 = parse_coord(&tokens[i + 1])?;
                    let x2 = parse_coord(&tokens[i + 2])?;
                    let y2 = parse_coord(&tokens[i + 3])?;
                    let x3 = parse_coord(&tokens[i + 4])?;
                    let y3 = parse_coord(&tokens[i + 5])?;
                    commands.push(DrawCommand::BezierTo {
                        x1,
                        y1,
                        x2,
                        y2,
                        x3,
                        y3,
                    });
                    i += 6;
                }

                if commands.is_empty()
                    || !matches!(commands.last(), Some(DrawCommand::BezierTo { .. }))
                {
                    return Err(RenderError::InvalidDrawCommand(
                        "Incomplete bezier command".to_string(),
                    ));
                }
            }
            "s" => {
                // B-spline (at least 3 points)
                let mut points = Vec::new();
                i += 1;

                // Collect points until we hit another command or end
                while i + 1 < tokens.len() {
                    if is_command(&tokens[i]) {
                        break;
                    }
                    let x = parse_coord(&tokens[i])?;
                    let y = parse_coord(&tokens[i + 1])?;
                    points.push((x, y));
                    i += 2;
                }

                if points.len() >= 3 {
                    // Close the spline by repeating first point if needed
                    let first = points[0];
                    let last = points[points.len() - 1];
                    if (first.0 - last.0).abs() > 0.01 || (first.1 - last.1).abs() > 0.01 {
                        points.push(first);
                    }
                    commands.push(DrawCommand::Spline { points });
                } else if !points.is_empty() {
                    return Err(RenderError::InvalidDrawCommand(
                        "B-spline needs at least 3 points".to_string(),
                    ));
                }
            }
            "p" => {
                // Extended B-spline
                let mut points = Vec::new();
                i += 1;

                // Collect points for extended spline
                while i + 1 < tokens.len() {
                    if is_command(&tokens[i]) {
                        break;
                    }
                    let x = parse_coord(&tokens[i])?;
                    let y = parse_coord(&tokens[i + 1])?;
                    points.push((x, y));
                    i += 2;
                }

                if points.len() >= 3 {
                    commands.push(DrawCommand::ExtendSpline { points });
                } else if !points.is_empty() {
                    return Err(RenderError::InvalidDrawCommand(
                        "Extended spline needs at least 3 points".to_string(),
                    ));
                }
            }
            "c" => {
                // Close path
                commands.push(DrawCommand::ClosePath);
                i += 1;
            }
            _ => {
                // Unknown command or coordinate without command
                i += 1;
            }
        }
    }

    Ok(commands)
}

/// Tokenize drawing command string
fn tokenize_drawing_commands(text: &str) -> Vec<String> {
    let mut tokens = Vec::new();
    let mut current = String::new();

    for ch in text.chars() {
        if ch == '\\' {
            // A backslash starts an override tag, which is never part of a `\p`
            // drawing. Real scripts append the closing tag to the drawing text
            // (e.g. `...105.31\p0}`); stop here so that trailing tag does not
            // poison the final coordinate and discard the whole shape. libass
            // likewise ends the drawing at the tag.
            break;
        }
        if ch.is_whitespace() {
            if !current.is_empty() {
                tokens.push(current.clone());
                current.clear();
            }
        } else if ch.is_ascii_alphabetic() {
            if !current.is_empty() {
                tokens.push(current.clone());
                current.clear();
            }
            tokens.push(ch.to_string().to_lowercase());
        } else {
            current.push(ch);
        }
    }

    if !current.is_empty() {
        tokens.push(current);
    }

    tokens
}

/// Check if token is a command
fn is_command(token: &str) -> bool {
    matches!(token, "m" | "n" | "l" | "b" | "s" | "p" | "c")
}

/// Parse coordinate string to f32
fn parse_coord(s: &str) -> Result<f32, RenderError> {
    s.parse::<f32>()
        .map_err(|_| RenderError::InvalidDrawCommand(format!("Invalid coordinate: {s}")))
}