mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
craft-hdr: Radiance HDR (RGBE) picture reader (#925)
FilmCraft's clean-room Radiance HDR reader as a crate: flat, old and new run-length scan lines, all eight axis orders, to linear float RGB. Scan lines are validated before the float picture is allocated.
This commit is contained in:
1 parent
64a8c7f74c
commit
49f5f5e83d
6 files changed
+408
No files matched your search
Generated
+7
@@ -1060,6 +1060,13 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-hdr"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-heif"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -44,6 +44,7 @@ craft-font-manager = { path = "crates/font-manager" }
|
||||
craft-font-store = { path = "crates/font-store" }
|
||||
craft-gainmap = { path = "crates/gainmap" }
|
||||
craft-gcode = { path = "crates/gcode" }
|
||||
craft-hdr = { path = "crates/hdr" }
|
||||
craft-mesh = { path = "crates/mesh" }
|
||||
craft-heif = { path = "crates/heif" }
|
||||
craft-fig = { path = "crates/fig" }
|
||||
|
||||
@@ -109,6 +109,9 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
|
||||
millimetres to `G0`/`G1` moves with laser power and feed, for cutting lasers and plotters, as a
|
||||
commented file or as bare lines for streaming. Its `grbl` module has the real-time command bytes
|
||||
and the status and acknowledgement parsers.
|
||||
- [`craft-hdr`](crates/hdr/src/lib.rs): Radiance HDR (RGBE) picture reader, from FilmCraft: flat,
|
||||
old and new run-length scan lines and all eight axis orders, to linear float RGB. Scan lines are
|
||||
validated before the picture is allocated.
|
||||
- [`craft-heif`](crates/heif/README.md): optional HEIF/HEIC still-image decoder, from LightCraft:
|
||||
heic-rs reads the container and HEVC pictures, and this crate converts to 16-bit RGB to match
|
||||
libheif (VUI range and matrix, chroma upsampling, `imir`, ICC on grid tiles). Also orientation,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "craft-hdr"
|
||||
description = "Clean-room Radiance HDR (RGBE) picture reader: flat, old and new run-length scan lines, all eight axis orders, to linear float RGB."
|
||||
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,239 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
//! Radiance HDR (`.hdr`, `.pic`) pictures, read from Greg Ward's public RGBE format description
|
||||
//! ("Real Pixels", Graphics Gems II): a text header (`#?RADIANCE` or `#?RGBE`,
|
||||
//! `FORMAT=32-bit_rle_rgbe`), a blank line, a resolution line (`-Y h +X w` and the seven other
|
||||
//! axis orders), then one RGBE pixel (shared exponent) per sample, flat, in the old run-length
|
||||
//! form or in the "new" per-channel run-length form (scan lines of 8 to 32767 pixels).
|
||||
//!
|
||||
//! [`decode`] returns scene-linear float RGB, top row first, so values above 1.0 survive. The
|
||||
//! `EXPOSURE` and `PRIMARIES` header lines are not applied, which is what other readers do too;
|
||||
//! the XYZE colour format is refused.
|
||||
//!
|
||||
//! Input is treated as hostile: the header length and the picture size are capped by [`Limits`],
|
||||
//! and every scan line is validated before the float picture is allocated, so a short file cannot
|
||||
//! ask for a large allocation.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Why a file could not be read.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The bytes do not start with `#?RADIANCE` or `#?RGBE`.
|
||||
#[error("not a Radiance HDR picture")]
|
||||
NotHdr,
|
||||
/// The header or the pixel data is damaged or cut short.
|
||||
#[error("Radiance HDR: {0}")]
|
||||
Malformed(&'static str),
|
||||
/// A valid picture this reader does not cover (the XYZE colour format, an unknown `FORMAT`).
|
||||
#[error("unsupported Radiance HDR: {0}")]
|
||||
Unsupported(String),
|
||||
/// The file declares more than [`Limits`] allow.
|
||||
#[error("Radiance HDR exceeds a limit: {0}")]
|
||||
LimitExceeded(&'static str),
|
||||
}
|
||||
|
||||
/// Result of this crate's functions.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Caps on what a file 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; each takes 12 bytes decoded. Default 2^26.
|
||||
pub max_pixels: u64,
|
||||
/// Longest header read, in bytes (real ones are a few hundred). Default 64 KiB.
|
||||
pub max_header: usize,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self { max_pixels: 1 << 26, max_header: 64 << 10 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded picture.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Picture {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Scene-linear RGB, three floats a pixel, top row first.
|
||||
pub rgb: Vec<f32>,
|
||||
}
|
||||
|
||||
/// Whether `bytes` starts like a Radiance picture.
|
||||
pub fn sniff(bytes: &[u8]) -> bool {
|
||||
bytes.starts_with(b"#?RADIANCE") || bytes.starts_with(b"#?RGBE")
|
||||
}
|
||||
|
||||
/// One resolution-line axis: `(is_x, positive)`.
|
||||
fn axis(tok: &str) -> Option<(bool, bool)> {
|
||||
match tok {
|
||||
"+X" => Some((true, true)),
|
||||
"-X" => Some((true, false)),
|
||||
"+Y" => Some((false, true)),
|
||||
"-Y" => Some((false, false)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// RGBE to linear floats: `mantissa × 2^(e − 136)`, zero for exponent 0. (Radiance's own reader
|
||||
/// adds half a mantissa step; other common readers do not, which also makes 1.0 =
|
||||
/// (128, 128, 128, 129) read back as exactly 1.0.)
|
||||
pub fn rgbe_to_linear(p: [u8; 4]) -> [f32; 3] {
|
||||
if p[3] == 0 {
|
||||
return [0.0; 3];
|
||||
}
|
||||
let scale = 2f32.powi(i32::from(p[3]) - 136);
|
||||
[f32::from(p[0]) * scale, f32::from(p[1]) * scale, f32::from(p[2]) * scale]
|
||||
}
|
||||
|
||||
/// Reads the pixels of one scan line into `line`.
|
||||
fn scanline(b: &[u8], pos: &mut usize, line: &mut [[u8; 4]]) -> Result<()> {
|
||||
let n = line.len();
|
||||
let take = |pos: &mut usize, k: usize| -> Result<&[u8]> {
|
||||
let s = pos.checked_add(k).and_then(|end| b.get(*pos..end)).ok_or(Error::Malformed("truncated pixel data"))?;
|
||||
*pos += k;
|
||||
Ok(s)
|
||||
};
|
||||
let head = pos.checked_add(4).and_then(|end| b.get(*pos..end));
|
||||
if (8..=32767).contains(&n)
|
||||
&& let Some(&[2, 2, hi, lo]) = head
|
||||
&& hi & 0x80 == 0
|
||||
{
|
||||
if usize::from(hi) << 8 | usize::from(lo) != n {
|
||||
return Err(Error::Malformed("scan line length does not match the picture"));
|
||||
}
|
||||
*pos += 4;
|
||||
for c in 0..4 {
|
||||
let mut x = 0;
|
||||
while x < n {
|
||||
let code = take(pos, 1)?.first().copied().unwrap_or(0);
|
||||
if code > 128 {
|
||||
let run = usize::from(code - 128);
|
||||
let v = take(pos, 1)?.first().copied().unwrap_or(0);
|
||||
let dst = line.get_mut(x..x + run).ok_or(Error::Malformed("run past the end of the scan line"))?;
|
||||
for p in dst {
|
||||
p[c] = v;
|
||||
}
|
||||
x += run;
|
||||
} else {
|
||||
let run = usize::from(code);
|
||||
if run == 0 {
|
||||
return Err(Error::Malformed("empty literal run"));
|
||||
}
|
||||
let src = take(pos, run)?;
|
||||
let dst = line.get_mut(x..x + run).ok_or(Error::Malformed("literal past the end of the scan line"))?;
|
||||
for (p, &v) in dst.iter_mut().zip(src) {
|
||||
p[c] = v;
|
||||
}
|
||||
x += run;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// Flat pixels, with the old run-length form: (1, 1, 1, k) repeats the previous pixel k << shift times.
|
||||
let (mut x, mut shift) = (0usize, 0u32);
|
||||
while x < n {
|
||||
let p: [u8; 4] = take(pos, 4)?.try_into().map_err(|_| Error::Malformed("truncated pixel data"))?;
|
||||
if p[..3] == [1, 1, 1] {
|
||||
let count = usize::from(p[3]).checked_shl(shift).unwrap_or(usize::MAX);
|
||||
let prev = if x > 0 { line.get(x - 1).copied() } else { None }.ok_or(Error::Malformed("run with no previous pixel"))?;
|
||||
let dst = line.get_mut(x..x.saturating_add(count).min(n)).ok_or(Error::Malformed("run past the end of the scan line"))?;
|
||||
dst.fill(prev);
|
||||
x += dst.len();
|
||||
shift = shift.saturating_add(8).min(24);
|
||||
} else {
|
||||
if let Some(d) = line.get_mut(x) {
|
||||
*d = p;
|
||||
}
|
||||
x += 1;
|
||||
shift = 0;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decodes a Radiance picture to linear float RGB, top row first.
|
||||
pub fn decode(b: &[u8], limits: &Limits) -> Result<Picture> {
|
||||
if !sniff(b) {
|
||||
return Err(Error::NotHdr);
|
||||
}
|
||||
// Header: lines up to the first empty one, then the resolution line.
|
||||
let mut pos = 0;
|
||||
let line = |pos: &mut usize| -> Result<String> {
|
||||
let rest = b.get(*pos..).ok_or(Error::Malformed("truncated header"))?;
|
||||
let end = rest.iter().take(limits.max_header).position(|&c| c == b'\n').ok_or(Error::Malformed("truncated header"))?;
|
||||
let s = String::from_utf8_lossy(rest.get(..end).unwrap_or_default()).trim_end_matches('\r').to_string();
|
||||
*pos += end + 1;
|
||||
Ok(s)
|
||||
};
|
||||
loop {
|
||||
let l = line(&mut pos)?;
|
||||
if l.is_empty() {
|
||||
break;
|
||||
}
|
||||
if let Some(f) = l.strip_prefix("FORMAT=") {
|
||||
match f.trim() {
|
||||
"32-bit_rle_rgbe" => {}
|
||||
"32-bit_rle_xyze" => return Err(Error::Unsupported("the XYZE colour format".into())),
|
||||
other => return Err(Error::Unsupported(format!("format {other}"))),
|
||||
}
|
||||
}
|
||||
if pos > limits.max_header {
|
||||
return Err(Error::LimitExceeded("header length"));
|
||||
}
|
||||
}
|
||||
let res = line(&mut pos)?;
|
||||
let bad = Error::Malformed("bad resolution line");
|
||||
let t: Vec<&str> = res.split_whitespace().collect();
|
||||
let &[a1, n1, a2, n2] = t.as_slice().first_chunk::<4>().ok_or(bad.clone())?;
|
||||
let ((x1, p1), (x2, p2)) = (axis(a1).ok_or(bad.clone())?, axis(a2).ok_or(bad.clone())?);
|
||||
if x1 == x2 {
|
||||
return Err(Error::Malformed("resolution line names one axis twice"));
|
||||
}
|
||||
let n1: usize = n1.parse().map_err(|_| bad.clone())?;
|
||||
let n2: usize = n2.parse().map_err(|_| bad.clone())?;
|
||||
let (w, h) = if x1 { (n1, n2) } else { (n2, n1) };
|
||||
if w == 0 || h == 0 {
|
||||
return Err(Error::Malformed("empty picture"));
|
||||
}
|
||||
let (Ok(width), Ok(height)) = (u32::try_from(w), u32::try_from(h)) else { return Err(Error::LimitExceeded("pixel count")) };
|
||||
let pixels = u64::from(width) * u64::from(height);
|
||||
if pixels > limits.max_pixels {
|
||||
return Err(Error::LimitExceeded("pixel count"));
|
||||
}
|
||||
// A scan line takes at least a few bits of file, so a header cannot ask for more than the data has.
|
||||
if (b.len().saturating_sub(pos) as u64).saturating_mul(4096) < pixels {
|
||||
return Err(Error::Malformed("truncated pixel data"));
|
||||
}
|
||||
// Validate every encoded scan line before allocating the float picture: a short file could
|
||||
// otherwise ask for the whole limit of zeroed pixels and only then fail on its first line.
|
||||
let pixel_data = pos;
|
||||
let mut scan = vec![[0u8; 4]; n2];
|
||||
for _ in 0..n1 {
|
||||
scanline(b, &mut pos, &mut scan)?;
|
||||
}
|
||||
|
||||
let total = w.checked_mul(h).and_then(|n| n.checked_mul(3)).ok_or(Error::LimitExceeded("pixel count"))?;
|
||||
let mut rgb = vec![0f32; total];
|
||||
pos = pixel_data;
|
||||
for i in 0..n1 {
|
||||
scanline(b, &mut pos, &mut scan)?;
|
||||
for (j, &p) in scan.iter().enumerate() {
|
||||
// The first axis counts scan lines, the second runs along one.
|
||||
let (x, y) = if x1 {
|
||||
(if p1 { i } else { w - 1 - i }, if p2 { h - 1 - j } else { j })
|
||||
} else {
|
||||
(if p2 { j } else { w - 1 - j }, if p1 { h - 1 - i } else { i })
|
||||
};
|
||||
let at = (y * w + x) * 3;
|
||||
if let Some(o) = rgb.get_mut(at..at + 3) {
|
||||
o.copy_from_slice(&rgbe_to_linear(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Picture { width, height, rgb })
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Ported from FilmCraft's Radiance HDR reader tests.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use super::*;
|
||||
|
||||
fn file(res: &str, body: &[u8]) -> Vec<u8> {
|
||||
let mut v = format!("#?RADIANCE\n# test\nFORMAT=32-bit_rle_rgbe\nEXPOSURE=1.0\n\n{res}\n").into_bytes();
|
||||
v.extend_from_slice(body);
|
||||
v
|
||||
}
|
||||
|
||||
fn dec(b: &[u8]) -> Result<Picture> {
|
||||
decode(b, &Limits::default())
|
||||
}
|
||||
|
||||
/// New-style RLE of one scan line, as Radiance's own writer does: per channel, runs of 3+ equal bytes.
|
||||
fn rle_line(line: &[[u8; 4]]) -> Vec<u8> {
|
||||
let n = line.len();
|
||||
let mut out = vec![2, 2, (n >> 8) as u8, n as u8];
|
||||
for c in 0..4 {
|
||||
let ch: Vec<u8> = line.iter().map(|p| p[c]).collect();
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
let run = ch[i..].iter().take(127).take_while(|&&b| b == ch[i]).count();
|
||||
if run >= 3 {
|
||||
out.extend([128 + run as u8, ch[i]]);
|
||||
i += run;
|
||||
} else {
|
||||
let mut j = i + 1;
|
||||
while j < n && j - i < 128 && !(j + 2 < n && ch[j] == ch[j + 1] && ch[j] == ch[j + 2]) {
|
||||
j += 1;
|
||||
}
|
||||
out.push((j - i) as u8);
|
||||
out.extend_from_slice(&ch[i..j]);
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgbe_values() {
|
||||
assert_eq!(rgbe_to_linear([0, 0, 0, 0]), [0.0; 3]);
|
||||
assert_eq!(rgbe_to_linear([128, 128, 128, 129]), [1.0; 3]);
|
||||
assert_eq!(rgbe_to_linear([128, 64, 0, 136]), [128.0, 64.0, 0.0]);
|
||||
assert_eq!(rgbe_to_linear([128, 128, 128, 128])[0], 0.5);
|
||||
assert!(rgbe_to_linear([255, 255, 255, 255])[0].is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_top_to_bottom() {
|
||||
let body = [[10u8, 20, 30, 136], [1, 2, 3, 136], [4, 5, 6, 137], [7, 8, 9, 135]].concat();
|
||||
let f = dec(&file("-Y 2 +X 2", &body)).unwrap();
|
||||
assert_eq!((f.width, f.height), (2, 2));
|
||||
// Exponent 137 scales by 2, 135 by 1/2.
|
||||
assert_eq!(f.rgb, [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 8.0, 10.0, 12.0, 3.5, 4.0, 4.5]);
|
||||
// The `#?RGBE` signature and CRLF header lines read the same.
|
||||
let mut crlf = b"#?RGBE\r\nFORMAT=32-bit_rle_rgbe\r\n\r\n-Y 2 +X 2\r\n".to_vec();
|
||||
crlf.extend_from_slice(&body);
|
||||
assert_eq!(dec(&crlf).unwrap(), f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orientations() {
|
||||
// 3 wide, 2 high, pixel value = its index in the file.
|
||||
let body: Vec<u8> = (0..6u8).flat_map(|i| [i, 0, 0, 136]).collect();
|
||||
let red = |res: &str| -> Vec<f32> { dec(&file(res, &body)).unwrap().rgb.chunks(3).map(|p| p[0]).collect() };
|
||||
assert_eq!(red("-Y 2 +X 3"), [0., 1., 2., 3., 4., 5.]);
|
||||
assert_eq!(red("+Y 2 +X 3"), [3., 4., 5., 0., 1., 2.]);
|
||||
assert_eq!(red("-Y 2 -X 3"), [2., 1., 0., 5., 4., 3.]);
|
||||
// Scan lines are columns: file index = x * 2 + (row, top first for -Y).
|
||||
assert_eq!(red("+X 3 -Y 2"), [0., 2., 4., 1., 3., 5.]);
|
||||
assert_eq!(red("-X 3 +Y 2"), [5., 3., 1., 4., 2., 0.]);
|
||||
let f = dec(&file("+X 3 -Y 2", &body)).unwrap();
|
||||
assert_eq!((f.width, f.height), (3, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_length_lines() {
|
||||
let w = 40usize;
|
||||
let rows: Vec<Vec<[u8; 4]>> =
|
||||
(0..3).map(|y| (0..w).map(|x| if x < 20 { [100, 50, 25, 130] } else { [(x * 5 + y) as u8, 7, 9, 131] }).collect()).collect();
|
||||
let body: Vec<u8> = rows.iter().flat_map(|r| rle_line(r)).collect();
|
||||
let f = dec(&file(&format!("-Y 3 +X {w}"), &body)).unwrap();
|
||||
for (y, r) in rows.iter().enumerate() {
|
||||
for (x, q) in r.iter().enumerate() {
|
||||
assert_eq!(&f.rgb[(y * w + x) * 3..(y * w + x) * 3 + 3], &rgbe_to_linear(*q), "pixel {x},{y}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_style_runs() {
|
||||
// One pixel, then (1,1,1,3) repeats it 3 times, then a new pixel.
|
||||
let body = [[5u8, 6, 7, 136], [1, 1, 1, 3], [9, 9, 9, 136]].concat();
|
||||
let f = dec(&file("-Y 1 +X 5", &body)).unwrap();
|
||||
assert_eq!(f.rgb.chunks(3).map(|p| p[0]).collect::<Vec<_>>(), [5.0, 5.0, 5.0, 5.0, 9.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_input_is_an_error_not_a_panic() {
|
||||
let ok = file("-Y 2 +X 2", &[0; 16]);
|
||||
assert!(dec(&ok).is_ok());
|
||||
for cut in 0..ok.len() - 1 {
|
||||
// A cut anywhere is an error (the header or the pixel data is incomplete), never a panic.
|
||||
assert!(dec(&ok[..cut]).is_err(), "{cut}");
|
||||
}
|
||||
assert_eq!(dec(b"P6 1 1 255 abc"), Err(Error::NotHdr));
|
||||
assert_eq!(dec(&file("-Y 2 +X 2", &[0; 8])), Err(Error::Malformed("truncated pixel data")));
|
||||
assert_eq!(dec(&file("-Y 0 +X 4", &[])), Err(Error::Malformed("empty picture")));
|
||||
assert_eq!(dec(&file("-Y 2 -Y 2", &[0; 16])), Err(Error::Malformed("resolution line names one axis twice")));
|
||||
assert_eq!(dec(&file("junk", &[0; 16])), Err(Error::Malformed("bad resolution line")));
|
||||
assert!(matches!(dec(b"#?RADIANCE\nFORMAT=32-bit_rle_xyze\n\n-Y 1 +X 1\n\0\0\0\0"), Err(Error::Unsupported(_))));
|
||||
assert!(matches!(dec(b"#?RADIANCE\nFORMAT=other\n\n-Y 1 +X 1\n\0\0\0\0"), Err(Error::Unsupported(_))));
|
||||
// An RLE line announcing the wrong width, a run past the end, a run at the start of a flat line.
|
||||
assert_eq!(dec(&file("-Y 1 +X 10", &[2, 2, 0, 11, 0])), Err(Error::Malformed("scan line length does not match the picture")));
|
||||
assert_eq!(dec(&file("-Y 1 +X 10", &[2, 2, 0, 10, 200, 1])), Err(Error::Malformed("run past the end of the scan line")));
|
||||
assert_eq!(dec(&file("-Y 1 +X 10", &[2, 2, 0, 10, 0])), Err(Error::Malformed("empty literal run")));
|
||||
assert_eq!(dec(&file("-Y 1 +X 3", &[1, 1, 1, 9, 0, 0, 0, 0])), Err(Error::Malformed("run with no previous pixel")));
|
||||
assert!(dec(b"#?RADIANCE").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_are_checked_before_allocating() {
|
||||
assert_eq!(dec(&file("-Y 99999999 +X 99999999", &[0; 16])), Err(Error::LimitExceeded("pixel count")));
|
||||
assert_eq!(dec(&file("-Y 99999999999 +X 1", &[0; 16])), Err(Error::LimitExceeded("pixel count")));
|
||||
// Within the pixel limit and past the coarse size check, but the scan lines are not there:
|
||||
// refused by validation, before the float picture is allocated.
|
||||
assert_eq!(dec(&file("-Y 67108864 +X 1", &[0; 16_384])), Err(Error::Malformed("truncated pixel data")));
|
||||
let ok = file("-Y 2 +X 2", &[0; 16]);
|
||||
assert!(decode(&ok, &Limits { max_pixels: 4, ..Limits::default() }).is_ok());
|
||||
assert_eq!(decode(&ok, &Limits { max_pixels: 3, ..Limits::default() }), Err(Error::LimitExceeded("pixel count")));
|
||||
// A header without its blank line inside the header limit.
|
||||
let mut long = b"#?RADIANCE\n".to_vec();
|
||||
for _ in 0..8 {
|
||||
long.extend_from_slice(b"# 0123456789\n");
|
||||
}
|
||||
long.extend_from_slice(b"\n-Y 1 +X 1\n\0\0\0\0");
|
||||
assert!(dec(&long).is_ok());
|
||||
assert_eq!(decode(&long, &Limits { max_header: 64, ..Limits::default() }), Err(Error::LimitExceeded("header length")));
|
||||
assert_eq!(decode(&long, &Limits { max_header: 8, ..Limits::default() }), Err(Error::Malformed("truncated header")));
|
||||
assert_eq!((Limits::default().max_pixels, Limits::default().max_header), (1 << 26, 64 << 10));
|
||||
}
|
||||
Reference in new issue
Block a user