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