use std::io::Cursor;
use wasm_bindgen::prelude::*;
use crate::models::TtpObject;
use crate::parser::parse_stream;
use crate::registry::PaletteRegistry;
use crate::renderer::render_sprite;
#[wasm_bindgen(start)]
pub fn init_panic_hook() {
#[cfg(feature = "wasm")]
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub struct RenderResult {
width: u32,
height: u32,
pixels: Vec<u8>,
warnings: Vec<String>,
}
#[wasm_bindgen]
impl RenderResult {
#[wasm_bindgen(getter)]
pub fn width(&self) -> u32 {
self.width
}
#[wasm_bindgen(getter)]
pub fn height(&self) -> u32 {
self.height
}
#[wasm_bindgen(getter)]
pub fn pixels(&self) -> Vec<u8> {
self.pixels.clone()
}
#[wasm_bindgen(getter)]
pub fn warnings(&self) -> Vec<String> {
self.warnings.clone()
}
}
fn parse_and_prepare(jsonl: &str) -> (PaletteRegistry, Vec<crate::models::Sprite>, Vec<String>) {
let parse_result = parse_stream(Cursor::new(jsonl));
let mut registry = PaletteRegistry::new();
let mut sprites = Vec::new();
let warnings: Vec<String> =
parse_result.warnings.iter().map(|w| format!("line {}: {}", w.line, w.message)).collect();
for obj in parse_result.objects {
match obj {
TtpObject::Palette(p) => {
registry.register(p);
}
TtpObject::Sprite(s) => {
sprites.push(s);
}
_ => {
}
}
}
(registry, sprites, warnings)
}
#[wasm_bindgen]
pub fn render_to_png(jsonl: &str) -> Vec<u8> {
let (registry, sprites, _warnings) = parse_and_prepare(jsonl);
if sprites.is_empty() {
return Vec::new();
}
let sprite = &sprites[0];
let resolved = registry.resolve_lenient(sprite);
let (image, _render_warnings) = render_sprite(sprite, &resolved.palette.colors);
let mut png_data = Vec::new();
{
use image::ImageEncoder;
let encoder = image::codecs::png::PngEncoder::new(&mut png_data);
encoder
.write_image(image.as_raw(), image.width(), image.height(), image::ColorType::Rgba8)
.ok();
}
png_data
}
#[wasm_bindgen]
pub fn render_to_rgba(jsonl: &str) -> RenderResult {
let (registry, sprites, mut warnings) = parse_and_prepare(jsonl);
if sprites.is_empty() {
warnings.push("No sprites found in input".to_string());
return RenderResult { width: 0, height: 0, pixels: Vec::new(), warnings };
}
let sprite = &sprites[0];
let resolved = registry.resolve_lenient(sprite);
if let Some(w) = &resolved.warning {
warnings.push(w.message.clone());
}
let (image, render_warnings) = render_sprite(sprite, &resolved.palette.colors);
for w in render_warnings {
warnings.push(w.message);
}
RenderResult {
width: image.width(),
height: image.height(),
pixels: image.into_raw(),
warnings,
}
}
#[wasm_bindgen]
pub fn list_sprites(jsonl: &str) -> Vec<String> {
let (_registry, sprites, _warnings) = parse_and_prepare(jsonl);
sprites.into_iter().map(|s| s.name).collect()
}
#[wasm_bindgen]
pub fn validate(jsonl: &str) -> Vec<String> {
let (registry, sprites, mut warnings) = parse_and_prepare(jsonl);
for sprite in &sprites {
let resolved = registry.resolve_lenient(sprite);
if let Some(w) = &resolved.warning {
warnings.push(format!("sprite '{}': {}", sprite.name, w.message));
}
}
warnings
}
#[cfg(test)]
mod tests {
use super::*;
const MINIMAL_DOT: &str = r##"{"type": "sprite", "name": "dot", "palette": {"{_}": "#00000000", "{x}": "#FF0000"}, "grid": ["{x}"]}"##;
const HEART_WITH_PALETTE: &str = r##"{"type": "palette", "name": "reds", "colors": {"{_}": "#00000000", "{r}": "#FF0000", "{p}": "#FF6B6B"}}
{"type": "sprite", "name": "heart", "palette": "reds", "grid": ["{_}{r}{r}{_}", "{r}{r}{r}{r}", "{_}{r}{r}{_}", "{_}{_}{r}{_}"]}"##;
#[test]
fn test_list_sprites() {
let result = list_sprites(MINIMAL_DOT);
assert_eq!(result, vec!["dot"]);
}
#[test]
fn test_list_sprites_multiple() {
let jsonl = r#"{"type": "sprite", "name": "a", "palette": {}, "grid": []}
{"type": "sprite", "name": "b", "palette": {}, "grid": []}"#;
let result = list_sprites(jsonl);
assert_eq!(result, vec!["a", "b"]);
}
#[test]
fn test_validate_valid() {
let result = validate(HEART_WITH_PALETTE);
assert!(result.is_empty(), "Expected no warnings: {:?}", result);
}
#[test]
fn test_validate_missing_palette() {
let jsonl =
r#"{"type": "sprite", "name": "bad", "palette": "nonexistent", "grid": ["{x}"]}"#;
let result = validate(jsonl);
assert!(!result.is_empty());
assert!(result[0].contains("not found"));
}
#[test]
fn test_render_to_rgba() {
let result = render_to_rgba(MINIMAL_DOT);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert_eq!(result.pixels().len(), 4); assert_eq!(result.pixels()[0], 255); assert_eq!(result.pixels()[1], 0); assert_eq!(result.pixels()[2], 0); assert_eq!(result.pixels()[3], 255); }
#[test]
fn test_render_to_rgba_no_sprites() {
let result = render_to_rgba(r#"{"type": "palette", "name": "empty", "colors": {}}"#);
assert_eq!(result.width(), 0);
assert_eq!(result.height(), 0);
assert!(result.warnings().iter().any(|w| w.contains("No sprites")));
}
#[test]
fn test_render_to_png() {
let result = render_to_png(MINIMAL_DOT);
assert!(!result.is_empty());
assert_eq!(&result[0..4], &[0x89, 0x50, 0x4E, 0x47]);
}
#[test]
fn test_render_to_png_no_sprites() {
let result = render_to_png(r#"{"type": "palette", "name": "empty", "colors": {}}"#);
assert!(result.is_empty());
}
}