mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-tiff: YCbCr to RGB and uncompressed/PackBits YCbCr decoding (#971)
PhotoCraft's YCbCr conversion (ReferenceBlackWhite, YCbCrCoefficients, libtiff-compatible green) and FilmCraft's decoding of uncompressed and PackBits subsampled chunky YCbCr images, with PackBits as its own module.
This commit is contained in:
1 parent
08882273d1
commit
603d43da85
5 files changed
+531
No files matched your search
@@ -7,6 +7,8 @@
|
||||
//! maker-note IFDs ([`makernote`]).
|
||||
//! - [`image::ImageInfo`] describes the pixel layout of an IFD (strips or tiles, planar config,
|
||||
//! compression, predictor) and enumerates its [`image::Chunk`]s.
|
||||
//! - [`ycbcr`] converts YCbCr samples to RGB (ReferenceBlackWhite, YCbCrCoefficients) and decodes
|
||||
//! uncompressed and PackBits subsampled YCbCr images; [`packbits`] decompresses PackBits.
|
||||
//! - [`writer::TiffWriter`] writes IFD trees (with strips/tiles, `SubIFDs` and pointer IFDs) in
|
||||
//! either byte order, classic or BigTIFF.
|
||||
//!
|
||||
@@ -22,9 +24,11 @@
|
||||
|
||||
pub mod image;
|
||||
pub mod makernote;
|
||||
pub mod packbits;
|
||||
mod reader;
|
||||
pub mod tags;
|
||||
pub mod writer;
|
||||
pub mod ycbcr;
|
||||
|
||||
pub use reader::{DirectoryEntries, EntrySpan, ParseOptions, decode as decode_value, directory_entries, entry_span, parse_ifd_at};
|
||||
pub use writer::{IfdBuilder, ImageData, TiffWriter};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//! PackBits (TIFF 6.0 §9) decompression.
|
||||
|
||||
/// Appends the bytes `src` decompresses to onto `out`, stopping once `out` holds `limit` bytes or
|
||||
/// at a run that the data ends inside. A header of −128 is a no-op.
|
||||
pub fn decode(src: &[u8], out: &mut Vec<u8>, limit: usize) {
|
||||
let mut p = 0;
|
||||
while out.len() < limit {
|
||||
let Some(&n) = src.get(p) else { return };
|
||||
let n = n as i8;
|
||||
p += 1;
|
||||
if n >= 0 {
|
||||
let len = n as usize + 1;
|
||||
let Some(lit) = p.checked_add(len).and_then(|end| src.get(p..end)) else { return };
|
||||
out.extend_from_slice(lit.get(..len.min(limit - out.len())).unwrap_or(lit));
|
||||
p += len;
|
||||
} else if n != -128 {
|
||||
let Some(&b) = src.get(p) else { return };
|
||||
let len = (1 - isize::from(n)) as usize;
|
||||
out.extend(std::iter::repeat_n(b, len.min(limit - out.len())));
|
||||
p += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn runs_literals_and_noops() {
|
||||
let mut out = Vec::new();
|
||||
// A literal of 2, a no-op, a run of 3, then a literal cut short by the end of the data.
|
||||
decode(&[1, 10, 11, 0x80, 0xFE, 7, 4, 1], &mut out, 100);
|
||||
assert_eq!(out, [10, 11, 7, 7, 7]);
|
||||
let mut capped = Vec::new();
|
||||
decode(&[0xFE, 7, 0xFE, 8], &mut capped, 4);
|
||||
assert_eq!(capped, [7, 7, 7, 8]);
|
||||
// The longest run, 128 bytes from two, appended to what is there.
|
||||
let mut out = vec![1];
|
||||
decode(&[0x81, 9], &mut out, 1000);
|
||||
assert_eq!((out.len(), out[1], out[128]), (129, 9, 9));
|
||||
let mut out = Vec::new();
|
||||
decode(&[0xFF], &mut out, 10);
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
//! YCbCr images (PhotometricInterpretation 6, TIFF 6.0 §20–21): the conversion to RGB, and the
|
||||
//! decoding of uncompressed and PackBits chunky 8-bit data units of any subsampling.
|
||||
//!
|
||||
//! [`YCbCr`] maps stored Y, Cb, Cr codes to RGB: ReferenceBlackWhite scales the codes and
|
||||
//! YCbCrCoefficients gives the luma weights; missing or unusable tags fall back to the TIFF
|
||||
//! defaults (BT.601 weights, full-range luma, chroma centred on the middle code). JPEG-compressed
|
||||
//! data (compression 7) uses the JPEG (JFIF) conversion and ignores both tags, as libtiff does
|
||||
//! when it asks the JPEG library for RGB. Use it on samples another decoder produced.
|
||||
//!
|
||||
//! [`decode_rgb8`] decodes a whole image whose data this crate can read itself: one strip-based,
|
||||
//! chunky, 8-bit, uncompressed or PackBits YCbCr image, the data units expanded (chroma repeated
|
||||
//! across each unit) and converted.
|
||||
//!
|
||||
//! # Green: a compatibility choice
|
||||
//!
|
||||
//! Red and blue follow the specification (TIFF 6.0 p. 91: `R = Cr·(2 − 2·LumaRed) + Y`,
|
||||
//! `B = Cb·(2 − 2·LumaBlue) + Y`). Green follows libtiff (`TIFFYCbCrToRGBInit`), which rewrites
|
||||
//! the specification's `G = (Y − LumaBlue·B − LumaRed·R) / LumaGreen` as
|
||||
//! `G = Y − (LumaRed·(2 − 2·LumaRed) / LumaGreen)·Cr − (LumaBlue·(2 − 2·LumaBlue) / LumaGreen)·Cb`,
|
||||
//! adding Y unscaled. The two agree whenever the weights sum to 1, as every standard set does
|
||||
//! (BT.601, BT.709). For other weights the specification's form tints neutral grays; libtiff's
|
||||
//! keeps them neutral and matches libtiff-based tools.
|
||||
|
||||
use crate::{Ifd, TiffError, packbits, tags};
|
||||
|
||||
/// YCbCrCoefficients.
|
||||
pub const YCBCR_COEFFICIENTS: u16 = 529;
|
||||
/// ReferenceBlackWhite.
|
||||
pub const REFERENCE_BLACK_WHITE: u16 = 532;
|
||||
|
||||
/// BT.601 luma weights: the TIFF default for YCbCrCoefficients.
|
||||
pub const BT601: [f64; 3] = [0.299, 0.587, 0.114];
|
||||
|
||||
/// How one image's YCbCr codes map to RGB.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct YCbCr {
|
||||
/// Luma weights of red, green and blue.
|
||||
pub luma: [f64; 3],
|
||||
/// Black and white codes of Y, Cb and Cr.
|
||||
pub reference_black_white: [f64; 6],
|
||||
}
|
||||
|
||||
impl YCbCr {
|
||||
/// TIFF's defaults for samples whose largest code is `max` (255 or 65535): BT.601 weights,
|
||||
/// full-range luma, chroma centred on the middle code. Also the JPEG (JFIF) conversion.
|
||||
pub fn defaults(max: u32) -> Self {
|
||||
let m = f64::from(max);
|
||||
let mid = (m + 1.0) / 2.0;
|
||||
YCbCr { luma: BT601, reference_black_white: [0.0, m, mid, m, mid, m] }
|
||||
}
|
||||
|
||||
/// The conversion `ifd` asks for, for samples whose largest code is `max`. Weights must each
|
||||
/// lie strictly between 0 and 1 and every black/white pair must differ; otherwise that tag's
|
||||
/// default is used. JPEG-compressed images (compression 7) always use the defaults.
|
||||
pub fn from_ifd(ifd: &Ifd, max: u32) -> Self {
|
||||
let defaults = Self::defaults(max);
|
||||
if ifd.u16(tags::COMPRESSION) == Some(tags::compression::JPEG) {
|
||||
return defaults;
|
||||
}
|
||||
let luma = ifd
|
||||
.f64s(YCBCR_COEFFICIENTS)
|
||||
.and_then(|v| <[f64; 3]>::try_from(v.get(..3)?).ok())
|
||||
.filter(|l| l.iter().all(|c| c.is_finite() && *c > 0.0 && *c < 1.0))
|
||||
.unwrap_or(defaults.luma);
|
||||
let reference_black_white = ifd
|
||||
.f64s(REFERENCE_BLACK_WHITE)
|
||||
.and_then(|v| <[f64; 6]>::try_from(v.get(..6)?).ok())
|
||||
.filter(|r| r.iter().all(|v| v.is_finite()) && r.as_chunks::<2>().0.iter().all(|[black, white]| (white - black).abs() > f64::EPSILON))
|
||||
.unwrap_or(defaults.reference_black_white);
|
||||
YCbCr { luma, reference_black_white }
|
||||
}
|
||||
|
||||
/// Lookup tables for samples whose largest code is `max`, so a pixel costs a few additions.
|
||||
pub fn tables(&self, max: u32) -> Tables {
|
||||
let m = f64::from(max);
|
||||
let [lr, lg, lb] = self.luma;
|
||||
let [y_black, y_white, cb_black, cb_white, cr_black, cr_white] = self.reference_black_white;
|
||||
// TIFF 6.0: chroma codes span ±(max + 1) / 2 − 1 around the black code; with the default
|
||||
// ReferenceBlackWhite that is the code minus the middle code.
|
||||
let half = (m + 1.0) / 2.0 - 1.0;
|
||||
let codes = || (0..=max).map(f64::from);
|
||||
let y = codes().map(|v| (v - y_black) * m / (y_white - y_black)).collect();
|
||||
let cb: Vec<f64> = codes().map(|v| (v - cb_black) * half / (cb_white - cb_black)).collect();
|
||||
let cr: Vec<f64> = codes().map(|v| (v - cr_black) * half / (cr_white - cr_black)).collect();
|
||||
let (kr, kb) = (2.0 - 2.0 * lr, 2.0 - 2.0 * lb);
|
||||
Tables {
|
||||
max: m,
|
||||
y,
|
||||
cr_r: cr.iter().map(|v| kr * v).collect(),
|
||||
cr_g: cr.iter().map(|v| -lr * kr * v / lg).collect(),
|
||||
cb_g: cb.iter().map(|v| -lb * kb * v / lg).collect(),
|
||||
cb_b: cb.iter().map(|v| kb * v).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts interleaved 8-bit pixels of `channels` samples, the first three Y, Cb, Cr, to R,
|
||||
/// G, B in place; other samples (alpha) are kept. A trailing partial pixel is left as it is.
|
||||
/// The work is per pixel, so a caller may convert bands of whole pixels in parallel.
|
||||
pub fn convert_u8(&self, data: &mut [u8], channels: usize) {
|
||||
if channels < 3 {
|
||||
return;
|
||||
}
|
||||
let t = self.tables(255);
|
||||
for px in data.chunks_exact_mut(channels) {
|
||||
if let [y, cb, cr, ..] = px {
|
||||
let [r, g, b] = t.rgb(usize::from(*y), usize::from(*cb), usize::from(*cr));
|
||||
(*y, *cb, *cr) = (r as u8, g as u8, b as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`YCbCr::convert_u8`] for 16-bit samples.
|
||||
pub fn convert_u16(&self, data: &mut [u16], channels: usize) {
|
||||
if channels < 3 {
|
||||
return;
|
||||
}
|
||||
let t = self.tables(65535);
|
||||
for px in data.chunks_exact_mut(channels) {
|
||||
if let [y, cb, cr, ..] = px {
|
||||
let [r, g, b] = t.rgb(usize::from(*y), usize::from(*cb), usize::from(*cr));
|
||||
(*y, *cb, *cr) = (r as u16, g as u16, b as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-code contributions of one [`YCbCr`] conversion.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Tables {
|
||||
max: f64,
|
||||
/// Scaled luma of each Y code.
|
||||
y: Vec<f64>,
|
||||
/// Red and green contributions of each Cr code.
|
||||
cr_r: Vec<f64>,
|
||||
cr_g: Vec<f64>,
|
||||
/// Green and blue contributions of each Cb code.
|
||||
cb_g: Vec<f64>,
|
||||
cb_b: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Tables {
|
||||
/// R, G and B of one pixel, rounded and clamped to the code range. Codes past the range read
|
||||
/// as 0.
|
||||
pub fn rgb(&self, y: usize, cb: usize, cr: usize) -> [u32; 3] {
|
||||
let at = |t: &[f64], i: usize| t.get(i).copied().unwrap_or(0.0);
|
||||
let yv = at(&self.y, y);
|
||||
// Green adds Y unscaled, as libtiff does (see "Green: a compatibility choice").
|
||||
let r = yv + at(&self.cr_r, cr);
|
||||
let g = yv + at(&self.cr_g, cr) + at(&self.cb_g, cb);
|
||||
let b = yv + at(&self.cb_b, cb);
|
||||
[r, g, b].map(|v| v.round().clamp(0.0, self.max) as u32)
|
||||
}
|
||||
}
|
||||
|
||||
/// The YCbCrSubSampling of `ifd` (default 2 × 2), when it is valid: each factor 1, 2 or 4.
|
||||
pub fn subsampling(ifd: &Ifd) -> Option<(u32, u32)> {
|
||||
match ifd.u64s(tags::YCBCR_SUBSAMPLING).as_deref() {
|
||||
None => Some((2, 2)),
|
||||
Some([h, v, ..]) if [1, 2, 4].contains(h) && [1, 2, 4].contains(v) => Some((*h as u32, *v as u32)),
|
||||
Some(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes of the data units of a `width` × `height` image at `(h, v)` subsampling: the image
|
||||
/// rounded up to whole units, each `h × v` luma samples then Cb and Cr (TIFF 6.0 §21).
|
||||
pub fn units_len(width: u32, height: u32, (h, v): (u32, u32)) -> Option<usize> {
|
||||
let units = u64::from(width.div_ceil(h.max(1))).checked_mul(u64::from(height.div_ceil(v.max(1))))?;
|
||||
usize::try_from(units.checked_mul(u64::from(h) * u64::from(v) + 2)?).ok()
|
||||
}
|
||||
|
||||
/// Expands 8-bit data units to interleaved Y, Cb, Cr pixels, `width × height × 3` bytes, top row
|
||||
/// first: each unit's chroma is repeated across its luma samples. `None` when `units` is shorter
|
||||
/// than [`units_len`].
|
||||
pub fn expand_units(units: &[u8], width: u32, height: u32, (h, v): (u32, u32)) -> Option<Vec<u8>> {
|
||||
let (w, ht, sh, sv) = (width as usize, height as usize, h.max(1) as usize, v.max(1) as usize);
|
||||
let need = units_len(width, height, (h, v))?;
|
||||
let units = units.get(..need)?;
|
||||
let units_x = w.div_ceil(sh);
|
||||
let mut out = vec![0u8; w.checked_mul(ht)?.checked_mul(3)?];
|
||||
for (i, unit) in units.chunks_exact(sh * sv + 2).enumerate() {
|
||||
let (ux, uy) = (i % units_x, i / units_x);
|
||||
let (luma, chroma) = unit.split_at(sh * sv);
|
||||
let [cb, cr] = <[u8; 2]>::try_from(chroma).ok()?;
|
||||
for (k, &y) in luma.iter().enumerate() {
|
||||
let (x, row) = (ux * sh + k % sh, uy * sv + k / sh);
|
||||
if x < w && row < ht {
|
||||
let at = (row * w + x) * 3;
|
||||
if let Some(px) = out.get_mut(at..at + 3) {
|
||||
px.copy_from_slice(&[y, cb, cr]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Decodes the YCbCr image of `ifd` in `data` (the whole file) to RGB, `width × height × 3`
|
||||
/// bytes, top row first. The image must be strip-based, chunky, three 8-bit samples a pixel and
|
||||
/// uncompressed or PackBits; anything else is [`TiffError::Invalid`]. More than `max_pixels`
|
||||
/// pixels is [`TiffError::Limit`], and a file too small to hold the image is refused before the data units are allocated.
|
||||
pub fn decode_rgb8(data: &[u8], ifd: &Ifd, max_pixels: u64) -> crate::Result<Vec<u8>> {
|
||||
// TiffError has no "unsupported" variant; adding one would break exhaustive matches downstream.
|
||||
let unsupported = |what: &str| TiffError::Invalid(format!("unsupported YCbCr image with {what}"));
|
||||
let invalid = |what: &str| TiffError::Invalid(format!("YCbCr image: {what}"));
|
||||
if ifd.u16(tags::PHOTOMETRIC) != Some(tags::photometric::YCBCR) {
|
||||
return Err(invalid("PhotometricInterpretation is not YCbCr"));
|
||||
}
|
||||
let width = ifd.u32(tags::IMAGE_WIDTH).ok_or(TiffError::MissingTag(tags::IMAGE_WIDTH))?;
|
||||
let height = ifd.u32(tags::IMAGE_LENGTH).ok_or(TiffError::MissingTag(tags::IMAGE_LENGTH))?;
|
||||
if width == 0 || height == 0 {
|
||||
return Err(invalid("empty image"));
|
||||
}
|
||||
if u64::from(width) * u64::from(height) > max_pixels {
|
||||
return Err(TiffError::Limit("pixel count"));
|
||||
}
|
||||
if ifd.u64(tags::SAMPLES_PER_PIXEL).unwrap_or(1) != 3 {
|
||||
return Err(unsupported("other than three samples per pixel"));
|
||||
}
|
||||
if ifd.u64s(tags::BITS_PER_SAMPLE).unwrap_or_else(|| vec![1]).iter().any(|&b| b != 8) {
|
||||
return Err(unsupported("other than 8 bits per sample"));
|
||||
}
|
||||
if ifd.u64(tags::PLANAR_CONFIGURATION).unwrap_or(1) != 1 {
|
||||
return Err(unsupported("separate planes"));
|
||||
}
|
||||
if ifd.contains(tags::TILE_OFFSETS) {
|
||||
return Err(unsupported("tiles"));
|
||||
}
|
||||
let packed = match ifd.u16(tags::COMPRESSION).unwrap_or(tags::compression::NONE) {
|
||||
tags::compression::NONE => false,
|
||||
tags::compression::PACKBITS => true,
|
||||
c => return Err(unsupported(&format!("compression {c}"))),
|
||||
};
|
||||
let sub = subsampling(ifd).ok_or_else(|| invalid("invalid YCbCrSubSampling"))?;
|
||||
let need = units_len(width, height, sub).ok_or(TiffError::Limit("pixel count"))?;
|
||||
// Uncompressed units are the file's bytes; PackBits expands at most 64 to 1 (two bytes, 128 out).
|
||||
if need > data.len().saturating_mul(if packed { 64 } else { 1 }) {
|
||||
return Err(TiffError::Truncated(data.len() as u64));
|
||||
}
|
||||
let offsets = ifd.u64s(tags::STRIP_OFFSETS).ok_or(TiffError::MissingTag(tags::STRIP_OFFSETS))?;
|
||||
let counts = ifd.u64s(tags::STRIP_BYTE_COUNTS).ok_or(TiffError::MissingTag(tags::STRIP_BYTE_COUNTS))?;
|
||||
// Fallible: `need` can be hundreds of MiB for a small PackBits file.
|
||||
let mut units: Vec<u8> = Vec::new();
|
||||
units.try_reserve_exact(need).map_err(|_| TiffError::Limit("memory"))?;
|
||||
let mut strip_bytes = 0usize;
|
||||
for (&off, &len) in offsets.iter().zip(&counts) {
|
||||
if units.len() >= need {
|
||||
break;
|
||||
}
|
||||
let strip = usize::try_from(off)
|
||||
.ok()
|
||||
.zip(usize::try_from(len).ok())
|
||||
.and_then(|(o, l)| data.get(o..o.checked_add(l)?))
|
||||
.ok_or(TiffError::Truncated(off))?;
|
||||
// The strips of one image do not overlap, so together they cannot exceed the file.
|
||||
strip_bytes = strip_bytes.saturating_add(strip.len());
|
||||
if strip_bytes > data.len() {
|
||||
return Err(invalid("strips overlap"));
|
||||
}
|
||||
if packed {
|
||||
packbits::decode(strip, &mut units, need);
|
||||
} else {
|
||||
units.extend_from_slice(strip.get(..need - units.len()).unwrap_or(strip));
|
||||
}
|
||||
}
|
||||
if units.len() < need {
|
||||
return Err(TiffError::Truncated(data.len() as u64));
|
||||
}
|
||||
let mut rgb = expand_units(&units, width, height, sub).ok_or_else(|| invalid("data units"))?;
|
||||
YCbCr::from_ifd(ifd, 255).convert_u8(&mut rgb, 3);
|
||||
Ok(rgb)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod roundtrip;
|
||||
mod ycbcr;
|
||||
@@ -0,0 +1,209 @@
|
||||
//! YCbCr: PhotoCraft's conversion tests and FilmCraft's decoding cases, on files this crate writes.
|
||||
|
||||
use craft_tiff::ycbcr::{self, BT601, REFERENCE_BLACK_WHITE, YCBCR_COEFFICIENTS, YCbCr};
|
||||
use craft_tiff::{ByteOrder, IfdBuilder, ImageData, Tiff, TiffError, TiffWriter, Value, tags};
|
||||
|
||||
fn convert(c: &YCbCr, px: [u8; 3]) -> [u8; 3] {
|
||||
let mut d = px.to_vec();
|
||||
c.convert_u8(&mut d, 3);
|
||||
[d[0], d[1], d[2]]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jfif_conversion_matches_known_colours() {
|
||||
let c = YCbCr::defaults(255);
|
||||
assert_eq!(convert(&c, [0, 128, 128]), [0, 0, 0]);
|
||||
assert_eq!(convert(&c, [255, 128, 128]), [255, 255, 255]);
|
||||
assert_eq!(convert(&c, [128, 128, 128]), [128, 128, 128]);
|
||||
// Pure red, green and blue as JFIF encodes them.
|
||||
assert_eq!(convert(&c, [76, 85, 255]), [254, 0, 0]);
|
||||
assert_eq!(convert(&c, [150, 44, 21]), [0, 255, 1]);
|
||||
assert_eq!(convert(&c, [29, 255, 107]), [0, 0, 254]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sixteen_bit_and_extra_samples() {
|
||||
let c = YCbCr::defaults(65535);
|
||||
let mut d = [65535u16, 32768, 32768, 1234];
|
||||
c.convert_u16(&mut d, 4);
|
||||
assert_eq!(d, [65535, 65535, 65535, 1234]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn studio_range_reference_black_white() {
|
||||
// Y 16–235, chroma 128 ± 112 (ITU-R BT.601 studio range).
|
||||
let c = YCbCr { luma: BT601, reference_black_white: [16.0, 235.0, 128.0, 240.0, 128.0, 240.0] };
|
||||
assert_eq!(convert(&c, [16, 128, 128]), [0, 0, 0]);
|
||||
assert_eq!(convert(&c, [235, 128, 128]), [255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weights_that_do_not_sum_to_one_keep_grays_neutral() {
|
||||
let c = YCbCr { luma: [0.4, 0.4, 0.4], reference_black_white: YCbCr::defaults(255).reference_black_white };
|
||||
for y in [0u8, 64, 128, 200, 255] {
|
||||
assert_eq!(convert(&c, [y, 128, 128]), [y, y, y]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_buffers_do_not_panic() {
|
||||
let c = YCbCr::defaults(255);
|
||||
let mut d = vec![1u8, 2];
|
||||
c.convert_u8(&mut d, 3);
|
||||
c.convert_u8(&mut d, 0);
|
||||
c.convert_u8(&mut d, 2);
|
||||
let mut w = vec![1u16];
|
||||
c.convert_u16(&mut w, 0);
|
||||
assert_eq!((d, w), (vec![1, 2], vec![1]));
|
||||
}
|
||||
|
||||
fn ifd_with(entries: &[(u16, Value)]) -> craft_tiff::Ifd {
|
||||
// A TIFF directory cannot be empty: give it a size.
|
||||
let mut b = IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Long(vec![1]));
|
||||
for (t, v) in entries {
|
||||
b.set(*t, v.clone());
|
||||
}
|
||||
let bytes = TiffWriter::new(ByteOrder::Little, false).write(&[b]).unwrap();
|
||||
Tiff::parse(&bytes).unwrap().ifds.remove(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tags_are_read_and_bad_ones_fall_back() {
|
||||
let d = YCbCr::defaults(255);
|
||||
assert_eq!(YCbCr::from_ifd(&ifd_with(&[]), 255), d);
|
||||
let rbw = Value::Rational(vec![(16, 1), (235, 1), (128, 1), (240, 1), (128, 1), (240, 1)]);
|
||||
let c = YCbCr::from_ifd(
|
||||
&ifd_with(&[(YCBCR_COEFFICIENTS, Value::Rational(vec![(2126, 10000), (7152, 10000), (722, 10000)])), (REFERENCE_BLACK_WHITE, rbw.clone())]),
|
||||
255,
|
||||
);
|
||||
assert_eq!((c.luma, c.reference_black_white), ([0.2126, 0.7152, 0.0722], [16.0, 235.0, 128.0, 240.0, 128.0, 240.0]));
|
||||
// JPEG-compressed data ignores both tags.
|
||||
assert_eq!(YCbCr::from_ifd(&ifd_with(&[(tags::COMPRESSION, Value::Short(vec![7])), (REFERENCE_BLACK_WHITE, rbw)]), 255), d);
|
||||
// A weight of 0 or 1, too few values, or a black equal to its white: that tag's default.
|
||||
let bad = ifd_with(&[
|
||||
(YCBCR_COEFFICIENTS, Value::Rational(vec![(0, 1), (1, 2), (1, 2)])),
|
||||
(REFERENCE_BLACK_WHITE, Value::Rational(vec![(16, 1), (16, 1), (128, 1), (240, 1), (128, 1), (240, 1)])),
|
||||
]);
|
||||
assert_eq!(YCbCr::from_ifd(&bad, 255), d);
|
||||
assert_eq!(YCbCr::from_ifd(&ifd_with(&[(YCBCR_COEFFICIENTS, Value::Rational(vec![(1, 2)]))]), 255).luma, BT601);
|
||||
}
|
||||
|
||||
/// A YCbCr TIFF of `w` × `h` at `sub` with these data units, in `strips` strips, uncompressed or
|
||||
/// PackBits (one literal packet per 128 bytes).
|
||||
fn file(w: u32, h: u32, sub: (u16, u16), units: &[u8], packbits: bool, extra: &[(u16, Value)]) -> Vec<u8> {
|
||||
let data = if packbits {
|
||||
units.chunks(128).flat_map(|c| std::iter::once((c.len() - 1) as u8).chain(c.iter().copied())).collect()
|
||||
} else {
|
||||
units.to_vec()
|
||||
};
|
||||
let mut b = IfdBuilder::new();
|
||||
b.set(tags::IMAGE_WIDTH, Value::Long(vec![w]))
|
||||
.set(tags::IMAGE_LENGTH, Value::Long(vec![h]))
|
||||
.set(tags::BITS_PER_SAMPLE, Value::Short(vec![8, 8, 8]))
|
||||
.set(tags::COMPRESSION, Value::Short(vec![if packbits { 32773 } else { 1 }]))
|
||||
.set(tags::PHOTOMETRIC, Value::Short(vec![6]))
|
||||
.set(tags::SAMPLES_PER_PIXEL, Value::Short(vec![3]))
|
||||
.set(tags::YCBCR_SUBSAMPLING, Value::Short(vec![sub.0, sub.1]))
|
||||
.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![data] });
|
||||
for (t, v) in extra {
|
||||
b.set(*t, v.clone());
|
||||
}
|
||||
TiffWriter::new(ByteOrder::Big, false).write(&[b]).unwrap()
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> craft_tiff::Result<Vec<u8>> {
|
||||
ycbcr::decode_rgb8(bytes, &Tiff::parse(bytes).unwrap().ifds[0], 1 << 28)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_units_expand_with_their_chroma() {
|
||||
// 3 × 2 at 2 × 1: two units a row, the second half outside the image.
|
||||
let units = [10, 20, 128, 128, 30, 40, 128, 128, 50, 60, 128, 128, 70, 80, 128, 128];
|
||||
let px = ycbcr::expand_units(&units, 3, 2, (2, 1)).unwrap();
|
||||
assert_eq!(px, [10, 128, 128, 20, 128, 128, 30, 128, 128, 50, 128, 128, 60, 128, 128, 70, 128, 128]);
|
||||
assert_eq!(ycbcr::units_len(3, 2, (2, 1)), Some(16));
|
||||
assert_eq!(ycbcr::units_len(5, 5, (4, 2)), Some(2 * 3 * 10));
|
||||
assert_eq!(ycbcr::expand_units(&units[..15], 3, 2, (2, 1)), None);
|
||||
// 2 × 2 at 2 × 2: four luma samples (row by row), one chroma pair for all of them.
|
||||
assert_eq!(ycbcr::expand_units(&[1, 2, 3, 4, 9, 8], 2, 2, (2, 2)).unwrap(), [1, 9, 8, 2, 9, 8, 3, 9, 8, 4, 9, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subsampled_files_decode_uncompressed_and_packbits() {
|
||||
// 4 × 2 at 2 × 2: grey, white, black and red units (red as JFIF codes it, one shared chroma).
|
||||
let units = [128, 128, 128, 128, 128, 128, 255, 255, 255, 255, 128, 128];
|
||||
for packbits in [false, true] {
|
||||
let rgb = decode(&file(4, 2, (2, 2), &units, packbits, &[])).unwrap();
|
||||
assert_eq!(rgb, [128, 128, 128, 128, 128, 128, 255, 255, 255, 255, 255, 255].repeat(2), "packbits {packbits}");
|
||||
}
|
||||
let red = decode(&file(1, 1, (1, 1), &[76, 85, 255], false, &[])).unwrap();
|
||||
assert_eq!(red, [254, 0, 0]);
|
||||
// The file's ReferenceBlackWhite applies.
|
||||
let studio = Value::Rational(vec![(16, 1), (235, 1), (128, 1), (240, 1), (128, 1), (240, 1)]);
|
||||
assert_eq!(decode(&file(1, 1, (1, 1), &[235, 128, 128], false, &[(REFERENCE_BLACK_WHITE, studio)])).unwrap(), [255, 255, 255]);
|
||||
// No subsampling tag means 2 × 2.
|
||||
assert_eq!(ycbcr::subsampling(&ifd_with(&[])), Some((2, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_it_does_not_decode_is_an_error() {
|
||||
let units = [128u8; 6];
|
||||
let ok = file(2, 2, (2, 2), &units, false, &[]);
|
||||
assert!(decode(&ok).is_ok());
|
||||
let with = |extra: &[(u16, Value)]| decode(&file(2, 2, (2, 2), &units, false, extra));
|
||||
for (tag, v) in [
|
||||
(tags::PHOTOMETRIC, Value::Short(vec![2])),
|
||||
(tags::SAMPLES_PER_PIXEL, Value::Short(vec![4])),
|
||||
(tags::BITS_PER_SAMPLE, Value::Short(vec![16, 16, 16])),
|
||||
(tags::PLANAR_CONFIGURATION, Value::Short(vec![2])),
|
||||
(tags::COMPRESSION, Value::Short(vec![5])),
|
||||
(tags::YCBCR_SUBSAMPLING, Value::Short(vec![3, 1])),
|
||||
(tags::TILE_OFFSETS, Value::Long(vec![8])),
|
||||
] {
|
||||
assert!(matches!(with(&[(tag, v.clone())]), Err(TiffError::Invalid(_))), "{tag} {v:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_and_hostile_files() {
|
||||
let units = [128u8; 6];
|
||||
let ok = file(2, 2, (2, 2), &units, false, &[]);
|
||||
let ifd = Tiff::parse(&ok).unwrap().ifds.remove(0);
|
||||
assert!(ycbcr::decode_rgb8(&ok, &ifd, 4).is_ok());
|
||||
assert_eq!(ycbcr::decode_rgb8(&ok, &ifd, 3), Err(TiffError::Limit("pixel count")));
|
||||
// A small file claiming a large image is refused before the data units are allocated.
|
||||
for packbits in [false, true] {
|
||||
let big = file(16384, 16384, (1, 1), &units, packbits, &[]);
|
||||
assert!(matches!(decode(&big), Err(TiffError::Truncated(_))), "packbits {packbits}");
|
||||
}
|
||||
// Data shorter than the image, and a strip outside the file.
|
||||
assert!(matches!(decode(&file(4, 4, (2, 2), &units, false, &[])), Err(TiffError::Truncated(_))));
|
||||
let mut moved = Tiff::parse(&ok).unwrap().ifds.remove(0);
|
||||
for e in &mut moved.entries {
|
||||
if e.tag == tags::STRIP_OFFSETS {
|
||||
e.value = Value::Long(vec![u32::MAX]);
|
||||
}
|
||||
}
|
||||
assert_eq!(ycbcr::decode_rgb8(&ok, &moved, 1 << 20), Err(TiffError::Truncated(u64::from(u32::MAX))));
|
||||
let mut far = ok.clone();
|
||||
// Every truncation and byte change either decodes or errors.
|
||||
for n in 0..ok.len() {
|
||||
if let Ok(t) = Tiff::parse(&ok[..n])
|
||||
&& let Some(ifd) = t.ifds.first()
|
||||
{
|
||||
let _ = ycbcr::decode_rgb8(&ok[..n], ifd, 1 << 20);
|
||||
}
|
||||
}
|
||||
for i in 0..ok.len() {
|
||||
for v in [0u8, 1, 0x80, 0xFF] {
|
||||
far.clone_from(&ok);
|
||||
far[i] = v;
|
||||
if let Ok(t) = Tiff::parse(&far)
|
||||
&& let Some(ifd) = t.ifds.first()
|
||||
&& let Ok(rgb) = ycbcr::decode_rgb8(&far, ifd, 1 << 20)
|
||||
{
|
||||
assert_eq!(rgb.len() as u64, u64::from(ifd.u32(tags::IMAGE_WIDTH).unwrap()) * u64::from(ifd.u32(tags::IMAGE_LENGTH).unwrap()) * 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user