vectorcraft/crates/render/src/encode/tiff.rs

366 lines
13 KiB
Rust

//! TIFF export (baseline TIFF 6.0): little-endian (`II`) or big-endian (`MM`), strips of about
//! 8 KiB stored as they are, LZW-compressed (weezl, with TIFF's early code-size switch) or
//! PackBits; black and white, grey, RGB or CMYK (separated, ink amounts) samples with an optional
//! unassociated alpha channel, the resolution and an embedded colour profile. [`write`] is the one
//! TIFF writer: raster exports ([`encode`]) and EPS previews both use it.
use std::borrow::Cow;
use vectorcraft_color::cms;
use super::jpeg::{self, ColorModel};
use super::on_white;
use crate::Rendered;
/// The order of the bytes in the file's numbers (the samples are bytes either way).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ByteOrder {
/// Least significant byte first (`II`).
#[default]
Little,
/// Most significant byte first (`MM`).
Big,
}
impl ByteOrder {
pub const ALL: [ByteOrder; 2] = [ByteOrder::Little, ByteOrder::Big];
pub fn id(self) -> &'static str {
match self {
ByteOrder::Little => "little",
ByteOrder::Big => "big",
}
}
pub fn label(self) -> &'static str {
match self {
ByteOrder::Little => "Little-endian (II)",
ByteOrder::Big => "Big-endian (MM)",
}
}
/// By id or label, ignoring case; also by the file's marker (`ii`, `mm`) and the platforms the
/// orders are known by (`ibm`, `mac`).
pub fn from_id(s: &str) -> Option<Self> {
let s = s.to_ascii_lowercase();
match s.as_str() {
"ii" | "ibm" | "pc" | "intel" => Some(ByteOrder::Little),
"mm" | "mac" | "motorola" => Some(ByteOrder::Big),
_ => Self::ALL.into_iter().find(|o| o.id() == s || o.label().eq_ignore_ascii_case(&s)),
}
}
fn u16(self, v: u16) -> [u8; 2] {
match self {
ByteOrder::Little => v.to_le_bytes(),
ByteOrder::Big => v.to_be_bytes(),
}
}
fn u32(self, v: u32) -> [u8; 4] {
match self {
ByteOrder::Little => v.to_le_bytes(),
ByteOrder::Big => v.to_be_bytes(),
}
}
}
/// How the strips are compressed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Compression {
None,
Lzw,
/// Run lengths, each row on its own.
PackBits,
}
impl Compression {
fn code(self) -> u16 {
match self {
Compression::None => 1,
Compression::Lzw => 5,
Compression::PackBits => 32773,
}
}
}
/// What the samples mean.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Photometric {
/// Grey or bilevel, 0 is white (a set bit is black).
WhiteIsZero,
/// Grey, 0 is black.
BlackIsZero,
Rgb,
/// Ink amounts, 0 is no ink (CMYK).
Separated,
}
impl Photometric {
fn code(self) -> u16 {
match self {
Photometric::WhiteIsZero => 0,
Photometric::BlackIsZero => 1,
Photometric::Rgb => 2,
Photometric::Separated => 5,
}
}
}
/// The pixels [`write`] stores.
pub struct Image<'a> {
pub width: u32,
pub height: u32,
/// Rows of samples, `bits` per sample, each row starting on a byte.
pub data: &'a [u8],
pub photometric: Photometric,
/// Samples per pixel, the alpha one included.
pub samples: u16,
/// Bits per sample: 1 or 8.
pub bits: u16,
/// The last sample is unassociated alpha.
pub alpha: bool,
}
/// How [`write`] stores the pixels and what else goes in the file.
pub struct Layout<'a> {
pub byte_order: ByteOrder,
pub compression: Compression,
/// Pixels per inch.
pub ppi: f64,
/// A colour profile to embed.
pub icc: Option<&'a [u8]>,
}
/// Uncompressed bytes per strip (the size the TIFF specification recommends).
const STRIP_BYTES: usize = 8 << 10;
/// A field's values.
enum Values {
Ascii(Vec<u8>),
Short(Vec<u16>),
Long(Vec<u32>),
Rational(u32, u32),
Undefined(Vec<u8>),
}
impl Values {
/// Field type and count.
fn kind(&self) -> (u16, usize) {
match self {
Values::Ascii(v) => (2, v.len()),
Values::Short(v) => (3, v.len()),
Values::Long(v) => (4, v.len()),
Values::Rational(..) => (5, 1),
Values::Undefined(v) => (7, v.len()),
}
}
/// Size in bytes.
fn len(&self) -> usize {
match self {
Values::Ascii(v) | Values::Undefined(v) => v.len(),
Values::Short(v) => v.len() * 2,
Values::Long(v) => v.len() * 4,
Values::Rational(..) => 8,
}
}
fn bytes(&self, o: ByteOrder) -> Vec<u8> {
match self {
Values::Ascii(v) | Values::Undefined(v) => v.clone(),
Values::Short(v) => v.iter().flat_map(|x| o.u16(*x)).collect(),
Values::Long(v) => v.iter().flat_map(|x| o.u32(*x)).collect(),
Values::Rational(n, d) => [o.u32(*n), o.u32(*d)].concat(),
}
}
}
/// Write `img` as a TIFF file: header, the image file directory, the values too long for it, then
/// the strips.
pub fn write(img: &Image, l: &Layout) -> Result<Vec<u8>, String> {
let too_large = || "the image is too large for TIFF (at most 4 GB): lower the resolution".to_string();
let (w, h) = (img.width as usize, img.height as usize);
let row = (w * usize::from(img.samples) * usize::from(img.bits)).div_ceil(8);
if w == 0 || h == 0 || !matches!(img.bits, 1 | 8) || img.samples == 0 || row.checked_mul(h) != Some(img.data.len()) {
return Err("TIFF encoding failed: the pixel buffer doesn't match the image size".into());
}
let rows_per_strip = (STRIP_BYTES / row).clamp(1, h);
let mut strips: Vec<u8> = Vec::with_capacity(img.data.len() / 2);
let mut offsets = vec![];
for strip in img.data.chunks(rows_per_strip * row) {
offsets.push(strips.len());
match l.compression {
Compression::None => strips.extend_from_slice(strip),
Compression::Lzw => {
let lzw = weezl::encode::Encoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
.encode(strip)
.map_err(|e| format!("TIFF encoding failed: {e}"))?;
strips.extend(lzw);
}
Compression::PackBits => strip.chunks(row).for_each(|r| packbits(r, &mut strips)),
}
// Strips start on a word boundary.
if strips.len() % 2 == 1 {
strips.push(0);
}
}
let counts: Vec<usize> = offsets.iter().zip(offsets.iter().skip(1).chain([&strips.len()])).map(|(a, b)| b - a).collect();
let mut fields: Vec<(u16, Values)> = vec![
(256, Values::Long(vec![img.width])),
(257, Values::Long(vec![img.height])),
(258, Values::Short(vec![img.bits; usize::from(img.samples)])),
(259, Values::Short(vec![l.compression.code()])),
(262, Values::Short(vec![img.photometric.code()])),
// StripOffsets: filled in once the directory's size is known.
(273, Values::Long(vec![0; offsets.len()])),
(277, Values::Short(vec![img.samples])),
(278, Values::Long(vec![rows_per_strip as u32])),
(279, Values::Long(counts.iter().map(|c| u32::try_from(*c).unwrap_or(u32::MAX)).collect())),
(282, resolution(l.ppi)),
(283, resolution(l.ppi)),
// Chunky: the samples of a pixel together.
(284, Values::Short(vec![1])),
// Inches.
(296, Values::Short(vec![2])),
(305, Values::Ascii(b"VectorCraft\0".to_vec())),
];
if img.photometric == Photometric::Separated {
// InkSet: CMYK.
fields.push((332, Values::Short(vec![1])));
}
if img.alpha {
// Unassociated alpha.
fields.push((338, Values::Short(vec![2])));
}
if let Some(icc) = l.icc {
fields.push((34675, Values::Undefined(icc.to_vec())));
}
// Header, directory (count, 12-byte entries, next directory), longer values, strips.
let dir_len = 2 + fields.len() * 12 + 4;
let extra_len: usize = fields.iter().map(|(_, v)| v.len()).filter(|n| *n > 4).map(|n| n.next_multiple_of(2)).sum();
let strip_at = 8 + dir_len + extra_len;
let end = strip_at.checked_add(strips.len()).filter(|n| u32::try_from(*n).is_ok()).ok_or_else(too_large)?;
if let Some((_, Values::Long(v))) = fields.iter_mut().find(|(tag, _)| *tag == 273) {
*v = offsets.iter().map(|o| (strip_at + o) as u32).collect();
}
let o = l.byte_order;
let mut out = Vec::with_capacity(end);
out.extend_from_slice(if o == ByteOrder::Little { b"II" } else { b"MM" });
out.extend(o.u16(42));
out.extend(o.u32(8));
out.extend(o.u16(fields.len() as u16));
let mut extra = Vec::with_capacity(extra_len);
for (tag, v) in &fields {
let (ty, count) = v.kind();
let mut bytes = v.bytes(o);
out.extend(o.u16(*tag));
out.extend(o.u16(ty));
out.extend(o.u32(u32::try_from(count).map_err(|_| too_large())?));
if bytes.len() <= 4 {
// Left-justified in the value field.
bytes.resize(4, 0);
out.extend(bytes);
} else {
out.extend(o.u32((8 + dir_len + extra.len()) as u32));
extra.extend(&bytes);
if bytes.len() % 2 == 1 {
extra.push(0);
}
}
}
out.extend(o.u32(0));
out.extend(extra);
out.extend(strips);
Ok(out)
}
/// A resolution as a rational, to a hundredth of a pixel per inch.
fn resolution(ppi: f64) -> Values {
let ppi = if ppi.is_finite() && ppi > 0.0 { ppi } else { 72.0 };
if ppi.fract() == 0.0 && ppi <= u32::MAX as f64 {
return Values::Rational(ppi as u32, 1);
}
Values::Rational((ppi * 100.0).round().clamp(1.0, u32::MAX as f64) as u32, 100)
}
/// `data` run-length encoded (PackBits: what TIFF's compression 32773 and PostScript's
/// `RunLengthDecode` read), without an end marker.
pub fn packbits(data: &[u8], out: &mut Vec<u8>) {
let mut i = 0;
while i < data.len() {
let Some(&b) = data.get(i) else { break };
let run = data.get(i..).map_or(1, |rest| rest.iter().take(128).take_while(|x| **x == b).count());
if run >= 3 {
out.push((257 - run) as u8);
out.push(b);
i += run;
continue;
}
// Literal bytes up to the next run of three.
let mut j = i;
while j < data.len() && j - i < 128 {
let three = data.get(j..j + 3).is_some_and(|w| w[0] == w[1] && w[1] == w[2]);
if three {
break;
}
j += 1;
}
let j = j.max(i + 1).min(data.len());
out.push((j - i - 1) as u8);
out.extend(data.get(i..j).unwrap_or_default());
i = j;
}
}
/// The TIFF options of a raster export.
#[derive(Clone, Debug, PartialEq)]
pub struct TiffOptions {
pub color_model: ColorModel,
pub lzw: bool,
pub byte_order: ByteOrder,
/// Embed the colour profile of the colour model.
pub embed_icc: bool,
}
impl Default for TiffOptions {
fn default() -> Self {
Self { color_model: ColorModel::Rgb, lzw: true, byte_order: ByteOrder::Little, embed_icc: true }
}
}
/// `img` as a TIFF in `o.color_model`: RGB, with an alpha channel when some pixel isn't opaque,
/// grey on white (few readers take grey with alpha), or CMYK (its colours separated on white;
/// [`crate::Renderer::export_region`] draws inks).
pub fn encode(img: &Rendered, ppi: f64, o: &TiffOptions) -> Result<Vec<u8>, String> {
let px = match o.color_model {
ColorModel::Cmyk => jpeg::separated(img),
ColorModel::Rgb | ColorModel::Gray => img.to_straight(),
};
encode_samples(&px, img.width, img.height, ppi, o)
}
/// Pixels as a TIFF in `o.color_model` (see [`encode`]): straight RGBA for RGB and grey, ink
/// amounts for CMYK.
pub(crate) fn encode_samples(px: &[u8], width: u32, height: u32, ppi: f64, o: &TiffOptions) -> Result<Vec<u8>, String> {
let model = o.color_model;
let (data, photometric, alpha) = match model {
ColorModel::Cmyk => (Cow::Borrowed(px), Photometric::Separated, false),
ColorModel::Rgb => {
let rgba = px.as_chunks::<4>().0;
let alpha = rgba.iter().any(|p| p[3] < 255);
let data = if alpha { Cow::Borrowed(px) } else { rgba.iter().flat_map(|p| [p[0], p[1], p[2]]).collect() };
(data, Photometric::Rgb, alpha)
}
ColorModel::Gray => {
let gray = jpeg::to_gray();
(px.as_chunks::<4>().0.iter().map(|p| gray(on_white(p))).collect(), Photometric::BlackIsZero, false)
}
};
let icc = if o.embed_icc { Some(cms::icc_bytes(&model.profile()).map_err(|e| format!("TIFF colour profile: {e}"))?) } else { None };
let samples = (model.channels() + usize::from(alpha)) as u16;
let image = Image { width, height, data: &data, photometric, samples, bits: 8, alpha };
let compression = if o.lzw { Compression::Lzw } else { Compression::None };
write(&image, &Layout { byte_order: o.byte_order, compression, ppi, icc: icc.as_deref() })
}