Skip to content

Commit c1a08e1

Browse files
authored
Merge pull request #2749 from image-rs/upgrade-tiff-0.11
[v0.25] Upgrade tiff 0.11
2 parents ec3bff1 + c4c4a38 commit c1a08e1

File tree

12 files changed

+147
-37
lines changed

12 files changed

+147
-37
lines changed

.github/workflows/rust.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ jobs:
3939
strategy:
4040
fail-fast: false
4141
matrix:
42-
rust: ["1.85.0", nightly, beta]
42+
rust: ["1.88.0", nightly, beta]
4343
steps:
4444
- uses: actions/checkout@v4
4545

4646
- uses: dtolnay/rust-toolchain@nightly
47-
if: ${{ matrix.rust == '1.85.0' }}
47+
if: ${{ matrix.rust == '1.88.0' }}
4848
- name: Generate Cargo.lock with minimal-version dependencies
49-
if: ${{ matrix.rust == '1.85.0' }}
49+
if: ${{ matrix.rust == '1.88.0' }}
5050
run: |
5151
cargo -Zminimal-versions generate-lockfile
5252
cargo update --offline num-bigint --precise 0.4.2
@@ -61,7 +61,7 @@ jobs:
6161
- name: build
6262
run: cargo build -v
6363
- name: test
64-
if: ${{ matrix.rust != '1.85.0' }}
64+
if: ${{ matrix.rust != '1.88.0' }}
6565
run: cargo test -v && cargo doc -v
6666

6767
test_other_archs:

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ edition = "2021"
55
resolver = "2"
66

77
# note: when changed, also update test runner in `.github/workflows/rust.yml`
8-
rust-version = "1.85.0"
8+
rust-version = "1.88.0"
99

1010
license = "MIT OR Apache-2.0"
1111
description = "Imaging library. Provides basic image processing and encoders/decoders for common image formats."
@@ -52,7 +52,7 @@ qoi = { version = "0.4", optional = true }
5252
ravif = { version = "0.12", default-features = false, optional = true }
5353
rayon = { version = "1.7.0", optional = true }
5454
rgb = { version = "0.8.48", default-features = false, optional = true }
55-
tiff = { version = "0.10.3", optional = true }
55+
tiff = { version = "0.11.2", optional = true }
5656
zune-core = { version = "0.5.0", default-features = false, optional = true }
5757
zune-jpeg = { version = "0.5.5", optional = true }
5858
serde = { version = "1.0.214", optional = true, features = ["derive"] }

