render: bounded cooperative HDR input foundation (#837)
A cancellable, bounded Radiance HDR decoder for render lighting input; reference images still accept PNG and JPEG only.
This commit is contained in:
1 parent
8319c35a32
commit
e99b3bd4e8
11 files changed
+697
-2
No files matched your search
@@ -7,6 +7,7 @@
|
||||
mod backend;
|
||||
mod camera;
|
||||
mod curve_display;
|
||||
pub mod lighting;
|
||||
mod raster;
|
||||
|
||||
pub use backend::{
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
//! Incremental RGBE scanline adapter for image 0.25.10's Radiance codec.
|
||||
//! Row/RLE handling is adapted from its `read_scanline`, `decode_component` and
|
||||
//! `decode_old_rle` (MIT OR Apache-2.0; see ATTRIBUTION.md and NOTICE). Metadata still
|
||||
//! uses the library. Changes add cooperative steps, checked runs, width validation
|
||||
//! and bounded radiance; no third-party asserts or input indexing are retained.
|
||||
|
||||
use super::{HdrError, HdrErrorCode as ErrorCode, Result, check_cancelled};
|
||||
use serde::Serialize;
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
|
||||
pub const MAX_HDR_BYTES: usize = 16 * 1024 * 1024;
|
||||
pub const MAX_HDR_WIDTH: u32 = 2048;
|
||||
pub const MAX_HDR_HEIGHT: u32 = 1024;
|
||||
pub const MAX_HDR_PIXELS: u64 = 2 * 1024 * 1024;
|
||||
const MAX_HEADER_BYTES: usize = 64 * 1024;
|
||||
const MAX_RADIANCE: f64 = 1e8;
|
||||
|
||||
fn invalid(message: impl Into<String>) -> HdrError {
|
||||
HdrError::new(ErrorCode::InvalidInput, message)
|
||||
}
|
||||
fn limit(message: impl Into<String>) -> HdrError {
|
||||
HdrError::new(ErrorCode::LimitExceeded, message)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct HdrInfo {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub encoded_bytes: u64,
|
||||
pub decoded_rgb_bytes: u64,
|
||||
/// Stored RGBE values are divided by these declared header corrections.
|
||||
pub exposure: f64,
|
||||
pub color_correction: [f64; 3],
|
||||
/// Source chromaticities (Rx Ry Gx Gy Bx By Wx Wy); absent means unspecified.
|
||||
pub primaries: Option<[f64; 8]>,
|
||||
#[serde(skip)]
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
/// Immutable input. Construction only inspects a bounded header; `validate` also checks
|
||||
/// every encoded row and corrected radiance before an import commits it to a document.
|
||||
#[derive(Clone)]
|
||||
pub struct HdrSource {
|
||||
bytes: Arc<Vec<u8>>,
|
||||
info: HdrInfo,
|
||||
}
|
||||
|
||||
impl HdrSource {
|
||||
pub fn new(bytes: Vec<u8>) -> Result<Self> {
|
||||
if bytes.is_empty() || bytes.len() > MAX_HDR_BYTES || bytes.capacity() > MAX_HDR_BYTES {
|
||||
return Err(limit("HDR input must contain at most 16 MiB"));
|
||||
}
|
||||
let mut cursor = Cursor::new(bytes.get(..bytes.len().min(MAX_HEADER_BYTES)).ok_or_else(|| invalid("invalid HDR header range"))?);
|
||||
let metadata = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
image::codecs::hdr::HdrDecoder::new(&mut cursor).map(|decoder| decoder.metadata())
|
||||
}))
|
||||
.map_err(|_| invalid("HDR metadata decoder failed"))?
|
||||
.map_err(|e| invalid(format!("invalid or oversized Radiance header: {e}")))?;
|
||||
let pixels = u64::from(metadata.width).checked_mul(u64::from(metadata.height)).ok_or_else(|| limit("HDR dimensions overflow"))?;
|
||||
if metadata.width == 0
|
||||
|| metadata.height == 0
|
||||
|| metadata.width > MAX_HDR_WIDTH
|
||||
|| metadata.height > MAX_HDR_HEIGHT
|
||||
|| pixels > MAX_HDR_PIXELS
|
||||
{
|
||||
return Err(limit("HDR preview input exceeds 2048x1024 / 2 million pixels"));
|
||||
}
|
||||
if metadata.width != metadata.height.saturating_mul(2) || metadata.orientation != ((1, 0), (0, 1)) {
|
||||
return Err(invalid("HDR preview requires a 2:1 equirectangular image with standard -Y +X orientation"));
|
||||
}
|
||||
let exposure = f64::from(metadata.exposure.unwrap_or(1.0));
|
||||
let (r, g, b) = metadata.color_correction.unwrap_or((1.0, 1.0, 1.0));
|
||||
let correction = [f64::from(r), f64::from(g), f64::from(b)];
|
||||
if [exposure].into_iter().chain(correction).any(|x| !x.is_finite() || x <= 0.0)
|
||||
|| metadata.pixel_aspect_ratio.is_some_and(|x| !x.is_finite() || (x - 1.0).abs() > 1e-6)
|
||||
{
|
||||
return Err(invalid("HDR exposure, colour correction and square-pixel metadata must be finite and positive"));
|
||||
}
|
||||
let offset = usize::try_from(cursor.position()).map_err(|_| invalid("HDR data offset overflow"))?;
|
||||
if offset.checked_add(4).is_none_or(|end| end > bytes.len()) {
|
||||
return Err(invalid("HDR input has no pixel data"));
|
||||
}
|
||||
let mut primaries = None;
|
||||
for (key, value) in &metadata.custom_attributes {
|
||||
if key == "PRIMARIES" {
|
||||
if primaries.is_some() {
|
||||
return Err(invalid("duplicate HDR primaries"));
|
||||
}
|
||||
let mut parsed = [0.0; 8];
|
||||
let mut values = value.split_whitespace();
|
||||
for out in &mut parsed {
|
||||
*out = values
|
||||
.next()
|
||||
.ok_or_else(|| invalid("missing HDR chromaticity"))?
|
||||
.parse::<f64>()
|
||||
.map_err(|_| invalid("invalid HDR chromaticity"))?;
|
||||
if !out.is_finite() || !(0.0..=1.0).contains(out) {
|
||||
return Err(invalid("invalid HDR chromaticity"));
|
||||
}
|
||||
}
|
||||
if values.next().is_some() {
|
||||
return Err(invalid("extra HDR chromaticity"));
|
||||
}
|
||||
primaries = Some(parsed);
|
||||
}
|
||||
}
|
||||
let info = HdrInfo {
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
encoded_bytes: bytes.len() as u64,
|
||||
decoded_rgb_bytes: pixels.checked_mul(12).ok_or_else(|| limit("HDR decoded size overflow"))?,
|
||||
exposure,
|
||||
color_correction: correction,
|
||||
primaries,
|
||||
offset,
|
||||
};
|
||||
Ok(Self { bytes: Arc::new(bytes), info })
|
||||
}
|
||||
pub fn info(&self) -> &HdrInfo {
|
||||
&self.info
|
||||
}
|
||||
pub fn encoded_bytes(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
}
|
||||
pub fn validate(&self, cancelled: impl Fn() -> bool) -> Result<()> {
|
||||
check_cancelled(&cancelled)?;
|
||||
let mut rows = Rows::new(self.clone())?;
|
||||
let mut steps = 0u64;
|
||||
while !rows.done() {
|
||||
check_cancelled(&cancelled)?;
|
||||
if rows.ready() {
|
||||
for x in 0..self.info.width as usize {
|
||||
check_cancelled(&cancelled)?;
|
||||
rows.linear(x)?;
|
||||
}
|
||||
rows.next_row()?;
|
||||
} else {
|
||||
rows.step()?;
|
||||
}
|
||||
steps = steps.checked_add(1).ok_or_else(|| limit("HDR decode work overflow"))?;
|
||||
if steps > MAX_HDR_PIXELS * 6 + 4096 {
|
||||
return Err(limit("HDR decode work budget exceeded"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "hdr_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
enum Stage {
|
||||
Start,
|
||||
Channel { channel: usize, x: usize },
|
||||
Old { x: usize, previous: [u8; 4], shift: u32, repeat: usize },
|
||||
Ready,
|
||||
Done,
|
||||
}
|
||||
|
||||
pub(super) struct Rows {
|
||||
source: HdrSource,
|
||||
offset: usize,
|
||||
row: u32,
|
||||
pixels: Vec<[u8; 4]>,
|
||||
scales: [f64; 256],
|
||||
stage: Stage,
|
||||
}
|
||||
|
||||
impl Rows {
|
||||
pub(super) fn new(source: HdrSource) -> Result<Self> {
|
||||
let mut pixels = Vec::new();
|
||||
pixels
|
||||
.try_reserve_exact(source.info.width as usize)
|
||||
.map_err(|_| HdrError::new(ErrorCode::AllocationFailed, "could not reserve HDR scanline"))?;
|
||||
pixels.resize(source.info.width as usize, [0; 4]);
|
||||
Ok(Self {
|
||||
offset: source.info.offset,
|
||||
source,
|
||||
row: 0,
|
||||
pixels,
|
||||
scales: std::array::from_fn(|e| 2f64.powi(e as i32 - 136)),
|
||||
stage: Stage::Start,
|
||||
})
|
||||
}
|
||||
pub(super) fn info(&self) -> &HdrInfo {
|
||||
&self.source.info
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn row(&self) -> u32 {
|
||||
self.row
|
||||
}
|
||||
pub(super) fn ready(&self) -> bool {
|
||||
matches!(self.stage, Stage::Ready)
|
||||
}
|
||||
pub(super) fn done(&self) -> bool {
|
||||
matches!(self.stage, Stage::Done)
|
||||
}
|
||||
fn byte(&mut self) -> Result<u8> {
|
||||
let b = self.source.bytes.get(self.offset).copied().ok_or_else(|| invalid("truncated HDR scanline"))?;
|
||||
self.offset = self.offset.checked_add(1).ok_or_else(|| invalid("HDR offset overflow"))?;
|
||||
Ok(b)
|
||||
}
|
||||
fn pixel(&mut self) -> Result<[u8; 4]> {
|
||||
Ok([self.byte()?, self.byte()?, self.byte()?, self.byte()?])
|
||||
}
|
||||
fn put(&mut self, x: usize, pixel: [u8; 4]) -> Result<()> {
|
||||
*self.pixels.get_mut(x).ok_or_else(|| invalid("HDR run exceeds its row"))? = pixel;
|
||||
Ok(())
|
||||
}
|
||||
/// One packet (at most 128 component writes), or one old-style repeated pixel.
|
||||
pub(super) fn step(&mut self) -> Result<()> {
|
||||
let width = self.pixels.len();
|
||||
let stage = std::mem::replace(&mut self.stage, Stage::Done);
|
||||
self.stage = match stage {
|
||||
Stage::Start => {
|
||||
let first = self.pixel()?;
|
||||
if first.get(..2) == Some(&[2, 2]) && first.get(2).is_some_and(|v| *v < 128) {
|
||||
let declared = usize::from(*first.get(2).ok_or_else(|| invalid("missing HDR width"))?) * 256
|
||||
+ usize::from(*first.get(3).ok_or_else(|| invalid("missing HDR width"))?);
|
||||
if declared != width {
|
||||
return Err(invalid("HDR scanline width differs from its header"));
|
||||
}
|
||||
Stage::Channel { channel: 0, x: 0 }
|
||||
} else {
|
||||
if first.get(..3) == Some(&[1, 1, 1]) {
|
||||
return Err(invalid("HDR row starts with an old run marker"));
|
||||
}
|
||||
self.put(0, first)?;
|
||||
if width == 1 { Stage::Ready } else { Stage::Old { x: 1, previous: first, shift: 0, repeat: 0 } }
|
||||
}
|
||||
}
|
||||
Stage::Channel { channel, x } => {
|
||||
let code = self.byte()?;
|
||||
if code == 0 {
|
||||
return Err(invalid("zero-length HDR RLE packet"));
|
||||
}
|
||||
let run = code > 128;
|
||||
let count = usize::from(if run { code - 128 } else { code });
|
||||
let end = x.checked_add(count).filter(|end| *end <= width).ok_or_else(|| invalid("HDR RLE packet exceeds its row"))?;
|
||||
let repeated = if run { self.byte()? } else { 0 };
|
||||
for at in x..end {
|
||||
let value = if run { repeated } else { self.byte()? };
|
||||
*self.pixels.get_mut(at).and_then(|p| p.get_mut(channel)).ok_or_else(|| invalid("invalid HDR component address"))? = value;
|
||||
}
|
||||
if end == width {
|
||||
if channel == 3 { Stage::Ready } else { Stage::Channel { channel: channel + 1, x: 0 } }
|
||||
} else {
|
||||
Stage::Channel { channel, x: end }
|
||||
}
|
||||
}
|
||||
Stage::Old { x, previous, shift, repeat } => {
|
||||
if repeat > 0 {
|
||||
self.put(x, previous)?;
|
||||
if x + 1 == width { Stage::Ready } else { Stage::Old { x: x + 1, previous, shift, repeat: repeat - 1 } }
|
||||
} else {
|
||||
let pixel = self.pixel()?;
|
||||
if pixel.get(..3) == Some(&[1, 1, 1]) {
|
||||
let count = u64::from(*pixel.get(3).ok_or_else(|| invalid("missing HDR repeat count"))?)
|
||||
.checked_shl(shift)
|
||||
.filter(|n| *n <= (width - x) as u64)
|
||||
.ok_or_else(|| invalid("old HDR run exceeds its row"))?;
|
||||
let next_shift = shift.checked_add(8).filter(|s| *s <= 32).ok_or_else(|| invalid("old HDR run multiplier overflow"))?;
|
||||
Stage::Old { x, previous, shift: next_shift, repeat: count as usize }
|
||||
} else {
|
||||
self.put(x, pixel)?;
|
||||
if x + 1 == width { Stage::Ready } else { Stage::Old { x: x + 1, previous: pixel, shift: 0, repeat: 0 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
Stage::Ready => return Err(invalid("HDR row must be consumed before decoding another")),
|
||||
Stage::Done => return Err(invalid("HDR input already consumed")),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
pub(super) fn linear(&self, x: usize) -> Result<[f32; 3]> {
|
||||
if !self.ready() {
|
||||
return Err(invalid("HDR row is not ready"));
|
||||
}
|
||||
let pixel = self.pixels.get(x).ok_or_else(|| invalid("invalid HDR pixel address"))?;
|
||||
let exponent = *pixel.get(3).ok_or_else(|| invalid("missing RGBE exponent"))?;
|
||||
if exponent == 0 {
|
||||
return Ok([0.0; 3]);
|
||||
}
|
||||
let scale = self.scales.get(usize::from(exponent)).copied().ok_or_else(|| invalid("invalid RGBE exponent"))? / self.source.info.exposure;
|
||||
let mut rgb = [0.0; 3];
|
||||
for (channel, out) in rgb.iter_mut().enumerate() {
|
||||
let value = f64::from(*pixel.get(channel).ok_or_else(|| invalid("invalid RGBE channel"))?) * scale
|
||||
/ self.source.info.color_correction.get(channel).copied().ok_or_else(|| invalid("invalid HDR correction"))?;
|
||||
if !value.is_finite() || !(0.0..=MAX_RADIANCE).contains(&value) {
|
||||
return Err(limit("corrected HDR radiance exceeds 100 million"));
|
||||
}
|
||||
*out = value as f32;
|
||||
}
|
||||
Ok(rgb)
|
||||
}
|
||||
pub(super) fn next_row(&mut self) -> Result<()> {
|
||||
if !self.ready() {
|
||||
return Err(invalid("HDR row is not ready"));
|
||||
}
|
||||
self.row = self.row.checked_add(1).ok_or_else(|| invalid("HDR row overflow"))?;
|
||||
self.stage = if self.row == self.source.info.height { Stage::Done } else { Stage::Start };
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
use super::*;
|
||||
use crate::lighting::{HdrDecodeTask, HdrTaskPoll};
|
||||
|
||||
#[test]
|
||||
fn cooperative_task_matches_codec_and_is_consumed_once() {
|
||||
for bytes in [uncompressed(""), rle(), literal_rle(), old_rle()] {
|
||||
let expected = image::load_from_memory_with_format(&bytes, image::ImageFormat::Hdr).unwrap().into_rgb32f();
|
||||
let mut task = HdrDecodeTask::new(HdrSource::new(bytes).unwrap()).unwrap();
|
||||
let mut calls = 0;
|
||||
loop {
|
||||
calls += 1;
|
||||
assert!(calls < 1000);
|
||||
match task.step(1, || false).unwrap() {
|
||||
HdrTaskPoll::Pending(progress) => assert!(progress.decoded_pixels < progress.total_pixels),
|
||||
HdrTaskPoll::Complete(result) => {
|
||||
assert_eq!(result.pixels().len(), expected.pixels().len());
|
||||
for (actual, expected) in result.pixels().iter().zip(expected.pixels()) {
|
||||
assert_eq!(actual, &expected.0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(calls > 8);
|
||||
assert_eq!(task.step(1, || false).err().unwrap().code, ErrorCode::Consumed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_or_bad_input_cannot_publish_partial_pixels_later() {
|
||||
let mut task = HdrDecodeTask::new(HdrSource::new(rle()).unwrap()).unwrap();
|
||||
assert!(matches!(task.step(7, || false).unwrap(), HdrTaskPoll::Pending(_)));
|
||||
assert_eq!(task.step(1, || true).err().unwrap().code, ErrorCode::Cancelled);
|
||||
assert_eq!(task.step(16384, || false).err().unwrap().code, ErrorCode::Consumed);
|
||||
let mut bytes = rle();
|
||||
bytes.pop();
|
||||
let mut task = HdrDecodeTask::new(HdrSource::new(bytes).unwrap()).unwrap();
|
||||
assert_eq!(task.step(16384, || false).err().unwrap().code, ErrorCode::InvalidInput);
|
||||
assert_eq!(task.step(1, || false).err().unwrap().code, ErrorCode::Consumed);
|
||||
|
||||
let checks = std::cell::Cell::new(0);
|
||||
let mut task = HdrDecodeTask::new(HdrSource::new(rle()).unwrap()).unwrap();
|
||||
let result = task.step(16384, || {
|
||||
checks.set(checks.get() + 1);
|
||||
checks.get() == 10
|
||||
});
|
||||
assert_eq!(result.err().unwrap().code, ErrorCode::Cancelled);
|
||||
assert_eq!(checks.get(), 10);
|
||||
assert_eq!(task.step(1, || false).err().unwrap().code, ErrorCode::Consumed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_primaries_are_preserved_without_assuming_srgb() {
|
||||
let source = HdrSource::new(uncompressed("PRIMARIES=0.64 0.33 0.3 0.6 0.15 0.06 0.3127 0.329\n")).unwrap();
|
||||
assert_eq!(source.info().primaries, Some([0.64, 0.33, 0.3, 0.6, 0.15, 0.06, 0.3127, 0.329]));
|
||||
assert!(HdrSource::new(uncompressed("PRIMARIES=NaN 0 0 0 0 0 0 0\n")).is_err());
|
||||
let mut excessive_capacity = Vec::with_capacity(MAX_HDR_BYTES + 1);
|
||||
excessive_capacity.extend_from_slice(&rle());
|
||||
assert_eq!(HdrSource::new(excessive_capacity).err().unwrap().code, ErrorCode::LimitExceeded);
|
||||
}
|
||||
|
||||
fn header(width: u32, height: u32, extra: &str) -> Vec<u8> {
|
||||
format!("#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n{extra}\n-Y {height} +X {width}\n").into_bytes()
|
||||
}
|
||||
fn uncompressed(extra: &str) -> Vec<u8> {
|
||||
let mut bytes = header(4, 2, extra);
|
||||
for _ in 0..8 {
|
||||
bytes.extend_from_slice(&[128, 64, 32, 131]);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
fn rle() -> Vec<u8> {
|
||||
let mut bytes = header(8, 4, "");
|
||||
for _ in 0..4 {
|
||||
bytes.extend_from_slice(&[2, 2, 0, 8]);
|
||||
for value in [128, 64, 32, 129] {
|
||||
bytes.extend_from_slice(&[136, value]);
|
||||
}
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn literal_rle() -> Vec<u8> {
|
||||
let mut bytes = header(8, 4, "");
|
||||
for _ in 0..4 {
|
||||
bytes.extend_from_slice(&[2, 2, 0, 8]);
|
||||
for value in [128, 64, 32, 129] {
|
||||
bytes.push(8);
|
||||
bytes.extend_from_slice(&[value; 8]);
|
||||
}
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn old_rle() -> Vec<u8> {
|
||||
let mut bytes = header(8, 4, "");
|
||||
for _ in 0..4 {
|
||||
bytes.extend_from_slice(&[128, 64, 32, 129, 1, 1, 1, 7]);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incremental_rows_match_the_original_codec_without_clipping_hdr_values() {
|
||||
for bytes in [uncompressed(""), rle(), literal_rle(), old_rle()] {
|
||||
let expected = image::load_from_memory_with_format(&bytes, image::ImageFormat::Hdr).unwrap().into_rgb32f();
|
||||
let source = HdrSource::new(bytes).unwrap();
|
||||
source.validate(|| false).unwrap();
|
||||
let mut rows = Rows::new(source).unwrap();
|
||||
while !rows.done() {
|
||||
if rows.ready() {
|
||||
for x in 0..rows.info().width as usize {
|
||||
assert_eq!(rows.linear(x).unwrap(), expected.get_pixel(x as u32, rows.row()).0);
|
||||
}
|
||||
rows.next_row().unwrap();
|
||||
} else {
|
||||
rows.step().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_exposure_and_colour_correction_restore_linear_radiance() {
|
||||
let source = HdrSource::new(uncompressed("EXPOSURE=2\nCOLORCORR=1 2 4\n")).unwrap();
|
||||
let mut rows = Rows::new(source).unwrap();
|
||||
while !rows.ready() {
|
||||
rows.step().unwrap();
|
||||
}
|
||||
assert_eq!(rows.linear(0).unwrap(), [2.0, 0.5, 0.125]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_runs_truncation_nonfinite_metadata_and_hostile_sizes_are_refused() {
|
||||
let mut truncated = rle();
|
||||
truncated.pop();
|
||||
let mut wrong_width = rle();
|
||||
let offset = HdrSource::new(wrong_width.clone()).unwrap().info.offset;
|
||||
wrong_width[offset + 3] = 9;
|
||||
let mut zero_run = rle();
|
||||
zero_run[offset + 4] = 0;
|
||||
let mut overflow = header(8, 4, "");
|
||||
overflow.extend_from_slice(&[128, 64, 32, 129]);
|
||||
for _ in 0..6 {
|
||||
overflow.extend_from_slice(&[1, 1, 1, 0]);
|
||||
}
|
||||
for bytes in [truncated, wrong_width, zero_run, overflow] {
|
||||
assert!(HdrSource::new(bytes).unwrap().validate(|| false).is_err());
|
||||
}
|
||||
for bytes in [header(u32::MAX, 1, ""), uncompressed("EXPOSURE=NaN\n"), uncompressed("PIXASPECT=0\n")] {
|
||||
assert!(HdrSource::new(bytes).is_err());
|
||||
}
|
||||
let mut too_large = rle();
|
||||
too_large.resize(MAX_HDR_BYTES + 1, 0);
|
||||
assert_eq!(HdrSource::new(too_large).err().unwrap().code, ErrorCode::LimitExceeded);
|
||||
let mut too_bright = uncompressed("");
|
||||
let offset = HdrSource::new(too_bright.clone()).unwrap().info.offset;
|
||||
too_bright[offset + 3] = 255;
|
||||
assert_eq!(HdrSource::new(too_bright).unwrap().validate(|| false).unwrap_err().code, ErrorCode::LimitExceeded);
|
||||
let source = HdrSource::new(rle()).unwrap();
|
||||
|
||||
assert_eq!(source.validate(|| true).unwrap_err().code, ErrorCode::Cancelled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_work_is_bounded_before_accepting_metadata() {
|
||||
let mut bytes = b"#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n".to_vec();
|
||||
bytes.extend(std::iter::repeat_n(b'x', MAX_HEADER_BYTES + 1));
|
||||
bytes.extend_from_slice(b"\n\n-Y 2 +X 4\n");
|
||||
bytes.extend_from_slice(&[128, 64, 32, 129]);
|
||||
assert!(HdrSource::new(bytes).is_err());
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Device-independent, bounded linear HDR input preparation.
|
||||
|
||||
mod hdr;
|
||||
mod task;
|
||||
pub use hdr::{HdrInfo, HdrSource, MAX_HDR_BYTES, MAX_HDR_HEIGHT, MAX_HDR_PIXELS, MAX_HDR_WIDTH};
|
||||
pub use task::{DecodedHdr, HdrDecodeTask, HdrProgress, HdrTaskPoll};
|
||||
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HdrErrorCode {
|
||||
InvalidInput,
|
||||
LimitExceeded,
|
||||
AllocationFailed,
|
||||
Cancelled,
|
||||
Consumed,
|
||||
Internal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct HdrError {
|
||||
pub code: HdrErrorCode,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl HdrError {
|
||||
pub fn new(code: HdrErrorCode, message: impl Into<String>) -> Self {
|
||||
Self { code, message: message.into() }
|
||||
}
|
||||
}
|
||||
impl fmt::Display for HdrError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for HdrError {}
|
||||
pub type Result<T> = std::result::Result<T, HdrError>;
|
||||
|
||||
fn check_cancelled(cancelled: &impl Fn() -> bool) -> Result<()> {
|
||||
if cancelled() { Err(HdrError::new(HdrErrorCode::Cancelled, "HDR decoding cancelled")) } else { Ok(()) }
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use super::{HdrError, HdrErrorCode, HdrInfo, HdrSource, MAX_HDR_PIXELS, Result, check_cancelled, hdr::Rows};
|
||||
|
||||
/// Linear RGB in the source's colour space, without gamut conversion or tone mapping.
|
||||
pub struct DecodedHdr {
|
||||
info: HdrInfo,
|
||||
pixels: Vec<[f32; 3]>,
|
||||
}
|
||||
impl DecodedHdr {
|
||||
pub fn info(&self) -> &HdrInfo {
|
||||
&self.info
|
||||
}
|
||||
pub fn pixels(&self) -> &[[f32; 3]] {
|
||||
&self.pixels
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct HdrProgress {
|
||||
pub decoded_pixels: u64,
|
||||
pub total_pixels: u64,
|
||||
}
|
||||
pub enum HdrTaskPoll {
|
||||
Pending(HdrProgress),
|
||||
Complete(DecodedHdr),
|
||||
}
|
||||
|
||||
/// Caller-owned cooperative preparation; no scheduler, GPU or document dependency.
|
||||
pub struct HdrDecodeTask {
|
||||
rows: Option<Rows>,
|
||||
pixels: Vec<[f32; 3]>,
|
||||
x: usize,
|
||||
work: u64,
|
||||
}
|
||||
impl HdrDecodeTask {
|
||||
pub fn new(source: HdrSource) -> Result<Self> {
|
||||
let count = u64::from(source.info().width) * u64::from(source.info().height);
|
||||
let count = usize::try_from(count).map_err(|_| HdrError::new(HdrErrorCode::LimitExceeded, "HDR pixel count overflow"))?;
|
||||
let mut pixels = Vec::new();
|
||||
pixels.try_reserve_exact(count).map_err(|_| HdrError::new(HdrErrorCode::AllocationFailed, "could not reserve HDR pixels"))?;
|
||||
Ok(Self { rows: Some(Rows::new(source)?), pixels, x: 0, work: 0 })
|
||||
}
|
||||
|
||||
/// Each unit consumes one RLE packet (at most 128 bytes) or one RGB pixel.
|
||||
/// Error/cancellation consumes the task and releases its partial buffers.
|
||||
pub fn step(&mut self, units: usize, cancelled: impl Fn() -> bool) -> Result<HdrTaskPoll> {
|
||||
let result = self.advance(units, cancelled);
|
||||
if result.is_err() {
|
||||
self.rows = None;
|
||||
self.pixels = Vec::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
fn advance(&mut self, units: usize, cancelled: impl Fn() -> bool) -> Result<HdrTaskPoll> {
|
||||
let rows = self.rows.as_mut().ok_or_else(|| HdrError::new(HdrErrorCode::Consumed, "HDR task already consumed"))?;
|
||||
for _ in 0..units.clamp(1, 16384) {
|
||||
check_cancelled(&cancelled)?;
|
||||
self.work = self
|
||||
.work
|
||||
.checked_add(1)
|
||||
.filter(|n| *n <= MAX_HDR_PIXELS * 6 + 4096)
|
||||
.ok_or_else(|| HdrError::new(HdrErrorCode::LimitExceeded, "HDR decode work budget exceeded"))?;
|
||||
if rows.ready() {
|
||||
self.pixels.push(rows.linear(self.x)?);
|
||||
self.x += 1;
|
||||
if self.x == rows.info().width as usize {
|
||||
rows.next_row()?;
|
||||
self.x = 0;
|
||||
}
|
||||
} else {
|
||||
rows.step()?;
|
||||
}
|
||||
if rows.done() {
|
||||
check_cancelled(&cancelled)?;
|
||||
let info = rows.info().clone();
|
||||
self.rows = None;
|
||||
return Ok(HdrTaskPoll::Complete(DecodedHdr { info, pixels: std::mem::take(&mut self.pixels) }));
|
||||
}
|
||||
}
|
||||
Ok(HdrTaskPoll::Pending(HdrProgress {
|
||||
decoded_pixels: self.pixels.len() as u64,
|
||||
total_pixels: u64::from(rows.info().width) * u64::from(rows.info().height),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user