icu_lib 0.3.0

Image Converter Ultra Library
Documentation
use mirx::{Color, FillRule, LineCap, LineJoin, Path, PathCmd, Scene, SceneOp, Transform};

fn fixed_f(v: mirx::Fixed) -> f32 {
    v.to_f32()
}

fn color_hex(c: &Color) -> String {
    format!("#{:02X}{:02X}{:02X}", c.r, c.g, c.b)
}

fn opacity_attr(opa: u8) -> String {
    if opa == 255 {
        String::new()
    } else {
        format!(" opacity=\"{:.3}\"", opa as f32 / 255.0)
    }
}

fn transform_attr(tf: &Transform) -> String {
    if *tf == Transform::IDENTITY {
        return String::new();
    }
    format!(
        " transform=\"matrix({} {} {} {} {} {})\"",
        fixed_f(tf.m00),
        fixed_f(tf.m10),
        fixed_f(tf.m01),
        fixed_f(tf.m11),
        fixed_f(tf.tx),
        fixed_f(tf.ty)
    )
}

fn fill_rule_str(r: FillRule) -> &'static str {
    match r {
        FillRule::NonZero => "nonzero",
        FillRule::EvenOdd => "evenodd",
    }
}

fn line_cap_str(c: LineCap) -> &'static str {
    match c {
        LineCap::Butt => "butt",
        LineCap::Round => "round",
        LineCap::Square => "square",
    }
}

fn line_join_str(j: LineJoin) -> &'static str {
    match j {
        LineJoin::Miter => "miter",
        LineJoin::Round => "round",
        LineJoin::Bevel => "bevel",
    }
}

fn path_to_d(path: &Path) -> String {
    let mut d = String::new();
    for cmd in &path.cmds {
        match cmd {
            PathCmd::MoveTo(p) => {
                d.push_str(&format!("M{} {} ", fixed_f(p.x), fixed_f(p.y)));
            }
            PathCmd::LineTo(p) => {
                d.push_str(&format!("L{} {} ", fixed_f(p.x), fixed_f(p.y)));
            }
            PathCmd::QuadTo { ctrl, end } => {
                d.push_str(&format!(
                    "Q{} {} {} {} ",
                    fixed_f(ctrl.x),
                    fixed_f(ctrl.y),
                    fixed_f(end.x),
                    fixed_f(end.y)
                ));
            }
            PathCmd::CubicTo { ctrl1, ctrl2, end } => {
                d.push_str(&format!(
                    "C{} {} {} {} {} {} ",
                    fixed_f(ctrl1.x),
                    fixed_f(ctrl1.y),
                    fixed_f(ctrl2.x),
                    fixed_f(ctrl2.y),
                    fixed_f(end.x),
                    fixed_f(end.y)
                ));
            }
            PathCmd::Close => {
                d.push_str("Z ");
            }
        }
    }
    d.trim_end().to_string()
}

