bbcat 0.5.8

Decode ANSI and BBS art in Rust, view it in terminals, or export PNG, APNG, and GIF images
Documentation
use std::{error::Error as StdError, path::Path, time::Duration};

use bbcat::{DecodeOptions, Format};

#[test]
fn decodes_and_encodes_through_the_public_api() -> Result<(), Box<dyn StdError>> {
    let document = bbcat::decode_with_options(
        b"\x1b[31mHi",
        DecodeOptions {
            file_name: Some(Path::new("welcome.ans")),
            width: Some(4),
        },
    )?;

    assert_eq!(document.format, Format::AnsiText);
    assert_eq!((document.screen.width, document.screen.height), (4, 1));
    assert_eq!(
        document.screen.cell(0, 0).unwrap().character,
        u16::from(b'H')
    );
    assert_eq!(document.screen.glyph_dimensions(), (8, 16));
    assert_eq!(document.screen.pixel_dimensions(), Some((32, 16)));
    assert_eq!(document.screen.palette(), bbcat::VGA_PALETTE);
    assert_eq!(document.screen.color(196), [255, 0, 0]);
    assert_eq!(document.screen.palette_256()[235], [38, 38, 38]);
    assert_eq!(document.screen.font().unwrap().len(), 256 * 16);
    assert!(document.screen.raster().is_none());

    let png = document.encode_png(1)?;
    assert!(png.starts_with(b"\x89PNG\r\n\x1a\n"));
    Ok(())
}

#[test]
fn preserves_xterm_256_colors_in_screen_and_png() -> Result<(), Box<dyn StdError>> {
    let document = bbcat::decode_with_options(
        b"\x1b[38;5;196;48;5;235mX",
        DecodeOptions {
            file_name: Some(Path::new("colors.ans")),
            width: Some(1),
        },
    )?;
    let cell = document.screen.cell(0, 0).unwrap();
    assert_eq!((cell.foreground, cell.background), (196, 235));
    assert_eq!(document.screen.color(cell.foreground), [255, 0, 0]);
    assert_eq!(document.screen.color(cell.background), [38, 38, 38]);

    let png = document.encode_png(1)?;
    assert_eq!(png[24], 8);
    Ok(())
}

#[test]
fn reports_the_detected_format() {
    let error = bbcat::decode(b"XBIN\x1a").unwrap_err();
    assert!(error.message().contains("truncated XBin header"));

    let data = concat!(
        r#"{"type":"Dimensions","text":"1x1","frame":"SAUCE_record"}"#,
        "\n",
        r#"{"id":"1","type":"frame","duration_ms":25}"#,
        "\n",
        r#"{"x":0,"y":0,"text":"X","color":"15","frame":"1"}"#,
    );
    let document = bbcat::decode(data.as_bytes()).unwrap();
    assert_eq!(document.format, Format::DarkDraw);
}

#[test]
fn exposes_character_and_raster_formats() -> Result<(), Box<dyn StdError>> {
    let xbin = [
        b'X', b'B', b'I', b'N', 0x1a, 1, 0, 1, 0, 0, 0x08, b'A', 0x1e,
    ];
    let document = bbcat::decode(&xbin)?;
    assert_eq!(document.format, Format::XBin);
    assert_eq!(document.screen.cell(0, 0).unwrap().foreground, 14);

    let document = bbcat::decode(b"!|*|c0F|L00000A0A|#")?;
    assert_eq!(document.format, Format::Ripscrip);
    let raster = document.screen.raster().unwrap();
    assert_eq!((raster.width, raster.height), (640, 350));
    assert_eq!(raster.pixels[0], 15);
    Ok(())
}

#[test]
fn exposes_animation_encoders_and_explicit_asciimation() -> Result<(), Box<dyn StdError>> {
    let ansi = b"\x1b[2J\x1b[H\x1b[1;1HA\x1b[1;1HB\x1b[2J";
    let document = bbcat::decode_with_options(
        ansi,
        DecodeOptions {
            file_name: Some(Path::new("demo.ans")),
            width: Some(1),
        },
    )?;
    assert!(document.animation.is_some());
    assert!(
        document
            .encode_apng(bbcat::DEFAULT_ANIMATION_BAUD, 1)?
            .starts_with(b"\x89PNG\r\n\x1a\n")
    );
    assert!(
        document
            .encode_gif(bbcat::DEFAULT_ANIMATION_BAUD, 1)?
            .starts_with(b"GIF89a")
    );

    let mut stream = String::from("2\n");
    for _ in 0..13 {
        stream.push_str("x\n");
    }
    let animation = bbcat::decode_asciimation(stream.as_bytes())?;
    assert_eq!(animation.frames[0].duration(), Duration::from_millis(200));
    assert_eq!(animation.frames[0].rows().len(), 13);
    Ok(())
}