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 withbuffer:null.{kind:'bin', id, page, annotations, m0..m5, deviceWidth, deviceHeight, pixelRatio, slugGlyphs}→ replies with an encodedStripPlanin 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;
}