pub fn scene_to_svg(scene: &Scene, width: u32, height: u32) -> String {
    let (w, h) = if width == 0 || height == 0 {
        let (max_w, max_h) = scene_bbox(scene);
        (max_w.max(1), max_h.max(1))
    } else {
        (width, height)
    };
    let mut svg = format!(
        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"0 0 {} {}\">",
        w, h, w, h
    );
    for op in &scene.ops {
        match op {
            SceneOp::GroupBegin {
                transform,
                opacity,
                ..
            } => {
                let mut attrs = String::new();
                if let Some(t) = transform {
                    attrs.push_str(&transform_attr(t));
                }
                if let Some(o) = opacity {
                    attrs.push_str(&opacity_attr(*o));
                }
                svg.push_str(&format!("<g{}>", attrs));
            }
            SceneOp::GroupEnd => {
                svg.push_str("</g>");
            }
            SceneOp::FillPath {
                path,
                transform,
                color,
                opa,
                fill_rule,
            } => {
                let d = path_to_d(path);
                let mut attrs = format!(
                    " d=\"{}\" fill=\"{}\" fill-opacity=\"{:.3}\" fill-rule=\"{}\"",
                    d,
                    color_hex(color),
                    *opa as f32 / 255.0,
                    fill_rule_str(*fill_rule)
                );
                if *transform != Transform::IDENTITY {
                    attrs.push_str(&transform_attr(transform));
                }
                svg.push_str(&format!("<path{}/>", attrs));
            }
            SceneOp::StrokePath {
                path,
                transform,
                color,
                width,
                opa,
                line_cap,
                line_join,
                miter_limit,
            } => {
                let d = path_to_d(path);
                let mut attrs = format!(
                    " d=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{}\" stroke-opacity=\"{:.3}\"",
                    d,
                    color_hex(color),
                    fixed_f(*width),
                    *opa as f32 / 255.0
                );
                attrs.push_str(&format!(" stroke-linecap=\"{}\"", line_cap_str(*line_cap)));
                attrs.push_str(&format!(" stroke-linejoin=\"{}\"", line_join_str(*line_join)));
                let ml = fixed_f(*miter_limit);
                if (ml - 4.0).abs() > 0.01 {
                    attrs.push_str(&format!(" stroke-miterlimit=\"{}\"", ml));
                }
                if *transform != Transform::IDENTITY {
                    attrs.push_str(&transform_attr(transform));
                }
                svg.push_str(&format!("<path{}/>", attrs));
            }
            SceneOp::FillRect {
                area,
                transform,
                color,
                radius,
                opa,
                ..
            } => {
                let mut attrs = format!(
                    " x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"{}\" fill-opacity=\"{:.3}\"",
                    fixed_f(area.x),
                    fixed_f(area.y),
                    fixed_f(area.w),
                    fixed_f(area.h),
                    color_hex(color),
                    *opa as f32 / 255.0
                );
                if *radius != mirx::Fixed::from_int(0) {
                    attrs.push_str(&format!(" rx=\"{}\" ry=\"{}\"", fixed_f(*radius), fixed_f(*radius)));
                }
                if *transform != Transform::IDENTITY {
                    attrs.push_str(&transform_attr(transform));
                }
                svg.push_str(&format!("<rect{}/>", attrs));
            }
            SceneOp::Border {
                area,
                transform,
                color,
                width,
                radius,
                opa,
                ..
            } => {
                let mut attrs = format!(
                    " x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{}\" stroke-opacity=\"{:.3}\"",
                    fixed_f(area.x),
                    fixed_f(area.y),
                    fixed_f(area.w),
                    fixed_f(area.h),
                    color_hex(color),
                    fixed_f(*width),
                    *opa as f32 / 255.0
                );
                if *radius != mirx::Fixed::from_int(0) {
                    attrs.push_str(&format!(" rx=\"{}\" ry=\"{}\"", fixed_f(*radius), fixed_f(*radius)));
                }
                if *transform != Transform::IDENTITY {
                    attrs.push_str(&transform_attr(transform));
                }
                svg.push_str(&format!("<rect{}/>", attrs));
            }
            SceneOp::Line {
                p1,
                p2,
                transform,
                color,
                width,
                opa,
                ..
            } => {
                let mut attrs = format!(
                    " x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"{}\" stroke-width=\"{}\" stroke-opacity=\"{:.3}\"",
                    fixed_f(p1.x),
                    fixed_f(p1.y),
                    fixed_f(p2.x),
                    fixed_f(p2.y),
                    color_hex(color),
                    fixed_f(*width),
                    *opa as f32 / 255.0
                );
                if *transform != Transform::IDENTITY {
                    attrs.push_str(&transform_attr(transform));
                }
                svg.push_str(&format!("<line{}/>", attrs));
            }
            SceneOp::Arc { .. } | SceneOp::Label { .. } | SceneOp::Blit { .. } => {}
        }
    }
    svg.push_str("</svg>");
    svg
}

