rusvid_core 0.2.1

Core library for rusvid
Documentation
/*
To rebuild all snapshots run `cargo test` with the env variable `TEST_REBUILD`.
To save temporary files from the tests for debugging run `cargo test` with the env variable `TEST_SAVE`.
*/

use std::io::Cursor;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use image::io::Reader;
use image::{ImageFormat, RgbImage};
use image_compare::rgb_hybrid_compare;
use rusvid_core::holder::likes::color_like::ColorLike;
use rusvid_core::holder::likes::path_like::PathLike;
use rusvid_core::holder::likes::types_like::TypesLike;
use rusvid_core::holder::object::Object;
use rusvid_core::holder::stroke::Stroke;
use rusvid_core::holder::svg_holder::{SvgHolder, SvgItem};
use rusvid_core::holder::transform::{Transform, TransformLogic};
use rusvid_core::pixel::Pixel;
use rusvid_core::plane::Plane;
use rusvid_core::point::Point;

const DELTA: f64 = 0.00005;

const SIMPLE_PATH: &'static [u8] = include_bytes!("./data/simple_path.bmp");
const SIMPLE_TRANSFORM: &'static [u8] = include_bytes!("./data/simple_transform.bmp");
const SIMPLE_TRANSFORM_STROKE: &'static [u8] = include_bytes!("./data/simple_transform_stroke.bmp");
const SIMPLE_TRANSFORM_COLOR: &'static [u8] = include_bytes!("./data/simple_transform_color.bmp");
const SIMPLE_TRANSFORM_POSITION: &'static [u8] =
    include_bytes!("./data/simple_transform_position.bmp");
const SIMPLE_TRANSFORM_MOVE: &'static [u8] = include_bytes!("./data/simple_transform_move.bmp");
const SIMPLE_TRANSFORM_COLOR_NONE: &'static [u8] =
    include_bytes!("./data/simple_transform_color_none.bmp");
const SIMPLE_TRANSFORM_VISIBILITY: &'static [u8] =
    include_bytes!("./data/simple_transform_visibility.bmp");

fn rebuild_snapshots<P: AsRef<Path>>(image: &RgbImage, name: P) -> Result<()> {
    if option_env!("TEST_REBUILD").is_some() {
        let mut path_buf = PathBuf::new();
        path_buf.push(std::env::current_dir()?);
        path_buf.push("./tests/data");
        path_buf.push(name);
        path_buf.set_extension("bmp");

        image.save_with_format(path_buf, ImageFormat::Bmp)?
    }

    Ok(())
}

fn save_if_env(image: &RgbImage, name: &str) -> Result<()> {
    if option_env!("TEST_SAVE").is_some() {
        image.save_with_format(format!("{}.png", name), ImageFormat::Png)?;
    }

    Ok(())
}

fn test_image(image: RgbImage, snapshot: &[u8], name: &str) -> Result<()> {
    let mut reader = Reader::new(Cursor::new(snapshot));
    reader.set_format(ImageFormat::Bmp);
    let test_image = reader.decode()?.to_rgb8();

    let result = rgb_hybrid_compare(&image, &test_image)?;

    save_if_env(&image, name)?;

    assert!(
        result.score > (1.0 - DELTA),
        "score: {} (at {})",
        result.score,
        name
    );

    Ok(())
}

fn rebuild_and_test<P: AsRef<Path> + Clone>(plane: Plane, name: P, snapshot: &[u8]) -> Result<()> {
    let image = plane.as_rgb_image()?;

    let binding = name.clone();
    let test_name = binding
        .as_ref()
        .to_str()
        .context("Can't transform `P` to &str")?;

    rebuild_snapshots(&image, name)?;
    test_image(image, snapshot, test_name)
}

