1#![cfg_attr(not(feature = "std"), no_std)]
2#![deny(unsafe_code)]
3
4#[cfg(not(feature = "std"))]
5extern crate alloc;
6
7#[cfg(not(feature = "std"))]
8use alloc::{format, vec, vec::Vec};
9#[cfg(feature = "std")]
10use std::{format, vec, vec::Vec};
11
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct Pixmap {
17 pub width: u32,
18 pub height: u32,
19 pub data: Vec<u8>,
21}
22
23impl AsRef<[u8]> for Pixmap {
24 fn as_ref(&self) -> &[u8] {
25 &self.data
26 }
27}
28
29impl Pixmap {
30 const MAX_PIXELS: usize = 64 * 1024 * 1024;
34
35 pub fn new(width: u32, height: u32, r: u8, g: u8, b: u8, a: u8) -> Self {
41 let Some(pixel_count) = (width as usize).checked_mul(height as usize) else {
42 return Self::default();
43 };
44 if pixel_count > Self::MAX_PIXELS {
45 return Self::default();
46 }
47 if r == g && g == b && b == a {
49 return Pixmap {
50 width,
51 height,
52 data: vec![r; pixel_count * 4],
53 };
54 }
55 let data = [r, g, b, a].repeat(pixel_count);
58 Pixmap {
59 width,
60 height,
61 data,
62 }
63 }
64
65 pub fn white(width: u32, height: u32) -> Self {
67 Self::new(width, height, 255, 255, 255, 255)
68 }
69
70 #[inline]
73 pub fn set_rgb(&mut self, x: u32, y: u32, r: u8, g: u8, b: u8) {
74 let idx = (y as usize * self.width as usize + x as usize) * 4;
75 if let Some(pixel) = self.data.get_mut(idx..idx + 4) {
76 pixel[0] = r;
77 pixel[1] = g;
78 pixel[2] = b;
79 pixel[3] = 255;
80 }
81 }
82
83 #[inline]
85 pub fn get_pixel(&self, x: u32, y: u32) -> Option<&[u8]> {
86 if x >= self.width || y >= self.height {
87 return None;
88 }
89 let idx = (y as usize * self.width as usize + x as usize) * 4;
90 self.data.get(idx..idx + 4)
91 }
92
93 #[inline]
95 pub fn get_rgb(&self, x: u32, y: u32) -> (u8, u8, u8) {
96 let idx = (y as usize * self.width as usize + x as usize) * 4;
97 if let Some(pixel) = self.data.get(idx..idx + 4) {
98 (pixel[0], pixel[1], pixel[2])
99 } else {
100 (0, 0, 0)
101 }
102 }
103
104 pub fn to_rgb(&self) -> Vec<u8> {
106 let pixel_count = self.data.len() / 4;
107 let mut out = Vec::with_capacity(pixel_count * 3);
108 for chunk in self.data.chunks_exact(4) {
109 out.push(chunk[0]);
110 out.push(chunk[1]);
111 out.push(chunk[2]);
112 }
113 out
114 }
115
116 pub fn to_ppm(&self) -> Vec<u8> {
120 let header = format!("P6\n{} {}\n255\n", self.width, self.height);
121 let pixel_count = self.data.len() / 4;
122 let mut out = Vec::with_capacity(header.len() + pixel_count * 3);
123 out.extend_from_slice(header.as_bytes());
124 for chunk in self.data.chunks_exact(4) {
125 out.push(chunk[0]); out.push(chunk[1]); out.push(chunk[2]); }
129 out
130 }
131
132 pub fn rotate_cw90(&self) -> Self {
134 let (w, h) = (self.width, self.height);
135 let mut dst = vec![0u8; (w * h * 4) as usize];
136 for y in 0..h {
137 for x in 0..w {
138 let src_off = ((y * w + x) * 4) as usize;
139 let dst_x = h - 1 - y;
140 let dst_y = x;
141 let dst_off = ((dst_y * h + dst_x) * 4) as usize;
142 dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
143 }
144 }
145 Pixmap {
146 width: h,
147 height: w,
148 data: dst,
149 }
150 }
151
152 pub fn rotate_180(&self) -> Self {
154 let (w, h) = (self.width, self.height);
155 let mut dst = vec![0u8; (w * h * 4) as usize];
156 for y in 0..h {
157 for x in 0..w {
158 let src_off = ((y * w + x) * 4) as usize;
159 let dst_off = (((h - 1 - y) * w + (w - 1 - x)) * 4) as usize;
160 dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
161 }
162 }
163 Pixmap {
164 width: w,
165 height: h,
166 data: dst,
167 }
168 }
169
170 pub fn rotate_ccw90(&self) -> Self {
172 let (w, h) = (self.width, self.height);
173 let mut dst = vec![0u8; (w * h * 4) as usize];
174 for y in 0..h {
175 for x in 0..w {
176 let src_off = ((y * w + x) * 4) as usize;
177 let dst_x = y;
178 let dst_y = w - 1 - x;
179 let dst_off = ((dst_y * h + dst_x) * 4) as usize;
180 dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
181 }
182 }
183 Pixmap {
184 width: h,
185 height: w,
186 data: dst,
187 }
188 }
189
190 pub fn to_gray8(&self) -> GrayPixmap {
196 let pixel_count = self.data.len() / 4;
197 let mut data = Vec::with_capacity(pixel_count);
198 for chunk in self.data.chunks_exact(4) {
199 let r = chunk[0] as u32;
200 let g = chunk[1] as u32;
201 let b = chunk[2] as u32;
202 let y = (r * 306 + g * 601 + b * 117) >> 10;
204 data.push(y.min(255) as u8);
205 }
206 GrayPixmap {
207 width: self.width,
208 height: self.height,
209 data,
210 }
211 }
212}
213
214#[derive(Debug, Clone, Default, PartialEq, Eq)]
219pub struct GrayPixmap {
220 pub width: u32,
221 pub height: u32,
222 pub data: Vec<u8>,
224}
225
226impl GrayPixmap {
227 #[inline]
229 pub fn get(&self, x: u32, y: u32) -> u8 {
230 self.data[(y as usize * self.width as usize) + x as usize]
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn white_pixmap() {
240 let pm = Pixmap::white(2, 2);
241 assert_eq!(pm.data.len(), 16);
242 for chunk in pm.data.chunks(4) {
243 assert_eq!(chunk, &[255, 255, 255, 255]);
244 }
245 }
246
247 #[test]
248 fn set_get_rgb() {
249 let mut pm = Pixmap::white(3, 3);
250 pm.set_rgb(1, 1, 100, 150, 200);
251 assert_eq!(pm.get_rgb(1, 1), (100, 150, 200));
252 assert_eq!(pm.get_rgb(0, 0), (255, 255, 255));
253 }
254
255 #[test]
256 fn rotate_cw90_swaps_dimensions() {
257 let pm = Pixmap::white(4, 2);
258 let r = pm.rotate_cw90();
259 assert_eq!((r.width, r.height), (2, 4));
260 }
261
262 #[test]
263 fn rotate_180_preserves_dimensions() {
264 let pm = Pixmap::white(4, 2);
265 let r = pm.rotate_180();
266 assert_eq!((r.width, r.height), (4, 2));
267 }
268
269 #[test]
270 fn rotate_ccw90_swaps_dimensions() {
271 let pm = Pixmap::white(4, 2);
272 let r = pm.rotate_ccw90();
273 assert_eq!((r.width, r.height), (2, 4));
274 }
275
276 #[test]
277 fn rotate_cw90_then_ccw90_is_identity() {
278 let mut pm = Pixmap::white(3, 2);
279 pm.set_rgb(0, 0, 255, 0, 0); pm.set_rgb(2, 1, 0, 0, 255); let roundtrip = pm.rotate_cw90().rotate_ccw90();
282 assert_eq!(roundtrip.data, pm.data);
283 assert_eq!((roundtrip.width, roundtrip.height), (pm.width, pm.height));
284 }
285
286 #[test]
287 fn rotate_180_twice_is_identity() {
288 let mut pm = Pixmap::white(3, 2);
289 pm.set_rgb(1, 0, 10, 20, 30);
290 let roundtrip = pm.rotate_180().rotate_180();
291 assert_eq!(roundtrip.data, pm.data);
292 }
293
294 #[test]
295 fn rotate_cw90_moves_top_left_to_top_right() {
296 let mut pm = Pixmap::white(2, 1);
298 pm.set_rgb(0, 0, 255, 0, 0);
299 let r = pm.rotate_cw90();
302 assert_eq!(r.width, 1);
303 assert_eq!(r.height, 2);
304 assert_eq!(r.get_rgb(0, 0), (255, 0, 0));
305 assert_eq!(r.get_rgb(0, 1), (255, 255, 255));
306 }
307
308 #[test]
310 fn as_ref_returns_data_slice() {
311 let pm = Pixmap::white(1, 1);
312 let slice: &[u8] = pm.as_ref();
313 assert_eq!(slice.len(), 4);
314 }
315
316 #[test]
318 fn new_overflow_returns_empty() {
319 let pm = Pixmap::new(u32::MAX, u32::MAX, 0, 0, 0, 0);
320 assert_eq!(pm.width, 0);
321 assert_eq!(pm.height, 0);
322 }
323
324 #[test]
325 fn new_exceeds_max_pixels_returns_empty() {
326 let pm = Pixmap::new(10000, 10000, 255, 0, 0, 255);
328 assert_eq!(pm.width, 0);
329 assert_eq!(pm.height, 0);
330 }
331
332 #[test]
334 fn get_pixel_out_of_bounds_returns_none() {
335 let pm = Pixmap::white(2, 2);
336 assert!(pm.get_pixel(2, 0).is_none());
337 assert!(pm.get_pixel(0, 2).is_none());
338 }
339
340 #[test]
341 fn get_pixel_in_bounds_returns_some() {
342 let mut pm = Pixmap::white(2, 2);
343 pm.set_rgb(1, 0, 10, 20, 30);
344 let p = pm.get_pixel(1, 0).expect("in bounds");
345 assert_eq!(&p[..3], &[10, 20, 30]);
346 }
347
348 #[test]
350 fn get_rgb_out_of_bounds_returns_zero() {
351 let pm = Pixmap::white(2, 2);
352 assert_eq!(pm.get_rgb(5, 5), (0, 0, 0));
353 }
354
355 #[test]
356 fn to_ppm_format() {
357 let mut pm = Pixmap::white(2, 1);
358 pm.set_rgb(0, 0, 255, 0, 0); pm.set_rgb(1, 0, 0, 0, 255); let ppm = pm.to_ppm();
361 let header = b"P6\n2 1\n255\n";
362 assert_eq!(&ppm[..header.len()], header);
363 assert_eq!(&ppm[header.len()..], &[255, 0, 0, 0, 0, 255]);
364 }
365}