bracket_rex/
rex.rs

1// The work here is heavily derived from rs-rexpaint, https://gitlab.com/medusacle/rs-rexpaint
2// It is Copyright (c) 2018, Mara <cyphergothic@protonmail.com>
3// It's under the DWTFYW Public License 2.0, so inclusion in an MIT-licensed program
4// isn't a problem.
5
6#![deny(non_upper_case_globals)]
7#![deny(non_camel_case_types)]
8#![deny(non_snake_case)]
9#![deny(unused_mut)]
10
11use std::io;
12use std::io::prelude::*;
13
14use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
15use flate2::read::GzDecoder;
16use flate2::write::GzEncoder;
17use flate2::Compression;
18
19use crate::prelude::XpColor;
20
21/// Structure representing a character and its foreground/background color
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct XpCell {
24    /// Character index
25    /// This depends on the font but will usually be a code page 437 character
26    /// (one way to convert to a rust unicode character one way is to use
27    /// `CP437_WINGDINGS.decode(...)` in the `codepage_437` crate!)
28    pub ch: u32,
29    /// Foreground color
30    pub fg: XpColor,
31    /// Background color
32    pub bg: XpColor,
33}
34
35/// Structure representing a layer
36/// Cells are in the same order as in the file, in column-major order (index of position x,y is y*height + x).
37#[derive(Debug, Clone, PartialEq)]
38pub struct XpLayer {
39    /// Width of layer (in cells)
40    pub width: usize,
41    /// Height of layer (in cells)
42    pub height: usize,
43    /// Content of layer
44    pub cells: Vec<XpCell>,
45}
46
47impl XpLayer {
48    /// Construct a new XpLayer of width by height. The contents will be empty (black foreground
49    /// and background, character 0).
50    pub fn new(width: usize, height: usize) -> XpLayer {
51        XpLayer {
52            width,
53            height,
54            cells: vec![
55                XpCell {
56                    ch: 0,
57                    fg: XpColor::BLACK,
58                    bg: XpColor::BLACK
59                };
60                width * height
61            ],
62        }
63    }
64
65    /// Get the cell at coordinates (x,y), or None if it is out of range.
66    pub fn get(&self, x: usize, y: usize) -> Option<&XpCell> {
67        if x < self.width && y < self.height {
68            Some(&self.cells[x * self.height + y])
69        } else {
70            None
71        }
72    }
73
74    /// Get mutable reference to the cell at coordinates (x,y), or None if it is out of range.
75    pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut XpCell> {
76        if x < self.width && y < self.height {
77            Some(&mut self.cells[x * self.height + y])
78        } else {
79            None
80        }
81    }
82}
83
84/// Structure representing a REXPaint image file which is a stack of layers
85#[derive(Debug, Clone, PartialEq)]
86pub struct XpFile {
87    /// Version number from header
88    pub version: i32,
89    /// Layers of the image
90    pub layers: Vec<XpLayer>,
91}
92
93impl XpFile {
94    /// Construct a new XpFile with one layer of width by height. The contents will be empty (black
95    /// foreground and background, character 0).
96    pub fn new(width: usize, height: usize) -> XpFile {
97        XpFile {
98            version: -1,
99            layers: vec![XpLayer::new(width, height)],
100        }
101    }
102
103    /// Helper to read from an BTerm resource
104    pub fn from_resource(path: &str) -> io::Result<XpFile> {
105        let res = bracket_embedding::prelude::EMBED
106            .lock()
107            .get_resource(path.to_string());
108        match res {
109            None => panic!("Unable to open resource"),
110            Some(r) => {
111                let buffer: Vec<u8> = Vec::from(r);
112                let mut bufslice = &*buffer;
113                XpFile::read(&mut bufslice)
114            }
115        }
116    }
117
118    /// Read a xp image from a stream
119    pub fn read<R: Read>(f: &mut R) -> io::Result<XpFile> {
120        let mut rdr = GzDecoder::new(f);
121        let version = rdr.read_i32::<LittleEndian>()?;
122        let num_layers = rdr.read_u32::<LittleEndian>()?;
123
124        let mut layers = Vec::<XpLayer>::new();
125        layers.reserve(num_layers as usize);
126        for _ in 0..num_layers {
127            let width = rdr.read_u32::<LittleEndian>()? as usize;
128            let height = rdr.read_u32::<LittleEndian>()? as usize;
129
130            let mut cells = Vec::<XpCell>::new();
131            cells.reserve(width * height);
132            for _ in 0..width {
133                // column-major order
134                for _ in 0..height {
135                    let ch = rdr.read_u32::<LittleEndian>()?;
136                    let fg = XpColor::read(&mut rdr)?;
137                    let bg = XpColor::read(&mut rdr)?;
138                    cells.push(XpCell { ch, fg, bg });
139                }
140            }
141            layers.push(XpLayer {
142                width,
143                height,
144                cells,
145            });
146        }
147        Ok(XpFile { version, layers })
148    }
149
150    /// Write a xp image to a stream
151    pub fn write<W: Write>(&self, f: &mut W) -> io::Result<()> {
152        let mut wr = GzEncoder::new(f, Compression::best());
153        wr.write_i32::<LittleEndian>(self.version)?; // only supported version is -1
154        wr.write_u32::<LittleEndian>(self.layers.len() as u32)?;
155        for layer in &self.layers {
156            wr.write_u32::<LittleEndian>(layer.width as u32)?;
157            wr.write_u32::<LittleEndian>(layer.height as u32)?;
158
159            for cell in &layer.cells {
160                wr.write_u32::<LittleEndian>(cell.ch)?;
161                cell.fg.write(&mut wr)?;
162                cell.bg.write(&mut wr)?;
163            }
164        }
165        Ok(())
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use std::fs::File;
173    use std::io::{Cursor, Seek, SeekFrom};
174
175    const WIDTH: usize = 80;
176    const HEIGHT: usize = 60;
177
178    #[test]
179    fn test_roundtrip() {
180        let mut xp = XpFile::new(WIDTH, HEIGHT);
181        for y in 0..HEIGHT {
182            for x in 0..WIDTH {
183                let cell = xp.layers[0].get_mut(x, y).unwrap();
184                cell.ch = (32 + x + y) as u32;
185                cell.fg = XpColor::new(y as u8, 0, 255 - y as u8);
186                cell.bg = XpColor::new(x as u8, 0, 255 - x as u8);
187            }
188        }
189
190        let mut f = Cursor::new(Vec::new());
191        xp.write(&mut f).unwrap();
192        f.seek(SeekFrom::Start(0)).unwrap();
193
194        let xp2 = XpFile::read(&mut f).unwrap();
195        assert_eq!(xp, xp2);
196    }
197
198    #[test]
199    fn test_image() {
200        let mut f = File::open("../bracket-terminal/resources/mltest.xp").unwrap();
201        let xp = XpFile::read(&mut f).unwrap();
202        assert_eq!(xp.version, -1);
203        assert_eq!(xp.layers.len(), 2);
204        assert_eq!(xp.layers[0].width, 8);
205        assert_eq!(xp.layers[0].height, 4);
206        assert_eq!(xp.layers[1].width, 8);
207        assert_eq!(xp.layers[1].height, 4);
208        assert_eq!(xp.layers[1].get(0, 0).unwrap().fg, XpColor::BLACK);
209        assert_eq!(xp.layers[1].get(0, 0).unwrap().bg.is_transparent(), true);
210        assert_eq!(xp.layers[1].get(0, 0).unwrap().ch, 32);
211        assert_eq!(xp.layers[1].get(2, 2).unwrap().ch, 'B' as u32);
212        assert_eq!(xp.layers[0].get(0, 0).unwrap().fg, XpColor::new(0, 0, 255));
213        assert_eq!(xp.layers[0].get(0, 0).unwrap().bg, XpColor::BLACK);
214        assert_eq!(xp.layers[0].get(0, 0).unwrap().ch, 'A' as u32);
215    }
216}