mabel_aseprite/
palette.rs

1use crate::{reader::AseReader, AsepriteParseError, Result};
2use nohash::IntMap;
3
4/// The color palette embedded in the file.
5#[derive(Debug)]
6pub struct ColorPalette {
7    //entries: Vec<ColorPaletteEntry>,
8    pub(crate) entries: IntMap<u32, ColorPaletteEntry>,
9}
10
11/// A single entry in a [ColorPalette].
12#[derive(Debug)]
13pub struct ColorPaletteEntry {
14    id: u32,
15    rgba8: [u8; 4],
16    name: Option<String>,
17}
18
19impl ColorPalette {
20    /// Total number of colors in the palette.
21    pub fn num_colors(&self) -> u32 {
22        self.entries.len() as u32
23    }
24
25    /// Look up entry at given index.
26    ///
27    /// The Aseprite file format spec does not guarantee the color indices to
28    /// go from `0..num_colors()` but there doesn't seem to be a way to violate
29    /// this constraint using the Aseprite GUI.
30    pub fn color(&self, index: u32) -> Option<&ColorPaletteEntry> {
31        self.entries.get(&index)
32    }
33
34    pub(crate) fn validate_indexed_pixels(&self, indexed_pixels: &[u8]) -> Result<()> {
35        // TODO: Make way more efficient at least for the common case where
36        // the palette goes from `0..num_colors`. Just search for a value >=
37        // num_colors. Maybe make palette an enum and discover dense format
38        // after parsing.
39        for pixel in indexed_pixels {
40            let color = self.color(*pixel as u32);
41            color.ok_or_else(|| {
42                AsepriteParseError::InvalidInput(format!("Palette index invalid: {}", pixel,))
43            })?;
44        }
45        Ok(())
46    }
47}
48
49impl ColorPaletteEntry {
50    /// The id of this entry is the same as its index in the palette.
51    pub fn id(&self) -> u32 {
52        self.id
53    }
54
55    /// Get the RGBA components as an array. Most color libraries allow you to
56    /// build an instance of their color type from such an array.
57    pub fn raw_rgba8(&self) -> [u8; 4] {
58        self.rgba8
59    }
60
61    /// The red channel of the color.
62    pub fn red(&self) -> u8 {
63        self.rgba8[0]
64    }
65
66    /// The green channel of the color.
67    pub fn green(&self) -> u8 {
68        self.rgba8[1]
69    }
70
71    /// The blue channel of the color.
72    pub fn blue(&self) -> u8 {
73        self.rgba8[2]
74    }
75
76    /// Alpha value of this color (0 = fully transparent, 255 = fully opaque).
77    pub fn alpha(&self) -> u8 {
78        self.rgba8[3]
79    }
80
81    /// The color name. Seems to be usually empty in practice.
82    pub fn name(&self) -> Option<&str> {
83        self.name.as_deref()
84    }
85}
86
87pub(crate) fn parse_chunk(data: &[u8]) -> Result<ColorPalette> {
88    let mut reader = AseReader::new(data);
89
90    let _num_total_entries = reader.dword()?;
91    let first_color_index = reader.dword()?;
92    let last_color_index = reader.dword()?;
93    reader.skip_reserved(8)?;
94
95    if last_color_index < first_color_index {
96        return Err(AsepriteParseError::InvalidInput(format!(
97            "Bad palette color indices: first={} last={}",
98            first_color_index, last_color_index,
99        )));
100    }
101
102    let count = last_color_index - first_color_index + 1;
103    //let mut entries = Vec::with_capacity(count as usize);
104    let mut entries = IntMap::default();
105
106    for id in 0..count {
107        let flags = reader.word()?;
108        let red = reader.byte()?;
109        let green = reader.byte()?;
110        let blue = reader.byte()?;
111        let alpha = reader.byte()?;
112        let name = if flags & 1 == 1 {
113            let s = reader.string()?;
114            Some(s)
115        } else {
116            None
117        };
118        let id = id + first_color_index;
119        entries.insert(
120            id,
121            ColorPaletteEntry {
122                id,
123                rgba8: [red, green, blue, alpha],
124                name,
125            },
126        );
127    }
128
129    Ok(ColorPalette { entries })
130}
131
132// Note: we want to map `0 -> 0` and `63 -> 255` and evenly for the in-between
133// points so we can't simply multiply by 4.
134fn scale_6bit_to_8bit(color: u8) -> Result<u8> {
135    if color >= 64 {
136        return Err(AsepriteParseError::InvalidInput(format!(
137            "6-bit color outside range: {}",
138            color
139        )));
140    }
141
142    // Duplicate the top two bits of the 6-bit input into the lower 2 bits after
143    // multiplying by 4. This "leans" the number towards 0 or towards 255, based
144    // on how close we are to either of them. Examples:
145    //
146    //     000010 -> 00001000 (2  ->   8)  2/63 = 0.032,   8/255 = 0.031
147    //     011111 -> 01111101 (31 -> 125) 31/63 = 0.492, 125/255 = 0.490
148    //     100000 -> 10000010 (32 -> 130) 32/63 = 0.508, 130/255 = 0.510
149    //     111111 -> 11111111 (63 -> 255)
150    //
151    Ok(color << 2 | color >> 4)
152}
153
154pub(crate) fn parse_old_chunk_04(data: &[u8]) -> Result<ColorPalette> {
155    let mut reader = AseReader::new(data);
156
157    let packet_count = reader.word()?;
158
159    let mut entries = IntMap::default();
160    let mut skip = 0;
161
162    for _ in 0..packet_count {
163        skip += reader.byte()? as u32;
164        let mut count = reader.byte()? as u32;
165
166        if count == 0 {
167            count = 256;
168        }
169
170        count += skip;
171        for id in skip..count {
172            let red = reader.byte()?;
173            let green = reader.byte()?;
174            let blue = reader.byte()?;
175            entries.insert(
176                id,
177                ColorPaletteEntry {
178                    id,
179                    rgba8: [red, green, blue, 255],
180                    name: None,
181                },
182            );
183        }
184    }
185
186    Ok(ColorPalette { entries })
187}
188
189pub(crate) fn parse_old_chunk_11(data: &[u8]) -> Result<ColorPalette> {
190    let mut reader = AseReader::new(data);
191
192    let packet_count = reader.word()?;
193
194    let mut entries = IntMap::default();
195    let mut skip = 0;
196
197    for _ in 0..packet_count {
198        skip += reader.byte()? as u32;
199        let mut count = reader.byte()? as u32;
200
201        if count == 0 {
202            count = 256;
203        }
204
205        count += skip;
206        for id in skip..count {
207            let red = scale_6bit_to_8bit(reader.byte()?)?;
208            let green = scale_6bit_to_8bit(reader.byte()?)?;
209            let blue = scale_6bit_to_8bit(reader.byte()?)?;
210            entries.insert(
211                id,
212                ColorPaletteEntry {
213                    id,
214                    rgba8: [red, green, blue, 255],
215                    name: None,
216                },
217            );
218        }
219    }
220
221    Ok(ColorPalette { entries })
222}