use otf_pixels_core::{
Codec, DecodeCapability, Decoder, Format, ImageDescriptor, Limits, Orientation, PixelFormat,
PixelsError, Region, Result, Source, TileMut,
};
use crate::ifd::{ByteOrder, Directory, parse_header, probe as probe_header};
use crate::image::{Layout, Photometric, TiffImage};
#[derive(Debug)]
pub struct TiffDecoder {
data: Vec<u8>,
image: TiffImage,
row: u32,
cached: Option<(usize, Vec<u8>)>,
}
impl TiffDecoder {
pub fn new<S: Source>(mut source: S, limits: Limits) -> Result<Self> {
let mut data = Vec::new();
let mut buffer = vec![0_u8; 256 * 1024];
loop {
let read = source.read(&mut buffer)?;
if read == 0 {
break;
}
let Some(chunk) = buffer.get(..read) else {
break;
};
data.extend_from_slice(chunk);
}
let (order, first_ifd) = parse_header(&data)?;
let directory = Directory::parse(&data, order, first_ifd)?;
let image = TiffImage::from_directory(&directory, order, &limits)?;
Ok(Self {
data,
image,
row: 0,
cached: None,
})
}
#[must_use]
pub const fn image(&self) -> &TiffImage {
&self.image
}
#[must_use]
pub const fn byte_order(&self) -> ByteOrder {
self.image.order
}
fn ensure_chunk(&mut self, index: usize) -> Result<()> {
if matches!(&self.cached, Some((cached, _)) if *cached == index) {
return Ok(());
}
let decoded = self.image.read_chunk(&self.data, index)?;
self.cached = Some((index, decoded));
Ok(())
}
fn blit_chunk(
&mut self,
column: u32,
chunk_row: u32,
region: Region,
out: &mut TileMut<'_>,
) -> Result<()> {
let across = self.image.chunks_across();
let index = (chunk_row * across + column) as usize;
if index >= self.image.offsets.len() {
return Err(PixelsError::malformed(
"tiff",
format!(
"chunk {index} is beyond the {} declared",
self.image.offsets.len()
),
));
}
self.ensure_chunk(index)?;
let Self { image, cached, .. } = self;
let Some((_, data)) = cached.as_ref() else {
return Err(PixelsError::graph("tiff chunk vanished after decoding"));
};
let area = image.chunk_region(column, chunk_row);
let (stored_width, _) = image.chunk_stored_size();
let stored_row_bytes = image.chunk_row_bytes();
let left = area.x.max(region.x);
let top = area.y.max(region.y);
let right = (area.x + area.width).min(region.x + region.width);
let bottom = (area.y + area.height).min(region.y + region.height);
if right <= left || bottom <= top {
return Ok(());
}
let mut expanded =
vec![0_u8; (right - left) as usize * image.descriptor.pixel.bytes_per_pixel()];
for y in top..bottom {
let within = (y - area.y) as usize;
let start = within * stored_row_bytes;
let Some(stored) = data.get(start..start + stored_row_bytes) else {
continue;
};
expand_row(
image,
stored,
(left - area.x) as usize,
(right - left) as usize,
stored_width as usize,
&mut expanded,
);
let Some(target) = out.row_mut(y) else {
continue;
};
let bpp = image.descriptor.pixel.bytes_per_pixel();
let at = (left - region.x) as usize * bpp;
let Some(slot) = target.get_mut(at..at + expanded.len()) else {
continue;
};
slot.copy_from_slice(&expanded);
}
Ok(())
}
fn read_region_into(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
if region.x + region.width > self.image.descriptor.width
|| region.y + region.height > self.image.descriptor.height
{
return Err(PixelsError::invalid_argument(
"region",
format!(
"{region} is outside a {}x{} image",
self.image.descriptor.width, self.image.descriptor.height
),
));
}
let (first_column, last_column, first_row, last_row) = self.chunks_covering(region);
for chunk_row in first_row..=last_row {
for column in first_column..=last_column {
self.blit_chunk(column, chunk_row, region, out)?;
}
}
Ok(())
}
fn chunks_covering(&self, region: Region) -> (u32, u32, u32, u32) {
match self.image.layout {
Layout::Strips { rows_per_strip } => {
let first = region.y / rows_per_strip;
let last = (region.y + region.height.saturating_sub(1)) / rows_per_strip;
(0, 0, first, last.min(self.image.chunks_down() - 1))
}
Layout::Tiles { width, height } => {
let first_column = region.x / width;
let last_column = (region.x + region.width.saturating_sub(1)) / width;
let first_row = region.y / height;
let last_row = (region.y + region.height.saturating_sub(1)) / height;
(
first_column,
last_column.min(self.image.chunks_across() - 1),
first_row,
last_row.min(self.image.chunks_down() - 1),
)
}
}
}
}
fn expand_row(
image: &TiffImage,
stored: &[u8],
from: usize,
count: usize,
stored_width: usize,
out: &mut [u8],
) {
let bits = image.bits_per_sample as usize;
let channels = image.samples_per_pixel as usize;
let format = image.descriptor.pixel;
let bpp = format.bytes_per_pixel();
let maximum = if bits >= 16 {
65535_u32
} else {
(1_u32 << bits) - 1
};
for index in 0..count {
let x = from + index;
if x >= stored_width {
break;
}
let Some(target) = out.get_mut(index * bpp..(index + 1) * bpp) else {
break;
};
let mut samples = [0_u32; 4];
for (channel, slot) in samples.iter_mut().enumerate().take(channels.min(4)) {
*slot = read_sample(stored, x * channels + channel, bits, image.order);
}
match image.photometric {
Photometric::Palette => {
let entries = 1_usize << bits;
let index = samples[0] as usize;
for channel in 0..3 {
let value = image
.color_map
.get(channel * entries + index)
.copied()
.unwrap_or(0);
if let Some(slot) = target.get_mut(channel) {
*slot = (value >> 8) as u8;
}
}
}
_ => {
for (channel, &sample) in samples.iter().enumerate().take(channels.min(4)) {
let mut value = sample;
let is_colour = channel < 3;
if image.photometric == Photometric::WhiteIsZero && is_colour {
value = maximum.saturating_sub(value);
}
write_sample(target, channel, value, bits, maximum, format);
}
}
}
}
}
fn read_sample(data: &[u8], index: usize, bits: usize, order: ByteOrder) -> u32 {
match bits {
16 => u32::from(order.u16(data, index * 2)),
8 => u32::from(data.get(index).copied().unwrap_or(0)),
1 | 2 | 4 => {
let per_byte = 8 / bits;
let byte = data.get(index / per_byte).copied().unwrap_or(0);
let shift = 8 - bits * (index % per_byte + 1);
u32::from((byte >> shift) & ((1_u16 << bits) - 1) as u8)
}
_ => 0,
}
}
fn write_sample(
target: &mut [u8],
channel: usize,
value: u32,
bits: usize,
maximum: u32,
format: PixelFormat,
) {
let widened = if bits >= 8 {
value
} else {
(value * 255 + maximum / 2) / maximum.max(1)
};
match format.sample_kind() {
otf_pixels_core::SampleKind::U16 => {
let scaled = widened as u16;
for (offset, byte) in scaled.to_ne_bytes().iter().enumerate() {
if let Some(slot) = target.get_mut(channel * 2 + offset) {
*slot = *byte;
}
}
}
_ => {
if let Some(slot) = target.get_mut(channel) {
*slot = widened.min(255) as u8;
}
}
}
}
impl Decoder for TiffDecoder {
fn descriptor(&self) -> ImageDescriptor {
self.image.descriptor
}
fn orientation(&self) -> Orientation {
self.image.orientation
}
fn icc_profile(&self) -> Option<&[u8]> {
self.image.icc.as_deref()
}
fn capability(&self) -> DecodeCapability {
if self.image.layout.is_random_access() {
DecodeCapability::Regions
} else {
DecodeCapability::Sequential
}
}
fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
if self.row >= self.image.descriptor.height {
return Err(PixelsError::invalid_argument(
"out",
format!(
"all {} rows have already been read",
self.image.descriptor.height
),
));
}
let row_bytes = self.image.descriptor.row_bytes();
if out.len() != row_bytes {
return Err(PixelsError::invalid_argument(
"out",
format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
));
}
let region = Region::new(0, self.row, self.image.descriptor.width, 1);
let pixel = self.image.descriptor.pixel;
let mut tile = TileMut::new(region, pixel, row_bytes, out)?;
self.read_region_into(region, &mut tile)?;
self.row += 1;
Ok(())
}
fn read_region(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
if !self.image.layout.is_random_access() {
return Err(PixelsError::unsupported(
"this TIFF is stored in strips; region decode requires tiles",
));
}
self.read_region_into(region, out)
}
}
#[must_use]
pub fn probe(prefix: &[u8]) -> bool {
probe_header(prefix)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TiffCodec;
impl Codec for TiffCodec {
fn format(&self) -> Format {
Format::Tiff
}
fn magic_len(&self) -> usize {
8
}
fn probe(&self, prefix: &[u8]) -> bool {
probe(prefix)
}
}