@@ -163,7 +163,11 @@ bool CodeStreamDecompress::decompress(grk_plugin_tile* tile)
163163 for (auto tileIndex : slated)
164164 {
165165 auto cacheEntry = tileCache_->get (tileIndex);
166- if (cacheEntry && cacheEntry->processor ->getImage () && !cacheEntry->dirty_ )
166+ if (!cacheEntry)
167+ continue ;
168+ auto proc = cacheEntry->processor ;
169+ if (proc->isBestEffortDecompressed () ||
170+ (proc->getImage () && (!cacheEntry->dirty_ || proc->allSOTMarkersParsed ())))
167171 tileCompletion_->complete (tileIndex);
168172 }
169173 }
@@ -183,7 +187,10 @@ bool CodeStreamDecompress::decompressImpl(std::set<uint16_t> slated)
183187 // Filter out fully cached tiles from slated
184188 std::erase_if (slated, [this ](uint16_t index) {
185189 auto cacheEntry = tileCache_->get (index);
186- return cacheEntry && cacheEntry->processor ->getImage () && !cacheEntry->dirty_ ;
190+ if (!cacheEntry)
191+ return false ;
192+ auto proc = cacheEntry->processor ;
193+ return proc->isBestEffortDecompressed () || (proc->getImage () && !cacheEntry->dirty_ );
187194 });
188195 if (slated.empty ())
189196 return true ;
@@ -385,6 +392,7 @@ void CodeStreamDecompress::decompressSequentialPrepare(void)
385392{
386393 stream_->seek (markerCache_->getTileStreamStart () + MARKER_BYTES );
387394 markerParser_.setSOT ();
395+ tileCache_->resetSOTParsing ();
388396 if (cp_.plmMarkers_ )
389397 cp_.plmMarkers_ ->rewind ();
390398 stream_->memAdvise (stream_->tell (), 0 , GrkAccessPattern::ACCESS_RANDOM );
@@ -400,6 +408,11 @@ void CodeStreamDecompress::decompressSequential(void)
400408 {
401409 if (!sequentialParseAndSchedule (true ))
402410 {
411+ // If we've exhausted all slated tiles' markers or the codestream
412+ // ran out of data (truncated image), stop gracefully.
413+ if (tileCache_->allSlatedSOTMarkersParsed (tilesToDecompress_.getSlatedTiles ()) ||
414+ markerParser_.endOfCodeStream () || stream_->numBytesLeft () == 0 )
415+ break ;
403416 success_ = false ;
404417 break ;
405418 }
@@ -443,6 +456,19 @@ void CodeStreamDecompress::decompressSequential(void)
443456 if (tileCache_->allSlatedSOTMarkersParsed (tilesToDecompress_.getSlatedTiles ()))
444457 break ;
445458 }
459+
460+ // Mark tiles that were never parsed (missing from truncated codestream)
461+ // as complete so wait() doesn't hang. Tiles that WERE parsed are still
462+ // being decompressed asynchronously and must not be completed here.
463+ if (tileCompletion_)
464+ {
465+ for (auto tileIndex : tilesToDecompress_.getSlatedTiles ())
466+ {
467+ auto cached = tileCache_->get (tileIndex);
468+ if (!cached || !cached->processor || !cached->processor ->allSOTMarkersParsed ())
469+ tileCompletion_->complete (tileIndex);
470+ }
471+ }
446472}
447473
448474bool CodeStreamDecompress::sequentialParseAndSchedule (bool multiTile)
@@ -468,7 +494,7 @@ bool CodeStreamDecompress::sequentialParseAndSchedule(bool multiTile)
468494 if (!processed)
469495 return false ;
470496 }
471- catch (const CorruptSOTMarkerException& csme)
497+ catch ([[maybe_unused]] const CorruptSOTMarkerException& csme)
472498 {
473499 return false ;
474500 }
@@ -832,6 +858,7 @@ std::function<void()> CodeStreamDecompress::postMultiTile(ITileProcessor* tilePr
832858 return ;
833859 }
834860 tileProcessor->post_decompressT2T1 (scratchImage_.get ());
861+ tileProcessor->setBestEffortDecompressed ();
835862 numTilesDecompressed_++;
836863 auto tileImage = tileProcessor->getImage ();
837864 if (!cp_.codingParams_ .dec_ .skipAllocateComposite_ && scratchImage_->has_multiple_tiles &&
0 commit comments