use crate::ccitt::{encode_ccitt, CcittVariant, FillOrder};
use crate::compress::{pack_deflate, pack_lzw, pack_packbits, pack_zstd};
use crate::error::{Result, TiffError as Error};
use crate::types::*;
pub type RgbColor = [u8; 3];
#[derive(Debug, Clone)]
pub struct EncodePage<'a> {
pub width: u32,
pub height: u32,
pub kind: EncodePixelFormat<'a>,
pub compression: TiffCompression,
pub predictor: bool,
pub planar: bool,
pub tiling: Option<(u32, u32)>,
pub bigtiff: bool,
}
#[derive(Debug, Clone)]
pub enum EncodePixelFormat<'a> {
Bilevel { pixels: &'a [u8] },
TransparencyMask { pixels: &'a [u8] },
Gray8 { pixels: &'a [u8] },
Gray16Le { pixels: &'a [u8] },
Rgb24 { pixels: &'a [u8] },
Palette8 {
indices: &'a [u8],
palette: &'a [RgbColor],
},
CieLab8 { pixels: &'a [u8] },
CieLabL8 { pixels: &'a [u8] },
Cmyk32 { pixels: &'a [u8] },
YCbCr24 { pixels: &'a [u8] },
YCbCrSubsampled24 {
pixels: &'a [u8],
subsampling: (u16, u16),
},
}
#[derive(Debug, Clone, Copy)]
pub enum TiffCompression {
None,
PackBits,
Lzw,
Deflate,
Zstd,
CcittRle,
CcittT4OneD {
eol_byte_aligned: bool,
},
CcittT4TwoD {
eol_byte_aligned: bool,
},
CcittT6,
}
impl TiffCompression {
fn tag_value(self) -> u16 {
match self {
TiffCompression::None => COMPRESSION_NONE,
TiffCompression::PackBits => COMPRESSION_PACKBITS,
TiffCompression::Lzw => COMPRESSION_LZW,
TiffCompression::Deflate => COMPRESSION_DEFLATE_ADOBE,
TiffCompression::Zstd => COMPRESSION_ZSTD,
TiffCompression::CcittRle => COMPRESSION_CCITT_HUFFMAN,
TiffCompression::CcittT4OneD { .. } | TiffCompression::CcittT4TwoD { .. } => {
COMPRESSION_CCITT_T4
}
TiffCompression::CcittT6 => COMPRESSION_CCITT_T6,
}
}
fn pack(self, raw: &[u8], width: u32, rows: u32) -> Result<Vec<u8>> {
match self {
TiffCompression::None => Ok(raw.to_vec()),
TiffCompression::PackBits => Ok(pack_packbits(raw)),
TiffCompression::Lzw => Ok(pack_lzw(raw)),
TiffCompression::Deflate => pack_deflate(raw),
TiffCompression::Zstd => pack_zstd(raw),
TiffCompression::CcittRle => encode_ccitt(
raw,
width,
rows,
CcittVariant::ModifiedHuffman,
FillOrder::MsbFirst,
),
TiffCompression::CcittT4OneD { eol_byte_aligned } => encode_ccitt(
raw,
width,
rows,
CcittVariant::T4OneD { eol_byte_aligned },
FillOrder::MsbFirst,
),
TiffCompression::CcittT4TwoD { eol_byte_aligned } => encode_ccitt(
raw,
width,
rows,
CcittVariant::T4TwoD { eol_byte_aligned },
FillOrder::MsbFirst,
),
TiffCompression::CcittT6 => {
encode_ccitt(raw, width, rows, CcittVariant::T6, FillOrder::MsbFirst)
}
}
}
fn is_ccitt(self) -> bool {
matches!(
self,
TiffCompression::CcittRle
| TiffCompression::CcittT4OneD { .. }
| TiffCompression::CcittT4TwoD { .. }
| TiffCompression::CcittT6
)
}
}
struct PlannedPage {
strips: Vec<Vec<u8>>,
ifd: PageIfd,
externals: Vec<(BlobId, Vec<u8>)>,
}
pub fn encode_tiff(page: &EncodePage<'_>) -> Result<Vec<u8>> {
encode_tiff_multi(std::slice::from_ref(page))
}
pub fn encode_tiff_multi(pages: &[EncodePage<'_>]) -> Result<Vec<u8>> {
if pages.is_empty() {
return Err(Error::invalid("TIFF encode: must supply at least one page"));
}
let bigtiff = pages[0].bigtiff;
if pages.iter().any(|p| p.bigtiff != bigtiff) {
return Err(Error::invalid(
"TIFF encode: encode_tiff_multi pages must all agree on `bigtiff` (cannot mix \
classic-TIFF and BigTIFF IFDs in one file)",
));
}
let header_size: u64 = if bigtiff { 16 } else { 8 };
let entry_size: u64 = if bigtiff { 20 } else { 12 }; let count_size: u64 = if bigtiff { 8 } else { 2 }; let next_ifd_size: u64 = if bigtiff { 8 } else { 4 }; let inline_threshold: usize = if bigtiff { 8 } else { 4 };
let offset_bytes: usize = if bigtiff { 8 } else { 4 }; let array_align: u64 = if bigtiff { 8 } else { 4 };
let mut planned: Vec<PlannedPage> = Vec::with_capacity(pages.len());
for p in pages {
let plan = plan_page_full(p, bigtiff)?;
planned.push(plan);
}
let mut cursor: u64 = header_size;
struct PlannedPageAddr {
strips: Vec<(u64, u64)>,
externals: Vec<(BlobId, u64, u64)>, strip_offsets_array: Option<u64>,
strip_byte_counts_array: Option<u64>,
ifd_offset: u64,
}
let mut addrs: Vec<PlannedPageAddr> = Vec::with_capacity(planned.len());
for plan in &planned {
let mut strip_addrs = Vec::with_capacity(plan.strips.len());
for strip in &plan.strips {
strip_addrs.push((cursor, strip.len() as u64));
cursor += strip.len() as u64;
}
let mut ext_addrs = Vec::with_capacity(plan.externals.len());
for (id, blob) in &plan.externals {
let align: u64 = match id {
BlobId::ReferenceBlackWhite => 4,
BlobId::BitsPerSample | BlobId::ColorMapWords => 2,
};
if cursor % align != 0 {
cursor += align - (cursor % align);
}
ext_addrs.push((*id, cursor, blob.len() as u64));
cursor += blob.len() as u64;
}
let (strip_offsets_array, strip_byte_counts_array) = if plan.strips.len() > 1 {
if cursor % array_align != 0 {
cursor += array_align - (cursor % array_align);
}
let so = cursor;
cursor += array_align * plan.strips.len() as u64;
let sbc = cursor;
cursor += array_align * plan.strips.len() as u64;
(Some(so), Some(sbc))
} else {
(None, None)
};
if cursor % 2 != 0 {
cursor += 1;
}
let ifd_offset = cursor;
let ifd_size = count_size + (plan.ifd.entries.len() as u64) * entry_size + next_ifd_size;
cursor += ifd_size;
addrs.push(PlannedPageAddr {
strips: strip_addrs,
externals: ext_addrs,
strip_offsets_array,
strip_byte_counts_array,
ifd_offset,
});
if !bigtiff && (ifd_offset > u32::MAX as u64 || cursor > u32::MAX as u64) {
return Err(Error::invalid(
"TIFF encode: classic-TIFF 32-bit offset overflow (would need BigTIFF — set \
EncodePage::bigtiff = true)",
));
}
}
let total = cursor as usize;
let mut out = vec![0u8; total];
if bigtiff {
out[0] = b'I';
out[1] = b'I';
out[2..4].copy_from_slice(&BIGTIFF_MAGIC.to_le_bytes());
out[4..6].copy_from_slice(&8u16.to_le_bytes()); out[6..8].copy_from_slice(&0u16.to_le_bytes()); out[8..16].copy_from_slice(&addrs[0].ifd_offset.to_le_bytes());
} else {
out[0] = b'I';
out[1] = b'I';
out[2..4].copy_from_slice(&TIFF_MAGIC.to_le_bytes());
out[4..8].copy_from_slice(&(addrs[0].ifd_offset as u32).to_le_bytes());
}
for (i, (plan, addr)) in planned.iter().zip(addrs.iter()).enumerate() {
for (strip, (off, size)) in plan.strips.iter().zip(addr.strips.iter()) {
assert_eq!(strip.len() as u64, *size);
out[*off as usize..(*off + *size) as usize].copy_from_slice(strip);
}
for (j, (id, off, size)) in addr.externals.iter().enumerate() {
assert_eq!(*id, plan.externals[j].0);
let blob = &plan.externals[j].1;
assert_eq!(blob.len() as u64, *size);
out[*off as usize..(*off + *size) as usize].copy_from_slice(blob);
}
if let Some(so) = addr.strip_offsets_array {
for (k, (off, _)) in addr.strips.iter().enumerate() {
let slot = so as usize + k * (array_align as usize);
if bigtiff {
out[slot..slot + 8].copy_from_slice(&off.to_le_bytes());
} else {
out[slot..slot + 4].copy_from_slice(&(*off as u32).to_le_bytes());
}
}
}
if let Some(sbc) = addr.strip_byte_counts_array {
for (k, (_, size)) in addr.strips.iter().enumerate() {
let slot = sbc as usize + k * (array_align as usize);
if bigtiff {
out[slot..slot + 8].copy_from_slice(&size.to_le_bytes());
} else {
out[slot..slot + 4].copy_from_slice(&(*size as u32).to_le_bytes());
}
}
}
let ifd_off = addr.ifd_offset as usize;
if bigtiff {
out[ifd_off..ifd_off + 8]
.copy_from_slice(&(plan.ifd.entries.len() as u64).to_le_bytes());
} else {
out[ifd_off..ifd_off + 2]
.copy_from_slice(&(plan.ifd.entries.len() as u16).to_le_bytes());
}
let entries_start = ifd_off + count_size as usize;
let next_ifd_off = entries_start + plan.ifd.entries.len() * (entry_size as usize);
for (k, e) in plan.ifd.entries.iter().enumerate() {
let entry_off = entries_start + k * (entry_size as usize);
out[entry_off..entry_off + 2].copy_from_slice(&e.tag.to_le_bytes());
out[entry_off + 2..entry_off + 4].copy_from_slice(&e.field_type.to_le_bytes());
if bigtiff {
out[entry_off + 4..entry_off + 12].copy_from_slice(&e.count.to_le_bytes());
} else {
if e.count > u32::MAX as u64 {
return Err(Error::invalid(
"TIFF encode: classic-TIFF entry count exceeds u32::MAX",
));
}
out[entry_off + 4..entry_off + 8].copy_from_slice(&(e.count as u32).to_le_bytes());
}
let slot_off = entry_off + if bigtiff { 12 } else { 8 };
let slot = &mut out[slot_off..slot_off + offset_bytes];
match &e.value {
IfdValue::Inline(bytes) => {
let n = bytes.len();
debug_assert!(n <= inline_threshold);
slot[..n].copy_from_slice(bytes);
for b in &mut slot[n..] {
*b = 0;
}
}
IfdValue::StripOffsets => {
let val: u64 = if let Some(so) = addr.strip_offsets_array {
so
} else {
addr.strips[0].0
};
if bigtiff {
slot.copy_from_slice(&val.to_le_bytes());
} else {
slot.copy_from_slice(&(val as u32).to_le_bytes());
}
}
IfdValue::StripByteCounts => {
let val: u64 = if let Some(sbc) = addr.strip_byte_counts_array {
sbc
} else {
addr.strips[0].1
};
if bigtiff {
slot.copy_from_slice(&val.to_le_bytes());
} else {
slot.copy_from_slice(&(val as u32).to_le_bytes());
}
}
IfdValue::ExternalBlob(id) => {
let (_, off, _) = addr
.externals
.iter()
.find(|(x, _, _)| *x == *id)
.ok_or_else(|| {
Error::invalid("TIFF encode: missing planned blob for entry")
})?;
if bigtiff {
slot.copy_from_slice(&off.to_le_bytes());
} else {
slot.copy_from_slice(&(*off as u32).to_le_bytes());
}
}
}
}
let next_offset: u64 = if i + 1 < addrs.len() {
addrs[i + 1].ifd_offset
} else {
0
};
if bigtiff {
out[next_ifd_off..next_ifd_off + 8].copy_from_slice(&next_offset.to_le_bytes());
} else {
out[next_ifd_off..next_ifd_off + 4]
.copy_from_slice(&(next_offset as u32).to_le_bytes());
}
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BlobId {
BitsPerSample,
ColorMapWords,
ReferenceBlackWhite,
}
#[derive(Debug, Clone)]
enum IfdValue {
Inline(Vec<u8>),
StripOffsets,
StripByteCounts,
ExternalBlob(BlobId),
}
#[derive(Debug, Clone)]
struct PageIfdEntry {
tag: u16,
field_type: u16,
count: u64,
value: IfdValue,
}
#[derive(Debug)]
struct PageIfd {
entries: Vec<PageIfdEntry>,
}
fn plan_page_full(p: &EncodePage<'_>, bigtiff: bool) -> Result<PlannedPage> {
if p.compression.is_ccitt()
&& !matches!(
p.kind,
EncodePixelFormat::Bilevel { .. } | EncodePixelFormat::TransparencyMask { .. }
)
{
return Err(Error::invalid(
"TIFF encode: CCITT compression (Compression=2/3) requires Bilevel or \
TransparencyMask input",
));
}
if p.planar
&& !matches!(
p.kind,
EncodePixelFormat::Rgb24 { .. }
| EncodePixelFormat::CieLab8 { .. }
| EncodePixelFormat::Cmyk32 { .. }
)
{
return Err(Error::invalid(
"TIFF encode: PlanarConfiguration=2 (separate planes) requires a multi-sample \
format; TIFF 6.0 §\"PlanarConfiguration\" says the field is irrelevant when \
SamplesPerPixel is 1 (Gray8 / Gray16Le / Palette8 / Bilevel / TransparencyMask / \
CieLabL8)",
));
}
if matches!(
p.kind,
EncodePixelFormat::YCbCr24 { .. } | EncodePixelFormat::YCbCrSubsampled24 { .. }
) {
if p.planar {
return Err(Error::invalid(
"TIFF encode: PlanarConfiguration=2 with YCbCr is not supported in this round \
(the §21 data-unit ordering changes shape under non-1:1 subsampling)",
));
}
if p.tiling.is_some() {
return Err(Error::invalid(
"TIFF encode: tiled layout with YCbCr is not supported in this round \
(the §21 data-unit packing is single-strip chunky only here)",
));
}
if p.predictor {
return Err(Error::invalid(
"TIFF encode: Predictor=2 with YCbCr is not supported in this round \
(the §21 chroma-difference samples and the §14 cumulative-add reversal \
interact through the §22 / §20 reference coding range)",
));
}
}
if let Some((tw, th)) = p.tiling {
if tw == 0 || th == 0 || tw % 16 != 0 || th % 16 != 0 {
return Err(Error::invalid(format!(
"TIFF encode: tile dimensions must be non-zero multiples of 16 \
(TIFF 6.0 §15 TileWidth / TileLength); got {tw}x{th}"
)));
}
if matches!(
p.kind,
EncodePixelFormat::Bilevel { .. } | EncodePixelFormat::TransparencyMask { .. }
) {
return Err(Error::invalid(
"TIFF encode: tiled layout (TIFF 6.0 §15) is not supported for 1-bit input \
(Bilevel / TransparencyMask) — sub-byte tile slicing is unimplemented on \
both sides",
));
}
if p.compression.is_ccitt() {
return Err(Error::invalid(
"TIFF encode: tiled layout cannot combine with CCITT compression \
(Compression=2/3), which is bilevel-only",
));
}
}
if p.predictor {
if matches!(
p.kind,
EncodePixelFormat::Bilevel { .. } | EncodePixelFormat::TransparencyMask { .. }
) {
return Err(Error::invalid(
"TIFF encode: Predictor=2 (horizontal differencing) is undefined for 1-bit \
input (Bilevel / TransparencyMask) — TIFF 6.0 §14 differences whole sample \
components",
));
}
if p.compression.is_ccitt() {
return Err(Error::invalid(
"TIFF encode: Predictor=2 cannot combine with CCITT compression \
(Compression=2/3); TIFF 6.0 §14 ties the predictor to the LZW family",
));
}
}
let (samples_per_pixel, bits_per_sample, photometric, mut raw_pixels, color_map_words) =
match &p.kind {
EncodePixelFormat::Bilevel { pixels } => {
let row_bytes = (p.width as usize).div_ceil(8);
let want = row_bytes * (p.height as usize);
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/Bilevel: pixel buffer is {} bytes, expected {want} \
(row_bytes={row_bytes}, height={})",
pixels.len(),
p.height
)));
}
(1u16, vec![1u16], PHOTO_WHITE_IS_ZERO, pixels.to_vec(), None)
}
EncodePixelFormat::TransparencyMask { pixels } => {
let row_bytes = (p.width as usize).div_ceil(8);
let want = row_bytes * (p.height as usize);
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/TransparencyMask: pixel buffer is {} bytes, expected \
{want} (row_bytes={row_bytes}, height={})",
pixels.len(),
p.height
)));
}
(
1u16,
vec![1u16],
PHOTO_TRANSPARENCY_MASK,
pixels.to_vec(),
None,
)
}
EncodePixelFormat::Gray8 { pixels } => {
let want = (p.width as usize) * (p.height as usize);
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/Gray8: pixel buffer is {} bytes, expected {want}",
pixels.len()
)));
}
(1u16, vec![8u16], PHOTO_BLACK_IS_ZERO, pixels.to_vec(), None)
}
EncodePixelFormat::Gray16Le { pixels } => {
let want = (p.width as usize) * (p.height as usize) * 2;
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/Gray16Le: pixel buffer is {} bytes, expected {want}",
pixels.len()
)));
}
(
1u16,
vec![16u16],
PHOTO_BLACK_IS_ZERO,
pixels.to_vec(),
None,
)
}
EncodePixelFormat::Rgb24 { pixels } => {
let want = (p.width as usize) * (p.height as usize) * 3;
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/Rgb24: pixel buffer is {} bytes, expected {want}",
pixels.len()
)));
}
(3u16, vec![8u16, 8, 8], PHOTO_RGB, pixels.to_vec(), None)
}
EncodePixelFormat::Palette8 { indices, palette } => {
let want = (p.width as usize) * (p.height as usize);
if indices.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/Palette8: index buffer is {} bytes, expected {want}",
indices.len()
)));
}
if palette.is_empty() || palette.len() > 256 {
return Err(Error::invalid(format!(
"TIFF encode/Palette8: palette must have 1..=256 entries (got {})",
palette.len()
)));
}
let mut cm = vec![0u16; 256 * 3];
for (i, c) in palette.iter().enumerate() {
cm[i] = ((c[0] as u16) << 8) | c[0] as u16;
cm[256 + i] = ((c[1] as u16) << 8) | c[1] as u16;
cm[512 + i] = ((c[2] as u16) << 8) | c[2] as u16;
}
(1u16, vec![8u16], PHOTO_PALETTE, indices.to_vec(), Some(cm))
}
EncodePixelFormat::CieLab8 { pixels } => {
let want = (p.width as usize) * (p.height as usize) * 3;
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/CieLab8: pixel buffer is {} bytes, expected {want}",
pixels.len()
)));
}
(3u16, vec![8u16, 8, 8], PHOTO_CIELAB, pixels.to_vec(), None)
}
EncodePixelFormat::CieLabL8 { pixels } => {
let want = (p.width as usize) * (p.height as usize);
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/CieLabL8: pixel buffer is {} bytes, expected {want}",
pixels.len()
)));
}
(1u16, vec![8u16], PHOTO_CIELAB, pixels.to_vec(), None)
}
EncodePixelFormat::Cmyk32 { pixels } => {
let want = (p.width as usize) * (p.height as usize) * 4;
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/Cmyk32: pixel buffer is {} bytes, expected {want}",
pixels.len()
)));
}
(4u16, vec![8u16, 8, 8, 8], PHOTO_CMYK, pixels.to_vec(), None)
}
EncodePixelFormat::YCbCr24 { pixels } => {
let want = (p.width as usize) * (p.height as usize) * 3;
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/YCbCr24: pixel buffer is {} bytes, expected {want}",
pixels.len()
)));
}
(3u16, vec![8u16, 8, 8], PHOTO_YCBCR, pixels.to_vec(), None)
}
EncodePixelFormat::YCbCrSubsampled24 {
pixels,
subsampling,
} => {
let (sh, sv) = *subsampling;
if !matches!((sh, sv), (1, 1) | (2, 1) | (2, 2) | (4, 1) | (4, 2)) {
return Err(Error::invalid(format!(
"TIFF encode/YCbCrSubsampled24: YCbCrSubSampling=({sh},{sv}) is not a \
TIFF 6.0 §21 legal pair — each factor must be 1/2/4 and \
YCbCrSubsampleVert <= YCbCrSubsampleHoriz; supported pairs are \
(1,1), (2,1), (2,2), (4,1), (4,2)"
)));
}
if p.width % (sh as u32) != 0 || p.height % (sv as u32) != 0 {
return Err(Error::invalid(format!(
"TIFF encode/YCbCrSubsampled24: ImageWidth ({}) must be a multiple of \
ChromaSubsampleHoriz ({sh}) and ImageLength ({}) a multiple of \
ChromaSubsampleVert ({sv}) — TIFF 6.0 §21 page 90",
p.width, p.height
)));
}
let want = (p.width as usize) * (p.height as usize) * 3;
if pixels.len() != want {
return Err(Error::invalid(format!(
"TIFF encode/YCbCrSubsampled24: pixel buffer is {} bytes, expected {want} \
(full-resolution width * height * 3)",
pixels.len()
)));
}
let packed = pack_ycbcr_data_units(
pixels,
p.width as usize,
p.height as usize,
sh as usize,
sv as usize,
);
(3u16, vec![8u16, 8, 8], PHOTO_YCBCR, packed, None)
}
};
let bps = bits_per_sample[0] as usize;
let strips: Vec<Vec<u8>> = if let Some((tile_w, tile_h)) = p.tiling {
if p.planar {
build_tiles_planar(
&raw_pixels,
p.width as usize,
p.height as usize,
tile_w as usize,
tile_h as usize,
samples_per_pixel as usize,
bps,
p.predictor,
p.compression,
)?
} else {
build_tiles(
&raw_pixels,
p.width as usize,
p.height as usize,
tile_w as usize,
tile_h as usize,
samples_per_pixel as usize,
bps,
p.predictor,
p.compression,
)?
}
} else if p.planar {
let spp = samples_per_pixel as usize;
let bytes_per_sample = bps / 8;
let pixels = (p.width as usize) * (p.height as usize);
let plane_len = pixels * bytes_per_sample;
let mut out_strips = Vec::with_capacity(spp);
for plane in 0..spp {
let mut plane_buf = vec![0u8; plane_len];
for px in 0..pixels {
let src = (px * spp + plane) * bytes_per_sample;
let dst = px * bytes_per_sample;
plane_buf[dst..dst + bytes_per_sample]
.copy_from_slice(&raw_pixels[src..src + bytes_per_sample]);
}
if p.predictor {
let plane_row_bytes = (p.width as usize) * bytes_per_sample;
forward_horizontal_predictor(
&mut plane_buf,
p.width as usize,
p.height as usize,
1,
bps,
plane_row_bytes,
)?;
}
out_strips.push(p.compression.pack(&plane_buf, p.width, p.height)?);
}
out_strips
} else {
if p.predictor {
let row_bytes = (p.width as usize) * (samples_per_pixel as usize) * (bps / 8);
forward_horizontal_predictor(
&mut raw_pixels,
p.width as usize,
p.height as usize,
samples_per_pixel as usize,
bps,
row_bytes,
)?;
}
vec![p.compression.pack(&raw_pixels, p.width, p.height)?]
};
let planar_config = if p.planar {
PLANAR_SEPARATE
} else {
PLANAR_CHUNKY
};
let strip_count: u64 = strips.len() as u64;
let mut entries: Vec<PageIfdEntry> = Vec::new();
let mut externals: Vec<(BlobId, Vec<u8>)> = Vec::new();
let new_subfile_type: u32 = if matches!(p.kind, EncodePixelFormat::TransparencyMask { .. }) {
1 << 2
} else {
0
};
entries.push(PageIfdEntry {
tag: TAG_NEW_SUBFILE_TYPE,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(new_subfile_type.to_le_bytes().to_vec()),
});
entries.push(PageIfdEntry {
tag: TAG_IMAGE_WIDTH,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(p.width.to_le_bytes().to_vec()),
});
entries.push(PageIfdEntry {
tag: TAG_IMAGE_LENGTH,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(p.height.to_le_bytes().to_vec()),
});
let bps_inline_bytes: Vec<u8> = bits_per_sample
.iter()
.flat_map(|b| b.to_le_bytes())
.collect();
let inline_threshold: usize = if bigtiff { 8 } else { 4 };
if bps_inline_bytes.len() <= inline_threshold {
entries.push(PageIfdEntry {
tag: TAG_BITS_PER_SAMPLE,
field_type: TYPE_SHORT,
count: bits_per_sample.len() as u64,
value: IfdValue::Inline(bps_inline_bytes),
});
} else {
externals.push((BlobId::BitsPerSample, bps_inline_bytes));
entries.push(PageIfdEntry {
tag: TAG_BITS_PER_SAMPLE,
field_type: TYPE_SHORT,
count: bits_per_sample.len() as u64,
value: IfdValue::ExternalBlob(BlobId::BitsPerSample),
});
}
entries.push(PageIfdEntry {
tag: TAG_COMPRESSION,
field_type: TYPE_SHORT,
count: 1,
value: IfdValue::Inline(p.compression.tag_value().to_le_bytes().to_vec()),
});
entries.push(PageIfdEntry {
tag: TAG_PHOTOMETRIC_INTERPRETATION,
field_type: TYPE_SHORT,
count: 1,
value: IfdValue::Inline(photometric.to_le_bytes().to_vec()),
});
let offset_field_type = if bigtiff { TYPE_LONG8 } else { TYPE_LONG };
if p.tiling.is_none() {
entries.push(PageIfdEntry {
tag: TAG_STRIP_OFFSETS,
field_type: offset_field_type,
count: strip_count,
value: IfdValue::StripOffsets,
});
}
entries.push(PageIfdEntry {
tag: TAG_SAMPLES_PER_PIXEL,
field_type: TYPE_SHORT,
count: 1,
value: IfdValue::Inline(samples_per_pixel.to_le_bytes().to_vec()),
});
if p.tiling.is_none() {
entries.push(PageIfdEntry {
tag: TAG_ROWS_PER_STRIP,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(p.height.to_le_bytes().to_vec()),
});
}
if p.tiling.is_none() {
entries.push(PageIfdEntry {
tag: TAG_STRIP_BYTE_COUNTS,
field_type: offset_field_type,
count: strip_count,
value: IfdValue::StripByteCounts,
});
}
entries.push(PageIfdEntry {
tag: TAG_PLANAR_CONFIGURATION,
field_type: TYPE_SHORT,
count: 1,
value: IfdValue::Inline(planar_config.to_le_bytes().to_vec()),
});
match p.compression {
TiffCompression::CcittT4OneD { eol_byte_aligned } => {
let mut flags: u32 = 0;
if eol_byte_aligned {
flags |= T4OPT_EOL_BYTE_ALIGNED;
}
entries.push(PageIfdEntry {
tag: TAG_T4_OPTIONS,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(flags.to_le_bytes().to_vec()),
});
}
TiffCompression::CcittT4TwoD { eol_byte_aligned } => {
let mut flags: u32 = T4OPT_2D_CODING;
if eol_byte_aligned {
flags |= T4OPT_EOL_BYTE_ALIGNED;
}
entries.push(PageIfdEntry {
tag: TAG_T4_OPTIONS,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(flags.to_le_bytes().to_vec()),
});
}
TiffCompression::CcittT6 => {
entries.push(PageIfdEntry {
tag: TAG_T6_OPTIONS,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(0u32.to_le_bytes().to_vec()),
});
}
_ => {}
}
if p.predictor {
entries.push(PageIfdEntry {
tag: TAG_PREDICTOR,
field_type: TYPE_SHORT,
count: 1,
value: IfdValue::Inline(PREDICTOR_HORIZONTAL.to_le_bytes().to_vec()),
});
}
if let Some(cm) = color_map_words {
let bytes: Vec<u8> = cm.iter().flat_map(|w| w.to_le_bytes()).collect();
let count = cm.len() as u64;
externals.push((BlobId::ColorMapWords, bytes));
entries.push(PageIfdEntry {
tag: TAG_COLOR_MAP,
field_type: TYPE_SHORT,
count,
value: IfdValue::ExternalBlob(BlobId::ColorMapWords),
});
}
if let Some((tile_w, tile_h)) = p.tiling {
entries.push(PageIfdEntry {
tag: TAG_TILE_WIDTH,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(tile_w.to_le_bytes().to_vec()),
});
entries.push(PageIfdEntry {
tag: TAG_TILE_LENGTH,
field_type: TYPE_LONG,
count: 1,
value: IfdValue::Inline(tile_h.to_le_bytes().to_vec()),
});
entries.push(PageIfdEntry {
tag: TAG_TILE_OFFSETS,
field_type: offset_field_type,
count: strip_count,
value: IfdValue::StripOffsets,
});
entries.push(PageIfdEntry {
tag: TAG_TILE_BYTE_COUNTS,
field_type: offset_field_type,
count: strip_count,
value: IfdValue::StripByteCounts,
});
}
if matches!(p.kind, EncodePixelFormat::Cmyk32 { .. }) {
entries.push(PageIfdEntry {
tag: TAG_INK_SET,
field_type: TYPE_SHORT,
count: 1,
value: IfdValue::Inline(INK_SET_CMYK.to_le_bytes().to_vec()),
});
entries.push(PageIfdEntry {
tag: TAG_NUMBER_OF_INKS,
field_type: TYPE_SHORT,
count: 1,
value: IfdValue::Inline(4u16.to_le_bytes().to_vec()),
});
}
let ycbcr_subsampling: Option<(u16, u16)> = match &p.kind {
EncodePixelFormat::YCbCr24 { .. } => Some((1, 1)),
EncodePixelFormat::YCbCrSubsampled24 { subsampling, .. } => Some(*subsampling),
_ => None,
};
if let Some((sh, sv)) = ycbcr_subsampling {
let mut ss = [0u8; 4];
ss[..2].copy_from_slice(&sh.to_le_bytes());
ss[2..4].copy_from_slice(&sv.to_le_bytes());
entries.push(PageIfdEntry {
tag: TAG_YCBCR_SUBSAMPLING,
field_type: TYPE_SHORT,
count: 2,
value: IfdValue::Inline(ss.to_vec()),
});
entries.push(PageIfdEntry {
tag: TAG_YCBCR_POSITIONING,
field_type: TYPE_SHORT,
count: 1,
value: IfdValue::Inline(1u16.to_le_bytes().to_vec()),
});
let rbw_pairs: [(u32, u32); 6] = [(0, 1), (255, 1), (128, 1), (255, 1), (128, 1), (255, 1)];
let mut rbw_bytes: Vec<u8> = Vec::with_capacity(48);
for (num, den) in rbw_pairs {
rbw_bytes.extend_from_slice(&num.to_le_bytes());
rbw_bytes.extend_from_slice(&den.to_le_bytes());
}
externals.push((BlobId::ReferenceBlackWhite, rbw_bytes));
entries.push(PageIfdEntry {
tag: TAG_REFERENCE_BLACK_WHITE,
field_type: TYPE_RATIONAL,
count: 6,
value: IfdValue::ExternalBlob(BlobId::ReferenceBlackWhite),
});
}
debug_assert!(entries.windows(2).all(|w| w[0].tag <= w[1].tag));
Ok(PlannedPage {
strips,
ifd: PageIfd { entries },
externals,
})
}
fn pack_ycbcr_data_units(src: &[u8], width: usize, height: usize, sh: usize, sv: usize) -> Vec<u8> {
let block_w = width / sh;
let block_h = height / sv;
let unit_len = sh * sv + 2;
let mut out = vec![0u8; block_w * block_h * unit_len];
for by in 0..block_h {
for bx in 0..block_w {
let unit_off = (by * block_w + bx) * unit_len;
let mut cb_sum: u32 = 0;
let mut cr_sum: u32 = 0;
for sy in 0..sv {
for sx in 0..sh {
let px = bx * sh + sx;
let py = by * sv + sy;
let s = (py * width + px) * 3;
out[unit_off + sy * sh + sx] = src[s];
cb_sum += src[s + 1] as u32;
cr_sum += src[s + 2] as u32;
}
}
let n = (sh * sv) as u32;
let cb = ((cb_sum + n / 2) / n) as u8;
let cr = ((cr_sum + n / 2) / n) as u8;
out[unit_off + sh * sv] = cb;
out[unit_off + sh * sv + 1] = cr;
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn build_tiles(
pixels: &[u8],
width: usize,
height: usize,
tile_w: usize,
tile_h: usize,
samples: usize,
bps: usize,
predictor: bool,
compression: TiffCompression,
) -> Result<Vec<Vec<u8>>> {
let bytes_per_sample = bps / 8;
let pixel_bytes = samples * bytes_per_sample;
let image_row_bytes = width * pixel_bytes;
let tile_row_bytes = tile_w * pixel_bytes;
let tile_size_bytes = tile_row_bytes * tile_h;
let tiles_across = width.div_ceil(tile_w);
let tiles_down = height.div_ceil(tile_h);
let mut out = Vec::with_capacity(tiles_across * tiles_down);
for ty in 0..tiles_down {
for tx in 0..tiles_across {
let mut tile = vec![0u8; tile_size_bytes];
for r in 0..tile_h {
let src_y = (ty * tile_h + r).min(height - 1);
for c in 0..tile_w {
let src_x = (tx * tile_w + c).min(width - 1);
let src_off = src_y * image_row_bytes + src_x * pixel_bytes;
let dst_off = r * tile_row_bytes + c * pixel_bytes;
tile[dst_off..dst_off + pixel_bytes]
.copy_from_slice(&pixels[src_off..src_off + pixel_bytes]);
}
}
if predictor {
forward_horizontal_predictor(
&mut tile,
tile_w,
tile_h,
samples,
bps,
tile_row_bytes,
)?;
}
out.push(compression.pack(&tile, tile_w as u32, tile_h as u32)?);
}
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn build_tiles_planar(
pixels: &[u8],
width: usize,
height: usize,
tile_w: usize,
tile_h: usize,
samples: usize,
bps: usize,
predictor: bool,
compression: TiffCompression,
) -> Result<Vec<Vec<u8>>> {
let bytes_per_sample = bps / 8;
let pixel_bytes = samples * bytes_per_sample;
let plane_len = width * height * bytes_per_sample;
let tiles_across = width.div_ceil(tile_w);
let tiles_down = height.div_ceil(tile_h);
let mut out = Vec::with_capacity(samples * tiles_across * tiles_down);
for plane in 0..samples {
let mut plane_buf = vec![0u8; plane_len];
let total_pixels = width * height;
for px in 0..total_pixels {
let src = px * pixel_bytes + plane * bytes_per_sample;
let dst = px * bytes_per_sample;
plane_buf[dst..dst + bytes_per_sample]
.copy_from_slice(&pixels[src..src + bytes_per_sample]);
}
let plane_tiles = build_tiles(
&plane_buf,
width,
height,
tile_w,
tile_h,
1,
bps,
predictor,
compression,
)?;
out.extend(plane_tiles);
}
Ok(out)
}
fn forward_horizontal_predictor(
buf: &mut [u8],
width: usize,
rows: usize,
samples: usize,
bps: usize,
row_bytes: usize,
) -> Result<()> {
if width == 0 || rows == 0 {
return Ok(());
}
match bps {
8 => {
for r in 0..rows {
let row = &mut buf[r * row_bytes..r * row_bytes + width * samples];
for x in (samples..(width * samples)).rev() {
row[x] = row[x].wrapping_sub(row[x - samples]);
}
}
}
16 => {
for r in 0..rows {
let row = &mut buf[r * row_bytes..r * row_bytes + width * samples * 2];
let pixels = width * samples;
for x in (samples..pixels).rev() {
let cur_off = x * 2;
let prev_off = (x - samples) * 2;
let cur = u16::from_le_bytes([row[cur_off], row[cur_off + 1]]);
let prev = u16::from_le_bytes([row[prev_off], row[prev_off + 1]]);
let new = cur.wrapping_sub(prev);
let bytes = new.to_le_bytes();
row[cur_off] = bytes[0];
row[cur_off + 1] = bytes[1];
}
}
}
_ => {
return Err(Error::invalid(format!(
"TIFF encode: Predictor=2 at bits_per_sample={bps} unsupported"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decode_tiff;
use crate::image::TiffPixelFormat;
fn ramp_gray8(w: u32, h: u32) -> Vec<u8> {
let mut v = Vec::with_capacity((w * h) as usize);
for y in 0..h {
for x in 0..w {
v.push(((x + y) & 0xFF) as u8);
}
}
v
}
fn pattern_rgb(w: u32, h: u32) -> Vec<u8> {
let mut v = Vec::with_capacity((w * h * 3) as usize);
for y in 0..h as u8 {
for x in 0..w as u8 {
v.push(x.wrapping_mul(7));
v.push(y.wrapping_mul(11));
v.push((x ^ y).wrapping_mul(13));
}
}
v
}
#[test]
fn encode_gray8_uncompressed_roundtrip() {
let pixels = ramp_gray8(32, 32);
let page = EncodePage {
width: 32,
height: 32,
kind: EncodePixelFormat::Gray8 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (32, 32));
assert_eq!(d.frame.planes[0].data, pixels);
}
#[test]
fn encode_gray16_packbits_roundtrip() {
let mut pixels = Vec::with_capacity(16 * 16 * 2);
for y in 0u16..16 {
for x in 0u16..16 {
let v = (x.wrapping_mul(257)).wrapping_add(y.wrapping_mul(513));
pixels.extend_from_slice(&v.to_le_bytes());
}
}
let page = EncodePage {
width: 16,
height: 16,
kind: EncodePixelFormat::Gray16Le { pixels: &pixels },
compression: TiffCompression::PackBits,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (16, 16));
assert_eq!(d.frame.planes[0].data, pixels);
}
#[test]
fn encode_rgb24_lzw_roundtrip() {
let pixels = pattern_rgb(20, 20);
let page = EncodePage {
width: 20,
height: 20,
kind: EncodePixelFormat::Rgb24 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (20, 20));
assert_eq!(d.frame.planes[0].data, pixels);
}
#[test]
fn encode_rgb24_deflate_roundtrip() {
let pixels = pattern_rgb(48, 24);
let page = EncodePage {
width: 48,
height: 24,
kind: EncodePixelFormat::Rgb24 { pixels: &pixels },
compression: TiffCompression::Deflate,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (48, 24));
assert_eq!(d.frame.planes[0].data, pixels);
}
#[test]
fn encode_palette_roundtrip() {
let palette = vec![[0, 0, 0], [255, 0, 0], [0, 255, 0], [255, 255, 255]];
let mut indices = Vec::with_capacity(8 * 8);
for y in 0..8 {
for x in 0..8 {
indices.push(((x ^ y) & 0x3) as u8);
}
}
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Palette8 {
indices: &indices,
palette: &palette,
},
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
let mut want = Vec::with_capacity(8 * 8 * 3);
for &idx in &indices {
let p = palette[idx as usize];
want.extend_from_slice(&p);
}
assert_eq!(d.frame.planes[0].data, want);
}
fn bilevel_checkerboard(w: u32, h: u32) -> Vec<u8> {
let row_bytes = (w as usize).div_ceil(8);
let mut out = vec![0u8; row_bytes * h as usize];
for y in 0..h as usize {
for x in 0..w as usize {
let on = ((x ^ y) & 1) == 1;
if on {
out[y * row_bytes + x / 8] |= 1 << (7 - (x % 8));
}
}
}
out
}
fn bilevel_stripes(w: u32, h: u32, period: u32) -> Vec<u8> {
let row_bytes = (w as usize).div_ceil(8);
let mut out = vec![0u8; row_bytes * h as usize];
for y in 0..h as usize {
for x in 0..w as usize {
let on = ((x as u32) / period) & 1 == 1;
if on {
out[y * row_bytes + x / 8] |= 1 << (7 - (x % 8));
}
}
}
out
}
fn bilevel_to_gray8(packed: &[u8], w: u32, h: u32) -> Vec<u8> {
let row_bytes = (w as usize).div_ceil(8);
let mut out = Vec::with_capacity((w * h) as usize);
for y in 0..h as usize {
let row = &packed[y * row_bytes..(y + 1) * row_bytes];
for x in 0..w as usize {
let bit = (row[x / 8] >> (7 - (x % 8))) & 1;
out.push(if bit == 0 { 0xFF } else { 0x00 });
}
}
out
}
#[test]
fn encode_bilevel_uncompressed_roundtrip() {
let packed = bilevel_checkerboard(24, 16);
let page = EncodePage {
width: 24,
height: 16,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (24, 16));
let want = bilevel_to_gray8(&packed, 24, 16);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_bilevel_ccitt_rle_roundtrip_checkerboard() {
let packed = bilevel_checkerboard(16, 8);
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::CcittRle,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
let want = bilevel_to_gray8(&packed, 16, 8);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_bilevel_ccitt_rle_roundtrip_stripes() {
let packed = bilevel_stripes(128, 4, 16);
let page = EncodePage {
width: 128,
height: 4,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::CcittRle,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
let want = bilevel_to_gray8(&packed, 128, 4);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_bilevel_ccitt_t4_1d_roundtrip() {
let packed = bilevel_stripes(96, 6, 8);
let page = EncodePage {
width: 96,
height: 6,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::CcittT4OneD {
eol_byte_aligned: false,
},
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
let want = bilevel_to_gray8(&packed, 96, 6);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_bilevel_ccitt_t4_1d_byte_aligned_roundtrip() {
let packed = bilevel_stripes(64, 8, 4);
let page = EncodePage {
width: 64,
height: 8,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::CcittT4OneD {
eol_byte_aligned: true,
},
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
let want = bilevel_to_gray8(&packed, 64, 8);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_ccitt_rejects_non_bilevel() {
let pixels = ramp_gray8(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Gray8 { pixels: &pixels },
compression: TiffCompression::CcittRle,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let r = encode_tiff(&page);
assert!(r.is_err());
}
#[test]
fn encode_multi_page_chain() {
let p1 = ramp_gray8(8, 8);
let p2 = pattern_rgb(8, 8);
let pages = vec![
EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Gray8 { pixels: &p1 },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
},
EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Rgb24 { pixels: &p2 },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
},
];
let bytes = encode_tiff_multi(&pages).unwrap();
let imgs = crate::decoder::decode_tiff_all(&bytes).unwrap();
assert_eq!(imgs.len(), 2);
assert_eq!(imgs[0].width, 8);
assert_eq!(imgs[0].planes[0].data, p1);
assert_eq!(imgs[1].width, 8);
assert_eq!(imgs[1].planes[0].data, p2);
}
fn pattern_gray16(w: u32, h: u32) -> Vec<u8> {
let mut v = Vec::with_capacity((w * h * 2) as usize);
for y in 0..h {
for x in 0..w {
let s = (x.wrapping_mul(311)).wrapping_add(y.wrapping_mul(101)) as u16;
v.extend_from_slice(&s.to_le_bytes());
}
}
v
}
fn predictor_roundtrip(
width: u32,
height: u32,
kind: EncodePixelFormat<'_>,
comp: TiffCompression,
) -> Vec<u8> {
let page = EncodePage {
width,
height,
kind,
compression: comp,
predictor: true,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (width, height));
d.frame.planes[0].data.clone()
}
#[test]
fn encode_gray8_predictor_lzw_roundtrip() {
let pixels = ramp_gray8(40, 24);
let out = predictor_roundtrip(
40,
24,
EncodePixelFormat::Gray8 { pixels: &pixels },
TiffCompression::Lzw,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_gray8_predictor_deflate_roundtrip() {
let pixels = ramp_gray8(33, 17);
let out = predictor_roundtrip(
33,
17,
EncodePixelFormat::Gray8 { pixels: &pixels },
TiffCompression::Deflate,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_gray8_predictor_none_roundtrip() {
let pixels = ramp_gray8(16, 16);
let out = predictor_roundtrip(
16,
16,
EncodePixelFormat::Gray8 { pixels: &pixels },
TiffCompression::None,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_gray16_predictor_lzw_roundtrip() {
let pixels = pattern_gray16(24, 20);
let out = predictor_roundtrip(
24,
20,
EncodePixelFormat::Gray16Le { pixels: &pixels },
TiffCompression::Lzw,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_gray16_predictor_deflate_roundtrip() {
let pixels = pattern_gray16(15, 9);
let out = predictor_roundtrip(
15,
9,
EncodePixelFormat::Gray16Le { pixels: &pixels },
TiffCompression::Deflate,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_rgb24_predictor_lzw_roundtrip() {
let pixels = pattern_rgb(28, 19);
let out = predictor_roundtrip(
28,
19,
EncodePixelFormat::Rgb24 { pixels: &pixels },
TiffCompression::Lzw,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_rgb24_predictor_deflate_roundtrip() {
let pixels = pattern_rgb(11, 13);
let out = predictor_roundtrip(
11,
13,
EncodePixelFormat::Rgb24 { pixels: &pixels },
TiffCompression::Deflate,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_rgb24_predictor_packbits_roundtrip() {
let pixels = pattern_rgb(9, 7);
let out = predictor_roundtrip(
9,
7,
EncodePixelFormat::Rgb24 { pixels: &pixels },
TiffCompression::PackBits,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_palette_predictor_roundtrip() {
let palette = vec![[0, 0, 0], [255, 0, 0], [0, 255, 0], [255, 255, 255]];
let mut indices = Vec::with_capacity(12 * 8);
for y in 0..8u32 {
for x in 0..12u32 {
indices.push(((x + y) & 0x3) as u8);
}
}
let page = EncodePage {
width: 12,
height: 8,
kind: EncodePixelFormat::Palette8 {
indices: &indices,
palette: &palette,
},
compression: TiffCompression::Lzw,
predictor: true,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
let mut want = Vec::with_capacity(12 * 8 * 3);
for &idx in &indices {
want.extend_from_slice(&palette[idx as usize]);
}
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_predictor_emits_tag_317() {
let pixels = ramp_gray8(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Gray8 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: true,
planar: false,
tiling: None,
bigtiff: false,
};
let b = encode_tiff(&page).unwrap();
let ifd_off = u32::from_le_bytes([b[4], b[5], b[6], b[7]]) as usize;
let count = u16::from_le_bytes([b[ifd_off], b[ifd_off + 1]]) as usize;
let mut found = None;
for k in 0..count {
let e = ifd_off + 2 + k * 12;
let tag = u16::from_le_bytes([b[e], b[e + 1]]);
if tag == TAG_PREDICTOR {
let ty = u16::from_le_bytes([b[e + 2], b[e + 3]]);
let val = u16::from_le_bytes([b[e + 8], b[e + 9]]);
found = Some((ty, val));
}
}
assert_eq!(found, Some((TYPE_SHORT, PREDICTOR_HORIZONTAL)));
let page2 = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Gray8 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let b2 = encode_tiff(&page2).unwrap();
let ifd2 = u32::from_le_bytes([b2[4], b2[5], b2[6], b2[7]]) as usize;
let count2 = u16::from_le_bytes([b2[ifd2], b2[ifd2 + 1]]) as usize;
for k in 0..count2 {
let e = ifd2 + 2 + k * 12;
let tag = u16::from_le_bytes([b2[e], b2[e + 1]]);
assert_ne!(tag, TAG_PREDICTOR);
}
}
#[test]
fn encode_predictor_rejects_bilevel() {
let packed = bilevel_checkerboard(16, 8);
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::Lzw,
predictor: true,
planar: false,
tiling: None,
bigtiff: false,
};
assert!(encode_tiff(&page).is_err());
}
#[test]
fn encode_predictor_rejects_ccitt() {
let packed = bilevel_checkerboard(16, 8);
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::CcittRle,
predictor: true,
planar: false,
tiling: None,
bigtiff: false,
};
assert!(encode_tiff(&page).is_err());
}
#[test]
fn forward_predictor_inverts_decoder_add_gray8() {
let mut row = vec![10u8, 12, 9, 9, 200, 201];
let orig = row.clone();
forward_horizontal_predictor(&mut row, 6, 1, 1, 8, 6).unwrap();
assert_eq!(row[0], 10);
assert_eq!(row[1], 12u8.wrapping_sub(10));
assert_eq!(row[5], 201u8.wrapping_sub(200));
for x in 1..6 {
row[x] = row[x].wrapping_add(row[x - 1]);
}
assert_eq!(row, orig);
}
fn planar_roundtrip(w: u32, h: u32, compression: TiffCompression, predictor: bool) {
let pixels = pattern_rgb(w, h);
let page = EncodePage {
width: w,
height: h,
kind: EncodePixelFormat::Rgb24 { pixels: &pixels },
compression,
predictor,
planar: true,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (w, h));
assert_eq!(d.frame.planes[0].data, pixels);
}
#[test]
fn encode_rgb24_planar_none_roundtrip() {
planar_roundtrip(20, 16, TiffCompression::None, false);
}
#[test]
fn encode_rgb24_planar_packbits_roundtrip() {
planar_roundtrip(33, 9, TiffCompression::PackBits, false);
}
#[test]
fn encode_rgb24_planar_lzw_roundtrip() {
planar_roundtrip(48, 24, TiffCompression::Lzw, false);
}
#[test]
fn encode_rgb24_planar_deflate_roundtrip() {
planar_roundtrip(17, 31, TiffCompression::Deflate, false);
}
#[test]
fn encode_rgb24_planar_predictor_lzw_roundtrip() {
planar_roundtrip(40, 20, TiffCompression::Lzw, true);
}
#[test]
fn encode_rgb24_planar_predictor_deflate_roundtrip() {
planar_roundtrip(28, 28, TiffCompression::Deflate, true);
}
#[test]
fn encode_planar_emits_three_strips_and_config_2() {
let pixels = pattern_rgb(16, 8);
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::Rgb24 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: true,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
assert_eq!(&bytes[0..2], b"II");
let ifd_off = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
let count = u16::from_le_bytes([bytes[ifd_off], bytes[ifd_off + 1]]) as usize;
let mut planar_cfg = None;
let mut strip_offsets_count = None;
let mut strip_byte_counts_count = None;
for k in 0..count {
let e = ifd_off + 2 + k * 12;
let tag = u16::from_le_bytes([bytes[e], bytes[e + 1]]);
let cnt = u32::from_le_bytes([bytes[e + 4], bytes[e + 5], bytes[e + 6], bytes[e + 7]]);
match tag {
TAG_PLANAR_CONFIGURATION => {
planar_cfg = Some(u16::from_le_bytes([bytes[e + 8], bytes[e + 9]]));
}
TAG_STRIP_OFFSETS => strip_offsets_count = Some(cnt),
TAG_STRIP_BYTE_COUNTS => strip_byte_counts_count = Some(cnt),
_ => {}
}
}
assert_eq!(planar_cfg, Some(PLANAR_SEPARATE));
assert_eq!(strip_offsets_count, Some(3));
assert_eq!(strip_byte_counts_count, Some(3));
}
#[test]
fn encode_planar_rejects_single_sample_formats() {
let g = ramp_gray8(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Gray8 { pixels: &g },
compression: TiffCompression::None,
predictor: false,
planar: true,
tiling: None,
bigtiff: false,
};
assert!(encode_tiff(&page).is_err());
let palette = vec![[0u8, 0, 0], [255, 255, 255]];
let indices = vec![0u8; 64];
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Palette8 {
indices: &indices,
palette: &palette,
},
compression: TiffCompression::None,
predictor: false,
planar: true,
tiling: None,
bigtiff: false,
};
assert!(encode_tiff(&page).is_err());
}
#[test]
fn encode_chunky_still_single_strip_config_1() {
let pixels = pattern_rgb(12, 6);
let page = EncodePage {
width: 12,
height: 6,
kind: EncodePixelFormat::Rgb24 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let ifd_off = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
let count = u16::from_le_bytes([bytes[ifd_off], bytes[ifd_off + 1]]) as usize;
for k in 0..count {
let e = ifd_off + 2 + k * 12;
let tag = u16::from_le_bytes([bytes[e], bytes[e + 1]]);
let cnt = u32::from_le_bytes([bytes[e + 4], bytes[e + 5], bytes[e + 6], bytes[e + 7]]);
if tag == TAG_PLANAR_CONFIGURATION {
assert_eq!(
u16::from_le_bytes([bytes[e + 8], bytes[e + 9]]),
PLANAR_CHUNKY
);
}
if tag == TAG_STRIP_OFFSETS || tag == TAG_STRIP_BYTE_COUNTS {
assert_eq!(cnt, 1);
}
}
}
fn tile_roundtrip(
width: u32,
height: u32,
kind: EncodePixelFormat<'_>,
comp: TiffCompression,
tiling: (u32, u32),
predictor: bool,
) -> Vec<u8> {
let page = EncodePage {
width,
height,
kind,
compression: comp,
predictor,
planar: false,
tiling: Some(tiling),
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (width, height));
d.frame.planes[0].data.clone()
}
#[test]
fn encode_gray8_tiled_single_tile_roundtrip() {
let pixels = ramp_gray8(16, 16);
let out = tile_roundtrip(
16,
16,
EncodePixelFormat::Gray8 { pixels: &pixels },
TiffCompression::None,
(16, 16),
false,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_gray8_tiled_grid_roundtrip() {
let pixels = ramp_gray8(48, 32);
for comp in [
TiffCompression::None,
TiffCompression::PackBits,
TiffCompression::Lzw,
TiffCompression::Deflate,
] {
let out = tile_roundtrip(
48,
32,
EncodePixelFormat::Gray8 { pixels: &pixels },
comp,
(16, 16),
false,
);
assert_eq!(out, pixels, "compression {comp:?}");
}
}
#[test]
fn encode_gray8_tiled_edge_padding_roundtrip() {
let pixels = ramp_gray8(40, 20);
let out = tile_roundtrip(
40,
20,
EncodePixelFormat::Gray8 { pixels: &pixels },
TiffCompression::Lzw,
(16, 16),
false,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_gray16_tiled_roundtrip() {
let pixels = pattern_gray16(48, 32);
let out = tile_roundtrip(
48,
32,
EncodePixelFormat::Gray16Le { pixels: &pixels },
TiffCompression::Deflate,
(16, 16),
false,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_rgb24_tiled_roundtrip() {
let pixels = pattern_rgb(50, 30);
for comp in [
TiffCompression::None,
TiffCompression::PackBits,
TiffCompression::Lzw,
TiffCompression::Deflate,
] {
let out = tile_roundtrip(
50,
30,
EncodePixelFormat::Rgb24 { pixels: &pixels },
comp,
(32, 16),
false,
);
assert_eq!(out, pixels, "compression {comp:?}");
}
}
#[test]
fn encode_rgb24_tiled_predictor_roundtrip() {
let pixels = pattern_rgb(48, 32);
let out = tile_roundtrip(
48,
32,
EncodePixelFormat::Rgb24 { pixels: &pixels },
TiffCompression::Lzw,
(16, 16),
true,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_gray8_tiled_predictor_edge_roundtrip() {
let pixels = ramp_gray8(40, 20);
let out = tile_roundtrip(
40,
20,
EncodePixelFormat::Gray8 { pixels: &pixels },
TiffCompression::Deflate,
(16, 16),
true,
);
assert_eq!(out, pixels);
}
#[test]
fn encode_palette_tiled_roundtrip() {
let palette = vec![[0, 0, 0], [255, 0, 0], [0, 255, 0], [255, 255, 255]];
let mut indices = Vec::with_capacity(40 * 20);
for y in 0..20u32 {
for x in 0..40u32 {
indices.push(((x + y) & 0x3) as u8);
}
}
let page = EncodePage {
width: 40,
height: 20,
kind: EncodePixelFormat::Palette8 {
indices: &indices,
palette: &palette,
},
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: Some((16, 16)),
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
let mut want = Vec::with_capacity(40 * 20 * 3);
for &idx in &indices {
want.extend_from_slice(&palette[idx as usize]);
}
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_tiled_emits_tile_tags_not_strip_tags() {
let pixels = ramp_gray8(48, 32);
let page = EncodePage {
width: 48,
height: 32,
kind: EncodePixelFormat::Gray8 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: Some((16, 16)),
bigtiff: false,
};
let b = encode_tiff(&page).unwrap();
let ifd_off = u32::from_le_bytes([b[4], b[5], b[6], b[7]]) as usize;
let count = u16::from_le_bytes([b[ifd_off], b[ifd_off + 1]]) as usize;
let mut seen = std::collections::HashMap::new();
for k in 0..count {
let e = ifd_off + 2 + k * 12;
let tag = u16::from_le_bytes([b[e], b[e + 1]]);
let cnt = u32::from_le_bytes([b[e + 4], b[e + 5], b[e + 6], b[e + 7]]);
seen.insert(tag, cnt);
}
assert!(!seen.contains_key(&TAG_STRIP_OFFSETS));
assert!(!seen.contains_key(&TAG_STRIP_BYTE_COUNTS));
assert!(!seen.contains_key(&TAG_ROWS_PER_STRIP));
assert!(seen.contains_key(&TAG_TILE_WIDTH));
assert!(seen.contains_key(&TAG_TILE_LENGTH));
assert_eq!(seen.get(&TAG_TILE_OFFSETS), Some(&6));
assert_eq!(seen.get(&TAG_TILE_BYTE_COUNTS), Some(&6));
let mut prev = 0u16;
for k in 0..count {
let e = ifd_off + 2 + k * 12;
let tag = u16::from_le_bytes([b[e], b[e + 1]]);
assert!(tag > prev, "tag {tag} not after {prev}");
prev = tag;
}
}
#[test]
fn encode_tiling_rejects_non_multiple_of_16() {
let pixels = ramp_gray8(32, 32);
let page = EncodePage {
width: 32,
height: 32,
kind: EncodePixelFormat::Gray8 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: Some((20, 16)),
bigtiff: false,
};
assert!(encode_tiff(&page).is_err());
}
#[test]
fn encode_tiling_rejects_bilevel() {
let packed = bilevel_checkerboard(32, 16);
let page = EncodePage {
width: 32,
height: 16,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: Some((16, 16)),
bigtiff: false,
};
assert!(encode_tiff(&page).is_err());
}
#[test]
fn encode_tiling_rejects_ccitt() {
let packed = bilevel_checkerboard(32, 16);
let page = EncodePage {
width: 32,
height: 16,
kind: EncodePixelFormat::Bilevel { pixels: &packed },
compression: TiffCompression::CcittRle,
predictor: false,
planar: false,
tiling: Some((16, 16)),
bigtiff: false,
};
assert!(encode_tiff(&page).is_err());
}
#[test]
fn encode_tiling_planar_rgb24_roundtrips() {
let pixels = pattern_rgb(32, 32);
let page = EncodePage {
width: 32,
height: 32,
kind: EncodePixelFormat::Rgb24 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: true,
tiling: Some((16, 16)),
bigtiff: false,
};
let bytes = encode_tiff(&page).expect("planar tiled encode");
let d = decode_tiff(&bytes).expect("planar tiled decode");
assert_eq!((d.width, d.height), (32, 32));
assert_eq!(d.frame.planes[0].data, pixels);
}
#[test]
fn encode_tiled_multi_page_chain() {
let p1 = ramp_gray8(48, 32);
let p2 = pattern_rgb(16, 16);
let pages = vec![
EncodePage {
width: 48,
height: 32,
kind: EncodePixelFormat::Gray8 { pixels: &p1 },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: Some((16, 16)),
bigtiff: false,
},
EncodePage {
width: 16,
height: 16,
kind: EncodePixelFormat::Rgb24 { pixels: &p2 },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
},
];
let bytes = encode_tiff_multi(&pages).unwrap();
let imgs = crate::decoder::decode_tiff_all(&bytes).unwrap();
assert_eq!(imgs.len(), 2);
assert_eq!(imgs[0].planes[0].data, p1);
assert_eq!(imgs[1].planes[0].data, p2);
}
fn pack_lab_byte(l_pct: f64, a_signed: i32, b_signed: i32) -> [u8; 3] {
let l_byte = (l_pct * 255.0 / 100.0).round().clamp(0.0, 255.0) as u8;
[l_byte, (a_signed as i8) as u8, (b_signed as i8) as u8]
}
fn lab_pattern_3sample(w: u32, h: u32) -> Vec<u8> {
let mut v = Vec::with_capacity((w * h * 3) as usize);
for y in 0..h {
for x in 0..w {
let l_pct = (x as f64) * 100.0 / (w as f64).max(1.0);
let a_signed = -127 + (2 * 127 * (y as i32) / (h as i32).max(1));
let b_signed = if (x ^ y) & 1 == 0 { 50 } else { -50 };
v.extend_from_slice(&pack_lab_byte(l_pct, a_signed, b_signed));
}
}
v
}
fn lab_l_ramp(w: u32, h: u32) -> Vec<u8> {
let mut v = Vec::with_capacity((w * h) as usize);
for y in 0..h {
for x in 0..w {
v.push(((x.wrapping_add(y)) & 0xFF) as u8);
}
}
v
}
fn decode_3sample_cielab(pixels: &[u8], w: u32, h: u32) -> Vec<u8> {
let row_bytes = (w as u64) * 3;
let strip_bytes = row_bytes * (h as u64);
assert_eq!(pixels.len() as u64, strip_bytes);
let num_entries: u16 = 8;
let ifd_offset: u32 = 8;
let ifd_size: u32 = 2 + (num_entries as u32) * 12 + 4;
let bps_blob_bytes: u32 = 3 * 2;
let blobs_offset: u32 = ifd_offset + ifd_size;
let bps_off = blobs_offset;
let pixels_off = bps_off + bps_blob_bytes;
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"II");
buf.extend_from_slice(&42u16.to_le_bytes());
buf.extend_from_slice(&ifd_offset.to_le_bytes());
buf.extend_from_slice(&num_entries.to_le_bytes());
let push = |buf: &mut Vec<u8>, tag: u16, ft: u16, count: u32, v: [u8; 4]| {
buf.extend_from_slice(&tag.to_le_bytes());
buf.extend_from_slice(&ft.to_le_bytes());
buf.extend_from_slice(&count.to_le_bytes());
buf.extend_from_slice(&v);
};
push(&mut buf, 256, 4, 1, w.to_le_bytes());
push(&mut buf, 257, 4, 1, h.to_le_bytes());
push(&mut buf, 258, 3, 3, bps_off.to_le_bytes());
let mut comp = [0u8; 4];
comp[..2].copy_from_slice(&1u16.to_le_bytes());
push(&mut buf, 259, 3, 1, comp);
let mut ph = [0u8; 4];
ph[..2].copy_from_slice(&8u16.to_le_bytes());
push(&mut buf, 262, 3, 1, ph);
push(&mut buf, 273, 4, 1, pixels_off.to_le_bytes());
let mut spp = [0u8; 4];
spp[..2].copy_from_slice(&3u16.to_le_bytes());
push(&mut buf, 277, 3, 1, spp);
push(&mut buf, 279, 4, 1, (strip_bytes as u32).to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
for _ in 0..3u16 {
buf.extend_from_slice(&8u16.to_le_bytes());
}
buf.extend_from_slice(pixels);
decode_tiff(&buf).unwrap().frame.planes[0].data.clone()
}
fn decode_1sample_cielab(pixels: &[u8], w: u32, h: u32) -> Vec<u8> {
let strip_bytes = (w as u64) * (h as u64);
assert_eq!(pixels.len() as u64, strip_bytes);
let num_entries: u16 = 8;
let ifd_offset: u32 = 8;
let ifd_size: u32 = 2 + (num_entries as u32) * 12 + 4;
let blobs_offset: u32 = ifd_offset + ifd_size;
let pixels_off = blobs_offset;
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"II");
buf.extend_from_slice(&42u16.to_le_bytes());
buf.extend_from_slice(&ifd_offset.to_le_bytes());
buf.extend_from_slice(&num_entries.to_le_bytes());
let push = |buf: &mut Vec<u8>, tag: u16, ft: u16, count: u32, v: [u8; 4]| {
buf.extend_from_slice(&tag.to_le_bytes());
buf.extend_from_slice(&ft.to_le_bytes());
buf.extend_from_slice(&count.to_le_bytes());
buf.extend_from_slice(&v);
};
push(&mut buf, 256, 4, 1, w.to_le_bytes());
push(&mut buf, 257, 4, 1, h.to_le_bytes());
let mut bps = [0u8; 4];
bps[..2].copy_from_slice(&8u16.to_le_bytes());
push(&mut buf, 258, 3, 1, bps);
let mut comp = [0u8; 4];
comp[..2].copy_from_slice(&1u16.to_le_bytes());
push(&mut buf, 259, 3, 1, comp);
let mut ph = [0u8; 4];
ph[..2].copy_from_slice(&8u16.to_le_bytes());
push(&mut buf, 262, 3, 1, ph);
push(&mut buf, 273, 4, 1, pixels_off.to_le_bytes());
let mut spp = [0u8; 4];
spp[..2].copy_from_slice(&1u16.to_le_bytes());
push(&mut buf, 277, 3, 1, spp);
push(&mut buf, 279, 4, 1, (strip_bytes as u32).to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(pixels);
decode_tiff(&buf).unwrap().frame.planes[0].data.clone()
}
#[test]
fn encode_cielab8_uncompressed_roundtrip() {
let pixels = lab_pattern_3sample(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (8, 8));
assert_eq!(d.pixel_format, TiffPixelFormat::Rgb24);
let want = decode_3sample_cielab(&pixels, 8, 8);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_cielab8_compressors_match_uncompressed() {
let pixels = lab_pattern_3sample(16, 8);
let baseline = {
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
for c in [
TiffCompression::PackBits,
TiffCompression::Lzw,
TiffCompression::Deflate,
] {
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: c,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let d = decode_tiff(&encode_tiff(&page).unwrap()).unwrap();
assert_eq!(d.frame.planes[0].data, baseline, "compressor {:?}", c);
}
}
#[test]
fn encode_cielab8_predictor_composes() {
let pixels = lab_pattern_3sample(20, 12);
let no_pred = {
let page = EncodePage {
width: 20,
height: 12,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
let with_pred = {
let page = EncodePage {
width: 20,
height: 12,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: true,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
assert_eq!(no_pred, with_pred);
}
#[test]
fn encode_cielab8_planar_composes() {
let pixels = lab_pattern_3sample(16, 8);
let chunky = {
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::Deflate,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
let planar = {
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::Deflate,
predictor: false,
planar: true,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
assert_eq!(chunky, planar);
}
#[test]
fn encode_cielab8_tiled_composes() {
let pixels = lab_pattern_3sample(32, 32);
let strip = {
let page = EncodePage {
width: 32,
height: 32,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
let tiled = {
let page = EncodePage {
width: 32,
height: 32,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: Some((16, 16)),
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
assert_eq!(strip, tiled);
}
#[test]
fn encode_cielab8_bigtiff_composes() {
let pixels = lab_pattern_3sample(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::Deflate,
predictor: false,
planar: false,
tiling: None,
bigtiff: true,
};
let bytes = encode_tiff(&page).unwrap();
assert_eq!(&bytes[..2], b"II");
assert_eq!(u16::from_le_bytes([bytes[2], bytes[3]]), 43);
let d = decode_tiff(&bytes).unwrap();
let want = decode_3sample_cielab(&pixels, 8, 8);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_cielab8_rejects_ccitt() {
let pixels = lab_pattern_3sample(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::CcittRle,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let err = encode_tiff(&page).unwrap_err();
assert!(format!("{err}").contains("CCITT"));
}
#[test]
fn encode_cielab8_wrong_buffer_size_rejected() {
let bad = vec![0u8; 100];
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &bad },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let err = encode_tiff(&page).unwrap_err();
assert!(format!("{err}").contains("CieLab8"));
}
#[test]
fn encode_cielab_l8_roundtrip() {
let pixels = lab_l_ramp(8, 4);
let page = EncodePage {
width: 8,
height: 4,
kind: EncodePixelFormat::CieLabL8 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (8, 4));
assert_eq!(d.pixel_format, TiffPixelFormat::Gray8);
let want = decode_1sample_cielab(&pixels, 8, 4);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_cielab_l8_rejects_planar() {
let pixels = lab_l_ramp(8, 4);
let page = EncodePage {
width: 8,
height: 4,
kind: EncodePixelFormat::CieLabL8 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: true,
tiling: None,
bigtiff: false,
};
let err = encode_tiff(&page).unwrap_err();
assert!(format!("{err}").contains("PlanarConfiguration"));
}
#[test]
fn encode_cielab_writes_photometric_8() {
let pixels = lab_pattern_3sample(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::CieLab8 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let ifd_off = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
let count = u16::from_le_bytes([bytes[ifd_off], bytes[ifd_off + 1]]) as usize;
let mut found = None;
for k in 0..count {
let entry_off = ifd_off + 2 + k * 12;
let tag = u16::from_le_bytes([bytes[entry_off], bytes[entry_off + 1]]);
if tag == 262 {
let val = u16::from_le_bytes([bytes[entry_off + 8], bytes[entry_off + 9]]);
found = Some(val);
}
}
assert_eq!(found, Some(8), "expected PhotometricInterpretation = 8");
}
fn cmyk_pattern_4sample(w: u32, h: u32) -> Vec<u8> {
let mut v = Vec::with_capacity((w * h * 4) as usize);
for y in 0..h {
for x in 0..w {
let c = ((x.wrapping_mul(7)) & 0xFF) as u8;
let m = ((y.wrapping_mul(11)) & 0xFF) as u8;
let y_byte = ((x ^ y).wrapping_mul(13) & 0xFF) as u8;
let k = ((y * 255) / h.max(1).saturating_sub(1).max(1)) as u8;
v.extend_from_slice(&[c, m, y_byte, k]);
}
}
v
}
fn decode_cmyk_4sample(pixels: &[u8], w: u32, h: u32) -> Vec<u8> {
let row_bytes = (w as u64) * 4;
let strip_bytes = row_bytes * (h as u64);
assert_eq!(pixels.len() as u64, strip_bytes);
let num_entries: u16 = 11;
let ifd_offset: u32 = 8;
let ifd_size: u32 = 2 + (num_entries as u32) * 12 + 4;
let bps_blob_bytes: u32 = 4 * 2; let blobs_offset: u32 = ifd_offset + ifd_size;
let bps_off = blobs_offset;
let pixels_off = bps_off + bps_blob_bytes;
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"II");
buf.extend_from_slice(&42u16.to_le_bytes());
buf.extend_from_slice(&ifd_offset.to_le_bytes());
buf.extend_from_slice(&num_entries.to_le_bytes());
let push = |buf: &mut Vec<u8>, tag: u16, ft: u16, count: u32, v: [u8; 4]| {
buf.extend_from_slice(&tag.to_le_bytes());
buf.extend_from_slice(&ft.to_le_bytes());
buf.extend_from_slice(&count.to_le_bytes());
buf.extend_from_slice(&v);
};
push(&mut buf, 254, 4, 1, 0u32.to_le_bytes());
push(&mut buf, 256, 4, 1, w.to_le_bytes());
push(&mut buf, 257, 4, 1, h.to_le_bytes());
push(&mut buf, 258, 3, 4, bps_off.to_le_bytes());
let mut comp = [0u8; 4];
comp[..2].copy_from_slice(&1u16.to_le_bytes());
push(&mut buf, 259, 3, 1, comp);
let mut ph = [0u8; 4];
ph[..2].copy_from_slice(&5u16.to_le_bytes());
push(&mut buf, 262, 3, 1, ph);
push(&mut buf, 273, 4, 1, pixels_off.to_le_bytes());
let mut spp = [0u8; 4];
spp[..2].copy_from_slice(&4u16.to_le_bytes());
push(&mut buf, 277, 3, 1, spp);
push(&mut buf, 279, 4, 1, (strip_bytes as u32).to_le_bytes());
let mut ink = [0u8; 4];
ink[..2].copy_from_slice(&1u16.to_le_bytes());
push(&mut buf, 332, 3, 1, ink);
let mut nink = [0u8; 4];
nink[..2].copy_from_slice(&4u16.to_le_bytes());
push(&mut buf, 334, 3, 1, nink);
buf.extend_from_slice(&0u32.to_le_bytes());
for _ in 0..4u16 {
buf.extend_from_slice(&8u16.to_le_bytes());
}
buf.extend_from_slice(pixels);
decode_tiff(&buf).unwrap().frame.planes[0].data.clone()
}
#[test]
fn encode_cmyk32_uncompressed_roundtrip() {
let pixels = cmyk_pattern_4sample(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!((d.width, d.height), (8, 8));
assert_eq!(d.pixel_format, TiffPixelFormat::Rgb24);
let want = decode_cmyk_4sample(&pixels, 8, 8);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_cmyk32_compressors_match_uncompressed() {
let pixels = cmyk_pattern_4sample(16, 8);
let baseline = {
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
for c in [
TiffCompression::PackBits,
TiffCompression::Lzw,
TiffCompression::Deflate,
] {
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: c,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let d = decode_tiff(&encode_tiff(&page).unwrap()).unwrap();
assert_eq!(d.frame.planes[0].data, baseline, "compressor {:?}", c);
}
}
#[test]
fn encode_cmyk32_predictor_composes() {
let pixels = cmyk_pattern_4sample(20, 12);
let no_pred = {
let page = EncodePage {
width: 20,
height: 12,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
let with_pred = {
let page = EncodePage {
width: 20,
height: 12,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: true,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
assert_eq!(no_pred, with_pred);
}
#[test]
fn encode_cmyk32_planar_composes() {
let pixels = cmyk_pattern_4sample(16, 8);
let chunky = {
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::Deflate,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
let planar = {
let page = EncodePage {
width: 16,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::Deflate,
predictor: false,
planar: true,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
assert_eq!(chunky, planar);
}
#[test]
fn encode_cmyk32_tiled_composes() {
let pixels = cmyk_pattern_4sample(32, 32);
let strip = {
let page = EncodePage {
width: 32,
height: 32,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
let tiled = {
let page = EncodePage {
width: 32,
height: 32,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::Lzw,
predictor: false,
planar: false,
tiling: Some((16, 16)),
bigtiff: false,
};
decode_tiff(&encode_tiff(&page).unwrap())
.unwrap()
.frame
.planes[0]
.data
.clone()
};
assert_eq!(strip, tiled);
}
#[test]
fn encode_cmyk32_bigtiff_composes() {
let pixels = cmyk_pattern_4sample(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::Deflate,
predictor: false,
planar: false,
tiling: None,
bigtiff: true,
};
let bytes = encode_tiff(&page).unwrap();
assert_eq!(&bytes[..2], b"II");
assert_eq!(u16::from_le_bytes([bytes[2], bytes[3]]), 43);
let d = decode_tiff(&bytes).unwrap();
let want = decode_cmyk_4sample(&pixels, 8, 8);
assert_eq!(d.frame.planes[0].data, want);
}
#[test]
fn encode_cmyk32_rejects_ccitt() {
let pixels = cmyk_pattern_4sample(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::CcittRle,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let err = encode_tiff(&page).unwrap_err();
assert!(format!("{err}").contains("CCITT"));
}
#[test]
fn encode_cmyk32_wrong_buffer_size_rejected() {
let bad = vec![0u8; 100];
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &bad },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let err = encode_tiff(&page).unwrap_err();
assert!(format!("{err}").contains("Cmyk32"));
}
#[test]
fn encode_cmyk32_writes_photometric_inkset_and_numberofinks() {
let pixels = cmyk_pattern_4sample(8, 8);
let page = EncodePage {
width: 8,
height: 8,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let ifd_off = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
let count = u16::from_le_bytes([bytes[ifd_off], bytes[ifd_off + 1]]) as usize;
let mut photo = None;
let mut ink_set = None;
let mut num_inks = None;
for k in 0..count {
let entry_off = ifd_off + 2 + k * 12;
let tag = u16::from_le_bytes([bytes[entry_off], bytes[entry_off + 1]]);
let val = u16::from_le_bytes([bytes[entry_off + 8], bytes[entry_off + 9]]);
match tag {
262 => photo = Some(val),
332 => ink_set = Some(val),
334 => num_inks = Some(val),
_ => {}
}
}
assert_eq!(photo, Some(5), "expected PhotometricInterpretation = 5");
assert_eq!(ink_set, Some(1), "expected InkSet = 1 (CMYK)");
assert_eq!(num_inks, Some(4), "expected NumberOfInks = 4");
}
#[test]
fn encode_cmyk32_pure_inks_collapse_to_expected_rgb() {
let pixels = [
255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, ];
let page = EncodePage {
width: 4,
height: 1,
kind: EncodePixelFormat::Cmyk32 { pixels: &pixels },
compression: TiffCompression::None,
predictor: false,
planar: false,
tiling: None,
bigtiff: false,
};
let bytes = encode_tiff(&page).unwrap();
let d = decode_tiff(&bytes).unwrap();
assert_eq!(d.pixel_format, TiffPixelFormat::Rgb24);
let rgb = &d.frame.planes[0].data;
assert_eq!(&rgb[0..3], &[0, 255, 255]);
assert_eq!(&rgb[3..6], &[255, 0, 255]);
assert_eq!(&rgb[6..9], &[255, 255, 0]);
assert_eq!(&rgb[9..12], &[0, 0, 0]);
}
}