Skip to content

Commit e33eea7

Browse files
authored
Merge pull request #82 from roteiro-gis/ian/dev
Deflate backend/levels and validation
2 parents 409eb0a + 4569376 commit e33eea7

12 files changed

Lines changed: 194 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- add `deflate_level(0..=9)` to `ImageBuilder` and `GeoTiffBuilder` for controlling Deflate output size/speed, plus `compress_with_level` in `tiff-writer::compress`; `BlockEncodingOptions` gains a `deflate_level` field
6+
- speed up decode and write paths: switch the Deflate backend to the pure-Rust `zlib-rs` (~20-40% faster on predictor-compressed rasters), stream overview resampling by source row spans (~6x faster streaming COG average overviews), copy raster regions by row slice instead of per-element indexing (~4.5x faster plain multiband writes), resolve per-block decode metadata once per read, encode writer tag values once, and reuse the floating-point predictor scratch buffer across rows
57
- pad one-shot COG edge tiles with the configured nodata fill value instead of zero, matching the streaming tile writer and overview generation paths
68
- reject writer configurations that pair the horizontal predictor with float samples, the floating-point predictor with integer samples, or float sample formats below 32 bits, instead of emitting files other readers refuse
79
- decode `BitsPerSample`/`SampleFormat` tags stored with nonstandard BYTE or LONG encodings instead of silently falling back to 1-bit defaults, and reject other unexpected tag types via new `Ifd::checked_bits_per_sample` / `Ifd::checked_sample_format` used by all decode paths

Cargo.lock

Lines changed: 7 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fuzz/Cargo.lock

Lines changed: 7 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

