use otf_pixels_core::{
Codec, DecodeCapability, Decoder, Format, ImageDescriptor, Limits, Orientation, PixelFormat,
PixelsError, Region, Result, Source, TileMut,
};
use crate::ifd::{ByteOrder, Directory, directory_chain, parse_header, probe as probe_header};
use crate::image::{Compression, Layout, Photometric, TiffImage};
use otf_pixels_core::exif::Exif;
use otf_pixels_core::{Density, ResolutionUnit, SourceInfo};
#[derive(Debug)]
pub struct TiffDecoder {
data: Vec<u8>,
image: TiffImage,
pages: u32,
row: u32,
cached: Option<(usize, Vec<u8>)>,
exif: Option<Vec<u8>>,
xmp: Option<Vec<u8>>,
info: SourceInfo,
}
pub(crate) const fn is_metadata_tag(tag: u16) -> bool {
matches!(
tag,
269 | 270
| 271
| 272
| 274
| 282
| 283
| 285
| 296
| 305
| 306
| 315
| 316
| 318
| 319
| 33_432
)
}
impl TiffDecoder {
pub fn new<S: Source>(source: S, limits: Limits) -> Result<Self> {
Self::with_page(source, limits, 0)
}
pub fn with_page<S: Source>(mut source: S, limits: Limits, page: u32) -> 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 chain = directory_chain(&data, order, first_ifd);
let pages = u32::try_from(chain.len()).unwrap_or(u32::MAX).max(1);
let offset = match page {
0 => first_ifd,
_ => chain.get(page as usize).copied().ok_or_else(|| {
PixelsError::invalid_argument(
"page",
format!("page {page} was asked for; the file has {pages} (0 is the first)"),
)
})?,
};
let directory = Directory::parse(&data, order, offset)?;
let image = TiffImage::from_directory(&directory, order, &limits)?;
let tree = Exif::parse_directory(&data, Some(offset));
let xmp = tree
.as_ref()
.and_then(|t| t.get(700))
.map(|e| e.data.clone());
let mut info = SourceInfo::default();
if let Some(t) = &tree {
info.density = match (t.rational(282), t.integer(296).unwrap_or(2)) {
(Some(x), unit @ (2 | 3)) if x > 0.0 => Some(Density {
x,
y: t.rational(283).filter(|&y| y > 0.0).unwrap_or(x),
unit: if unit == 3 {
ResolutionUnit::Centimetre
} else {
ResolutionUnit::Inch
},
}),
_ => None,
};
}
let exif = tree
.map(|t| t.retain(is_metadata_tag))
.filter(|t| !t.ifd0.is_empty())
.map(|t| t.to_bytes());
info.bits_per_sample = u8::try_from(image.bits_per_sample).ok();
info.palette = image.photometric == Photometric::Palette;
info.compression = Some(match image.compression {
Compression::None => "none",
Compression::Lzw => "lzw",
Compression::Deflate => "deflate",
Compression::PackBits => "packbits",
});
Ok(Self {
data,
image,
pages,
row: 0,
cached: None,
exif,
xmp,
info,
})
}
#[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 pages(&self) -> u32 {
self.pages
}
fn orientation(&self) -> Orientation {
self.image.orientation
}
fn icc_profile(&self) -> Option<&[u8]> {
self.image.icc.as_deref()
}
fn exif(&self) -> Option<&[u8]> {
self.exif.as_deref()
}
fn xmp(&self) -> Option<&[u8]> {
self.xmp.as_deref()
}
fn source_info(&self) -> SourceInfo {
self.info.clone()
}
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)
}
}