use std::path::Path;
use anyhow::{Context, Result};
use badterm_core::sauce::{self, Sauce};
use badterm_core::{Cell, Terminal};
use crate::bezel::Bezel;
use crate::crt::CrtMode;
use crate::render::{Display, Font, Frame, Renderer, Scaling};
#[derive(Default)]
pub struct RenderOptions {
pub crt: CrtMode,
pub nine_dot: bool,
pub aspect: bool,
pub screen_only: bool,
pub font: u8,
pub bezel: Option<Bezel>,
pub glow: crate::crt::Glow,
pub scanlines: crate::crt::Scanlines,
pub curvature: crate::crt::Curvature,
}
const MAX_ROWS: usize = 2000;
pub struct Art {
pub term: Terminal,
pub rows: usize,
pub sauce: Option<Sauce>,
}
pub fn load(data: &[u8], font: u8, screen_only: bool) -> Art {
let (content, sauce) = sauce::split(data);
let ice = sauce.as_ref().is_some_and(|s| s.ice) || content.windows(5).any(|w| w == b"\x1b[?33");
let cols = match sauce.as_ref().map(|s| s.width) {
Some(w @ 40..=255) if !screen_only => w as usize,
_ => 80,
};
let font = if font != 0 { font } else { sauce.as_ref().map_or(0, Sauce::cterm_font) };
let rows = if screen_only { 25 } else { MAX_ROWS };
let mut term = Terminal::new(cols, rows, 0);
term.screen.ice_colors = ice;
term.screen.base_ice = ice;
term.screen.base_font = font;
term.screen.look.fonts[0] = font;
term.feed(content);
let used = if screen_only {
25
} else {
let last = (0..rows).rev().find(|&y| term.screen.row(y).iter().any(|c| !c.is_blank()));
last.map_or(25, |y| (y + 1).max(25))
};
Art { term, rows: used, sauce }
}
pub fn render_file(input: &Path, output: &Path, opts: RenderOptions) -> Result<(usize, usize)> {
let data = std::fs::read(input).with_context(|| format!("reading {}", input.display()))?;
let Art { term, rows: used, .. } = load(&data, opts.font, opts.screen_only);
let ice = term.screen.ice_colors;
let cols = term.screen.cols();
let lines: Vec<&[Cell]> = (0..used).map(|y| term.screen.row(y)).collect();
let cell_w = if opts.nine_dot { 9 } else { 8 };
let mut r = Renderer::new(Font::vga_8x16(), cols, used);
r.set_layout(Font::vga_8x16(), cell_w, cols, used);
let look = &term.screen.look;
r.draw(&Frame { look, look_rows: used, ..Frame::plain(lines, None, ice) }, true, false);
let (nw, nh) = (r.native_width(), r.native_height());
let (w, h, pixels) = if let Some(bezel) = &opts.bezel {
let (w, h) = (nw * 2 + nw / 2, nw * 2);
let frame = bezel.render(w, h);
let (sw, sh) = (frame.screen.2.max(1) as f64, frame.screen.3.max(1) as f64);
let pixel_aspect = sh / sw * nw as f64 / nh as f64;
let mut out = vec![0; w * h];
r.blit_in(
&mut out,
w,
h,
frame.screen,
&Display {
scaling: Scaling::Fill,
pixel_aspect,
crt: opts.crt,
glow: opts.glow,
scanlines: opts.scanlines,
curvature: opts.curvature,
},
);
crate::bezel::composite(&mut out, &frame.pixels);
(w, h, out)
} else if opts.crt == CrtMode::Off && !opts.aspect {
(nw, nh, r.pixels().to_vec())
} else {
let pixel_aspect = if opts.aspect { nw as f64 / 400.0 * 0.75 } else { 1.0 };
let (w, h) = (nw * 2, (nh as f64 * 2.0 * pixel_aspect).round() as usize);
let mut out = vec![0; w * h];
r.blit(
&mut out,
w,
h,
&Display {
scaling: Scaling::Fill,
pixel_aspect,
crt: opts.crt,
glow: opts.glow,
scanlines: opts.scanlines,
curvature: opts.curvature,
},
);
(w, h, out)
};
std::fs::write(output, crate::png::encode(&pixels, w, h))
.with_context(|| format!("writing {}", output.display()))?;
Ok((w, h))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sauce_sets_width_font_and_ice() {
let mut file = b"\x1b[1;31mART\x1b[0m".to_vec();
file.push(0x1A);
let mut r = vec![0u8; 128];
r[..7].copy_from_slice(b"SAUCE00");
r[94] = 1;
r[96..98].copy_from_slice(&132u16.to_le_bytes());
r[105] = 1;
r[106..120].copy_from_slice(b"Amiga Topaz 2+");
file.extend(r);
let art = load(&file, 0, false);
assert_eq!(art.term.screen.cols(), 132);
assert!(art.term.screen.ice_colors);
assert_eq!(art.term.screen.look.fonts[0], 40);
assert_eq!(art.rows, 25);
assert_eq!(load(&file, 42, false).term.screen.look.fonts[0], 42);
}
#[test]
fn renders_a_file() {
let dir = std::env::temp_dir().join(format!("badterm-render-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let ans = dir.join("a.ans");
std::fs::write(&ans, b"\x1b[1;32mHello\r\n".repeat(30)).unwrap();
let png = dir.join("a.png");
assert_eq!(render_file(&ans, &png, RenderOptions::default()).unwrap(), (640, 30 * 16));
let opts = RenderOptions { crt: CrtMode::Green, screen_only: true, ..RenderOptions::default() };
assert_eq!(render_file(&ans, &png, opts).unwrap(), (1280, 800));
let bezel = Some(Bezel::find("Beige, 1980s", &dir).unwrap());
let opts = RenderOptions { screen_only: true, bezel, ..RenderOptions::default() };
assert_eq!(render_file(&ans, &png, opts).unwrap(), (1600, 1280));
assert!(std::fs::read(&png).unwrap().starts_with(b"\x89PNG"));
std::fs::remove_dir_all(&dir).ok();
}
}