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
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum PixmapError {
36 Overflow {
38 width: u32,
40 height: u32,
42 },
43 TooLarge {
45 width: u32,
47 height: u32,
49 pixels: usize,
51 max: usize,
53 },
54}
55
56impl core::fmt::Display for PixmapError {
57 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58 match self {
59 PixmapError::Overflow { width, height } => {
60 write!(f, "pixmap {width}x{height} overflows the pixel count")
61 }
62 PixmapError::TooLarge {
63 width,
64 height,
65 pixels,
66 max,
67 } => {
68 write!(
69 f,
70 "pixmap {width}x{height} = {pixels} pixels exceeds the limit of {max}"
71 )
72 }
73 }
74 }
75}
76
77impl core::error::Error for PixmapError {}
78
79impl Pixmap {
80 pub const MAX_PIXELS: usize = 64 * 1024 * 1024;
86
87 pub fn try_new(
95 width: u32,
96 height: u32,
97 r: u8,
98 g: u8,
99 b: u8,
100 a: u8,
101 ) -> Result<Self, PixmapError> {
102 let Some(pixel_count) = (width as usize).checked_mul(height as usize) else {
103 return Err(PixmapError::Overflow { width, height });
104 };
105 if pixel_count > Self::MAX_PIXELS {
106 return Err(PixmapError::TooLarge {
107 width,
108 height,
109 pixels: pixel_count,
110 max: Self::MAX_PIXELS,
111 });
112 }
113 if r == g && g == b && b == a {
115 return Ok(Pixmap {
116 width,
117 height,
118 data: vec![r; pixel_count * 4],
119 });
120 }
121 let data = [r, g, b, a].repeat(pixel_count);
124 Ok(Pixmap {
125 width,
126 height,
127 data,
128 })
129 }
130
131 #[deprecated(
138 since = "0.34.0",
139 note = "use `Pixmap::try_new`, which reports an oversized request instead of returning an empty pixmap"
140 )]
141 pub fn new(width: u32, height: u32, r: u8, g: u8, b: u8, a: u8) -> Self {
142 Self::try_new(width, height, r, g, b, a).unwrap_or_default()
143 }
144
145 pub fn try_white(width: u32, height: u32) -> Result<Self, PixmapError> {
151 Self::try_new(width, height, 255, 255, 255, 255)
152 }
153
154 pub fn white(width: u32, height: u32) -> Self {
159 Self::try_white(width, height).unwrap_or_default()
160 }
161
162 #[inline]
165 pub fn set_rgb(&mut self, x: u32, y: u32, r: u8, g: u8, b: u8) {
166 let idx = (y as usize * self.width as usize + x as usize) * 4;
167 if let Some(pixel) = self.data.get_mut(idx..idx + 4) {
168 pixel[0] = r;
169 pixel[1] = g;
170 pixel[2] = b;
171 pixel[3] = 255;
172 }
173 }
174
175 #[inline]
177 pub fn get_pixel(&self, x: u32, y: u32) -> Option<&[u8]> {
178 if x >= self.width || y >= self.height {
179 return None;
180 }
181 let idx = (y as usize * self.width as usize + x as usize) * 4;
182 self.data.get(idx..idx + 4)
183 }
184
185 #[inline]
187 pub fn get_rgb(&self, x: u32, y: u32) -> (u8, u8, u8) {
188 let idx = (y as usize * self.width as usize + x as usize) * 4;
189 if let Some(pixel) = self.data.get(idx..idx + 4) {
190 (pixel[0], pixel[1], pixel[2])
191 } else {
192 (0, 0, 0)
193 }
194 }
195
196 pub fn to_rgb(&self) -> Vec<u8> {
198 let pixel_count = self.data.len() / 4;
199 let mut out = Vec::with_capacity(pixel_count * 3);
200 for chunk in self.data.as_chunks::<4>().0 {
201 out.push(chunk[0]);
202 out.push(chunk[1]);
203 out.push(chunk[2]);
204 }
205 out
206 }
207
208 pub fn to_ppm(&self) -> Vec<u8> {
212 let header = format!("P6\n{} {}\n255\n", self.width, self.height);
213 let pixel_count = self.data.len() / 4;
214 let mut out = Vec::with_capacity(header.len() + pixel_count * 3);
215 out.extend_from_slice(header.as_bytes());
216 for chunk in self.data.as_chunks::<4>().0 {
217 out.push(chunk[0]); out.push(chunk[1]); out.push(chunk[2]); }
221 out
222 }
223
224 pub fn rotate_cw90(&self) -> Self {
226 let (w, h) = (self.width, self.height);
227 let mut dst = vec![0u8; (w * h * 4) as usize];
228 for y in 0..h {
229 for x in 0..w {
230 let src_off = ((y * w + x) * 4) as usize;
231 let dst_x = h - 1 - y;
232 let dst_y = x;
233 let dst_off = ((dst_y * h + dst_x) * 4) as usize;
234 dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
235 }
236 }
237 Pixmap {
238 width: h,
239 height: w,
240 data: dst,
241 }
242 }
243
244 pub fn rotate_180(&self) -> Self {
246 let (w, h) = (self.width, self.height);
247 let mut dst = vec![0u8; (w * h * 4) as usize];
248 for y in 0..h {
249 for x in 0..w {
250 let src_off = ((y * w + x) * 4) as usize;
251 let dst_off = (((h - 1 - y) * w + (w - 1 - x)) * 4) as usize;
252 dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
253 }
254 }
255 Pixmap {
256 width: w,
257 height: h,
258 data: dst,
259 }
260 }
261
262 pub fn rotate_ccw90(&self) -> Self {
264 let (w, h) = (self.width, self.height);
265 let mut dst = vec![0u8; (w * h * 4) as usize];
266 for y in 0..h {
267 for x in 0..w {
268 let src_off = ((y * w + x) * 4) as usize;
269 let dst_x = y;
270 let dst_y = w - 1 - x;
271 let dst_off = ((dst_y * h + dst_x) * 4) as usize;
272 dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
273 }
274 }
275 Pixmap {
276 width: h,
277 height: w,
278 data: dst,
279 }
280 }
281
282 pub fn to_gray8(&self) -> GrayPixmap {
288 let pixel_count = self.data.len() / 4;
289 let mut data = Vec::with_capacity(pixel_count);
290 for chunk in self.data.as_chunks::<4>().0 {
291 let r = chunk[0] as u32;
292 let g = chunk[1] as u32;
293 let b = chunk[2] as u32;
294 let y = (r * 306 + g * 601 + b * 117) >> 10;
296 data.push(y.min(255) as u8);
297 }
298 GrayPixmap {
299 width: self.width,
300 height: self.height,
301 data,
302 }
303 }
304}
305
306#[derive(Debug, Clone, Default, PartialEq, Eq)]
311pub struct GrayPixmap {
312 pub width: u32,
313 pub height: u32,
314 pub data: Vec<u8>,
316}
317
318impl GrayPixmap {
319 #[inline]
321 pub fn get(&self, x: u32, y: u32) -> u8 {
322 self.data[(y as usize * self.width as usize) + x as usize]
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn white_pixmap() {
332 let pm = Pixmap::white(2, 2);
333 assert_eq!(pm.data.len(), 16);
334 for chunk in pm.data.chunks(4) {
335 assert_eq!(chunk, &[255, 255, 255, 255]);
336 }
337 }
338
339 #[test]
340 fn set_get_rgb() {
341 let mut pm = Pixmap::white(3, 3);
342 pm.set_rgb(1, 1, 100, 150, 200);
343 assert_eq!(pm.get_rgb(1, 1), (100, 150, 200));
344 assert_eq!(pm.get_rgb(0, 0), (255, 255, 255));
345 }
346
347 #[test]
348 fn rotate_cw90_swaps_dimensions() {
349 let pm = Pixmap::white(4, 2);
350 let r = pm.rotate_cw90();
351 assert_eq!((r.width, r.height), (2, 4));
352 }
353
354 #[test]
355 fn rotate_180_preserves_dimensions() {
356 let pm = Pixmap::white(4, 2);
357 let r = pm.rotate_180();
358 assert_eq!((r.width, r.height), (4, 2));
359 }
360
361 #[test]
362 fn rotate_ccw90_swaps_dimensions() {
363 let pm = Pixmap::white(4, 2);
364 let r = pm.rotate_ccw90();
365 assert_eq!((r.width, r.height), (2, 4));
366 }
367
368 #[test]
369 fn rotate_cw90_then_ccw90_is_identity() {
370 let mut pm = Pixmap::white(3, 2);
371 pm.set_rgb(0, 0, 255, 0, 0); pm.set_rgb(2, 1, 0, 0, 255); let roundtrip = pm.rotate_cw90().rotate_ccw90();
374 assert_eq!(roundtrip.data, pm.data);
375 assert_eq!((roundtrip.width, roundtrip.height), (pm.width, pm.height));
376 }
377
378 #[test]
379 fn rotate_180_twice_is_identity() {
380 let mut pm = Pixmap::white(3, 2);
381 pm.set_rgb(1, 0, 10, 20, 30);
382 let roundtrip = pm.rotate_180().rotate_180();
383 assert_eq!(roundtrip.data, pm.data);
384 }
385
386 #[test]
387 fn rotate_cw90_moves_top_left_to_top_right() {
388 let mut pm = Pixmap::white(2, 1);
390 pm.set_rgb(0, 0, 255, 0, 0);
391 let r = pm.rotate_cw90();
394 assert_eq!(r.width, 1);
395 assert_eq!(r.height, 2);
396 assert_eq!(r.get_rgb(0, 0), (255, 0, 0));
397 assert_eq!(r.get_rgb(0, 1), (255, 255, 255));
398 }
399
400 #[test]
402 fn as_ref_returns_data_slice() {
403 let pm = Pixmap::white(1, 1);
404 let slice: &[u8] = pm.as_ref();
405 assert_eq!(slice.len(), 4);
406 }
407
408 #[test]
411 fn try_new_largest_request_is_refused() {
412 let err = Pixmap::try_new(u32::MAX, u32::MAX, 0, 0, 0, 0).unwrap_err();
415 #[cfg(target_pointer_width = "32")]
416 assert_eq!(
417 err,
418 PixmapError::Overflow {
419 width: u32::MAX,
420 height: u32::MAX
421 }
422 );
423 #[cfg(not(target_pointer_width = "32"))]
424 assert!(matches!(
425 err,
426 PixmapError::TooLarge {
427 width: u32::MAX,
428 height: u32::MAX,
429 max: Pixmap::MAX_PIXELS,
430 ..
431 }
432 ));
433 }
434
435 #[test]
436 fn overflow_error_names_the_request() {
437 let err = PixmapError::Overflow {
438 width: 70000,
439 height: 70000,
440 };
441 assert_eq!(
442 format!("{err}"),
443 "pixmap 70000x70000 overflows the pixel count"
444 );
445 }
446
447 #[test]
448 fn try_new_exceeds_max_pixels_reports_count() {
449 let err = Pixmap::try_new(10000, 10000, 255, 0, 0, 255).unwrap_err();
451 assert_eq!(
452 err,
453 PixmapError::TooLarge {
454 width: 10000,
455 height: 10000,
456 pixels: 100_000_000,
457 max: Pixmap::MAX_PIXELS
458 }
459 );
460 assert!(format!("{err}").contains("100000000"));
461 }
462
463 #[test]
464 fn try_new_at_max_pixels_allocates() {
465 let pm = Pixmap::try_new(Pixmap::MAX_PIXELS as u32, 1, 1, 2, 3, 4).unwrap();
467 assert_eq!(pm.data.len(), Pixmap::MAX_PIXELS * 4);
468 assert_eq!(&pm.data[..4], &[1, 2, 3, 4]);
469 }
470
471 #[test]
472 #[allow(deprecated)]
473 fn new_returns_empty_on_refusal() {
474 let pm = Pixmap::new(u32::MAX, u32::MAX, 0, 0, 0, 0);
475 assert_eq!((pm.width, pm.height, pm.data.len()), (0, 0, 0));
476 let pm = Pixmap::new(10000, 10000, 255, 0, 0, 255);
477 assert_eq!((pm.width, pm.height, pm.data.len()), (0, 0, 0));
478 }
479
480 #[test]
481 fn try_white_matches_white() {
482 let pm = Pixmap::try_white(3, 2).unwrap();
483 assert_eq!(pm, Pixmap::white(3, 2));
484 assert!(Pixmap::try_white(u32::MAX, u32::MAX).is_err());
485 assert_eq!(Pixmap::white(u32::MAX, u32::MAX).width, 0);
486 }
487
488 #[test]
490 fn get_pixel_out_of_bounds_returns_none() {
491 let pm = Pixmap::white(2, 2);
492 assert!(pm.get_pixel(2, 0).is_none());
493 assert!(pm.get_pixel(0, 2).is_none());
494 }
495
496 #[test]
497 fn get_pixel_in_bounds_returns_some() {
498 let mut pm = Pixmap::white(2, 2);
499 pm.set_rgb(1, 0, 10, 20, 30);
500 let p = pm.get_pixel(1, 0).expect("in bounds");
501 assert_eq!(&p[..3], &[10, 20, 30]);
502 }
503
504 #[test]
506 fn get_rgb_out_of_bounds_returns_zero() {
507 let pm = Pixmap::white(2, 2);
508 assert_eq!(pm.get_rgb(5, 5), (0, 0, 0));
509 }
510
511 #[test]
512 fn to_ppm_format() {
513 let mut pm = Pixmap::white(2, 1);
514 pm.set_rgb(0, 0, 255, 0, 0); pm.set_rgb(1, 0, 0, 0, 255); let ppm = pm.to_ppm();
517 let header = b"P6\n2 1\n255\n";
518 assert_eq!(&ppm[..header.len()], header);
519 assert_eq!(&ppm[header.len()..], &[255, 0, 0, 0, 0, 255]);
520 }
521}