Skip to content

Commit d77ed37

Browse files
telecosetemesi254
authored andcommitted
docs(jpeg): clarify incremental decode contract
1 parent d221107 commit d77ed37

3 files changed

Lines changed: 96 additions & 7 deletions

File tree

crates/zune-jpeg/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,51 @@ fn main()->Result<(),DecoderErrors> {
2727
The decoder supports more manipulations via `DecoderOptions`,
2828
see additional documentation in the library.
2929

30+
### Incremental input
31+
32+
`JpegDecoder` can be retried on the same decoder when the underlying reader can
33+
see more bytes later. Callers should treat `DecodeErrors::is_recoverable_eof()`
34+
as the signal to feed more input and retry; any other error is a hard decode
35+
failure.
36+
37+
After `decode_headers()` succeeds, `info()` and `output_buffer_size()` are
38+
available. During `decode_into()`, the same decoder and output buffer must be
39+
kept across retries. If scan decoding returns recoverable EOF,
40+
`decoded_output_bytes()` and `decoded_scanlines()` report the stable prefix of
41+
the output buffer that can be displayed or copied before retrying.
42+
43+
```Rust
44+
use zune_core::bytestream::ZCursor;
45+
use zune_jpeg::JpegDecoder;
46+
47+
let mut decoder = JpegDecoder::new(ZCursor::new(&jpeg_bytes));
48+
49+
loop {
50+
match decoder.decode_headers() {
51+
Ok(()) => break,
52+
Err(error) if error.is_recoverable_eof() => {
53+
// Make more input bytes visible to the same reader, then retry.
54+
}
55+
Err(error) => return Err(error)
56+
}
57+
}
58+
59+
let mut pixels = vec![0; decoder.output_buffer_size().unwrap()];
60+
61+
loop {
62+
match decoder.decode_into(&mut pixels) {
63+
Ok(()) => break,
64+
Err(error) if error.is_recoverable_eof() => {
65+
let stable_bytes = decoder.decoded_output_bytes().unwrap_or(0);
66+
let stable_scanlines = decoder.decoded_scanlines().unwrap_or(0);
67+
// Display or copy the stable prefix, feed more input, then retry
68+
// with the same decoder and `pixels` buffer.
69+
}
70+
Err(error) => return Err(error)
71+
}
72+
}
73+
```
74+
3075
## Goals
3176

3277
The implementation aims to have the following goals achieved,

crates/zune-jpeg/src/decoder.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,17 @@ where
621621
};
622622
}
623623

624+
/// Return the number of output bytes known to be stable after the most
625+
/// recent `decode_into` attempt.
626+
///
627+
/// On recoverable EOF this is the prefix the caller may display or copy,
628+
/// provided the next retry uses the same decoder and output buffer. It is
629+
/// `None` until headers are complete and the output layout is known.
630+
#[must_use]
631+
pub fn decoded_output_bytes(&self) -> Option<usize> {
632+
Some(self.pixels_decoded.min(self.output_buffer_size()?))
633+
}
634+
624635
/// Return the number of output scanlines known to be stable after the
625636
/// most recent `decode_into` attempt.
626637
///
@@ -629,17 +640,14 @@ where
629640
/// call `decode_into` again to continue decoding.
630641
#[must_use]
631642
pub fn decoded_scanlines(&self) -> Option<usize> {
632-
if !self.headers_decoded {
633-
return None;
634-
}
635-
643+
let decoded_output_bytes = self.decoded_output_bytes()?;
636644
let row_stride = usize::from(self.width())
637645
.checked_mul(self.options.jpeg_get_out_colorspace().num_components())?;
638646
if row_stride == 0 {
639647
return Some(0);
640648
}
641649

642-
Some((self.pixels_decoded / row_stride).min(usize::from(self.height())))
650+
Some((decoded_output_bytes / row_stride).min(usize::from(self.height())))
643651
}
644652