geotiff-writer/src/builder.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub struct GeoTiffBuilder {
5555
pub(crate) predictor: Predictor,
5656
pub(crate) lerc_options: Option<tiff_writer::LercOptions>,
5757
pub(crate) jpeg_options: Option<JpegOptions>,
58+
pub(crate) deflate_level: Option<u32>,
5859
pub(crate) extra_samples: Vec<ExtraSample>,
5960
pub(crate) color_map: Option<ColorMap>,
6061
pub(crate) ink_set: Option<InkSet>,
@@ -85,6 +86,7 @@ impl GeoTiffBuilder {
8586
predictor: Predictor::None,
8687
lerc_options: None,
8788
jpeg_options: None,
89+
deflate_level: None,
8890
extra_samples: Vec::new(),
8991
color_map: None,
9092
ink_set: None,
@@ -297,6 +299,15 @@ impl GeoTiffBuilder {
297299
self
298300
}
299301

302+
/// Set the Deflate compression level (0-9).
303+
///
304+
/// Applies to `Compression::Deflate` output. The additional Deflate layer
305+
/// of `LERC+Deflate` always uses the codec default level.
306+
pub fn deflate_level(mut self, level: u32) -> Self {
307+
self.deflate_level = Some(level);
308+
self
309+
}
310+
300311
/// Set predictor (requires compression != None).
301312
pub fn predictor(mut self, predictor: Predictor) -> Self {
302313
// LERC and JPEG do not use TIFF predictors; ignore the request.
@@ -580,6 +591,9 @@ impl GeoTiffBuilder {
580591
if let Some(opts) = self.jpeg_options {
581592
ib = ib.jpeg_options(opts);
582593
}
594+
if let Some(level) = self.deflate_level {
595+
ib = ib.deflate_level(level);
596+
}
583597

584598
if let (Some(tw), Some(th)) = (self.tile_width, self.tile_height) {
585599
ib = ib.tiles(tw, th);

geotiff-writer/src/cog.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ struct CogBlockEncoding {
9797
block_height: u32,
9898
lerc_options: Option<LercOptions>,
9999
jpeg_options: Option<JpegOptions>,
100+
deflate_level: Option<u32>,
100101
}
101102

102103
#[derive(Debug, Clone, Copy)]
@@ -108,6 +109,7 @@ struct TileWritePlan {
108109
predictor: Predictor,
109110
lerc_options: Option<LercOptions>,
110111
jpeg_options: Option<JpegOptions>,
112+
deflate_level: Option<u32>,
111113
}
112114

113115
#[derive(Debug, Clone, Copy)]
@@ -525,6 +527,7 @@ fn compress_cog_block<T: NumericSample>(
525527
samples_per_pixel: encoding.samples_per_pixel,
526528
row_width_pixels: encoding.row_width_pixels,
527529
jpeg_options: encoding.jpeg_options.as_ref(),
530+
deflate_level: encoding.deflate_level,
528531
},
529532
block_index,
530533
)
@@ -1034,6 +1037,7 @@ impl CogBuilder {
10341037
predictor: self.inner.predictor,
10351038
lerc_options: self.inner.lerc_options,
10361039
jpeg_options: self.inner.jpeg_options,
1040+
deflate_level: self.inner.deflate_level,
10371041
};
10381042
let grid = RawTileGrid {
10391043
tile_width: tw,
@@ -1115,6 +1119,7 @@ pub struct CogTileWriter<T: NumericSample, W: Write + Seek> {
11151119
predictor: Predictor,
11161120
lerc_options: Option<LercOptions>,
11171121
jpeg_options: Option<JpegOptions>,
1122+
deflate_level: Option<u32>,
11181123
overview_levels: Vec<u32>,
11191124
resampling: Resampling,
11201125
fill_value: T,
@@ -1159,6 +1164,7 @@ impl<T: NumericSample, W: Write + Seek> CogTileWriter<T, W> {
11591164
predictor: cog.inner.predictor,
11601165
lerc_options: cog.inner.lerc_options,
11611166
jpeg_options: cog.inner.jpeg_options,
1167+
deflate_level: cog.inner.deflate_level,
11621168
overview_levels,
11631169
resampling: cog.resampling,
11641170
fill_value,
@@ -1347,6 +1353,7 @@ impl<T: NumericSample, W: Write + Seek> CogTileWriter<T, W> {
13471353
predictor: self.predictor,
13481354
lerc_options: self.lerc_options,
13491355
jpeg_options: self.jpeg_options,
1356+
deflate_level: self.deflate_level,
13501357
};
13511358
let grid = RawTileGrid {
13521359
tile_width: tw,
@@ -1614,6 +1621,7 @@ where
16141621
block_height: plan.tile_height as u32,
16151622
lerc_options: plan.lerc_options,
16161623
jpeg_options: plan.jpeg_options,
1624+
deflate_level: plan.deflate_level,
16171625
},
16181626
)?;
16191627
}
@@ -1637,6 +1645,7 @@ where
16371645
block_height: plan.tile_height as u32,
16381646
lerc_options: plan.lerc_options,
16391647
jpeg_options: plan.jpeg_options,
1648+
deflate_level: plan.deflate_level,
16401649
},
16411650
)?;
16421651
}
@@ -1698,6 +1707,7 @@ fn spool_base_blocks_from_store<T: NumericSample>(
16981707
block_height: plan.tile_height as u32,
16991708
lerc_options: plan.lerc_options,
17001709
jpeg_options: plan.jpeg_options,
1710+
deflate_level: plan.deflate_level,
17011711
},
17021712
)?;
17031713
}
@@ -1725,6 +1735,7 @@ fn spool_base_blocks_from_store<T: NumericSample>(
17251735
block_height: plan.tile_height as u32,
17261736
lerc_options: plan.lerc_options,
17271737
jpeg_options: plan.jpeg_options,
1738+
deflate_level: plan.deflate_level,
17281739
},
17291740
)?;
17301741
}
@@ -1800,6 +1811,7 @@ fn spool_tiled_data_3d<T: NumericSample>(
18001811
block_height: th as u32,
18011812
lerc_options: plan.lerc_options,
18021813
jpeg_options: plan.jpeg_options,
1814+
deflate_level: plan.deflate_level,
18031815
},
18041816
)?;
18051817
}
@@ -1835,6 +1847,7 @@ fn spool_tiled_data_3d<T: NumericSample>(
18351847
block_height: th as u32,
18361848
lerc_options: plan.lerc_options,
18371849
jpeg_options: plan.jpeg_options,
1850+
deflate_level: plan.deflate_level,
18381851
},
18391852
)?;
18401853
}

