use std::fs::File;
use std::io::{self, BufWriter, Seek, Write};
use std::path::Path;
use tiff::encoder::{colortype, Compression, DeflateLevel, TiffEncoder};
use tiff::tags::{ExtraSamples, Tag};
use tiff::TiffError;
use super::check_pixels;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TiffCompression {
None,
#[default]
Deflate,
Lzw,
Packbits,
}
impl TiffCompression {
fn to_encoder(self) -> Compression {
match self {
TiffCompression::None => Compression::Uncompressed,
TiffCompression::Deflate => Compression::Deflate(DeflateLevel::Balanced),
TiffCompression::Lzw => Compression::Lzw,
TiffCompression::Packbits => Compression::Packbits,
}
}
}
pub fn write_tiff_to<W: Write + Seek>(
writer: W,
width: u32,
height: u32,
pixels: &[u8],
compression: TiffCompression,
) -> io::Result<()> {
check_pixels(width, height, pixels)?;
let mut encoder = TiffEncoder::new(writer)
.map_err(io_err)?
.with_compression(compression.to_encoder());
let mut image = encoder
.new_image::<colortype::RGBA8>(width, height)
.map_err(io_err)?;
image
.encoder()
.write_tag(
Tag::ExtraSamples,
&[ExtraSamples::UnassociatedAlpha.to_u16()][..],
)
.map_err(io_err)?;
image.write_data(pixels).map_err(io_err)
}
pub fn encode_tiff(
width: u32,
height: u32,
pixels: &[u8],
compression: TiffCompression,
) -> io::Result<Vec<u8>> {
let mut out = io::Cursor::new(Vec::new());
write_tiff_to(&mut out, width, height, pixels, compression)?;
Ok(out.into_inner())
}
pub fn write_tiff(
path: impl AsRef<Path>,
width: u32,
height: u32,
pixels: &[u8],
compression: TiffCompression,
) -> io::Result<()> {
let file = File::create(path)?;
write_tiff_to(BufWriter::new(file), width, height, pixels, compression)
}
fn io_err(e: TiffError) -> io::Error {
io::Error::other(e)
}
#[cfg(test)]
mod tests {
use super::*;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::ColorType;
const ALL: [TiffCompression; 4] = [
TiffCompression::None,
TiffCompression::Deflate,
TiffCompression::Lzw,
TiffCompression::Packbits,
];
fn gradient(width: u32, height: u32) -> Vec<u8> {
let mut px = Vec::with_capacity((width as usize) * (height as usize) * 4);
for y in 0..height {
for x in 0..width {
px.extend_from_slice(&[x as u8, y as u8, 128, (x * 8) as u8]);
}
}
px
}
#[test]
fn encode_tiff_opens_with_a_byte_order_marker() {
let bytes = encode_tiff(4, 3, &gradient(4, 3), TiffCompression::Deflate).expect("encode");
assert!(
bytes.starts_with(b"II\x2a\x00") || bytes.starts_with(b"MM\x00\x2a"),
"not a TIFF header: {:?}",
&bytes[..4]
);
}
#[test]
fn every_compression_round_trips_the_pixels_exactly() {
let pixels = gradient(16, 9);
for compression in ALL {
let bytes = encode_tiff(16, 9, &pixels, compression).expect("encode");
let mut decoder = Decoder::new(io::Cursor::new(bytes)).expect("decode");
assert_eq!(decoder.dimensions().expect("dimensions"), (16, 9));
assert_eq!(decoder.colortype().expect("colortype"), ColorType::RGBA(8));
match decoder.read_image().expect("read") {
DecodingResult::U8(got) => {
assert_eq!(got, pixels, "{compression:?} altered the pixels")
}
other => panic!("{compression:?} decoded as {other:?}, expected 8-bit samples"),
}
}
}
#[test]
fn alpha_is_declared_unassociated() {
let bytes = encode_tiff(4, 3, &gradient(4, 3), TiffCompression::Deflate).expect("encode");
let mut decoder = Decoder::new(io::Cursor::new(bytes)).expect("decode");
assert_eq!(
decoder
.get_tag_u32_vec(Tag::ExtraSamples)
.expect("ExtraSamples"),
vec![u32::from(ExtraSamples::UnassociatedAlpha.to_u16())]
);
}
#[test]
fn encode_tiff_and_write_tiff_agree_byte_for_byte() {
let pixels = gradient(5, 4);
let in_memory = encode_tiff(5, 4, &pixels, TiffCompression::Lzw).expect("encode");
let dir = std::env::temp_dir().join("hephaestus_tiff_roundtrip");
std::fs::create_dir_all(&dir).expect("create temp dir");
let path = dir.join("roundtrip.tiff");
write_tiff(&path, 5, 4, &pixels, TiffCompression::Lzw).expect("write");
let on_disk = std::fs::read(&path).expect("read back");
let _ = std::fs::remove_file(&path);
assert_eq!(in_memory, on_disk);
}
#[test]
fn wrong_length_buffer_is_rejected() {
let short = vec![0u8; 4 * 3 * 4 - 1];
let err = encode_tiff(4, 3, &short, TiffCompression::Deflate).expect_err("must fail");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let mut sink = io::Cursor::new(Vec::new());
let err = write_tiff_to(&mut sink, 4, 3, &short, TiffCompression::Deflate)
.expect_err("must fail");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert!(
sink.into_inner().is_empty(),
"nothing should be written on a size error"
);
}
}