Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b21bcc75c | ||
|
|
fe305ec24a | ||
|
|
a3ab252168 | ||
|
|
bd64e438a4 | ||
|
|
fa225e820b | ||
|
|
ea373bae00 | ||
|
|
23d1867f75 | ||
|
|
234b2ba677 | ||
|
|
a63a01319c | ||
|
|
efd2a6dbd8 | ||
|
|
d38fba4268 | ||
|
|
1732481b01 | ||
|
|
355ecc8ad9 | ||
|
|
985f3822c8 | ||
|
|
f6d95249dd |
No files matched your search
+40
-1
@@ -1,4 +1,4 @@
|
||||
# Version 0.10.4 (unreleased)
|
||||
# Version 0.11.0
|
||||
|
||||
- `Directory` now implements `FromIterator<(Tag, Value)>`.
|
||||
|
||||
@@ -10,6 +10,45 @@ Changes:
|
||||
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).
|
||||
- The decoder handles planar data, current limited to non-subsampled channels.
|
||||
The `Decoder::read_image` method return planes one-after-another depending on
|
||||
the size of the buffer that was passed.
|
||||
- `Decoder::read_image_to_buffer` now takes `&mut DecodingResult` and resizes
|
||||
it according to the required layout. Previously, a borrowed `DecodingBuffer`
|
||||
was passed which can be replaced by calling `as_bytes_mut` and
|
||||
`read_image_bytes`.
|
||||
- Several methods of tags are now `const`. Note that does not guarantee any
|
||||
particular value when calling these methods.
|
||||
|
||||
Fixes:
|
||||
- Fix a bug in the uncompressed encoder that could lead to short writes, i.e.
|
||||
data silently dropped when the underlying writer did not accept all data in a
|
||||
single write call.
|
||||
- Encoding YCbCr data now writes the `ChromaSubsampling` tag as `(1, 1)` to
|
||||
indicate no subsampling, instead of leaving it at its default of `(2, 2)`.
|
||||
- The decoder will reject subsampled YCbCr data as there is no upsampling
|
||||
routine, except for JPEG compressed images where the JPEG decoders handles
|
||||
this. Since the buffer in that case indicates a full-sized plane for all
|
||||
color samples any future support for the tag will upsample all planes (at
|
||||
least within this major version).
|
||||
|
||||
Additions:
|
||||
- Added support for the `CieLab` color type.
|
||||
- Added `DecodingResult::resize_to` to create a buffer with a matching sample
|
||||
type and dimensions.
|
||||
- Added `ByteOrder::native` to access the platform's native endianness.
|
||||
- Added `ByteOrder::convert` to change the byte-order of values in a byte
|
||||
buffer, depending on their `Type` as described at runtime.
|
||||
- Added `DirectoryOffset::new` to encode a directory whose offset is known to
|
||||
the caller but that has not been written through the encoder itself.
|
||||
- Added `Encoder::extra_samples` to encode images with more samples than their
|
||||
color's trait implementation would otherwise suggest.
|
||||
- Added `DirectoryEncoder::extend_from` to encode multiple tag entries from a
|
||||
directory whose values were written to the file by means outside the
|
||||
encoder's control.
|
||||
- Added `Decoder::read_coding_unit_bytes` to retrieve data of corresponding
|
||||
coordinates from potentially planar data, which is encoded in multiple chunks
|
||||
of the file.
|
||||
|
||||
# Version 0.10.3
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tiff"
|
||||
version = "0.10.3"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
||||
+101
-5
@@ -85,6 +85,7 @@ pub(crate) struct Image {
|
||||
pub tile_attributes: Option<TileAttributes>,
|
||||
pub chunk_offsets: Vec<u64>,
|
||||
pub chunk_bytes: Vec<u64>,
|
||||
pub chroma_subsampling: (u16, u16),
|
||||
}
|
||||
|
||||
/// Describes how to read a tile-aligned portion of the image.
|
||||
@@ -95,6 +96,19 @@ pub(crate) struct ReadoutLayout {
|
||||
pub planar_config: PlanarConfiguration,
|
||||
|
||||
/// The sample interpretation (interpret with planar_config).
|
||||
///
|
||||
/// FIXME: we should not require this here. The ability to turn out the raw bytes from the
|
||||
/// sample arrays is very different from turning out interpretable color. Firstly we can always
|
||||
/// readout `Multiband` but currently only use that ColorType in special circumstances (it must
|
||||
/// not overlap cases where actually want to use a ColorType).
|
||||
///
|
||||
/// And then we have CIE Lab, which uses a tuple of `(u8, i8, i8)`, that is still filterable
|
||||
/// but still not represented by any of our `DecoderResult` variants. Other color variants
|
||||
/// depend on extra tags (YCbCrCoefficients/0x0211) and we don't have a good side channel to
|
||||
/// tag the output with all that TIFF specific information, so arguably we should process and
|
||||
/// apply those to the data so it becomes a self-contained representation.
|
||||
///
|
||||
/// This should be computed at a higher level, in `Decoder`, instead.
|
||||
pub color: ColorType,
|
||||
/// The number of bytes from one row to another.
|
||||
pub minimum_row_stride: usize,
|
||||
@@ -262,6 +276,25 @@ impl Image {
|
||||
.transpose()?
|
||||
.unwrap_or(PlanarConfiguration::Chunky);
|
||||
|
||||
let ycbcr_subsampling = tag_reader.find_tag_uint_vec::<u16>(Tag::ChromaSubsampling)?;
|
||||
|
||||
let chroma_subsampling = if let Some(subsamples) = &ycbcr_subsampling {
|
||||
let [a, b] = subsamples.as_slice() else {
|
||||
return Err(TiffError::FormatError(TiffFormatError::InvalidCountForTag(
|
||||
Tag::ChromaSubsampling,
|
||||
subsamples.len(),
|
||||
)));
|
||||
};
|
||||
|
||||
// ImageWidth and ImageLength are constrained to be integer multiples of
|
||||
// YCbCrSubsampleHoriz and YCbCrSubsampleVert respectively. TileWidth and TileLength
|
||||
// have the same constraints. RowsPerStrip must be an integer multiple of
|
||||
// YCbCrSubsampleVert.
|
||||
(*a, *b)
|
||||
} else {
|
||||
(2, 2)
|
||||
};
|
||||
|
||||
let planes = match planar_config {
|
||||
PlanarConfiguration::Chunky => 1,
|
||||
PlanarConfiguration::Planar => samples,
|
||||
@@ -373,6 +406,7 @@ impl Image {
|
||||
tile_attributes,
|
||||
chunk_offsets,
|
||||
chunk_bytes,
|
||||
chroma_subsampling,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -435,15 +469,60 @@ impl Image {
|
||||
}),
|
||||
}
|
||||
}
|
||||
// TODO: this is bad we should not fail at this point
|
||||
PhotometricInterpretation::RGBPalette
|
||||
| PhotometricInterpretation::TransparencyMask
|
||||
| PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
||||
// ```
|
||||
// struct IccLab /* Interpretation 9* {
|
||||
// pub L: u8, // SampleFormat::Uint
|
||||
// pub a: u8, // SampleFormat::Uint, defined as TiffLab::a + 128
|
||||
// pub b: u8, // SampleFormat::Uint, defined as TiffLab::b + 128
|
||||
// }
|
||||
// ```
|
||||
PhotometricInterpretation::IccLab => match self.photometric_samples {
|
||||
3 if matches!(self.sample_format, SampleFormat::Uint) => {
|
||||
Ok(ColorType::Lab(self.bits_per_sample))
|
||||
}
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
},
|
||||
// Unsupported due to inherently heterogeneous sample types. This is represented as:
|
||||
// ```
|
||||
// struct TiffLab /* Interpretation 8* {
|
||||
// pub L: u8, // SampleFormat::Uint
|
||||
// pub a: i8, // SampleFormat::Int
|
||||
// pub b: i8, // SampleFormat::Int
|
||||
// }
|
||||
// ```
|
||||
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
PhotometricInterpretation::CIELab,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
// Unsupported due to extra unfiltering and conversion steps. We need to find the
|
||||
// Decode tag (SRATIONAL; 2 * SamplesPerPixel) and apply the following conversion:
|
||||
//
|
||||
// L* = Decode[0] + Lsample x (Decode[1] - Decode[0]) / (2^n -1)
|
||||
// …
|
||||
//
|
||||
// So we'll have a larger depth in the output and either worry about reducing fractions
|
||||
// or turn everything into floats. That's a lot of decisions.
|
||||
PhotometricInterpretation::ItuLab => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
PhotometricInterpretation::CIELab,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::TransparencyMask => {
|
||||
Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,6 +736,23 @@ impl Image {
|
||||
));
|
||||
}
|
||||
|
||||
// Only this color type interprets the tag, which is defined with a default of (2, 2)
|
||||
if matches!(color, ColorType::YCbCr(_)) && self.chroma_subsampling != (1, 1) {
|
||||
// The JPEG library does upsampling for us and defines its buffers correctly
|
||||
// (presumably). All other compression schemes are not supported..
|
||||
//
|
||||
// NOTE: as explained in <fa225e820b96bef35f01bf4685654beeb4a8df0c> we may be better
|
||||
// off supporting this tag by consistently upsampling, not by adjusting the buffer
|
||||
// size. At least as a default this makes more sense and is much more permissive in
|
||||
// case the compression stream disagrees with the tags (we would not have enough / or
|
||||
// the wrong buffer layout if we only asked for subsampled planes in a planar layout).
|
||||
if !matches!(self.compression_method, CompressionMethod::ModernJPEG) {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::ChromaSubsampling,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ReadoutLayout {
|
||||
planar_config: self.planar_config,
|
||||
color,
|
||||
|
||||
+2
-1
@@ -860,6 +860,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
tile_attributes: None,
|
||||
chunk_offsets: Vec::new(),
|
||||
chunk_bytes: Vec::new(),
|
||||
chroma_subsampling: (2, 2),
|
||||
},
|
||||
};
|
||||
decoder.next_image()?;
|
||||
@@ -1277,7 +1278,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
///
|
||||
/// It will re-allocate the buffer into the correct type and size, within the decoder's
|
||||
/// configured limits, and then pass it to the underlying method. This is essentially a
|
||||
/// type-safe wrapper around the raw [`Self::read_chunk_to_bytes`] method.
|
||||
/// type-safe wrapper around the raw [`Self::read_chunk_bytes`] method.
|
||||
///
|
||||
/// Note that for planar images each chunk contains only one sample of the underlying data.
|
||||
pub fn read_chunk_to_buffer(
|
||||
|
||||
+10
-1
@@ -11,7 +11,10 @@ use std::{
|
||||
use crate::{
|
||||
decoder::ifd::Entry,
|
||||
error::{TiffResult, UsageError},
|
||||
tags::{CompressionMethod, ExtraSamples, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
|
||||
tags::{
|
||||
CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit,
|
||||
SampleFormat, Tag, Type,
|
||||
},
|
||||
Directory, TiffError, TiffFormatError,
|
||||
};
|
||||
|
||||
@@ -565,6 +568,12 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
|
||||
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
|
||||
|
||||
if matches!(<T>::TIFF_VALUE, PhotometricInterpretation::YCbCr) {
|
||||
// The default for this tag is 2,2 for subsampling but we do not support such a
|
||||
// transformation. Instead all samples must be provided.
|
||||
encoder.write_tag(Tag::ChromaSubsampling, &[1u16, 1u16][..])?;
|
||||
}
|
||||
|
||||
encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
|
||||
|
||||
encoder.write_tag(
|
||||
|
||||
@@ -90,6 +90,9 @@ quick_error! {
|
||||
InvalidTypeForTag {
|
||||
display("tag has invalid type")
|
||||
}
|
||||
InvalidCountForTag(tag: Tag, len: usize) {
|
||||
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
|
||||
}
|
||||
StripTileTagConflict {
|
||||
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
|
||||
}
|
||||
@@ -156,6 +159,9 @@ quick_error! {
|
||||
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
|
||||
display("unsupported photometric interpretation \"{interpretation:?}\"")
|
||||
}
|
||||
ChromaSubsampling {
|
||||
display("chroma subsampling of YCbCr color is unsupported")
|
||||
}
|
||||
MisalignedTileBoundaries {
|
||||
display("tile rows are not aligned to byte boundaries")
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ pub enum ColorType {
|
||||
/// Pixel is YCbCr
|
||||
YCbCr(u8),
|
||||
|
||||
/// Pixel is CIE L*a*b* (ICC LAb)
|
||||
Lab(u8),
|
||||
|
||||
/// Pixel has multiple bands/channels
|
||||
Multiband { bit_depth: u8, num_samples: u16 },
|
||||
}
|
||||
@@ -58,6 +61,7 @@ impl ColorType {
|
||||
| ColorType::CMYK(b)
|
||||
| ColorType::CMYKA(b)
|
||||
| ColorType::YCbCr(b)
|
||||
| ColorType::Lab(b)
|
||||
| ColorType::Multiband { bit_depth: b, .. } => b,
|
||||
}
|
||||
}
|
||||
@@ -72,6 +76,7 @@ impl ColorType {
|
||||
ColorType::CMYK(_) => 4,
|
||||
ColorType::CMYKA(_) => 5,
|
||||
ColorType::YCbCr(_) => 3,
|
||||
ColorType::Lab(_) => 3,
|
||||
ColorType::Multiband { num_samples, .. } => num_samples,
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -20,7 +20,7 @@ macro_rules! tags {
|
||||
|
||||
impl $name {
|
||||
#[inline(always)]
|
||||
fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
|
||||
const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
|
||||
match n {
|
||||
$( $val => Ok($name::$tag), )*
|
||||
n => Err(n),
|
||||
@@ -28,7 +28,7 @@ macro_rules! tags {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn __to_inner_type(&self) -> $ty {
|
||||
const fn __to_inner_type(&self) -> $ty {
|
||||
match *self {
|
||||
$( $name::$tag => $val, )*
|
||||
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
|
||||
@@ -42,19 +42,25 @@ macro_rules! tags {
|
||||
($name:tt, u16, $($unknown_doc:ident)*) => {
|
||||
impl $name {
|
||||
#[inline(always)]
|
||||
pub fn from_u16(val: u16) -> Option<Self> {
|
||||
Self::__from_inner_type(val).ok()
|
||||
pub const fn from_u16(val: u16) -> Option<Self> {
|
||||
match Self::__from_inner_type(val) {
|
||||
Ok(v) => Some(v),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
#[inline(always)]
|
||||
pub fn from_u16_exhaustive($unknown_doc: u16) -> Self {
|
||||
Self::__from_inner_type($unknown_doc).unwrap_or_else(|_| $name::Unknown($unknown_doc))
|
||||
pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
|
||||
match Self::__from_inner_type($unknown_doc) {
|
||||
Ok(v) => v,
|
||||
Err(_) => $name::Unknown($unknown_doc),
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_u16(&self) -> u16 {
|
||||
pub const fn to_u16(&self) -> u16 {
|
||||
Self::__to_inner_type(self)
|
||||
}
|
||||
}
|
||||
@@ -122,6 +128,11 @@ pub enum Tag(u16) unknown(
|
||||
SMaxSampleValue = 341, // TODO add support
|
||||
// JPEG
|
||||
JPEGTables = 347,
|
||||
// Subsampling
|
||||
#[doc(alias = "YCbCrSubsampling")]
|
||||
ChromaSubsampling = 530, // TODO add support
|
||||
#[doc(alias = "YCbCrPositioning")]
|
||||
ChromaPositioning = 531, // TODO add support
|
||||
// GeoTIFF
|
||||
ModelPixelScaleTag = 33550, // (SoftDesk)
|
||||
ModelTransformationTag = 34264, // (JPL Carto Group)
|
||||
@@ -266,6 +277,8 @@ pub enum PhotometricInterpretation(u16) {
|
||||
CMYK = 5,
|
||||
YCbCr = 6,
|
||||
CIELab = 8,
|
||||
IccLab = 9,
|
||||
ItuLab = 10,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user