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.
This commit is contained in:
2026-09-27 17:33:19 -04:00
parent 0d9feb0556
commit 77a1925bac
940 changed files with 66954 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
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)
}
+114
View File
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: LGPL-2.1
// Copyright 2023 Daniel Vogelbacher <daniel@chaospixel.com>
pub mod convert;
pub mod original;
pub mod writer;
use crate::imgop::Rect;
pub const DNG_VERSION_V1_0: [u8; 4] = [1, 0, 0, 0];
pub const DNG_VERSION_V1_1: [u8; 4] = [1, 1, 0, 0];
pub const DNG_VERSION_V1_2: [u8; 4] = [1, 2, 0, 0];
pub const DNG_VERSION_V1_3: [u8; 4] = [1, 3, 0, 0];
pub const DNG_VERSION_V1_4: [u8; 4] = [1, 4, 0, 0];
pub const DNG_VERSION_V1_5: [u8; 4] = [1, 5, 0, 0];
pub const DNG_VERSION_V1_6: [u8; 4] = [1, 6, 0, 0];
/// Convert internal crop rectangle to DNG active area
///
/// DNG ActiveArea is:
/// Top, Left, Bottom, Right
pub fn rect_to_dng_area(area: &Rect) -> [u16; 4] {
[
area.p.y as u16,
area.p.x as u16,
area.p.y as u16 + area.d.h as u16,
area.p.x as u16 + area.d.w as u16,
]
/*
[
image.crops[0] as u16, // top
image.crops[3] as u16, // left
//(image.height-image.crops[0]-image.crops[2]) as u16, // bottom
//(image.width-image.crops[1]-image.crops[3]) as u16, // Right
(image.height - (image.crops[2])) as u16, // bottom coord
(image.width - (image.crops[1])) as u16, // Right coord
]
*/
}
#[cfg(feature = "clap")]
impl clap::ValueEnum for DngCompression {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Lossless, Self::Uncompressed]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(match self {
Self::Uncompressed => clap::builder::PossibleValue::new("uncompressed"),
Self::Lossless => clap::builder::PossibleValue::new("lossless"),
})
}
}
#[derive(Clone, Copy, Debug)]
pub enum DngPhotometricConversion {
Original,
Linear,
}
impl Default for DngPhotometricConversion {
fn default() -> Self {
Self::Original
}
}
#[derive(Clone, Copy, Debug)]
pub enum CropMode {
Best,
ActiveArea,
None,
}
#[cfg(feature = "clap")]
impl clap::ValueEnum for CropMode {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Best, Self::ActiveArea, Self::None]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(match self {
Self::Best => clap::builder::PossibleValue::new("best"),
Self::ActiveArea => clap::builder::PossibleValue::new("activearea"),
Self::None => clap::builder::PossibleValue::new("none"),
})
}
}
/*
impl FromStr for CropMode {
type Err = String;
fn from_str(mode: &str) -> std::result::Result<Self, Self::Err> {
Ok(match mode {
"best" => Self::Best,
"activearea" => Self::ActiveArea,
"none" => Self::None,
_ => return Err(format!("Unknown CropMode value: {}", mode)),
})
}
}
*/
/// Quality of preview images
const PREVIEW_JPEG_QUALITY: f32 = 0.75;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Compression mode for DNG
pub enum DngCompression {
/// No compression is applied
Uncompressed,
/// Lossless JPEG-92 compression
Lossless,
// Lossy
}
+246
View File
@@ -0,0 +1,246 @@
// SPDX-License-Identifier: LGPL-2.1
// Copyright 2021 Daniel Vogelbacher <daniel@chaospixel.com>
use byteorder::{BigEndian, ReadBytesExt};
use libflate::zlib::{Decoder, EncodeOptions, Encoder};
use log::debug;
use rayon::prelude::*;
use std::{
io::{self, Read, Seek, SeekFrom, Write},
mem::size_of,
ops::Neg,
};
// DNG requires this block size
const COMPRESS_BLOCK_SIZE: u32 = 65536;
pub type OriginalDigest = [u8; 16];
pub struct OriginalCompressed {
raw_fork_size: u32,
chunks: Vec<ForkBlock>,
digest: Option<OriginalDigest>,
}
impl OriginalCompressed {
pub fn new<T>(stream: &mut T, digest: Option<OriginalDigest>) -> io::Result<Self>
where
T: Read + Seek,
{
let start = stream.stream_position()?;
let raw_fork_size: u32 = stream.read_u32::<BigEndian>()?;
let raw_fork_blocks: u32 = raw_fork_size.div_ceil(COMPRESS_BLOCK_SIZE); // (raw_fork_size + (COMPRESS_BLOCK_SIZE - 1)) / COMPRESS_BLOCK_SIZE
let mut index_list: Vec<u32> = Vec::with_capacity(raw_fork_blocks as usize + 1);
for _ in 0..raw_fork_blocks + 1 {
let idx = stream.read_u32::<BigEndian>()?;
index_list.push(idx);
}
let mut chunks = Vec::with_capacity(index_list.len());
let mut iter = index_list.into_iter().map(u64::from);
if let Some(mut offset) = iter.next() {
stream.seek(SeekFrom::Start(start + offset))?;
for end in iter {
let len = end
.checked_sub(offset)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Offset underflow"))?;
let mut chunk = vec![0; len as usize];
stream.read_exact(&mut chunk)?;
chunks.push(ForkBlock::new(chunk));
offset = end;
}
}
Ok(Self { chunks, raw_fork_size, digest })
}
pub fn decompress<T>(&self, stream: &mut T, verify_digest: bool) -> io::Result<usize>
where
T: Write,
{
let mut ctx = md5::Context::new();
let mut total = 0;
for chunk in self.chunks.iter().map(ForkBlock::decompress) {
let buf = chunk?;
stream.write_all(&buf)?;
total = buf.len();
ctx.consume(&buf);
}
let new_digest = ctx.finalize().into();
debug!("Encoded calculated original data digest: {:x?}", self.digest);
debug!("New calculated original data digest: {:x?}", new_digest);
if self.digest.ne(&Some(new_digest)) {
if verify_digest {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Embedded original digest and output digest mismatch, data may be corrupt",
));
} else {
log::warn!("Embedded original digest and output digest mismatch, data may be corrupt, but verify checks are disabled");
}
}
Ok(total)
}
/// Read bytes from stream until EOF, split into chunks
/// and compress each one.
pub fn compress<T>(stream: &mut T) -> io::Result<Self>
where
T: Seek + Read,
{
let pos = stream.stream_position()?;
stream.seek(SeekFrom::End(0))?;
let uncomp_len = stream.stream_position()? - pos;
stream.seek(SeekFrom::Current((uncomp_len as i64).neg()))?;
let raw_fork_size = u32::try_from(uncomp_len).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let raw_fork_blocks = raw_fork_size.div_ceil(COMPRESS_BLOCK_SIZE); // (raw_fork_size + (COMPRESS_BLOCK_SIZE - 1)) / COMPRESS_BLOCK_SIZE
let mut forks = Vec::with_capacity(raw_fork_blocks as usize);
let mut ctx = md5::Context::new();
loop {
let mut buf = Vec::with_capacity(COMPRESS_BLOCK_SIZE as usize);
stream.take(COMPRESS_BLOCK_SIZE as u64).read_to_end(&mut buf)?;
if buf.is_empty() {
break;
}
ctx.consume(&buf);
forks.push(buf);
//chunks.push(ForkBlock::compress(&buf)?);
}
let chunks = forks.par_iter().flat_map(ForkBlock::compress).collect();
let digest = Some(ctx.finalize().into());
Ok(Self { raw_fork_size, chunks, digest })
}
pub fn digest(&self) -> Option<OriginalDigest> {
self.digest
}
/// Write compressed chunks to output stream.
pub fn write_to_stream<T>(&self, stream: &mut T) -> io::Result<()>
where
T: Write,
{
stream.write_all(&self.raw_fork_size.to_be_bytes())?; // Fork 1
let chunks_start: u32 = (size_of::<u32>() + (self.chunks.len() + 1) * size_of::<u32>()) as u32;
// Offset of first chunk
stream.write_all(&chunks_start.to_be_bytes())?;
// Write all other end offsets.
for end in self.chunks.iter().map(ForkBlock::len).scan(chunks_start, |end, len| {
*end += len as u32;
Some(*end)
}) {
stream.write_all(&end.to_be_bytes())?;
}
for chunk in self.chunks.iter() {
stream.write_all(&chunk.chunk)?;
}
stream.write_all(&0u32.to_be_bytes())?;
stream.write_all(&0u32.to_be_bytes())?;
stream.write_all(&0u32.to_be_bytes())?;
stream.write_all(&0u32.to_be_bytes())?;
stream.write_all(&0u32.to_be_bytes())?;
stream.write_all(&0u32.to_be_bytes())?;
stream.write_all(&0u32.to_be_bytes())?;
Ok(())
}
}
/// Single chunk for compressed data
struct ForkBlock {
/// Compressed data for block
chunk: Vec<u8>,
}
impl ForkBlock {
fn new(chunk: Vec<u8>) -> Self {
Self { chunk }
}
fn len(&self) -> usize {
self.chunk.len()
}
fn compress(buf: impl AsRef<[u8]>) -> io::Result<Self> {
let mut encoder = Encoder::with_options(
Vec::with_capacity(COMPRESS_BLOCK_SIZE as usize),
EncodeOptions::new().block_size(COMPRESS_BLOCK_SIZE as usize),
)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
encoder.write_all(buf.as_ref()).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
Ok(ForkBlock {
chunk: encoder.finish().into_result().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?,
})
}
fn decompress(&self) -> io::Result<Vec<u8>> {
let mut decoder = Decoder::new(self.chunk.as_slice()).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let mut chunk = Vec::new();
decoder.read_to_end(&mut chunk).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
Ok(chunk)
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
#[test]
fn empty_data() -> std::result::Result<(), Box<dyn std::error::Error>> {
//let data = [0x00, 0xFF, 0xDD];
let data = [];
let mut file = Cursor::new(data);
// Compress
let orig = OriginalCompressed::compress(&mut file)?;
let digest = orig.digest;
let mut out = Cursor::new(Vec::new());
orig.write_to_stream(&mut out)?;
out.seek(SeekFrom::Start(0))?;
// Reload
let comp = OriginalCompressed::new(&mut out, digest)?;
// Decompress
let mut restored = Cursor::new(Vec::new());
comp.decompress(&mut restored, true)?;
// Compare
let unpacked = restored.into_inner();
assert_eq!(unpacked, data);
assert_eq!(digest, Some(md5::compute(&unpacked).into()));
Ok(())
}
#[test]
fn dummy_data() -> std::result::Result<(), Box<dyn std::error::Error>> {
let data = [0x00, 0xFF, 0xDD, 0x00, 0x00];
let mut file = Cursor::new(data);
// Compress
let orig = OriginalCompressed::compress(&mut file)?;
let digest = orig.digest;
let mut out = Cursor::new(Vec::new());
orig.write_to_stream(&mut out)?;
out.seek(SeekFrom::Start(0))?;
// Reload
let comp = OriginalCompressed::new(&mut out, digest)?;
// Decompress
let mut restored = Cursor::new(Vec::new());
comp.decompress(&mut restored, true)?;
// Compare
let unpacked = restored.into_inner();
assert_eq!(unpacked, data);
assert_eq!(digest, Some(md5::compute(&unpacked).into()));
Ok(())
}
}
+784
View File
@@ -0,0 +1,784 @@
use std::{
borrow::Cow,
io::{self, Seek, Write},
mem::size_of,
time::Instant,
};
use image::{DynamicImage, codecs::jpeg::JpegEncoder, imageops::FilterType};
use log::debug;
use rayon::prelude::*;
use crate::{
CFA, RawImage, RawImageData,
decoders::{Camera, RawMetadata},
dng::rect_to_dng_area,
envparams::{rawler_dng_multistrip_threshold, rawler_dng_rows_per_strip},
formats::tiff::{
CompressionMethod, PhotometricInterpretation, PreviewColorSpace, Rational, TiffError, Value,
writer::{DirectoryWriter, TiffWriter, transfer_entry},
},
imgop::{Dim2, Point, Rect},
ljpeg92::LjpegCompressor,
pixarray::PixU16,
rawimage::{BlackLevel, RawPhotometricInterpretation, WhiteLevel},
tags::ExifTag,
tiles::ImageTiler,
};
use crate::{
formats::tiff::SRational,
imgop::xyz::Illuminant,
tags::{DngTag, TiffCommonTag},
};
use super::{CropMode, DNG_VERSION_V1_6, DngCompression, DngPhotometricConversion, original::OriginalCompressed};
pub type DngError = TiffError;
pub type Result<T> = std::result::Result<T, DngError>;
pub struct DngWriter<B>
where
B: Write + Seek,
{
pub dng: TiffWriter<B>,
root_ifd: DirectoryWriter,
//raw_ifd: DirectoryWriter,
//preview_ifd: DirectoryWriter,
exif_ifd: DirectoryWriter,
subs: Vec<u32>,
}
pub struct SubFrameWriter<'w, B>
where
B: Write + Seek,
{
writer: &'w mut DngWriter<B>,
ifd: Option<DirectoryWriter>,
}
impl<'w, B> SubFrameWriter<'w, B>
where
B: Write + Seek,
{
pub fn new(writer: &'w mut DngWriter<B>, subtype: u32, use_root: bool) -> Self {
let ifd = if use_root {
writer.root_ifd_mut().add_tag(TiffCommonTag::NewSubFileType, subtype);
None
} else {
let mut ifd = DirectoryWriter::new();
ifd.add_tag(TiffCommonTag::NewSubFileType, subtype);
Some(ifd)
};
Self { ifd, writer }
}
pub fn ifd(&mut self) -> &DirectoryWriter {
self.ifd.as_ref().unwrap_or(self.writer.root_ifd())
}
pub fn ifd_mut(&mut self) -> &mut DirectoryWriter {
self.ifd.as_mut().unwrap_or(self.writer.root_ifd_mut())
}
pub fn rgb_image_u8(&mut self, data: &[u8], width: usize, height: usize, compression: DngCompression, predictor: u8) -> Result<()> {
let cpp = 3;
let rawimagedata = PixU16::new_with(data.iter().copied().map(u16::from).collect(), width * cpp, height);
let mut cam = Camera::new();
cam.cfa = CFA::new("RGGB");
let wb_coeffs = [1.0, 1.0, 1.0, 1.0];
let blacklevel = Some(BlackLevel::new(&[0_u32, 0, 0], 1, 1, 3));
let whitelevel = Some(WhiteLevel::new_bits(8, cpp));
let photometric = RawPhotometricInterpretation::LinearRaw;
let rawimage = RawImage::new(cam, rawimagedata, cpp, wb_coeffs, photometric, blacklevel, whitelevel, false);
self.raw_image(&rawimage, CropMode::None, compression, DngPhotometricConversion::Original, predictor)
}
pub fn rgb_image_u16(&mut self, data: &[u16], width: usize, height: usize, compression: DngCompression, predictor: u8) -> Result<()> {
let cpp = 3;
let rawimagedata = PixU16::new_with(data.to_vec(), width * cpp, height);
let mut cam = Camera::new();
cam.cfa = CFA::new("RGGB");
let wb_coeffs = [1.0, 1.0, 1.0, 1.0];
let blacklevel = Some(BlackLevel::new(&[0_u32, 0, 0], 1, 1, 3));
let whitelevel = Some(WhiteLevel::new_bits(16, cpp));
let photometric = RawPhotometricInterpretation::LinearRaw;
let rawimage = RawImage::new(cam, rawimagedata, cpp, wb_coeffs, photometric, blacklevel, whitelevel, false);
self.raw_image(&rawimage, CropMode::None, compression, DngPhotometricConversion::Original, predictor)
}
pub fn image(&mut self, _image: &RawImageData, _width: u16, _height: u16) -> Result<()> {
todo!()
}
pub fn raw_image(
&mut self,
rawimage: &RawImage,
cropmode: CropMode,
compression: DngCompression,
photometric_conversion: DngPhotometricConversion,
predictor: u8,
) -> Result<()> {
match photometric_conversion {
DngPhotometricConversion::Original => self.write_rawimage(Cow::Borrowed(rawimage), cropmode, compression, predictor)?,
DngPhotometricConversion::Linear => {
if rawimage.cpp == 3 {
self.write_rawimage(Cow::Borrowed(rawimage), cropmode, compression, predictor)?;
} else {
let rawimage = rawimage.linearize().unwrap(); // TODO: implement me
self.write_rawimage(Cow::Borrowed(&rawimage), cropmode, compression, predictor)?;
}
}
}
/*
for (tag, value) in rawimage.dng_tags.iter() {
self.ifd.add_untyped_tag(*tag, value.clone())?;
}
*/
Ok(())
}
fn write_rawimage(&mut self, mut rawimage: Cow<RawImage>, cropmode: CropMode, compression: DngCompression, predictor: u8) -> Result<()> {
if compression == DngCompression::Lossless && matches!(rawimage.data, RawImageData::Float(_)) {
// Lossless (LJPEG92) can only be used for 16 bit integer data.
// If we have floats, convert them.
rawimage.to_mut().data.force_integer();
rawimage.to_mut().whitelevel.0.iter_mut().for_each(|x| *x = u16::MAX as u32);
rawimage.to_mut().bps = 16; // Reset bps as intgers are scaled to u16 range.
}
if rawimage.cpp > 1 || matches!(rawimage.photometric, RawPhotometricInterpretation::Cfa(_)) {
self.writer.as_shot_neutral(wbcoeff_to_tiff_value(&rawimage));
// Add matrix and illumninant
let mut available_matrices = rawimage.color_matrix.clone();
if let Some(first_key) = available_matrices.keys().next().cloned() {
let first_matrix = available_matrices
.remove_entry(&Illuminant::A)
.or_else(|| available_matrices.remove_entry(&Illuminant::A))
.or_else(|| available_matrices.remove_entry(&first_key))
.expect("No matrix found");
self
.writer
.color_matrix(1, first_matrix.0, matrix_to_tiff_value(&first_matrix.1, 10_000).as_slice());
if let Some(second_matrix) = available_matrices
.remove_entry(&Illuminant::D65)
.or_else(|| available_matrices.remove_entry(&Illuminant::D50))
{
self
.writer
.color_matrix(2, second_matrix.0, matrix_to_tiff_value(&second_matrix.1, 10_000).as_slice());
}
}
}
let full_size = Rect::new(Point::new(0, 0), Dim2::new(rawimage.width, rawimage.height));
// Active area or uncropped
let active_area: Rect = match cropmode {
CropMode::ActiveArea | CropMode::Best => rawimage.active_area.unwrap_or(full_size),
CropMode::None => full_size,
};
assert!(active_area.p.x + active_area.d.w <= rawimage.width);
assert!(active_area.p.y + active_area.d.h <= rawimage.height);
//self.ifd.add_tag(TiffCommonTag::NewSubFileType, 0_u16)?; // Raw
self.ifd_mut().add_tag(TiffCommonTag::ImageWidth, rawimage.width as u32);
self.ifd_mut().add_tag(TiffCommonTag::ImageLength, rawimage.height as u32);
self.ifd_mut().add_tag(DngTag::ActiveArea, rect_to_dng_area(&active_area));
match cropmode {
CropMode::ActiveArea => {
let crop = active_area;
assert!(crop.p.x >= active_area.p.x);
assert!(crop.p.y >= active_area.p.y);
self.ifd_mut().add_tag(
DngTag::DefaultCropOrigin,
[(crop.p.x - active_area.p.x) as u16, (crop.p.y - active_area.p.y) as u16],
);
self.ifd_mut().add_tag(DngTag::DefaultCropSize, [crop.d.w as u16, crop.d.h as u16]);
}
CropMode::Best => {
let crop = rawimage.crop_area.unwrap_or(active_area);
assert!(crop.p.x >= active_area.p.x);
assert!(crop.p.y >= active_area.p.y);
self.ifd_mut().add_tag(
DngTag::DefaultCropOrigin,
[(crop.p.x - active_area.p.x) as u16, (crop.p.y - active_area.p.y) as u16],
);
self.ifd_mut().add_tag(DngTag::DefaultCropSize, [crop.d.w as u16, crop.d.h as u16]);
}
CropMode::None => {}
}
self.ifd_mut().add_tag(ExifTag::PlanarConfiguration, 1_u16);
self.ifd_mut().add_tag(
DngTag::DefaultScale,
[
Rational::new(rawimage.camera.default_scale.0[0][0], rawimage.camera.default_scale.0[0][1]),
Rational::new(rawimage.camera.default_scale.0[1][0], rawimage.camera.default_scale.0[1][1]),
],
);
self.ifd_mut().add_tag(
DngTag::BestQualityScale,
Rational::new(rawimage.camera.best_quality_scale.0[0], rawimage.camera.best_quality_scale.0[1]),
);
// Whitelevel
assert_eq!(rawimage.whitelevel.0.len(), rawimage.cpp, "Whitelevel sample count must match cpp");
if rawimage.whitelevel.0.iter().all(|x| *x <= (u16::MAX as u32)) {
// Add as u16
self
.ifd_mut()
.add_tag(DngTag::WhiteLevel, &rawimage.whitelevel.0.iter().map(|x| *x as u16).collect::<Vec<u16>>());
} else {
self.ifd_mut().add_tag(DngTag::WhiteLevel, &rawimage.whitelevel.0);
}
// Blacklevel
let blacklevel = rawimage.blacklevel.shift(active_area.p.x, active_area.p.y);
self
.ifd_mut()
.add_tag(DngTag::BlackLevelRepeatDim, [blacklevel.height as u16, blacklevel.width as u16]);
if blacklevel.levels.iter().all(|x| x.d == 1) {
let payload: Vec<u32> = blacklevel.levels.iter().map(|x| x.n as u32).collect();
if payload.iter().all(|x| *x <= (u16::MAX as u32)) {
// Add as u16
self
.ifd_mut()
.add_tag(DngTag::BlackLevel, &payload.into_iter().map(|x| x as u16).collect::<Vec<u16>>());
} else {
// Add as u32
self.ifd_mut().add_tag(DngTag::BlackLevel, &payload);
}
} else {
// Add as RATIONAL
self.ifd_mut().add_tag(DngTag::BlackLevel, blacklevel.levels.as_slice());
}
if !rawimage.blackareas.is_empty() {
let data: Vec<u16> = rawimage.blackareas.iter().flat_map(rect_to_dng_area).collect();
self.ifd_mut().add_tag(DngTag::MaskedAreas, &data);
}
self.ifd_mut().add_tag(TiffCommonTag::SamplesPerPixel, rawimage.cpp as u16);
match &rawimage.photometric {
RawPhotometricInterpretation::BlackIsZero => {
assert_eq!(rawimage.cpp, 1);
self.ifd_mut().add_tag(TiffCommonTag::PhotometricInt, PhotometricInterpretation::BlackIsZero);
}
RawPhotometricInterpretation::Cfa(config) => {
assert!(config.cfa.is_valid());
assert_eq!(rawimage.cpp, 1);
let cfa = config.cfa.shift(active_area.p.x, active_area.p.y);
self
.ifd_mut()
.add_tag(TiffCommonTag::CFARepeatPatternDim, [cfa.width as u16, cfa.height as u16]);
self.ifd_mut().add_tag(TiffCommonTag::CFAPattern, &cfa.flat_pattern()[..]);
self.ifd_mut().add_tag(TiffCommonTag::PhotometricInt, PhotometricInterpretation::CFA);
self.ifd_mut().add_tag(DngTag::CFAPlaneColor, &config.colors);
self.ifd_mut().add_tag(DngTag::CFALayout, 1_u16); // Square layout
}
RawPhotometricInterpretation::LinearRaw => {
self.ifd_mut().add_tag(TiffCommonTag::PhotometricInt, PhotometricInterpretation::LinearRaw);
}
}
match compression {
DngCompression::Uncompressed => {
self.ifd_mut().add_tag(TiffCommonTag::Compression, CompressionMethod::None);
dng_put_raw_uncompressed(self, &rawimage)?;
}
DngCompression::Lossless => {
self.ifd_mut().add_tag(TiffCommonTag::Compression, CompressionMethod::ModernJPEG);
dng_put_raw_ljpeg(self, &rawimage, predictor)?;
}
}
/*
for (tag, value) in rawimage.dng_tags.iter() {
self.ifd.add_untyped_tag(*tag, value.clone())?;
}
*/
Ok(())
}
pub fn preview(&mut self, img: &DynamicImage, quality: f32) -> Result<()> {
let now = Instant::now();
let preview_img = if img.width() > 1024 {
DynamicImage::ImageRgb8(img.resize(1024, 768, FilterType::Nearest).to_rgb8())
} else {
DynamicImage::ImageRgb8(img.to_rgb8())
};
debug!("preview downscale: {} s", now.elapsed().as_secs_f32());
self.ifd_mut().add_tag(TiffCommonTag::ImageWidth, Value::long(preview_img.width()));
self.ifd_mut().add_tag(TiffCommonTag::ImageLength, Value::long(preview_img.height()));
self.ifd_mut().add_tag(TiffCommonTag::Compression, CompressionMethod::ModernJPEG);
self.ifd_mut().add_tag(TiffCommonTag::BitsPerSample, [8_u16, 8, 8]);
self.ifd_mut().add_tag(TiffCommonTag::SampleFormat, [1_u16, 1, 1]);
self.ifd_mut().add_tag(TiffCommonTag::PhotometricInt, PhotometricInterpretation::YCbCr);
self.ifd_mut().add_tag(TiffCommonTag::RowsPerStrip, Value::long(preview_img.height()));
self.ifd_mut().add_tag(TiffCommonTag::SamplesPerPixel, 3_u16);
self.ifd_mut().add_tag(DngTag::PreviewColorSpace, PreviewColorSpace::SRgb); // ??
//ifd.add_tag(TiffRootTag::XResolution, Rational { n: 1, d: 1 })?;
//ifd.add_tag(TiffRootTag::YResolution, Rational { n: 1, d: 1 })?;
//ifd.add_tag(TiffRootTag::ResolutionUnit, ResolutionUnit::None.to_u16())?;
let now = Instant::now();
let offset = self.writer.dng.position()?;
// TODO: improve offsets?
let jpeg_encoder = JpegEncoder::new_with_quality(&mut self.writer.dng.writer, (quality * 100.0).max(100.0) as u8);
preview_img
.write_with_encoder(jpeg_encoder)
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("Failed to write jpeg preview: {:?}", err)))?;
let data_len = self.writer.dng.position()? - offset;
debug!("writing preview: {} s", now.elapsed().as_secs_f32());
self.ifd_mut().add_value(TiffCommonTag::StripOffsets, Value::Long(vec![offset]));
self.ifd_mut().add_tag(TiffCommonTag::StripByteCounts, Value::Long(vec![data_len]));
Ok(())
}
pub fn finalize(self) -> Result<()> {
if let Some(ifd) = self.ifd {
let offset = ifd.build(&mut self.writer.dng)?;
self.writer.subs.push(offset);
}
Ok(())
}
}
impl<B> DngWriter<B>
where
B: Write + Seek,
{
pub fn new(buf: B, backward_version: [u8; 4]) -> Result<Self> {
let dng = TiffWriter::new(buf)?;
let mut root_ifd = DirectoryWriter::new();
let mut exif_ifd = DirectoryWriter::new();
root_ifd.add_tag(DngTag::DNGBackwardVersion, backward_version);
root_ifd.add_tag(DngTag::DNGVersion, DNG_VERSION_V1_6);
// Add EXIF version 0220
exif_ifd.add_tag_undefined(ExifTag::ExifVersion, vec![48, 50, 50, 48]);
Ok(Self {
dng,
root_ifd,
exif_ifd,
subs: Vec::new(),
})
}
pub fn as_shot_neutral(&mut self, wb: impl AsRef<[Rational]>) {
// Only write tag if wb is valid
if wb.as_ref()[0].n != 0 {
self.root_ifd.add_tag(DngTag::AsShotNeutral, wb.as_ref());
}
}
pub fn color_matrix(&mut self, slot: usize, illu: Illuminant, matrix: impl AsRef<[SRational]>) {
match slot {
1 => {
self.root_ifd.add_tag(DngTag::CalibrationIlluminant1, u16::from(illu));
self.root_ifd.add_tag(DngTag::ColorMatrix1, matrix.as_ref());
}
2 => {
self.root_ifd.add_tag(DngTag::CalibrationIlluminant2, u16::from(illu));
self.root_ifd.add_tag(DngTag::ColorMatrix2, matrix.as_ref());
}
_ => todo!(),
}
}
pub fn load_metadata(&mut self, metadata: &RawMetadata) -> Result<()> {
metadata.write_exif_tags(&mut self.dng, &mut self.root_ifd, &mut self.exif_ifd)?;
// DNG has a lens info tag that is identical to the LensSpec tag in EXIF IFD
transfer_entry(&mut self.root_ifd, DngTag::LensInfo, &metadata.exif.lens_spec)?;
if let Some(id) = &metadata.unique_image_id {
self.root_ifd.add_tag(DngTag::RawDataUniqueID, id.to_le_bytes());
}
Ok(())
}
pub fn xpacket(&mut self, xpacket: impl AsRef<[u8]>) -> Result<()> {
self.root_ifd.add_tag(ExifTag::ApplicationNotes, xpacket.as_ref());
Ok(())
}
pub fn load_base_tags(&mut self, rawimage: &RawImage) -> Result<()> {
self.root_ifd.add_tag(TiffCommonTag::Make, rawimage.clean_make.as_str());
self.root_ifd.add_tag(TiffCommonTag::Model, rawimage.clean_model.as_str());
let uq_model = format!("{} {}", rawimage.clean_make, rawimage.clean_model);
self.root_ifd.add_tag(DngTag::UniqueCameraModel, uq_model.as_str());
Ok(())
}
pub fn close(mut self) -> Result<()> {
if !self.exif_ifd.is_empty() {
let exif_ifd_offset = self.exif_ifd.build(&mut self.dng)?;
self.root_ifd.add_tag(TiffCommonTag::ExifIFDPointer, exif_ifd_offset);
}
// Add SubIFDs
if !self.subs.is_empty() {
self.root_ifd.add_tag(TiffCommonTag::SubIFDs, &self.subs);
}
self.dng.build(self.root_ifd)?;
Ok(())
}
pub fn original_file(&mut self, original: &OriginalCompressed, filename: impl AsRef<str>) -> Result<()> {
let mut buf = std::io::Cursor::new(Vec::new());
original.write_to_stream(&mut buf)?;
self.root_ifd.add_tag_undefined(DngTag::OriginalRawFileData, buf.into_inner());
self.root_ifd.add_tag(DngTag::OriginalRawFileName, filename.as_ref());
if let Some(digest) = original.digest() {
self.root_ifd.add_tag(DngTag::OriginalRawFileDigest, digest);
}
Ok(())
}
pub fn subframe(&mut self, id: u32) -> SubFrameWriter<'_, B> {
SubFrameWriter::new(self, id, false)
}
pub fn subframe_on_root(&mut self, id: u32) -> SubFrameWriter<'_, B> {
SubFrameWriter::new(self, id, true)
}
/// Write thumbnail image into DNG
pub fn thumbnail(&mut self, img: &DynamicImage) -> Result<()> {
let thumb_img = img.resize(240, 120, FilterType::Nearest).to_rgb8();
self.root_ifd.add_tag(TiffCommonTag::NewSubFileType, 1_u32);
self.root_ifd.add_tag(TiffCommonTag::ImageWidth, thumb_img.width() as u32);
self.root_ifd.add_tag(TiffCommonTag::ImageLength, thumb_img.height() as u32);
self.root_ifd.add_tag(TiffCommonTag::Compression, CompressionMethod::None);
self.root_ifd.add_tag(TiffCommonTag::BitsPerSample, [8_u16, 8, 8]);
self.root_ifd.add_tag(TiffCommonTag::SampleFormat, [1_u16, 1, 1]);
self.root_ifd.add_tag(TiffCommonTag::PhotometricInt, PhotometricInterpretation::RGB);
self.root_ifd.add_tag(TiffCommonTag::SamplesPerPixel, 3_u16);
//ifd.add_tag(TiffRootTag::XResolution, Rational { n: 1, d: 1 })?;
//ifd.add_tag(TiffRootTag::YResolution, Rational { n: 1, d: 1 })?;
//ifd.add_tag(TiffRootTag::ResolutionUnit, ResolutionUnit::None.to_u16())?;
let offset = self.dng.write_data(&thumb_img)?;
self.root_ifd.add_tag(TiffCommonTag::StripOffsets, offset);
self.root_ifd.add_tag(TiffCommonTag::StripByteCounts, thumb_img.len() as u32);
self.root_ifd.add_tag(TiffCommonTag::RowsPerStrip, thumb_img.height() as u32);
Ok(())
}
pub fn root_ifd(&mut self) -> &DirectoryWriter {
&self.root_ifd
}
pub fn root_ifd_mut(&mut self) -> &mut DirectoryWriter {
&mut self.root_ifd
}
pub fn exif_ifd(&mut self) -> &DirectoryWriter {
&self.exif_ifd
}
pub fn exif_ifd_mut(&mut self) -> &mut DirectoryWriter {
&mut self.exif_ifd
}
}
/// DNG requires the WB values to be the reciprocal
fn wbcoeff_to_tiff_value(rawimage: &RawImage) -> Vec<Rational> {
let wb = &rawimage.wb_coeffs;
match &rawimage.photometric {
RawPhotometricInterpretation::BlackIsZero => {
vec![Rational::new(1, 1)] // TODO: is this useful?
}
RawPhotometricInterpretation::Cfa(config) => {
assert!([1, 3, 4].contains(&config.cfa.unique_colors()));
let mut values = Vec::with_capacity(4);
values.push(Rational::new_f32(1.0 / wb[0], 100000));
values.push(Rational::new_f32(1.0 / wb[1], 100000));
values.push(Rational::new_f32(1.0 / wb[2], 100000));
if config.cfa.unique_colors() == 4 {
values.push(Rational::new_f32(1.0 / wb[3], 100000));
}
values
}
RawPhotometricInterpretation::LinearRaw => {
//assert_eq!(rawimage.cpp, 3);
match rawimage.cpp {
1 => {
vec![Rational::new(1, 1)]
}
3 => {
let mut values = Vec::with_capacity(3);
values.push(Rational::new_f32(1.0 / wb[0], 100000));
values.push(Rational::new_f32(1.0 / wb[1], 100000));
values.push(Rational::new_f32(1.0 / wb[2], 100000));
values
}
_ => todo!(),
}
}
}
}
fn matrix_to_tiff_value(xyz_to_cam: &[f32], d: i32) -> Vec<SRational> {
xyz_to_cam.iter().map(|a| SRational::new((a * d as f32) as i32, d)).collect()
}
/// Compress RAW image with LJPEG-92
///
/// Data is split into multiple tiles, each tile is compressed seperately
///
/// Predictor mode 4,5,6,7 is best for images where two images
/// lines are merged, because then the image bayer pattern is:
/// RGRGGBGB
/// RGRGGBGB
/// Instead of the default:
/// RGRG
/// GBGB
/// RGRG
/// GBGB
fn dng_put_raw_ljpeg<W>(subframe: &mut SubFrameWriter<W>, rawimage: &RawImage, predictor: u8) -> Result<()>
where
W: Seek + Write,
{
let tile_w = 256 & !0b111; // ensure div 16
let tile_h = 256 & !0b111;
let lj92_data = match rawimage.data {
RawImageData::Integer(ref data) => {
// Inject black pixel data for testing purposes.
// let data = vec![0x0000; data.len()];
//let tiled_data = TiledData::new(&data, rawimage.width, rawimage.height, rawimage.cpp);
// Only merge two lines into one for higher predictors, if image is CFA
let tiled_data: Vec<Vec<u16>> = ImageTiler::new(data, rawimage.width, rawimage.height, rawimage.cpp, tile_w, tile_h).collect();
let (j_width, j_height, components, realign) = match &rawimage.photometric {
RawPhotometricInterpretation::BlackIsZero => {
assert_eq!(rawimage.cpp, 1);
(tile_w, tile_h, 1, 1)
}
RawPhotometricInterpretation::Cfa(config) => {
assert_eq!(rawimage.cpp, 1);
let realign = if (4..=7).contains(&predictor) && config.cfa.width == 2 && config.cfa.height == 2 {
2
} else {
1
};
(tile_w / 2, tile_h, 2, realign)
}
RawPhotometricInterpretation::LinearRaw => {
(tile_w, tile_h, rawimage.cpp, 1) /* RGB LinearRaw */
}
};
debug!("LJPEG compression: bit depth: {}", rawimage.bps);
let tiles_compr: Vec<Vec<u8>> = tiled_data
.par_iter()
.map(|tile| {
//assert_eq!((tile_w * rawimage.cpp) % components, 0);
//assert_eq!((tile_w * rawimage.cpp) % 2, 0);
//assert_eq!(tile_h % 2, 0);
let state = LjpegCompressor::new(tile, j_width * realign, j_height / realign, components, rawimage.bps as u8, predictor, 0, 0).unwrap();
state.encode().unwrap()
})
.collect();
tiles_compr
}
RawImageData::Float(ref _data) => {
panic!("invalid format");
}
};
let mut tile_offsets: Vec<u32> = Vec::new();
let mut tile_sizes: Vec<u32> = Vec::new();
lj92_data.iter().for_each(|tile| {
let offs = subframe.writer.dng.write_data(tile).unwrap();
tile_offsets.push(offs);
tile_sizes.push((tile.len() * size_of::<u8>()) as u32);
});
subframe
.ifd_mut()
.add_tag(TiffCommonTag::BitsPerSample, &vec![rawimage.bps as u16; rawimage.cpp]);
subframe.ifd_mut().add_tag(TiffCommonTag::SampleFormat, &vec![1_u16; rawimage.cpp]);
subframe.ifd_mut().add_tag(TiffCommonTag::TileOffsets, &tile_offsets);
subframe.ifd_mut().add_tag(TiffCommonTag::TileByteCounts, &tile_sizes);
subframe.ifd_mut().add_tag(TiffCommonTag::TileWidth, tile_w as u16);
subframe.ifd_mut().add_tag(TiffCommonTag::TileLength, tile_h as u16);
Ok(())
}
/// Write RAW uncompressed into DNG
///
/// This uses unsigned 16 bit values for storage
/// Data is split into multiple strips
fn dng_put_raw_uncompressed<W>(subframe: &mut SubFrameWriter<W>, rawimage: &RawImage) -> Result<()>
where
W: Write + Seek,
{
let mut strip_offsets: Vec<u32> = Vec::new();
let mut strip_sizes: Vec<u32> = Vec::new();
let mut strip_rows: Vec<u32> = Vec::new();
let rows_per_strip = if rawimage.height > rawler_dng_multistrip_threshold().unwrap_or(100) {
rawler_dng_rows_per_strip().unwrap_or(256)
} else {
rawimage.height
};
match rawimage.data {
RawImageData::Integer(ref data) => {
for strip in data.chunks(rows_per_strip * rawimage.width * rawimage.cpp) {
let offset = subframe.writer.dng.write_data_u16_le(strip)?;
strip_offsets.push(offset);
strip_sizes.push(std::mem::size_of_val(strip) as u32);
strip_rows.push((strip.len() / (rawimage.width * rawimage.cpp)) as u32);
}
subframe.ifd_mut().add_tag(TiffCommonTag::SampleFormat, &vec![1_u16; rawimage.cpp]); // Unsigned Integer
subframe.ifd_mut().add_tag(TiffCommonTag::BitsPerSample, &vec![16_u16; rawimage.cpp]);
}
RawImageData::Float(ref data) => {
for strip in data.chunks(rows_per_strip * rawimage.width * rawimage.cpp) {
let offset = subframe.writer.dng.write_data_f32_le(strip)?;
strip_offsets.push(offset);
strip_sizes.push(std::mem::size_of_val(strip) as u32);
strip_rows.push((strip.len() / (rawimage.width * rawimage.cpp)) as u32);
}
subframe.ifd_mut().add_tag(TiffCommonTag::SampleFormat, &vec![3_u16; rawimage.cpp]); // IEEE Float
subframe.ifd_mut().add_tag(TiffCommonTag::BitsPerSample, &vec![32_u16; rawimage.cpp]);
}
};
subframe.ifd_mut().add_tag(TiffCommonTag::StripOffsets, &strip_offsets);
subframe.ifd_mut().add_tag(TiffCommonTag::StripByteCounts, &strip_sizes);
subframe.ifd_mut().add_tag(TiffCommonTag::RowsPerStrip, &strip_rows);
Ok(())
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use crate::dng::DNG_VERSION_V1_4;
use super::*;
#[test]
fn build_empty_dng() -> std::result::Result<(), Box<dyn std::error::Error>> {
let mut buf = Cursor::new(Vec::new());
let mut dng = DngWriter::new(&mut buf, DNG_VERSION_V1_4)?;
dng.root_ifd_mut().add_tag(TiffCommonTag::Artist, "Test");
dng.close()?;
#[cfg(target_endian = "little")]
let expected_output = [
73, 73, 42, 0, 36, 0, 0, 0, 1, 0, 0, 144, 7, 0, 4, 0, 0, 0, 48, 50, 50, 48, 0, 0, 0, 0, 0, 0, 84, 101, 115, 116, 0, 0, 0, 0, 4, 0, 59, 1, 2, 0, 5, 0, 0,
0, 28, 0, 0, 0, 105, 135, 4, 0, 1, 0, 0, 0, 8, 0, 0, 0, 18, 198, 1, 0, 4, 0, 0, 0, 1, 6, 0, 0, 19, 198, 1, 0, 4, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 0,
];
#[cfg(not(target_endian = "little"))]
let expected_output = [
77, 77, 0, 42, 0, 0, 0, 36, 0, 1, 144, 0, 0, 7, 0, 0, 0, 4, 48, 50, 50, 48, 0, 0, 0, 0, 0, 0, 84, 101, 115, 116, 0, 0, 0, 0, 0, 4, 1, 59, 0, 2, 0, 0, 0,
5, 0, 0, 0, 28, 135, 105, 0, 4, 0, 0, 0, 1, 0, 0, 0, 8, 198, 18, 0, 1, 0, 0, 0, 4, 0, 0, 6, 1, 198, 19, 0, 1, 0, 0, 0, 4, 0, 0, 4, 1, 0, 0, 0, 0,
];
assert_eq!(expected_output, buf.into_inner().as_slice());
Ok(())
}
#[cfg(feature = "samplecheck")]
#[test]
fn convert_canon_cr3_to_dng() -> std::result::Result<(), Box<dyn std::error::Error>> {
use crate::{
decoders::RawDecodeParams,
dng::{DNG_VERSION_V1_4, PREVIEW_JPEG_QUALITY},
rawsource::RawSource,
};
use std::{
fs::File,
io::{BufReader, BufWriter},
path::PathBuf,
};
let mut rawdb = PathBuf::from(std::env::var("RAWLER_RAWDB").expect("RAWLER_RAWDB variable must be set in order to run RAW test!"));
rawdb.push("cameras/Canon/EOS R6/raw_modes/Canon EOS R6_RAW_ISO_100_nocrop_nodual.CR3");
let rawfile = RawSource::new(&rawdb)?;
let original_thread = std::thread::spawn(|| OriginalCompressed::compress(&mut BufReader::new(File::open(rawdb).unwrap())));
let decoder = crate::get_decoder(&rawfile)?;
let rawimage = decoder.raw_image(&rawfile, &RawDecodeParams::default(), false)?;
let full_image = decoder.full_image(&rawfile, &RawDecodeParams::default())?.unwrap();
let metadata = decoder.raw_metadata(&rawfile, &RawDecodeParams::default())?;
let predictor = 1;
let buf = BufWriter::new(Cursor::new(Vec::new()));
//let buf = BufWriter::new(File::create("/tmp/dng_writer_simple_test.dng")?);
let mut dng = DngWriter::new(buf, DNG_VERSION_V1_4)?;
let mut raw = dng.subframe(0);
raw.raw_image(
&rawimage,
CropMode::Best,
DngCompression::Lossless,
DngPhotometricConversion::Original,
predictor,
)?;
raw.finalize()?;
dng.thumbnail(&full_image)?;
let mut preview = dng.subframe(1);
preview.preview(&full_image, PREVIEW_JPEG_QUALITY)?;
preview.finalize()?;
dng.load_base_tags(&rawimage)?;
dng.load_metadata(&metadata)?;
if let Some(xpacket) = decoder.xpacket(&rawfile, &RawDecodeParams::default())? {
dng.xpacket(xpacket)?;
}
let original = original_thread.join().unwrap()?;
dng.original_file(&original, "test.CR3")?;
dng.close()?;
Ok(())
}
}