Merge pull request #311 from image-rs/decode-and-encode-extra-samples
Decode and encode extra samples
This commit is contained in:
12 files changed
+295
-25
No files matched your search
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
- `Directory` now implements `FromIterator<(Tag, Value)>`.
|
- `Directory` now implements `FromIterator<(Tag, Value)>`.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- The decoder now interprets the `ExtraSamples` tag. The sample count must now
|
||||||
|
more strict match the expected value with alpha channels only allowing for
|
||||||
|
explicitly denoted unassociated or associated alpha. This effects the
|
||||||
|
indicated color type when decoding images with additional samples indicated
|
||||||
|
as unspecified relation. Previously, these may have been interpreted as
|
||||||
|
alpha by the total sample count (e.g. RgbA if 4 samples under a photometric
|
||||||
|
interpretation of RGB).
|
||||||
|
|
||||||
# Version 0.10.3
|
# Version 0.10.3
|
||||||
|
|
||||||
New features:
|
New features:
|
||||||
|
|||||||
+150
-17
@@ -4,7 +4,8 @@ use super::tag_reader::TagReader;
|
|||||||
use super::ChunkType;
|
use super::ChunkType;
|
||||||
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
|
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
|
CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration, Predictor,
|
||||||
|
SampleFormat, Tag,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
||||||
@@ -69,6 +70,11 @@ pub(crate) struct Image {
|
|||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub bits_per_sample: u8,
|
pub bits_per_sample: u8,
|
||||||
pub samples: u16,
|
pub samples: u16,
|
||||||
|
/// The `ExtraSamples`, defaulting to empty if not given.
|
||||||
|
pub extra_samples: Vec<ExtraSamples>,
|
||||||
|
/// Number of samples that belong to the photometric interpretation, samples except
|
||||||
|
/// `ExtraSamples` (338, 0x0152) tag.
|
||||||
|
pub photometric_samples: u16,
|
||||||
pub sample_format: SampleFormat,
|
pub sample_format: SampleFormat,
|
||||||
pub photometric_interpretation: PhotometricInterpretation,
|
pub photometric_interpretation: PhotometricInterpretation,
|
||||||
pub compression_method: CompressionMethod,
|
pub compression_method: CompressionMethod,
|
||||||
@@ -134,10 +140,30 @@ impl Image {
|
|||||||
.map(Value::into_u16)
|
.map(Value::into_u16)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or(1);
|
.unwrap_or(1);
|
||||||
|
|
||||||
if samples == 0 {
|
if samples == 0 {
|
||||||
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
|
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let extra_samples = match tag_reader.find_tag(Tag::ExtraSamples)? {
|
||||||
|
Some(n) => n.into_u16_vec()?,
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let extra_samples = extra_samples
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| ExtraSamples::from_u16(x).unwrap_or(ExtraSamples::Unspecified))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let photometric_samples = match usize::from(samples).checked_sub(extra_samples.len()) {
|
||||||
|
None => {
|
||||||
|
return Err(TiffError::FormatError(
|
||||||
|
TiffFormatError::InconsistentSizesEncountered,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(n) => n as u16,
|
||||||
|
};
|
||||||
|
|
||||||
let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
|
let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
|
||||||
Some(vals) => {
|
Some(vals) => {
|
||||||
let sample_format: Vec<_> = vals
|
let sample_format: Vec<_> = vals
|
||||||
@@ -297,6 +323,8 @@ impl Image {
|
|||||||
height,
|
height,
|
||||||
bits_per_sample: bits_per_sample[0],
|
bits_per_sample: bits_per_sample[0],
|
||||||
samples,
|
samples,
|
||||||
|
extra_samples,
|
||||||
|
photometric_samples,
|
||||||
sample_format,
|
sample_format,
|
||||||
photometric_interpretation,
|
photometric_interpretation,
|
||||||
compression_method,
|
compression_method,
|
||||||
@@ -312,15 +340,19 @@ impl Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
||||||
|
let is_alpha_extra_samples = matches!(
|
||||||
|
self.extra_samples.as_slice(),
|
||||||
|
[ExtraSamples::AssociatedAlpha] | [ExtraSamples::UnassociatedAlpha]
|
||||||
|
);
|
||||||
|
|
||||||
match self.photometric_interpretation {
|
match self.photometric_interpretation {
|
||||||
PhotometricInterpretation::RGB => match self.samples {
|
PhotometricInterpretation::RGB => match self.photometric_samples {
|
||||||
3 => Ok(ColorType::RGB(self.bits_per_sample)),
|
3 => Ok(if is_alpha_extra_samples {
|
||||||
|
ColorType::RGBA(self.bits_per_sample)
|
||||||
|
} else {
|
||||||
|
ColorType::RGB(self.bits_per_sample)
|
||||||
|
}),
|
||||||
4 => Ok(ColorType::RGBA(self.bits_per_sample)),
|
4 => Ok(ColorType::RGBA(self.bits_per_sample)),
|
||||||
// FIXME: We should _ignore_ other components. In particular:
|
|
||||||
// > Beware of extra components. Some TIFF files may have more components per pixel
|
|
||||||
// than you think. A Baseline TIFF reader must skip over them gracefully,using the
|
|
||||||
// values of the SamplesPerPixel and BitsPerSample fields.
|
|
||||||
// > -- TIFF 6.0 Specification, Section 7, Additional Baseline requirements.
|
|
||||||
_ => Err(TiffError::UnsupportedError(
|
_ => Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::InterpretationWithBits(
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
self.photometric_interpretation,
|
self.photometric_interpretation,
|
||||||
@@ -328,8 +360,12 @@ impl Image {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
PhotometricInterpretation::CMYK => match self.samples {
|
PhotometricInterpretation::CMYK => match self.photometric_samples {
|
||||||
4 => Ok(ColorType::CMYK(self.bits_per_sample)),
|
4 => Ok(if is_alpha_extra_samples {
|
||||||
|
ColorType::CMYKA(self.bits_per_sample)
|
||||||
|
} else {
|
||||||
|
ColorType::CMYK(self.bits_per_sample)
|
||||||
|
}),
|
||||||
5 => Ok(ColorType::CMYKA(self.bits_per_sample)),
|
5 => Ok(ColorType::CMYKA(self.bits_per_sample)),
|
||||||
_ => Err(TiffError::UnsupportedError(
|
_ => Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::InterpretationWithBits(
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
@@ -338,7 +374,7 @@ impl Image {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
PhotometricInterpretation::YCbCr => match self.samples {
|
PhotometricInterpretation::YCbCr => match self.photometric_samples {
|
||||||
3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
|
3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
|
||||||
_ => Err(TiffError::UnsupportedError(
|
_ => Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::InterpretationWithBits(
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
@@ -351,6 +387,9 @@ impl Image {
|
|||||||
// later called when that interpretation is read. That function does not support
|
// later called when that interpretation is read. That function does not support
|
||||||
// Multiband as a color type and will error. It's unclear how to resolve that exactly.
|
// Multiband as a color type and will error. It's unclear how to resolve that exactly.
|
||||||
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => {
|
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => {
|
||||||
|
// Note: compatibility with previous implementation requires us to return extra
|
||||||
|
// samples as `Multiband`. For gray images however the better choice would be
|
||||||
|
// returning a `Gray` color, i.e. matching on `photometric_samples` instead.
|
||||||
match self.samples {
|
match self.samples {
|
||||||
1 => Ok(ColorType::Gray(self.bits_per_sample)),
|
1 => Ok(ColorType::Gray(self.bits_per_sample)),
|
||||||
_ => Ok(ColorType::Multiband {
|
_ => Ok(ColorType::Multiband {
|
||||||
@@ -371,12 +410,28 @@ impl Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the multiband color describing this with its extra samples.
|
||||||
|
pub(crate) fn color_multiband_with_extras(&self) -> ColorType {
|
||||||
|
ColorType::Multiband {
|
||||||
|
bit_depth: self.bits_per_sample,
|
||||||
|
num_samples: self.samples,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describe this with an accurate color or a multiband.
|
||||||
|
pub(crate) fn color_or_fallback(&self) -> ColorType {
|
||||||
|
self.colortype()
|
||||||
|
.unwrap_or_else(|_| self.color_multiband_with_extras())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn minimum_row_stride(&self, dims: (u32, u32)) -> Option<NonZeroUsize> {
|
pub(crate) fn minimum_row_stride(&self, dims: (u32, u32)) -> Option<NonZeroUsize> {
|
||||||
let (width, height) = dims;
|
let (width, height) = dims;
|
||||||
|
|
||||||
|
let color = self.color_or_fallback();
|
||||||
|
|
||||||
let row_stride = u64::from(width)
|
let row_stride = u64::from(width)
|
||||||
.saturating_mul(self.samples_per_pixel() as u64)
|
.saturating_mul(u64::from(self.samples_per_out_texel(color)))
|
||||||
.saturating_mul(self.bits_per_sample as u64)
|
.saturating_mul(u64::from(self.bits_per_sample))
|
||||||
.div_ceil(8);
|
.div_ceil(8);
|
||||||
|
|
||||||
// Note: row stride should be smaller than the len if we have an actual buffer. If there
|
// Note: row stride should be smaller than the len if we have an actual buffer. If there
|
||||||
@@ -490,6 +545,13 @@ impl Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn samples_per_out_texel(&self, color: ColorType) -> u16 {
|
||||||
|
match self.planar_config {
|
||||||
|
PlanarConfiguration::Chunky => color.num_samples(),
|
||||||
|
PlanarConfiguration::Planar => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Number of strips per pixel.
|
/// Number of strips per pixel.
|
||||||
pub(crate) fn strips_per_pixel(&self) -> usize {
|
pub(crate) fn strips_per_pixel(&self) -> usize {
|
||||||
match self.planar_config {
|
match self.planar_config {
|
||||||
@@ -643,6 +705,7 @@ impl Image {
|
|||||||
.ok_or(TiffError::FormatError(
|
.ok_or(TiffError::FormatError(
|
||||||
TiffFormatError::InconsistentSizesEncountered,
|
TiffFormatError::InconsistentSizesEncountered,
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
if *compressed_bytes > limits.intermediate_buffer_size as u64 {
|
if *compressed_bytes > limits.intermediate_buffer_size as u64 {
|
||||||
return Err(TiffError::LimitsExceeded);
|
return Err(TiffError::LimitsExceeded);
|
||||||
}
|
}
|
||||||
@@ -650,8 +713,16 @@ impl Image {
|
|||||||
let compression_method = self.compression_method;
|
let compression_method = self.compression_method;
|
||||||
let photometric_interpretation = self.photometric_interpretation;
|
let photometric_interpretation = self.photometric_interpretation;
|
||||||
let predictor = self.predictor;
|
let predictor = self.predictor;
|
||||||
let samples = self.samples_per_pixel();
|
|
||||||
|
|
||||||
|
let samples = self.samples_per_pixel();
|
||||||
|
let data_samples = self.samples_per_out_texel(color_type);
|
||||||
|
|
||||||
|
// We have two dimensions: the 2d rectangle of encoded data and the 2d rectangle this
|
||||||
|
// takes up in the output. Each has an associated count of bits per pixel. The first
|
||||||
|
// dimension, i.e. a ''row'', is the number of pixels that are encoded with bit packing
|
||||||
|
// while the second is the byte-padded array of each so encoded slices.
|
||||||
|
//
|
||||||
|
// During decoding we map the relevant bits from one to the other.
|
||||||
let chunk_dims = self.chunk_dimensions()?;
|
let chunk_dims = self.chunk_dimensions()?;
|
||||||
let data_dims = self.chunk_data_dimensions(chunk_index)?;
|
let data_dims = self.chunk_data_dimensions(chunk_index)?;
|
||||||
|
|
||||||
@@ -661,7 +732,7 @@ impl Image {
|
|||||||
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
|
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
|
||||||
|
|
||||||
let data_row_bits = (u64::from(data_dims.0) * u64::from(self.bits_per_sample))
|
let data_row_bits = (u64::from(data_dims.0) * u64::from(self.bits_per_sample))
|
||||||
.checked_mul(samples as u64)
|
.checked_mul(data_samples as u64)
|
||||||
.ok_or(TiffError::LimitsExceeded)?;
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
|
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
|
||||||
|
|
||||||
@@ -677,7 +748,11 @@ impl Image {
|
|||||||
chunk_dims,
|
chunk_dims,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if output_row_stride == chunk_row_bytes {
|
let is_all_bits = samples == usize::from(data_samples);
|
||||||
|
let is_output_chunk_rows = output_row_stride == chunk_row_bytes;
|
||||||
|
|
||||||
|
if is_output_chunk_rows && is_all_bits {
|
||||||
|
// Here we can read directly into the output buffer itself.
|
||||||
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
|
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
|
||||||
reader.read_exact(tile)?;
|
reader.read_exact(tile)?;
|
||||||
|
|
||||||
@@ -690,6 +765,7 @@ impl Image {
|
|||||||
predictor,
|
predictor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
super::invert_colors(tile, color_type, self.sample_format)?;
|
super::invert_colors(tile, color_type, self.sample_format)?;
|
||||||
}
|
}
|
||||||
@@ -711,7 +787,8 @@ impl Image {
|
|||||||
super::invert_colors(row, color_type, self.sample_format)?;
|
super::invert_colors(row, color_type, self.sample_format)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if is_all_bits {
|
||||||
|
// We read row-by-row but each row fits in its output buffer.
|
||||||
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
|
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
|
||||||
let row = &mut row[..data_row_bytes];
|
let row = &mut row[..data_row_bytes];
|
||||||
reader.read_exact(row)?;
|
reader.read_exact(row)?;
|
||||||
@@ -729,6 +806,48 @@ impl Image {
|
|||||||
byte_order,
|
byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
|
super::invert_colors(row, color_type, self.sample_format)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The encoded data potentially takes up more space than the output data so we must be
|
||||||
|
// prepared to discard some of it. That decision is bit-by-bit.
|
||||||
|
let bits_per_pixel = u32::from(self.bits_per_sample) * u32::from(self.samples);
|
||||||
|
// Assumes the photometric samples are always the start.. This is slightly problematic.
|
||||||
|
// To expand spport we should instead have different methods of transforming the read
|
||||||
|
// buffer data, not only the `compact_photometric_bytes` method below and then choose
|
||||||
|
// from the right one with supplied parameters. Then we can also bit-for-bit copy with
|
||||||
|
// a selection for better performance.
|
||||||
|
let photometric_bit_end = u32::from(self.bits_per_sample) * data_samples as u32;
|
||||||
|
|
||||||
|
debug_assert!(bits_per_pixel >= photometric_bit_end);
|
||||||
|
|
||||||
|
if bits_per_pixel % 8 != 0 || photometric_bit_end % 8 != 0 {
|
||||||
|
return Err(TiffError::UnsupportedError(
|
||||||
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
|
self.photometric_interpretation,
|
||||||
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
|
||||||
|
let mut encoded = vec![0u8; chunk_row_bytes];
|
||||||
|
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
|
||||||
|
reader.read_exact(&mut encoded)?;
|
||||||
|
|
||||||
|
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
|
||||||
|
|
||||||
|
super::fix_endianness_and_predict(
|
||||||
|
row,
|
||||||
|
color_type.bit_depth(),
|
||||||
|
samples,
|
||||||
|
byte_order,
|
||||||
|
predictor,
|
||||||
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
super::invert_colors(row, color_type, self.sample_format)?;
|
super::invert_colors(row, color_type, self.sample_format)?;
|
||||||
}
|
}
|
||||||
@@ -737,4 +856,18 @@ impl Image {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn a contiguous buffer of a whole number of raw sample arrays into a whole number of
|
||||||
|
/// photometric sample arrays by removing the extra samples in-between.
|
||||||
|
fn compact_photometric_bytes(
|
||||||
|
raw: &mut [u8],
|
||||||
|
row: &mut [u8],
|
||||||
|
photo_range: &std::ops::Range<u32>,
|
||||||
|
) {
|
||||||
|
raw.chunks_exact_mut(photo_range.end as usize)
|
||||||
|
.zip(row.chunks_exact_mut(photo_range.start as usize))
|
||||||
|
.for_each(|(src, dst)| {
|
||||||
|
dst.copy_from_slice(&src[..photo_range.start as usize]);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+13
-3
@@ -667,6 +667,8 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
height: 0,
|
height: 0,
|
||||||
bits_per_sample: 1,
|
bits_per_sample: 1,
|
||||||
samples: 1,
|
samples: 1,
|
||||||
|
extra_samples: vec![],
|
||||||
|
photometric_samples: 1,
|
||||||
sample_format: SampleFormat::Uint,
|
sample_format: SampleFormat::Uint,
|
||||||
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
|
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
|
||||||
compression_method: CompressionMethod::None,
|
compression_method: CompressionMethod::None,
|
||||||
@@ -981,8 +983,10 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
output_width: usize,
|
output_width: usize,
|
||||||
) -> TiffResult<()> {
|
) -> TiffResult<()> {
|
||||||
|
let color = self.image.color_or_fallback();
|
||||||
|
|
||||||
let output_row_stride = (output_width as u64)
|
let output_row_stride = (output_width as u64)
|
||||||
.saturating_mul(self.image.samples_per_pixel() as u64)
|
.saturating_mul(self.image.samples_per_out_texel(color) as u64)
|
||||||
.saturating_mul(self.image.bits_per_sample as u64)
|
.saturating_mul(self.image.bits_per_sample as u64)
|
||||||
.div_ceil(8);
|
.div_ceil(8);
|
||||||
|
|
||||||
@@ -1032,9 +1036,12 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
.map_err(|_| TiffError::LimitsExceeded)?
|
.map_err(|_| TiffError::LimitsExceeded)?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let color = self.image.color_or_fallback();
|
||||||
|
let samples = self.image.samples_per_out_texel(color);
|
||||||
|
|
||||||
let buffer_size = row_samples
|
let buffer_size = row_samples
|
||||||
.checked_mul(height)
|
.checked_mul(height)
|
||||||
.and_then(|x| x.checked_mul(self.image.samples_per_pixel()))
|
.and_then(|x| x.checked_mul(samples.into()))
|
||||||
.ok_or(TiffError::LimitsExceeded)?;
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
|
|
||||||
Ok(match self.image().sample_format {
|
Ok(match self.image().sample_format {
|
||||||
@@ -1232,13 +1239,16 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
chunk_dimensions.0.min(width),
|
chunk_dimensions.0.min(width),
|
||||||
chunk_dimensions.1.min(height),
|
chunk_dimensions.1.min(height),
|
||||||
);
|
);
|
||||||
|
|
||||||
if chunk_dimensions.0 == 0 || chunk_dimensions.1 == 0 {
|
if chunk_dimensions.0 == 0 || chunk_dimensions.1 == 0 {
|
||||||
return Err(TiffError::FormatError(
|
return Err(TiffError::FormatError(
|
||||||
TiffFormatError::InconsistentSizesEncountered,
|
TiffFormatError::InconsistentSizesEncountered,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let samples = self.image().samples_per_pixel();
|
let color = self.image().colortype()?;
|
||||||
|
let samples = self.image().samples_per_out_texel(color);
|
||||||
|
|
||||||
if samples == 0 {
|
if samples == 0 {
|
||||||
return Err(TiffError::FormatError(
|
return Err(TiffError::FormatError(
|
||||||
TiffFormatError::InconsistentSizesEncountered,
|
TiffFormatError::InconsistentSizesEncountered,
|
||||||
|
|||||||
+57
-4
@@ -11,7 +11,7 @@ use std::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
decoder::ifd::Entry,
|
decoder::ifd::Entry,
|
||||||
error::{TiffResult, UsageError},
|
error::{TiffResult, UsageError},
|
||||||
tags::{CompressionMethod, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
|
tags::{CompressionMethod, ExtraSamples, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
|
||||||
Directory, TiffError, TiffFormatError,
|
Directory, TiffError, TiffFormatError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -506,6 +506,7 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
|
|||||||
row_samples: u64,
|
row_samples: u64,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
extra_samples: Vec<u16>,
|
||||||
rows_per_strip: u64,
|
rows_per_strip: u64,
|
||||||
strip_offsets: Vec<K::OffsetType>,
|
strip_offsets: Vec<K::OffsetType>,
|
||||||
strip_byte_count: Vec<K::OffsetType>,
|
strip_byte_count: Vec<K::OffsetType>,
|
||||||
@@ -562,9 +563,6 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
encoder.write_tag(Tag::Compression, compression.tag().to_u16())?;
|
encoder.write_tag(Tag::Compression, compression.tag().to_u16())?;
|
||||||
encoder.write_tag(Tag::Predictor, predictor.to_u16())?;
|
encoder.write_tag(Tag::Predictor, predictor.to_u16())?;
|
||||||
|
|
||||||
encoder.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
|
|
||||||
let sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
|
|
||||||
encoder.write_tag(Tag::SampleFormat, &sample_format[..])?;
|
|
||||||
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
|
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
|
||||||
|
|
||||||
encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
|
encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
|
||||||
@@ -583,6 +581,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
strip_idx: 0,
|
strip_idx: 0,
|
||||||
row_samples,
|
row_samples,
|
||||||
rows_per_strip,
|
rows_per_strip,
|
||||||
|
extra_samples: vec![],
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
strip_offsets: Vec::new(),
|
strip_offsets: Vec::new(),
|
||||||
@@ -594,6 +593,27 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extra_samples(&mut self, extra: &[ExtraSamples]) -> Result<(), TiffError> {
|
||||||
|
if self.strip_idx != 0 {
|
||||||
|
return Err(TiffError::UsageError(
|
||||||
|
UsageError::ReconfiguredAfterImageWrite,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let samples = self.extra_samples.len() + extra.len() + <T>::BITS_PER_SAMPLE.len();
|
||||||
|
let row_samples = u64::from(self.width) * u64::try_from(samples)?;
|
||||||
|
|
||||||
|
self.extra_samples
|
||||||
|
.extend(extra.iter().map(ExtraSamples::to_u16));
|
||||||
|
|
||||||
|
self.encoder
|
||||||
|
.write_tag(Tag::ExtraSamples, &self.extra_samples[..])?;
|
||||||
|
|
||||||
|
self.row_samples = row_samples;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Number of samples the next strip should have.
|
/// Number of samples the next strip should have.
|
||||||
pub fn next_strip_sample_count(&self) -> u64 {
|
pub fn next_strip_sample_count(&self) -> u64 {
|
||||||
if self.strip_idx >= self.strip_count {
|
if self.strip_idx >= self.strip_count {
|
||||||
@@ -731,6 +751,39 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
|
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
|
||||||
|
if self.extra_samples.is_empty() {
|
||||||
|
self.encoder
|
||||||
|
.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
|
||||||
|
} else {
|
||||||
|
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
|
||||||
|
let replicated =
|
||||||
|
core::iter::repeat(<T>::BITS_PER_SAMPLE[0]).take(self.extra_samples.len());
|
||||||
|
sample_format.extend(replicated);
|
||||||
|
|
||||||
|
self.encoder
|
||||||
|
.write_tag(Tag::BitsPerSample, &sample_format[..])?;
|
||||||
|
|
||||||
|
self.encoder.write_tag(
|
||||||
|
Tag::SamplesPerPixel,
|
||||||
|
u16::try_from(<T>::BITS_PER_SAMPLE.len() + self.extra_samples.len())?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
|
||||||
|
let extra_format = sample_format
|
||||||
|
.first()
|
||||||
|
.copied()
|
||||||
|
// Frankly should not occur, we have no sample format without at least one entry. We
|
||||||
|
// would need an interface upgrade to handle this however. Either decide to support
|
||||||
|
// heterogeneous sample formats or a way to provide fallback that is used for purely
|
||||||
|
// extra samples and not provided via a slice used for samples themselves.
|
||||||
|
.unwrap_or(SampleFormat::Void.to_u16());
|
||||||
|
|
||||||
|
sample_format.extend(core::iter::repeat(extra_format).take(self.extra_samples.len()));
|
||||||
|
|
||||||
|
self.encoder
|
||||||
|
.write_tag(Tag::SampleFormat, &sample_format[..])?;
|
||||||
|
|
||||||
self.encoder
|
self.encoder
|
||||||
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
|
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
|
||||||
self.encoder.write_tag(
|
self.encoder.write_tag(
|
||||||
|
|||||||
@@ -190,6 +190,9 @@ quick_error! {
|
|||||||
ZeroIfdPointer {
|
ZeroIfdPointer {
|
||||||
display("the offset 0 can not point to a valid IFD")
|
display("the offset 0 can not point to a valid IFD")
|
||||||
}
|
}
|
||||||
|
ReconfiguredAfterImageWrite {
|
||||||
|
display("attempted to reconfigure the encoder after image writing has started")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
@@ -61,4 +61,18 @@ impl ColorType {
|
|||||||
| ColorType::Multiband { bit_depth: b, .. } => b,
|
| ColorType::Multiband { bit_depth: b, .. } => b,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn num_samples(&self) -> u16 {
|
||||||
|
match *self {
|
||||||
|
ColorType::Gray(_) => 1,
|
||||||
|
ColorType::RGB(_) => 3,
|
||||||
|
ColorType::Palette(_) => 1,
|
||||||
|
ColorType::GrayA(_) => 2,
|
||||||
|
ColorType::RGBA(_) => 4,
|
||||||
|
ColorType::CMYK(_) => 4,
|
||||||
|
ColorType::CMYKA(_) => 5,
|
||||||
|
ColorType::YCbCr(_) => 3,
|
||||||
|
ColorType::Multiband { num_samples, .. } => num_samples,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+11
@@ -311,6 +311,17 @@ pub enum SampleFormat(u16) unknown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags! {
|
||||||
|
pub enum ExtraSamples(u16) {
|
||||||
|
/// There is no specified association between the sample and the image.
|
||||||
|
Unspecified = 0,
|
||||||
|
/// The sample is associated alpha, i.e. pre-multiplied color.
|
||||||
|
AssociatedAlpha = 1,
|
||||||
|
/// The sample is unassociated alpha such as a mask. There might be more than one such sample.
|
||||||
|
UnassociatedAlpha = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Byte order of the TIFF file.
|
/// Byte order of the TIFF file.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ByteOrder {
|
pub enum ByteOrder {
|
||||||
|
|||||||
@@ -684,3 +684,39 @@ fn test_fax4() {
|
|||||||
fn test_fax4_white_is_min() {
|
fn test_fax4_white_is_min() {
|
||||||
test_image_sum_u8("imagemagick_group4.tiff", ColorType::Gray(1), 3742820);
|
test_image_sum_u8("imagemagick_group4.tiff", ColorType::Gray(1), 3742820);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_extra_bits_image() {
|
||||||
|
use tiff::{encoder, tags::ExtraSamples};
|
||||||
|
|
||||||
|
let path = PathBuf::from(TEST_IMAGE_DIR).join("extra_bits_rgb_8b.tiff");
|
||||||
|
let img_file = File::create(path).expect("Cannot find test image!");
|
||||||
|
|
||||||
|
let mut encoder = encoder::TiffEncoder::new(img_file).expect("Cannot create encoder");
|
||||||
|
let mut img = encoder.new_image::<encoder::colortype::RGB8>(8, 8).unwrap();
|
||||||
|
|
||||||
|
img.extra_samples(&[ExtraSamples::Unspecified]).unwrap();
|
||||||
|
|
||||||
|
// all pixels are supposed to be 0, the extra is 1
|
||||||
|
let data: [u8; 256] = core::array::from_fn(|i| (i as u8 % 4).saturating_sub(2));
|
||||||
|
|
||||||
|
img.write_data(&data)
|
||||||
|
.expect("correct amount of data to write");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extra_bits_gray() {
|
||||||
|
test_image_sum_u8(
|
||||||
|
"extra_bits_gray_8b.tiff",
|
||||||
|
ColorType::Multiband {
|
||||||
|
bit_depth: 8,
|
||||||
|
num_samples: 2,
|
||||||
|
},
|
||||||
|
64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extra_bits_rgb() {
|
||||||
|
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ fn encode_decode() {
|
|||||||
),
|
),
|
||||||
(Tag::Compression, ifd::Value::Short(1)),
|
(Tag::Compression, ifd::Value::Short(1)),
|
||||||
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
|
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
|
||||||
(Tag::StripOffsets, ifd::Value::Unsigned(47)),
|
(Tag::StripOffsets, ifd::Value::Unsigned(35)),
|
||||||
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
|
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
|
||||||
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
|
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
|
||||||
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
|
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
|
||||||
@@ -235,6 +235,7 @@ const TEST_IMAGE_DIR: &str = "./tests/images/";
|
|||||||
|
|
||||||
macro_rules! test_roundtrip {
|
macro_rules! test_roundtrip {
|
||||||
($name:ident, $buffer:ident, $buffer_ty:ty) => {
|
($name:ident, $buffer:ident, $buffer_ty:ty) => {
|
||||||
|
#[track_caller]
|
||||||
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
|
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
|
||||||
file: &str,
|
file: &str,
|
||||||
expected_type: ColorType,
|
expected_type: ColorType,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user