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}