use std::fs::File;
use std::io::{BufWriter, Seek, Write};
use std::path::Path;
use crate::core::image::RgbImage;
use crate::core::metadata::ImageMetadata;
use crate::error::RawResult;
use crate::tiff::writer::{IfdEntry, TiffWriter};
use crate::tiff::{ByteOrder, TiffTag};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DngExportConfig {
pub software: Option<String>,
pub embed_exif: bool,
pub embed_gps: bool,
}
impl DngExportConfig {
pub fn archival() -> Self {
Self {
software: Some(format!("rawshift {}", env!("CARGO_PKG_VERSION"))),
embed_exif: true,
embed_gps: true,
}
}
}
pub fn export_dng_to_writer<W: Write + Seek>(
writer: W,
image: &RgbImage,
metadata: &ImageMetadata,
config: &DngExportConfig,
) -> RawResult<()> {
let mut writer = TiffWriter::new(writer, ByteOrder::LittleEndian);
writer.write_header()?;
let (strip_offset, strip_bytes) = writer.write_image_strip_rgb16(&image.data)?;
let mut entries = build_dng_ifd(image, metadata, config, strip_offset, strip_bytes);
let ifd_offset = writer.write_ifd(&mut entries, 0)?;
writer.update_ifd0_offset(ifd_offset as u32)?;
Ok(())
}
pub fn export_dng(
path: &Path,
image: &RgbImage,
metadata: &ImageMetadata,
config: &DngExportConfig,
) -> RawResult<()> {
let file = File::create(path)?;
export_dng_to_writer(BufWriter::new(file), image, metadata, config)
}
fn build_dng_ifd(
image: &RgbImage,
metadata: &ImageMetadata,
config: &DngExportConfig,
strip_offset: u64,
strip_bytes: u64,
) -> Vec<IfdEntry> {
let mut entries = vec![
IfdEntry::long(TiffTag::ImageWidth, image.width()),
IfdEntry::long(TiffTag::ImageLength, image.height()),
IfdEntry::shorts(TiffTag::BitsPerSample, &[16, 16, 16]),
IfdEntry::short(TiffTag::Compression, 1), IfdEntry::short(TiffTag::PhotometricInterpretation, 2), IfdEntry::short(TiffTag::SamplesPerPixel, 3),
IfdEntry::long(TiffTag::RowsPerStrip, image.height()),
IfdEntry::long(TiffTag::StripOffsets, strip_offset as u32),
IfdEntry::long(TiffTag::StripByteCounts, strip_bytes as u32),
IfdEntry::short(TiffTag::PlanarConfiguration, 1), IfdEntry::rational(TiffTag::XResolution, 72, 1),
IfdEntry::rational(TiffTag::YResolution, 72, 1),
IfdEntry::short(TiffTag::ResolutionUnit, 2), ];
if let Some(orient) = metadata.image.orientation {
entries.push(IfdEntry::short(TiffTag::Orientation, orient));
} else {
entries.push(IfdEntry::short(TiffTag::Orientation, 1)); }
entries.push(IfdEntry::bytes(TiffTag::DNGVersion, &[1, 7, 0, 0]));
entries.push(IfdEntry::bytes(TiffTag::DNGBackwardVersion, &[1, 7, 0, 0]));
if !metadata.camera.make.is_empty() {
entries.push(IfdEntry::ascii(TiffTag::Make, &metadata.camera.make));
}
if !metadata.camera.model.is_empty() {
entries.push(IfdEntry::ascii(TiffTag::Model, &metadata.camera.model));
}
let unique_model = metadata
.camera
.unique_camera_model
.as_deref()
.unwrap_or_else(|| {
if !metadata.camera.model.is_empty() {
&metadata.camera.model
} else {
"Unknown Camera"
}
});
entries.push(IfdEntry::ascii(TiffTag::UniqueCameraModel, unique_model));
if let Some(ref sw) = config.software {
entries.push(IfdEntry::ascii(TiffTag::Software, sw));
}
if let Some(cm1) = &metadata.dng_color.color_matrix_1 {
let rationals: Vec<(i32, i32)> = cm1
.iter()
.map(|&v| ((v * 10000.0).round() as i32, 10000))
.collect();
entries.push(IfdEntry::srationals(TiffTag::ColorMatrix1, &rationals));
} else {
let default_cm: Vec<(i32, i32)> = [1.0f64, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
.iter()
.map(|&v| ((v * 10000.0).round() as i32, 10000))
.collect();
entries.push(IfdEntry::srationals(TiffTag::ColorMatrix1, &default_cm));
}
if let Some(ill1) = metadata.dng_color.calibration_illuminant_1 {
entries.push(IfdEntry::short(TiffTag::CalibrationIlluminant1, ill1));
} else {
entries.push(IfdEntry::short(TiffTag::CalibrationIlluminant1, 21)); }
if let Some(cm2) = &metadata.dng_color.color_matrix_2 {
let rationals: Vec<(i32, i32)> = cm2
.iter()
.map(|&v| ((v * 10000.0).round() as i32, 10000))
.collect();
entries.push(IfdEntry::srationals(TiffTag::ColorMatrix2, &rationals));
if let Some(ill2) = metadata.dng_color.calibration_illuminant_2 {
entries.push(IfdEntry::short(TiffTag::CalibrationIlluminant2, ill2));
} else {
entries.push(IfdEntry::short(TiffTag::CalibrationIlluminant2, 17)); }
}
if let Some(neutral) = &metadata.dng_color.as_shot_neutral {
let rationals: Vec<(u32, u32)> = neutral
.iter()
.map(|&v| ((v * 1000000.0).round() as u32, 1000000))
.collect();
entries.push(IfdEntry::rationals(TiffTag::AsShotNeutral, &rationals));
}
if let Some(balance) = &metadata.dng_color.analog_balance {
let rationals: Vec<(u32, u32)> = balance
.iter()
.map(|&v| ((v * 1000000.0).round() as u32, 1000000))
.collect();
entries.push(IfdEntry::rationals(TiffTag::AnalogBalance, &rationals));
}
if let Some(be) = metadata.dng_calibration.baseline_exposure {
let int_part = (be * 100.0).round() as i32;
entries.push(IfdEntry::srational(
TiffTag::BaselineExposure,
int_part,
100,
));
}
if !metadata.image.black_levels.is_empty() {
let black: Vec<(u32, u32)> = metadata
.image
.black_levels
.iter()
.take(3)
.map(|&v| (v, 1))
.collect();
if black.len() == 3 {
entries.push(IfdEntry::rationals(TiffTag::BlackLevel, &black));
}
}
if let Some(wl) = metadata.image.white_level {
entries.push(IfdEntry::longs(TiffTag::WhiteLevel, &[wl, wl, wl]));
} else {
entries.push(IfdEntry::longs(TiffTag::WhiteLevel, &[65535, 65535, 65535]));
}
entries
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::image::RgbImage;
#[test]
fn test_build_dng_ifd() {
let image = RgbImage::new(100, 50, vec![0u16; 100 * 50 * 3]);
let metadata = ImageMetadata::default();
let config = DngExportConfig::archival();
let entries = build_dng_ifd(&image, &metadata, &config, 1024, 30000);
assert!(
entries
.iter()
.any(|e| e.tag == TiffTag::ImageWidth.as_u16())
);
assert!(
entries
.iter()
.any(|e| e.tag == TiffTag::ImageLength.as_u16())
);
assert!(
entries
.iter()
.any(|e| e.tag == TiffTag::DNGVersion.as_u16())
);
assert!(
entries
.iter()
.any(|e| e.tag == TiffTag::UniqueCameraModel.as_u16())
);
assert!(
entries
.iter()
.any(|e| e.tag == TiffTag::ColorMatrix1.as_u16())
);
}
}