integration-tests/tiff-integration/tests/tiff_writer_roundtrip.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,3 +673,54 @@ fn explicit_bigtiff_roundtrips_small_images() {
673673
assert_eq!(offset, Some(0));
674674
assert_eq!(values, vec![1, 2, 3, 4]);
675675
}
676+
677+
#[test]
678+
fn deflate_level_controls_output_size_and_roundtrips() {
679+
use std::io::Cursor;
680+
use tiff_writer::{TiffWriter, WriteOptions};
681+
682+
let samples: Vec<u16> = (0..64 * 64)
683+
.map(|index| ((index / 7) % 500) as u16)
684+
.collect();
685+
686+
let mut sizes = Vec::new();
687+
for level in [1u32, 9] {
688+
let mut writer = TiffWriter::new(Cursor::new(Vec::new()), WriteOptions::default()).unwrap();
689+
let handle = writer
690+
.add_image(
691+
ImageBuilder::new(64, 64)
692+
.sample_type::<u16>()
693+
.compression(Compression::Deflate)
694+
.deflate_level(level)
695+
.strips(64),
696+
)
697+
.unwrap();
698+
writer.write_block(&handle, 0, &samples).unwrap();
699+
let bytes = writer.finish().unwrap().into_inner();
700+
701+
let file = tiff_reader::TiffFile::from_bytes(bytes.clone()).unwrap();
702+
let decoded = file.read_image::<u16>(0).unwrap();
703+
assert_eq!(decoded.into_raw_vec_and_offset().0, samples);
704+
sizes.push(bytes.len());
705+
}
706+
assert!(
707+
sizes[1] <= sizes[0],
708+
"level 9 output ({}) should not exceed level 1 output ({})",
709+
sizes[1],
710+
sizes[0]
711+
);
712+
713+
let err = ImageBuilder::new(16, 16)
714+
.compression(Compression::Deflate)
715+
.deflate_level(12)
716+
.validate()
717+
.unwrap_err();
718+
assert!(err.to_string().contains("deflate_level"), "{err}");
719+
720+
let err = ImageBuilder::new(16, 16)
721+
.compression(Compression::Lzw)
722+
.deflate_level(6)
723+
.validate()
724+
.unwrap_err();
725+
assert!(err.to_string().contains("Deflate compression"), "{err}");
726+
}

tiff-reader/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ lerc-reader = { workspace = true }
1919
ndarray = { workspace = true }
2020
thiserror = { workspace = true }
2121
memmap2 = { workspace = true }
22-
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
22+
flate2 = { version = "1.0.35", default-features = false, features = ["zlib-rs"] }
2323
weezl = "0.1"
2424
lru = "0.16.3"
2525
parking_lot = "0.12"

tiff-reader/src/filters.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ fn read_bounded_to_end<R: Read>(
287287
) -> Result<Vec<u8>> {
288288
let probe_limit = decoded_len_probe_limit(index, codec, decoded_len_limit)?;
289289
let mut reader = reader.take(probe_limit as u64);
290-
let mut out = Vec::with_capacity(decoded_len_limit.min(8192));
291-
let mut scratch = [0u8; 8192];
290+
let mut out = Vec::with_capacity(decoded_len_limit.min(64 * 1024));
291+
let mut scratch = vec![0u8; probe_limit.min(64 * 1024)];
292292

293293
loop {
294294
let remaining = probe_limit.saturating_sub(out.len());

tiff-writer/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ tiff-core = { version = "0.7.0", path = "../tiff-core" }
1717
lerc-core = { workspace = true }
1818
lerc-writer = { workspace = true }
1919
thiserror = { workspace = true }
20-
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
20+
flate2 = { version = "1.0.35", default-features = false, features = ["zlib-rs"] }
2121
weezl = "0.1"
2222

2323
[features]

tiff-writer/src/builder.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ pub struct ImageBuilder {
7070
pub(crate) subfile_type: u32,
7171
pub(crate) lerc_options: Option<LercOptions>,
7272
pub(crate) jpeg_options: Option<JpegOptions>,
73+
pub(crate) deflate_level: Option<u32>,
7374
}
7475

7576
impl ImageBuilder {
@@ -97,6 +98,7 @@ impl ImageBuilder {
9798
subfile_type: 0,
9899
lerc_options: None,
99100
jpeg_options: None,
101+
deflate_level: None,
100102
}
101103
}
102104

@@ -145,6 +147,16 @@ impl ImageBuilder {
145147
self
146148
}
147149

150+
/// Set the Deflate compression level (0-9).
151+
///
152+
/// Applies to `Compression::Deflate`/`Compression::DeflateOld` blocks.
153+
/// The additional Deflate layer of `LERC+Deflate` always uses the codec
154+
/// default level.
155+
pub fn deflate_level(mut self, level: u32) -> Self {
156+
self.deflate_level = Some(level);
157+
self
158+
}
159+
148160
pub fn photometric(mut self, p: PhotometricInterpretation) -> Self {
149161
self.photometric = p;
150162
self
@@ -756,6 +768,21 @@ impl ImageBuilder {
756768
.into(),
757769
));
758770
}
771+
if let Some(level) = self.deflate_level {
772+
if level > 9 {
773+
return Err(crate::error::Error::InvalidConfig(format!(
774+
"deflate_level must be 0-9, got {level}"
775+
)));
776+
}
777+
if !matches!(
778+
self.compression,
779+
Compression::Deflate | Compression::DeflateOld
780+
) {
781+
return Err(crate::error::Error::InvalidConfig(
782+
"deflate_level requires Deflate compression".into(),
783+
));
784+
}
785+
}
759786
self.validate_color_model()?;
760787
if matches!(self.compression, Compression::Jpeg) {
761788
self.validate_jpeg_config()?;

0 commit comments

Comments
 (0)