decodeBanded static method
Decodes page pageIndex of the file at filePath in horizontal
bands, spread across up to workerCount isolates — each opens its own
handle on filePath (a decoded TiffImage and its underlying file
handle can't cross an isolate boundary, so there's no way to share one
already-open document across workers). onBand is called back on the
caller's isolate once per decoded band, so it can do arbitrary work
per band — write a file, accumulate into a buffer, feed a progress
counter — without that work itself needing to be isolate-safe. Bands
from different workers can interleave, so don't assume they arrive in
increasing TiffBand.y order across the whole page (they do arrive in
order within any one worker's own share of it).
bandHeight is the height of each delivered TiffBand — independent
of the taller, tile/strip-aligned chunk actually decoded per call (see
TiffChunkPlan); one chunk decode gets sliced into bandHeight-tall
pieces before delivery, so a caller that wants a tight per-band memory
footprint doesn't have to give up the tile-alignment that avoids
redundant redecoding.
maxBytesPerChunk bounds one worker's one in-flight chunk decode (see
TiffChunkPlan.forBudget) — it is not divided by workerCount
here, since up to workerCount chunks can be in flight at once. If
you need the aggregate peak (workerCount * maxBytesPerChunk, roughly)
bounded by some total budget, pick workerCount via
TiffChunkPlan.recommendedWorkerCount against that budget and this
same maxBytesPerChunk — deliberately not done automatically here,
since shrinking maxBytesPerChunk to fit more workers instead
reintroduces the redundant-redecode problem TiffChunkPlan exists to
avoid (see its doc comment).
If the source uses a codec that needs a plugged-in decoder (JPEG,
Compression 6/7 — see package:tiff/tiff_image_adapter.dart), pass
setUpIsolate as a static or top-level function reference (not a
closure capturing local state, which can't cross an isolate boundary)
that wires it up — e.g. TiffImageAdapter.enableJpegSupport — called
once in every worker isolate before that worker decodes anything.
Throws a TiffException (wrapping whatever error message a worker reported) if any worker's decode fails; the remaining workers are killed rather than left to keep running.
Implementation
static Future<void> decodeBanded({
required String filePath,
required int pageIndex,
required int bandHeight,
required int maxBytesPerChunk,
required int workerCount,
required void Function(TiffBand band) onBand,
void Function()? setUpIsolate,
}) async {
if (bandHeight <= 0) {
throw ArgumentError.value(bandHeight, 'bandHeight', 'must be > 0');
}
if (workerCount <= 0) {
throw ArgumentError.value(workerCount, 'workerCount', 'must be > 0');
}
final metadataSource = FileByteSource.open(File(filePath));
final int width;
final List<(int, int)> chunks;
try {
final document = TiffDecoder.decodeSource(metadataSource);
if (pageIndex < 0 || pageIndex >= document.images.length) {
throw ArgumentError.value(
pageIndex,
'pageIndex',
'out of range (page count: ${document.images.length})',
);
}
final metadata = document.images[pageIndex].metadata;
width = metadata.width;
chunks = TiffChunkPlan.forBudget(
metadata,
maxBytesPerChunk: maxBytesPerChunk,
).chunks;
} finally {
metadataSource.close();
}
if (chunks.isEmpty) return;
final effectiveWorkerCount = workerCount < chunks.length
? workerCount
: chunks.length;
final chunksByWorker = List.generate(
effectiveWorkerCount,
(_) => <(int, int)>[],
);
for (var i = 0; i < chunks.length; i++) {
chunksByWorker[i % effectiveWorkerCount].add(chunks[i]);
}
final receivePort = ReceivePort();
final isolates = <Isolate>[];
String? workerError;
try {
for (final assigned in chunksByWorker) {
isolates.add(
await Isolate.spawn(_bandWorkerEntry, (
receivePort.sendPort,
filePath,
pageIndex,
width,
bandHeight,
assigned,
setUpIsolate,
)),
);
}
var finishedWorkers = 0;
await for (final message in receivePort) {
if (message is (int, int, Uint8List)) {
final (y, height, rgba) = message;
onBand((y: y, height: height, rgba: rgba));
} else if (message == true) {
finishedWorkers++;
if (finishedWorkers == effectiveWorkerCount) break;
} else {
workerError = message as String;
break;
}
}
} finally {
for (final isolate in isolates) {
isolate.kill(priority: Isolate.immediate);
}
receivePort.close();
}
if (workerError != null) throw TiffException(workerError);
}