1use zune_core::bytestream::ZCursor;
8use zune_core::colorspace::ColorSpace;
9use zune_core::options::EncoderOptions;
10use zune_png::{PngDecoder, PngEncoder};
11
12#[allow(dead_code)]
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ImageFormat {
15 Png,
16 Jpeg,
17 Bmp,
18 Tga,
19 Hdr,
20 Gif,
21 Webp,
22 Tiff,
23 Unknown,
24}
25
26#[allow(dead_code)]
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum PixelFormat {
29 Rgb8,
30 Rgba8,
31 Grayscale8,
32 Rgba16,
33}
34
35#[allow(dead_code)]
36#[derive(Debug, Clone)]
37pub struct ImageHeader {
38 pub width: u32,
39 pub height: u32,
40 pub format: ImageFormat,
41 pub pixel_format: PixelFormat,
42}
43
44#[allow(dead_code)]
45#[derive(Debug, Clone)]
46pub struct EncodeConfig {
47 pub quality: u8,
48 pub format: ImageFormat,
49}
50
51#[derive(Debug, Clone)]
53pub struct RawDecodeResult {
54 pub width: usize,
55 pub height: usize,
56 pub pixels: Vec<u8>,
57}
58
59#[allow(dead_code)]
60#[derive(Debug, Clone)]
61pub struct DecodeResult {
62 pub header: ImageHeader,
63 pub pixel_count: usize,
64 pub byte_size: usize,
65}
66
67#[derive(Debug, thiserror::Error)]
69pub enum ImageError {
70 #[error("Image encoding failed: {0}")]
71 EncodeError(String),
72 #[error("Image decoding failed: {0}")]
73 DecodeError(String),
74 #[error("Invalid or unrecognised magic bytes")]
75 InvalidMagic,
76 #[error("Input data was truncated or too short")]
77 TruncatedInput,
78 #[error("Unsupported compression method in BMP")]
79 UnsupportedCompression,
80}
81
82pub fn detect_format(bytes: &[u8]) -> ImageFormat {
84 if bytes.len() >= 8 && bytes[..8] == [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] {
85 return ImageFormat::Png;
86 }
87 if bytes.len() >= 3 && bytes[..3] == [0xFF, 0xD8, 0xFF] {
88 return ImageFormat::Jpeg;
89 }
90 if bytes.len() >= 2 && &bytes[..2] == b"BM" {
91 return ImageFormat::Bmp;
92 }
93 if bytes.len() >= 6 && (&bytes[..6] == b"GIF87a" || &bytes[..6] == b"GIF89a") {
94 return ImageFormat::Gif;
95 }
96 if bytes.len() >= 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
97 return ImageFormat::Webp;
98 }
99 if bytes.len() >= 4 && (&bytes[..4] == b"II*\x00" || &bytes[..4] == b"MM\x00*") {
100 return ImageFormat::Tiff;
101 }
102 ImageFormat::Unknown
103}
104
105pub fn bmp_encode_rgb(width: u32, height: u32, pixels: &[u8]) -> Vec<u8> {
109 let stride = width as usize * 3;
110 let row_bytes = (stride + 3) & !3; let pixel_data_size = row_bytes * height as usize;
112 let file_size = (54 + pixel_data_size) as u32;
113 let mut buf = Vec::with_capacity(file_size as usize);
114
115 buf.extend_from_slice(b"BM");
117 buf.extend_from_slice(&file_size.to_le_bytes());
118 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&54u32.to_le_bytes()); buf.extend_from_slice(&40u32.to_le_bytes()); buf.extend_from_slice(&(width as i32).to_le_bytes());
124 buf.extend_from_slice(&(height as i32).to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&24u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&(pixel_data_size as u32).to_le_bytes());
129 buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); let padding = row_bytes - stride;
136 for row in (0..height as usize).rev() {
137 let row_src = &pixels[row * stride..(row + 1) * stride];
138 for px in row_src.chunks_exact(3) {
139 buf.push(px[2]); buf.push(px[1]); buf.push(px[0]); }
143 buf.extend(std::iter::repeat_n(0u8, padding));
144 }
145 buf
146}
147
148pub fn bmp_encode_rgba(width: u32, height: u32, pixels: &[u8]) -> Vec<u8> {
152 let stride = width as usize * 4;
153 let row_bytes = stride; let pixel_data_size = row_bytes * height as usize;
155 let file_size = (54 + pixel_data_size) as u32;
156 let mut buf = Vec::with_capacity(file_size as usize);
157
158 buf.extend_from_slice(b"BM");
160 buf.extend_from_slice(&file_size.to_le_bytes());
161 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&54u32.to_le_bytes()); buf.extend_from_slice(&40u32.to_le_bytes()); buf.extend_from_slice(&(width as i32).to_le_bytes());
167 buf.extend_from_slice(&(height as i32).to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&32u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&(pixel_data_size as u32).to_le_bytes());
172 buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); for row in (0..height as usize).rev() {
179 let row_src = &pixels[row * stride..(row + 1) * stride];
180 for px in row_src.chunks_exact(4) {
181 buf.push(px[2]); buf.push(px[1]); buf.push(px[0]); buf.push(px[3]); }
186 }
187 buf
188}
189
190pub fn bmp_decode(bytes: &[u8]) -> Result<RawDecodeResult, ImageError> {
192 if bytes.len() < 54 || &bytes[0..2] != b"BM" {
193 return Err(ImageError::InvalidMagic);
194 }
195 let pixel_data_offset = u32::from_le_bytes(
196 bytes[10..14]
197 .try_into()
198 .map_err(|_| ImageError::TruncatedInput)?,
199 ) as usize;
200 let width = i32::from_le_bytes(
201 bytes[18..22]
202 .try_into()
203 .map_err(|_| ImageError::TruncatedInput)?,
204 );
205 let height = i32::from_le_bytes(
206 bytes[22..26]
207 .try_into()
208 .map_err(|_| ImageError::TruncatedInput)?,
209 );
210 let bpp = u16::from_le_bytes(
211 bytes[28..30]
212 .try_into()
213 .map_err(|_| ImageError::TruncatedInput)?,
214 );
215 let compression = u32::from_le_bytes(
216 bytes[30..34]
217 .try_into()
218 .map_err(|_| ImageError::TruncatedInput)?,
219 );
220
221 if compression != 0 {
222 return Err(ImageError::UnsupportedCompression);
223 }
224
225 let (abs_height, bottom_up) = if height < 0 {
226 (-height as usize, false)
227 } else {
228 (height as usize, true)
229 };
230 let abs_width = width.unsigned_abs() as usize;
231 let channels = (bpp / 8) as usize;
232
233 if channels != 3 && channels != 4 {
234 return Err(ImageError::DecodeError(format!("Unsupported bpp: {}", bpp)));
235 }
236
237 let row_stride_padded = (abs_width * channels + 3) & !3;
238 let expected_pixel_bytes = row_stride_padded * abs_height;
239
240 let pixel_bytes = bytes
241 .get(pixel_data_offset..)
242 .ok_or(ImageError::TruncatedInput)?;
243
244 if pixel_bytes.len() < expected_pixel_bytes {
245 return Err(ImageError::TruncatedInput);
246 }
247
248 let mut pixels = Vec::with_capacity(abs_width * abs_height * channels);
249 for row_idx in 0..abs_height {
250 let src_row = if bottom_up {
251 abs_height - 1 - row_idx
252 } else {
253 row_idx
254 };
255 let row_start = src_row * row_stride_padded;
256 let row_data = &pixel_bytes[row_start..row_start + abs_width * channels];
257 for px in row_data.chunks_exact(channels) {
258 pixels.push(px[2]); pixels.push(px[1]); pixels.push(px[0]); if channels == 4 {
263 pixels.push(px[3]); }
265 }
266 }
267
268 Ok(RawDecodeResult {
269 width: abs_width,
270 height: abs_height,
271 pixels,
272 })
273}
274
275pub fn png_encode_rgb(width: usize, height: usize, pixels: &[u8]) -> Result<Vec<u8>, ImageError> {
277 let opts = EncoderOptions::new(
278 width,
279 height,
280 ColorSpace::RGB,
281 zune_core::bit_depth::BitDepth::Eight,
282 );
283 let mut encoder = PngEncoder::new(pixels, opts);
284 let mut out: Vec<u8> = Vec::new();
285 encoder
286 .encode(&mut out)
287 .map_err(|e| ImageError::EncodeError(format!("{:?}", e)))?;
288 Ok(out)
289}
290
291pub fn png_decode(bytes: &[u8]) -> Result<RawDecodeResult, ImageError> {
293 let mut decoder = PngDecoder::new(ZCursor::new(bytes));
294 let raw_pixels = decoder
295 .decode_raw()
296 .map_err(|e| ImageError::DecodeError(e.to_string()))?;
297 let (width, height) = decoder
298 .dimensions()
299 .ok_or_else(|| ImageError::DecodeError("No dimensions after decode".into()))?;
300 Ok(RawDecodeResult {
301 width,
302 height,
303 pixels: raw_pixels,
304 })
305}
306
307#[allow(dead_code)]
308pub fn default_encode_config(fmt: ImageFormat) -> EncodeConfig {
309 EncodeConfig {
310 quality: 90,
311 format: fmt,
312 }
313}
314
315#[allow(dead_code)]
319pub fn encode_stub(header: &ImageHeader, pixels: &[u8], cfg: &EncodeConfig) -> Vec<u8> {
320 let pixel_count = (header.width as usize) * (header.height as usize);
321 match cfg.format {
322 ImageFormat::Bmp => {
323 let expected_rgba = pixel_count * 4;
324 let expected_rgb = pixel_count * 3;
325 if header.pixel_format == PixelFormat::Rgba8 && pixels.len() == expected_rgba {
326 bmp_encode_rgba(header.width, header.height, pixels)
327 } else if pixels.len() == expected_rgb {
328 bmp_encode_rgb(header.width, header.height, pixels)
329 } else {
330 vec![
332 0x42u8,
333 0x4Du8,
334 (header.width & 0xFF) as u8,
335 (header.height & 0xFF) as u8,
336 ]
337 }
338 }
339 ImageFormat::Png => {
340 match png_encode_rgb(header.width as usize, header.height as usize, pixels) {
341 Ok(encoded) => encoded,
342 Err(_) => {
343 vec![
344 0x89u8,
345 0x00,
346 (header.width & 0xFF) as u8,
347 (header.height & 0xFF) as u8,
348 ]
349 }
350 }
351 }
352 _ => {
353 let fmt_byte = match cfg.format {
354 ImageFormat::Jpeg => 0xFFu8,
355 ImageFormat::Tga => 0x00u8,
356 ImageFormat::Hdr => 0x23u8,
357 ImageFormat::Gif => 0x47u8,
358 ImageFormat::Webp => 0x52u8,
359 ImageFormat::Tiff => 0x49u8,
360 _ => 0x00u8,
361 };
362 vec![
363 fmt_byte,
364 0x00,
365 (header.width & 0xFF) as u8,
366 (header.height & 0xFF) as u8,
367 ]
368 }
369 }
370}
371
372#[allow(dead_code)]
375pub fn decode_stub(data: &[u8]) -> Option<DecodeResult> {
376 if data.is_empty() {
377 return None;
378 }
379
380 let fmt = detect_format(data);
381
382 match fmt {
383 ImageFormat::Bmp => {
384 if let Ok(raw) = bmp_decode(data) {
385 let channels = raw.pixels.len() / (raw.width * raw.height).max(1);
386 let pixel_count = raw.width * raw.height;
387 let pf = if channels == 4 {
388 PixelFormat::Rgba8
389 } else {
390 PixelFormat::Rgb8
391 };
392 let header = ImageHeader {
393 width: raw.width as u32,
394 height: raw.height as u32,
395 format: ImageFormat::Bmp,
396 pixel_format: pf,
397 };
398 return Some(DecodeResult {
399 byte_size: raw.pixels.len(),
400 pixel_count,
401 header,
402 });
403 }
404 }
405 ImageFormat::Png => {
406 if let Ok(raw) = png_decode(data) {
407 let pixel_count = raw.width * raw.height;
408 let channels = raw.pixels.len() / pixel_count.max(1);
409 let pf = if channels == 4 {
410 PixelFormat::Rgba8
411 } else {
412 PixelFormat::Rgb8
413 };
414 let header = ImageHeader {
415 width: raw.width as u32,
416 height: raw.height as u32,
417 format: ImageFormat::Png,
418 pixel_format: pf,
419 };
420 return Some(DecodeResult {
421 byte_size: raw.pixels.len(),
422 pixel_count,
423 header,
424 });
425 }
426 }
427 _ => {}
428 }
429
430 let header = ImageHeader {
432 width: 1,
433 height: 1,
434 format: ImageFormat::Unknown,
435 pixel_format: PixelFormat::Rgba8,
436 };
437 let pixel_count = (header.width * header.height) as usize;
438 let bpp = pixel_format_bytes_per_pixel(&header.pixel_format) as usize;
439 Some(DecodeResult {
440 byte_size: pixel_count * bpp,
441 pixel_count,
442 header,
443 })
444}
445
446#[allow(dead_code)]
447pub fn image_format_name(fmt: &ImageFormat) -> &'static str {
448 match fmt {
449 ImageFormat::Png => "PNG",
450 ImageFormat::Jpeg => "JPEG",
451 ImageFormat::Bmp => "BMP",
452 ImageFormat::Tga => "TGA",
453 ImageFormat::Hdr => "HDR",
454 ImageFormat::Gif => "GIF",
455 ImageFormat::Webp => "WEBP",
456 ImageFormat::Tiff => "TIFF",
457 ImageFormat::Unknown => "Unknown",
458 }
459}
460
461#[allow(dead_code)]
462pub fn pixel_format_bytes_per_pixel(fmt: &PixelFormat) -> u32 {
463 match fmt {
464 PixelFormat::Rgb8 => 3,
465 PixelFormat::Rgba8 => 4,
466 PixelFormat::Grayscale8 => 1,
467 PixelFormat::Rgba16 => 8,
468 }
469}
470
471#[allow(dead_code)]
472pub fn image_byte_size(header: &ImageHeader) -> usize {
473 let pixels = (header.width as usize) * (header.height as usize);
474 let bpp = pixel_format_bytes_per_pixel(&header.pixel_format) as usize;
475 pixels * bpp
476}
477
478#[allow(dead_code)]
479pub fn image_header_to_json(h: &ImageHeader) -> String {
480 format!(
481 "{{\"width\":{},\"height\":{},\"format\":\"{}\",\"pixel_format\":\"{}\"}}",
482 h.width,
483 h.height,
484 image_format_name(&h.format),
485 pixel_format_name(&h.pixel_format),
486 )
487}
488
489fn pixel_format_name(fmt: &PixelFormat) -> &'static str {
490 match fmt {
491 PixelFormat::Rgb8 => "RGB8",
492 PixelFormat::Rgba8 => "RGBA8",
493 PixelFormat::Grayscale8 => "Grayscale8",
494 PixelFormat::Rgba16 => "RGBA16",
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn test_default_encode_config() {
504 let cfg = default_encode_config(ImageFormat::Png);
505 assert_eq!(cfg.quality, 90);
506 assert_eq!(cfg.format, ImageFormat::Png);
507 }
508
509 #[test]
510 fn test_encode_stub_nonempty() {
511 let header = ImageHeader {
512 width: 4,
513 height: 4,
514 format: ImageFormat::Png,
515 pixel_format: PixelFormat::Rgba8,
516 };
517 let cfg = default_encode_config(ImageFormat::Png);
518 let pixels = vec![128u8; 4 * 4 * 3];
520 let bytes = encode_stub(&header, &pixels, &cfg);
521 assert!(!bytes.is_empty());
522 }
523
524 #[test]
525 fn test_decode_stub_empty_returns_none() {
526 let result = decode_stub(&[]);
527 assert!(result.is_none());
528 }
529
530 #[test]
531 fn test_decode_stub_nonempty_returns_some() {
532 let result = decode_stub(&[0x89, 0x50]);
533 assert!(result.is_some());
534 let dr = result.expect("should succeed");
535 assert!(dr.pixel_count > 0);
536 assert!(dr.byte_size > 0);
537 }
538
539 #[test]
540 fn test_image_format_name() {
541 assert_eq!(image_format_name(&ImageFormat::Png), "PNG");
542 assert_eq!(image_format_name(&ImageFormat::Jpeg), "JPEG");
543 assert_eq!(image_format_name(&ImageFormat::Bmp), "BMP");
544 assert_eq!(image_format_name(&ImageFormat::Tga), "TGA");
545 assert_eq!(image_format_name(&ImageFormat::Hdr), "HDR");
546 }
547
548 #[test]
549 fn test_pixel_format_bytes_per_pixel() {
550 assert_eq!(pixel_format_bytes_per_pixel(&PixelFormat::Rgb8), 3);
551 assert_eq!(pixel_format_bytes_per_pixel(&PixelFormat::Rgba8), 4);
552 assert_eq!(pixel_format_bytes_per_pixel(&PixelFormat::Grayscale8), 1);
553 assert_eq!(pixel_format_bytes_per_pixel(&PixelFormat::Rgba16), 8);
554 }
555
556 #[test]
557 fn test_image_byte_size() {
558 let header = ImageHeader {
559 width: 2,
560 height: 3,
561 format: ImageFormat::Bmp,
562 pixel_format: PixelFormat::Rgb8,
563 };
564 assert_eq!(image_byte_size(&header), 18); }
566
567 #[test]
568 fn test_image_header_to_json() {
569 let h = ImageHeader {
570 width: 1920,
571 height: 1080,
572 format: ImageFormat::Jpeg,
573 pixel_format: PixelFormat::Rgba8,
574 };
575 let json = image_header_to_json(&h);
576 assert!(json.contains("1920"));
577 assert!(json.contains("JPEG"));
578 assert!(json.contains("RGBA8"));
579 }
580
581 #[test]
582 fn test_encode_different_formats() {
583 let header = ImageHeader {
584 width: 1,
585 height: 1,
586 format: ImageFormat::Tga,
587 pixel_format: PixelFormat::Grayscale8,
588 };
589 let cfg_jpeg = default_encode_config(ImageFormat::Jpeg);
590 let cfg_bmp = default_encode_config(ImageFormat::Bmp);
591 let b1 = encode_stub(&header, &[128], &cfg_jpeg);
592 let b2 = encode_stub(&header, &[128], &cfg_bmp);
593 assert_ne!(b1[0], b2[0]);
594 }
595
596 #[test]
599 fn test_bmp_encode_decode_24bit() {
600 let pixels: Vec<u8> = vec![
601 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0, ];
604 let encoded = bmp_encode_rgb(2, 2, &pixels);
605 assert!(encoded.starts_with(b"BM"));
606 let decoded = bmp_decode(&encoded).expect("BMP decode");
607 assert_eq!(decoded.width, 2);
608 assert_eq!(decoded.height, 2);
609 assert_eq!(&decoded.pixels[..3], &[255, 0, 0]); }
611
612 #[test]
613 fn test_bmp_padding_alignment() {
614 let pixels: Vec<u8> = vec![255, 0, 0, 0, 255, 0, 0, 0, 255];
616 let encoded = bmp_encode_rgb(3, 1, &pixels);
617 let file_size = u32::from_le_bytes(encoded[2..6].try_into().expect("file size slice"));
619 assert_eq!(file_size, 66);
620 }
621
622 #[test]
625 fn test_png_encode_decode_rgb() {
626 let pixels: Vec<u8> = vec![255, 0, 0, 0, 255, 0]; let encoded = png_encode_rgb(2, 1, &pixels).expect("PNG encode");
628 assert_eq!(
629 &encoded[..8],
630 &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
631 );
632 let decoded = png_decode(&encoded).expect("PNG decode");
633 assert_eq!(decoded.width, 2);
634 assert_eq!(decoded.height, 1);
635 }
636
637 #[test]
640 fn test_format_detection_png() {
641 let magic = [0x89u8, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
642 assert_eq!(detect_format(&magic), ImageFormat::Png);
643 }
644
645 #[test]
646 fn test_format_detection_jpeg() {
647 let magic = [0xFFu8, 0xD8, 0xFF, 0xE0];
648 assert_eq!(detect_format(&magic), ImageFormat::Jpeg);
649 }
650
651 #[test]
652 fn test_format_detection_bmp() {
653 assert_eq!(detect_format(b"BM\x00"), ImageFormat::Bmp);
654 }
655
656 #[test]
657 fn test_format_detection_unknown() {
658 assert_eq!(detect_format(b"\x00\x01\x02\x03"), ImageFormat::Unknown);
659 }
660}