runPdfRenderWorker function

void runPdfRenderWorker()

Runs the render worker inside a dedicated Web Worker.

dart_pdf_editor ships a prebuilt worker as a Flutter package asset, so most consuming apps do not call this directly. Apps that want to self-host a custom worker can still compile a tiny worker script that calls this:

// web/pdf_render_worker.dart
import 'package:dart_pdf_editor/render_worker_web.dart';
void main() => runPdfRenderWorker();

compiled with dart compile js web/pdf_render_worker.dart -o web/pdf_render_worker.dart.js and served alongside the app; set pdfRenderWorkerScriptUrl = 'pdf_render_worker.dart.js' before opening a viewer to use that custom script. See doc/render_worker_web.md for the full wiring.

Protocol (mirrors the native isolate backend):

  • {kind:'init', bytes:ArrayBuffer|SharedArrayBuffer, shared} → opens the document, replies {kind:'ready', shared}.
  • {kind:'record', id, page, annotations} → replies {kind:'result', id, buffer:ArrayBuffer|null} (null = the page can't be offloaded; the main thread renders it locally).
  • {kind:'cancel', id} → cancels only the matching active request, so a late message cannot abort its successor; a match abandons the interpreter walk early and replies with buffer:null.
  • {kind:'bin', id, page, annotations, m0..m5, deviceWidth, deviceHeight, pixelRatio, slugGlyphs} → replies with an encoded StripPlan in the same result shape (null = bin locally).
  • {kind:'detail', id, page, annotations, m0..m5, deviceWidth, deviceHeight, pixelRatio, regionLeft..regionTop} → replies with transferable command and plan buffers produced by one cancellable worker job.

Implementation

void runPdfRenderWorker() {
  final scope = globalContext as web.DedicatedWorkerGlobalScope;
  PdfDocument? document;
  var reuseTranscripts = true;
  var collectTimings = false;
  PdfCancellationToken? activeToken;
  int? activeRequestId;
  final transcriptCache = PdfWorkerTranscriptCache();

  // The handler MUST stay synchronous (return void): `.toJS` cannot convert a
  // Future-returning function, so an `async` handler fails `dart compile js`
  // ("invalid types in its function signature: Future<Null> Function(...)").
  // The cancellable record below therefore runs in a fire-and-forget inner
  // async closure instead of making the handler itself async.
  scope.onmessage = ((web.MessageEvent event) {
    final data = event.data as JSObject?;
    if (data == null) return;
    final kind = (data.getProperty('kind'.toJS) as JSString?)?.toDart;

    if (kind == 'init') {
      // Extract AND open inside the try: a malformed transfer (the cast or the
      // ArrayBuffer view can throw on some hosts) must NOT skip the 'ready'
      // reply below, or the main thread waits on it forever. A null document
      // simply declines every page to a local render.
      var shared = false;
      Stopwatch? openClock;
      try {
        shared =
            (data.getProperty('shared'.toJS) as JSBoolean?)?.toDart ?? false;
        reuseTranscripts =
            (data.getProperty('reuseTranscripts'.toJS) as JSBoolean?)?.toDart ??
            true;
        collectTimings =
            (data.getProperty('timings'.toJS) as JSBoolean?)?.toDart ?? false;
        // Light up the COS-layer facade alongside the trace timings; each
        // result attaches (and resets) its per-job snapshot.
        PdfPerf.enabled = collectTimings;
        if (collectTimings) openClock = Stopwatch()..start();
        final buffer = data.getProperty('bytes'.toJS) as JSObject;
        final bytes = shared
            ? _jsUint8View(buffer).toDart
            : (buffer as JSArrayBuffer).toDart.asUint8List();
        document = PdfDocument.open(bytes);
      } catch (_) {
        document = null; // bad transfer / broken document → local renders
      }
      // ALWAYS reply ready, even on failure, so the client never hangs.
      openClock?.stop();
      final ready = JSObject()
        ..setProperty('kind'.toJS, 'ready'.toJS)
        ..setProperty('shared'.toJS, shared.toJS);
      if (openClock != null) {
        ready.setProperty('openUs'.toJS, openClock.elapsedMicroseconds.toJS);
      }
      scope.postMessage(ready);
      return;
    }

    if (kind == 'cancel') {
      final id = (data.getProperty('id'.toJS) as JSNumber?)?.toDartInt;
      if (id != null && id == activeRequestId) {
        activeToken?.cancelled = true;
      } else if (id != null) {
        final ignored = JSObject()
          ..setProperty('kind'.toJS, 'cancelIgnored'.toJS)
          ..setProperty('targetId'.toJS, id.toJS);
        final active = activeRequestId;
        if (active != null) {
          ignored.setProperty('activeId'.toJS, active.toJS);
        }
        scope.postMessage(ignored);
      }
      return;
    }

    if (kind == 'bin' || kind == 'detail') {
      final id = (data.getProperty('id'.toJS) as JSNumber).toDartInt;
      final page = (data.getProperty('page'.toJS) as JSNumber).toDartInt;
      final annotations =
          (data.getProperty('annotations'.toJS) as JSBoolean).toDart;
      final matrix = <double>[
        for (var i = 0; i < 6; i++)
          (data.getProperty('m$i'.toJS) as JSNumber).toDartDouble,
      ];
      final deviceWidth =
          (data.getProperty('deviceWidth'.toJS) as JSNumber).toDartInt;
      final deviceHeight =
          (data.getProperty('deviceHeight'.toJS) as JSNumber).toDartInt;
      final pixelRatio =
          (data.getProperty('pixelRatio'.toJS) as JSNumber).toDartDouble;
      final slugGlyphs =
          (data.getProperty('slugGlyphs'.toJS) as JSBoolean?)?.toDart ?? false;
      final token = PdfCancellationToken();
      activeToken = token;
      activeRequestId = id;
      () async {
        final timings = collectTimings ? PdfWorkerPhaseTimings() : null;
        final workerClock = collectTimings ? (Stopwatch()..start()) : null;
        Uint8List? out;
        Uint8List? detailPlan;
        String? error;
        final doc = document;
        try {
          if (doc != null) {
            if (kind == 'detail') {
              final region = PdfRect(
                (data.getProperty('regionLeft'.toJS) as JSNumber).toDartDouble,
                (data.getProperty('regionBottom'.toJS) as JSNumber)
                    .toDartDouble,
                (data.getProperty('regionRight'.toJS) as JSNumber).toDartDouble,
                (data.getProperty('regionTop'.toJS) as JSNumber).toDartDouble,
              );
              final detail = await _recordStripDetailAsync(
                doc,
                transcriptCache,
                page,
                annotations,
                matrix,
                deviceWidth,
                deviceHeight,
                pixelRatio,
                region,
                token,
                timings: timings,
              );
              out = detail?.$1;
              detailPlan = detail?.$2;
            } else {
              out = await _binStripsAsync(
                doc,
                transcriptCache,
                page,
                annotations,
                matrix,
                deviceWidth,
                deviceHeight,
                pixelRatio,
                slugGlyphs,
                token,
                timings: timings,
              );
            }
          }
        } on PdfCancelledException {
          out = null;
        } catch (e, st) {
          out = null;
          error = '$e\n$st';
        }
        if (identical(activeToken, token)) {
          activeToken = null;
          activeRequestId = null;
        }
        workerClock?.stop();
        if (detailPlan == null) {
          _postResult(
            scope,
            id,
            out,
            error,
            timings,
            workerClock?.elapsedMicroseconds,
          );
        } else {
          _postDetailResult(
            scope,
            id,
            out!,
            detailPlan,
            timings,
            workerClock?.elapsedMicroseconds,
          );
        }
      }();
      return;
    }

    if (kind == 'regionIndex') {
      final id = (data.getProperty('id'.toJS) as JSNumber).toDartInt;
      final page = (data.getProperty('page'.toJS) as JSNumber).toDartInt;
      final annotations =
          (data.getProperty('annotations'.toJS) as JSBoolean).toDart;
      final maxCommands =
          (data.getProperty('maxCommands'.toJS) as JSNumber).toDartInt;
      final buildGrid =
          (data.getProperty('buildGrid'.toJS) as JSBoolean?)?.toDart ?? false;
      final token = PdfCancellationToken();
      activeToken = token;
      activeRequestId = id;
      () async {
        final timings = collectTimings ? PdfWorkerPhaseTimings() : null;
        final workerClock = collectTimings ? (Stopwatch()..start()) : null;
        Uint8List? out;
        String? error;
        final doc = document;
        try {
          if (doc != null) {
            out = await _buildRegionIndexAsync(
              doc,
              transcriptCache,
              page,
              annotations,
              maxCommands,
              buildGrid,
              token,
              timings: timings,
            );
          }
        } on PdfCancelledException {
          out = null;
        } catch (e, st) {
          out = null;
          error = '$e\n$st';
        }
        if (identical(activeToken, token)) {
          activeToken = null;
          activeRequestId = null;
        }
        workerClock?.stop();
        _postResult(
          scope,
          id,
          out,
          error,
          timings,
          workerClock?.elapsedMicroseconds,
        );
      }();
      return;
    }

    if (kind != 'record') return;
    final id = (data.getProperty('id'.toJS) as JSNumber).toDartInt;
    final page = (data.getProperty('page'.toJS) as JSNumber).toDartInt;
    final annotations =
        (data.getProperty('annotations'.toJS) as JSBoolean).toDart;
    final imagePixelRatio =
        (data.getProperty('imageRatio'.toJS) as JSNumber?)?.toDartDouble;
    // Default true so an older client that doesn't send the flag still decodes.
    final decodeImages =
        (data.getProperty('decodeImages'.toJS) as JSBoolean?)?.toDart ?? true;
    final commandLimit =
        (data.getProperty('commandLimit'.toJS) as JSNumber?)?.toDartInt;
    final regionLeft =
        (data.getProperty('regionLeft'.toJS) as JSNumber?)?.toDartDouble;
    final regionBottom =
        (data.getProperty('regionBottom'.toJS) as JSNumber?)?.toDartDouble;
    final regionRight =
        (data.getProperty('regionRight'.toJS) as JSNumber?)?.toDartDouble;
    final regionTop =
        (data.getProperty('regionTop'.toJS) as JSNumber?)?.toDartDouble;
    final imageDecodeRegion =
        regionLeft != null &&
            regionBottom != null &&
            regionRight != null &&
            regionTop != null
        ? PdfRect(regionLeft, regionBottom, regionRight, regionTop)
        : null;

    final token = PdfCancellationToken();
    activeToken = token;
    activeRequestId = id;
    // Fire-and-forget: launch the cancellable walk without awaiting it here, so
    // the message handler returns void (see the note above) while a subsequent
    // 'cancel' message can still flip token.cancelled mid-walk.
    () async {
      final timings = collectTimings ? PdfWorkerPhaseTimings() : null;
      final workerClock = collectTimings ? (Stopwatch()..start()) : null;
      Uint8List? out;
      String? error;
      final doc = document;
      try {
        if (doc != null) {
          out = await _recordPageAsync(
            doc,
            transcriptCache,
            reuseTranscripts,
            page,
            annotations,
            imagePixelRatio,
            decodeImages,
            commandLimit,
            imageDecodeRegion,
            token,
            timings: timings,
          );
        }
      } on PdfCancelledException {
        out = null;
      } catch (e, st) {
        out = null; // any failure → the main thread renders this page locally
        error = '$e\n$st';
      }
      // Only clear the active token if it is still ours - a newer record may
      // have replaced it while this one was running.
      if (identical(activeToken, token)) {
        activeToken = null;
        activeRequestId = null;
      }

      workerClock?.stop();
      _postResult(
        scope,
        id,
        out,
        error,
        timings,
        workerClock?.elapsedMicroseconds,
      );
    }();
  }).toJS;
}