@@ -302,11 +302,25 @@ pub struct JpegDecoder<T> {
302302 pub ( crate ) progressive_mcus_buffer : [ Vec < i16 > ; MAX_COMPONENTS ] ,
303303 /// Whether per-row checkpointing is enabled for the current decode.
304304 ///
305- /// Only `true` on a retry `decode_into` call (when `scan_state` was
306- /// already `Some` on entry). This keeps the one-shot decode path free
307- /// of per-row overhead while still enabling fine-grained resume on
308- /// incremental retries .
305+ /// By default this becomes `true` after a previous scan attempt has run,
306+ /// keeping one-shot decode free of per-row overhead. `incremental_mode`
307+ /// enables the same checkpoints on the first scan attempt for streaming
308+ /// callers .
309309 pub ( crate ) mcu_checkpoints_enabled : bool ,
310+ /// Whether row checkpoints should also be recorded on the first scan
311+ /// decode attempt.
312+ ///
313+ /// Disabled by default to keep one-shot decode free of checkpoint work;
314+ /// streaming callers can opt in before `decode_into` to avoid replaying
315+ /// from scan start after the first recoverable scan EOF.
316+ incremental_mode : bool ,
317+ /// Whether this decoder has already attempted scan decoding.
318+ ///
319+ /// `scan_state` becomes `Some` as soon as headers reach SOS, including
320+ /// after an explicit `decode_headers` call. This flag tracks the narrower
321+ /// condition needed for default checkpoint gating: a previous
322+ /// `decode_into` scan attempt actually ran.
323+ scan_decode_attempted : bool ,
310324 /// Scratch buffer that header marker parsers fill with the marker body
311325 /// before mutating decoder state.
312326 ///
@@ -541,6 +555,8 @@ where
541555 scan_state : None ,
542556 pixels_decoded : 0 ,
543557 mcu_checkpoints_enabled : false ,
558+ incremental_mode : false ,
559+ scan_decode_attempted : false ,
544560 progressive_mcus_buffer : core:: array:: from_fn ( |_| Vec :: new ( ) ) ,
545561 marker_body_scratch : Vec :: new ( )
546562 }
@@ -632,6 +648,33 @@ where
632648 Some ( self . pixels_decoded . min ( self . output_buffer_size ( ) ?) )
633649 }
634650
651+ /// Return whether incremental mode is enabled.
652+ ///
653+ /// Incremental mode records per-row checkpoints during the first scan
654+ /// decode attempt, allowing a later retry after recoverable EOF to resume
655+ /// from the latest stable row instead of replaying from scan start.
656+ ///
657+ /// It is disabled by default so one-shot decoding keeps the lowest
658+ /// overhead path.
659+ #[ must_use]
660+ pub const fn incremental_mode ( & self ) -> bool {
661+ self . incremental_mode
662+ }
663+
664+ /// Enable or disable incremental mode.
665+ ///
666+ /// Call this before the first `decode_into` scan attempt when the caller
667+ /// expects input to arrive incrementally. In this mode baseline Huffman
668+ /// single-SOS scans save row checkpoints on the first attempt, trading a
669+ /// small amount of checkpoint work for less replay on the next retry.
670+ ///
671+ /// The default is `false`, which preserves the zero-overhead one-shot
672+ /// path and only enables row checkpoints after a previous scan decode
673+ /// attempt has run.
674+ pub fn set_incremental_mode ( & mut self , enabled : bool ) {
675+ self . incremental_mode = enabled;
676+ }
677+
635678 /// Return the number of output scanlines known to be stable after the
636679 /// most recent `decode_into` attempt.
637680 ///
@@ -1238,6 +1281,12 @@ where
12381281 /// from hard failures: `Err(e)` where `e.is_recoverable_eof()` means feed
12391282 /// more input and retry, while any other `Err` is non-recoverable.
12401283 ///
1284+ /// By default, row checkpoints are enabled after a previous scan decode
1285+ /// attempt, so the first one-shot decode avoids checkpoint overhead. Call
1286+ /// [`set_incremental_mode`](Self::set_incremental_mode) before the first
1287+ /// scan attempt to record row checkpoints immediately when input is
1288+ /// expected to arrive incrementally.
1289+ ///
12411290 /// On success the decoder keeps scan-start replay state, so a later
12421291 /// `decode_into` call is well-defined and produces bit-identical pixels.
12431292 /// Replay re-runs entropy decoding from the first SOS.
@@ -1283,15 +1332,6 @@ where
12831332 pixels_written : usize ,
12841333 dc_predictions : [ ( i32 , i32 ) ; MAX_COMPONENTS ]
12851334 }
1286- // Enable per-row checkpointing only on retry calls (when scan_state
1287- // already existed before this decode_into invocation). One-shot
1288- // decoding skips checkpoint overhead entirely.
1289- let retrying_scan = self . scan_state . is_some ( ) ;
1290- self . mcu_checkpoints_enabled = retrying_scan;
1291- if !retrying_scan {
1292- self . pixels_decoded = 0 ;
1293- }
1294-
12951335 let scan_plan = self . scan_state . as_deref ( ) . map ( |state| ScanPlan {
12961336 scan_start_position : state. scan_start_position ,
12971337 outer_append_snapshot : state. append_snapshot ,
@@ -1388,6 +1428,17 @@ where
13881428 let out_len = core:: cmp:: min ( out. len ( ) , expected_size) ;
13891429 let out = & mut out[ 0 ..out_len] ;
13901430
1431+ // By default, enable per-row checkpointing only after a previous
1432+ // scan decode attempt has run. Incremental mode opts into the same
1433+ // checkpoints on the first scan attempt so a streaming caller avoids
1434+ // one scan-start replay.
1435+ let previous_scan_attempt = self . scan_decode_attempted ;
1436+ self . mcu_checkpoints_enabled = previous_scan_attempt || self . incremental_mode ;
1437+ if !previous_scan_attempt {
1438+ self . pixels_decoded = 0 ;
1439+ }
1440+ self . scan_decode_attempted = true ;
1441+
13911442 let result: Result < ( ) , DecodeErrors > ;
13921443 if self . is_arithmetic {
13931444 #[ cfg( feature = "arith" ) ]
0 commit comments