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)]
320pub fn encode_stub(header: &ImageHeader, pixels: &[u8], cfg: &EncodeConfig) -> Vec<u8> {
321 let pixel_count = (header.width as usize) * (header.height as usize);
322 match cfg.format {
323 ImageFormat::Bmp => {
324 let expected_rgba = pixel_count * 4;
325 let expected_rgb = pixel_count * 3;
326 if header.pixel_format == PixelFormat::Rgba8 && pixels.len() == expected_rgba {
327 bmp_encode_rgba(header.width, header.height, pixels)
328 } else if pixels.len() == expected_rgb {
329 bmp_encode_rgb(header.width, header.height, pixels)
330 } else {
331 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(_) => vec![
343 0x89u8,
344 0x00,
345 (header.width & 0xFF) as u8,
346 (header.height & 0xFF) as u8,
347 ],
348 }
349 }
350 ImageFormat::Jpeg => {
351 let quality = cfg.quality.clamp(1, 100);
352 match super::image_jpeg::jpeg_encode_rgb(header.width, header.height, pixels, quality) {
353 Ok(encoded) => encoded,
354 Err(_) => vec![
355 0xFFu8,
356 0xD8,
357 (header.width & 0xFF) as u8,
358 (header.height & 0xFF) as u8,
359 ],
360 }
361 }
362 ImageFormat::Gif => {
363 match super::image_gif::gif_encode_rgb(header.width, header.height, pixels) {
364 Ok(encoded) => encoded,
365 Err(_) => vec![
366 0x47u8,
367 0x49,
368 (header.width & 0xFF) as u8,
369 (header.height & 0xFF) as u8,
370 ],
371 }
372 }
373 ImageFormat::Webp => {
374 match super::image_webp::webp_encode_rgb(header.width, header.height, pixels) {
375 Ok(encoded) => encoded,
376 Err(_) => vec![
377 0x52u8,
378 0x49,
379 (header.width & 0xFF) as u8,
380 (header.height & 0xFF) as u8,
381 ],
382 }
383 }
384 ImageFormat::Tiff => {
385 match super::image_tiff::tiff_encode_rgb(header.width, header.height, pixels) {
386 Ok(encoded) => encoded,
387 Err(_) => vec![
388 0x49u8,
389 0x49,
390 (header.width & 0xFF) as u8,
391 (header.height & 0xFF) as u8,
392 ],
393 }
394 }
395 _ => {
396 let fmt_byte = match cfg.format {
397 ImageFormat::Tga => 0x00u8,
398 ImageFormat::Hdr => 0x23u8,
399 _ => 0x00u8,
400 };
401 vec![
402 fmt_byte,
403 0x00,
404 (header.width & 0xFF) as u8,
405 (header.height & 0xFF) as u8,
406 ]
407 }
408 }
409}
410
411#[allow(dead_code)]
415pub fn decode_stub(data: &[u8]) -> Option<DecodeResult> {
416 if data.is_empty() {
417 return None;
418 }
419
420 let fmt = detect_format(data);
421
422 let raw_result: Option<(RawDecodeResult, ImageFormat)> = match fmt {
423 ImageFormat::Bmp => bmp_decode(data).ok().map(|r| (r, ImageFormat::Bmp)),
424 ImageFormat::Png => png_decode(data).ok().map(|r| (r, ImageFormat::Png)),
425 ImageFormat::Jpeg => super::image_jpeg::jpeg_decode(data)
426 .ok()
427 .map(|r| (r, ImageFormat::Jpeg)),
428 ImageFormat::Gif => super::image_gif::gif_decode(data)
429 .ok()
430 .map(|r| (r, ImageFormat::Gif)),
431 ImageFormat::Webp => super::image_webp::webp_decode(data)
432 .ok()
433 .map(|r| (r, ImageFormat::Webp)),
434 ImageFormat::Tiff => super::image_tiff::tiff_decode(data)
435 .ok()
436 .map(|r| (r, ImageFormat::Tiff)),
437 _ => None,
438 };
439
440 if let Some((raw, img_fmt)) = raw_result {
441 let pixel_count = raw.width * raw.height;
442 let channels = raw.pixels.len().checked_div(pixel_count).unwrap_or(3);
443 let pf = if channels == 4 {
444 PixelFormat::Rgba8
445 } else {
446 PixelFormat::Rgb8
447 };
448 let header = ImageHeader {
449 width: raw.width as u32,
450 height: raw.height as u32,
451 format: img_fmt,
452 pixel_format: pf,
453 };
454 return Some(DecodeResult {
455 byte_size: raw.pixels.len(),
456 pixel_count,
457 header,
458 });
459 }
460
461 let header = ImageHeader {
463 width: 1,
464 height: 1,
465 format: ImageFormat::Unknown,
466 pixel_format: PixelFormat::Rgba8,
467 };
468 let pixel_count = (header.width * header.height) as usize;
469 let bpp = pixel_format_bytes_per_pixel(&header.pixel_format) as usize;
470 Some(DecodeResult {
471 byte_size: pixel_count * bpp,
472 pixel_count,
473 header,
474 })
475}
476
477#[allow(dead_code)]
478pub fn image_format_name(fmt: &ImageFormat) -> &'static str {
479 match fmt {
480 ImageFormat::Png => "PNG",
481 ImageFormat::Jpeg => "JPEG",
482 ImageFormat::Bmp => "BMP",
483 ImageFormat::Tga => "TGA",
484 ImageFormat::Hdr => "HDR",
485 ImageFormat::Gif => "GIF",
486 ImageFormat::Webp => "WEBP",
487 ImageFormat::Tiff => "TIFF",
488 ImageFormat::Unknown => "Unknown",
489 }
490}
491
492#[allow(dead_code)]
493pub fn pixel_format_bytes_per_pixel(fmt: &PixelFormat) -> u32 {
494 match fmt {
495 PixelFormat::Rgb8 => 3,
496 PixelFormat::Rgba8 => 4,
497 PixelFormat::Grayscale8 => 1,
498 PixelFormat::Rgba16 => 8,
499 }
500}
501
502#[allow(dead_code)]
503pub fn image_byte_size(header: &ImageHeader) -> usize {
504 let pixels = (header.width as usize) * (header.height as usize);
505 let bpp = pixel_format_bytes_per_pixel(&header.pixel_format) as usize;
506 pixels * bpp
507}
508
509#[allow(dead_code)]
510pub fn image_header_to_json(h: &ImageHeader) -> String {
511 format!(
512 "{{\"width\":{},\"height\":{},\"format\":\"{}\",\"pixel_format\":\"{}\"}}",
513 h.width,
514 h.height,
515 image_format_name(&h.format),
516 pixel_format_name(&h.pixel_format),
517 )
518}
519
520fn pixel_format_name(fmt: &PixelFormat) -> &'static str {
521 match fmt {
522 PixelFormat::Rgb8 => "RGB8",
523 PixelFormat::Rgba8 => "RGBA8",
524 PixelFormat::Grayscale8 => "Grayscale8",
525 PixelFormat::Rgba16 => "RGBA16",
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_default_encode_config() {
535 let cfg = default_encode_config(ImageFormat::Png);
536 assert_eq!(cfg.quality, 90);
537 assert_eq!(cfg.format, ImageFormat::Png);
538 }
539
540 #[test]
541 fn test_encode_stub_nonempty() {
542 let header = ImageHeader {
543 width: 4,
544 height: 4,
545 format: ImageFormat::Png,
546 pixel_format: PixelFormat::Rgba8,
547 };
548 let cfg = default_encode_config(ImageFormat::Png);
549 let pixels = vec![128u8; 4 * 4 * 3];
551 let bytes = encode_stub(&header, &pixels, &cfg);
552 assert!(!bytes.is_empty());
553 }
554
555 #[test]
556 fn test_decode_stub_empty_returns_none() {
557 let result = decode_stub(&[]);
558 assert!(result.is_none());
559 }
560
561 #[test]
562 fn test_decode_stub_nonempty_returns_some() {
563 let result = decode_stub(&[0x89, 0x50]);
564 assert!(result.is_some());
565 let dr = result.expect("should succeed");
566 assert!(dr.pixel_count > 0);
567 assert!(dr.byte_size > 0);
568 }
569
570 #[test]
571 fn test_image_format_name() {
572 assert_eq!(image_format_name(&ImageFormat::Png), "PNG");
573 assert_eq!(image_format_name(&ImageFormat::Jpeg), "JPEG");
574 assert_eq!(image_format_name(&ImageFormat::Bmp), "BMP");
575 assert_eq!(image_format_name(&ImageFormat::Tga), "TGA");
576 assert_eq!(image_format_name(&ImageFormat::Hdr), "HDR");
577 }
578
579 #[test]
580 fn test_pixel_format_bytes_per_pixel() {
581 assert_eq!(pixel_format_bytes_per_pixel(&PixelFormat::Rgb8), 3);
582 assert_eq!(pixel_format_bytes_per_pixel(&PixelFormat::Rgba8), 4);
583 assert_eq!(pixel_format_bytes_per_pixel(&PixelFormat::Grayscale8), 1);
584 assert_eq!(pixel_format_bytes_per_pixel(&PixelFormat::Rgba16), 8);
585 }
586
587 #[test]
588 fn test_image_byte_size() {
589 let header = ImageHeader {
590 width: 2,
591 height: 3,
592 format: ImageFormat::Bmp,
593 pixel_format: PixelFormat::Rgb8,
594 };
595 assert_eq!(image_byte_size(&header), 18); }
597
598 #[test]
599 fn test_image_header_to_json() {
600 let h = ImageHeader {
601 width: 1920,
602 height: 1080,
603 format: ImageFormat::Jpeg,
604 pixel_format: PixelFormat::Rgba8,
605 };
606 let json = image_header_to_json(&h);
607 assert!(json.contains("1920"));
608 assert!(json.contains("JPEG"));
609 assert!(json.contains("RGBA8"));
610 }
611
612 #[test]
613 fn test_encode_different_formats() {
614 let header = ImageHeader {
615 width: 1,
616 height: 1,
617 format: ImageFormat::Tga,
618 pixel_format: PixelFormat::Grayscale8,
619 };
620 let cfg_jpeg = default_encode_config(ImageFormat::Jpeg);
621 let cfg_bmp = default_encode_config(ImageFormat::Bmp);
622 let b1 = encode_stub(&header, &[128], &cfg_jpeg);
623 let b2 = encode_stub(&header, &[128], &cfg_bmp);
624 assert_ne!(b1[0], b2[0]);
625 }
626
627 #[test]
630 fn test_bmp_encode_decode_24bit() {
631 let pixels: Vec<u8> = vec![
632 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0, ];
635 let encoded = bmp_encode_rgb(2, 2, &pixels);
636 assert!(encoded.starts_with(b"BM"));
637 let decoded = bmp_decode(&encoded).expect("BMP decode");
638 assert_eq!(decoded.width, 2);
639 assert_eq!(decoded.height, 2);
640 assert_eq!(&decoded.pixels[..3], &[255, 0, 0]); }
642
643 #[test]
644 fn test_bmp_padding_alignment() {
645 let pixels: Vec<u8> = vec![255, 0, 0, 0, 255, 0, 0, 0, 255];
647 let encoded = bmp_encode_rgb(3, 1, &pixels);
648 let file_size = u32::from_le_bytes(encoded[2..6].try_into().expect("file size slice"));
650 assert_eq!(file_size, 66);
651 }
652
653 #[test]
656 fn test_png_encode_decode_rgb() {
657 let pixels: Vec<u8> = vec![255, 0, 0, 0, 255, 0]; let encoded = png_encode_rgb(2, 1, &pixels).expect("PNG encode");
659 assert_eq!(
660 &encoded[..8],
661 &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
662 );
663 let decoded = png_decode(&encoded).expect("PNG decode");
664 assert_eq!(decoded.width, 2);
665 assert_eq!(decoded.height, 1);
666 }
667
668 #[test]
671 fn test_format_detection_png() {
672 let magic = [0x89u8, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
673 assert_eq!(detect_format(&magic), ImageFormat::Png);
674 }
675
676 #[test]
677 fn test_format_detection_jpeg() {
678 let magic = [0xFFu8, 0xD8, 0xFF, 0xE0];
679 assert_eq!(detect_format(&magic), ImageFormat::Jpeg);
680 }
681
682 #[test]
683 fn test_format_detection_bmp() {
684 assert_eq!(detect_format(b"BM\x00"), ImageFormat::Bmp);
685 }
686
687 #[test]
688 fn test_format_detection_unknown() {
689 assert_eq!(detect_format(b"\x00\x01\x02\x03"), ImageFormat::Unknown);
690 }
691}