From d011cc1e5265854ea2de0c3fdd11702fb6c8d380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 24 May 2020 11:58:45 +0200 Subject: [PATCH 1/6] slp: initial 8 bit and 32 bit SLP support --- Cargo.toml | 1 + crates/genie-slp/Cargo.toml | 12 + crates/genie-slp/src/lib.rs | 487 +++++++++++++++++++++ crates/genie-slp/test/fixtures/eslogo1.slp | Bin 0 -> 38810 bytes crates/genie-slp/test/fixtures/eslogo2.slp | Bin 0 -> 38810 bytes 5 files changed, 500 insertions(+) create mode 100644 crates/genie-slp/Cargo.toml create mode 100644 crates/genie-slp/src/lib.rs create mode 100755 crates/genie-slp/test/fixtures/eslogo1.slp create mode 100755 crates/genie-slp/test/fixtures/eslogo2.slp diff --git a/Cargo.toml b/Cargo.toml index 2b64029..1f577c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "crates/genie-lang", "crates/genie-rec", "crates/genie-scx", + "crates/genie-slp", "crates/genie-support", "crates/jascpal" ] diff --git a/crates/genie-slp/Cargo.toml b/crates/genie-slp/Cargo.toml new file mode 100644 index 0000000..023c8d9 --- /dev/null +++ b/crates/genie-slp/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "genie-slp" +version = "0.1.0" +authors = ["Renée Kooi "] +edition = "2018" + +[dependencies] +byteorder = "^1.3.1" +rgb = "0.8.17" + +[dev-dependencies] +anyhow = "1.0.31" diff --git a/crates/genie-slp/src/lib.rs b/crates/genie-slp/src/lib.rs new file mode 100644 index 0000000..7749698 --- /dev/null +++ b/crates/genie-slp/src/lib.rs @@ -0,0 +1,487 @@ +//! Parser for the Age of Empires 1/2 graphic file format, SLP. + +#![deny(future_incompatible)] +#![deny(nonstandard_style)] +#![deny(rust_2018_idioms)] +#![deny(unsafe_code)] +#![warn(missing_docs)] +#![warn(unused)] + +use byteorder::{ReadBytesExt, LE}; +pub use rgb::RGBA8; +use std::ffi::CStr; +use std::fmt::{self, Debug, Display}; +use std::io::{Cursor, Read, Result, Seek, SeekFrom}; + +/// Trait for graphic formats. +pub trait Format: Sized { + /// The data type for a single pixel. + type Pixel: Copy; + + /// Read a draw command from an input stream. + fn read_command(reader: R) -> Result>; +} + +/// Trait for SLP formats. SLP formats differ in the size of the pixel data. +pub trait SLPFormat { + /// The data type for a single pixel. + type Pixel: Copy; + + /// Read a single pixel value from an input stream. + fn read_pixel(reader: R) -> Result; +} + +/// The classic 8-bit palette-based pixel format, used across all versions. +pub struct PalettePixelFormat; +/// The 32-bit RGBA pixel format introduced in Age of Empires 2: HD Edition. +pub struct RGBAPixelFormat; + +impl SLPFormat for PalettePixelFormat { + type Pixel = u8; + + fn read_pixel(mut reader: R) -> Result { + reader.read_u8() + } +} + +impl SLPFormat for RGBAPixelFormat { + type Pixel = RGBA8; + + fn read_pixel(mut reader: R) -> Result { + let [r, g, b, a] = reader.read_u32::()?.to_le_bytes(); + Ok(RGBA8 { r, g, b, a }) + } +} + +impl Format for S +where + S::Pixel: Copy, +{ + type Pixel = S::Pixel; + + fn read_command(mut reader: R) -> Result> { + fn read_pixels(num_pixels: u32, mut reader: R) -> Result> + where + R: Read, + S: SLPFormat, + S::Pixel: Copy, + { + let mut pixels = Vec::with_capacity(num_pixels as usize); + for _ in 0..num_pixels { + pixels.push(S::read_pixel(&mut reader)?); + } + Ok(pixels) + } + let command = reader.read_u8()?; + if command & 0b1111 == 0b1111 { + return Ok(Command::NextLine); + } + if command & 0b11 == 0b00 { + let num_pixels = u32::from(command >> 2); + let pixels = read_pixels::<&mut R, S>(num_pixels, &mut reader)?; + return Ok(Command::Copy(pixels)); + } + if command & 0b11 == 0b01 { + let num_pixels = u32::from(if command >> 2 != 0 { + command >> 2 + } else { + reader.read_u8()? + }); + return Ok(Command::Skip(num_pixels)); + } + if command & 0b1111 == 0b0010 { + let num_pixels = u32::from(((command & 0b1111_0000) << 4) + reader.read_u8()?); + let pixels = read_pixels::<&mut R, S>(num_pixels, &mut reader)?; + return Ok(Command::Copy(pixels)); + } + if command & 0b1111 == 0b0011 { + let num_pixels = u32::from(((command & 0b1111_0000) << 4) + reader.read_u8()?); + return Ok(Command::Skip(num_pixels)); + } + if command & 0b1111 == 0b0110 { + let num_pixels = u32::from(if command >> 4 != 0 { + command >> 4 + } else { + reader.read_u8()? + }); + let pixels = read_pixels::<&mut R, S>(num_pixels, &mut reader)?; + return Ok(Command::PlayerCopy(pixels)); + } + if command & 0b1111 == 0b0111 { + let num_pixels = u32::from(if command >> 4 != 0 { + command >> 4 + } else { + reader.read_u8()? + }); + let pixel = S::read_pixel(&mut reader)?; + return Ok(Command::Fill(num_pixels, pixel)); + } + if command & 0b1111 == 0b1010 { + let num_pixels = u32::from(if command >> 4 != 0 { + command >> 4 + } else { + reader.read_u8()? + }); + let pixel = S::read_pixel(&mut reader)?; + return Ok(Command::PlayerFill(num_pixels, pixel)); + } + unimplemented!() + } +} + +/// An SLP command. +#[derive(Debug, Clone)] +pub enum Command { + /// Copy pixels to the output. + Copy(Vec), + /// Copy pixels to the output, applying a player colour transformation. + PlayerCopy(Vec), + /// Fill this many pixels in the output with a specific colour. + Fill(u32, Pixel), + /// Fill this many pixels in the output with a specific colour, applying a player colour + /// transformation. + PlayerFill(u32, Pixel), + /// Skip this many pixels in the output. + Skip(u32), + /// Continue to the next line. + NextLine, +} + +/* +impl Command { + pub fn to_writer(&self, w: &mut W) -> Result<()> { + match self { + Command::Copy(colors) => { + let n = colors.len(); + if n >= 64 { + w.write_u8(0x02 | ((n & 0xF00) >> 4))?; + w.write_u8(n & 0xFF)?; + } else { + w.write_u8(0x00 | (n << 2))?; + } + w.write_all(&colors)?; + } + _ => unimplemented!(), + } + Ok(()) + } + + pub fn to_bytes(&self) -> Vec { + let mut v = vec![]; + self.to_writer(&mut v).unwrap(); + v + } +} +*/ + +/// SLP file version identifier. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct SLPVersion([u8; 4]); + +impl Display for SLPVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", std::str::from_utf8(&self.0).unwrap()) + } +} + +impl Debug for SLPVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SLPVersion({})", self) + } +} + +/// Outline data: how many pixels at the start and end of each row should be transparent. +#[derive(Debug, Default)] +pub struct Outline { + /// How many pixels should be transparent at the start of the row. + pub left: u16, + /// How many pixels should be transparent at the end of the row. + pub right: u16, +} + +impl Outline { + /// Read SLP frame row outline data from an input stream. + pub fn read_from(mut input: impl Read) -> Result { + let left = input.read_u16::()?; + let right = input.read_u16::()?; + + Ok(Self { left, right }) + } + + /// Is this entire row transparent? + pub fn is_transparent(&self) -> bool { + ((self.left | self.right) & 0x8000) == 0x8000 + } +} + +#[derive(Debug, Default)] +struct SLPFrameMeta { + command_table_offset: u32, + outline_table_offset: u32, + palette_offset: u32, + properties: u32, + width: i32, + height: i32, + hotspot: (i32, i32), + outlines: Vec, + command_offsets: Vec, +} + +impl SLPFrameMeta { + pub(crate) fn read_from(mut input: impl Read) -> Result { + let mut frame = Self::default(); + frame.command_table_offset = input.read_u32::()?; + frame.outline_table_offset = input.read_u32::()?; + frame.palette_offset = input.read_u32::()?; + frame.properties = input.read_u32::()?; + frame.width = input.read_i32::()?; + frame.height = input.read_i32::()?; + frame.hotspot = (input.read_i32::()?, input.read_i32::()?); + Ok(frame) + } + + pub(crate) fn read_outlines(&mut self, input: &[u8]) -> Result<()> { + let mut input = Cursor::new(&input[(self.outline_table_offset as usize)..]); + for _ in 0..self.height { + self.outlines.push(Outline::read_from(&mut input)?); + } + Ok(()) + } + + pub(crate) fn read_command_offsets(&mut self, input: &[u8]) -> Result<()> { + let mut input = Cursor::new(&input[(self.command_table_offset as usize)..]); + for _ in 0..self.height { + self.command_offsets.push(input.read_u32::()? as usize); + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct SLPFrame<'slp> { + meta: &'slp SLPFrameMeta, + buffer: &'slp [u8], +} + +impl<'slp> SLPFrame<'slp> { + fn new(meta: &'slp SLPFrameMeta, buffer: &'slp [u8]) -> Self { + Self { meta, buffer } + } + + /// Get the size of this frame in pixels. Returns `(width, height)`. + pub fn size(&self) -> (i32, i32) { + (self.meta.width, self.meta.height) + } + + /// Get the hotspot location of this frame. + pub fn hotspot(&self) -> (i32, i32) { + self.meta.hotspot + } + + /// Does this frame contain 32 bit pixel data? + pub fn is_32bit(&self) -> bool { + self.meta.properties & 7 == 7 + } + + /// Does this frame contain 8 bit pixel data? + pub fn is_8bit(&self) -> bool { + !self.is_32bit() + } + + /// Iterate over the commands in this 8 bit frame. + /// + /// # Panics + /// This function panics if this frame's pixel format is not 8 bit. + pub fn render_8bit(&self) -> FrameCommandIterator<'_, PalettePixelFormat> { + assert!(self.is_8bit(), "render_8bit() called on a 32 bit frame"); + FrameCommandIterator::new(self.buffer, &self.meta.command_offsets) + } + + /// Iterate over the commands in this 32 bit frame. + /// + /// # Panics + /// This function panics if this frame's pixel format is not 32 bit. + pub fn render_32bit(&self) -> FrameCommandIterator<'_, RGBAPixelFormat> { + assert!(self.is_32bit(), "render_32bit() called on an 8 bit frame"); + FrameCommandIterator::new(self.buffer, &self.meta.command_offsets) + } +} + +#[derive(Debug)] +pub struct SLP { + bytes: Vec, + version: SLPVersion, + comment: String, + frames: Vec, +} + +impl SLP { + /// Read an SLP file from an input stream. This reads the full stream into memory. + pub fn read_from(mut input: impl Read) -> Result { + let mut bytes = Vec::new(); + input.read_to_end(&mut bytes)?; + Self::from_bytes(bytes) + } + + /// Read an SLP file from a byte array. + pub fn from_bytes(bytes: Vec) -> Result { + let mut input = Cursor::new(&bytes); + let version = { + let mut bytes = [0; 4]; + input.read_exact(&mut bytes)?; + SLPVersion(bytes) + }; + let num_frames = input.read_i32::()? as u32 as usize; + let comment = { + let mut bytes = [0; 24]; + input.read_exact(&mut bytes)?; + CStr::from_bytes_with_nul(&bytes) + .expect("could not create CStr from comment") + .to_str() + .expect("comment not utf-8") + .to_string() + }; + + let mut frames = Vec::with_capacity(num_frames); + for _ in 0..num_frames { + frames.push(SLPFrameMeta::read_from(&mut input)?); + } + + for frame in frames.iter_mut() { + frame.read_outlines(&bytes)?; + frame.read_command_offsets(&bytes)?; + } + + Ok(Self { + bytes, + version, + comment, + frames, + }) + } + + /// Iterate over the frames in this SLP file. + pub fn frames(&self) -> impl Iterator> + '_ { + self.frames + .iter() + .map(move |frame| SLPFrame::new(frame, &self.bytes)) + } + + /// Get an individual frame. + /// + /// # Panics + /// This function panics if the `index` is out of bounds. + pub fn frame(&self, index: usize) -> SLPFrame<'_> { + SLPFrame::new(&self.frames[index], &self.bytes) + } + + /// Get the number of frames in this SLP file. + pub fn num_frames(&self) -> usize { + self.frames.len() + } +} + +/// Iterator over commands in an SLP frame. +pub struct FrameCommandIterator<'a, F> +where + F: Format, +{ + line: u32, + end: bool, + offsets: &'a [usize], + buffer: Cursor<&'a [u8]>, + _format: std::marker::PhantomData, +} + +impl<'a, F> FrameCommandIterator<'a, F> +where + F: Format, +{ + fn new(bytes: &'a [u8], offsets: &'a [usize]) -> Self { + let mut buffer = Cursor::new(bytes); + buffer.seek(SeekFrom::Start(offsets[0] as u64)).unwrap(); + Self { + _format: std::marker::PhantomData, + line: 0, + end: false, + offsets, + buffer, + } + } + + /// Jump to the next line. Remaining commands on the current line will not be read. + pub fn next_line(&mut self) { + self.line += 1; + match self.offsets.get(self.line as usize).copied() { + Some(offset) => { + self.buffer.seek(SeekFrom::Start(offset as u64)).unwrap(); + } + None => { + // Return `None` the next time `next()` is called. + self.end = true; + } + } + } +} + +impl<'a, F> Iterator for FrameCommandIterator<'a, F> +where + F: Format, +{ + type Item = Result>; + + fn next(&mut self) -> Option { + if self.end { + return None; + } + + match F::read_command(&mut self.buffer) { + Ok(command) => match command { + Command::NextLine => { + self.next_line(); + Some(Ok(command)) + } + command => Some(Ok(command)), + }, + err @ Err(_) => Some(err), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + + #[test] + fn parse_slp() -> anyhow::Result<()> { + let f = File::open("test/fixtures/eslogo1.slp")?; + let slp = SLP::read_from(f)?; + assert_eq!(slp.num_frames(), 2); + assert!(slp.frame(0).is_32bit()); + assert!(slp.frame(1).is_32bit()); + assert!(!slp.frame(0).is_8bit()); + assert!(!slp.frame(1).is_8bit()); + assert_eq!(slp.frame(0).size(), (127, 92)); + assert_eq!(slp.frame(1).size(), (127, 92)); + Ok(()) + } + + #[test] + fn render_32bit() -> anyhow::Result<()> { + let f = File::open("test/fixtures/eslogo1.slp")?; + let slp = SLP::read_from(f)?; + for command in slp.frame(0).render_32bit() { + let _ = command?; + } + Ok(()) + } + + #[test] + #[should_panic = "render_8bit() called on a 32 bit frame"] + fn render_32bit_slp_as_8bit() { + let f = File::open("test/fixtures/eslogo1.slp").unwrap(); + let slp = SLP::read_from(f).unwrap(); + for _ in slp.frame(0).render_8bit() {} + } +} diff --git a/crates/genie-slp/test/fixtures/eslogo1.slp b/crates/genie-slp/test/fixtures/eslogo1.slp new file mode 100755 index 0000000000000000000000000000000000000000..a99648b0d2607333db625fd90a2980bdd3a6277a GIT binary patch literal 38810 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 Hcg6n!UH!AU literal 0 HcmV?d00001 From 98a7129a3ffa9cf743972f9d0743613599408781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 24 May 2020 13:02:57 +0200 Subject: [PATCH 2/6] slp: integrate jascpal, add renderslp example --- Cargo.toml | 2 + crates/genie-slp/Cargo.toml | 3 +- crates/genie-slp/src/lib.rs | 89 ++++++++++++++------------ examples/renderslp.rs | 120 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 5 files changed, 176 insertions(+), 40 deletions(-) create mode 100644 examples/renderslp.rs diff --git a/Cargo.toml b/Cargo.toml index 1f577c2..3cebc67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,14 @@ genie-hki = { version = "0.2.1", path = "crates/genie-hki" } genie-lang = { version = "0.2.1", path = "crates/genie-lang" } genie-rec = { version = "0.1.0", path = "crates/genie-rec" } genie-scx = { version = "3.0.0", path = "crates/genie-scx" } +genie-slp = { version = "0.0.0", path = "crates/genie-slp" } jascpal = { version = "0.1.1", path = "crates/jascpal" } [dev-dependencies] structopt = "^0.3.11" anyhow = "1.0.27" simplelog = "0.8.0" +image = "0.23.4" [workspace] members = [ diff --git a/crates/genie-slp/Cargo.toml b/crates/genie-slp/Cargo.toml index 023c8d9..82b6267 100644 --- a/crates/genie-slp/Cargo.toml +++ b/crates/genie-slp/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "genie-slp" -version = "0.1.0" +version = "0.0.0" authors = ["Renée Kooi "] edition = "2018" [dependencies] byteorder = "^1.3.1" +jascpal = { version = "0.1.1", path = "../jascpal" } rgb = "0.8.17" [dev-dependencies] diff --git a/crates/genie-slp/src/lib.rs b/crates/genie-slp/src/lib.rs index 7749698..b56e135 100644 --- a/crates/genie-slp/src/lib.rs +++ b/crates/genie-slp/src/lib.rs @@ -8,6 +8,7 @@ #![warn(unused)] use byteorder::{ReadBytesExt, LE}; +pub use jascpal::PaletteIndex; pub use rgb::RGBA8; use std::ffi::CStr; use std::fmt::{self, Debug, Display}; @@ -37,10 +38,10 @@ pub struct PalettePixelFormat; pub struct RGBAPixelFormat; impl SLPFormat for PalettePixelFormat { - type Pixel = u8; + type Pixel = PaletteIndex; fn read_pixel(mut reader: R) -> Result { - reader.read_u8() + reader.read_u8().map(Into::into) } } @@ -53,10 +54,7 @@ impl SLPFormat for RGBAPixelFormat { } } -impl Format for S -where - S::Pixel: Copy, -{ +impl Format for S { type Pixel = S::Pixel; fn read_command(mut reader: R) -> Result> { @@ -64,7 +62,6 @@ where where R: Read, S: SLPFormat, - S::Pixel: Copy, { let mut pixels = Vec::with_capacity(num_pixels as usize); for _ in 0..num_pixels { @@ -147,32 +144,46 @@ pub enum Command { NextLine, } -/* -impl Command { - pub fn to_writer(&self, w: &mut W) -> Result<()> { +impl Command { + pub fn map_color( + self, + mut transform: impl FnMut(Pixel) -> OutputPixel, + ) -> Command { match self { - Command::Copy(colors) => { - let n = colors.len(); - if n >= 64 { - w.write_u8(0x02 | ((n & 0xF00) >> 4))?; - w.write_u8(n & 0xFF)?; - } else { - w.write_u8(0x00 | (n << 2))?; + Self::Copy(pixels) => Command::Copy(pixels.into_iter().map(transform).collect()), + Self::Fill(num, pixel) => Command::Fill(num, transform(pixel)), + Self::PlayerCopy(pixels) => todo!(), + Self::PlayerFill(num, pixel) => todo!(), + Self::Skip(num) => Command::Skip(num), + Self::NextLine => Command::NextLine, + } + } + + /* + pub fn to_writer(&self, w: &mut W) -> Result<()> { + match self { + Command::Copy(colors) => { + let n = colors.len(); + if n >= 64 { + w.write_u8(0x02 | ((n & 0xF00) >> 4))?; + w.write_u8(n & 0xFF)?; + } else { + w.write_u8(0x00 | (n << 2))?; + } + w.write_all(&colors)?; } - w.write_all(&colors)?; + _ => unimplemented!(), } - _ => unimplemented!(), + Ok(()) } - Ok(()) - } - pub fn to_bytes(&self) -> Vec { - let mut v = vec![]; - self.to_writer(&mut v).unwrap(); - v - } + pub fn to_bytes(&self) -> Vec { + let mut v = vec![]; + self.to_writer(&mut v).unwrap(); + v + } + */ } -*/ /// SLP file version identifier. #[derive(Clone, Copy, PartialEq, Eq)] @@ -220,8 +231,8 @@ struct SLPFrameMeta { outline_table_offset: u32, palette_offset: u32, properties: u32, - width: i32, - height: i32, + width: u32, + height: u32, hotspot: (i32, i32), outlines: Vec, command_offsets: Vec, @@ -234,8 +245,8 @@ impl SLPFrameMeta { frame.outline_table_offset = input.read_u32::()?; frame.palette_offset = input.read_u32::()?; frame.properties = input.read_u32::()?; - frame.width = input.read_i32::()?; - frame.height = input.read_i32::()?; + frame.width = input.read_u32::()?; + frame.height = input.read_u32::()?; frame.hotspot = (input.read_i32::()?, input.read_i32::()?); Ok(frame) } @@ -269,7 +280,7 @@ impl<'slp> SLPFrame<'slp> { } /// Get the size of this frame in pixels. Returns `(width, height)`. - pub fn size(&self) -> (i32, i32) { + pub fn size(&self) -> (u32, u32) { (self.meta.width, self.meta.height) } @@ -292,18 +303,18 @@ impl<'slp> SLPFrame<'slp> { /// /// # Panics /// This function panics if this frame's pixel format is not 8 bit. - pub fn render_8bit(&self) -> FrameCommandIterator<'_, PalettePixelFormat> { + pub fn render_8bit(&self) -> SLPFrameCommands<'_, PalettePixelFormat> { assert!(self.is_8bit(), "render_8bit() called on a 32 bit frame"); - FrameCommandIterator::new(self.buffer, &self.meta.command_offsets) + SLPFrameCommands::new(self.buffer, &self.meta.command_offsets) } /// Iterate over the commands in this 32 bit frame. /// /// # Panics /// This function panics if this frame's pixel format is not 32 bit. - pub fn render_32bit(&self) -> FrameCommandIterator<'_, RGBAPixelFormat> { + pub fn render_32bit(&self) -> SLPFrameCommands<'_, RGBAPixelFormat> { assert!(self.is_32bit(), "render_32bit() called on an 8 bit frame"); - FrameCommandIterator::new(self.buffer, &self.meta.command_offsets) + SLPFrameCommands::new(self.buffer, &self.meta.command_offsets) } } @@ -382,7 +393,7 @@ impl SLP { } /// Iterator over commands in an SLP frame. -pub struct FrameCommandIterator<'a, F> +pub struct SLPFrameCommands<'a, F> where F: Format, { @@ -393,7 +404,7 @@ where _format: std::marker::PhantomData, } -impl<'a, F> FrameCommandIterator<'a, F> +impl<'a, F> SLPFrameCommands<'a, F> where F: Format, { @@ -424,7 +435,7 @@ where } } -impl<'a, F> Iterator for FrameCommandIterator<'a, F> +impl<'a, F> Iterator for SLPFrameCommands<'a, F> where F: Format, { diff --git a/examples/renderslp.rs b/examples/renderslp.rs new file mode 100644 index 0000000..0a371db --- /dev/null +++ b/examples/renderslp.rs @@ -0,0 +1,120 @@ +use anyhow::bail; +use genie::slp::{Command, RGBA8}; +use genie::{Palette, SLP}; +use image::png::PNGEncoder; +use image::{ColorType, ImageResult}; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Cli { + filename: PathBuf, + #[structopt(long = "out", short = "o")] + output: PathBuf, + #[structopt(long = "palette", short = "p")] + palette: Option, + #[structopt(long = "frame", short = "f")] + frame: Option, +} + +struct Output { + f: File, + size: (u32, u32), + image_data: Vec, +} + +impl Output { + fn new(path: impl AsRef, size: (u32, u32)) -> io::Result { + let f = File::create(path)?; + + Ok(Self { + f, + size, + image_data: Vec::with_capacity((size.0 * size.1 * 4) as usize), + }) + } + + fn write_pixel(&mut self, pixel: RGBA8) { + // println!("write {:?}", pixel); + let RGBA8 { r, g, b, a } = pixel; + self.image_data.extend_from_slice(&[r, g, b, a]); + } + + fn write(&mut self, command: Command) { + match command { + Command::Copy(pixels) => { + pixels.into_iter().for_each(|p| self.write_pixel(p)); + } + Command::Fill(num, pixel) => { + (0..num).for_each(|_| self.write_pixel(pixel)); + } + Command::PlayerCopy(pixels) => todo!(), + Command::PlayerFill(num, pixel) => todo!(), + Command::Skip(num) => { + (0..num).for_each(|_| self.write_pixel(RGBA8::default())); + } + Command::NextLine => { + // should do this differently + let pixel_width = self.size.0; + let byte_width = (pixel_width * 4) as usize; + let line_progress = self.image_data.len() % byte_width; + if line_progress == 0 { + return; + } + let byte_remaining = byte_width - line_progress; + let pixel_remaining = byte_remaining / 4; + (0..pixel_remaining).for_each(|_| self.write_pixel(RGBA8::default())); + } + } + } + + fn finish(self) -> ImageResult<()> { + let encoder = PNGEncoder::new(self.f); + encoder.encode(&self.image_data, self.size.0, self.size.1, ColorType::Rgba8)?; + Ok(()) + } +} + +pub fn main() -> anyhow::Result<()> { + let args = Cli::from_args(); + let f = File::open(&args.filename)?; + let slp = SLP::read_from(f)?; + + if let Some(frame) = args.frame { + let frame = slp.frame(frame); + if frame.is_8bit() && args.palette.is_none() { + bail!("That frame uses 8-bit palette indexes. Please provide a `--palette` file"); + } + if frame.is_32bit() && args.palette.is_some() { + println!("note: Ignoring palette because the frame uses 32-bit colour"); + } + + let mut output = Output::new(args.output, frame.size())?; + if frame.is_8bit() { + let palette = { + let f = File::open(args.palette.unwrap())?; + Palette::read_from(f)? + }; + + for command in frame.render_8bit() { + let command = command?.map_color(|index| palette[index].alpha(255)); + output.write(command); + } + } else { + for command in frame.render_32bit() { + let command = command?; + output.write(command); + } + } + + output.finish()?; + } else { + for (id, frame) in slp.frames().enumerate() { + println!("#{} - {}×{}", id, frame.size().0, frame.size().1); + } + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index a33d411..e2116ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ pub use genie_hki as hki; pub use genie_lang as lang; pub use genie_rec as rec; pub use genie_scx as scx; +pub use genie_slp as slp; pub use jascpal as pal; pub use genie_cpx::Campaign; @@ -98,4 +99,5 @@ pub use genie_hki::HotkeyInfo; pub use genie_lang::LangFile; pub use genie_rec::RecordedGame; pub use genie_scx::Scenario; +pub use genie_slp::SLP; pub use jascpal::Palette; From e2e00f32bbf4b19f0bb67a8b9cdc27d5e18e4d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 24 May 2020 13:09:41 +0200 Subject: [PATCH 3/6] slp: add map_color doc comment and test --- crates/genie-slp/src/lib.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/genie-slp/src/lib.rs b/crates/genie-slp/src/lib.rs index b56e135..90f1b83 100644 --- a/crates/genie-slp/src/lib.rs +++ b/crates/genie-slp/src/lib.rs @@ -127,7 +127,7 @@ impl Format for S { } /// An SLP command. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Command { /// Copy pixels to the output. Copy(Vec), @@ -145,6 +145,29 @@ pub enum Command { } impl Command { + /// Transform the pixel colour values in this command. + /// + /// # Examples + /// Use `map_color()` to apply a palette: + /// + /// ```rust + /// use genie_slp::{Command, RGBA8}; + /// + /// let mut palette = vec![RGBA8::default(); 256]; + /// palette[63] = RGBA8::new(0, 0xFF, 0x00, 0xFF); // green + /// palette[127] = RGBA8::new(0, 0, 0xFF, 0xFF); // blue + /// let command = Command::Copy(vec![127, 127, 63, 63]); + /// let command = command.map_color(|palette_index: u8| palette[palette_index as usize]); + /// assert_eq!( + /// command, + /// Command::Copy(vec![ + /// RGBA8::new(0, 0, 0xFF, 0xFF), + /// RGBA8::new(0, 0, 0xFF, 0xFF), + /// RGBA8::new(0, 0xFF, 0, 0xFF), + /// RGBA8::new(0, 0xFF, 0, 0xFF), + /// ]) + /// ); + /// ``` pub fn map_color( self, mut transform: impl FnMut(Pixel) -> OutputPixel, From bee1f27286bf6a5e7cb657402926ba03850549d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 24 May 2020 13:09:49 +0200 Subject: [PATCH 4/6] slp: cargo fix --- crates/genie-slp/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/genie-slp/src/lib.rs b/crates/genie-slp/src/lib.rs index 90f1b83..c1d7f39 100644 --- a/crates/genie-slp/src/lib.rs +++ b/crates/genie-slp/src/lib.rs @@ -175,8 +175,8 @@ impl Command { match self { Self::Copy(pixels) => Command::Copy(pixels.into_iter().map(transform).collect()), Self::Fill(num, pixel) => Command::Fill(num, transform(pixel)), - Self::PlayerCopy(pixels) => todo!(), - Self::PlayerFill(num, pixel) => todo!(), + Self::PlayerCopy(_pixels) => todo!(), + Self::PlayerFill(_num, _pixel) => todo!(), Self::Skip(num) => Command::Skip(num), Self::NextLine => Command::NextLine, } From e8363ec1835a866bd7132cf30f6074c3faa68598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 24 May 2020 13:15:18 +0200 Subject: [PATCH 5/6] slp: silence bit mask style nit from clippy --- crates/genie-slp/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/genie-slp/src/lib.rs b/crates/genie-slp/src/lib.rs index c1d7f39..3aeb7db 100644 --- a/crates/genie-slp/src/lib.rs +++ b/crates/genie-slp/src/lib.rs @@ -57,6 +57,7 @@ impl SLPFormat for RGBAPixelFormat { impl Format for S { type Pixel = S::Pixel; + #[allow(clippy::verbose_bit_mask)] fn read_command(mut reader: R) -> Result> { fn read_pixels(num_pixels: u32, mut reader: R) -> Result> where From 123dbfb6af024dd067a1c69fd89bef6edeec63b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 24 May 2020 13:28:43 +0200 Subject: [PATCH 6/6] slp: rename `render_*bit` functions --- crates/genie-slp/src/lib.rs | 18 +++++++++--------- examples/renderslp.rs | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/genie-slp/src/lib.rs b/crates/genie-slp/src/lib.rs index 3aeb7db..025fb9e 100644 --- a/crates/genie-slp/src/lib.rs +++ b/crates/genie-slp/src/lib.rs @@ -327,8 +327,8 @@ impl<'slp> SLPFrame<'slp> { /// /// # Panics /// This function panics if this frame's pixel format is not 8 bit. - pub fn render_8bit(&self) -> SLPFrameCommands<'_, PalettePixelFormat> { - assert!(self.is_8bit(), "render_8bit() called on a 32 bit frame"); + pub fn commands_8bit(&self) -> SLPFrameCommands<'_, PalettePixelFormat> { + assert!(self.is_8bit(), "commands_8bit() called on a 32 bit frame"); SLPFrameCommands::new(self.buffer, &self.meta.command_offsets) } @@ -336,8 +336,8 @@ impl<'slp> SLPFrame<'slp> { /// /// # Panics /// This function panics if this frame's pixel format is not 32 bit. - pub fn render_32bit(&self) -> SLPFrameCommands<'_, RGBAPixelFormat> { - assert!(self.is_32bit(), "render_32bit() called on an 8 bit frame"); + pub fn commands_32bit(&self) -> SLPFrameCommands<'_, RGBAPixelFormat> { + assert!(self.is_32bit(), "commands_32bit() called on an 8 bit frame"); SLPFrameCommands::new(self.buffer, &self.meta.command_offsets) } } @@ -503,20 +503,20 @@ mod tests { } #[test] - fn render_32bit() -> anyhow::Result<()> { + fn commands_32bit() -> anyhow::Result<()> { let f = File::open("test/fixtures/eslogo1.slp")?; let slp = SLP::read_from(f)?; - for command in slp.frame(0).render_32bit() { + for command in slp.frame(0).commands_32bit() { let _ = command?; } Ok(()) } #[test] - #[should_panic = "render_8bit() called on a 32 bit frame"] - fn render_32bit_slp_as_8bit() { + #[should_panic = "commands_8bit() called on a 32 bit frame"] + fn commands_32bit_slp_as_8bit() { let f = File::open("test/fixtures/eslogo1.slp").unwrap(); let slp = SLP::read_from(f).unwrap(); - for _ in slp.frame(0).render_8bit() {} + for _ in slp.frame(0).commands_8bit() {} } } diff --git a/examples/renderslp.rs b/examples/renderslp.rs index 0a371db..9542258 100644 --- a/examples/renderslp.rs +++ b/examples/renderslp.rs @@ -98,12 +98,12 @@ pub fn main() -> anyhow::Result<()> { Palette::read_from(f)? }; - for command in frame.render_8bit() { + for command in frame.commands_8bit() { let command = command?.map_color(|index| palette[index].alpha(255)); output.write(command); } } else { - for command in frame.render_32bit() { + for command in frame.commands_32bit() { let command = command?; output.write(command); }