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.
232 lines
7.2 KiB
Rust
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) = ¶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<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)
|
|
}
|