Files
dtourolle 77a1925bac Vendor rawler 0.7.2 unmodified
The crate as crates.io publishes it, minus .cargo-ok, its Cargo.lock and
data/testdata (13 MB of sample files only its own tests read). Not yet
routed through [patch.crates-io]; the next commit is the patch.
2026-09-27 17:33:19 -04:00

232 lines
7.2 KiB
Rust

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<String>,
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<W: Write + Seek + Send>(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<W>(raw_source: &RawSource, dng: &mut W, original_filename: impl AsRef<str>, 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<W>(
rawfile: &RawSource,
dng: &mut W,
original_filename: impl AsRef<str>,
original_compress_thread: Option<JoinHandle<Result<OriginalCompressed, std::io::Error>>>,
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) = &params.artist {
dng.root_ifd_mut().add_tag(TiffCommonTag::Artist, artist);
}
dng.root_ifd_mut().add_tag(TiffCommonTag::Software, &params.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<DynamicImage> {
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)
}