1#[cfg(feature = "arw-decode")]
7pub(crate) mod arw;
8#[cfg(feature = "cr2-decode")]
9pub(crate) mod cr2;
10#[cfg(feature = "cr3-decode")]
11pub(crate) mod cr3;
12#[cfg(feature = "crw-decode")]
13pub(crate) mod crw;
14#[cfg(feature = "dng-decode")]
15pub(crate) mod dng;
16#[cfg(feature = "dng-encode")]
17pub(crate) mod dng_export;
18mod encode;
19pub mod export;
20#[cfg(feature = "heic-decode")]
21pub(crate) mod heic;
22#[cfg(feature = "nef-decode")]
23pub(crate) mod nef;
24#[cfg(feature = "raf-decode")]
25pub(crate) mod raf;
26pub mod registry;
27pub(crate) mod standard;
28
29#[cfg(feature = "dng-encode")]
30pub use dng_export::{DngExportConfig, export_dng};
31pub use encode::{encode_rgb_image, encode_rgb_image_to_vec, encode_rgb_image_to_writer};
32#[cfg(feature = "heic-decode")]
33pub use heic::{HeicAuxImage, HeicAuxKind, HeicFile};
34pub use registry::{available_decoders, available_encoders};
35#[cfg(feature = "jxl-decode")]
36pub use standard::decode_jxl_partial;
37pub use standard::{
38 DecodeOptions, GifDecodeConfig, ImageAvifDecodeConfig, ImageProbe, JxlOxideDecodeConfig,
39 LibheifDecodeConfig, LibwebpDecodeConfig, ResvgDecodeConfig, StandardFormat, TiffDecodeConfig,
40 ZuneJpegDecodeConfig, ZunePngDecodeConfig, ZunePpmDecodeConfig, decode_standard_image,
41 decode_standard_image_with, detect_standard_format, probe_standard_image,
42 read_standard_image_metadata,
43};
44
45#[cfg(feature = "tiff-parser")]
46use crate::tiff::{TiffParser, TiffTag};
47
48#[cfg(any_raw)]
49use {
50 crate::core::image::{RawImage, RgbImage},
51 crate::error::{RawError, RawResult},
52 crate::processing::ProcessingOptions,
53 crate::transforms::{
54 apply_bad_pixel_correction, apply_bilateral_filter, apply_black_level, apply_ca_correction,
55 apply_color_matrix, apply_tone_reproduction, apply_white_balance, apply_white_balance_raw,
56 compute_camera_to_srgb,
57 },
58 export::EncodeOptions,
59 std::io::{Read, Seek, SeekFrom},
60 std::path::Path,
61 tracing::instrument,
62};
63
64#[cfg(any_raw)]
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68pub enum RawFormat {
69 #[cfg(feature = "arw-decode")]
71 Arw,
72 #[cfg(feature = "cr2-decode")]
74 Cr2,
75 #[cfg(feature = "cr3-decode")]
77 Cr3,
78 #[cfg(feature = "crw-decode")]
80 Crw,
81 #[cfg(feature = "dng-decode")]
83 Dng,
84 #[cfg(feature = "nef-decode")]
86 Nef,
87 #[cfg(feature = "raf-decode")]
89 Raf,
90}
91
92#[cfg(any_raw)]
93impl std::fmt::Display for RawFormat {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 #[cfg(feature = "arw-decode")]
97 RawFormat::Arw => write!(f, "ARW"),
98 #[cfg(feature = "cr2-decode")]
99 RawFormat::Cr2 => write!(f, "CR2"),
100 #[cfg(feature = "cr3-decode")]
101 RawFormat::Cr3 => write!(f, "CR3"),
102 #[cfg(feature = "crw-decode")]
103 RawFormat::Crw => write!(f, "CRW"),
104 #[cfg(feature = "dng-decode")]
105 RawFormat::Dng => write!(f, "DNG"),
106 #[cfg(feature = "nef-decode")]
107 RawFormat::Nef => write!(f, "NEF"),
108 #[cfg(feature = "raf-decode")]
109 RawFormat::Raf => write!(f, "RAF"),
110 }
111 }
112}
113
114#[cfg(any_raw)]
115pub enum RawFile<R> {
119 #[cfg(feature = "arw-decode")]
121 Arw(Box<arw::ArwFile<R>>),
122 #[cfg(feature = "cr2-decode")]
124 Cr2(Box<cr2::Cr2File<R>>),
125 #[cfg(feature = "cr3-decode")]
127 Cr3(Box<cr3::Cr3File<R>>),
128 #[cfg(feature = "crw-decode")]
130 Crw(Box<crw::CrwFile<R>>),
131 #[cfg(feature = "dng-decode")]
133 Dng(Box<dng::DngFile<R>>),
134 #[cfg(feature = "nef-decode")]
136 Nef(Box<nef::NefFile<R>>),
137 #[cfg(feature = "raf-decode")]
139 Raf(Box<raf::RafFile<R>>),
140}
141
142#[cfg(any_raw)]
149macro_rules! raw_format_dispatch {
150 ($self:expr, $inner:ident => $body:expr) => {
151 match $self {
152 #[cfg(feature = "arw-decode")]
153 Self::Arw($inner) => $body,
154 #[cfg(feature = "cr2-decode")]
155 Self::Cr2($inner) => $body,
156 #[cfg(feature = "cr3-decode")]
157 Self::Cr3($inner) => $body,
158 #[cfg(feature = "crw-decode")]
159 Self::Crw($inner) => $body,
160 #[cfg(feature = "dng-decode")]
161 Self::Dng($inner) => $body,
162 #[cfg(feature = "nef-decode")]
163 Self::Nef($inner) => $body,
164 #[cfg(feature = "raf-decode")]
165 Self::Raf($inner) => $body,
166 }
167 };
168}
169
170#[cfg(any_raw)]
171impl<R: Read + Seek> RawFile<R> {
172 pub fn open(mut reader: R) -> RawResult<Self> {
176 let format = Self::detect_format(&mut reader)?;
177
178 match format {
179 #[cfg(feature = "arw-decode")]
180 RawFormat::Arw => {
181 let file = arw::ArwFile::parse(reader)?;
182 Ok(RawFile::Arw(Box::new(file)))
183 }
184 #[cfg(feature = "cr2-decode")]
185 RawFormat::Cr2 => {
186 let file = cr2::Cr2File::parse(reader)?;
187 Ok(RawFile::Cr2(Box::new(file)))
188 }
189 #[cfg(feature = "cr3-decode")]
190 RawFormat::Cr3 => {
191 let file = cr3::Cr3File::parse(reader)?;
192 Ok(RawFile::Cr3(Box::new(file)))
193 }
194 #[cfg(feature = "crw-decode")]
195 RawFormat::Crw => {
196 let file = crw::CrwFile::parse(reader)?;
197 Ok(RawFile::Crw(Box::new(file)))
198 }
199 #[cfg(feature = "dng-decode")]
200 RawFormat::Dng => {
201 let file = dng::DngFile::parse(reader)?;
202 Ok(RawFile::Dng(Box::new(file)))
203 }
204 #[cfg(feature = "nef-decode")]
205 RawFormat::Nef => {
206 let file = nef::NefFile::parse(reader)?;
207 Ok(RawFile::Nef(Box::new(file)))
208 }
209 #[cfg(feature = "raf-decode")]
210 RawFormat::Raf => {
211 let file = raf::RafFile::parse(reader)?;
212 Ok(RawFile::Raf(Box::new(file)))
213 }
214 }
215 }
216
217 pub fn metadata(&self) -> crate::core::ImageMetadata {
221 use crate::core::MetadataExtractor;
222 raw_format_dispatch!(self, inner => inner.extract_metadata())
223 }
224
225 pub fn thumbnail(&mut self) -> RawResult<Option<Vec<u8>>> {
230 raw_format_dispatch!(self, inner => inner.thumbnail())
231 }
232
233 pub fn decode_raw(&mut self) -> RawResult<RawImage> {
238 raw_format_dispatch!(self, inner => inner.decode_raw())
239 }
240
241 #[instrument(skip(self), fields(process = ?processing_options))]
247 pub fn process(&mut self, processing_options: &ProcessingOptions) -> RawResult<RgbImage> {
248 tracing::trace!("Processing raw file to RGB");
249
250 let mut wb_applied_to_cfa = false;
251
252 let mut rgb_image = if self.is_linear_raw_dng() {
254 #[cfg(feature = "dng-decode")]
255 {
256 tracing::trace!("Using LinearRaw path (already demosaiced)");
257 let RawFile::Dng(dng) = self else {
258 unreachable!()
259 };
260
261 let metadata = dng.metadata();
262 let bit_depth = metadata.map(|m| m.bit_depth).unwrap_or(16);
263 let linearization_table = metadata.and_then(|m| m.linearization_table.as_ref());
264
265 let is_scaled_by_table = if let Some(table) = linearization_table {
266 if !table.is_empty() {
267 let max_val = table.iter().max().copied().unwrap_or(0);
268 tracing::trace!("LinearizationTable present. Max value: {}", max_val);
269 max_val > 4095
270 } else {
271 false
272 }
273 } else {
274 false
275 };
276
277 let mut image = dng.decode_linear_raw()?;
278
279 let shift = if is_scaled_by_table {
280 0
281 } else {
282 16u8.saturating_sub(bit_depth)
283 };
284
285 if shift > 0 {
286 tracing::debug!(
287 "Scaling {}-bit linear data to 16-bit (shift: {})",
288 bit_depth,
289 shift
290 );
291 for pixel in &mut image.data {
292 let val = (*pixel as u32) << shift;
293 *pixel = val.min(65535) as u16;
294 }
295 }
296 image
297 }
298 #[cfg(not(feature = "dng-decode"))]
299 unreachable!()
300 } else {
301 tracing::trace!("Using standard CFA path (demosaicing needed)");
302
303 let cfa_wb = processing_options.white_balance.or_else(|| {
304 let meta = self.metadata();
305 if let Some(neutral) = meta.dng_color.as_shot_neutral
306 && neutral[0] > 0.0
307 && neutral[1] > 0.0
308 && neutral[2] > 0.0
309 {
310 tracing::trace!("Using AsShotNeutral from metadata: {:?}", neutral);
311 return Some((
312 1.0 / neutral[0] as f32,
313 1.0 / neutral[1] as f32,
314 1.0 / neutral[2] as f32,
315 ));
316 }
317 tracing::warn!(
318 "No white balance metadata found. Image may appear green (unbalanced)."
319 );
320 None
321 });
322
323 let mut raw_image = self.decode_raw()?;
324
325 apply_black_level(&mut raw_image);
326
327 if let Some(mode) = processing_options.bad_pixel_correction {
328 tracing::trace!("Applying bad pixel correction: {:?}", mode);
329 apply_bad_pixel_correction(&mut raw_image, mode, 0.5);
330 }
331
332 let effective_white = raw_image
333 .white_level()
334 .saturating_sub(raw_image.black_levels()[0]);
335 if let Some(coeffs) = cfa_wb {
336 apply_white_balance_raw(&mut raw_image, coeffs);
337 wb_applied_to_cfa = true;
338 }
339
340 if effective_white > 0 && effective_white < 65535 {
341 let scale = 65535.0 / effective_white as f32;
342 for pixel in &mut raw_image.data {
343 *pixel = (*pixel as f32 * scale).min(65535.0) as u16;
344 }
345 }
346
347 let demosaic_impl = processing_options.demosaic.to_demosaic();
348 let mut rgb = demosaic_impl.demosaic(&raw_image);
349
350 rgb.set_baseline_exposure(raw_image.baseline_exposure());
351 rgb.set_default_crop(raw_image.default_crop());
352
353 rgb
354 };
355
356 tracing::trace!("Applying post-processing");
358
359 if let Some(exposure) = rgb_image.baseline_exposure() {
360 tracing::debug!(
361 "Applying BaselineExposure={:.2} EV with filmic tone mapping",
362 exposure
363 );
364 } else {
365 tracing::trace!("Applying filmic tone mapping (no BaselineExposure)");
366 }
367
368 if let Some(crop) = rgb_image.default_crop() {
369 tracing::trace!(
370 "Cropping to default crop: {}x{} at {},{}",
371 crop.size.width,
372 crop.size.height,
373 crop.origin.x,
374 crop.origin.y
375 );
376 crate::transforms::orientation::apply_crop(&mut rgb_image, crop);
377 }
378
379 let wb_coeffs = if wb_applied_to_cfa {
380 None
381 } else {
382 processing_options.white_balance.or_else(|| {
383 let meta = self.metadata();
384 if let Some(neutral) = meta.dng_color.as_shot_neutral
385 && neutral[0] > 0.0
386 && neutral[1] > 0.0
387 && neutral[2] > 0.0
388 {
389 tracing::trace!("Using AsShotNeutral from metadata: {:?}", neutral);
390 return Some((
391 1.0 / neutral[0] as f32,
392 1.0 / neutral[1] as f32,
393 1.0 / neutral[2] as f32,
394 ));
395 }
396
397 if !self.is_linear_raw_dng() {
398 tracing::warn!(
399 "No white balance metadata found. Image may appear green (unbalanced)."
400 );
401 }
402
403 None
404 })
405 };
406
407 if let Some(coeffs) = wb_coeffs {
408 tracing::trace!("Applying white balance: {:?}", coeffs);
409 apply_white_balance(&mut rgb_image, coeffs);
410 }
411
412 let color_matrix = processing_options.color_matrix.or_else(|| {
413 let meta = self.metadata();
414 let xyz_to_cam = meta
415 .dng_color
416 .color_matrix_2
417 .or(meta.dng_color.color_matrix_1)
418 .or_else(|| {
419 let model = &meta.camera.model;
420 if model.is_empty() {
421 return None;
422 }
423 let cal = crate::data::cameras::find_camera_calibration(model)?;
424 tracing::trace!("Using camera database color matrix for {}", model);
425 cal.color_matrix_2.or(cal.color_matrix_1)
426 });
427
428 if let Some(ref cm) = xyz_to_cam {
429 match compute_camera_to_srgb(cm) {
430 Some(m) => {
431 tracing::trace!("Auto-resolved camera-to-sRGB color matrix");
432 Some(m)
433 }
434 None => {
435 tracing::warn!("Color matrix is singular, skipping color correction");
436 None
437 }
438 }
439 } else {
440 tracing::debug!("No color matrix available in metadata or camera database");
441 None
442 }
443 });
444 if let Some(matrix) = color_matrix {
445 tracing::trace!("Applying color matrix");
446 apply_color_matrix(&mut rgb_image, &matrix);
447 }
448
449 if let Some(sigma) = processing_options.denoise_sigma {
450 tracing::trace!("Applying bilateral denoise: sigma={}", sigma);
451 let radius = (sigma * 2.0).ceil() as u32;
452 apply_bilateral_filter(&mut rgb_image, sigma, sigma * 10000.0, radius);
453 }
454
455 if let Some((red_scale, blue_scale)) = processing_options.ca_correction {
456 tracing::trace!(
457 "Applying CA correction: red_scale={}, blue_scale={}",
458 red_scale,
459 blue_scale
460 );
461 apply_ca_correction(&mut rgb_image, red_scale, blue_scale);
462 }
463
464 if let Some(g) = processing_options.gamma {
465 tracing::trace!("Applying custom gamma override: {}", g);
466 }
467 apply_tone_reproduction(&mut rgb_image, processing_options.gamma);
468
469 let raw_orientation = self.metadata().image.orientation.unwrap_or(1);
470 if raw_orientation != 1 {
471 tracing::trace!("Applying orientation transform: {}", raw_orientation);
472 crate::transforms::orientation::apply_orientation(&mut rgb_image, raw_orientation);
473 }
474
475 rgb_image.set_color_space(crate::core::ColorSpace::Srgb);
477
478 Ok(rgb_image)
479 }
480
481 #[instrument(
492 skip(self),
493 fields(
494 path = %path.as_ref().display(),
495 process = ?processing_options,
496 encode = ?encode_options
497 )
498 )]
499 pub fn export<P: AsRef<Path>>(
500 &mut self,
501 path: P,
502 processing_options: &ProcessingOptions,
503 encode_options: &EncodeOptions,
504 ) -> RawResult<()> {
505 tracing::trace!("Exporting raw file");
506
507 let rgb_image = self.process(processing_options)?;
508
509 let raw_orientation = self.metadata().image.orientation.unwrap_or(1);
513 let exif_metadata = {
514 let mut m = self.metadata();
515 if raw_orientation != 1 {
516 m.image.orientation = Some(1);
517 }
518 m
519 };
520
521 tracing::info!("Encoding image to disk: {:?}", path.as_ref());
522 encode_rgb_image(&rgb_image, &exif_metadata, path.as_ref(), encode_options)
523 }
524
525 pub fn is_linear_raw_dng(&self) -> bool {
527 match self {
528 #[cfg(feature = "dng-decode")]
529 RawFile::Dng(dng) => dng.metadata().map(|m| m.is_linear_raw).unwrap_or(false),
530 #[cfg(feature = "arw-decode")]
531 RawFile::Arw(_) => false,
532 #[cfg(feature = "cr2-decode")]
533 RawFile::Cr2(_) => false,
534 #[cfg(feature = "cr3-decode")]
535 RawFile::Cr3(_) => false,
536 #[cfg(feature = "crw-decode")]
537 RawFile::Crw(_) => false,
538 #[cfg(feature = "nef-decode")]
539 RawFile::Nef(_) => false,
540 #[cfg(feature = "raf-decode")]
541 RawFile::Raf(_) => false,
542 }
543 }
544
545 fn detect_format(reader: &mut R) -> RawResult<RawFormat> {
547 let start = reader.stream_position()?;
550 let mut header = [0u8; 16];
551 reader.read_exact(&mut header)?;
552 reader.seek(SeekFrom::Start(start))?;
553
554 #[cfg(feature = "raf-decode")]
556 if raf::is_raf(&header) {
557 return Ok(RawFormat::Raf);
558 }
559
560 #[cfg(feature = "cr3-decode")]
563 if cr3::is_cr3(&header) {
564 return Ok(RawFormat::Cr3);
565 }
566
567 #[cfg(feature = "crw-decode")]
571 if crw::is_crw(&header) {
572 return Ok(RawFormat::Crw);
573 }
574
575 let is_tiff = (header[0] == b'I' && header[1] == b'I' && header[2] == 42 && header[3] == 0)
577 || (header[0] == b'M' && header[1] == b'M' && header[2] == 0 && header[3] == 42);
578
579 if !is_tiff {
580 return Err(RawError::Unsupported(
581 "Not a TIFF-based RAW file".to_string(),
582 ));
583 }
584
585 #[cfg(feature = "cr2-decode")]
588 if cr2::is_cr2(&header) {
589 return Ok(RawFormat::Cr2);
590 }
591
592 #[cfg(feature = "tiff-parser")]
594 {
595 let mut parser = TiffParser::new(reader)?;
596 let ifd0 = parser.parse_ifd0()?;
597
598 #[cfg(feature = "dng-decode")]
600 if ifd0.get(TiffTag::DNGVersion).is_some() {
601 return Ok(RawFormat::Dng);
602 }
603
604 if let Some(make_entry) = ifd0.get(TiffTag::Make)
606 && let Ok(value) = parser.read_value(make_entry)
607 && let Some(make) = value.as_str()
608 {
609 let make_lower = make.to_lowercase();
610 #[cfg(feature = "arw-decode")]
611 if make_lower.contains("sony") {
612 return Ok(RawFormat::Arw);
613 }
614 #[cfg(feature = "nef-decode")]
616 if make_lower.contains("nikon") {
617 return Ok(RawFormat::Nef);
618 }
619 }
620 }
621
622 Err(RawError::Unsupported(
624 "Unrecognized camera manufacturer".to_string(),
625 ))
626 }
627}
628#[cfg(test)]
629mod tests {
630 #[cfg(any_raw)]
631 use super::{RawFile, RawFormat};
632 #[cfg(any_raw)]
633 use crate::error::RawError;
634 #[cfg(any_raw)]
635 use std::io::Cursor;
636
637 #[test]
640 fn test_wb_before_normalization_no_clipping_for_midtones() {
641 let white_level: u16 = 16383;
644 let black_level: u16 = 512;
645 let effective_white = white_level - black_level; let (r_gain, g_gain, b_gain) = (2.35f32, 1.0f32, 1.65f32);
647
648 let r_raw: u16 = 3377; let g_raw: u16 = 7936;
654 let b_raw: u16 = 4810;
655
656 let white_f = effective_white as f32;
658 let r_wb = (r_raw as f32 * r_gain).min(white_f) as u16;
659 let g_wb = (g_raw as f32 * g_gain).min(white_f) as u16;
660 let b_wb = (b_raw as f32 * b_gain).min(white_f) as u16;
661
662 assert!(
664 r_wb <= effective_white,
665 "R clipped: {r_wb} > {effective_white}"
666 );
667 assert!(
668 g_wb <= effective_white,
669 "G clipped: {g_wb} > {effective_white}"
670 );
671 assert!(
672 b_wb <= effective_white,
673 "B clipped: {b_wb} > {effective_white}"
674 );
675
676 let scale = 65535.0 / effective_white as f32;
679 let r_16 = (r_wb as f32 * scale) as u16;
680 let g_16 = (g_wb as f32 * scale) as u16;
681 let b_16 = (b_wb as f32 * scale) as u16;
682
683 let expected = 32767u16;
685 let tolerance = 2000u16;
686 assert!(
687 r_16.abs_diff(expected) < tolerance,
688 "R {r_16} not near {expected}"
689 );
690 assert!(
691 g_16.abs_diff(expected) < tolerance,
692 "G {g_16} not near {expected}"
693 );
694 assert!(
695 b_16.abs_diff(expected) < tolerance,
696 "B {b_16} not near {expected}"
697 );
698 }
699
700 #[test]
703 fn test_old_approach_clips_midtones() {
704 let white_level: u16 = 16383;
705 let black_level: u16 = 512;
706 let effective_white = white_level - black_level; let r_gain = 2.35f32;
708
709 let r_raw: u16 = (effective_white as f32 * 0.44) as u16; let shift = 16u8.saturating_sub(14); let r_shifted = ((r_raw as u32) << shift).min(65535) as u16; let r_old = (r_shifted as f32 * r_gain).min(65535.0) as u16;
717
718 let white_f = effective_white as f32;
720 let r_wb = (r_raw as f32 * r_gain).min(white_f) as u16;
721 let scale = 65535.0 / effective_white as f32;
722 let r_new = (r_wb as f32 * scale).min(65535.0) as u16;
723
724 assert_eq!(r_old, 65535, "Old approach should clip to white");
726
727 let r_channel_white = (effective_white as f32 / r_gain) as u16; let r_neutral_50pct = r_channel_white / 2; let r_wb_neutral = (r_neutral_50pct as f32 * r_gain).min(white_f) as u16;
734 let r_new_neutral = (r_wb_neutral as f32 * scale).min(65535.0) as u16;
735
736 let r_shifted_neutral = ((r_neutral_50pct as u32) << shift).min(65535) as u16;
737 let r_old_neutral = (r_shifted_neutral as f32 * r_gain).min(65535.0) as u16;
738
739 assert!(r_old_neutral < 65535, "Old neutral 50% should not clip");
741 assert!(r_new_neutral < 65535, "New neutral 50% should not clip");
743 assert!(
745 r_new_neutral.abs_diff(32767) < 3000,
746 "New neutral 50% {r_new_neutral} should be ~50% of 65535"
747 );
748
749 let _ = r_new;
751 }
752
753 #[cfg(any_raw)]
754 #[test]
755 fn test_detect_format_invalid_magic() {
756 let mut data = vec![0u8; 32];
759 data[..14].copy_from_slice(b"not a raw file");
760 let mut cursor = Cursor::new(data);
761 let result = RawFile::detect_format(&mut cursor);
762 assert!(
763 matches!(result, Err(RawError::Unsupported(_))),
764 "Should fail with UnsupportedFormat for invalid magic: {:?}",
765 result
766 );
767 }
768
769 #[cfg(feature = "tiff-parser")]
770 #[test]
771 fn test_detect_format_tiff_no_make() {
772 let mut data = vec![0u8; 32];
775 data[0..2].copy_from_slice(b"II");
776 data[2..4].copy_from_slice(&42u16.to_le_bytes());
777 data[4..8].copy_from_slice(&8u32.to_le_bytes()); data[8..10].copy_from_slice(&0u16.to_le_bytes()); data[10..14].copy_from_slice(&0u32.to_le_bytes()); let mut cursor = Cursor::new(data);
782 let result = RawFile::detect_format(&mut cursor);
783 assert!(
784 matches!(result, Err(RawError::Unsupported(_))),
785 "Should fail with UnsupportedFormat for unrecognized camera: {:?}",
786 result
787 );
788 }
789
790 #[cfg(feature = "dng-decode")]
791 #[test]
792 fn test_detect_format_dng() {
793 let mut data = vec![0u8; 64];
795 data[0..2].copy_from_slice(b"II");
797 data[2..4].copy_from_slice(&42u16.to_le_bytes());
798 data[4..8].copy_from_slice(&8u32.to_le_bytes());
799
800 let entry_count = 1u16;
802 data[8..10].copy_from_slice(&entry_count.to_le_bytes());
803
804 data[10..12].copy_from_slice(&0xC612u16.to_le_bytes());
807 data[12..14].copy_from_slice(&1u16.to_le_bytes()); data[14..18].copy_from_slice(&4u32.to_le_bytes()); data[18..22].copy_from_slice(&[1, 1, 0, 0]); data[22..26].copy_from_slice(&0u32.to_le_bytes());
813
814 let mut cursor = Cursor::new(data);
815 let result = RawFile::detect_format(&mut cursor);
816 assert!(matches!(result, Ok(RawFormat::Dng)));
817 }
818
819 #[cfg(feature = "dng-decode")]
820 #[test]
821 fn test_detect_format_sony_dng() {
822 let mut data = vec![0u8; 128];
825 data[0..2].copy_from_slice(b"II");
827 data[2..4].copy_from_slice(&42u16.to_le_bytes());
828 data[4..8].copy_from_slice(&8u32.to_le_bytes());
829
830 let entry_count = 2u16;
832 data[8..10].copy_from_slice(&entry_count.to_le_bytes());
833
834 let make_offset = 64u32;
836 data[10..12].copy_from_slice(&0x010Fu16.to_le_bytes());
837 data[12..14].copy_from_slice(&2u16.to_le_bytes());
838 data[14..18].copy_from_slice(&5u32.to_le_bytes());
839 data[18..22].copy_from_slice(&make_offset.to_le_bytes());
840
841 data[22..24].copy_from_slice(&0xC612u16.to_le_bytes());
844 data[24..26].copy_from_slice(&1u16.to_le_bytes());
845 data[26..30].copy_from_slice(&4u32.to_le_bytes());
846 data[30..34].copy_from_slice(&[1, 1, 0, 0]);
847
848 data[34..38].copy_from_slice(&0u32.to_le_bytes());
850
851 data[64..69].copy_from_slice(b"Sony\0");
853
854 let mut cursor = Cursor::new(data);
855 let result = RawFile::detect_format(&mut cursor);
856 assert!(matches!(result, Ok(RawFormat::Dng)));
857 }
858
859 fn make_test_rgb(width: u32, height: u32, data: Vec<u16>) -> crate::core::image::RgbImage {
864 crate::core::image::RgbImage::new(width, height, data)
865 }
866
867 #[test]
868 fn test_flip_horizontal_2x1() {
869 use crate::transforms::orientation::flip_horizontal;
870 let mut img = make_test_rgb(2, 1, vec![10, 11, 12, 20, 21, 22]);
871 flip_horizontal(&mut img);
872 assert_eq!(img.data, vec![20, 21, 22, 10, 11, 12]);
873 }
874
875 #[test]
876 fn test_rotate_180() {
877 use crate::transforms::orientation::rotate_180;
878 let mut img = make_test_rgb(2, 1, vec![1, 2, 3, 4, 5, 6]);
879 rotate_180(&mut img);
880 assert_eq!(img.data, vec![4, 5, 6, 1, 2, 3]);
881 }
882
883 #[test]
884 fn test_rotate_90_cw_1x2() {
885 use crate::transforms::orientation::rotate_90_cw;
886 let mut img = make_test_rgb(1, 2, vec![1, 2, 3, 4, 5, 6]);
887 rotate_90_cw(&mut img);
888 assert_eq!(img.width(), 2);
889 assert_eq!(img.height(), 1);
890 assert_eq!(img.data, vec![4, 5, 6, 1, 2, 3]);
891 }
892
893 #[test]
894 fn test_rotate_90_ccw_2x1() {
895 use crate::transforms::orientation::rotate_90_ccw;
896 let mut img = make_test_rgb(2, 1, vec![1, 2, 3, 4, 5, 6]);
897 rotate_90_ccw(&mut img);
898 assert_eq!(img.width(), 1);
899 assert_eq!(img.height(), 2);
900 assert_eq!(img.data, vec![4, 5, 6, 1, 2, 3]);
901 }
902
903 #[test]
904 fn test_orientation_identity() {
905 use crate::transforms::orientation::apply_orientation;
906 let mut img = make_test_rgb(2, 2, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
907 let original = img.data.clone();
908 apply_orientation(&mut img, 1);
909 assert_eq!(img.data, original);
910 }
911
912 #[test]
913 fn test_orientation_6_cw_then_ccw_is_identity() {
914 use crate::transforms::orientation::apply_orientation;
915 let mut img = make_test_rgb(
916 3,
917 2,
918 vec![
919 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
920 ],
921 );
922 let original_data = img.data.clone();
923 let original_w = img.width();
924 let original_h = img.height();
925 apply_orientation(&mut img, 6); apply_orientation(&mut img, 8); assert_eq!(img.width(), original_w);
928 assert_eq!(img.height(), original_h);
929 assert_eq!(img.data, original_data);
930 }
931
932 #[cfg(any_raw)]
933 #[test]
934 fn test_open_empty_reader_returns_error() {
935 let cursor = Cursor::new(vec![]);
937 let result = RawFile::open(cursor);
938 assert!(
939 result.is_err(),
940 "Opening an empty reader should return an error"
941 );
942 }
943
944 #[cfg(any_raw)]
945 #[test]
946 fn test_detect_format_empty_returns_error() {
947 let mut cursor = Cursor::new(vec![]);
949 let result = RawFile::detect_format(&mut cursor);
950 assert!(
951 result.is_err(),
952 "detect_format on empty input should return an error"
953 );
954 }
955}