#[test]
fn simple_path() {
    let mut svg = SvgHolder::new();
    let triangle = SvgItem::new(
        vec![
            PathLike::Move(Point::new(100.0, 100.0)),
            PathLike::Line(Point::new(150.0, 100.0)),
            PathLike::Line(Point::new(120.0, 150.0)),
            PathLike::Close,
        ],
        Some(ColorLike::Color([0, 255, 100, 255].into())),
    );
    svg.add_item(triangle);

    let heart = SvgItem::new(
        vec![
            PathLike::Move(Point::new(100.0, 100.0)),
            PathLike::Line(Point::new(150.0, 50.0)),
            PathLike::CurveTo(
                Point::new(100.0, 25.0),
                Point::new(169.0, 11.0),
                Point::new(119.0, -13.0),
            ),
            PathLike::CurveTo(
                Point::new(50.0, 50.0),
                Point::new(80.0, -13.0),
                Point::new(30.0, 11.0),
            ),
            PathLike::Close,
        ],
        Some(ColorLike::Color([255, 0, 0, 255].into())),
    );
    svg.add_item(heart);

    let object = Object::new(TypesLike::Svg(svg));

    let plane = object.render(300, 300).unwrap();
    rebuild_and_test(plane, "simple_path", SIMPLE_PATH).unwrap();
}

#[test]
fn simple_transform() {
    let mut svg = SvgHolder::new();
    let triangle_id = svg.add_item(SvgItem::new(
        vec![
            PathLike::Move(Point::new(100.0, 100.0)),
            PathLike::Line(Point::new(150.0, 100.0)),
            PathLike::Line(Point::new(120.0, 150.0)),
            PathLike::Close,
        ],
        Some(ColorLike::Color([0, 255, 120, 255].into())),
    ));

    let heart_id = svg.add_item(SvgItem::new(
        vec![
            PathLike::Move(Point::new(100.0, 100.0)),
            PathLike::Line(Point::new(150.0, 50.0)),
            PathLike::CurveTo(
                Point::new(100.0, 25.0),
                Point::new(169.0, 11.0),
                Point::new(119.0, -13.0),
            ),
            PathLike::CurveTo(
                Point::new(50.0, 50.0),
                Point::new(80.0, -13.0),
                Point::new(30.0, 11.0),
            ),
            PathLike::Close,
        ],
        Some(ColorLike::Color([200, 100, 20, 255].into())),
    ));

    let mut object = Object::new(TypesLike::Svg(svg));
    let plane = object.render(300, 300).unwrap();
    rebuild_and_test(plane, "simple_transform", SIMPLE_TRANSFORM).unwrap();

    object
        .transform_by_id(
            &triangle_id,
            &Transform::Stroke(Some(Stroke {
                paint: ColorLike::Color([100, 50, 120, 255].into()),
                width: 1.75,
                ..Stroke::default()
            })),
        )
        .unwrap();
    let plane = object.render(300, 300).unwrap();
    rebuild_and_test(plane, "simple_transform_stroke", SIMPLE_TRANSFORM_STROKE).unwrap();

    object
        .transform_by_id(
            &heart_id,
            &Transform::Color(Some(ColorLike::Color(Pixel::new(230, 57, 70, 255)))),
        )
        .unwrap();
    let plane = object.render(300, 300).unwrap();
    rebuild_and_test(plane, "simple_transform_color", SIMPLE_TRANSFORM_COLOR).unwrap();

    object
        .transform_by_id(&triangle_id, &Transform::Position(Point::new(200.0, 200.0)))
        .unwrap();
    let plane = object.render(300, 300).unwrap();
    rebuild_and_test(
        plane,
        "simple_transform_position",
        SIMPLE_TRANSFORM_POSITION,
    )
    .unwrap();

    object
        .transform_by_id(&heart_id, &Transform::Move(Point::new(50.0, 0.0)))
        .unwrap();
    let plane = object.render(300, 300).unwrap();
    rebuild_and_test(plane, "simple_transform_move", SIMPLE_TRANSFORM_MOVE).unwrap();

    object
        .transform_by_id(&triangle_id, &Transform::Color(None))
        .unwrap();
    let plane = object.render(300, 300).unwrap();
    rebuild_and_test(
        plane,
        "simple_transform_color_none",
        SIMPLE_TRANSFORM_COLOR_NONE,
    )
    .unwrap();

    object
        .transform_by_id(&triangle_id, &Transform::Visibility(false))
        .unwrap();
    let plane = object.render(300, 300).unwrap();
    rebuild_and_test(
        plane,
        "simple_transform_visibility",
        SIMPLE_TRANSFORM_VISIBILITY,
    )
    .unwrap();
}