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:
+231
@@ -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) = ¶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)
|
||||
}
|
||||
+114
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user