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:
selune authored and GitHub committed 2026-10-13 16:33:51 -04:00
1 parent 8319c35a32
commit e99b3bd4e8
11 files changed
+697 -2

No files matched your search

+1
View File
@@ -7,6 +7,7 @@
mod backend;
mod camera;
mod curve_display;
pub mod lighting;
mod raster;
pub use backend::{
+305
View File
@@ -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(())
}
}
+172
View File
@@ -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());
}
+43
View File
@@ -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(()) }
}
+84
View File
@@ -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),
}))
}
}