fn scene_bbox(scene: &Scene) -> (u32, u32) {
    let mut max_x = 0f32;
    let mut max_y = 0f32;
    for op in &scene.ops {
        match op {
            SceneOp::FillPath { path, .. } | SceneOp::StrokePath { path, .. } => {
                for cmd in &path.cmds {
                    if let Some(p) = cmd_endpoint(cmd) {
                        max_x = max_x.max(p.0);
                        max_y = max_y.max(p.1);
                    }
                }
            }
            SceneOp::FillRect { area, .. } | SceneOp::Border { area, .. } => {
                max_x = max_x.max(fixed_f(area.x) + fixed_f(area.w));
                max_y = max_y.max(fixed_f(area.y) + fixed_f(area.h));
            }
            SceneOp::Line { p1, p2, .. } => {
                max_x = max_x.max(fixed_f(p1.x)).max(fixed_f(p2.x));
                max_y = max_y.max(fixed_f(p1.y)).max(fixed_f(p2.y));
            }
            _ => {}
        }
    }
    (max_x.ceil().max(1.0) as u32, max_y.ceil().max(1.0) as u32)
}

fn cmd_endpoint(cmd: &PathCmd) -> Option<(f32, f32)> {
    match cmd {
        PathCmd::MoveTo(p) | PathCmd::LineTo(p) => Some((fixed_f(p.x), fixed_f(p.y))),
        PathCmd::QuadTo { end, .. } | PathCmd::CubicTo { end, .. } => {
            Some((fixed_f(end.x), fixed_f(end.y)))
        }
        PathCmd::Close => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use mirx::{Fixed, Point};

    #[test]
    fn empty_scene_produces_minimal_svg() {
        let scene = Scene { ops: Vec::new() };
        let svg = scene_to_svg(&scene, 10, 10);
        assert!(svg.contains("<svg"));
        assert!(svg.contains("</svg>"));
        assert!(svg.contains("viewBox=\"0 0 10 10\""));
    }

    #[test]
    fn fill_path_emits_path_element() {
        let cmds = vec![
            PathCmd::MoveTo(Point::new(Fixed::from_int(0), Fixed::from_int(0))),
            PathCmd::LineTo(Point::new(Fixed::from_int(10), Fixed::from_int(0))),
            PathCmd::Close,
        ];
        let scene = Scene {
            ops: vec![SceneOp::FillPath {
                path: Path { cmds },
                transform: Transform::IDENTITY,
                color: Color { r: 255, g: 0, b: 0, a: 255 },
                opa: 255,
                fill_rule: FillRule::NonZero,
            }],
        };
        let svg = scene_to_svg(&scene, 20, 20);
        assert!(svg.contains("<path"));
        assert!(svg.contains("d=\"M0 0 L10 0 Z\""));
        assert!(svg.contains("fill=\"#FF0000\""));
        assert!(svg.contains("fill-rule=\"nonzero\""));
    }

    #[test]
    fn fill_rect_emits_rect_element() {
        let scene = Scene {
            ops: vec![SceneOp::FillRect {
                area: mirx::Rect::new(Fixed::from_int(5), Fixed::from_int(5), Fixed::from_int(15), Fixed::from_int(20)),
                transform: Transform::IDENTITY,
                quad: None,
                color: Color { r: 0, g: 128, b: 255, a: 255 },
                radius: Fixed::from_int(4),
                opa: 200,
            }],
        };
        let svg = scene_to_svg(&scene, 40, 40);
        assert!(svg.contains("<rect"));
        assert!(svg.contains("x=\"5\""));
        assert!(svg.contains("width=\"15\""));
        assert!(svg.contains("rx=\"4\""));
        assert!(svg.contains("fill=\"#0080FF\""));
    }

    #[test]
    fn group_emits_g_element() {
        let scene = Scene {
            ops: vec![
                SceneOp::GroupBegin {
                    transform: None,
                    opacity: Some(128),
                    clip: None,
                    mask: None,
                    filter: None,
                    disjoint_hint: false,
                },
                SceneOp::GroupEnd,
            ],
        };
        let svg = scene_to_svg(&scene, 10, 10);
        assert!(svg.contains("<g opacity=\"0.502\">"));
        assert!(svg.contains("</g>"));
    }
}