#![deny(missing_debug_implementations)]
#![deny(non_upper_case_globals)]
#![deny(non_camel_case_types)]
#![deny(non_snake_case)]
#![deny(unused_mut)]
#![warn(missing_docs)]
use std::io;
use std::io::prelude::*;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XpColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl XpColor {
pub const BLACK: XpColor = XpColor { r: 0, g: 0, b: 0 };
pub const TRANSPARENT: XpColor = XpColor {
r: 255,
g: 0,
b: 255,
};
pub fn new(r: u8, g: u8, b: u8) -> XpColor {
XpColor { r, g, b }
}
pub fn is_transparent(self) -> bool {
self == XpColor::TRANSPARENT
}
fn read<T: ReadBytesExt>(rdr: &mut T) -> io::Result<XpColor> {
let r = rdr.read_u8()?;
let g = rdr.read_u8()?;
let b = rdr.read_u8()?;
Ok(XpColor { r, g, b })
}
fn write<T: WriteBytesExt>(self, wr: &mut T) -> io::Result<()> {
wr.write_u8(self.r)?;
wr.write_u8(self.g)?;
wr.write_u8(self.b)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XpCell {
pub ch: u32,
pub fg: XpColor,
pub bg: XpColor,
}
#[derive(Debug, Clone, PartialEq)]
pub struct XpLayer {
pub width: usize,
pub height: usize,
pub cells: Vec<XpCell>,
}
impl XpLayer {
pub fn new(width: usize, height: usize) -> XpLayer {
XpLayer {
width,
height,
cells: vec![
XpCell {
ch: 0,
fg: XpColor::BLACK,
bg: XpColor::BLACK
};
width * height
],
}
}
pub fn get(&self, x: usize, y: usize) -> Option<&XpCell> {
if x < self.width && y < self.height {
Some(&self.cells[x * self.height + y])
} else {
None
}
}
pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut XpCell> {
if x < self.width && y < self.height {
Some(&mut self.cells[x * self.height + y])
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct XpFile {
pub version: i32,
pub layers: Vec<XpLayer>,
}
impl XpFile {
pub fn new(width: usize, height: usize) -> XpFile {
XpFile {
version: -1,
layers: vec![XpLayer::new(width, height)],
}
}
pub fn read<R: Read>(f: &mut R) -> io::Result<XpFile> {
let mut rdr = GzDecoder::new(f);
let version = rdr.read_i32::<LittleEndian>()?;
let num_layers = rdr.read_u32::<LittleEndian>()?;
let mut layers = Vec::<XpLayer>::new();
layers.reserve(num_layers as usize);
for _layer in 0..num_layers {
let width = rdr.read_u32::<LittleEndian>()? as usize;
let height = rdr.read_u32::<LittleEndian>()? as usize;
let mut cells = Vec::<XpCell>::new();
cells.reserve(width * height);
for _x in 0..width {
for _y in 0..height {
let ch = rdr.read_u32::<LittleEndian>()?;
let fg = XpColor::read(&mut rdr)?;
let bg = XpColor::read(&mut rdr)?;
cells.push(XpCell { ch, fg, bg });
}
}
layers.push(XpLayer {
width,
height,
cells,
});
}
Ok(XpFile { version, layers })
}
pub fn write<W: Write>(&self, f: &mut W) -> io::Result<()> {
let mut wr = GzEncoder::new(f, Compression::best());
wr.write_i32::<LittleEndian>(self.version)?; wr.write_u32::<LittleEndian>(self.layers.len() as u32)?;
for layer in &self.layers {
wr.write_u32::<LittleEndian>(layer.width as u32)?;
wr.write_u32::<LittleEndian>(layer.height as u32)?;
for cell in &layer.cells {
wr.write_u32::<LittleEndian>(cell.ch)?;
cell.fg.write(&mut wr)?;
cell.bg.write(&mut wr)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::{Cursor, Seek, SeekFrom};
const WIDTH: usize = 80;
const HEIGHT: usize = 60;
#[test]
fn test_roundtrip() {
let mut xp = XpFile::new(WIDTH, HEIGHT);
for y in 0..HEIGHT {
for x in 0..WIDTH {
let cell = xp.layers[0].get_mut(x, y).unwrap();
cell.ch = (32 + x + y) as u32;
cell.fg = XpColor::new(y as u8, 0, 255 - y as u8);
cell.bg = XpColor::new(x as u8, 0, 255 - x as u8);
}
}
let mut f = Cursor::new(Vec::new());
xp.write(&mut f).unwrap();
f.seek(SeekFrom::Start(0)).unwrap();
let xp2 = XpFile::read(&mut f).unwrap();
assert_eq!(xp, xp2);
}
#[test]
fn test_image() {
let mut f = File::open("test_images/mltest.xp").unwrap();
let xp = XpFile::read(&mut f).unwrap();
assert_eq!(xp.version, -1);
assert_eq!(xp.layers.len(), 2);
assert_eq!(xp.layers[0].width, 8);
assert_eq!(xp.layers[0].height, 4);
assert_eq!(xp.layers[1].width, 8);
assert_eq!(xp.layers[1].height, 4);
assert_eq!(xp.layers[1].get(0, 0).unwrap().fg, XpColor::BLACK);
assert_eq!(xp.layers[1].get(0, 0).unwrap().bg.is_transparent(), true);
assert_eq!(xp.layers[1].get(0, 0).unwrap().ch, 32);
assert_eq!(xp.layers[1].get(2, 2).unwrap().ch, 'B' as u32);
assert_eq!(xp.layers[0].get(0, 0).unwrap().fg, XpColor::new(0, 0, 255));
assert_eq!(xp.layers[0].get(0, 0).unwrap().bg, XpColor::BLACK);
assert_eq!(xp.layers[0].get(0, 0).unwrap().ch, 'A' as u32);
}
}