645653
/// Get an immutable reference to the decoder options
@@ -1220,7 +1228,15 @@ where
12201228
///
12211229
/// On a recoverable EOF (`DecodeErrors::is_recoverable_eof()`) the
12221230
/// decoder keeps enough state to resume; the caller can grow the input
1223-
/// stream and call `decode_into` again.
1231+
/// stream and call `decode_into` again. The caller must keep using the
1232+
/// same decoder and output buffer for retries. After a recoverable scan
1233+
/// EOF, [`decoded_output_bytes`](Self::decoded_output_bytes) and
1234+
/// [`decoded_scanlines`](Self::decoded_scanlines) describe the stable
1235+
/// prefix in that output buffer.
1236+
///
1237+
/// Embedders should use the returned error to distinguish retryable EOF
1238+
/// from hard failures: `Err(e)` where `e.is_recoverable_eof()` means feed
1239+
/// more input and retry, while any other `Err` is non-recoverable.
12241240
///
12251241
/// On success the decoder keeps scan-start replay state, so a later
12261242
/// `decode_into` call is well-defined and produces bit-identical pixels.
@@ -1431,7 +1447,9 @@ where
14311447
///
14321448
/// If the reader runs out of data the error will satisfy
14331449
/// [`is_recoverable_eof()`](crate::errors::DecodeErrors::is_recoverable_eof);
1434-
/// the caller may retry after providing more data.
1450+
/// the caller may retry after providing more data. After success,
1451+
/// [`output_buffer_size`](Self::output_buffer_size) and [`info`](Self::info)
1452+
/// are available.
14351453
pub fn decode_headers(&mut self) -> Result<(), DecodeErrors> {
14361454
self.decode_headers_internal()?;
14371455
Ok(())

crates/zune-jpeg/tests/incremental.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,16 @@ fn decode_into_replay_after_success_matches_oneshot() {
236236
fn incomplete_data_returns_recoverable_eof() {
237237
// Empty input → recoverable EOF
238238
let mut dec = JpegDecoder::new(ZCursor::new(&[] as &[u8]));
239+
assert!(dec.info().is_none());
240+
assert_eq!(dec.output_buffer_size(), None);
241+
assert_eq!(dec.decoded_output_bytes(), None);
242+
assert_eq!(dec.decoded_scanlines(), None);
239243
let err = dec.decode_headers().unwrap_err();
240244
assert!(err.is_recoverable_eof(), "empty input: expected recoverable EOF, got: {err:?}");
245+
assert!(dec.info().is_none());
246+
assert_eq!(dec.output_buffer_size(), None);
247+
assert_eq!(dec.decoded_output_bytes(), None);
248+
assert_eq!(dec.decoded_scanlines(), None);
241249

242250
// Truncated just after SOI → recoverable EOF
243251
let data = include_bytes!("../../../test-images/jpeg/synthetic_image.jpg");
@@ -249,6 +257,10 @@ fn incomplete_data_returns_recoverable_eof() {
249257
let mut dec = JpegDecoder::new(ZCursor::new(&[0x00, 0x00]));
250258
let err = dec.decode_headers().unwrap_err();
251259
assert!(!err.is_recoverable_eof(), "bad magic: should not be recoverable, got: {err:?}");
260+
assert!(dec.info().is_none());
261+
assert_eq!(dec.output_buffer_size(), None);
262+
assert_eq!(dec.decoded_output_bytes(), None);
263+
assert_eq!(dec.decoded_scanlines(), None);
252264

253265
// Non-EOF error variants → NOT recoverable
254266
use zune_jpeg::errors::DecodeErrors;
@@ -1271,6 +1283,8 @@ fn per_row_checkpoint_avoids_full_scan_replay() {
12711283
decoder
12721284
.decode_headers()
12731285
.expect("headers should be fully visible at cutoff");
1286+
assert_eq!(decoder.decoded_output_bytes(), Some(0));
1287+
assert_eq!(decoder.decoded_scanlines(), Some(0));
12741288
let mut out = vec![0u8; decoder.output_buffer_size().unwrap()];
12751289

12761290
// First decode attempt — should fail with recoverable EOF.
@@ -1285,6 +1299,11 @@ fn per_row_checkpoint_avoids_full_scan_replay() {
12851299
let first_scanlines = decoder
12861300
.decoded_scanlines()
12871301
.expect("headers are decoded, so partial progress should be known");
1302+
let first_bytes = decoder
1303+
.decoded_output_bytes()
1304+
.expect("headers are decoded, so partial progress should be known");
1305+
assert!(first_bytes > 0, "scan EOF should expose a stable output prefix");
1306+
assert!(first_bytes < out.len(), "truncated decode should not report full output");
12881307
assert!(
12891308
first_scanlines > 0 && first_scanlines < usize::from(decoder.info().unwrap().height),
12901309
"expected a stable partial prefix, got {first_scanlines} scanlines"
@@ -1300,6 +1319,8 @@ fn per_row_checkpoint_avoids_full_scan_replay() {
13001319
err.is_recoverable_eof(),
13011320
"expected recoverable EOF on second attempt, got {err:?}"
13021321
);
1322+
assert!(decoder.decoded_output_bytes().unwrap() >= first_bytes);
1323+
assert!(decoder.decoded_scanlines().unwrap() >= first_scanlines);
13031324

13041325
// Third attempt — expose full data. Should resume from the per-row
13051326
// checkpoint saved during the second attempt.
@@ -1309,6 +1330,11 @@ fn per_row_checkpoint_avoids_full_scan_replay() {
13091330
.decode_into(&mut out)
13101331
.expect("full data should allow decode to complete");
13111332
assert_pixels_match(&out, &expected, "per_row_checkpoint", data.len());
1333+
assert_eq!(
1334+
decoder.decoded_output_bytes(),
1335+
Some(out.len()),
1336+
"successful decode should report the full output buffer as stable"
1337+
);
13121338
assert_eq!(
13131339
decoder.decoded_scanlines(),
13141340
Some(usize::from(decoder.info().unwrap().height)),

0 commit comments

Comments
 (0)