runPdfRenderWorker function

void runPdfRenderWorker()

Runs the render worker inside a dedicated Web Worker. A consuming web app ships 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; the app then sets pdfRenderWorkerScriptUrl = 'pdf_render_worker.dart.js' before opening a viewer. 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'} → sets the active cancellation token so the in-flight interpreter walk abandons early, replying with buffer:null.

Implementation

void runPdfRenderWorker() {
  final scope = globalContext as web.DedicatedWorkerGlobalScope;
  PdfDocument? document;
  PdfCancellationToken? activeToken;

  // 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;
      try {
        shared =
            (data.getProperty('shared'.toJS) as JSBoolean?)?.toDart ?? false;
        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.
      scope.postMessage(JSObject()
        ..setProperty('kind'.toJS, 'ready'.toJS)
        ..setProperty('shared'.toJS, shared.toJS));
      return;
    }

    if (kind == 'cancel') {
      activeToken?.cancelled = true;
      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;
    // 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 {
      Uint8List? out;
      String? error;
      final doc = document;
      try {
        if (doc != null) {
          out = await _recordPageAsync(doc, page, annotations, imagePixelRatio,
              decodeImages, commandLimit, imageDecodeRegion, token);
        }
      } 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;

      final result = JSObject()
        ..setProperty('kind'.toJS, 'result'.toJS)
        ..setProperty('id'.toJS, id.toJS);
      if (out == null) {
        result.setProperty('buffer'.toJS, null);
        if (error != null) result.setProperty('error'.toJS, error.toJS);
        scope.postMessage(result);
      } else {
        // Copy to an exact-length buffer, then transfer it (zero-copy).
        final jsBuffer = Uint8List.fromList(out).buffer.toJS;
        result.setProperty('buffer'.toJS, jsBuffer);
        scope.postMessage(result, <JSAny>[jsBuffer].toJS);
      }
    }();
  }).toJS;
}