Skip to main content

csp_format/
image.rs

1//! Plain in-memory images and PNG input / output.
2
3use crate::error::{Error, Result};
4
5/// RGBA, 8 bits per channel, straight (not premultiplied) alpha.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct RgbaImage {
8    /// Width in pixels.
9    pub width: u32,
10    /// Height in pixels.
11    pub height: u32,
12    /// `width * height * 4` bytes, rows top to bottom, R G B A.
13    pub pixels: Vec<u8>,
14}
15
16impl RgbaImage {
17    /// A transparent image.
18    pub fn new(width: u32, height: u32) -> Self {
19        RgbaImage { width, height, pixels: vec![0; width as usize * height as usize * 4] }
20    }
21
22    /// Sets every pixel to `pixel` (RGBA).
23    pub fn fill(&mut self, pixel: [u8; 4]) {
24        for chunk in self.pixels.as_chunks_mut::<4>().0.iter_mut() {
25            chunk.copy_from_slice(&pixel);
26        }
27    }
28
29    /// The RGBA pixel at (x, y). Panics outside the image.
30    pub fn pixel(&self, x: u32, y: u32) -> [u8; 4] {
31        let at = (y as usize * self.width as usize + x as usize) * 4;
32        self.pixels[at..at + 4].try_into().expect("4 bytes")
33    }
34
35    /// Sets the RGBA pixel at (x, y). Panics outside the image.
36    pub fn set_pixel(&mut self, x: u32, y: u32, pixel: [u8; 4]) {
37        let at = (y as usize * self.width as usize + x as usize) * 4;
38        self.pixels[at..at + 4].copy_from_slice(&pixel);
39    }
40
41    /// The bytes of row `y`.
42    pub fn row_mut(&mut self, y: u32) -> &mut [u8] {
43        let stride = self.width as usize * 4;
44        &mut self.pixels[y as usize * stride..(y as usize + 1) * stride]
45    }
46
47    /// Bounding box (x0, y0, x1, y1, exclusive ends) of the pixels with alpha.
48    pub fn alpha_bbox(&self) -> Option<(u32, u32, u32, u32)> {
49        let (mut x0, mut y0, mut x1, mut y1) = (u32::MAX, u32::MAX, 0, 0);
50        for y in 0..self.height {
51            for x in 0..self.width {
52                if self.pixel(x, y)[3] != 0 {
53                    x0 = x0.min(x);
54                    y0 = y0.min(y);
55                    x1 = x1.max(x + 1);
56                    y1 = y1.max(y + 1);
57                }
58            }
59        }
60        (x0 != u32::MAX).then_some((x0, y0, x1, y1))
61    }
62
63    /// Shrink to `width`×`height` with an area (box) filter on premultiplied colour.
64    pub fn resize_area(&self, width: u32, height: u32) -> RgbaImage {
65        let mut result = RgbaImage::new(width.max(1), height.max(1));
66        if self.width == 0 || self.height == 0 {
67            return result;
68        }
69        let sx = self.width as f64 / result.width as f64;
70        let sy = self.height as f64 / result.height as f64;
71        for ty in 0..result.height {
72            let y0 = ty as f64 * sy;
73            let y1 = y0 + sy;
74            for tx in 0..result.width {
75                let x0 = tx as f64 * sx;
76                let x1 = x0 + sx;
77                let mut sum = [0f64; 4];
78                let mut area = 0f64;
79                let mut y = y0.floor() as u32;
80                while (y as f64) < y1 && y < self.height {
81                    let wy = (y1.min(y as f64 + 1.0) - y0.max(y as f64)).max(0.0);
82                    let mut x = x0.floor() as u32;
83                    while (x as f64) < x1 && x < self.width {
84                        let wx = (x1.min(x as f64 + 1.0) - x0.max(x as f64)).max(0.0);
85                        let weight = wx * wy;
86                        let pixel = self.pixel(x, y);
87                        let alpha = pixel[3] as f64 / 255.0;
88                        sum[0] += pixel[0] as f64 * alpha * weight;
89                        sum[1] += pixel[1] as f64 * alpha * weight;
90                        sum[2] += pixel[2] as f64 * alpha * weight;
91                        sum[3] += alpha * weight;
92                        area += weight;
93                        x += 1;
94                    }
95                    y += 1;
96                }
97                if area > 0.0 && sum[3] > 0.0 {
98                    let alpha = sum[3] / area;
99                    result.set_pixel(
100                        tx,
101                        ty,
102                        [
103                            (sum[0] / sum[3]).round().clamp(0.0, 255.0) as u8,
104                            (sum[1] / sum[3]).round().clamp(0.0, 255.0) as u8,
105                            (sum[2] / sum[3]).round().clamp(0.0, 255.0) as u8,
106                            (alpha * 255.0).round().clamp(0.0, 255.0) as u8,
107                        ],
108                    );
109                }
110            }
111        }
112        result
113    }
114
115    /// Halve with a 2×2 box on premultiplied colour (width and height rounded down).
116    pub fn halve(&self) -> RgbaImage {
117        let image = self;
118        let (w, h) = ((image.width / 2).max(1), (image.height / 2).max(1));
119        let mut out = RgbaImage::new(w, h);
120        for y in 0..h {
121            for x in 0..w {
122                let mut sum = [0u32; 4];
123                for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
124                    let p = image.pixel((2 * x + dx).min(image.width - 1), (2 * y + dy).min(image.height - 1));
125                    let a = p[3] as u32;
126                    sum[0] += p[0] as u32 * a;
127                    sum[1] += p[1] as u32 * a;
128                    sum[2] += p[2] as u32 * a;
129                    sum[3] += a;
130                }
131                // All colour sums are 0 when the alpha sum is, so `max(1)` only avoids 0 / 0.
132                let total = sum[3].max(1);
133                let pixel = [
134                    ((sum[0] + total / 2) / total) as u8,
135                    ((sum[1] + total / 2) / total) as u8,
136                    ((sum[2] + total / 2) / total) as u8,
137                    ((sum[3] + 2) / 4) as u8,
138                ];
139                out.set_pixel(x, y, pixel);
140            }
141        }
142        out
143    }
144
145    /// Copy `other` onto self with its top-left at (x, y), replacing pixels
146    /// (no blending); parts outside are dropped.
147    pub fn paste(&mut self, other: &RgbaImage, x: i64, y: i64) {
148        for sy in 0..other.height as i64 {
149            let ty = y + sy;
150            if ty < 0 || ty >= self.height as i64 {
151                continue;
152            }
153            for sx in 0..other.width as i64 {
154                let tx = x + sx;
155                if tx < 0 || tx >= self.width as i64 {
156                    continue;
157                }
158                self.set_pixel(tx as u32, ty as u32, other.pixel(sx as u32, sy as u32));
159            }
160        }
161    }
162
163    /// Draw `other` over self (normal blending, straight alpha) with its
164    /// top-left at (x, y); parts outside are dropped.
165    pub fn draw_over(&mut self, other: &RgbaImage, x: i64, y: i64) {
166        for sy in 0..other.height as i64 {
167            let ty = y + sy;
168            if ty < 0 || ty >= self.height as i64 {
169                continue;
170            }
171            for sx in 0..other.width as i64 {
172                let tx = x + sx;
173                if tx < 0 || tx >= self.width as i64 {
174                    continue;
175                }
176                let s = other.pixel(sx as u32, sy as u32);
177                if s[3] == 0 {
178                    continue;
179                }
180                let d = self.pixel(tx as u32, ty as u32);
181                let (sa, da) = (s[3] as f32 / 255.0, d[3] as f32 / 255.0);
182                let a = sa + da * (1.0 - sa);
183                let mix = |c: usize| (s[c] as f32 * sa + d[c] as f32 * da * (1.0 - sa)) / a;
184                let out = [mix(0), mix(1), mix(2), a * 255.0].map(|v| v.round().clamp(0.0, 255.0) as u8);
185                self.set_pixel(tx as u32, ty as u32, out);
186            }
187        }
188    }
189
190    /// Encodes as an 8-bit RGBA PNG.
191    pub fn to_png(&self) -> Result<Vec<u8>> {
192        encode_png(self.width, self.height, png::ColorType::Rgba, &self.pixels)
193    }
194
195    /// Decodes a PNG (any colour type; converted to RGBA).
196    pub fn from_png(data: &[u8]) -> Result<Self> {
197        decode_png(data)
198    }
199}
200
201/// One 8-bit channel (masks).
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct GrayImage {
204    /// Width in pixels.
205    pub width: u32,
206    /// Height in pixels.
207    pub height: u32,
208    /// `width * height` bytes, rows top to bottom.
209    pub pixels: Vec<u8>,
210}
211
212impl GrayImage {
213    /// A black (all 0) image.
214    pub fn new(width: u32, height: u32) -> Self {
215        GrayImage { width, height, pixels: vec![0; width as usize * height as usize] }
216    }
217
218    /// Encodes as an 8-bit grayscale PNG.
219    pub fn to_png(&self) -> Result<Vec<u8>> {
220        encode_png(self.width, self.height, png::ColorType::Grayscale, &self.pixels)
221    }
222}
223
224/// ITU-R BT.601 luma, as used for gray layers.
225pub fn luma(r: u8, g: u8, b: u8) -> u8 {
226    ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114 + 500) / 1000) as u8
227}
228
229fn encode_png(width: u32, height: u32, color: png::ColorType, pixels: &[u8]) -> Result<Vec<u8>> {
230    let mut output = Vec::new();
231    let mut encoder = png::Encoder::new(&mut output, width, height);
232    encoder.set_color(color);
233    encoder.set_depth(png::BitDepth::Eight);
234    encoder.set_compression(png::Compression::Fast);
235    let mut writer = encoder.write_header().map_err(|error| Error::format(error.to_string()))?;
236    writer.write_image_data(pixels).map_err(|error| Error::format(error.to_string()))?;
237    writer.finish().map_err(|error| Error::format(error.to_string()))?;
238    Ok(output)
239}
240
241/// Decode any 8-bit PNG (gray, gray+alpha, RGB, RGBA, palette) to RGBA.
242pub fn decode_png(data: &[u8]) -> Result<RgbaImage> {
243    let mut decoder = png::Decoder::new(std::io::Cursor::new(data));
244    decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
245    let mut reader = decoder.read_info().map_err(|error| Error::format(format!("PNG: {error}")))?;
246    let size = reader.output_buffer_size().ok_or_else(|| Error::format("PNG too large"))?;
247    let mut buffer = vec![0; size];
248    let info = reader.next_frame(&mut buffer).map_err(|error| Error::format(format!("PNG: {error}")))?;
249    let (width, height) = (info.width, info.height);
250    let pixels = &buffer[..info.buffer_size()];
251    let mut image = RgbaImage::new(width, height);
252    let count = width as usize * height as usize;
253    match info.color_type {
254        png::ColorType::Rgba => image.pixels.copy_from_slice(&pixels[..count * 4]),
255        png::ColorType::Rgb => {
256            for (target, source) in image.pixels.as_chunks_mut::<4>().0.iter_mut().zip(pixels.as_chunks::<3>().0.iter())
257            {
258                target.copy_from_slice(&[source[0], source[1], source[2], 255]);
259            }
260        }
261        png::ColorType::GrayscaleAlpha => {
262            for (target, source) in image.pixels.as_chunks_mut::<4>().0.iter_mut().zip(pixels.as_chunks::<2>().0.iter())
263            {
264                target.copy_from_slice(&[source[0], source[0], source[0], source[1]]);
265            }
266        }
267        png::ColorType::Grayscale => {
268            for (target, source) in image.pixels.as_chunks_mut::<4>().0.iter_mut().zip(pixels.iter()) {
269                target.copy_from_slice(&[*source, *source, *source, 255]);
270            }
271        }
272        png::ColorType::Indexed => return Err(Error::format("PNG palette was not expanded")),
273    }
274    Ok(image)
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn png_round_trip() {
283        let mut image = RgbaImage::new(3, 2);
284        image.set_pixel(2, 1, [1, 2, 3, 4]);
285        let decoded = decode_png(&image.to_png().unwrap()).unwrap();
286        assert_eq!(decoded, image);
287    }
288
289    #[test]
290    fn area_resize_averages() {
291        let mut image = RgbaImage::new(2, 2);
292        image.fill([100, 100, 100, 255]);
293        image.set_pixel(0, 0, [200, 200, 200, 255]);
294        let small = image.resize_area(1, 1);
295        assert_eq!(small.pixel(0, 0), [125, 125, 125, 255]);
296    }
297}