rexpaint/
lib.rs

1//! Provides for reading of REXPaint .xp files
2//!
3//! Copyright (C) 2018 Mara <cyphergothic@protonmail.com>
4//! This work is free. You can redistribute it and/or modify it under the
5//! terms of the Do What The Fuck You Want To Public License, Version 2,
6#![deny(missing_debug_implementations)]
7#![deny(non_upper_case_globals)]
8#![deny(non_camel_case_types)]
9#![deny(non_snake_case)]
10#![deny(unused_mut)]
11#![warn(missing_docs)]
12
13use std::io;
14use std::io::prelude::*;
15
16use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
17use flate2::read::GzDecoder;
18use flate2::write::GzEncoder;
19use flate2::Compression;
20
21/// Structure representing the components of one color
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct XpColor {
24    /// Red component 0..255
25    pub r: u8,
26    /// Green component 0..255
27    pub g: u8,
28    /// Blue component 0..255
29    pub b: u8,
30}
31
32impl XpColor {
33    /// deepest black
34    pub const BLACK: XpColor = XpColor { r: 0, g: 0, b: 0 };
35    /// color 0xff00ff (hot pink) is regarded as transparent
36    pub const TRANSPARENT: XpColor = XpColor {
37        r: 255,
38        g: 0,
39        b: 255,
40    };
41
42    /// Construct a new color from r,g,b values
43    pub fn new(r: u8, g: u8, b: u8) -> XpColor {
44        XpColor { r, g, b }
45    }
46
47    /// Return whether this color is considered transparent (if this is the background color of a
48    /// cell, the layer below it will see through)
49    pub fn is_transparent(self) -> bool {
50        self == XpColor::TRANSPARENT
51    }
52
53    /// Read a RGB color from a `ReadBytesExt`
54    fn read<T: ReadBytesExt>(rdr: &mut T) -> io::Result<XpColor> {
55        let r = rdr.read_u8()?;
56        let g = rdr.read_u8()?;
57        let b = rdr.read_u8()?;
58        Ok(XpColor { r, g, b })
59    }
60
61    /// Write a RGB color to a `WriteBytesExt`
62    fn write<T: WriteBytesExt>(self, wr: &mut T) -> io::Result<()> {
63        wr.write_u8(self.r)?;
64        wr.write_u8(self.g)?;
65        wr.write_u8(self.b)?;
66        Ok(())
67    }
68}
69
70/// Structure representing a character and its foreground/background color
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct XpCell {
73    /// Character index
74    /// This depends on the font but will usually be a code page 437 character
75    /// (one way to convert to a rust unicode character one way is to use
76    /// `CP437_WINGDINGS.decode(...)` in the `codepage_437` crate!)
77    pub ch: u32,
78    /// Foreground color
79    pub fg: XpColor,
80    /// Background color
81    pub bg: XpColor,
82}
83
84/// Structure representing a layer
85/// Cells are in the same order as in the file, in column-major order (index of position x,y is y*height + x).
86#[derive(Debug, Clone, PartialEq)]
87pub struct XpLayer {
88    /// Width of layer (in cells)
89    pub width: usize,
90    /// Height of layer (in cells)
91    pub height: usize,
92    /// Content of layer
93    pub cells: Vec<XpCell>,
94}
95
96impl XpLayer {
97    /// Construct a new XpLayer of width by height. The contents will be empty (black foreground
98    /// and background, character 0).
99    pub fn new(width: usize, height: usize) -> XpLayer {
100        XpLayer {
101            width,
102            height,
103            cells: vec![
104                XpCell {
105                    ch: 0,
106                    fg: XpColor::BLACK,
107                    bg: XpColor::BLACK
108                };
109                width * height
110            ],
111        }
112    }
113
114    /// Get the cell at coordinates (x,y), or None if it is out of range.
115    pub fn get(&self, x: usize, y: usize) -> Option<&XpCell> {
116        if x < self.width && y < self.height {
117            Some(&self.cells[x * self.height + y])
118        } else {
119            None
120        }
121    }
122
123    /// Get mutable reference to the cell at coordinates (x,y), or None if it is out of range.
124    pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut XpCell> {
125        if x < self.width && y < self.height {
126            Some(&mut self.cells[x * self.height + y])
127        } else {
128            None
129        }
130    }
131}
132
133/// Structure representing a REXPaint image file which is a stack of layers
134#[derive(Debug, Clone, PartialEq)]
135pub struct XpFile {
136    /// Version number from header
137    pub version: i32,
138    /// Layers of the image
139    pub layers: Vec<XpLayer>,
140}
141
142impl XpFile {
143    /// Construct a new XpFile with one layer of width by height. The contents will be empty (black
144    /// foreground and background, character 0).
145    pub fn new(width: usize, height: usize) -> XpFile {
146        XpFile {
147            version: -1,
148            layers: vec![XpLayer::new(width, height)],
149        }
150    }
151
152    /// Read a xp image from a stream
153    pub fn read<R: Read>(f: &mut R) -> io::Result<XpFile> {
154        let mut rdr = GzDecoder::new(f);
155        let version = rdr.read_i32::<LittleEndian>()?;
156        let num_layers = rdr.read_u32::<LittleEndian>()?;
157
158        let mut layers = Vec::<XpLayer>::new();
159        layers.reserve(num_layers as usize);
160        for _layer in 0..num_layers {
161            let width = rdr.read_u32::<LittleEndian>()? as usize;
162            let height = rdr.read_u32::<LittleEndian>()? as usize;
163
164            let mut cells = Vec::<XpCell>::new();
165            cells.reserve(width * height);
166            for _y in 0..width {
167                // column-major order
168                for _x in 0..height {
169                    let ch = rdr.read_u32::<LittleEndian>()?;
170                    let fg = XpColor::read(&mut rdr)?;
171                    let bg = XpColor::read(&mut rdr)?;
172                    cells.push(XpCell { ch, fg, bg });
173                }
174            }
175            layers.push(XpLayer {
176                width,
177                height,
178                cells,
179            });
180        }
181        Ok(XpFile { version, layers })
182    }
183
184    /// Write a xp image to a stream
185    pub fn write<W: Write>(&self, f: &mut W) -> io::Result<()> {
186        let mut wr = GzEncoder::new(f, Compression::best());
187        wr.write_i32::<LittleEndian>(self.version)?; // only supported version is -1
188        wr.write_u32::<LittleEndian>(self.layers.len() as u32)?;
189        for layer in &self.layers {
190            wr.write_u32::<LittleEndian>(layer.width as u32)?;
191            wr.write_u32::<LittleEndian>(layer.height as u32)?;
192
193            for cell in &layer.cells {
194                wr.write_u32::<LittleEndian>(cell.ch)?;
195                cell.fg.write(&mut wr)?;
196                cell.bg.write(&mut wr)?;
197            }
198        }
199        Ok(())
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use std::fs::File;
207    use std::io::{Cursor, Seek, SeekFrom};
208
209    const WIDTH: usize = 80;
210    const HEIGHT: usize = 60;
211
212    #[test]
213    fn test_roundtrip() {
214        let mut xp = XpFile::new(WIDTH, HEIGHT);
215        for y in 0..HEIGHT {
216            for x in 0..WIDTH {
217                let cell = xp.layers[0].get_mut(x, y).unwrap();
218                cell.ch = (32 + x + y) as u32;
219                cell.fg = XpColor::new(y as u8, 0, 255 - y as u8);
220                cell.bg = XpColor::new(x as u8, 0, 255 - x as u8);
221            }
222        }
223
224        let mut f = Cursor::new(Vec::new());
225        xp.write(&mut f).unwrap();
226        f.seek(SeekFrom::Start(0)).unwrap();
227
228        let xp2 = XpFile::read(&mut f).unwrap();
229        assert_eq!(xp, xp2);
230    }
231
232    #[test]
233    fn test_image() {
234        let mut f = File::open("test_images/mltest.xp").unwrap();
235        let xp = XpFile::read(&mut f).unwrap();
236        assert_eq!(xp.version, -1);
237        assert_eq!(xp.layers.len(), 2);
238        assert_eq!(xp.layers[0].width, 8);
239        assert_eq!(xp.layers[0].height, 4);
240        assert_eq!(xp.layers[1].width, 8);
241        assert_eq!(xp.layers[1].height, 4);
242        assert_eq!(xp.layers[1].get(0, 0).unwrap().fg, XpColor::BLACK);
243        assert_eq!(xp.layers[1].get(0, 0).unwrap().bg.is_transparent(), true);
244        assert_eq!(xp.layers[1].get(0, 0).unwrap().ch, 32);
245        assert_eq!(xp.layers[1].get(2, 2).unwrap().ch, 'B' as u32);
246        assert_eq!(xp.layers[0].get(0, 0).unwrap().fg, XpColor::new(0, 0, 255));
247        assert_eq!(xp.layers[0].get(0, 0).unwrap().bg, XpColor::BLACK);
248        assert_eq!(xp.layers[0].get(0, 0).unwrap().ch, 'A' as u32);
249    }
250}