Skip to main content

pfx_load/
image.rs

1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2pub enum ColorSpace {
3    Srgb,
4    Linear,
5}
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum Pixels {
9    Eight(Vec<u8>),
10    Sixteen(Vec<u16>),
11}
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Image {
15    pub width: u32,
16    pub height: u32,
17    pub space: ColorSpace,
18    pub pixels: Pixels,
19}
20
21impl Image {
22    pub fn png(bytes: &[u8]) -> Result<Self, String> {
23        let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
24        decoder.set_transformations(png::Transformations::EXPAND);
25        let mut reader = decoder.read_info().map_err(|e| format!("png: {e}"))?;
26        let space = colour(reader.info());
27        let (width, height) = reader.info().size();
28        if width == 0 || height == 0 {
29            return Err("png: the image is empty".into());
30        }
31        let size = reader
32            .output_buffer_size()
33            .ok_or("png: the image is too large")?;
34        let mut buf = vec![0u8; size];
35        let frame = reader
36            .next_frame(&mut buf)
37            .map_err(|e| format!("png: {e}"))?;
38        let pixels = expand(&frame, &buf)?;
39        Ok(Image {
40            width: frame.width,
41            height: frame.height,
42            space,
43            pixels,
44        })
45    }
46
47    pub fn rgba16(
48        bytes: &[u8],
49        width: u32,
50        height: u32,
51        space: ColorSpace,
52    ) -> Result<Self, String> {
53        if width == 0 || height == 0 {
54            return Err("rgba16: the image is empty".into());
55        }
56        let samples = (width as usize)
57            .checked_mul(height as usize)
58            .and_then(|count| count.checked_mul(4))
59            .ok_or("rgba16: the image is too large")?;
60        let need = samples
61            .checked_mul(2)
62            .ok_or("rgba16: the image is too large")?;
63        if bytes.len() != need {
64            return Err(format!(
65                "rgba16: {width}x{height} needs {need} bytes, got {}",
66                bytes.len()
67            ));
68        }
69        let pixels = bytes
70            .chunks_exact(2)
71            .map(|sample| u16::from_le_bytes([sample[0], sample[1]]))
72            .collect();
73        Ok(Image {
74            width,
75            height,
76            space,
77            pixels: Pixels::Sixteen(pixels),
78        })
79    }
80}
81
82fn colour(info: &png::Info) -> ColorSpace {
83    if info.srgb.is_some() {
84        return ColorSpace::Srgb;
85    }
86    if let Some(gamma) = info.gama_chunk {
87        let value = gamma.into_value();
88        if (value - 1.0).abs() <= 0.01 {
89            return ColorSpace::Linear;
90        }
91        if (value - (1.0 / 2.2)).abs() <= 0.02 {
92            return ColorSpace::Srgb;
93        }
94    }
95    if info.bit_depth == png::BitDepth::Sixteen {
96        ColorSpace::Linear
97    } else {
98        ColorSpace::Srgb
99    }
100}
101
102fn expand(frame: &png::OutputInfo, buf: &[u8]) -> Result<Pixels, String> {
103    if frame.color_type == png::ColorType::Indexed {
104        return Err("png: the palette was not expanded".into());
105    }
106    let channels = frame.color_type.samples();
107    let wide = match frame.bit_depth {
108        png::BitDepth::Eight => false,
109        png::BitDepth::Sixteen => true,
110        depth => return Err(format!("png: bit depth {} is unsupported", depth as u8)),
111    };
112    let sample_bytes = if wide { 2 } else { 1 };
113    let pixel_bytes = channels * sample_bytes;
114    if frame.line_size < frame.width as usize * pixel_bytes {
115        return Err("png: a row is short".into());
116    }
117    let height = frame.height as usize;
118    let width = frame.width as usize;
119    if wide {
120        let mut out = Vec::with_capacity(width * height * 4);
121        for y in 0..height {
122            let row = buf
123                .get(y * frame.line_size..)
124                .ok_or("png: the frame is short")?;
125            for x in 0..width {
126                let pixel = row.get(x * pixel_bytes..).ok_or("png: a row is short")?;
127                let sample =
128                    |index: usize| u16::from_be_bytes([pixel[index * 2], pixel[index * 2 + 1]]);
129                let (r, g, b, a) = match channels {
130                    1 => {
131                        let value = sample(0);
132                        (value, value, value, 65535)
133                    }
134                    2 => {
135                        let value = sample(0);
136                        (value, value, value, sample(1))
137                    }
138                    3 => (sample(0), sample(1), sample(2), 65535),
139                    4 => (sample(0), sample(1), sample(2), sample(3)),
140                    _ => return Err("png: an unexpected channel count".into()),
141                };
142                out.extend([r, g, b, a]);
143            }
144        }
145        Ok(Pixels::Sixteen(out))
146    } else {
147        let mut out = Vec::with_capacity(width * height * 4);
148        for y in 0..height {
149            let row = buf
150                .get(y * frame.line_size..)
151                .ok_or("png: the frame is short")?;
152            for x in 0..width {
153                let pixel = row.get(x * pixel_bytes..).ok_or("png: a row is short")?;
154                let (r, g, b, a) = match channels {
155                    1 => (pixel[0], pixel[0], pixel[0], 255),
156                    2 => (pixel[0], pixel[0], pixel[0], pixel[1]),
157                    3 => (pixel[0], pixel[1], pixel[2], 255),
158                    4 => (pixel[0], pixel[1], pixel[2], pixel[3]),
159                    _ => return Err("png: an unexpected channel count".into()),
160                };
161                out.extend([r, g, b, a]);
162            }
163        }
164        Ok(Pixels::Eight(out))
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    fn swatch8() -> Vec<u8> {
173        vec![
174            255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 16, 32, 64, 128,
175        ]
176    }
177
178    fn swatch16() -> Vec<u16> {
179        vec![
180            65535, 0, 0, 65535, 0, 65535, 0, 65535, 0, 0, 65535, 65535, 256, 512, 1024, 32768,
181        ]
182    }
183
184    fn encode(image: &Image) -> Vec<u8> {
185        let mut bytes = Vec::new();
186        let mut encoder = png::Encoder::new(&mut bytes, image.width, image.height);
187        encoder.set_color(png::ColorType::Rgba);
188        match image.space {
189            ColorSpace::Srgb => {
190                encoder.set_source_srgb(png::SrgbRenderingIntent::Perceptual);
191            }
192            ColorSpace::Linear => {
193                encoder.set_source_gamma(png::ScaledFloat::from_scaled(100_000));
194            }
195        }
196        let data = match &image.pixels {
197            Pixels::Eight(pixels) => {
198                encoder.set_depth(png::BitDepth::Eight);
199                pixels.clone()
200            }
201            Pixels::Sixteen(pixels) => {
202                encoder.set_depth(png::BitDepth::Sixteen);
203                pixels
204                    .iter()
205                    .flat_map(|sample| sample.to_be_bytes())
206                    .collect()
207            }
208        };
209        let mut writer = encoder.write_header().unwrap();
210        writer.write_image_data(&data).unwrap();
211        drop(writer);
212        bytes
213    }
214
215    #[test]
216    fn eight_and_sixteen_bit_pngs_round_trip() {
217        let eight = Image::png(include_bytes!("../tests/data/swatch8.png")).unwrap();
218        assert_eq!(eight.width, 2);
219        assert_eq!(eight.height, 2);
220        assert_eq!(eight.space, ColorSpace::Srgb);
221        assert_eq!(eight.pixels, Pixels::Eight(swatch8()));
222        let again = Image::png(&encode(&eight)).unwrap();
223        assert_eq!(again, eight);
224
225        let sixteen = Image::png(include_bytes!("../tests/data/swatch16.png")).unwrap();
226        assert_eq!(sixteen.space, ColorSpace::Linear);
227        assert_eq!(sixteen.pixels, Pixels::Sixteen(swatch16()));
228        let again = Image::png(&encode(&sixteen)).unwrap();
229        assert_eq!(again, sixteen);
230
231        let mut linear = eight.clone();
232        linear.space = ColorSpace::Linear;
233        let decoded = Image::png(&encode(&linear)).unwrap();
234        assert_eq!(decoded.space, ColorSpace::Linear);
235        assert_eq!(decoded.pixels, eight.pixels);
236
237        let mut srgb = sixteen.clone();
238        srgb.space = ColorSpace::Srgb;
239        let decoded = Image::png(&encode(&srgb)).unwrap();
240        assert_eq!(decoded.space, ColorSpace::Srgb);
241        assert_eq!(decoded.pixels, sixteen.pixels);
242    }
243
244    #[test]
245    fn raw_rgba16_keeps_both_bytes_and_its_stated_space() {
246        let image = Image::rgba16(
247            include_bytes!("../tests/data/swatch.rgba16"),
248            2,
249            2,
250            ColorSpace::Linear,
251        )
252        .unwrap();
253        assert_eq!(image.pixels, Pixels::Sixteen(swatch16()));
254        assert_eq!(image.space, ColorSpace::Linear);
255        assert!(Image::rgba16(&[0, 1], 1, 1, ColorSpace::Linear).is_err());
256    }
257}