decodeBanded static method

Future<void> decodeBanded({
  1. required String filePath,
  2. required int pageIndex,
  3. required int bandHeight,
  4. required int maxBytesPerChunk,
  5. required int workerCount,
  6. required void onBand(
    1. TiffBand band
    ),
  7. void setUpIsolate()?,
})

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