Skip to content

added DecodingResult struct, changed decoding and predictor API to allow in-place #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/bytecast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Trivial, internal byte transmutation.
//!
//! A dependency like bytemuck would give us extra assurance of the safety but overall would not
//! reduce the amount of total unsafety. We don't use it in the interface where the traits would
//! really become useful.
//!
//! SAFETY: These are benign casts as we apply them to fixed size integer types only. All of them
//! are naturally aligned, valid for all bit patterns and their alignment is surely at most their
//! size (we assert the latter fact since it is 'implementation defined' if following the letter of
//! the unsafe code guidelines).
//!
//! TODO: Would like to use std-lib here.
// from image-tiff src/bytecast.rs
// see https://stackoverflow.com/a/29042896/14681457
use std::{mem, slice};

macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => {
pub(crate) fn $const(slice: &[$int]) -> &[u8] {
assert!(mem::align_of::<$int>() <= mem::size_of::<$int>());
unsafe { slice::from_raw_parts(slice.as_ptr() as *const u8, mem::size_of_val(slice)) }
}
$(pub(crate) fn $mut(slice: &mut [$int]) -> &mut [u8] {
assert!(mem::align_of::<$int>() <= mem::size_of::<$int>());
unsafe { slice::from_raw_parts_mut(slice.as_mut_ptr() as *mut u8, mem::size_of_val(slice)) }
})*
}}
Comment on lines +17 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a pretty strong 👎 on having any unsafe in our code, especially for a transmute like this.


integral_slice_as_bytes!(i8, i8_as_ne_bytes, i8_as_ne_mut_bytes);
integral_slice_as_bytes!(u16, u16_as_ne_bytes, u16_as_ne_mut_bytes);
integral_slice_as_bytes!(i16, i16_as_ne_bytes, i16_as_ne_mut_bytes);
integral_slice_as_bytes!(u32, u32_as_ne_bytes, u32_as_ne_mut_bytes);
integral_slice_as_bytes!(i32, i32_as_ne_bytes, i32_as_ne_mut_bytes);
integral_slice_as_bytes!(u64, u64_as_ne_bytes, u64_as_ne_mut_bytes);
integral_slice_as_bytes!(i64, i64_as_ne_bytes, i64_as_ne_mut_bytes);
integral_slice_as_bytes!(f32, f32_as_ne_bytes, f32_as_ne_mut_bytes);
integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes);
2 changes: 1 addition & 1 deletion src/cog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ mod test {
let ifd = &tiff.ifds[1];
let tile = ifd.fetch_tile(0, 0, reader.as_ref()).await.unwrap();
let tile = tile.decode(&Default::default()).unwrap();
std::fs::write("img.buf", tile).unwrap();
std::fs::write("img.buf", tile.as_u8_buf()).unwrap();
}

#[ignore = "local file"]
Expand Down
69 changes: 46 additions & 23 deletions src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::io::{Cursor, Read};
use bytes::Bytes;
use flate2::bufread::ZlibDecoder;

use crate::error::AsyncTiffResult;
use crate::error::{AsyncTiffError, AsyncTiffResult};
use crate::tiff::tags::{CompressionMethod, PhotometricInterpretation};
use crate::tiff::{TiffError, TiffUnsupportedError};

Expand Down Expand Up @@ -54,10 +54,11 @@ pub trait Decoder: Debug + Send + Sync {
/// Decode a TIFF tile.
fn decode_tile(
&self,
buffer: Bytes,
compressed_buffer: Bytes,
result_buffer: &mut [u8],
photometric_interpretation: PhotometricInterpretation,
jpeg_tables: Option<&[u8]>,
) -> AsyncTiffResult<Bytes>;
) -> AsyncTiffResult<()>;
}

/// A decoder for the Deflate compression method.
Expand All @@ -67,14 +68,14 @@ pub struct DeflateDecoder;
impl Decoder for DeflateDecoder {
fn decode_tile(
&self,
buffer: Bytes,
compressed_buffer: Bytes,
result_buffer: &mut [u8],
Comment on lines +71 to +72
Copy link
Member

@kylebarron kylebarron Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me that passing in a result buffer is a meaningful improvement. I'd much rather use bytes::Bytes (i.e. an Arc<Vec<u8>>) that provides cheap cloning. (I.e. keep the existing API)

_photometric_interpretation: PhotometricInterpretation,
_jpeg_tables: Option<&[u8]>,
) -> AsyncTiffResult<Bytes> {
let mut decoder = ZlibDecoder::new(Cursor::new(buffer));
let mut buf = Vec::new();
decoder.read_to_end(&mut buf)?;
Ok(buf.into())
) -> AsyncTiffResult<()> {
let mut decoder = ZlibDecoder::new(Cursor::new(compressed_buffer));
decoder.read_exact(result_buffer)?;
Ok(())
}
}

