use std::{ ffi::OsStr, io::{Cursor, Seek, Write}, path::Path, sync::Arc, thread::JoinHandle, }; use image::DynamicImage; use crate::{ RawImage, RawlerError, decoders::{Decoder, RawDecodeParams, WellKnownIFD}, dng::{DNG_VERSION_V1_4, PREVIEW_JPEG_QUALITY, original::OriginalCompressed, writer::DngWriter}, formats::tiff::Entry, imgop::develop::RawDevelop, rawsource::RawSource, tags::{DngTag, ExifTag, TiffCommonTag}, }; use super::{CropMode, DngCompression, DngPhotometricConversion}; /// Parameters for DNG conversion #[derive(Clone, Debug)] pub struct ConvertParams { pub embedded: bool, pub compression: DngCompression, pub photometric_conversion: DngPhotometricConversion, pub apply_scaling: bool, pub crop: CropMode, pub predictor: u8, pub preview: bool, pub thumbnail: bool, pub artist: Option, pub software: String, pub index: usize, pub keep_mtime: bool, } impl Default for ConvertParams { fn default() -> Self { Self { embedded: true, compression: DngCompression::Lossless, photometric_conversion: DngPhotometricConversion::Original, apply_scaling: false, crop: CropMode::Best, predictor: 1, preview: true, thumbnail: true, artist: None, software: "DNGLab".into(), index: 0, keep_mtime: false, } } } /// Convert a raw input file into DNG /// /// We don't accept a DNG file path here, because we don't know /// how to handle existing target files, buffering, etc. /// This is up to the caller. pub fn convert_raw_file(raw: &Path, dng: &mut W, params: &ConvertParams) -> crate::Result<()> { let original_filename = raw.file_name().and_then(OsStr::to_str).unwrap_or_default(); //let raw_stream = BufReader::new(File::open(raw)?); // TODO: add path hint to error? //let rawfile = RawFile::new(PathBuf::from(raw), raw_stream); let rawfile = Arc::new(RawSource::new(raw)?); let original_compress_thread = if params.embedded { let orig_source = rawfile.clone(); Some(std::thread::spawn(move || OriginalCompressed::compress(&mut orig_source.reader()))) } else { None }; internal_convert(&rawfile, dng, original_filename, original_compress_thread, params) } /// Convert a raw input file into DNG pub fn convert_raw_source(raw_source: &RawSource, dng: &mut W, original_filename: impl AsRef, params: &ConvertParams) -> crate::Result<()> where W: Write + Seek + Send, { let original_compress_thread = if params.embedded { let mut original_stream = Cursor::new(raw_source.as_vec()?); Some(std::thread::spawn(move || OriginalCompressed::compress(&mut original_stream))) } else { None }; internal_convert(raw_source, dng, original_filename, original_compress_thread, params) } fn internal_convert( rawfile: &RawSource, dng: &mut W, original_filename: impl AsRef, original_compress_thread: Option>>, params: &ConvertParams, ) -> crate::Result<()> where W: Write + Seek + Send, { let decoder = crate::get_decoder(rawfile)?; let raw_params = RawDecodeParams { image_index: params.index }; let mut rawimage = decoder.raw_image(rawfile, &raw_params, false)?; let metadata = decoder.raw_metadata(rawfile, &raw_params)?; log::info!( "DNG conversion: '{}', make: {}, model: {}, raw-image-count: {}", original_filename.as_ref(), rawimage.clean_make, rawimage.clean_model, decoder.raw_image_count()? ); if params.apply_scaling { rawimage.apply_scaling()?; } log::debug!("wb coeff: {:?}", rawimage.wb_coeffs); let mut dng = DngWriter::new(dng, DNG_VERSION_V1_4)?; // Write RAW image for subframe type 0 // If no thumbnail should be written to root IFD, we need to put the raw image into // root IFD instead. let mut raw = if params.thumbnail { dng.subframe(0) } else { dng.subframe_on_root(0) }; raw.raw_image(&rawimage, params.crop, params.compression, params.photometric_conversion, params.predictor)?; // Check for DNG raw IFD related tags if let Some(dng_raw_ifd) = decoder.ifd(WellKnownIFD::VirtualDngRawTags)? { raw.ifd_mut().copy(dng_raw_ifd.value_iter()); } raw.finalize()?; // Write preview and thumbnail if requested if params.preview || params.thumbnail { match generate_preview(rawfile, decoder.as_ref(), &rawimage, &raw_params) { Ok(image) => { if params.preview { let mut preview = dng.subframe(1); preview.preview(&image, PREVIEW_JPEG_QUALITY)?; preview.finalize()?; } if params.thumbnail { dng.thumbnail(&image)?; } } Err(err) => log::warn!("Failed to get review image, continue anyway: {:?}", err), } } // Write metadata dng.load_base_tags(&rawimage)?; dng.load_metadata(&metadata)?; if !dng.root_ifd().contains(ExifTag::Orientation) { dng.root_ifd_mut().add_tag(ExifTag::Orientation, rawimage.orientation.to_u16()); } // Check for DNG root IFD related tags if let Some(dng_root_ifd) = decoder.ifd(WellKnownIFD::VirtualDngRootTags)? { dng.root_ifd_mut().copy(dng_root_ifd.value_iter()); } // Check for TIFF root IFD related tags if let Some(tiff_root) = decoder.ifd(WellKnownIFD::Root)? { dng.root_ifd_mut().copy(tiff_root.value_iter().filter(|(tag, _)| { [ // Tags from CinemaDNG files TiffCommonTag::TimeCodes as u16, TiffCommonTag::FrameFrate as u16, TiffCommonTag::TStop as u16, ] .contains(tag) })); } // Remove makernotes from EXIF if MakerNoteSafety is not 1 (safe) if let Some(Entry { value: crate::formats::tiff::Value::Short(v), .. }) = decoder .ifd(WellKnownIFD::VirtualDngRootTags)? .and_then(|ifd| ifd.get_entry(DngTag::MakerNoteSafety).cloned()) { if v.get(0).copied().unwrap_or(0) == 0 { dng.exif_ifd_mut().remove_tag(ExifTag::MakerNotes); } } if let Some(xpacket) = decoder.xpacket(rawfile, &raw_params)? { dng.xpacket(&xpacket)?; } if let Some(handle) = original_compress_thread { let original = handle .join() .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to join compression thread: {:?}", err)))??; dng.original_file(&original, original_filename)?; } if let Some(artist) = ¶ms.artist { dng.root_ifd_mut().add_tag(TiffCommonTag::Artist, artist); } dng.root_ifd_mut().add_tag(TiffCommonTag::Software, ¶ms.software); dng .root_ifd_mut() .add_tag(ExifTag::ModifyDate, chrono::Local::now().format("%Y:%m:%d %H:%M:%S").to_string()); dng.close()?; Ok(()) } fn generate_preview(rawfile: &RawSource, decoder: &dyn Decoder, rawimage: &RawImage, params: &RawDecodeParams) -> crate::Result { let image = match decoder.full_image(rawfile, params)? { Some(image) => Ok(image), None => { log::warn!("Preview image not found, try to generate sRGB from RAW"); let dev = RawDevelop::default(); let image = dev.develop_intermediate(rawimage)?; image .to_dynamic_image() .ok_or_else(|| RawlerError::DecoderFailed("Failed to generate preview image".to_string())) } }?; log::debug!("Using preview image with source dimension {}x{}", image.width(), image.height()); Ok(image) }