pie_format/
lib.rs

1/* PIE - Pixel Indexed Encoding
2   Version 1.0.1
3   
4   Description
5   -----------
6   This lossless image format only optionally stores colors in the file.
7   It is designed to be used in conjunction with a palette from which
8   colours can be sampled by the decoder.
9   
10   Using an external palette reduces uncompressed image size by 75%
11   assuming a four channel format like RGBA, or 60% assuming a 3
12   channel format like RGB without alpha.
13   
14   Using an internal palette will increase the size depending on the
15   palette, but still generally be smaller than other formats like PNG
16   for pixel art.
17   
18   Comparison
19   ----------
20   In the images/ folder you will find randomly selected .png pixel art
21   images from lospec.org as well as converted .pie files. If any of
22   these images are your and you want it removed, please create an issue.
23   
24   a-strawberry-dude-509249.pie    77.00% the size of the png version
25   cubikism-023391.pie             81.00% ..
26   dune-portraits-787893.pie       74.00% ..
27   goblin-slayer-808592.pie        63.00% ..
28   khorne-berserker-509756.pie     50.00% ..
29   snowfighter-844418.pie          64.00% ..
30   
31   Memory Layout
32   -------------
33   ┌─ PIE Image Format ──────────────────────────────────────────────┐
34   │ magic    u8[3] -- Magic bytes "PIE"                             │
35   │ version  u8    -- Version                                       │
36   │ width    u16   -- Width in pixels (BE)                          │
37   │ height   u16   -- Height in pixels (BE)                         │
38   │ flags    u8    -- 0b00000001 is whether the palette is included │
39   │                -- 0b00000010 is whether there is transparency   │
40   │                -- Other bits are reserved for future updates    │
41   │ length   u16   -- Run count of the data section (BE)            │
42   │ data     u8[]  -- Indices into palette (external or internal)   │
43   │ palette? u8[]  -- Optional palette included in the image        │
44   │                -- Stride can be 3 or 4 depending on RGB/RGBA    │
45   └─────────────────────────────────────────────────────────────────┘
46   
47   Data Compression
48   ----------------
49   Given this format is designed for pixel art images, some assumptions
50   are made.
51   
52   1. Palettes generally have 2-64 colours and very rarely exceed 256.
53   2. Horizontal repeating pixels will be common.
54   
55   Therefore: 
56   - A Palette may contain up to 256 colours. Indices into the Palette may
57     therfore be represented by a single byte.
58   - RLE is used for horizontal runs of pixels that have the same index.
59   - The vertical axis is not considered.
60   
61   Runs can be no longer than 255 pixels and they wrap to the next row
62   as a byte array is 1-Dimensional and has no concept of rows.
63   
64   Palette Compression
65   -------------------
66   The palette is not compressed.
67*/
68
69//! A reference implementation for the PIE image format.
70//! This lossless image format only optionally stores colors in the file.
71//! It is designed to be used in conjunction with a palette from which
72//! colours can be sampled by the decoder.
73//! Using an external palette reduces uncompressed image size by 75%
74//! assuming a four channel format like RGBA, or 60% assuming a 3
75//! channel format like RGB without alpha.
76//! Using an internal palette will increase the size depending on the
77//! palette, but still generally be smaller than other formats like PNG
78//! for pixel art or images with limited palettes.
79use std::{fs::{File, self}, io::Read, collections::HashMap};
80
81const FLAG_PALETTE: u8      = 1 << 0;
82const FLAG_TRANSPARENCY: u8 = 1 << 1;
83const HEADER_SIZE: usize = 11;
84
85#[derive(Debug, PartialEq, Clone, Copy)]
86pub enum PixelFormat {
87    RGB, RGBA,
88}
89
90/// Decoded PIE file into pixel data for use in your graphics pipeline.
91#[derive(Debug, PartialEq)]
92pub struct DecodedPIE {
93    pub width: u16,
94    pub height: u16,
95    pub format: PixelFormat,
96    pub pixels: Vec<u8>,
97}
98
99/// A struct encoded with the necessary data for writing. You cannot just dump this struct into a
100/// file. To write - use the [`self::write`] function.
101#[derive(Debug, PartialEq)]
102pub struct EncodedPIE {
103    pub width: u16,
104    pub height: u16,
105    pub indices: Vec<u8>,
106    pub palette: Option<Palette>,
107}
108
109#[derive(Debug, PartialEq)]
110pub enum DecodeError {
111    MissingPalette,
112}
113
114#[derive(Debug, PartialEq)]
115pub enum EncodeError {
116    WrongPixelCount,
117    ColorNotInPalette,
118}
119
120/// Palette for embedding or keeping external. The maximum amount of colours supported is 256.
121#[derive(Debug, PartialEq, Clone)]
122pub struct Palette {
123    pub format: PixelFormat,
124    pub colors: Vec<u8>, // Stride will be 4 for RGBA, 3 for RGB.
125}
126
127/// Encode and write a PIE file to disk.
128/// # Arguments
129/// * `path` - Path to the file.
130/// * `width` - Width in pixels.
131/// * `height` - Height in pixels.
132/// * `embed_palette` - If true, will embed the palette into the file.
133/// * `palette` - Optional palette to be embedded or referred to. If None, a palette will be
134///               generated on the fly and indices will match the auto-generated palette.
135/// * `pixels` - The pixel data in RGB or RGBA byte format.
136/// external palette.
137pub fn write(path: &str, width: u16, height: u16, embed_palette: bool, maybe_palette: Option<&Palette>, pixels: Vec<u8>) -> Result<bool, EncodeError> {
138    let encoded = encode(width, height, &pixels, embed_palette, maybe_palette).expect("Failed to encode data.");
139    let mut flags = 0;
140
141    if encoded.indices.len() / 2 > u16::MAX as usize {
142        return Err(EncodeError::WrongPixelCount);
143    }
144
145    let mut bytes: Vec<u8> = vec!['P' as u8, 'I' as u8, 'E' as u8, 1];
146    bytes.append(&mut width.to_be_bytes().to_vec());
147    bytes.append(&mut height.to_be_bytes().to_vec());
148    bytes.push(0); // Fill with flags later
149    bytes.append(&mut ((encoded.indices.len() / 2) as u16).to_be_bytes().to_vec());
150    bytes.append(&mut encoded.indices.to_vec());
151
152    if embed_palette {
153        flags |= FLAG_PALETTE;
154        bytes.append(&mut encoded.palette.unwrap().colors.to_vec());
155    }
156
157    bytes[8] = flags;
158
159    fs::write(path, &bytes).expect("Failed to write file.");
160    Ok(true)
161}
162
163/// Encode an array of RGB or RGBA bytes into an EncodedPIE.
164/// Note that an EncodedPIE struct is not the same format as a saved .PIE file.
165/// To get the correct format for saving, use the write function.
166pub fn encode(width: u16, height: u16, pixel_bytes: &[u8], embed_palette: bool, maybe_palette: Option<&Palette>) -> Result<EncodedPIE, EncodeError> {
167    let mut encoded = EncodedPIE {
168        width, height,
169        indices: Vec::new(),
170        palette: None
171    };
172
173
174    let mut chunk_size = 4;
175    if pixel_bytes.len() == (width as usize * height as usize * 3) {
176        chunk_size = 3;
177    };
178
179    // If palette is not included, it must be created on the fly.
180    if maybe_palette.is_none() {
181        let mut indices = Vec::new();
182        let mut palette = Palette {
183            format: if chunk_size == 3 { PixelFormat::RGB } else { PixelFormat::RGBA },
184            colors: Vec::new()
185        };
186        let mut map = HashMap::new();
187        let mut index: u8 = 0;
188        for chunk in pixel_bytes.chunks(chunk_size) {
189            if !map.contains_key(chunk) {
190                map.insert(chunk, index);
191                index += 1;
192                palette.colors.append(&mut chunk.to_vec());
193            }
194
195            indices.push(*map.get(chunk).unwrap() as u8);
196        }
197
198        if embed_palette {
199            encoded.palette = Some(palette);
200        }
201        encoded.indices = rle(&indices, 255);
202    } else if let Some(palette) = maybe_palette {
203        let mut indices = Vec::new();
204        let map = palette.colors.chunks(chunk_size).into_iter().enumerate().fold(HashMap::new(), |mut acc, (idx, x)| {
205            acc.insert(x, idx);
206            acc
207        });
208        for chunk in pixel_bytes.chunks(chunk_size) {
209            if !map.contains_key(chunk) {
210                return Err(EncodeError::ColorNotInPalette);
211            }
212
213            indices.push(*map.get(chunk).unwrap() as u8);
214
215            if embed_palette {
216                encoded.palette = Some(palette.to_owned());
217            }
218            encoded.indices = rle(&indices, 255);
219        }
220    }
221
222    Ok(encoded)
223}
224
225/// Encode a series of u8s into runs `(count, value)` with a max of `limit`.
226pub fn rle(data: &[u8], limit: usize) -> Vec<u8> {
227    let mut encoded = Vec::new();
228    let mut i = 0;
229    while i < data.len() {
230        let mut count = 1;
231        while i + count < data.len() && data[i] == data[i + count] && count < limit {
232            count += 1;
233        }
234        encoded.push(count as u8);
235        encoded.push(data[i]);
236        i += count;
237    }
238    encoded
239}
240
241/// Read a PIE file from disk and decode it into a DecodedPIE.
242/// Palette is required if not included in the image.
243/// # Arguments
244/// * `path` - A string slice that is a path to the file on disk.
245/// * `palette` - An optional palette that must be included if the PIE file was saved with an
246/// external palette.
247pub fn read(path: &str, palette: Option<&Palette>) -> Result<DecodedPIE, DecodeError> {
248    let mut file = File::open(path).expect("Could not open file");
249    let mut bytes = Vec::new();
250    file.read_to_end(&mut bytes).expect("Could not read file");
251
252    decode(&bytes, palette)
253}
254
255/// Decode raw bytes from PIE format into a [`DecodedPIE`].
256/// * `bytes` - The raw bytes including header, index data, and optionally palette.
257/// * `palette` - Required if the palette is not embedded in `bytes`.
258pub fn decode(bytes: &[u8], maybe_palette: Option<&Palette>) -> Result<DecodedPIE, DecodeError> {
259    let mut decoded = DecodedPIE {
260        width: 0, height: 0,
261        format: PixelFormat::RGB, pixels: vec![]
262    };
263
264    let mut palette = Palette {
265        format: PixelFormat::RGB,
266        colors: Vec::new(),
267    };
268
269    assert!(bytes[0] == 'P' as u8 && bytes[1] == 'I' as u8 && bytes[2] == 'E' as u8);
270    decoded.width = u16::from_be_bytes([bytes[4], bytes[5]]);
271    decoded.height = u16::from_be_bytes([bytes[6], bytes[7]]);
272    let flags = bytes[8];
273
274    palette.format = PixelFormat::RGB;
275    let mut step = 3;
276
277    if flags & FLAG_TRANSPARENCY > 0 {
278        palette.format = PixelFormat::RGBA;
279        step = 4;
280    }
281
282    let data_length = u16::from_be_bytes([bytes[9], bytes[10]]);
283
284    if flags & FLAG_PALETTE > 0 {
285        for (index, _) in bytes.iter().skip(HEADER_SIZE + (data_length * 2) as usize).enumerate().step_by(step) {
286            let absolute_index = HEADER_SIZE + (data_length * 2) as usize + index - 1;
287            for i in 0..step {
288                palette.colors.push(bytes[absolute_index + step - i]);
289            }
290        }
291    } else if let Some(p) = maybe_palette {
292        palette.format = p.format;
293        palette.colors = p.colors.to_owned();
294    } else {
295        return Err(DecodeError::MissingPalette);
296    }
297
298    for i in (HEADER_SIZE..(HEADER_SIZE + (data_length * 2) as usize)).step_by(2) {
299        let run_length = bytes[i];
300        let color_index = bytes[i + 1] as usize * step;
301
302        for _ in 0..run_length {
303            decoded.pixels.append(&mut vec![palette.colors[color_index + 2], palette.colors[color_index + 1], palette.colors[color_index]]);
304        }
305    }
306
307    decoded.format = palette.format;
308
309    Ok(decoded)
310}
311
312#[test]
313fn test_decode() {
314    let bytes = include_bytes!("../images/test_embedded_palette.pie");
315    let decoded = decode(bytes, None).unwrap();
316    let palette_bytes: [u8; 12] = [
317        0x6A, 0xBE, 0x30,
318        0xFF, 0xFF, 0xFF,
319        0x00, 0x00, 0x00,
320        0x5B, 0x6E, 0xE1,
321    ];
322    let start_pixel: [u8; 3] = [0x6A, 0xBE, 0x30];
323    let end_pixel: [u8; 3] = [0x5B, 0x6E, 0xE1];
324    let decoded_with_palette = decode(bytes, Some(&Palette {
325        format: PixelFormat::RGB,
326        colors: palette_bytes.to_vec(),
327    })).unwrap();
328
329    assert_eq!(start_pixel, decoded.pixels[0..3]);
330    assert_eq!(end_pixel, decoded.pixels[decoded.pixels.len() - 3..]);
331    assert_eq!(decoded.pixels, decoded_with_palette.pixels);
332}
333
334#[test]
335fn test_encode() {
336    let pixels: Vec<u8> = vec![
337        0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
338        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
339        0xFF, 0x00, 0xCC, 0xFF, 0x00, 0xCC, 0xFF, 0x00, 0xCC, 0xFF, 0x00, 0xCC, 0xFF, 0x00, 0xCC, 
340        0xBE, 0xEF, 0x00, 0xBE, 0xEF, 0x00, 0xBE, 0xEF, 0x00, 0xBE, 0xEF, 0x00, 0xFF, 0xFF, 0xFF, 
341    ];
342
343    let palette = Palette {
344        format: PixelFormat::RGB,
345        colors: vec![
346            0xFF, 0xFF, 0xFF,
347            0xFF, 0x00, 0x00,
348            0xBE, 0xEF, 0x00,
349            0xFF, 0x00, 0xCC,
350        ],
351    };
352
353    let encoded = encode(5, 4, &pixels, true, Some(&palette)).unwrap();
354    assert_eq!([5, 1] as [u8; 2], encoded.indices[0..2]);
355    assert_eq!([5, 0] as [u8; 2], encoded.indices[2..4]);
356    assert_eq!([5, 3] as [u8; 2], encoded.indices[4..6]);
357    assert_eq!([4, 2] as [u8; 2], encoded.indices[6..8]);
358    assert_eq!([1, 0] as [u8; 2], encoded.indices[8..10]);
359    assert_eq!(palette.colors, encoded.palette.unwrap().colors);
360
361    let encoded = encode(5, 4, &pixels, false, Some(&palette)).unwrap();
362    assert_eq!([5, 1] as [u8; 2], encoded.indices[0..2]);
363    assert_eq!([5, 0] as [u8; 2], encoded.indices[2..4]);
364    assert_eq!([5, 3] as [u8; 2], encoded.indices[4..6]);
365    assert_eq!([4, 2] as [u8; 2], encoded.indices[6..8]);
366    assert_eq!([1, 0] as [u8; 2], encoded.indices[8..10]);
367    assert!(encoded.palette.is_none());
368
369    let encoded = encode(5, 4, &pixels, true, None).unwrap();
370    assert_eq!([5, 0] as [u8; 2], encoded.indices[0..2]);
371    assert_eq!([5, 1] as [u8; 2], encoded.indices[2..4]);
372    assert_eq!([5, 2] as [u8; 2], encoded.indices[4..6]);
373    assert_eq!([4, 3] as [u8; 2], encoded.indices[6..8]);
374    assert_eq!([1, 1] as [u8; 2], encoded.indices[8..10]);
375    assert_eq!([0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xCC, 0xBE, 0xEF, 0x00] as [u8; 12], encoded.palette.unwrap().colors.as_slice());
376
377    let encoded = encode(5, 4, &pixels, false, None).unwrap();
378    assert_eq!([5, 0] as [u8; 2], encoded.indices[0..2]);
379    assert_eq!([5, 1] as [u8; 2], encoded.indices[2..4]);
380    assert_eq!([5, 2] as [u8; 2], encoded.indices[4..6]);
381    assert_eq!([4, 3] as [u8; 2], encoded.indices[6..8]);
382    assert_eq!([1, 1] as [u8; 2], encoded.indices[8..10]);
383    assert!(encoded.palette.is_none());
384}
385
386#[test]
387fn test_read() {
388    let decoded = read("images/test_embedded_palette.pie", None).unwrap();
389    let palette_bytes: [u8; 12] = [
390        0x6A, 0xBE, 0x30,
391        0xFF, 0xFF, 0xFF,
392        0x00, 0x00, 0x00,
393        0x5B, 0x6E, 0xE1,
394    ];
395    let decoded_with_palette = read("images/test_embedded_palette.pie", Some(&Palette {
396        format: PixelFormat::RGB,
397        colors: palette_bytes.to_vec(),
398    })).unwrap();
399
400    let start_pixel: [u8; 3] = [0x6A, 0xBE, 0x30];
401    let end_pixel: [u8; 3] = [0x5B, 0x6E, 0xE1];
402
403    assert_eq!(start_pixel, decoded.pixels[0..3]);
404    assert_eq!(end_pixel, decoded.pixels[decoded.pixels.len() - 3..]);
405    assert_eq!(decoded.pixels, decoded_with_palette.pixels);
406}
407
408#[test]
409fn test_write() {
410    let pixels: Vec<u8> = vec![
411        0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
412        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
413        0xFF, 0x00, 0xCC, 0xFF, 0x00, 0xCC, 0xFF, 0x00, 0xCC, 0xFF, 0x00, 0xCC, 0xFF, 0x00, 0xCC, 
414        0xBE, 0xEF, 0x00, 0xBE, 0xEF, 0x00, 0xBE, 0xEF, 0x00, 0xBE, 0xEF, 0x00, 0xFF, 0xFF, 0xFF, 
415    ];
416
417    let palette = Palette {
418        format: PixelFormat::RGB,
419        colors: vec![
420            0xFF, 0xFF, 0xFF,
421            0xFF, 0x00, 0x00,
422            0xBE, 0xEF, 0x00,
423            0xFF, 0x00, 0xCC,
424        ],
425    };
426
427    assert!(write("tmp.pie", 5, 4, true, Some(&palette), pixels.to_owned()).is_ok());
428
429    let decoded = read("tmp.pie", Some(&palette)).expect("Could not read");
430    assert_eq!(pixels, decoded.pixels);
431    assert!(fs::remove_file("tmp.pie").is_ok());
432}