Expand All @@ -85,11 +86,17 @@ pub struct JPEGDecoder;
impl Decoder for JPEGDecoder {
fn decode_tile(
&self,
buffer: Bytes,
compressed_buffer: Bytes,
result_buffer: &mut [u8],
photometric_interpretation: PhotometricInterpretation,
jpeg_tables: Option<&[u8]>,
) -> AsyncTiffResult<Bytes> {
decode_modern_jpeg(buffer, photometric_interpretation, jpeg_tables)
) -> AsyncTiffResult<()> {
decode_modern_jpeg(
compressed_buffer,
result_buffer,
photometric_interpretation,
jpeg_tables,
)
}
}

Expand All @@ -100,14 +107,23 @@ pub struct LZWDecoder;
impl Decoder for LZWDecoder {
fn decode_tile(
&self,
buffer: Bytes,
compressed_buffer: Bytes,
result_buffer: &mut [u8],
_photometric_interpretation: PhotometricInterpretation,
_jpeg_tables: Option<&[u8]>,
) -> AsyncTiffResult<Bytes> {
) -> AsyncTiffResult<()> {
// https://github.com/image-rs/image-tiff/blob/90ae5b8e54356a35e266fb24e969aafbcb26e990/src/decoder/stream.rs#L147
let mut decoder = weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8);
let decoded = decoder.decode(&buffer).expect("failed to decode LZW data");
Ok(decoded.into())
let buf_res = decoder.decode_bytes(&compressed_buffer, result_buffer);
match buf_res.status {
Err(e) => Err(AsyncTiffError::External(Box::new(e))),
Ok(lzw_status) => match lzw_status {
weezl::LzwStatus::Ok | weezl::LzwStatus::Done => Ok(()),
weezl::LzwStatus::NoProgress => Err(AsyncTiffError::General(
"Internal LZW decoder reported no progress".into(),
)),
},
}
}
}

Expand All @@ -118,20 +134,25 @@ pub struct UncompressedDecoder;
impl Decoder for UncompressedDecoder {
fn decode_tile(
&self,
buffer: Bytes,
compressed_buffer: Bytes,
result_buffer: &mut [u8],
_photometric_interpretation: PhotometricInterpretation,
_jpeg_tables: Option<&[u8]>,
) -> AsyncTiffResult<Bytes> {
Ok(buffer)
) -> AsyncTiffResult<()> {
assert_eq!(compressed_buffer.len(), result_buffer.len());
// we still need to copy into the typed array
result_buffer.copy_from_slice(&compressed_buffer);
Ok(())
}
}

