mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
craft-tga: Targa reader and run-length writer (#892)
FilmCraft's clean-room Targa reader and PhotoCraft's run-length writer in one crate. The reader returns RGBA8 and names the layout the file stored; the writer's packets end with their scan line.
This commit is contained in:
1 parent
b2f247d3bf
commit
9435232e51
6 files changed
+637
No files matched your search
Generated
+7
@@ -1439,6 +1439,13 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-tga"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-tiff"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -80,6 +80,7 @@ craft-silhouette-studio = { path = "crates/silhouette-studio" }
|
||||
craft-svg = { path = "crates/svg" }
|
||||
craft-svg-paths = { path = "crates/svg-paths" }
|
||||
craft-text = { path = "crates/text" }
|
||||
craft-tga = { path = "crates/tga" }
|
||||
craft-timeline = { path = "crates/timeline" }
|
||||
craft-tiff = { path = "crates/tiff" }
|
||||
craft-ui = { path = "crates/ui" }
|
||||
|
||||
@@ -120,6 +120,10 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
|
||||
Hasselblad 3FR/FFF, Minolta MRW, Epson ERF, Mamiya MEF, Kodak DCS, Pentax PEF, Samsung SRW and Canon CRW (through
|
||||
`craft-crw`) so far; demosaicing and everything after stay with the develop
|
||||
pipeline.
|
||||
- [`craft-tga`](crates/tga/src/lib.rs): Truevision Targa, from FilmCraft (reader) and PhotoCraft
|
||||
(writer): true-colour, grayscale and colour-mapped pictures, raw or run-length encoded, any
|
||||
origin, to RGBA8, and a run-length writer whose packets end with their scan line. Bounded on
|
||||
hostile input.
|
||||
- [`craft-idml`](crates/idml/README.md): IDML (InDesign Markup Language) packages: the ZIP container read with bounded part sizes and lenient part names, `designmap.xml` with its includes in place, the `<Properties>` form on craft-xml elements, value codecs and the package writer; and InDesign Tagged Text, read and written.
|
||||
- [`craft-timeline`](crates/timeline/README.md): editorial timeline interchange: exact media time and SMPTE timecode, import/export reports, media path and `file://` URL handling, Avid Log Exchange (ALE) and CMX 3600 EDL documents, format detection. From FilmCraft.
|
||||
- [`craft-tiff`](crates/tiff/README.md): TIFF and BigTIFF IFD reader and writer, from LightCraft:
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "craft-tga"
|
||||
description = "Clean-room Truevision Targa (TGA) reader and run-length writer: true-colour, grayscale and colour-mapped pictures, any origin, packets that end with their scan line."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
@@ -0,0 +1,374 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
//! Truevision Targa (`.tga`), read and written from the Truevision TGA File Format Specification 2.0.
|
||||
//!
|
||||
//! Reading ([`decode`]): image types 1, 2 and 3 (colour-mapped, true-colour, grayscale) and their
|
||||
//! run-length encoded forms 9, 10 and 11; 15- and 16-bit (5-5-5, the top bit as alpha when the
|
||||
//! descriptor says one attribute bit), 24-bit BGR and 32-bit BGRA true-colour, 8-bit grayscale and
|
||||
//! 16-bit grayscale with alpha, colour maps of 15-, 16-, 24- or 32-bit entries indexed by 8 or 16
|
||||
//! bits; any origin corner. Run-length packets may cross scan lines (older writers do it). The
|
||||
//! result is straight-alpha RGBA8, top row first, with [`Picture::layout`] naming what the file
|
||||
//! stored.
|
||||
//!
|
||||
//! Writing ([`encode`]): 8-bit grayscale, grayscale with alpha, RGB or RGBA, run-length encoded
|
||||
//! (types 10 and 11), rows stored bottom-up (the bottom-left origin every reader assumes, also
|
||||
//! those that ignore the origin bits), alpha bits in the descriptor and the TGA 2.0 footer. A
|
||||
//! packet never crosses a scan line: TGA 2.0 forbids it, and readers that decode a row at a time
|
||||
//! misread files that do.
|
||||
//!
|
||||
//! Input is treated as hostile: the picture size is capped by [`Limits`], and a run-length file
|
||||
//! must be long enough to hold the pixels its header declares before anything is allocated.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Length of the fixed header.
|
||||
const HEADER: usize = 18;
|
||||
/// TGA 2.0 files end with this signature (after two 32-bit area offsets).
|
||||
const SIGNATURE: &[u8] = b"TRUEVISION-XFILE.\0";
|
||||
/// The TGA 2.0 footer written: no extension or developer area, then the signature.
|
||||
const FOOTER: &[u8; 26] = b"\0\0\0\0\0\0\0\0TRUEVISION-XFILE.\0";
|
||||
/// Most pixels one packet holds (a 7-bit count, plus one).
|
||||
const MAX_PACKET: usize = 128;
|
||||
|
||||
/// Why a file could not be read or written.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The header or the pixel data is damaged or cut short.
|
||||
#[error("Targa: {0}")]
|
||||
Malformed(&'static str),
|
||||
/// A Targa image type this reader does not cover.
|
||||
#[error("unsupported Targa: image type {0}")]
|
||||
Unsupported(u8),
|
||||
/// The file declares more than [`Limits`] allow.
|
||||
#[error("Targa exceeds a limit: {0}")]
|
||||
LimitExceeded(&'static str),
|
||||
/// The picture handed to the writer cannot be stored.
|
||||
#[error("Targa: cannot write: {0}")]
|
||||
Encode(String),
|
||||
}
|
||||
|
||||
/// Result of this crate's functions.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Caps on what a header may ask the reader to allocate, checked before anything is allocated.
|
||||
/// `#[non_exhaustive]`: start from [`Limits::default`] and set the fields you change.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct Limits {
|
||||
/// Most pixels (width × height) a picture may have. Default 2^28 (16384 × 16384).
|
||||
pub max_pixels: u64,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self { max_pixels: 1 << 28 }
|
||||
}
|
||||
}
|
||||
|
||||
/// What a picture's pixels are: what a file stored, or what the writer is given.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
/// One byte a pixel.
|
||||
Gray,
|
||||
/// Two bytes a pixel: gray, alpha.
|
||||
GrayAlpha,
|
||||
/// Three bytes a pixel.
|
||||
Rgb,
|
||||
/// Four bytes a pixel, straight alpha.
|
||||
Rgba,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
/// Bytes of one pixel.
|
||||
pub fn channels(self) -> usize {
|
||||
match self {
|
||||
Layout::Gray => 1,
|
||||
Layout::GrayAlpha => 2,
|
||||
Layout::Rgb => 3,
|
||||
Layout::Rgba => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded picture.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Picture {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Straight-alpha RGBA8, top row first, whatever the file stored.
|
||||
pub rgba: Vec<u8>,
|
||||
/// What the file stored (a colour-mapped file reports its map entries' layout).
|
||||
pub layout: Layout,
|
||||
/// Whether any pixel is not fully opaque.
|
||||
pub has_alpha: bool,
|
||||
}
|
||||
|
||||
/// Whether `bytes` looks like a Targa file: a plausible header and either a TGA 2.0 footer or,
|
||||
/// for the original format (which has no signature), a Targa file extension on `name`.
|
||||
pub fn sniff(name: &str, bytes: &[u8]) -> bool {
|
||||
let header_ok = || {
|
||||
let Some(h) = bytes.first_chunk::<HEADER>() else { return false };
|
||||
let (map_type, kind, depth) = (h[1], h[2], h[16]);
|
||||
let (w, ht) = (u16::from_le_bytes([h[12], h[13]]), u16::from_le_bytes([h[14], h[15]]));
|
||||
map_type <= 1 && matches!(kind, 1 | 2 | 3 | 9 | 10 | 11) && w > 0 && ht > 0 && matches!(depth, 8 | 15 | 16 | 24 | 32)
|
||||
};
|
||||
let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase());
|
||||
let named = matches!(ext.as_deref(), Some("tga" | "targa" | "icb" | "vda" | "vst"));
|
||||
header_ok() && (named || bytes.ends_with(SIGNATURE))
|
||||
}
|
||||
|
||||
/// Pixel layout of the stored values (after the colour-map lookup, for mapped images).
|
||||
#[derive(Clone, Copy)]
|
||||
enum Stored {
|
||||
/// 15/16-bit `ARRRRRGG GGGBBBBB`, little-endian; `alpha`: the top bit is an alpha bit.
|
||||
Rgb555 {
|
||||
alpha: bool,
|
||||
},
|
||||
Bgr24,
|
||||
Bgra32,
|
||||
Gray8,
|
||||
GrayAlpha16,
|
||||
}
|
||||
|
||||
impl Stored {
|
||||
fn true_colour(depth: u8, attr_bits: u8) -> Option<Self> {
|
||||
Some(match depth {
|
||||
15 => Stored::Rgb555 { alpha: false },
|
||||
16 => Stored::Rgb555 { alpha: attr_bits == 1 },
|
||||
24 => Stored::Bgr24,
|
||||
32 => Stored::Bgra32,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn bytes(self) -> usize {
|
||||
match self {
|
||||
Stored::Rgb555 { .. } | Stored::GrayAlpha16 => 2,
|
||||
Stored::Bgr24 => 3,
|
||||
Stored::Bgra32 => 4,
|
||||
Stored::Gray8 => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(self) -> Layout {
|
||||
match self {
|
||||
Stored::Rgb555 { alpha: false } | Stored::Bgr24 => Layout::Rgb,
|
||||
Stored::Rgb555 { alpha: true } | Stored::Bgra32 => Layout::Rgba,
|
||||
Stored::Gray8 => Layout::Gray,
|
||||
Stored::GrayAlpha16 => Layout::GrayAlpha,
|
||||
}
|
||||
}
|
||||
|
||||
/// RGBA of one stored value (`p` holds exactly [`bytes`](Self::bytes) bytes).
|
||||
fn rgba(self, p: &[u8]) -> [u8; 4] {
|
||||
let b = |i: usize| p.get(i).copied().unwrap_or(0);
|
||||
match self {
|
||||
Stored::Rgb555 { alpha } => {
|
||||
let v = u16::from_le_bytes([b(0), b(1)]);
|
||||
let five = |s: u16| {
|
||||
let c = ((v >> s) & 31) as u8;
|
||||
(c << 3) | (c >> 2)
|
||||
};
|
||||
let a = if !alpha || v & 0x8000 != 0 { 255 } else { 0 };
|
||||
[five(10), five(5), five(0), a]
|
||||
}
|
||||
Stored::Bgr24 => [b(2), b(1), b(0), 255],
|
||||
Stored::Bgra32 => [b(2), b(1), b(0), b(3)],
|
||||
Stored::Gray8 => [b(0), b(0), b(0), 255],
|
||||
Stored::GrayAlpha16 => [b(0), b(0), b(0), b(1)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a Targa file to straight-alpha RGBA8, top row first.
|
||||
pub fn decode(bytes: &[u8], limits: &Limits) -> Result<Picture> {
|
||||
let err = Error::Malformed;
|
||||
let h = bytes.first_chunk::<HEADER>().ok_or(err("truncated header"))?;
|
||||
let (id_len, map_type, kind) = (usize::from(h[0]), h[1], h[2]);
|
||||
let map_first = usize::from(u16::from_le_bytes([h[3], h[4]]));
|
||||
let map_len = usize::from(u16::from_le_bytes([h[5], h[6]]));
|
||||
let map_depth = h[7];
|
||||
let (w, ht) = (usize::from(u16::from_le_bytes([h[12], h[13]])), usize::from(u16::from_le_bytes([h[14], h[15]])));
|
||||
let (depth, descriptor) = (h[16], h[17]);
|
||||
let attr_bits = descriptor & 0x0F;
|
||||
let (right_to_left, top_to_bottom) = (descriptor & 0x10 != 0, descriptor & 0x20 != 0);
|
||||
if w == 0 || ht == 0 {
|
||||
return Err(err("empty picture"));
|
||||
}
|
||||
// Both are 16-bit, so the product fits.
|
||||
let pixels = w * ht;
|
||||
if pixels as u64 > limits.max_pixels {
|
||||
return Err(Error::LimitExceeded("pixel count"));
|
||||
}
|
||||
let rle = kind >= 9;
|
||||
// The stored value layout and, for colour-mapped images, the palette.
|
||||
let mut palette: Vec<[u8; 4]> = Vec::new();
|
||||
let map_entry = if map_type == 1 {
|
||||
Some(Stored::true_colour(map_depth, if map_depth == 16 { attr_bits.min(1) } else { 0 }).ok_or(err("colour map entry size"))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut at = HEADER + id_len;
|
||||
if let Some(l) = map_entry {
|
||||
let end = at + map_len * l.bytes();
|
||||
let map = bytes.get(at..end).ok_or(err("truncated colour map"))?;
|
||||
palette = map.chunks_exact(l.bytes()).map(|e| l.rgba(e)).collect();
|
||||
at = end;
|
||||
}
|
||||
let (stored_as, mapped) = match kind & 7 {
|
||||
1 => {
|
||||
if map_type != 1 || !matches!(depth, 8 | 16) {
|
||||
return Err(err("colour-mapped picture without an 8/16-bit index and a colour map"));
|
||||
}
|
||||
(if depth == 8 { Stored::Gray8 } else { Stored::GrayAlpha16 }, true)
|
||||
}
|
||||
2 => (Stored::true_colour(depth, attr_bits).ok_or(err("true-colour pixel depth"))?, false),
|
||||
3 => match depth {
|
||||
8 => (Stored::Gray8, false),
|
||||
16 => (Stored::GrayAlpha16, false),
|
||||
_ => return Err(err("grayscale pixel depth")),
|
||||
},
|
||||
_ => return Err(Error::Unsupported(kind)),
|
||||
};
|
||||
let data = bytes.get(at..).unwrap_or_default();
|
||||
let bpp = stored_as.bytes();
|
||||
// The stored values, in file order.
|
||||
let stored: Vec<u8> = if rle {
|
||||
unpack_rle(data, pixels, bpp)?
|
||||
} else {
|
||||
let n = pixels.checked_mul(bpp).ok_or(Error::LimitExceeded("image size"))?;
|
||||
data.get(..n).ok_or(err("truncated pixel data"))?.to_vec()
|
||||
};
|
||||
let mut rgba = vec![0u8; pixels.checked_mul(4).ok_or(Error::LimitExceeded("image size"))?];
|
||||
for (i, v) in stored.chunks_exact(bpp).enumerate().take(pixels) {
|
||||
let (x, y) = (i % w, i / w);
|
||||
let x = if right_to_left { w - 1 - x } else { x };
|
||||
let y = if top_to_bottom { y } else { ht - 1 - y };
|
||||
let px = if mapped {
|
||||
let byte = |i: usize| v.get(i).copied().unwrap_or(0);
|
||||
let index = if bpp == 1 { usize::from(byte(0)) } else { usize::from(u16::from_le_bytes([byte(0), byte(1)])) };
|
||||
*index.checked_sub(map_first).and_then(|k| palette.get(k)).ok_or(err("colour index outside the colour map"))?
|
||||
} else {
|
||||
stored_as.rgba(v)
|
||||
};
|
||||
let o = (y * w + x) * 4;
|
||||
if let Some(d) = rgba.get_mut(o..o + 4) {
|
||||
d.copy_from_slice(&px);
|
||||
}
|
||||
}
|
||||
// 32-bit files from writers that leave the attribute bits unset often store zero alpha
|
||||
// everywhere: such a picture is opaque, not invisible.
|
||||
let alpha_used = match (stored_as, mapped) {
|
||||
(Stored::Bgra32, false) => attr_bits != 0 || rgba.as_chunks::<4>().0.iter().any(|p| p[3] != 0),
|
||||
_ => true,
|
||||
};
|
||||
if !alpha_used {
|
||||
rgba.as_chunks_mut::<4>().0.iter_mut().for_each(|p| p[3] = 255);
|
||||
}
|
||||
let has_alpha = rgba.as_chunks::<4>().0.iter().any(|p| p[3] != 255);
|
||||
let layout = match (mapped, map_entry) {
|
||||
(true, Some(entry)) => entry.layout(),
|
||||
_ => stored_as.layout(),
|
||||
};
|
||||
Ok(Picture { width: w as u32, height: ht as u32, rgba, layout, has_alpha })
|
||||
}
|
||||
|
||||
/// Expands run-length packets into `pixels` stored values of `bpp` bytes each. Packets may span
|
||||
/// scan lines (the TGA 2.0 spec discourages it; older writers do it).
|
||||
fn unpack_rle(mut data: &[u8], pixels: usize, bpp: usize) -> Result<Vec<u8>> {
|
||||
let short = Error::Malformed("truncated run-length data");
|
||||
// A two-byte packet expands to at most 128 values: a header asking for more is lying.
|
||||
let bound = data.len().saturating_mul(64).saturating_add(128);
|
||||
if pixels > bound {
|
||||
return Err(short);
|
||||
}
|
||||
let total = pixels.checked_mul(bpp).ok_or(Error::LimitExceeded("image size"))?;
|
||||
let mut out = Vec::with_capacity(total);
|
||||
while out.len() < total {
|
||||
let (&head, rest) = data.split_first().ok_or(short.clone())?;
|
||||
let count = usize::from(head & 0x7F) + 1;
|
||||
let want = (total - out.len()).min(count * bpp);
|
||||
if head & 0x80 != 0 {
|
||||
let v = rest.get(..bpp).ok_or(short.clone())?;
|
||||
for _ in 0..want / bpp {
|
||||
out.extend_from_slice(v);
|
||||
}
|
||||
data = rest.get(bpp..).unwrap_or_default();
|
||||
} else {
|
||||
let n = count * bpp;
|
||||
let v = rest.get(..n).ok_or(short.clone())?;
|
||||
out.extend_from_slice(v.get(..want).unwrap_or(v));
|
||||
data = rest.get(n..).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Encodes 8-bit pixels (`height` rows of `width × layout.channels()` bytes, top row first) as a
|
||||
/// run-length encoded Targa.
|
||||
pub fn encode(width: u32, height: u32, layout: Layout, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let (bpp, image_type, alpha_bits) = match layout {
|
||||
Layout::Gray => (1usize, 11u8, 0u8),
|
||||
Layout::GrayAlpha => (2, 11, 8),
|
||||
Layout::Rgb => (3, 10, 0),
|
||||
Layout::Rgba => (4, 10, 8),
|
||||
};
|
||||
let (Ok(w16), Ok(h16)) = (u16::try_from(width), u16::try_from(height)) else {
|
||||
return Err(Error::Encode(format!("dimensions exceed 65535x65535: {width}x{height}")));
|
||||
};
|
||||
let row = usize::from(w16) * bpp;
|
||||
if data.len() != row * usize::from(h16) {
|
||||
return Err(Error::Encode("buffer size mismatch".into()));
|
||||
}
|
||||
let mut out = Vec::with_capacity(HEADER + data.len() + data.len() / MAX_PACKET + FOOTER.len());
|
||||
out.extend_from_slice(&[0, 0, image_type, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
out.extend_from_slice(&w16.to_le_bytes());
|
||||
out.extend_from_slice(&h16.to_le_bytes());
|
||||
// Bits per pixel; the descriptor holds the alpha bits, origin bits 0 (bottom-left).
|
||||
out.extend_from_slice(&[(bpp * 8) as u8, alpha_bits]);
|
||||
let mut px = vec![0u8; row];
|
||||
// `row` is 0 only for an empty picture, which has no rows to write.
|
||||
for line in data.chunks_exact(row.max(1)).rev() {
|
||||
px.copy_from_slice(line);
|
||||
// Targa stores colour as BGR.
|
||||
if bpp >= 3 {
|
||||
for p in px.chunks_exact_mut(bpp) {
|
||||
p.swap(0, 2);
|
||||
}
|
||||
}
|
||||
rle_row(&px, bpp, &mut out);
|
||||
}
|
||||
out.extend_from_slice(FOOTER);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// One scan line as run-length packets that end with it: a run of two or more equal pixels is a
|
||||
/// run packet, anything else goes out in raw packets.
|
||||
fn rle_row(row: &[u8], bpp: usize, out: &mut Vec<u8>) {
|
||||
let px: Vec<&[u8]> = row.chunks_exact(bpp).collect();
|
||||
let n = px.len();
|
||||
let at = |i: usize| px.get(i).copied().unwrap_or_default();
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
let mut run = 1;
|
||||
while i + run < n && run < MAX_PACKET && at(i + run) == at(i) {
|
||||
run += 1;
|
||||
}
|
||||
if run >= 2 {
|
||||
out.push(0x80 | (run - 1) as u8);
|
||||
out.extend_from_slice(at(i));
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
// Raw until two equal pixels start a run, the packet is full or the row ends. The first
|
||||
// pixel always goes in: it differs from the one after it, or it is the row's last.
|
||||
let start = i;
|
||||
while i < n && i - start < MAX_PACKET && !(i > start && i + 1 < n && at(i) == at(i + 1)) {
|
||||
i += 1;
|
||||
}
|
||||
out.push((i - start - 1) as u8);
|
||||
for p in px.get(start..i).unwrap_or_default() {
|
||||
out.extend_from_slice(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//! The reader's tests come from FilmCraft's decoder, the writer's from PhotoCraft's encoder.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use super::*;
|
||||
|
||||
fn header(kind: u8, w: u16, h: u16, depth: u8, descriptor: u8) -> Vec<u8> {
|
||||
let mut b = vec![0, 0, kind, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
b.extend_from_slice(&w.to_le_bytes());
|
||||
b.extend_from_slice(&h.to_le_bytes());
|
||||
b.extend_from_slice(&[depth, descriptor]);
|
||||
b
|
||||
}
|
||||
|
||||
fn dec(bytes: &[u8]) -> Result<Picture> {
|
||||
decode(bytes, &Limits::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_up_bgr_is_flipped() {
|
||||
let mut f = header(2, 2, 2, 24, 0);
|
||||
// Bottom row first: blue, green; then the top row: red, white.
|
||||
f.extend_from_slice(&[255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]);
|
||||
let r = dec(&f).unwrap();
|
||||
assert_eq!((r.width, r.height, r.has_alpha, r.layout), (2, 2, false, Layout::Rgb));
|
||||
assert_eq!(r.rgba, [255, 0, 0, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 255, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_origin_corner() {
|
||||
// 2 × 2 gray, stored values 1 2 / 3 4 in file order.
|
||||
for (descriptor, want) in [(0x00, [3, 4, 1, 2]), (0x10, [4, 3, 2, 1]), (0x20, [1, 2, 3, 4]), (0x30, [2, 1, 4, 3])] {
|
||||
let mut f = header(3, 2, 2, 8, descriptor);
|
||||
f.extend_from_slice(&[1, 2, 3, 4]);
|
||||
let r = dec(&f).unwrap();
|
||||
assert_eq!(r.rgba.chunks(4).map(|p| p[0]).collect::<Vec<_>>(), want, "descriptor {descriptor:#x}");
|
||||
assert_eq!(r.layout, Layout::Gray);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rle_runs_and_raw_packets_cross_lines() {
|
||||
let mut f = header(10, 3, 2, 32, 0x28);
|
||||
// A run of 4 translucent red, then 2 raw pixels.
|
||||
f.extend_from_slice(&[0x83, 0, 0, 255, 128, 0x01, 1, 2, 3, 255, 4, 5, 6, 255]);
|
||||
let r = dec(&f).unwrap();
|
||||
assert!(r.has_alpha);
|
||||
assert_eq!(r.layout, Layout::Rgba);
|
||||
assert_eq!(&r.rgba[..16], &[255, 0, 0, 128].repeat(4)[..]);
|
||||
assert_eq!(&r.rgba[16..], &[3, 2, 1, 255, 6, 5, 4, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_map_and_16_bit_alpha() {
|
||||
let mut f = header(1, 2, 1, 8, 0x20);
|
||||
f[1] = 1; // colour map present
|
||||
f[3..5].copy_from_slice(&1u16.to_le_bytes()); // first entry index 1
|
||||
f[5..7].copy_from_slice(&2u16.to_le_bytes());
|
||||
f[7] = 24;
|
||||
f.extend_from_slice(&[10, 20, 30, 40, 50, 60, 2, 1]);
|
||||
let r = dec(&f).unwrap();
|
||||
assert_eq!(r.rgba, [60, 50, 40, 255, 30, 20, 10, 255]);
|
||||
assert_eq!(r.layout, Layout::Rgb);
|
||||
f[18 + 6] = 0; // index 0 is below the map's first entry
|
||||
assert_eq!(dec(&f), Err(Error::Malformed("colour index outside the colour map")));
|
||||
|
||||
let mut f = header(2, 2, 1, 16, 0x21);
|
||||
f.extend_from_slice(&0xFC00u16.to_le_bytes()); // opaque full red
|
||||
f.extend_from_slice(&0x03E0u16.to_le_bytes()); // transparent full green
|
||||
let r = dec(&f).unwrap();
|
||||
assert_eq!(r.rgba, [255, 0, 0, 255, 0, 255, 0, 0]);
|
||||
|
||||
// 16-bit grayscale with alpha.
|
||||
let mut f = header(3, 1, 1, 16, 0x28);
|
||||
f.extend_from_slice(&[7, 9]);
|
||||
let r = dec(&f).unwrap();
|
||||
assert_eq!((r.rgba.as_slice(), r.layout, r.has_alpha), (&[7, 7, 7, 9][..], Layout::GrayAlpha, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_alpha_32_bit_without_attribute_bits_is_opaque() {
|
||||
let mut f = header(2, 1, 1, 32, 0);
|
||||
f.extend_from_slice(&[1, 2, 3, 0]);
|
||||
let r = dec(&f).unwrap();
|
||||
assert_eq!((r.rgba.as_slice(), r.has_alpha), (&[3, 2, 1, 255][..], false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_headers_fail_without_allocating() {
|
||||
// 65535 × 65535 run-length encoded from a few bytes, truncated raw data, bad types.
|
||||
let mut f = header(10, 65535, 65535, 32, 0);
|
||||
f.extend_from_slice(&[0xFF, 1, 2, 3, 4]);
|
||||
assert_eq!(dec(&f), Err(Error::LimitExceeded("pixel count")));
|
||||
let mut f = header(10, 16384, 16384, 32, 0);
|
||||
f.extend_from_slice(&[0xFF, 1, 2, 3, 4]);
|
||||
assert_eq!(dec(&f), Err(Error::Malformed("truncated run-length data")));
|
||||
let mut f = header(2, 4, 4, 24, 0);
|
||||
f.extend_from_slice(&[0; 10]);
|
||||
assert_eq!(dec(&f), Err(Error::Malformed("truncated pixel data")));
|
||||
assert!(dec(&header(2, 1, 1, 7, 0)).is_err());
|
||||
assert!(dec(&header(1, 1, 1, 8, 0)).is_err());
|
||||
assert_eq!(dec(&header(2, 0, 1, 24, 0)), Err(Error::Malformed("empty picture")));
|
||||
assert_eq!(dec(&header(4, 1, 1, 24, 0)), Err(Error::Unsupported(4)));
|
||||
assert_eq!(dec(&[0; 5]), Err(Error::Malformed("truncated header")));
|
||||
for n in 0..40 {
|
||||
let mut f = header(9, 3, 3, 8, 0);
|
||||
f[1] = 1;
|
||||
f[5] = 2;
|
||||
f[7] = 16;
|
||||
f.extend_from_slice(&[0x82, 1, 0, 1, 0x01, 0, 0]);
|
||||
f.truncate(n);
|
||||
let _ = dec(&f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pixel_limit_is_checked() {
|
||||
let mut f = header(3, 2, 2, 8, 0);
|
||||
f.extend_from_slice(&[0; 4]);
|
||||
assert!(decode(&f, &Limits { max_pixels: 4 }).is_ok());
|
||||
assert_eq!(decode(&f, &Limits { max_pixels: 3 }), Err(Error::LimitExceeded("pixel count")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sniffs_by_footer_or_name() {
|
||||
let mut f = header(2, 1, 1, 24, 0);
|
||||
f.extend_from_slice(&[0, 0, 0]);
|
||||
assert!(sniff("x.TGA", &f));
|
||||
assert!(!sniff("x.bin", &f));
|
||||
f.extend_from_slice(&[0; 8]);
|
||||
f.extend_from_slice(SIGNATURE);
|
||||
assert!(sniff("x.bin", &f));
|
||||
assert!(!sniff("x.tga", b"\x89PNG\r\n\x1a\n0000000000"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_packets_at_the_128_pixel_boundary() {
|
||||
for bpp in 1..=4 {
|
||||
for (pixels, headers) in [(1, vec![0]), (2, vec![0x81]), (127, vec![0xfe]), (128, vec![0xff]), (129, vec![0xff, 0])] {
|
||||
let pixel: Vec<u8> = (1..=bpp as u8).collect();
|
||||
let mut encoded = Vec::new();
|
||||
rle_row(&pixel.repeat(pixels), bpp, &mut encoded);
|
||||
let expected: Vec<u8> = headers.into_iter().flat_map(|header| std::iter::once(header).chain(pixel.iter().copied())).collect();
|
||||
assert_eq!(encoded, expected, "{pixels} pixels, {bpp} bytes per pixel");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_packets_at_the_128_pixel_boundary() {
|
||||
for bpp in 1..=4 {
|
||||
for pixels in [127usize, 128, 129] {
|
||||
let row: Vec<u8> = (0..pixels).flat_map(|x| vec![x as u8; bpp]).collect();
|
||||
let mut encoded = Vec::new();
|
||||
rle_row(&row, bpp, &mut encoded);
|
||||
let mut expected = vec![(pixels.min(128) - 1) as u8];
|
||||
expected.extend_from_slice(&row[..pixels.min(128) * bpp]);
|
||||
if pixels > 128 {
|
||||
expected.push(0);
|
||||
expected.extend_from_slice(&row[128 * bpp..]);
|
||||
}
|
||||
assert_eq!(encoded, expected, "{pixels} pixels, {bpp} bytes per pixel");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_packets_stop_before_a_run_of_complete_pixels() {
|
||||
// Equal first channels alone do not make an RGBA run; alpha participates too.
|
||||
let a = [10, 20, 30, 40];
|
||||
let b = [10, 20, 30, 41];
|
||||
let c = [50, 60, 70, 80];
|
||||
let row = [a, b, c, c, a].concat();
|
||||
let mut encoded = Vec::new();
|
||||
rle_row(&row, 4, &mut encoded);
|
||||
assert_eq!(encoded, [vec![1], a.to_vec(), b.to_vec(), vec![0x81], c.to_vec(), vec![0], a.to_vec()].concat());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_fit_the_unsigned_16_bit_header() {
|
||||
for (width, height) in [(65535u32, 1u32), (1, 65535)] {
|
||||
let file = encode(width, height, Layout::Gray, &vec![0; 65535]).unwrap();
|
||||
assert_eq!(&file[12..14], &(width as u16).to_le_bytes());
|
||||
assert_eq!(&file[14..16], &(height as u16).to_le_bytes());
|
||||
}
|
||||
for (width, height) in [(65536u32, 1u32), (1, 65536)] {
|
||||
assert!(matches!(encode(width, height, Layout::Gray, &vec![0; 65536]), Err(Error::Encode(_))), "{width}x{height}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_buffer_sizes_are_refused() {
|
||||
for layout in [Layout::Gray, Layout::GrayAlpha, Layout::Rgb, Layout::Rgba] {
|
||||
let len = 2 * 2 * layout.channels();
|
||||
for bytes in [len - 1, len + 1] {
|
||||
assert!(matches!(encode(2, 2, layout, &vec![0; bytes]), Err(Error::Encode(_))), "{layout:?}: {bytes} bytes");
|
||||
}
|
||||
}
|
||||
assert!(encode(0, 0, Layout::Rgb, &[]).is_ok());
|
||||
}
|
||||
|
||||
/// What the writer produces, the reader returns: every layout, with rows that hold runs, raw
|
||||
/// stretches and packets longer than 128 pixels, and no packet crossing a row.
|
||||
#[test]
|
||||
fn written_files_read_back() {
|
||||
for layout in [Layout::Gray, Layout::GrayAlpha, Layout::Rgb, Layout::Rgba] {
|
||||
let n = layout.channels();
|
||||
let (w, h) = (300usize, 3usize);
|
||||
let data: Vec<u8> =
|
||||
(0..w * h).flat_map(|i| (0..n).map(move |c| if i % w < 140 { 9 + c as u8 } else { (i * 7 + c * 31) as u8 | 1 })).collect();
|
||||
let file = encode(w as u32, h as u32, layout, &data).unwrap();
|
||||
assert!(file.ends_with(SIGNATURE) && sniff("x.bin", &file));
|
||||
// Walk the packets: each row's packets add up to exactly the row.
|
||||
let mut at = HEADER;
|
||||
for _ in 0..h {
|
||||
let mut px = 0;
|
||||
while px < w {
|
||||
let count = usize::from(file[at] & 0x7F) + 1;
|
||||
at += 1 + if file[at] & 0x80 != 0 { n } else { count * n };
|
||||
px += count;
|
||||
}
|
||||
assert_eq!(px, w, "a packet crossed a row");
|
||||
}
|
||||
assert_eq!(at, file.len() - FOOTER.len());
|
||||
let r = dec(&file).unwrap();
|
||||
assert_eq!((r.width, r.height, r.layout), (w as u32, h as u32, layout));
|
||||
let want: Vec<u8> = data
|
||||
.chunks(n)
|
||||
.flat_map(|p| match layout {
|
||||
Layout::Gray => [p[0], p[0], p[0], 255],
|
||||
Layout::GrayAlpha => [p[0], p[0], p[0], p[1]],
|
||||
Layout::Rgb => [p[0], p[1], p[2], 255],
|
||||
Layout::Rgba => [p[0], p[1], p[2], p[3]],
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(r.rgba, want, "{layout:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user