1#[cfg(any_standard_decode)]
7use std::io::Cursor;
8
9#[cfg(feature = "zune-runtime")]
10use zune_core::bytestream::ZCursor;
11#[cfg(feature = "zune-runtime")]
12use zune_core::colorspace::ColorSpace;
13#[cfg(feature = "zune-runtime")]
14use zune_core::options::DecoderOptions;
15#[cfg(feature = "zune-runtime")]
16use zune_core::result::DecodingResult;
17
18use crate::core::CodecId;
19use crate::core::image::{RgbImage, Size};
20use crate::error::{FormatError, RawError, RawResult};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub enum StandardFormat {
26 Gif,
28 Jpeg,
30 Png,
32 WebP,
34 Jxl,
36 Tiff,
38 Avif,
40 Heic,
42 Svg,
44 Apv,
46 Ppm,
48}
49
50impl StandardFormat {
51 pub fn name(self) -> &'static str {
53 match self {
54 StandardFormat::Gif => "GIF",
55 StandardFormat::Jpeg => "JPEG",
56 StandardFormat::Png => "PNG",
57 StandardFormat::WebP => "WebP",
58 StandardFormat::Jxl => "JXL",
59 StandardFormat::Tiff => "TIFF",
60 StandardFormat::Avif => "AVIF",
61 StandardFormat::Heic => "HEIC",
62 StandardFormat::Svg => "SVG",
63 StandardFormat::Apv => "APV",
64 StandardFormat::Ppm => "PPM",
65 }
66 }
67
68 pub fn extension(self) -> &'static str {
70 match self {
71 StandardFormat::Gif => "gif",
72 StandardFormat::Jpeg => "jpg",
73 StandardFormat::Png => "png",
74 StandardFormat::WebP => "webp",
75 StandardFormat::Jxl => "jxl",
76 StandardFormat::Tiff => "tiff",
77 StandardFormat::Avif => "avif",
78 StandardFormat::Heic => "heic",
79 StandardFormat::Svg => "svg",
80 StandardFormat::Apv => "apv",
81 StandardFormat::Ppm => "ppm",
82 }
83 }
84
85 pub fn from_extension(ext: &str) -> Option<StandardFormat> {
91 match ext.to_ascii_lowercase().as_str() {
92 "gif" => Some(StandardFormat::Gif),
93 "jpg" | "jpeg" | "jpe" | "jfif" => Some(StandardFormat::Jpeg),
94 "png" => Some(StandardFormat::Png),
95 "webp" => Some(StandardFormat::WebP),
96 "jxl" => Some(StandardFormat::Jxl),
97 "tiff" | "tif" => Some(StandardFormat::Tiff),
98 "avif" => Some(StandardFormat::Avif),
99 "heic" | "heif" => Some(StandardFormat::Heic),
100 "svg" | "svgz" => Some(StandardFormat::Svg),
101 "apv" => Some(StandardFormat::Apv),
102 "ppm" | "pgm" | "pnm" | "pam" | "pfm" => Some(StandardFormat::Ppm),
103 _ => None,
104 }
105 }
106
107 pub fn mime_type(self) -> &'static str {
109 match self {
110 StandardFormat::Gif => "image/gif",
111 StandardFormat::Jpeg => "image/jpeg",
112 StandardFormat::Png => "image/png",
113 StandardFormat::WebP => "image/webp",
114 StandardFormat::Jxl => "image/jxl",
115 StandardFormat::Tiff => "image/tiff",
116 StandardFormat::Avif => "image/avif",
117 StandardFormat::Heic => "image/heic",
118 StandardFormat::Svg => "image/svg+xml",
119 StandardFormat::Apv => "video/apv",
120 StandardFormat::Ppm => "image/x-portable-pixmap",
121 }
122 }
123
124 pub fn supports_decode(self) -> bool {
126 match self {
127 #[cfg(feature = "gif-decode")]
128 StandardFormat::Gif => true,
129 #[cfg(feature = "jpeg-decode")]
130 StandardFormat::Jpeg => true,
131 #[cfg(feature = "png-decode")]
132 StandardFormat::Png => true,
133 #[cfg(feature = "webp-decode")]
134 StandardFormat::WebP => true,
135 #[cfg(feature = "jxl-decode")]
136 StandardFormat::Jxl => true,
137 #[cfg(feature = "tiff-decode")]
138 StandardFormat::Tiff => true,
139 #[cfg(feature = "avif-decode")]
140 StandardFormat::Avif => true,
141 #[cfg(feature = "svg-decode")]
142 StandardFormat::Svg => true,
143 #[cfg(feature = "heic-decode")]
144 StandardFormat::Heic => true,
145 #[cfg(feature = "ppm-decode")]
146 StandardFormat::Ppm => true,
147 _ => false,
148 }
149 }
150
151 pub fn supports_encode(self) -> bool {
153 match self {
154 #[cfg(feature = "png-encode")]
155 StandardFormat::Png => true,
156 #[cfg(feature = "jpeg-encode")]
157 StandardFormat::Jpeg => true,
158 #[cfg(feature = "webp-encode")]
159 StandardFormat::WebP => true,
160 #[cfg(feature = "avif-encode")]
161 StandardFormat::Avif => true,
162 #[cfg(feature = "jxl-encode")]
163 StandardFormat::Jxl => true,
164 _ => false,
165 }
166 }
167}
168
169impl std::fmt::Display for StandardFormat {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.write_str(self.name())
172 }
173}
174
175pub fn detect_standard_format(data: &[u8]) -> Option<StandardFormat> {
183 if data.len() < 8 {
184 return None;
185 }
186 match data {
187 d if d.len() >= 6 && (&d[0..6] == b"GIF87a" || &d[0..6] == b"GIF89a") => {
189 Some(StandardFormat::Gif)
190 }
191 d if d[0] == 0xFF && d[1] == 0xD8 && d[2] == 0xFF => Some(StandardFormat::Jpeg),
193 d if &d[0..8] == b"\x89PNG\r\n\x1a\n" => Some(StandardFormat::Png),
195 d if d.len() >= 12 && &d[0..4] == b"RIFF" && &d[8..12] == b"WEBP" => {
197 Some(StandardFormat::WebP)
198 }
199 d if d[0] == 0xFF && d[1] == 0x0A => Some(StandardFormat::Jxl),
201 d if d.len() >= 12 && &d[4..8] == b"JXL " => Some(StandardFormat::Jxl),
203 d if (d[0] == 0x49 && d[1] == 0x49 && d[2] == 0x2A && d[3] == 0x00)
205 || (d[0] == 0x4D && d[1] == 0x4D && d[2] == 0x00 && d[3] == 0x2A) =>
206 {
207 Some(StandardFormat::Tiff)
208 }
209 d if d.len() >= 12
211 && &d[4..8] == b"ftyp"
212 && (&d[8..12] == b"avif" || &d[8..12] == b"avis" || &d[8..12] == b"mif1") =>
213 {
214 Some(StandardFormat::Avif)
215 }
216 #[cfg(feature = "heic-decode")]
218 d if d.len() >= 12
219 && &d[4..8] == b"ftyp"
220 && (&d[8..12] == b"heic"
221 || &d[8..12] == b"heis"
222 || &d[8..12] == b"hevc"
223 || &d[8..12] == b"hevx") =>
224 {
225 Some(StandardFormat::Heic)
226 }
227 d if d.len() >= 12
229 && &d[4..8] == b"ftyp"
230 && (&d[8..12] == b"apv1" || &d[8..12] == b"apvx") =>
231 {
232 Some(StandardFormat::Apv)
233 }
234 d if d[0] == b'P'
238 && matches!(d[1], b'5' | b'6' | b'7' | b'F' | b'f')
239 && d[2].is_ascii_whitespace() =>
240 {
241 Some(StandardFormat::Ppm)
242 }
243 d if d.len() >= 4
245 && (&d[0..4] == b"<?xm" || &d[0..4] == b"<svg" || &d[0..4] == b"<!--") =>
246 {
247 if d.windows(4).any(|w| w == b"<svg") {
248 Some(StandardFormat::Svg)
249 } else {
250 None
251 }
252 }
253 _ => None,
254 }
255}
256
257#[inline(always)]
262#[cfg_attr(not(any_standard_decode), allow(dead_code))]
263fn u8_to_u16(v: u8) -> u16 {
264 (v as u16) * 257
265}
266
267#[cfg(feature = "gif-decode")]
270fn decode_gif(data: &[u8]) -> RawResult<RgbImage> {
271 use gif::{ColorOutput, DecodeOptions};
272
273 let mut opts = DecodeOptions::new();
274 opts.set_color_output(ColorOutput::RGBA);
275 let mut decoder = opts.read_info(Cursor::new(data)).map_err(|e| {
276 RawError::Format(FormatError::ImageDecode {
277 format: "GIF",
278 message: e.to_string(),
279 })
280 })?;
281
282 let canvas_width = decoder.width() as u32;
283 let canvas_height = decoder.height() as u32;
284
285 let frame = decoder
286 .read_next_frame()
287 .map_err(|e| {
288 RawError::Format(FormatError::ImageDecode {
289 format: "GIF",
290 message: e.to_string(),
291 })
292 })?
293 .ok_or_else(|| {
294 RawError::Format(FormatError::ImageDecode {
295 format: "GIF",
296 message: "no frames in GIF".to_string(),
297 })
298 })?;
299
300 let frame_width = frame.width as usize;
301 let frame_height = frame.height as usize;
302 let frame_left = frame.left as usize;
303 let frame_top = frame.top as usize;
304
305 let mut out = vec![0u16; (canvas_width as usize) * (canvas_height as usize) * 3];
307
308 let buf = &frame.buffer[..];
309 let expected_rgba = frame_width * frame_height * 4;
311 if buf.len() < expected_rgba {
312 return Err(RawError::Format(FormatError::ImageDecode {
313 format: "GIF",
314 message: format!(
315 "frame buffer too small: got {} bytes, expected {} ({}x{}x4)",
316 buf.len(),
317 expected_rgba,
318 frame_width,
319 frame_height,
320 ),
321 }));
322 }
323
324 for row in 0..frame_height {
325 let canvas_y = frame_top + row;
326 if canvas_y >= canvas_height as usize {
327 break;
328 }
329 for col in 0..frame_width {
330 let canvas_x = frame_left + col;
331 if canvas_x >= canvas_width as usize {
332 continue;
333 }
334 let src = (row * frame_width + col) * 4;
335 let dst = (canvas_y * canvas_width as usize + canvas_x) * 3;
336 out[dst] = u8_to_u16(buf[src]);
337 out[dst + 1] = u8_to_u16(buf[src + 1]);
338 out[dst + 2] = u8_to_u16(buf[src + 2]);
339 }
341 }
342
343 Ok(RgbImage::new(canvas_width, canvas_height, out))
344}
345
346#[cfg(feature = "jpeg-decode")]
349fn decode_jpeg(data: &[u8], cfg: &ZuneJpegDecodeConfig) -> RawResult<RgbImage> {
350 let mut opts = DecoderOptions::default()
351 .jpeg_set_out_colorspace(ColorSpace::RGB)
352 .set_strict_mode(cfg.strict);
353 if let Some(w) = cfg.max_width {
354 opts = opts.set_max_width(w);
355 }
356 if let Some(h) = cfg.max_height {
357 opts = opts.set_max_height(h);
358 }
359 let cursor = ZCursor::new(data);
360 let mut decoder = zune_jpeg::JpegDecoder::new_with_options(cursor, opts);
361
362 let pixels = decoder.decode().map_err(|e| {
363 RawError::Format(FormatError::ImageDecode {
364 format: "JPEG",
365 message: format!("{e:?}"),
366 })
367 })?;
368
369 let (w, h) = decoder
370 .dimensions()
371 .map(|(w, h)| (w as u32, h as u32))
372 .ok_or_else(|| {
373 RawError::Format(FormatError::ImageDecode {
374 format: "JPEG",
375 message: "could not read image dimensions after decode".to_string(),
376 })
377 })?;
378
379 let data_u16: Vec<u16> = pixels.iter().map(|&v| u8_to_u16(v)).collect();
381
382 Ok(RgbImage::new(w, h, data_u16))
383}
384
385#[cfg(feature = "png-decode")]
388fn decode_png(data: &[u8], cfg: &ZunePngDecodeConfig) -> RawResult<RgbImage> {
389 let mut opts = DecoderOptions::default()
392 .png_set_strip_to_8bit(false)
393 .set_strict_mode(cfg.strict)
394 .png_set_confirm_crc(cfg.confirm_crc);
395 if let Some(w) = cfg.max_width {
396 opts = opts.set_max_width(w);
397 }
398 if let Some(h) = cfg.max_height {
399 opts = opts.set_max_height(h);
400 }
401 let cursor = ZCursor::new(data);
402 let mut decoder = zune_png::PngDecoder::new_with_options(cursor, opts);
403
404 let result = decoder.decode().map_err(|e| {
405 RawError::Format(FormatError::ImageDecode {
406 format: "PNG",
407 message: format!("{e:?}"),
408 })
409 })?;
410
411 let info = decoder.info().ok_or_else(|| {
412 RawError::Format(FormatError::ImageDecode {
413 format: "PNG",
414 message: "could not read PNG info after decode".to_string(),
415 })
416 })?;
417
418 let w = info.width as u32;
419 let h = info.height as u32;
420
421 let colorspace = decoder.colorspace().unwrap_or(ColorSpace::RGB);
423 let n = colorspace.num_components();
424
425 let samples_u16: Vec<u16> = match result {
427 DecodingResult::U8(px) => px.iter().map(|&v| u8_to_u16(v)).collect(),
428 DecodingResult::U16(px) => px,
429 _ => {
430 return Err(RawError::Format(FormatError::ImageDecode {
431 format: "PNG",
432 message: "unexpected pixel depth in decoded result".to_string(),
433 }));
434 }
435 };
436
437 let data_u16: Vec<u16> = match colorspace {
439 ColorSpace::RGB => samples_u16,
440 ColorSpace::RGBA => {
441 samples_u16
443 .chunks_exact(n)
444 .flat_map(|px| [px[0], px[1], px[2]])
445 .collect()
446 }
447 ColorSpace::Luma => {
448 samples_u16.iter().flat_map(|&v| [v, v, v]).collect()
450 }
451 ColorSpace::LumaA => {
452 samples_u16
454 .chunks_exact(n)
455 .flat_map(|px| [px[0], px[0], px[0]])
456 .collect()
457 }
458 _ => {
459 return Err(RawError::Format(FormatError::ImageDecode {
460 format: "PNG",
461 message: format!("unsupported PNG colorspace: {colorspace:?}"),
462 }));
463 }
464 };
465
466 Ok(RgbImage::new(w, h, data_u16))
467}
468
469#[cfg(feature = "webp-decode")]
472fn decode_webp(data: &[u8]) -> RawResult<RgbImage> {
473 let (w, h, rgb) = crate::codecs::webp::decode_webp_rgb(data).map_err(|e| {
474 RawError::Format(FormatError::ImageDecode {
475 format: "WebP",
476 message: e,
477 })
478 })?;
479
480 let data_u16: Vec<u16> = rgb.iter().map(|&v| u8_to_u16(v)).collect();
481
482 Ok(RgbImage::new(w, h, data_u16))
483}
484
485#[cfg(feature = "jxl-decode")]
488fn decode_jxl(data: &[u8]) -> RawResult<RgbImage> {
489 use jxl_oxide::JxlImage;
490
491 let image = JxlImage::builder().read(Cursor::new(data)).map_err(|e| {
492 RawError::Format(FormatError::ImageDecode {
493 format: "JXL",
494 message: format!("{e}"),
495 })
496 })?;
497
498 let w = image.width();
499 let h = image.height();
500
501 if image.num_loaded_keyframes() == 0 {
502 return Err(RawError::Format(FormatError::ImageDecode {
503 format: "JXL",
504 message: "no keyframes decoded".to_string(),
505 }));
506 }
507
508 let render = image.render_frame(0).map_err(|e| {
509 RawError::Format(FormatError::ImageDecode {
510 format: "JXL",
511 message: format!("{e}"),
512 })
513 })?;
514
515 jxl_render_to_rgb(w, h, image.pixel_format(), render)
516}
517
518#[cfg(feature = "jxl-decode")]
521fn jxl_render_to_rgb(
522 w: u32,
523 h: u32,
524 pixel_format: jxl_oxide::PixelFormat,
525 render: jxl_oxide::Render,
526) -> RawResult<RgbImage> {
527 use jxl_oxide::PixelFormat;
528
529 let total_pixels = (w as usize) * (h as usize);
530
531 let mut stream = render.stream_no_alpha();
533 let channels = stream.channels() as usize;
534
535 let mut samples_u16 = vec![0u16; total_pixels * channels];
536 stream.write_to_buffer(&mut samples_u16);
537
538 let data_u16: Vec<u16> = match pixel_format {
539 PixelFormat::Rgb => samples_u16,
540 PixelFormat::Gray => samples_u16.iter().flat_map(|&v| [v, v, v]).collect(),
541 PixelFormat::Rgba | PixelFormat::Graya => {
542 if pixel_format == PixelFormat::Graya {
543 samples_u16
544 .chunks_exact(channels)
545 .flat_map(|px| [px[0], px[0], px[0]])
546 .collect()
547 } else {
548 samples_u16
549 .chunks_exact(channels)
550 .flat_map(|px| [px[0], px[1], px[2]])
551 .collect()
552 }
553 }
554 _ => {
555 return Err(RawError::Format(FormatError::ImageDecode {
556 format: "JXL",
557 message: format!("unsupported pixel format {pixel_format:?}"),
558 }));
559 }
560 };
561
562 Ok(RgbImage::new(w, h, data_u16))
563}
564
565#[cfg(feature = "jxl-decode")]
579pub fn decode_jxl_partial(data: &[u8]) -> RawResult<(RgbImage, bool)> {
580 use jxl_oxide::{InitializeResult, JxlImage};
581
582 let jxl_err = |e| {
583 RawError::Format(FormatError::ImageDecode {
584 format: "JXL",
585 message: format!("{e}"),
586 })
587 };
588
589 let mut uninit = JxlImage::builder().build_uninit();
591 let mut offset = 0usize;
592 let mut image = loop {
593 let consumed = if offset < data.len() {
594 uninit.feed_bytes(&data[offset..]).map_err(jxl_err)?
595 } else {
596 0
597 };
598 offset += consumed;
599 match uninit.try_init().map_err(jxl_err)? {
600 InitializeResult::Initialized(img) => break img,
601 InitializeResult::NeedMoreData(next) => {
602 uninit = next;
603 if consumed == 0 {
604 return Err(RawError::Format(FormatError::ImageDecode {
605 format: "JXL",
606 message: "stream too short to read the image header".to_string(),
607 }));
608 }
609 }
610 }
611 };
612
613 while offset < data.len() {
616 let consumed = image.feed_bytes(&data[offset..]).map_err(jxl_err)?;
617 if consumed == 0 {
618 break;
619 }
620 offset += consumed;
621 }
622
623 let (w, h) = (image.width(), image.height());
624 let pixel_format = image.pixel_format();
625
626 let (render, complete) = if image.num_loaded_keyframes() > 0 {
629 (image.render_frame(0).map_err(jxl_err)?, true)
630 } else {
631 (image.render_loading_frame().map_err(jxl_err)?, false)
632 };
633
634 let rgb = jxl_render_to_rgb(w, h, pixel_format, render)?;
635 Ok((tag_srgb(rgb), complete))
636}
637
638#[cfg(feature = "tiff-decode")]
641fn decode_tiff(data: &[u8]) -> RawResult<RgbImage> {
642 use tiff::ColorType;
643 use tiff::decoder::{Decoder, DecodingResult};
644
645 let cursor = Cursor::new(data);
646 let mut decoder = Decoder::new(cursor).map_err(|e| {
647 RawError::Format(FormatError::ImageDecode {
648 format: "TIFF",
649 message: format!("{e}"),
650 })
651 })?;
652
653 let (w, h) = decoder.dimensions().map_err(|e| {
654 RawError::Format(FormatError::ImageDecode {
655 format: "TIFF",
656 message: format!("{e}"),
657 })
658 })?;
659
660 let color_type = decoder.colortype().map_err(|e| {
661 RawError::Format(FormatError::ImageDecode {
662 format: "TIFF",
663 message: format!("{e}"),
664 })
665 })?;
666
667 let result = decoder.read_image().map_err(|e| {
668 RawError::Format(FormatError::ImageDecode {
669 format: "TIFF",
670 message: format!("{e}"),
671 })
672 })?;
673
674 let samples_u16: Vec<u16> = match result {
676 DecodingResult::U8(px) => px.iter().map(|&v| u8_to_u16(v)).collect(),
677 DecodingResult::U16(px) => px,
678 DecodingResult::U32(px) => px.iter().map(|&v| (v >> 16) as u16).collect(),
679 DecodingResult::F32(px) => px
680 .iter()
681 .map(|&v| (v.clamp(0.0, 1.0) * 65535.0) as u16)
682 .collect(),
683 _ => {
684 return Err(RawError::Format(FormatError::ImageDecode {
685 format: "TIFF",
686 message: format!("unsupported TIFF sample type for color type {color_type:?}"),
687 }));
688 }
689 };
690
691 let data_u16: Vec<u16> = match color_type {
693 ColorType::RGB(_) => samples_u16,
694 ColorType::RGBA(_) => samples_u16
695 .chunks_exact(4)
696 .flat_map(|px| [px[0], px[1], px[2]])
697 .collect(),
698 ColorType::Gray(_) => samples_u16.iter().flat_map(|&v| [v, v, v]).collect(),
699 ColorType::GrayA(_) => samples_u16
700 .chunks_exact(2)
701 .flat_map(|px| [px[0], px[0], px[0]])
702 .collect(),
703 ColorType::CMYK(_) => {
704 samples_u16
706 .chunks_exact(4)
707 .flat_map(|px| {
708 let c = px[0] as f64 / 65535.0;
709 let m = px[1] as f64 / 65535.0;
710 let y = px[2] as f64 / 65535.0;
711 let k = px[3] as f64 / 65535.0;
712 let r = ((1.0 - c) * (1.0 - k) * 65535.0) as u16;
713 let g = ((1.0 - m) * (1.0 - k) * 65535.0) as u16;
714 let b = ((1.0 - y) * (1.0 - k) * 65535.0) as u16;
715 [r, g, b]
716 })
717 .collect()
718 }
719 _ => {
720 return Err(RawError::Format(FormatError::ImageDecode {
721 format: "TIFF",
722 message: format!("unsupported TIFF color type: {color_type:?}"),
723 }));
724 }
725 };
726
727 Ok(RgbImage::new(w, h, data_u16))
728}
729
730#[cfg(feature = "avif-decode")]
733fn decode_avif(data: &[u8]) -> RawResult<RgbImage> {
734 use image::DynamicImage;
735 use image::codecs::avif::AvifDecoder;
736
737 let decoder = AvifDecoder::new(Cursor::new(data)).map_err(|e| {
738 RawError::Format(FormatError::ImageDecode {
739 format: "AVIF",
740 message: format!("{e}"),
741 })
742 })?;
743
744 let img = DynamicImage::from_decoder(decoder).map_err(|e| {
745 RawError::Format(FormatError::ImageDecode {
746 format: "AVIF",
747 message: format!("{e}"),
748 })
749 })?;
750
751 let rgb = img.into_rgb16();
752 let w = rgb.width();
753 let h = rgb.height();
754 Ok(RgbImage::new(w, h, rgb.into_raw()))
755}
756
757#[cfg(not(feature = "avif-decode"))]
758fn decode_avif(_data: &[u8]) -> RawResult<RgbImage> {
759 Err(RawError::Unsupported(
760 "AVIF decoding requires the `avif-decode` feature flag.".to_string(),
761 ))
762}
763
764#[cfg(feature = "heic-decode")]
768fn decode_heic(data: &[u8]) -> RawResult<RgbImage> {
769 let decoded = crate::codecs::heic::decode_primary(data).map_err(|message| {
770 RawError::Format(FormatError::ImageDecode {
771 format: "HEIC",
772 message,
773 })
774 })?;
775 Ok(RgbImage::new(decoded.width, decoded.height, decoded.rgb))
776}
777
778#[cfg(not(feature = "heic-decode"))]
779fn decode_heic(_data: &[u8]) -> RawResult<RgbImage> {
780 Err(RawError::Format(FormatError::ImageDecode {
781 format: "HEIC",
782 message: "HEIC decoding requires the `heic` feature flag.".to_string(),
783 }))
784}
785
786#[cfg(feature = "svg-decode")]
789fn decode_svg(data: &[u8], cfg: &ResvgDecodeConfig) -> RawResult<RgbImage> {
790 use resvg::{tiny_skia, usvg};
791
792 let options = usvg::Options {
793 dpi: cfg.dpi,
794 ..usvg::Options::default()
795 };
796 let tree = usvg::Tree::from_data(data, &options).map_err(|e| {
797 RawError::Format(FormatError::ImageDecode {
798 format: "SVG",
799 message: e.to_string(),
800 })
801 })?;
802
803 let pixmap_size = tree.size().to_int_size();
804 let width = pixmap_size.width();
805 let height = pixmap_size.height();
806
807 let mut pixmap = tiny_skia::Pixmap::new(width, height).ok_or_else(|| {
808 RawError::Format(FormatError::ImageDecode {
809 format: "SVG",
810 message: "Failed to create pixmap".to_string(),
811 })
812 })?;
813
814 resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
815
816 let rgba = pixmap.data();
818 let data_u16: Vec<u16> = rgba
819 .chunks_exact(4)
820 .flat_map(|chunk| {
821 [
822 chunk[0] as u16 * 257, chunk[1] as u16 * 257, chunk[2] as u16 * 257, ]
826 })
827 .collect();
828
829 Ok(RgbImage::new(width, height, data_u16))
830}
831
832#[cfg(not(feature = "svg-decode"))]
833fn decode_svg(_data: &[u8], _cfg: &ResvgDecodeConfig) -> RawResult<RgbImage> {
834 Err(RawError::Format(FormatError::ImageDecode {
835 format: "SVG",
836 message: "SVG support requires the 'svg' feature flag".to_string(),
837 }))
838}
839
840fn decode_apv(_data: &[u8]) -> RawResult<RgbImage> {
843 Err(RawError::Format(FormatError::ImageDecode {
846 format: "APV",
847 message: "APV codec decoding is not yet implemented. \
848 The APV codec is an open format but no Rust decoder exists yet."
849 .to_string(),
850 }))
851}
852
853#[cfg(feature = "ppm-decode")]
856fn decode_ppm(data: &[u8], _cfg: &ZunePpmDecodeConfig) -> RawResult<RgbImage> {
857 let cursor = ZCursor::new(data);
858 let mut decoder = zune_ppm::PPMDecoder::new_with_options(cursor, DecoderOptions::default());
859
860 let result = decoder.decode().map_err(|e| {
861 RawError::Format(FormatError::ImageDecode {
862 format: "PPM",
863 message: format!("{e:?}"),
864 })
865 })?;
866
867 let (w, h) = decoder
868 .dimensions()
869 .map(|(w, h)| (w as u32, h as u32))
870 .ok_or_else(|| {
871 RawError::Format(FormatError::ImageDecode {
872 format: "PPM",
873 message: "could not read image dimensions after decode".to_string(),
874 })
875 })?;
876
877 let colorspace = decoder.colorspace().unwrap_or(ColorSpace::RGB);
878 let n = colorspace.num_components();
879
880 let samples_u16: Vec<u16> = match result {
882 DecodingResult::U8(px) => px.iter().map(|&v| u8_to_u16(v)).collect(),
883 DecodingResult::U16(px) => px,
884 DecodingResult::F32(px) => px
885 .iter()
886 .map(|&v| (v.clamp(0.0, 1.0) * 65535.0) as u16)
887 .collect(),
888 _ => {
889 return Err(RawError::Format(FormatError::ImageDecode {
890 format: "PPM",
891 message: "unexpected pixel depth in decoded result".to_string(),
892 }));
893 }
894 };
895
896 let data_u16: Vec<u16> = match colorspace {
898 ColorSpace::RGB => samples_u16,
899 ColorSpace::RGBA => samples_u16
900 .chunks_exact(n)
901 .flat_map(|px| [px[0], px[1], px[2]])
902 .collect(),
903 ColorSpace::Luma => samples_u16.iter().flat_map(|&v| [v, v, v]).collect(),
904 ColorSpace::LumaA => samples_u16
905 .chunks_exact(n)
906 .flat_map(|px| [px[0], px[0], px[0]])
907 .collect(),
908 _ => {
909 return Err(RawError::Format(FormatError::ImageDecode {
910 format: "PPM",
911 message: format!("unsupported PPM colorspace: {colorspace:?}"),
912 }));
913 }
914 };
915
916 Ok(RgbImage::new(w, h, data_u16))
917}
918
919#[derive(Debug, Clone, PartialEq, Eq, Default)]
923#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
924pub struct ZuneJpegDecodeConfig {
925 pub max_width: Option<usize>,
928 pub max_height: Option<usize>,
931 pub strict: bool,
934}
935
936#[derive(Debug, Clone, PartialEq, Eq, Default)]
938#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
939pub struct ZunePngDecodeConfig {
940 pub max_width: Option<usize>,
943 pub max_height: Option<usize>,
946 pub confirm_crc: bool,
948 pub strict: bool,
950}
951
952#[derive(Debug, Clone, PartialEq)]
954#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
955pub struct ResvgDecodeConfig {
956 pub dpi: f32,
959}
960
961impl Default for ResvgDecodeConfig {
962 fn default() -> Self {
963 Self { dpi: 96.0 }
964 }
965}
966
967macro_rules! empty_decode_config {
971 ($(#[$m:meta])* $name:ident, $lib:literal) => {
972 #[doc = concat!("Per-implementation configuration for the `", $lib, "` decoder.")]
973 $(#[$m])*
978 #[derive(Debug, Clone, PartialEq, Eq, Default)]
979 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
980 pub struct $name {}
981 };
982}
983
984empty_decode_config!(LibwebpDecodeConfig, "libwebp");
985empty_decode_config!(JxlOxideDecodeConfig, "jxl-oxide");
986empty_decode_config!(GifDecodeConfig, "gif");
987empty_decode_config!(TiffDecodeConfig, "tiff");
988empty_decode_config!(ImageAvifDecodeConfig, "image (avif-native)");
989empty_decode_config!(LibheifDecodeConfig, "libheif");
990empty_decode_config!(ZunePpmDecodeConfig, "zune-ppm");
991
992#[derive(Debug, Clone, PartialEq)]
1004#[non_exhaustive]
1005#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1006pub enum DecodeOptions {
1007 #[cfg(feature = "jpeg-decode")]
1009 JpegZune(ZuneJpegDecodeConfig),
1010 #[cfg(feature = "png-decode")]
1012 PngZune(ZunePngDecodeConfig),
1013 #[cfg(feature = "webp-decode")]
1015 WebpLibwebp(LibwebpDecodeConfig),
1016 #[cfg(feature = "jxl-decode")]
1018 JxlOxide(JxlOxideDecodeConfig),
1019 #[cfg(feature = "gif-decode")]
1021 Gif(GifDecodeConfig),
1022 #[cfg(feature = "tiff-decode")]
1024 Tiff(TiffDecodeConfig),
1025 #[cfg(feature = "avif-decode")]
1027 AvifImage(ImageAvifDecodeConfig),
1028 #[cfg(feature = "heic-decode")]
1030 HeicLibheif(LibheifDecodeConfig),
1031 #[cfg(feature = "svg-decode")]
1033 SvgResvg(ResvgDecodeConfig),
1034 #[cfg(feature = "ppm-decode")]
1036 PpmZune(ZunePpmDecodeConfig),
1037}
1038
1039impl DecodeOptions {
1040 pub fn format(&self) -> StandardFormat {
1042 match self {
1043 #[cfg(feature = "jpeg-decode")]
1044 DecodeOptions::JpegZune(_) => StandardFormat::Jpeg,
1045 #[cfg(feature = "png-decode")]
1046 DecodeOptions::PngZune(_) => StandardFormat::Png,
1047 #[cfg(feature = "webp-decode")]
1048 DecodeOptions::WebpLibwebp(_) => StandardFormat::WebP,
1049 #[cfg(feature = "jxl-decode")]
1050 DecodeOptions::JxlOxide(_) => StandardFormat::Jxl,
1051 #[cfg(feature = "gif-decode")]
1052 DecodeOptions::Gif(_) => StandardFormat::Gif,
1053 #[cfg(feature = "tiff-decode")]
1054 DecodeOptions::Tiff(_) => StandardFormat::Tiff,
1055 #[cfg(feature = "avif-decode")]
1056 DecodeOptions::AvifImage(_) => StandardFormat::Avif,
1057 #[cfg(feature = "heic-decode")]
1058 DecodeOptions::HeicLibheif(_) => StandardFormat::Heic,
1059 #[cfg(feature = "svg-decode")]
1060 DecodeOptions::SvgResvg(_) => StandardFormat::Svg,
1061 #[cfg(feature = "ppm-decode")]
1062 DecodeOptions::PpmZune(_) => StandardFormat::Ppm,
1063 #[allow(unreachable_patterns)]
1066 _ => unreachable!(),
1067 }
1068 }
1069
1070 pub fn codec_id(&self) -> CodecId {
1072 match self {
1073 #[cfg(feature = "jpeg-decode")]
1074 DecodeOptions::JpegZune(_) => CodecId::new("jpeg/zune"),
1075 #[cfg(feature = "png-decode")]
1076 DecodeOptions::PngZune(_) => CodecId::new("png/zune"),
1077 #[cfg(feature = "webp-decode")]
1078 DecodeOptions::WebpLibwebp(_) => CodecId::new("webp/libwebp"),
1079 #[cfg(feature = "jxl-decode")]
1080 DecodeOptions::JxlOxide(_) => CodecId::new("jxl/jxl-oxide"),
1081 #[cfg(feature = "gif-decode")]
1082 DecodeOptions::Gif(_) => CodecId::new("gif/gif"),
1083 #[cfg(feature = "tiff-decode")]
1084 DecodeOptions::Tiff(_) => CodecId::new("tiff/tiff"),
1085 #[cfg(feature = "avif-decode")]
1086 DecodeOptions::AvifImage(_) => CodecId::new("avif/image"),
1087 #[cfg(feature = "heic-decode")]
1088 DecodeOptions::HeicLibheif(_) => CodecId::new("heic/libheif"),
1089 #[cfg(feature = "svg-decode")]
1090 DecodeOptions::SvgResvg(_) => CodecId::new("svg/resvg"),
1091 #[cfg(feature = "ppm-decode")]
1092 DecodeOptions::PpmZune(_) => CodecId::new("ppm/zune"),
1093 #[allow(unreachable_patterns)]
1094 _ => unreachable!(),
1095 }
1096 }
1097
1098 pub fn default_for(format: StandardFormat) -> Option<DecodeOptions> {
1103 match format {
1104 #[cfg(feature = "jpeg-decode")]
1105 StandardFormat::Jpeg => Some(DecodeOptions::JpegZune(ZuneJpegDecodeConfig::default())),
1106 #[cfg(feature = "png-decode")]
1107 StandardFormat::Png => Some(DecodeOptions::PngZune(ZunePngDecodeConfig::default())),
1108 #[cfg(feature = "webp-decode")]
1109 StandardFormat::WebP => {
1110 Some(DecodeOptions::WebpLibwebp(LibwebpDecodeConfig::default()))
1111 }
1112 #[cfg(feature = "jxl-decode")]
1113 StandardFormat::Jxl => Some(DecodeOptions::JxlOxide(JxlOxideDecodeConfig::default())),
1114 #[cfg(feature = "gif-decode")]
1115 StandardFormat::Gif => Some(DecodeOptions::Gif(GifDecodeConfig::default())),
1116 #[cfg(feature = "tiff-decode")]
1117 StandardFormat::Tiff => Some(DecodeOptions::Tiff(TiffDecodeConfig::default())),
1118 #[cfg(feature = "avif-decode")]
1119 StandardFormat::Avif => {
1120 Some(DecodeOptions::AvifImage(ImageAvifDecodeConfig::default()))
1121 }
1122 #[cfg(feature = "heic-decode")]
1123 StandardFormat::Heic => {
1124 Some(DecodeOptions::HeicLibheif(LibheifDecodeConfig::default()))
1125 }
1126 #[cfg(feature = "svg-decode")]
1127 StandardFormat::Svg => Some(DecodeOptions::SvgResvg(ResvgDecodeConfig::default())),
1128 #[cfg(feature = "ppm-decode")]
1129 StandardFormat::Ppm => Some(DecodeOptions::PpmZune(ZunePpmDecodeConfig::default())),
1130 #[allow(unreachable_patterns)]
1131 _ => None,
1132 }
1133 }
1134}
1135
1136pub fn decode_standard_image(data: &[u8], format: StandardFormat) -> RawResult<RgbImage> {
1153 let decoded: RawResult<RgbImage> = match format {
1154 #[cfg(feature = "gif-decode")]
1155 StandardFormat::Gif => decode_gif(data),
1156 #[cfg(feature = "jpeg-decode")]
1157 StandardFormat::Jpeg => decode_jpeg(data, &ZuneJpegDecodeConfig::default()),
1158 #[cfg(feature = "png-decode")]
1159 StandardFormat::Png => decode_png(data, &ZunePngDecodeConfig::default()),
1160 #[cfg(feature = "webp-decode")]
1161 StandardFormat::WebP => decode_webp(data),
1162 #[cfg(feature = "jxl-decode")]
1163 StandardFormat::Jxl => decode_jxl(data),
1164 #[cfg(feature = "tiff-decode")]
1165 StandardFormat::Tiff => decode_tiff(data),
1166 StandardFormat::Avif => decode_avif(data),
1167 StandardFormat::Heic => decode_heic(data),
1168 StandardFormat::Svg => decode_svg(data, &ResvgDecodeConfig::default()),
1169 #[cfg(feature = "ppm-decode")]
1170 StandardFormat::Ppm => decode_ppm(data, &ZunePpmDecodeConfig::default()),
1171 StandardFormat::Apv => decode_apv(data),
1172 #[allow(unreachable_patterns)]
1173 _ => Err(RawError::Unsupported(format!(
1174 "Decoding {:?} requires a feature flag that is not enabled.",
1175 format.name()
1176 ))),
1177 };
1178 decoded.map(tag_srgb)
1179}
1180
1181#[cfg_attr(not(any_standard_decode), allow(unused_variables, unreachable_code))]
1191pub fn decode_standard_image_with(data: &[u8], options: &DecodeOptions) -> RawResult<RgbImage> {
1192 let decoded: RawResult<RgbImage> = match options {
1193 #[cfg(feature = "jpeg-decode")]
1194 DecodeOptions::JpegZune(cfg) => decode_jpeg(data, cfg),
1195 #[cfg(feature = "png-decode")]
1196 DecodeOptions::PngZune(cfg) => decode_png(data, cfg),
1197 #[cfg(feature = "webp-decode")]
1198 DecodeOptions::WebpLibwebp(_cfg) => decode_webp(data),
1199 #[cfg(feature = "jxl-decode")]
1200 DecodeOptions::JxlOxide(_cfg) => decode_jxl(data),
1201 #[cfg(feature = "gif-decode")]
1202 DecodeOptions::Gif(_cfg) => decode_gif(data),
1203 #[cfg(feature = "tiff-decode")]
1204 DecodeOptions::Tiff(_cfg) => decode_tiff(data),
1205 #[cfg(feature = "avif-decode")]
1206 DecodeOptions::AvifImage(_cfg) => decode_avif(data),
1207 #[cfg(feature = "heic-decode")]
1208 DecodeOptions::HeicLibheif(_cfg) => decode_heic(data),
1209 #[cfg(feature = "svg-decode")]
1210 DecodeOptions::SvgResvg(cfg) => decode_svg(data, cfg),
1211 #[cfg(feature = "ppm-decode")]
1212 DecodeOptions::PpmZune(cfg) => decode_ppm(data, cfg),
1213 #[allow(unreachable_patterns)]
1216 _ => unreachable!(),
1217 };
1218 decoded.map(tag_srgb)
1219}
1220
1221fn tag_srgb(mut image: RgbImage) -> RgbImage {
1231 image.set_color_space(crate::core::ColorSpace::Srgb);
1232 image
1233}
1234
1235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1242#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1243#[non_exhaustive]
1244pub struct ImageProbe {
1245 pub format: StandardFormat,
1247 pub size: Size,
1249 pub bit_depth: Option<u8>,
1251 pub color_space: crate::core::ColorSpace,
1253}
1254
1255fn probe_err(format: &'static str, msg: impl Into<String>) -> RawError {
1256 RawError::Format(FormatError::ImageDecode {
1257 format,
1258 message: format!("probe: {}", msg.into()),
1259 })
1260}
1261
1262pub fn probe_standard_image(data: &[u8]) -> RawResult<ImageProbe> {
1273 let format = detect_standard_format(data)
1274 .ok_or_else(|| RawError::Unsupported("unrecognized image format".to_string()))?;
1275
1276 let (size, bit_depth) = match format {
1277 StandardFormat::Png => probe_png(data)?,
1278 StandardFormat::Jpeg => probe_jpeg(data)?,
1279 StandardFormat::Gif => probe_gif(data)?,
1280 StandardFormat::WebP => probe_webp(data)?,
1281 StandardFormat::Tiff => probe_tiff(data)?,
1282 StandardFormat::Avif => probe_isobmff(data, "AVIF")?,
1283 StandardFormat::Heic => probe_isobmff(data, "HEIC")?,
1284 StandardFormat::Ppm => probe_ppm(data)?,
1285 StandardFormat::Jxl => {
1286 #[cfg(feature = "jxl-decode")]
1287 {
1288 probe_jxl(data)?
1289 }
1290 #[cfg(not(feature = "jxl-decode"))]
1291 {
1292 return Err(RawError::Unsupported(
1293 "probing JXL requires the `jxl-decode` feature".to_string(),
1294 ));
1295 }
1296 }
1297 StandardFormat::Svg | StandardFormat::Apv => {
1298 return Err(probe_err(
1299 format.name(),
1300 "format has no intrinsic raster dimensions",
1301 ));
1302 }
1303 };
1304
1305 Ok(ImageProbe {
1306 format,
1307 size,
1308 bit_depth,
1309 color_space: crate::core::ColorSpace::Srgb,
1310 })
1311}
1312
1313fn probe_png(data: &[u8]) -> RawResult<(Size, Option<u8>)> {
1315 if data.len() < 26 || &data[12..16] != b"IHDR" {
1316 return Err(probe_err("PNG", "missing IHDR chunk"));
1317 }
1318 let width = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
1319 let height = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
1320 Ok((Size::new(width, height), Some(data[24])))
1321}
1322
1323fn probe_jpeg(data: &[u8]) -> RawResult<(Size, Option<u8>)> {
1325 let mut i = 2; while i + 1 < data.len() {
1327 if data[i] != 0xFF {
1328 i += 1;
1329 continue;
1330 }
1331 let marker = data[i + 1];
1332 if marker == 0xFF {
1334 i += 1;
1335 continue;
1336 }
1337 if marker == 0x00 || marker == 0x01 || (0xD0..=0xD9).contains(&marker) {
1338 i += 2;
1339 continue;
1340 }
1341 if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
1343 if i + 9 > data.len() {
1344 return Err(probe_err("JPEG", "truncated SOF segment"));
1345 }
1346 let precision = data[i + 4];
1347 let height = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
1348 let width = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
1349 return Ok((Size::new(width, height), Some(precision)));
1350 }
1351 if i + 4 > data.len() {
1354 break;
1355 }
1356 let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
1357 i += 2 + len.max(2);
1358 }
1359 Err(probe_err("JPEG", "no SOF marker found"))
1360}
1361
1362fn probe_gif(data: &[u8]) -> RawResult<(Size, Option<u8>)> {
1364 if data.len() < 10 {
1365 return Err(probe_err("GIF", "truncated header"));
1366 }
1367 let width = u16::from_le_bytes([data[6], data[7]]) as u32;
1368 let height = u16::from_le_bytes([data[8], data[9]]) as u32;
1369 Ok((Size::new(width, height), Some(8)))
1370}
1371
1372fn probe_webp(data: &[u8]) -> RawResult<(Size, Option<u8>)> {
1374 if data.len() < 30 || &data[8..12] != b"WEBP" {
1375 return Err(probe_err("WebP", "not a RIFF/WEBP container"));
1376 }
1377 match &data[12..16] {
1379 b"VP8X" => {
1380 let width = 1 + u32::from_le_bytes([data[24], data[25], data[26], 0]);
1381 let height = 1 + u32::from_le_bytes([data[27], data[28], data[29], 0]);
1382 Ok((Size::new(width, height), Some(8)))
1383 }
1384 b"VP8 " => {
1385 let width = u16::from_le_bytes([data[26], data[27]]) as u32 & 0x3FFF;
1387 let height = u16::from_le_bytes([data[28], data[29]]) as u32 & 0x3FFF;
1388 Ok((Size::new(width, height), Some(8)))
1389 }
1390 b"VP8L" => {
1391 let bits = u32::from_le_bytes([data[21], data[22], data[23], data[24]]);
1393 let width = (bits & 0x3FFF) + 1;
1394 let height = ((bits >> 14) & 0x3FFF) + 1;
1395 Ok((Size::new(width, height), Some(8)))
1396 }
1397 _ => Err(probe_err("WebP", "unrecognized WebP chunk")),
1398 }
1399}
1400
1401fn probe_tiff(data: &[u8]) -> RawResult<(Size, Option<u8>)> {
1403 if data.len() < 8 {
1404 return Err(probe_err("TIFF", "truncated header"));
1405 }
1406 let le = match &data[0..2] {
1407 b"II" => true,
1408 b"MM" => false,
1409 _ => return Err(probe_err("TIFF", "bad byte-order mark")),
1410 };
1411 let rd16 = |b: &[u8]| {
1412 if le {
1413 u16::from_le_bytes([b[0], b[1]])
1414 } else {
1415 u16::from_be_bytes([b[0], b[1]])
1416 }
1417 };
1418 let rd32 = |b: &[u8]| {
1419 if le {
1420 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
1421 } else {
1422 u32::from_be_bytes([b[0], b[1], b[2], b[3]])
1423 }
1424 };
1425
1426 let ifd_off = rd32(&data[4..8]) as usize;
1427 if ifd_off + 2 > data.len() {
1428 return Err(probe_err("TIFF", "IFD offset out of bounds"));
1429 }
1430 let count = rd16(&data[ifd_off..]) as usize;
1431 let (mut width, mut height) = (None, None);
1432 for entry in 0..count {
1433 let base = ifd_off + 2 + entry * 12;
1434 if base + 12 > data.len() {
1435 break;
1436 }
1437 let tag = rd16(&data[base..]);
1438 let ty = rd16(&data[base + 2..]);
1439 let value = &data[base + 8..base + 12];
1440 let scalar = match ty {
1441 3 => rd16(value) as u32, 4 => rd32(value), _ => continue,
1444 };
1445 match tag {
1446 0x0100 => width = Some(scalar),
1447 0x0101 => height = Some(scalar),
1448 _ => {}
1449 }
1450 }
1451 match (width, height) {
1452 (Some(w), Some(h)) => Ok((Size::new(w, h), None)),
1453 _ => Err(probe_err("TIFF", "ImageWidth/ImageLength not found")),
1454 }
1455}
1456
1457fn probe_isobmff(data: &[u8], format: &'static str) -> RawResult<(Size, Option<u8>)> {
1460 let mut best: Option<(u32, u32)> = None;
1461 let mut i = 0;
1462 while i + 16 <= data.len() {
1463 if &data[i..i + 4] == b"ispe" {
1464 let w = u32::from_be_bytes([data[i + 8], data[i + 9], data[i + 10], data[i + 11]]);
1466 let h = u32::from_be_bytes([data[i + 12], data[i + 13], data[i + 14], data[i + 15]]);
1467 let larger =
1468 best.is_none_or(|(bw, bh)| (bw as u64 * bh as u64) < (w as u64 * h as u64));
1469 if larger {
1470 best = Some((w, h));
1471 }
1472 }
1473 i += 1;
1474 }
1475 match best {
1476 Some((w, h)) => Ok((Size::new(w, h), None)),
1477 None => Err(probe_err(format, "no `ispe` box found")),
1478 }
1479}
1480
1481fn probe_ppm(data: &[u8]) -> RawResult<(Size, Option<u8>)> {
1483 let mut tokens: Vec<&[u8]> = Vec::new();
1487 let mut i = 2;
1488 while i < data.len() && tokens.len() < 3 {
1489 match data[i] {
1490 b'#' => {
1491 while i < data.len() && data[i] != b'\n' {
1492 i += 1;
1493 }
1494 }
1495 b if b.is_ascii_whitespace() => i += 1,
1496 _ => {
1497 let start = i;
1498 while i < data.len() && !data[i].is_ascii_whitespace() && data[i] != b'#' {
1499 i += 1;
1500 }
1501 tokens.push(&data[start..i]);
1502 }
1503 }
1504 }
1505 let parse =
1506 |t: &[u8]| -> Option<u32> { std::str::from_utf8(t).ok().and_then(|s| s.parse().ok()) };
1507 let width = tokens.first().and_then(|&t| parse(t));
1508 let height = tokens.get(1).and_then(|&t| parse(t));
1509 match (width, height) {
1510 (Some(w), Some(h)) => {
1511 let bits = tokens
1513 .get(2)
1514 .and_then(|&t| parse(t))
1515 .map(|maxval| if maxval > 255 { 16u8 } else { 8 });
1516 Ok((Size::new(w, h), bits))
1517 }
1518 _ => Err(probe_err("PPM", "could not read width/height")),
1519 }
1520}
1521
1522#[cfg(feature = "jxl-decode")]
1524fn probe_jxl(data: &[u8]) -> RawResult<(Size, Option<u8>)> {
1525 use jxl_oxide::{InitializeResult, JxlImage};
1526
1527 let mut uninit = JxlImage::builder().build_uninit();
1528 uninit
1529 .feed_bytes(data)
1530 .map_err(|e| probe_err("JXL", e.to_string()))?;
1531 match uninit
1532 .try_init()
1533 .map_err(|e| probe_err("JXL", e.to_string()))?
1534 {
1535 InitializeResult::Initialized(img) => Ok((Size::new(img.width(), img.height()), None)),
1536 InitializeResult::NeedMoreData(_) => Err(probe_err(
1537 "JXL",
1538 "stream too short to read the image header",
1539 )),
1540 }
1541}
1542
1543#[cfg(test)]
1544mod probe_tests {
1545 use super::*;
1546
1547 #[test]
1548 fn probe_png_reads_ihdr() {
1549 let mut png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
1551 png.extend_from_slice(&13u32.to_be_bytes());
1552 png.extend_from_slice(b"IHDR");
1553 png.extend_from_slice(&640u32.to_be_bytes());
1554 png.extend_from_slice(&480u32.to_be_bytes());
1555 png.extend_from_slice(&[8, 2, 0, 0, 0]); let probe = probe_standard_image(&png).expect("probe PNG");
1557 assert_eq!(probe.format, StandardFormat::Png);
1558 assert_eq!(probe.size, Size::new(640, 480));
1559 assert_eq!(probe.bit_depth, Some(8));
1560 }
1561
1562 #[test]
1563 fn probe_gif_reads_screen_descriptor() {
1564 let mut gif = Vec::from(*b"GIF89a");
1565 gif.extend_from_slice(&320u16.to_le_bytes());
1566 gif.extend_from_slice(&200u16.to_le_bytes());
1567 gif.extend_from_slice(&[0, 0, 0]);
1568 let probe = probe_standard_image(&gif).expect("probe GIF");
1569 assert_eq!(probe.size, Size::new(320, 200));
1570 }
1571
1572 #[test]
1573 fn probe_rejects_garbage() {
1574 assert!(probe_standard_image(b"not an image at all").is_err());
1575 }
1576}
1577
1578#[cfg(feature = "exif")]
1598pub fn read_standard_image_metadata(
1599 data: &[u8],
1600 format: StandardFormat,
1601) -> crate::core::metadata::ImageMetadata {
1602 use crate::metadata::exif::ExifParser;
1603 use little_exif::filetype::FileExtension;
1604
1605 #[cfg(feature = "heic-decode")]
1607 if format == StandardFormat::Heic {
1608 return crate::formats::heic::read_heic_metadata(data);
1609 }
1610
1611 let file_type = match format {
1612 StandardFormat::Jpeg => FileExtension::JPEG,
1613 StandardFormat::Tiff => FileExtension::TIFF,
1614 StandardFormat::WebP => FileExtension::WEBP,
1615 StandardFormat::Avif => FileExtension::HEIF,
1616 StandardFormat::Png => FileExtension::PNG {
1617 as_zTXt_chunk: false,
1618 },
1619 _ => return crate::core::metadata::ImageMetadata::default(),
1621 };
1622
1623 ExifParser::parse_from_bytes(data, file_type)
1624}
1625
1626#[cfg(not(feature = "exif"))]
1632pub fn read_standard_image_metadata(
1633 _data: &[u8],
1634 _format: StandardFormat,
1635) -> crate::core::metadata::ImageMetadata {
1636 crate::core::metadata::ImageMetadata::default()
1637}
1638
1639#[cfg(test)]
1642mod tests {
1643 use super::*;
1644
1645 #[test]
1648 fn detect_gif89a() {
1649 let magic = *b"GIF89a\x01\x00";
1650 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Gif));
1651 }
1652
1653 #[test]
1654 fn detect_gif87a() {
1655 let magic = *b"GIF87a\x01\x00";
1656 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Gif));
1657 }
1658
1659 #[test]
1660 fn detect_gif_non_gif_returns_none() {
1661 let magic = *b"GIF99z\x01\x00";
1662 assert_ne!(detect_standard_format(&magic), Some(StandardFormat::Gif));
1663 }
1664
1665 #[test]
1666 fn detect_jpeg() {
1667 let magic = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46];
1668 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Jpeg));
1669 }
1670
1671 #[test]
1672 fn detect_png() {
1673 let magic = *b"\x89PNG\r\n\x1a\n";
1674 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Png));
1675 }
1676
1677 #[test]
1678 fn detect_webp() {
1679 let mut magic = [0u8; 12];
1680 magic[0..4].copy_from_slice(b"RIFF");
1681 magic[8..12].copy_from_slice(b"WEBP");
1682 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::WebP));
1683 }
1684
1685 #[test]
1686 fn detect_jxl_bare() {
1687 let magic = [0xFF, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
1688 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Jxl));
1689 }
1690
1691 #[test]
1692 fn detect_jxl_container() {
1693 let mut magic = [0u8; 12];
1694 magic[4..8].copy_from_slice(b"JXL ");
1696 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Jxl));
1697 }
1698
1699 #[test]
1700 fn detect_tiff_le() {
1701 let magic = [0x49, 0x49, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00];
1702 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Tiff));
1703 }
1704
1705 #[test]
1706 fn detect_tiff_be() {
1707 let magic = [0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08];
1708 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Tiff));
1709 }
1710
1711 #[test]
1712 fn detect_avif() {
1713 let mut magic = [0u8; 12];
1714 magic[4..8].copy_from_slice(b"ftyp");
1715 magic[8..12].copy_from_slice(b"avif");
1716 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Avif));
1717 }
1718
1719 #[test]
1720 fn detect_avif_avis() {
1721 let mut magic = [0u8; 12];
1722 magic[4..8].copy_from_slice(b"ftyp");
1723 magic[8..12].copy_from_slice(b"avis");
1724 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Avif));
1725 }
1726
1727 #[test]
1728 fn detect_unknown_returns_none() {
1729 let magic = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
1730 assert_eq!(detect_standard_format(&magic), None);
1731 }
1732
1733 #[test]
1734 fn detect_too_short_returns_none() {
1735 assert_eq!(detect_standard_format(&[0xFF, 0xD8]), None);
1736 }
1737
1738 #[test]
1741 fn standard_format_name() {
1742 assert_eq!(StandardFormat::Gif.name(), "GIF");
1743 assert_eq!(StandardFormat::Jpeg.name(), "JPEG");
1744 assert_eq!(StandardFormat::Png.name(), "PNG");
1745 assert_eq!(StandardFormat::WebP.name(), "WebP");
1746 assert_eq!(StandardFormat::Jxl.name(), "JXL");
1747 assert_eq!(StandardFormat::Tiff.name(), "TIFF");
1748 assert_eq!(StandardFormat::Avif.name(), "AVIF");
1749 }
1750
1751 #[test]
1752 fn standard_format_variants_exist() {
1753 let _variants = [
1754 StandardFormat::Gif,
1755 StandardFormat::Jpeg,
1756 StandardFormat::Png,
1757 StandardFormat::WebP,
1758 StandardFormat::Jxl,
1759 StandardFormat::Tiff,
1760 StandardFormat::Avif,
1761 StandardFormat::Heic,
1762 StandardFormat::Svg,
1763 StandardFormat::Apv,
1764 StandardFormat::Ppm,
1765 ];
1766 }
1767
1768 #[test]
1771 fn u8_to_u16_min_max() {
1772 assert_eq!(u8_to_u16(0), 0);
1773 assert_eq!(u8_to_u16(255), 65535);
1774 assert_eq!(u8_to_u16(1), 257);
1775 assert_eq!(u8_to_u16(128), 32896);
1776 }
1777
1778 #[test]
1781 fn jpeg_roundtrip_dimensions() {
1782 const W: u16 = 4;
1784 const H: u16 = 4;
1785 let pixels: Vec<u8> = (0..(W as usize * H as usize * 3))
1786 .map(|i| (i * 17 % 256) as u8)
1787 .collect();
1788
1789 let mut encoded = Vec::new();
1790 let encoder = jpeg_encoder::Encoder::new(&mut encoded, 95);
1791 encoder
1792 .encode(&pixels, W, H, jpeg_encoder::ColorType::Rgb)
1793 .expect("JPEG encode failed");
1794
1795 let decoded =
1796 decode_standard_image(&encoded, StandardFormat::Jpeg).expect("JPEG decode failed");
1797
1798 assert_eq!(decoded.width(), W as u32);
1799 assert_eq!(decoded.height(), H as u32);
1800 assert_eq!(decoded.data.len(), W as usize * H as usize * 3);
1801 }
1802
1803 #[test]
1806 fn png_roundtrip_dimensions() {
1807 const W: usize = 4;
1810 const H: usize = 4;
1811 let pixels_u8: Vec<u8> = (0..(W * H * 3)).map(|i| (i * 13 % 256) as u8).collect();
1812
1813 let opts = zune_core::options::EncoderOptions::default()
1814 .set_width(W)
1815 .set_height(H)
1816 .set_colorspace(ColorSpace::RGB);
1817 let mut encoded = Vec::new();
1818 let mut encoder = zune_png::PngEncoder::new(&pixels_u8, opts);
1819 encoder.encode(&mut encoded).expect("PNG encode failed");
1820
1821 let decoded =
1822 decode_standard_image(&encoded, StandardFormat::Png).expect("PNG decode failed");
1823
1824 assert_eq!(decoded.width(), W as u32);
1825 assert_eq!(decoded.height(), H as u32);
1826 assert_eq!(decoded.data.len(), W * H * 3);
1827 assert_eq!(decoded.data[0], u8_to_u16(pixels_u8[0]));
1829 }
1830
1831 #[test]
1834 fn decode_options_default_for_apv_is_none() {
1835 assert!(DecodeOptions::default_for(StandardFormat::Apv).is_none());
1837 }
1838
1839 #[cfg(feature = "png-decode")]
1840 #[test]
1841 fn decode_options_default_for_roundtrips_format() {
1842 let opts = DecodeOptions::default_for(StandardFormat::Png).expect("png decoder");
1843 assert_eq!(opts.format(), StandardFormat::Png);
1844 assert!(matches!(opts, DecodeOptions::PngZune(_)));
1845 }
1846
1847 #[cfg(feature = "png-decode")]
1848 #[test]
1849 fn decode_standard_image_with_selects_png_backend() {
1850 const W: usize = 4;
1851 const H: usize = 4;
1852 let pixels_u8: Vec<u8> = (0..(W * H * 3)).map(|i| (i * 13 % 256) as u8).collect();
1853
1854 let opts = zune_core::options::EncoderOptions::default()
1855 .set_width(W)
1856 .set_height(H)
1857 .set_colorspace(ColorSpace::RGB);
1858 let mut encoded = Vec::new();
1859 let mut encoder = zune_png::PngEncoder::new(&pixels_u8, opts);
1860 encoder.encode(&mut encoded).expect("PNG encode failed");
1861
1862 let cfg = ZunePngDecodeConfig {
1864 confirm_crc: true,
1865 ..ZunePngDecodeConfig::default()
1866 };
1867 let via_with = decode_standard_image_with(&encoded, &DecodeOptions::PngZune(cfg))
1868 .expect("decode_standard_image_with failed");
1869 let via_default =
1871 decode_standard_image(&encoded, StandardFormat::Png).expect("PNG decode failed");
1872
1873 assert_eq!(via_with.width(), W as u32);
1874 assert_eq!(via_with.height(), H as u32);
1875 assert_eq!(via_with.data, via_default.data);
1876 }
1877
1878 #[test]
1881 fn detect_then_decode_jpeg() {
1882 const W: u16 = 2;
1883 const H: u16 = 2;
1884 let pixels = vec![
1885 100u8, 150u8, 200u8, 50u8, 75u8, 100u8, 200u8, 220u8, 240u8, 10u8, 20u8, 30u8,
1886 ];
1887 let mut encoded = Vec::new();
1888 jpeg_encoder::Encoder::new(&mut encoded, 90)
1889 .encode(&pixels, W, H, jpeg_encoder::ColorType::Rgb)
1890 .unwrap();
1891
1892 let fmt = detect_standard_format(&encoded);
1893 assert_eq!(fmt, Some(StandardFormat::Jpeg));
1894 let img = decode_standard_image(&encoded, fmt.unwrap()).unwrap();
1895 assert_eq!(img.width(), W as u32);
1896 assert_eq!(img.height(), H as u32);
1897 }
1898
1899 #[test]
1902 fn detect_ppm_p6() {
1903 let magic = *b"P6\n2 2\n255\n";
1904 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Ppm));
1905 }
1906
1907 #[test]
1908 fn detect_ppm_rejects_ascii_pbm() {
1909 let magic = *b"P1\n2 2\n0 0\n";
1911 assert_eq!(detect_standard_format(&magic), None);
1912 }
1913
1914 #[cfg(feature = "ppm-decode")]
1915 #[test]
1916 fn ppm_p6_decode_dimensions_and_samples() {
1917 let pixels: [u8; 12] = [
1921 100, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, ];
1926 let mut file = b"P6\n2 2\n255\n".to_vec();
1927 file.extend_from_slice(&pixels);
1928
1929 let fmt = detect_standard_format(&file);
1930 assert_eq!(fmt, Some(StandardFormat::Ppm));
1931
1932 let decoded = decode_standard_image(&file, StandardFormat::Ppm).expect("PPM decode failed");
1933 assert_eq!(decoded.width(), 2);
1934 assert_eq!(decoded.height(), 2);
1935 assert_eq!(decoded.data.len(), 2 * 2 * 3);
1936 assert_eq!(decoded.data[0], u8_to_u16(pixels[0]));
1938 assert_eq!(decoded.data[11], u8_to_u16(pixels[11]));
1939 }
1940
1941 fn make_minimal_gif() -> Vec<u8> {
1951 use gif::{Encoder, Frame, Repeat};
1955 use std::borrow::Cow;
1956
1957 let palette: &[u8] = &[
1958 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1964 ];
1965
1966 let mut out: Vec<u8> = Vec::new();
1967 let mut encoder = Encoder::new(&mut out, 2, 2, palette).expect("gif encoder init");
1968 encoder.set_repeat(Repeat::Finite(0)).expect("set repeat");
1969
1970 let frame = Frame {
1971 width: 2,
1972 height: 2,
1973 buffer: Cow::Owned(vec![0u8, 1, 2, 3]),
1975 ..Frame::default()
1976 };
1977 encoder.write_frame(&frame).expect("write gif frame");
1978 drop(encoder);
1979 out
1980 }
1981
1982 #[test]
1983 fn gif_decode_dimensions() {
1984 let gif_data = make_minimal_gif();
1985 let img =
1986 decode_standard_image(&gif_data, StandardFormat::Gif).expect("GIF decode must succeed");
1987 assert_eq!(img.width(), 2, "decoded width must be 2");
1988 assert_eq!(img.height(), 2, "decoded height must be 2");
1989 assert_eq!(
1990 img.data.len(),
1991 2 * 2 * 3,
1992 "must have 12 u16 samples (2×2×3)"
1993 );
1994 }
1995
1996 #[test]
1997 fn gif_decode_detect_then_decode() {
1998 let gif_data = make_minimal_gif();
1999 let fmt = detect_standard_format(&gif_data);
2000 assert_eq!(
2001 fmt,
2002 Some(StandardFormat::Gif),
2003 "format detection must return GIF"
2004 );
2005 let img = decode_standard_image(&gif_data, fmt.unwrap()).unwrap();
2006 assert_eq!(img.width(), 2);
2007 assert_eq!(img.height(), 2);
2008 }
2009
2010 #[test]
2011 fn gif_decode_first_pixel_is_red() {
2012 let gif_data = make_minimal_gif();
2013 let img = decode_standard_image(&gif_data, StandardFormat::Gif).unwrap();
2014 assert_eq!(img.data[0], u8_to_u16(255), "R of top-left pixel");
2016 assert_eq!(img.data[1], u8_to_u16(0), "G of top-left pixel");
2017 assert_eq!(img.data[2], u8_to_u16(0), "B of top-left pixel");
2018 }
2019
2020 #[test]
2021 fn gif_decode_invalid_data_returns_error() {
2022 let junk = vec![0u8; 32];
2023 let result = decode_standard_image(&junk, StandardFormat::Gif);
2024 assert!(result.is_err(), "junk data must return an error");
2025 }
2026
2027 #[test]
2030 fn tiff_invalid_data_returns_error() {
2031 let magic = [0x49u8, 0x49, 0x2A, 0x00, 0, 0, 0, 8];
2033 let result = decode_standard_image(&magic, StandardFormat::Tiff);
2034 assert!(result.is_err());
2035 }
2036
2037 fn make_minimal_tiff_rgb8() -> Vec<u8> {
2039 use std::io::Cursor;
2040 use tiff::encoder::{TiffEncoder, colortype::RGB8};
2041 let mut cursor = Cursor::new(Vec::new());
2042 {
2043 let mut enc = TiffEncoder::new(&mut cursor).unwrap();
2044 let pixels: Vec<u8> = vec![
2045 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, ];
2050 enc.write_image::<RGB8>(2, 2, &pixels).unwrap();
2051 }
2052 cursor.into_inner()
2053 }
2054
2055 #[test]
2056 fn tiff_decode_dimensions() {
2057 let tiff_data = make_minimal_tiff_rgb8();
2058 let img = decode_standard_image(&tiff_data, StandardFormat::Tiff)
2059 .expect("TIFF decode must succeed");
2060 assert_eq!(img.width(), 2);
2061 assert_eq!(img.height(), 2);
2062 assert_eq!(img.data.len(), 2 * 2 * 3);
2063 }
2064
2065 #[test]
2066 fn tiff_decode_first_pixel_is_red() {
2067 let tiff_data = make_minimal_tiff_rgb8();
2068 let img = decode_standard_image(&tiff_data, StandardFormat::Tiff).unwrap();
2069 assert_eq!(img.data[0], u8_to_u16(255), "R of top-left pixel");
2070 assert_eq!(img.data[1], u8_to_u16(0), "G of top-left pixel");
2071 assert_eq!(img.data[2], u8_to_u16(0), "B of top-left pixel");
2072 }
2073
2074 #[test]
2075 fn tiff_detect_then_decode() {
2076 let tiff_data = make_minimal_tiff_rgb8();
2077 let fmt = detect_standard_format(&tiff_data);
2078 assert_eq!(fmt, Some(StandardFormat::Tiff));
2079 let img = decode_standard_image(&tiff_data, fmt.unwrap()).unwrap();
2080 assert_eq!(img.width(), 2);
2081 assert_eq!(img.height(), 2);
2082 }
2083
2084 fn make_tiff_gray8() -> Vec<u8> {
2086 use std::io::Cursor;
2087 use tiff::encoder::{TiffEncoder, colortype::Gray8};
2088 let mut cursor = Cursor::new(Vec::new());
2089 {
2090 let mut enc = TiffEncoder::new(&mut cursor).unwrap();
2091 let pixels: Vec<u8> = (0..16).map(|i| (i * 17) as u8).collect();
2092 enc.write_image::<Gray8>(4, 4, &pixels).unwrap();
2093 }
2094 cursor.into_inner()
2095 }
2096
2097 #[test]
2098 fn tiff_decode_grayscale_expands_to_rgb() {
2099 let tiff_data = make_tiff_gray8();
2100 let img = decode_standard_image(&tiff_data, StandardFormat::Tiff).unwrap();
2101 assert_eq!(img.width(), 4);
2102 assert_eq!(img.height(), 4);
2103 assert_eq!(img.data.len(), 4 * 4 * 3);
2104 for px in img.data.chunks_exact(3) {
2106 assert_eq!(px[0], px[1]);
2107 assert_eq!(px[1], px[2]);
2108 }
2109 }
2110
2111 fn make_tiff_rgba8() -> Vec<u8> {
2113 use std::io::Cursor;
2114 use tiff::encoder::{TiffEncoder, colortype::RGBA8};
2115 let mut cursor = Cursor::new(Vec::new());
2116 {
2117 let mut enc = TiffEncoder::new(&mut cursor).unwrap();
2118 let pixels: Vec<u8> = vec![
2119 255, 0, 0, 128, 0, 255, 0, 255, 0, 0, 255, 0, 255, 255, 255, 255, ];
2124 enc.write_image::<RGBA8>(2, 2, &pixels).unwrap();
2125 }
2126 cursor.into_inner()
2127 }
2128
2129 #[test]
2130 fn tiff_decode_rgba_drops_alpha() {
2131 let tiff_data = make_tiff_rgba8();
2132 let img = decode_standard_image(&tiff_data, StandardFormat::Tiff).unwrap();
2133 assert_eq!(img.width(), 2);
2134 assert_eq!(img.height(), 2);
2135 assert_eq!(img.data.len(), 2 * 2 * 3);
2137 assert_eq!(img.data[0], u8_to_u16(255));
2139 assert_eq!(img.data[1], u8_to_u16(0));
2140 assert_eq!(img.data[2], u8_to_u16(0));
2141 }
2142
2143 fn make_tiff_rgb16() -> Vec<u8> {
2145 use std::io::Cursor;
2146 use tiff::encoder::{TiffEncoder, colortype::RGB16};
2147 let mut cursor = Cursor::new(Vec::new());
2148 {
2149 let mut enc = TiffEncoder::new(&mut cursor).unwrap();
2150 let pixels: Vec<u16> = vec![
2151 65535, 0, 0, 0, 65535, 0, 0, 0, 65535, 32768, 32768, 32768, ];
2156 enc.write_image::<RGB16>(2, 2, &pixels).unwrap();
2157 }
2158 cursor.into_inner()
2159 }
2160
2161 #[test]
2162 fn tiff_decode_16bit_preserves_values() {
2163 let tiff_data = make_tiff_rgb16();
2164 let img = decode_standard_image(&tiff_data, StandardFormat::Tiff).unwrap();
2165 assert_eq!(img.width(), 2);
2166 assert_eq!(img.height(), 2);
2167 assert_eq!(img.data[0], 65535); assert_eq!(img.data[1], 0); assert_eq!(img.data[2], 0); }
2172
2173 #[cfg(not(feature = "avif-decode"))]
2174 #[test]
2175 fn avif_returns_unsupported_without_feature() {
2176 let mut magic = [0u8; 12];
2177 magic[4..8].copy_from_slice(b"ftyp");
2178 magic[8..12].copy_from_slice(b"avif");
2179 let result = decode_standard_image(&magic, StandardFormat::Avif);
2180 assert!(result.is_err());
2181 assert!(matches!(result, Err(RawError::Unsupported(_))));
2182 }
2183
2184 #[test]
2187 fn standard_format_name_new_variants() {
2188 assert_eq!(StandardFormat::Heic.name(), "HEIC");
2189 assert_eq!(StandardFormat::Svg.name(), "SVG");
2190 assert_eq!(StandardFormat::Apv.name(), "APV");
2191 }
2192
2193 #[test]
2196 fn standard_format_display() {
2197 let all = [
2198 (StandardFormat::Gif, "GIF"),
2199 (StandardFormat::Jpeg, "JPEG"),
2200 (StandardFormat::Png, "PNG"),
2201 (StandardFormat::WebP, "WebP"),
2202 (StandardFormat::Jxl, "JXL"),
2203 (StandardFormat::Tiff, "TIFF"),
2204 (StandardFormat::Avif, "AVIF"),
2205 (StandardFormat::Heic, "HEIC"),
2206 (StandardFormat::Svg, "SVG"),
2207 (StandardFormat::Apv, "APV"),
2208 ];
2209 for (fmt, expected) in all {
2210 assert_eq!(format!("{}", fmt), expected);
2211 assert_eq!(fmt.to_string(), expected);
2212 }
2213 }
2214
2215 #[test]
2218 fn extension_roundtrip() {
2219 let all = [
2220 StandardFormat::Gif,
2221 StandardFormat::Jpeg,
2222 StandardFormat::Png,
2223 StandardFormat::WebP,
2224 StandardFormat::Jxl,
2225 StandardFormat::Tiff,
2226 StandardFormat::Avif,
2227 StandardFormat::Heic,
2228 StandardFormat::Svg,
2229 StandardFormat::Apv,
2230 ];
2231 for fmt in all {
2232 let ext = fmt.extension();
2233 assert_eq!(
2234 StandardFormat::from_extension(ext),
2235 Some(fmt),
2236 "roundtrip failed for {:?} (ext={ext})",
2237 fmt
2238 );
2239 }
2240 }
2241
2242 #[test]
2243 fn from_extension_case_insensitive() {
2244 assert_eq!(
2245 StandardFormat::from_extension("JPG"),
2246 Some(StandardFormat::Jpeg)
2247 );
2248 assert_eq!(
2249 StandardFormat::from_extension("Png"),
2250 Some(StandardFormat::Png)
2251 );
2252 assert_eq!(
2253 StandardFormat::from_extension("WEBP"),
2254 Some(StandardFormat::WebP)
2255 );
2256 }
2257
2258 #[test]
2259 fn from_extension_aliases() {
2260 for ext in ["jpg", "jpeg", "jpe", "jfif"] {
2262 assert_eq!(
2263 StandardFormat::from_extension(ext),
2264 Some(StandardFormat::Jpeg),
2265 "alias {ext}"
2266 );
2267 }
2268 assert_eq!(
2270 StandardFormat::from_extension("tif"),
2271 Some(StandardFormat::Tiff)
2272 );
2273 assert_eq!(
2275 StandardFormat::from_extension("heif"),
2276 Some(StandardFormat::Heic)
2277 );
2278 assert_eq!(
2280 StandardFormat::from_extension("svgz"),
2281 Some(StandardFormat::Svg)
2282 );
2283 }
2284
2285 #[test]
2286 fn from_extension_unknown_returns_none() {
2287 assert_eq!(StandardFormat::from_extension("bmp"), None);
2288 assert_eq!(StandardFormat::from_extension(""), None);
2289 assert_eq!(StandardFormat::from_extension("raw"), None);
2290 }
2291
2292 #[test]
2293 fn mime_types() {
2294 assert_eq!(StandardFormat::Gif.mime_type(), "image/gif");
2295 assert_eq!(StandardFormat::Jpeg.mime_type(), "image/jpeg");
2296 assert_eq!(StandardFormat::Png.mime_type(), "image/png");
2297 assert_eq!(StandardFormat::WebP.mime_type(), "image/webp");
2298 assert_eq!(StandardFormat::Jxl.mime_type(), "image/jxl");
2299 assert_eq!(StandardFormat::Tiff.mime_type(), "image/tiff");
2300 assert_eq!(StandardFormat::Avif.mime_type(), "image/avif");
2301 assert_eq!(StandardFormat::Heic.mime_type(), "image/heic");
2302 assert_eq!(StandardFormat::Svg.mime_type(), "image/svg+xml");
2303 assert_eq!(StandardFormat::Apv.mime_type(), "video/apv");
2304 }
2305
2306 #[test]
2309 fn supports_decode_standard_formats() {
2310 assert!(StandardFormat::Gif.supports_decode());
2311 assert!(StandardFormat::Jpeg.supports_decode());
2312 assert!(StandardFormat::Png.supports_decode());
2313 assert!(StandardFormat::WebP.supports_decode());
2314 assert!(StandardFormat::Jxl.supports_decode());
2315 assert!(StandardFormat::Tiff.supports_decode());
2316 #[cfg(feature = "avif-decode")]
2318 assert!(StandardFormat::Avif.supports_decode());
2319 #[cfg(not(feature = "avif-decode"))]
2320 assert!(!StandardFormat::Avif.supports_decode());
2321 #[cfg(feature = "heic-decode")]
2323 assert!(StandardFormat::Heic.supports_decode());
2324 #[cfg(not(feature = "heic-decode"))]
2325 assert!(!StandardFormat::Heic.supports_decode());
2326 assert!(!StandardFormat::Apv.supports_decode());
2328 }
2329
2330 #[test]
2331 fn supports_encode_standard_formats() {
2332 assert!(StandardFormat::Png.supports_encode());
2333 assert!(StandardFormat::Jpeg.supports_encode());
2334 assert!(StandardFormat::WebP.supports_encode());
2335 assert!(!StandardFormat::Gif.supports_encode());
2337 assert!(!StandardFormat::Tiff.supports_encode());
2338 assert!(!StandardFormat::Heic.supports_encode());
2339 assert!(!StandardFormat::Apv.supports_encode());
2340 }
2341
2342 #[cfg(feature = "heic-decode")]
2345 #[test]
2346 fn detect_heic_heic_brand() {
2347 let mut magic = [0u8; 12];
2348 magic[4..8].copy_from_slice(b"ftyp");
2349 magic[8..12].copy_from_slice(b"heic");
2350 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Heic));
2351 }
2352
2353 #[cfg(feature = "heic-decode")]
2354 #[test]
2355 fn detect_heic_heis_brand() {
2356 let mut magic = [0u8; 12];
2357 magic[4..8].copy_from_slice(b"ftyp");
2358 magic[8..12].copy_from_slice(b"heis");
2359 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Heic));
2360 }
2361
2362 #[cfg(feature = "heic-decode")]
2363 #[test]
2364 fn detect_heic_hevc_brand() {
2365 let mut magic = [0u8; 12];
2366 magic[4..8].copy_from_slice(b"ftyp");
2367 magic[8..12].copy_from_slice(b"hevc");
2368 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Heic));
2369 }
2370
2371 #[cfg(feature = "heic-decode")]
2372 #[test]
2373 fn detect_heic_hevx_brand() {
2374 let mut magic = [0u8; 12];
2375 magic[4..8].copy_from_slice(b"ftyp");
2376 magic[8..12].copy_from_slice(b"hevx");
2377 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Heic));
2378 }
2379
2380 #[test]
2381 fn detect_non_heic_ftyp_cr3_returns_none_for_heic() {
2382 let mut magic = [0u8; 12];
2384 magic[4..8].copy_from_slice(b"ftyp");
2385 magic[8..12].copy_from_slice(b"crx ");
2386 assert_ne!(detect_standard_format(&magic), Some(StandardFormat::Heic));
2387 }
2388
2389 #[cfg(feature = "heic-decode")]
2390 #[test]
2391 fn heic_decode_returns_error() {
2392 let mut magic = [0u8; 12];
2393 magic[4..8].copy_from_slice(b"ftyp");
2394 magic[8..12].copy_from_slice(b"heic");
2395 let result = decode_standard_image(&magic, StandardFormat::Heic);
2396 assert!(result.is_err());
2397 assert!(matches!(
2398 result,
2399 Err(RawError::Format(FormatError::ImageDecode {
2400 format: "HEIC",
2401 ..
2402 }))
2403 ));
2404 }
2405
2406 #[test]
2409 fn detect_apv_apv1_brand() {
2410 let mut magic = [0u8; 12];
2411 magic[4..8].copy_from_slice(b"ftyp");
2412 magic[8..12].copy_from_slice(b"apv1");
2413 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Apv));
2414 }
2415
2416 #[test]
2417 fn detect_apv_apvx_brand() {
2418 let mut magic = [0u8; 12];
2419 magic[4..8].copy_from_slice(b"ftyp");
2420 magic[8..12].copy_from_slice(b"apvx");
2421 assert_eq!(detect_standard_format(&magic), Some(StandardFormat::Apv));
2422 }
2423
2424 #[test]
2425 fn apv_decode_returns_error() {
2426 let mut magic = [0u8; 12];
2427 magic[4..8].copy_from_slice(b"ftyp");
2428 magic[8..12].copy_from_slice(b"apv1");
2429 let result = decode_standard_image(&magic, StandardFormat::Apv);
2430 assert!(result.is_err());
2431 assert!(matches!(
2432 result,
2433 Err(RawError::Format(FormatError::ImageDecode {
2434 format: "APV",
2435 ..
2436 }))
2437 ));
2438 }
2439
2440 #[test]
2443 fn detect_svg_xml_prefix() {
2444 let data = b"<?xml version=\"1.0\"?><svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
2445 assert_eq!(detect_standard_format(data), Some(StandardFormat::Svg));
2446 }
2447
2448 #[test]
2449 fn detect_svg_bare_svg_tag() {
2450 let data = b"<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
2451 assert_eq!(detect_standard_format(data), Some(StandardFormat::Svg));
2452 }
2453
2454 #[test]
2455 fn detect_svg_xml_without_svg_tag_returns_none() {
2456 let data = b"<?xml version=\"1.0\"?><root></root>";
2458 assert_eq!(detect_standard_format(data), None);
2459 }
2460
2461 #[test]
2462 fn svg_decode_returns_error_without_feature() {
2463 #[cfg(not(feature = "svg-decode"))]
2465 {
2466 let data = b"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\"></svg>";
2467 let result = decode_standard_image(data, StandardFormat::Svg);
2468 assert!(result.is_err());
2469 assert!(matches!(
2470 result,
2471 Err(RawError::Format(FormatError::ImageDecode {
2472 format: "SVG",
2473 ..
2474 }))
2475 ));
2476 }
2477 #[cfg(feature = "svg-decode")]
2479 {
2480 assert_eq!(StandardFormat::Svg.name(), "SVG");
2482 }
2483 }
2484
2485 #[cfg(feature = "svg-decode")]
2486 #[test]
2487 fn svg_decode_simple_rect() {
2488 let svg = br#"<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4">
2489 <rect width="4" height="4" fill="red"/>
2490 </svg>"#;
2491 let result = decode_standard_image(svg, StandardFormat::Svg);
2492 assert!(result.is_ok());
2493 let img = result.unwrap();
2494 assert_eq!(img.width(), 4);
2495 assert_eq!(img.height(), 4);
2496 assert_eq!(img.data.len(), 4 * 4 * 3);
2497 }
2498
2499 #[test]
2502 fn read_metadata_unsupported_format_returns_default() {
2503 let gif_header = b"GIF89a\x01\x00\x01\x00\x80\x00\x00\xff\x00\x00\x00\x00\x00\x3b";
2505 let md = read_standard_image_metadata(gif_header, StandardFormat::Gif);
2506 assert!(md.camera.make.is_empty());
2507 assert!(md.exif.iso.is_none());
2508 }
2509
2510 #[test]
2511 fn read_metadata_invalid_data_returns_default() {
2512 let junk = b"\x00\x01\x02\x03\x04\x05\x06\x07";
2514 let md = read_standard_image_metadata(junk, StandardFormat::Jpeg);
2515 assert!(md.camera.make.is_empty());
2516 }
2517
2518 #[cfg(feature = "avif")]
2519 #[test]
2520 fn avif_exif_round_trip() {
2521 use crate::core::metadata::*;
2522 use crate::formats::encode_rgb_image;
2523 use crate::formats::export::{
2524 CommonEncodeOptions, EncodeOptions, MetadataEmbedOptions, RavifEncodeConfig,
2525 };
2526
2527 let data: Vec<u16> = vec![65535, 0, 0, 65535, 0, 0, 65535, 0, 0, 65535, 0, 0];
2529 let rgb = RgbImage::new(2, 2, data);
2530
2531 let md = ImageMetadata {
2533 camera: CameraInfo {
2534 make: "TestMake".to_string(),
2535 model: "TestModel".to_string(),
2536 ..Default::default()
2537 },
2538 exif: ExifInfo {
2539 iso: Some(400),
2540 focal_length: Some(URational::new(50, 1)),
2541 ..Default::default()
2542 },
2543 datetime: DateTimeInfo {
2544 datetime_original: Some("2025:06:15 12:00:00".to_string()),
2545 ..Default::default()
2546 },
2547 ..Default::default()
2548 };
2549
2550 let tmp = std::env::temp_dir().join("rawshift_avif_exif_test.avif");
2551 let opts = EncodeOptions::AvifRavif(RavifEncodeConfig {
2552 quality: 60,
2553 speed: 10,
2554 common: CommonEncodeOptions {
2555 metadata: MetadataEmbedOptions {
2556 embed_icc: false,
2557 ..MetadataEmbedOptions::default()
2558 },
2559 ..Default::default()
2560 },
2561 });
2562 encode_rgb_image(&rgb, &md, &tmp, &opts).expect("encode AVIF");
2563
2564 let avif_bytes = std::fs::read(&tmp).expect("read back AVIF");
2566 let read_md = read_standard_image_metadata(&avif_bytes, StandardFormat::Avif);
2567
2568 assert_eq!(read_md.camera.make, "TestMake", "make round-trip");
2570 assert_eq!(read_md.camera.model, "TestModel", "model round-trip");
2571 assert_eq!(read_md.exif.iso, Some(400), "ISO round-trip");
2572 assert_eq!(
2573 read_md.exif.focal_length.map(|r| r.numerator),
2574 Some(50),
2575 "focal_length round-trip"
2576 );
2577 assert_eq!(
2578 read_md.datetime.datetime_original,
2579 Some("2025:06:15 12:00:00".to_string()),
2580 "datetime_original round-trip"
2581 );
2582
2583 let _ = std::fs::remove_file(&tmp);
2584 }
2585
2586 #[cfg(feature = "avif-decode")]
2587 #[test]
2588 fn avif_supports_decode_with_feature() {
2589 assert!(StandardFormat::Avif.supports_decode());
2590 }
2591
2592 #[cfg(feature = "avif-encode")]
2593 #[test]
2594 fn avif_supports_encode_with_feature() {
2595 assert!(StandardFormat::Avif.supports_encode());
2596 }
2597}