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);
}