src/codecs/avif/encoder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ impl<W: Write> AvifEncoder<W> {
188188
Err(PodCastError::TargetAlignmentGreaterAndInputNotAligned) => {
189189
// Sad, but let's allocate.
190190
// bytemuck checks alignment _before_ slop but size mismatch before this..
191-
if buf.len() % size_of::<Channel>() != 0 {
191+
if !buf.len().is_multiple_of(size_of::<Channel>()) {
192192
Err(ImageError::Parameter(ParameterError::from_kind(
193193
ParameterErrorKind::DimensionMismatch,
194194
)))

src/codecs/dxt.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ impl<R: Read> DxtDecoder<R> {
8080
height: u32,
8181
variant: DxtVariant,
8282
) -> Result<DxtDecoder<R>, ImageError> {
83-
if width % 4 != 0 || height % 4 != 0 {
83+
if !width.is_multiple_of(4) || !height.is_multiple_of(4) {
8484
// TODO: this is actually a bit of a weird case. We could return `DecodingError` but
8585
// it's not really the format that is wrong However, the encoder should surely return
8686
// `EncodingError` so it would be the logical choice for symmetry.
@@ -289,7 +289,7 @@ fn decode_dxt1_block(source: &[u8], dest: &mut [u8]) {
289289
/// Decode a row of DXT1 data to four rows of RGB data.
290290
/// `source.len()` should be a multiple of 8, otherwise this panics.
291291
fn decode_dxt1_row(source: &[u8], dest: &mut [u8]) {
292-
assert!(source.len() % 8 == 0);
292+
assert!(source.len().is_multiple_of(8));
293293
let block_count = source.len() / 8;
294294
assert!(dest.len() >= block_count * 48);
295295

@@ -310,7 +310,7 @@ fn decode_dxt1_row(source: &[u8], dest: &mut [u8]) {
310310
/// Decode a row of DXT3 data to four rows of RGBA data.
311311
/// `source.len()` should be a multiple of 16, otherwise this panics.
312312
fn decode_dxt3_row(source: &[u8], dest: &mut [u8]) {
313-
assert!(source.len() % 16 == 0);
313+
assert!(source.len().is_multiple_of(16));
314314
let block_count = source.len() / 16;
315315
assert!(dest.len() >= block_count * 64);
316316

@@ -331,7 +331,7 @@ fn decode_dxt3_row(source: &[u8], dest: &mut [u8]) {
331331
/// Decode a row of DXT5 data to four rows of RGBA data.
332332
/// `source.len()` should be a multiple of 16, otherwise this panics.
333333
fn decode_dxt5_row(source: &[u8], dest: &mut [u8]) {
334-
assert!(source.len() % 16 == 0);
334+
assert!(source.len().is_multiple_of(16));
335335
let block_count = source.len() / 16;
336336
assert!(dest.len() >= block_count * 64);
337337

src/codecs/pnm/decoder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ impl Sample for PbmBit {
786786

787787
fn bytelen(width: u32, height: u32, samples: u32) -> ImageResult<usize> {
788788
let count = width * samples;
789-
let linelen = (count / 8) + u32::from((count % 8) != 0);
789+
let linelen = (count / 8) + u32::from(!count.is_multiple_of(8));
790790
Ok((linelen * height) as usize)
791791
}
792792

src/codecs/tiff.rs

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ where
3333

3434
// We only use an Option here so we can call with_limits on the decoder without moving.
3535
inner: Option<Decoder<R>>,
36+
buffer: DecodingResult,
3637
}
3738

3839
impl<R> TiffDecoder<R>
@@ -56,21 +57,6 @@ where
5657
Err(other) => return Err(ImageError::from_tiff_decode(other)),
5758
}
5859

59-
let planar_config = inner
60-
.find_tag(Tag::PlanarConfiguration)
61-
.map(|res| res.and_then(|r| r.into_u16().ok()).unwrap_or_default())
62-
.unwrap_or_default();
63-
64-
// Decode not supported for non Chunky Planar Configuration
65-
if planar_config > 1 {
66-
Err(ImageError::Unsupported(
67-
UnsupportedError::from_format_and_kind(
68-
ImageFormat::Tiff.into(),
69-
UnsupportedErrorKind::GenericFeature(String::from("PlanarConfiguration = 2")),
70-
),
71-
))?;
72-
}
73-
7460
let color_type = match tiff_color_type {
7561
tiff::ColorType::Gray(1) => ColorType::L8,
7662
tiff::ColorType::Gray(8) => ColorType::L8,
@@ -118,6 +104,7 @@ where
118104
color_type,
119105
original_color_type,
120106
inner: Some(inner),
107+
buffer: DecodingResult::U8(vec![]),
121108
})
122109
}
123110

@@ -133,6 +120,66 @@ where
133120
};
134121
total_pixels.saturating_mul(bytes_per_pixel)
135122
}
123+
124+
/// Interleave planes in our `buffer` into `output`.
125+
fn interleave_planes(
126+
&mut self,
127+
layout: tiff::decoder::BufferLayoutPreference,
128+
output: &mut [u8],
129+
) -> ImageResult<()> {
130+
if self.original_color_type != self.color_type.into() {
131+
return Err(ImageError::Unsupported(
132+
UnsupportedError::from_format_and_kind(
133+
ImageFormat::Tiff.into(),
134+
UnsupportedErrorKind::GenericFeature(
135+
"Planar TIFF with CMYK color type is not supported".to_string(),
136+
),
137+
),
138+
));
139+
}
140+
141+
// This only works if we and `tiff` agree on the layout, including the color type, of
142+
// the sample matrix.
143+
//
144+
// TODO: triple buffer in the other case and fixup the planar layout independent of
145+
// sample type. Problem description follows:
146+
//
147+
// That will suck since we can't call `interleave_planes` with a `ColorType` argument,
148+
// Changing that parameter to `ExtendedColorType` is a can of worms, and exposing the
149+
// underlying generic function is an optimization killer (we may want to help LLVM
150+
// optimize this interleaving by SIMD). For LumaAlpha(1) colors we should do the bit
151+
// expansion at the same time as interleaving to avoid wasting the memory traversal but
152+
// expand-then-interleave is at least clear, albeit an extra buffer required. Meanwhile
153+
// for `Cmyk8`/`Cmyk16` our output is smaller than the tiff buffer (4 samples to 3, or
154+
// 5 to 4 if we had alpha) and not wanting multiple conversion function implementations
155+
// we should interleave-then-expand?
156+
//
157+
// The hard part of the solution will be managing complexity.
158+
let plane_stride = layout.plane_stride.map_or(0, |n| n.get());
159+
let bytes = self.buffer.as_buffer(0);
160+
161+
let planes = bytes
162+
.as_bytes()
163+
.chunks_exact(plane_stride)
164+
.collect::<Vec<_>>();
165+
166+
// Gracefully handle a mismatch of expectations. This should not occur in practice as we
167+
// check that all planes have been read (see note on `read_image_to_buffer` usage below).
168+
if planes.len() < usize::from(self.color_type.channel_count()) {
169+
return Err(ImageError::Decoding(DecodingError::new(
170+
ImageFormat::Tiff.into(),
171+
"Not enough planes read from TIFF image".to_string(),
172+
)));
173+
}
174+
175+
utils::interleave_planes(
176+
output,
177+
self.color_type,
178+
&planes[..usize::from(self.color_type.channel_count())],
179+
);
180+
181+
Ok(())
182+
}
136183
}
137184

138185
fn check_sample_format(sample_format: u16, color_type: tiff::ColorType) -> Result<(), ImageError> {
@@ -319,15 +366,30 @@ impl<R: BufRead + Seek> ImageDecoder for TiffDecoder<R> {
319366
Ok(())
320367
}
321368

322-
fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
369+
fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> {
323370
assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes()));
324371

325-
match self
372+
let layout = self
326373
.inner
374+
.as_mut()
327375
.unwrap()
328-
.read_image()
329-
.map_err(ImageError::from_tiff_decode)?
330-
{
376+
.read_image_to_buffer(&mut self.buffer)
377+
.map_err(ImageError::from_tiff_decode)?;
378+
379+
// Check if we have all of the planes. Otherwise we ran into the allocation limit.
380+
if self.buffer.as_buffer(0).as_bytes().len() < layout.complete_len {
381+
return Err(ImageError::Limits(LimitError::from_kind(
382+
LimitErrorKind::InsufficientMemory,
383+
)));
384+
}
385+
386+
if layout.planes > 1 {
387+
// Note that we do not support planar layouts if we have to do conversion. Yet. See a
388+
// more detailed comment in the implementation.
389+
return self.interleave_planes(layout, buf);
390+
}
391+
392+
match self.buffer {
331393
DecodingResult::U8(v) if self.original_color_type == ExtendedColorType::Cmyk8 => {
332394
let mut out_cur = Cursor::new(buf);
333395
for cmyk in v.chunks_exact(4) {
@@ -383,6 +445,7 @@ impl<R: BufRead + Seek> ImageDecoder for TiffDecoder<R> {
383445
}
384446
DecodingResult::F16(_) => unreachable!(),
385447
}
448+
386449
Ok(())
387450
}
388451

src/imageops/fast_blur.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ fn boxes_for_gauss(sigma: f32, n: usize) -> Vec<usize> {
128128

129129
#[inline]
130130
fn ceil_to_odd(x: usize) -> usize {
131-
if x % 2 == 0 {
131+
if x.is_multiple_of(2) {
132132
x + 1
133133
} else {
134134
x

src/imageops/sample.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,7 +1213,7 @@ impl GaussianBlurParameters {
12131213
#[inline]
12141214
fn round_to_nearest_odd(x: f32) -> u32 {
12151215
let n = x.round() as u32;
1216-
if n % 2 != 0 {
1216+
if !n.is_multiple_of(2) {
12171217
n
12181218
} else {
12191219
let lower = n - 1;
@@ -1237,7 +1237,7 @@ impl GaussianBlurParameters {
12371237

12381238
fn kernel_size_from_sigma(sigma: f32) -> u32 {
12391239
let possible_size = (((((sigma - 0.8) / 0.3) + 1.) * 2.) + 1.).max(3.) as u32;
1240-
if possible_size % 2 == 0 {
1240+
if possible_size.is_multiple_of(2) {
12411241
return possible_size + 1;
12421242
}
12431243
possible_size

src/metadata/cicp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1155,7 +1155,7 @@ impl CicpRgb {
11551155
{
11561156
use crate::traits::private::LayoutWithColor as Layout;
11571157

1158-
assert!(buffer.len() % from_layout.channels() == 0);
1158+
assert!(buffer.len().is_multiple_of(from_layout.channels()));
11591159
let pixels = buffer.len() / from_layout.channels();
11601160

11611161
let mut output: Vec<IntoSubpixel> = vec_try_with_capacity(pixels * into_layout.channels())

src/utils/mod.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub(crate) fn expand_bits(bit_depth: u8, row_size: u32, buf: &[u8]) -> Vec<u8> {
4040
let mask = (1u8 << bit_depth as usize) - 1;
4141
let scaling_factor = 255 / ((1 << bit_depth as usize) - 1);
4242
let bit_width = row_size * u32::from(bit_depth);
43-
let skip = if bit_width % 8 == 0 {
43+
let skip = if bit_width.is_multiple_of(8) {
4444
0
4545
} else {
4646
(8 - bit_width % 8) / u32::from(bit_depth)
@@ -63,6 +63,53 @@ pub(crate) fn expand_bits(bit_depth: u8, row_size: u32, buf: &[u8]) -> Vec<u8> {
6363
p
6464
}
6565

66+
#[inline(always)]
67+
pub(crate) fn interleave_planes(out: &mut [u8], color: crate::ColorType, planes: &[&[u8]]) {
68+
#[track_caller]
69+
pub(crate) fn trampoline<const PLANES: usize, const N: usize>(
70+
out: &mut [u8],
71+
planes: &[&[u8]],
72+
) {
73+
interleave_planes_inner::<PLANES, N>(
74+
out.as_chunks_mut::<N>().0,
75+
<[_; PLANES]>::try_from(planes)
76+
.unwrap()
77+
.map(|arr| arr.as_chunks::<N>().0),
78+
)
79+
}
80+
81+
assert_eq!(planes.len(), usize::from(color.channel_count()));
82+
83+
match color {
84+
crate::ColorType::L8 => trampoline::<1, 1>(out, planes),
85+
crate::ColorType::La8 => trampoline::<2, 1>(out, planes),
86+
crate::ColorType::Rgb8 => trampoline::<3, 1>(out, planes),
87+
crate::ColorType::Rgba8 => trampoline::<4, 1>(out, planes),
88+
crate::ColorType::L16 => trampoline::<1, 2>(out, planes),
89+
crate::ColorType::La16 => trampoline::<2, 2>(out, planes),
90+
crate::ColorType::Rgb16 => trampoline::<3, 2>(out, planes),
91+
crate::ColorType::Rgba16 => trampoline::<4, 2>(out, planes),
92+
crate::ColorType::Rgb32F => trampoline::<3, 4>(out, planes),
93+
crate::ColorType::Rgba32F => trampoline::<4, 4>(out, planes),
94+
}
95+
}
96+
97+
#[inline(always)]
98+
fn interleave_planes_inner<const PLANES: usize, const N: usize>(
99+
out: &mut [[u8; N]],
100+
planes: [&[[u8; N]]; PLANES],
101+
) {
102+
let mut iters = planes.map(|plane| plane.iter().copied());
103+
for out in out.as_chunks_mut::<PLANES>().0 {
104+
let vals = iters.each_mut().map(Iterator::next);
105+
106+
// I'd like to use array::zip once stable.
107+
for i in 0..PLANES {
108+
out[i] = vals[i].unwrap_or(out[i]);
109+
}
110+
}
111+
}
112+
66113
/// Checks if the provided dimensions would cause an overflow.
67114
#[allow(dead_code)]
68115
// When no image formats that use it are enabled

0 commit comments

Comments
 (0)