// https://github.com/image-rs/image-tiff/blob/3bfb43e83e31b0da476832067ada68a82b378b7b/src/decoder/image.rs#L389-L450
fn decode_modern_jpeg(
buf: Bytes,
compressed_buffer: Bytes,
result_buffer: &mut [u8],
photometric_interpretation: PhotometricInterpretation,
jpeg_tables: Option<&[u8]>,
) -> AsyncTiffResult<Bytes> {
) -> AsyncTiffResult<()> {
// Construct new jpeg_reader wrapping a SmartReader.
//
// JPEG compression in TIFF allows saving quantization and/or huffman tables in one central
Expand All @@ -142,7 +163,7 @@ fn decode_modern_jpeg(
// data is removed because it follows `jpeg_tables`. Similary, `jpeg_tables` ends with a `EOI`
// (HEX: `0xFFD9`) or __end of image__ marker, this has to be removed as well (last two bytes
// of `jpeg_tables`).
let reader = Cursor::new(buf);
let reader = Cursor::new(compressed_buffer);

let jpeg_reader = match jpeg_tables {
Some(jpeg_tables) => {
Expand Down Expand Up @@ -177,5 +198,7 @@ fn decode_modern_jpeg(
}

let data = decoder.decode()?;
Ok(data.into())
// jpeg decoder doesn't support decoding into a buffer -> copy
result_buffer.copy_from_slice(&data);
Ok(())
}
123 changes: 123 additions & 0 deletions src/decoding_result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use crate::{
bytecast::*,
error::{AsyncTiffError, AsyncTiffResult},
predictor::PredictorInfo,
tiff::{tags::SampleFormat, TiffError, TiffUnsupportedError},
};

/// Result of a decoding process
#[derive(Debug)]
#[non_exhaustive]
pub enum DecodingResult {
/// A vector of unsigned bytes
U8(Vec<u8>),
/// A vector of unsigned words
U16(Vec<u16>),
/// A vector of 32 bit unsigned ints
U32(Vec<u32>),
/// A vector of 64 bit unsigned ints
U64(Vec<u64>),
/// A vector of 32 bit IEEE floats
F32(Vec<f32>),
/// A vector of 64 bit IEEE floats
F64(Vec<f64>),
/// A vector of 8 bit signed ints
I8(Vec<i8>),
/// A vector of 16 bit signed ints
I16(Vec<i16>),
/// A vector of 32 bit signed ints
I32(Vec<i32>),
/// A vector of 64 bit signed ints
I64(Vec<i64>),
}

impl DecodingResult {
/// use this result as a `&mut[u8]` buffer
pub fn as_mut_u8_buf(&mut self) -> &mut [u8] {
match self {
DecodingResult::U8(v) => v,
DecodingResult::U16(v) => u16_as_ne_mut_bytes(v),
DecodingResult::U32(v) => u32_as_ne_mut_bytes(v),
DecodingResult::U64(v) => u64_as_ne_mut_bytes(v),
DecodingResult::I8(v) => i8_as_ne_mut_bytes(v),
DecodingResult::I16(v) => i16_as_ne_mut_bytes(v),
DecodingResult::I32(v) => i32_as_ne_mut_bytes(v),
DecodingResult::I64(v) => i64_as_ne_mut_bytes(v),
DecodingResult::F32(v) => f32_as_ne_mut_bytes(v),
DecodingResult::F64(v) => f64_as_ne_mut_bytes(v),
}
}

/// use this result as a `&[u8]` buffer
pub fn as_u8_buf(&self) -> &[u8] {
match self {
DecodingResult::U8(v) => v,
DecodingResult::U16(v) => u16_as_ne_bytes(v),
DecodingResult::U32(v) => u32_as_ne_bytes(v),
DecodingResult::U64(v) => u64_as_ne_bytes(v),
DecodingResult::I8(v) => i8_as_ne_bytes(v),
DecodingResult::I16(v) => i16_as_ne_bytes(v),
DecodingResult::I32(v) => i32_as_ne_bytes(v),
DecodingResult::I64(v) => i64_as_ne_bytes(v),
DecodingResult::F32(v) => f32_as_ne_bytes(v),
DecodingResult::F64(v) => f64_as_ne_bytes(v),
}
}

/// create a properly sized `Self` from PredictorInfo struct for a single chunk (tile/strip)
// similar to image-tiff's Decoder::result_buffer
pub fn from_predictor_info(
info: PredictorInfo,
chunk_x: usize,
chunk_y: usize,
) -> AsyncTiffResult<Self> {
// this part is outside of result_buffer

// since the calculations are in pixels rather than bytes (predictors
// use bytes), we do them here, rather than adding even more functions
// to PredictorInfo
let width = info.chunk_width_pixels(chunk_x as _)?;
let height = info.output_rows(chunk_y as _)?;

let buffer_size = match (width as usize)
.checked_mul(height)
.and_then(|x| x.checked_mul(info.samples_per_pixel as _))
{
Some(s) => s,
None => return Err(AsyncTiffError::InternalTIFFError(TiffError::IntSizeError)),
};

let max_sample_bits = info.bits_per_sample();

match info.sample_format {
SampleFormat::Uint => match max_sample_bits {
n if n <= 8 => Ok(DecodingResult::U8(vec![0u8; buffer_size])),
n if n <= 16 => Ok(DecodingResult::U16(vec![0u16; buffer_size])),
n if n <= 32 => Ok(DecodingResult::U32(vec![0u32; buffer_size])),
n if n <= 64 => Ok(DecodingResult::U64(vec![0u64; buffer_size])),
n => Err(AsyncTiffError::InternalTIFFError(
TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedBitsPerChannel(n)),
)),
},
SampleFormat::Int => match max_sample_bits {
n if n <= 8 => Ok(DecodingResult::I8(vec![0i8; buffer_size])),
n if n <= 16 => Ok(DecodingResult::I16(vec![0i16; buffer_size])),
n if n <= 32 => Ok(DecodingResult::I32(vec![0i32; buffer_size])),
n if n <= 64 => Ok(DecodingResult::I64(vec![0i64; buffer_size])),
n => Err(AsyncTiffError::InternalTIFFError(
TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedBitsPerChannel(n)),
)),
},
SampleFormat::IEEEFP => match max_sample_bits {
32 => Ok(DecodingResult::F32(vec![0f32; buffer_size])),
64 => Ok(DecodingResult::F64(vec![0f64; buffer_size])),
n => Err(AsyncTiffError::InternalTIFFError(
TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedBitsPerChannel(n)),
)),
},
format => Err(AsyncTiffError::InternalTIFFError(
TiffUnsupportedError::UnsupportedSampleFormat(vec![format]).into(),
)),
}
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

pub mod reader;
// TODO: maybe rename this mod
pub(crate) mod bytecast;
mod cog;
pub mod decoder;
mod decoding_result;
pub mod error;
pub mod geo;
mod ifd;
Expand All @@ -14,5 +16,6 @@ pub mod tiff;
mod tile;

pub use cog::TIFF;
pub use decoding_result::DecodingResult;
pub use ifd::ImageFileDirectory;
pub use tile::Tile;
Loading
Loading