@reekon-tools/react-native-pdf-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/PdfCanvas.podspec +80 -0
  2. package/README.md +1391 -0
  3. package/android/build.gradle +175 -0
  4. package/android/consumer-rules.pro +3 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +2 -0
  8. package/android/gradlew +248 -0
  9. package/android/gradlew.bat +98 -0
  10. package/android/settings.gradle +34 -0
  11. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
  12. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
  13. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
  14. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
  15. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
  16. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
  17. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
  18. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
  19. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
  20. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
  21. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
  22. package/android/src/jsi/cpp/CMakeLists.txt +33 -0
  23. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
  24. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
  25. package/android/src/main/AndroidManifest.xml +7 -0
  26. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
  27. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
  28. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
  29. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
  30. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
  31. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
  32. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
  33. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
  34. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
  35. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
  36. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
  37. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
  38. package/android/tools/compile-gate.sh +289 -0
  39. package/dist/cache.d.ts +161 -0
  40. package/dist/cache.js +389 -0
  41. package/dist/clock.d.ts +21 -0
  42. package/dist/clock.js +31 -0
  43. package/dist/controller.d.ts +131 -0
  44. package/dist/controller.js +1002 -0
  45. package/dist/geometry.d.ts +96 -0
  46. package/dist/geometry.js +248 -0
  47. package/dist/index.d.ts +52 -0
  48. package/dist/index.js +50 -0
  49. package/dist/index.native.d.ts +21 -0
  50. package/dist/index.native.js +23 -0
  51. package/dist/index.web.d.ts +24 -0
  52. package/dist/index.web.js +26 -0
  53. package/dist/layout.d.ts +78 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/peer-check.d.ts +83 -0
  56. package/dist/peer-check.js +183 -0
  57. package/dist/planner.d.ts +35 -0
  58. package/dist/planner.js +530 -0
  59. package/dist/policy.d.ts +14 -0
  60. package/dist/policy.js +168 -0
  61. package/dist/rasterizer/android.d.ts +121 -0
  62. package/dist/rasterizer/android.js +126 -0
  63. package/dist/rasterizer/buffer.d.ts +18 -0
  64. package/dist/rasterizer/buffer.js +18 -0
  65. package/dist/rasterizer/fake.d.ts +92 -0
  66. package/dist/rasterizer/fake.js +433 -0
  67. package/dist/rasterizer/index.d.ts +38 -0
  68. package/dist/rasterizer/index.js +69 -0
  69. package/dist/rasterizer/index.native.d.ts +19 -0
  70. package/dist/rasterizer/index.native.js +80 -0
  71. package/dist/rasterizer/index.web.d.ts +63 -0
  72. package/dist/rasterizer/index.web.js +75 -0
  73. package/dist/rasterizer/ingest.d.ts +180 -0
  74. package/dist/rasterizer/ingest.js +307 -0
  75. package/dist/rasterizer/ios.d.ts +84 -0
  76. package/dist/rasterizer/ios.js +155 -0
  77. package/dist/rasterizer/native-bridge.d.ts +261 -0
  78. package/dist/rasterizer/native-bridge.js +413 -0
  79. package/dist/rasterizer/web/client.d.ts +84 -0
  80. package/dist/rasterizer/web/client.js +359 -0
  81. package/dist/rasterizer/web/engine.d.ts +69 -0
  82. package/dist/rasterizer/web/engine.js +308 -0
  83. package/dist/rasterizer/web/index.d.ts +26 -0
  84. package/dist/rasterizer/web/index.js +21 -0
  85. package/dist/rasterizer/web/pdfium.d.ts +213 -0
  86. package/dist/rasterizer/web/pdfium.js +170 -0
  87. package/dist/rasterizer/web/protocol.d.ts +138 -0
  88. package/dist/rasterizer/web/protocol.js +37 -0
  89. package/dist/rasterizer/web/session.d.ts +106 -0
  90. package/dist/rasterizer/web/session.js +413 -0
  91. package/dist/rasterizer/web/worker.d.ts +44 -0
  92. package/dist/rasterizer/web/worker.js +76 -0
  93. package/dist/react/PdfContentView.d.ts +98 -0
  94. package/dist/react/PdfContentView.js +189 -0
  95. package/dist/react/index.d.ts +6 -0
  96. package/dist/react/index.js +3 -0
  97. package/dist/react/usePdfDocument.d.ts +134 -0
  98. package/dist/react/usePdfDocument.js +275 -0
  99. package/dist/react/usePdfLayer.d.ts +94 -0
  100. package/dist/react/usePdfLayer.js +388 -0
  101. package/dist/skia/index.d.ts +26 -0
  102. package/dist/skia/index.js +25 -0
  103. package/dist/skia/index.native.d.ts +14 -0
  104. package/dist/skia/index.native.js +16 -0
  105. package/dist/skia/index.web.d.ts +12 -0
  106. package/dist/skia/index.web.js +14 -0
  107. package/dist/skia/resolve.native.d.ts +18 -0
  108. package/dist/skia/resolve.native.js +22 -0
  109. package/dist/skia/resolve.web.d.ts +41 -0
  110. package/dist/skia/resolve.web.js +62 -0
  111. package/dist/skia/skia-api.d.ts +94 -0
  112. package/dist/skia/skia-api.js +113 -0
  113. package/dist/testing/index.d.ts +27 -0
  114. package/dist/testing/index.js +28 -0
  115. package/dist/testing/scenes.d.ts +100 -0
  116. package/dist/testing/scenes.js +267 -0
  117. package/dist/types.d.ts +611 -0
  118. package/dist/types.js +21 -0
  119. package/dist/web-init.d.ts +68 -0
  120. package/dist/web-init.js +64 -0
  121. package/ios/Package.swift +58 -0
  122. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
  123. package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
  124. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
  125. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
  126. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
  127. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
  128. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
  129. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
  130. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
  131. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
  132. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
  133. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
  134. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
  135. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
  136. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
  137. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
  138. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
  139. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
  140. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
  141. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
  142. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
  143. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
  144. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
  145. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
  146. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
  147. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
  148. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
  149. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
  150. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
  151. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
  152. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
  153. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
  154. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
  155. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
  156. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
  157. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
  158. package/package.json +144 -0
  159. package/react-native.config.js +40 -0
  160. package/scripts/check-skia-peer.mjs +86 -0
  161. package/scripts/skia-peer-check.mjs +363 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The PDFium surface this package uses, and the adapter that produces it.
3
+ *
4
+ * ------------------------------------------------------------------------
5
+ * WHY A STRUCTURAL INTERFACE INSTEAD OF `@embedpdf/pdfium`'s OWN TYPE
6
+ * ------------------------------------------------------------------------
7
+ *
8
+ * Same discipline as `AndroidPdfNativeModule` in `../android.ts`, and for the
9
+ * same two reasons.
10
+ *
11
+ * 1. THE ENGINE STAYS PURE AND TESTABLE. `./engine.ts` takes a
12
+ * `PdfiumBinding` and never names the package, so the whole open/geometry/
13
+ * render/error path runs in vitest against the real WASM module — or
14
+ * against a stub — with no worker, no DOM and no bundler in the loop.
15
+ * 2. THE PUBLISHED TYPES CARRY NO PEER. `@embedpdf/pdfium` is an OPTIONAL peer
16
+ * dependency: a consumer who only ships native must not have their build
17
+ * break on a missing module referenced from our `.d.ts`. Nothing in this
18
+ * file imports it, not even as a type. `./worker.ts` is the one module that
19
+ * names it at all, and it exports nothing typed by it.
20
+ *
21
+ * The obvious risk of a hand-written structural type is that it drifts from the
22
+ * real module and nobody notices until runtime. It cannot here: `./worker.ts`
23
+ * calls `bindPdfium(await init(...))` with the genuine `WrappedPdfiumModule`,
24
+ * and that file IS type-checked by `tsc --noEmit`. A wrong arity or a renamed
25
+ * export fails the build.
26
+ *
27
+ * ------------------------------------------------------------------------
28
+ * EVERY CONSTANT AND SEMANTIC BELOW WAS MEASURED AGAINST 2.15.0, NOT READ
29
+ * ------------------------------------------------------------------------
30
+ *
31
+ * The notes that say "measured" mean a script that called the real WASM module
32
+ * under node and printed the answer. See each one.
33
+ */
34
+ import { PdfError } from '../../types.js';
35
+ /* ------------------------------------------------------------------ *
36
+ * Constants
37
+ * ------------------------------------------------------------------ */
38
+ /**
39
+ * `FPDFBitmap_BGRA`. The only four-channel format PDFium's public bitmap API
40
+ * offers, and the one every render path below creates.
41
+ *
42
+ * It describes the BUFFER, not the render: adding `FPDF_REVERSE_BYTE_ORDER` to
43
+ * the render flags leaves the bitmap tagged BGRA while the bytes come out RGBA.
44
+ * That is not a contradiction to fix — the flag is documented as acting on the
45
+ * composite, and it is why `renderPage` derives `PixelFormat` from the FLAGS it
46
+ * passed rather than from the bitmap format it asked for.
47
+ */
48
+ export const FPDF_BITMAP_BGRA = 4;
49
+ /** Draw PDF-embedded annotations. Answers `RasterRequest.annotations`. */
50
+ export const FPDF_ANNOT = 0x01;
51
+ /**
52
+ * Write RGBA instead of BGRA.
53
+ *
54
+ * MEASURED: with the flag, a `0.2 0.4 0.9 rg` fill reads `[51, 102, 230, 255]`;
55
+ * without it, `[230, 102, 51, 255]`. Same bitmap format, swapped R and B.
56
+ *
57
+ * THE WEB BACKEND ALWAYS PASSES IT, and that is a deliberate choice about the
58
+ * INGEST side rather than about PDFium. `imageFromPixels` maps `'bgra8888'` to
59
+ * `ColorType.BGRA_8888`, which CanvasKit exposes but which a GPU-backed build
60
+ * may still refuse from `MakeImage` — and the recovery for that is a full extra
61
+ * pass over the buffer swizzling R and B in JS. RGBA is the configuration every
62
+ * Skia backend accepts, PDFium produces it for free inside the composite it was
63
+ * already doing, and the JS-side fallback pass never runs.
64
+ */
65
+ export const FPDF_REVERSE_BYTE_ORDER = 0x10;
66
+ /**
67
+ * `FPDF_GetLastError()`'s codes.
68
+ *
69
+ * ONLY MEANINGFUL IMMEDIATELY AFTER A FAILED DOCUMENT LOAD. MEASURED: after
70
+ * loading an encrypted document without a password (which sets 4), a subsequent
71
+ * `FPDF_GetPageSizeByIndexF` for an out-of-range index returns false and leaves
72
+ * the error at 4 — the stale value from the earlier call. So this table is
73
+ * consulted in exactly one place, `mapLoadError`, and every other failure in
74
+ * this backend is decided by its own bounds check or return value.
75
+ */
76
+ export const PDFIUM_ERR = {
77
+ SUCCESS: 0,
78
+ UNKNOWN: 1,
79
+ FILE: 2,
80
+ FORMAT: 3,
81
+ PASSWORD: 4,
82
+ SECURITY: 5,
83
+ PAGE: 6,
84
+ };
85
+ /**
86
+ * Turn an initialised `@embedpdf/pdfium` module into a `PdfiumBinding`.
87
+ *
88
+ * Deliberately mechanical. The only judgement in here is `heapU8`, which reads
89
+ * `HEAPU8` off the module on EVERY call rather than closing over it — see
90
+ * `PdfiumHeap`.
91
+ */
92
+ export function bindPdfium(module) {
93
+ const { pdfium } = module;
94
+ return {
95
+ heapU8: () => pdfium.HEAPU8,
96
+ malloc: byteLength => pdfium.wasmExports.malloc(byteLength),
97
+ free: pointer => pdfium.wasmExports.free(pointer),
98
+ getFloat: pointer => Number(pdfium.getValue(pointer, 'float')),
99
+ FPDF_LoadMemDocument: (pointer, byteLength, password) => module.FPDF_LoadMemDocument(pointer, byteLength, password),
100
+ FPDF_CloseDocument: document => {
101
+ module.FPDF_CloseDocument(document);
102
+ },
103
+ FPDF_GetLastError: () => module.FPDF_GetLastError(),
104
+ FPDF_GetPageCount: document => module.FPDF_GetPageCount(document),
105
+ FPDF_GetPageSizeByIndexF: (document, index, sizeOut) => module.FPDF_GetPageSizeByIndexF(document, index, sizeOut),
106
+ EPDF_GetPageRotationByIndex: (document, index) => module.EPDF_GetPageRotationByIndex(document, index),
107
+ FPDF_LoadPage: (document, index) => module.FPDF_LoadPage(document, index),
108
+ FPDF_ClosePage: page => {
109
+ module.FPDF_ClosePage(page);
110
+ },
111
+ FPDFBitmap_CreateEx: (width, height, format, firstScan, stride) => module.FPDFBitmap_CreateEx(width, height, format, firstScan, stride),
112
+ FPDFBitmap_Destroy: bitmap => {
113
+ module.FPDFBitmap_Destroy(bitmap);
114
+ },
115
+ FPDFBitmap_FillRect: (bitmap, left, top, width, height, color) => module.FPDFBitmap_FillRect(bitmap, left, top, width, height, color),
116
+ FPDFBitmap_GetBuffer: bitmap => module.FPDFBitmap_GetBuffer(bitmap),
117
+ FPDFBitmap_GetStride: bitmap => module.FPDFBitmap_GetStride(bitmap),
118
+ FPDF_RenderPageBitmap: (bitmap, page, startX, startY, sizeX, sizeY, rotate, flags) => {
119
+ module.FPDF_RenderPageBitmap(bitmap, page, startX, startY, sizeX, sizeY, rotate, flags);
120
+ },
121
+ };
122
+ }
123
+ /* ------------------------------------------------------------------ *
124
+ * Errors
125
+ * ------------------------------------------------------------------ */
126
+ /**
127
+ * `FPDF_GetLastError()` after a failed load -> the frozen `PdfErrorCode` union.
128
+ *
129
+ * `passwordSupplied` is what tells the two password outcomes apart, and it has
130
+ * to: MEASURED, PDFium returns 4 both for an encrypted document opened with no
131
+ * password and for one opened with the WRONG password. There is no code that
132
+ * distinguishes them, so the caller's own knowledge does — and the distinction
133
+ * matters, because one of them means "prompt the user" and the other means
134
+ * "tell them they got it wrong".
135
+ */
136
+ export function mapLoadError(error, passwordSupplied) {
137
+ switch (error) {
138
+ case PDFIUM_ERR.FILE:
139
+ return 'not-found';
140
+ case PDFIUM_ERR.FORMAT:
141
+ // MEASURED: both a truncated document and an eight-byte non-PDF report
142
+ // this.
143
+ return 'corrupt';
144
+ case PDFIUM_ERR.PASSWORD:
145
+ return passwordSupplied ? 'password-incorrect' : 'password-required';
146
+ case PDFIUM_ERR.SECURITY:
147
+ // An encryption scheme this PDFium build cannot handle. Not corruption
148
+ // and not a wrong password — nothing the user can do about it.
149
+ return 'unsupported';
150
+ case PDFIUM_ERR.PAGE:
151
+ return 'corrupt';
152
+ default:
153
+ return 'backend-failure';
154
+ }
155
+ }
156
+ const ERROR_NAMES = {
157
+ [PDFIUM_ERR.SUCCESS]: 'SUCCESS',
158
+ [PDFIUM_ERR.UNKNOWN]: 'UNKNOWN',
159
+ [PDFIUM_ERR.FILE]: 'FILE',
160
+ [PDFIUM_ERR.FORMAT]: 'FORMAT',
161
+ [PDFIUM_ERR.PASSWORD]: 'PASSWORD',
162
+ [PDFIUM_ERR.SECURITY]: 'SECURITY',
163
+ [PDFIUM_ERR.PAGE]: 'PAGE',
164
+ };
165
+ /** The load failure, as the error a host can branch on. */
166
+ export function loadError(error, passwordSupplied) {
167
+ const name = ERROR_NAMES[error] ?? String(error);
168
+ return new PdfError(mapLoadError(error, passwordSupplied), `PDFium could not open the document (FPDF_GetLastError = ${error} ` +
169
+ `${name}${passwordSupplied ? ', a password was supplied' : ''}).`);
170
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The wire between the main thread and the PDFium worker.
3
+ *
4
+ * PURE TYPES AND ONE PREDICATE. Both sides import this file, which is what stops
5
+ * the two halves of a message from being written twice and drifting — the exact
6
+ * failure mode the Android backend avoids by making `PdfErrorCode`'s members the
7
+ * wire format on the Java side too.
8
+ *
9
+ * EVERYTHING HERE IS STRUCTURED-CLONEABLE. `DocRect`, `PageGeometry` and
10
+ * `RasterRequest` are plain data by construction, and the one binary payload is
11
+ * an `ArrayBuffer` that is TRANSFERRED rather than copied — see
12
+ * `RenderedResponse.bytes`.
13
+ *
14
+ * ------------------------------------------------------------------------
15
+ * WHY THE WASM SOURCE TRAVELS IN THE `open` MESSAGE
16
+ * ------------------------------------------------------------------------
17
+ *
18
+ * Because a worker is created per document (see `./client.ts`), so the first
19
+ * message it ever receives is `open` and there is nothing for a separate
20
+ * `init` message to be ordered against. Folding it in removes a state — "worker
21
+ * exists but has not been told where its wasm is" — that could otherwise be
22
+ * observed by a `render` arriving first.
23
+ */
24
+ import type { AlphaEncoding, PageGeometry, PdfErrorCode, PixelFormat, RasterRequest } from '../../types.js';
25
+ /**
26
+ * Where the worker gets `pdfium.wasm`.
27
+ *
28
+ * TWO FORMS, AND BOTH ARE THE HOST'S TO SUPPLY — this package hardcodes no URL,
29
+ * for the same reason `./web-init.ts` hardcodes no CanvasKit URL: the version
30
+ * that has to match is the one the host's tree installed, and a baked-in CDN
31
+ * path either fights that pin or silently serves a different wasm than the JS
32
+ * bindings were built against.
33
+ *
34
+ * - `{url}` — handed to Emscripten's `locateFile`, so it can stream-compile.
35
+ * The normal case: `import wasmUrl from '@embedpdf/pdfium/pdfium.wasm?url'`,
36
+ * and the one to prefer: the browser's HTTP cache serves the second document's
37
+ * worker from memory, so a session pays for the download once.
38
+ * - `{binary}` — already-fetched bytes. What the node tests use (read off disk),
39
+ * and what a host with its own asset pipeline or an offline bundle wants. It
40
+ * is COPIED into each worker rather than transferred — transferring would
41
+ * neuter the host's buffer and the second document would find it empty — so a
42
+ * 4.6 MB structured clone is paid per `open()`.
43
+ */
44
+ export type PdfiumWasmSource = {
45
+ url: string;
46
+ } | {
47
+ binary: ArrayBuffer;
48
+ };
49
+ /** The document, as it crosses to the worker. `{uri}` is fetched THERE. */
50
+ export type PdfiumDocumentSource = {
51
+ url: string;
52
+ } | {
53
+ bytes: ArrayBuffer;
54
+ };
55
+ export interface OpenRequest {
56
+ kind: 'open';
57
+ id: number;
58
+ wasm: PdfiumWasmSource;
59
+ source: PdfiumDocumentSource;
60
+ password?: string;
61
+ }
62
+ export interface RenderRequest {
63
+ kind: 'render';
64
+ id: number;
65
+ request: RasterRequest;
66
+ }
67
+ /**
68
+ * Drop a render that has not started yet.
69
+ *
70
+ * BEST EFFORT AND HONEST ABOUT IT. The worker renders synchronously, so a
71
+ * `cancel` can only ever be delivered BETWEEN renders — never into one. What it
72
+ * genuinely does is stop a QUEUED render from starting, which is why the worker
73
+ * yields to its event loop between jobs (see `./session.ts`): without that
74
+ * yield, messages queued behind a burst of renders would not be read until the
75
+ * burst had finished and this message would be worthless.
76
+ */
77
+ export interface CancelRequest {
78
+ kind: 'cancel';
79
+ id: number;
80
+ }
81
+ /**
82
+ * Close the document and stop.
83
+ *
84
+ * REDUNDANT IN A REAL WORKER, and kept anyway. `RasterizerHandle.close()`
85
+ * terminates the worker, which reclaims the entire WASM heap in one step — the
86
+ * whole reason a worker is created per document. This message will usually not
87
+ * even be read before that happens.
88
+ *
89
+ * It earns its place on the other side: the in-process worker the tests drive
90
+ * has no `terminate()` that can free an Emscripten heap, so this is what
91
+ * exercises `PdfiumDocument.close()` — the `FPDF_CloseDocument`-then-`free`
92
+ * ordering that a use-after-free in the WASM heap depends on — under vitest.
93
+ */
94
+ export interface CloseRequest {
95
+ kind: 'close';
96
+ }
97
+ export type WorkerRequest = OpenRequest | RenderRequest | CancelRequest | CloseRequest;
98
+ export interface OpenedResponse {
99
+ kind: 'opened';
100
+ id: number;
101
+ pageCount: number;
102
+ pages: PageGeometry[];
103
+ }
104
+ export interface RenderedResponse {
105
+ kind: 'rendered';
106
+ id: number;
107
+ /** TRANSFERRED. Neutered on the worker side the moment it is posted. */
108
+ bytes: ArrayBuffer;
109
+ width: number;
110
+ height: number;
111
+ rowBytes: number;
112
+ format: PixelFormat;
113
+ /**
114
+ * Carried on the wire rather than restated on the main thread, for the same
115
+ * reason `RasterPixels` carries it: the declaration belongs next to the bytes
116
+ * it describes. It is constant for this backend today; a second render path
117
+ * (a flattened export, a different bitmap format) would make it not be, and
118
+ * the wire would already say so.
119
+ */
120
+ alpha: AlphaEncoding;
121
+ }
122
+ export interface ErrorResponse {
123
+ kind: 'error';
124
+ /** Null for a failure that belongs to no single request. */
125
+ id: number | null;
126
+ code: PdfErrorCode;
127
+ message: string;
128
+ }
129
+ export type WorkerResponse = OpenedResponse | RenderedResponse | ErrorResponse;
130
+ /**
131
+ * Is this a message from our worker?
132
+ *
133
+ * A worker's `message` event is not a private channel — anything with a handle
134
+ * to it can post — and the payload arrives as `unknown`. Narrowing here rather
135
+ * than casting at the call site means a malformed message becomes a rejected
136
+ * promise with a name on it, not a `TypeError` deep in the connection.
137
+ */
138
+ export declare function isWorkerResponse(value: unknown): value is WorkerResponse;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The wire between the main thread and the PDFium worker.
3
+ *
4
+ * PURE TYPES AND ONE PREDICATE. Both sides import this file, which is what stops
5
+ * the two halves of a message from being written twice and drifting — the exact
6
+ * failure mode the Android backend avoids by making `PdfErrorCode`'s members the
7
+ * wire format on the Java side too.
8
+ *
9
+ * EVERYTHING HERE IS STRUCTURED-CLONEABLE. `DocRect`, `PageGeometry` and
10
+ * `RasterRequest` are plain data by construction, and the one binary payload is
11
+ * an `ArrayBuffer` that is TRANSFERRED rather than copied — see
12
+ * `RenderedResponse.bytes`.
13
+ *
14
+ * ------------------------------------------------------------------------
15
+ * WHY THE WASM SOURCE TRAVELS IN THE `open` MESSAGE
16
+ * ------------------------------------------------------------------------
17
+ *
18
+ * Because a worker is created per document (see `./client.ts`), so the first
19
+ * message it ever receives is `open` and there is nothing for a separate
20
+ * `init` message to be ordered against. Folding it in removes a state — "worker
21
+ * exists but has not been told where its wasm is" — that could otherwise be
22
+ * observed by a `render` arriving first.
23
+ */
24
+ /**
25
+ * Is this a message from our worker?
26
+ *
27
+ * A worker's `message` event is not a private channel — anything with a handle
28
+ * to it can post — and the payload arrives as `unknown`. Narrowing here rather
29
+ * than casting at the call site means a malformed message becomes a rejected
30
+ * promise with a name on it, not a `TypeError` deep in the connection.
31
+ */
32
+ export function isWorkerResponse(value) {
33
+ if (typeof value !== 'object' || value === null)
34
+ return false;
35
+ const kind = value.kind;
36
+ return kind === 'opened' || kind === 'rendered' || kind === 'error';
37
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The worker's own logic, with `self` factored out.
3
+ *
4
+ * `./worker.ts` is nothing but the four lines that bind this to a real
5
+ * `DedicatedWorkerGlobalScope`; everything a bug could live in — the queue, the
6
+ * cancellation window, the fetch, the error mapping, the open-before-render
7
+ * ordering — is here, takes its dependencies as arguments, and runs in vitest
8
+ * under plain node against the REAL PDFium module.
9
+ *
10
+ * ------------------------------------------------------------------------
11
+ * THE YIELD IS THE CANCELLATION MECHANISM
12
+ * ------------------------------------------------------------------------
13
+ *
14
+ * A worker processes messages one at a time and `FPDF_RenderPageBitmap` is
15
+ * synchronous, so nothing can interrupt a render that has started. What CAN be
16
+ * stopped is a render that has not started — and only if the `cancel` message
17
+ * gets read first. Handling each `render` message by rendering inside the
18
+ * handler would make that impossible: messages are FIFO, so three queued renders
19
+ * would all complete before a `cancel` posted after them was ever looked at.
20
+ *
21
+ * So renders are QUEUED here and drained one per macrotask, with a yield between
22
+ * them. That yield is the only window in which a `cancel` can land, and it is
23
+ * exactly what `RasterizerCapabilities.interruptibleRender: false` means in
24
+ * practice: cancellation happens between requests, never inside one.
25
+ *
26
+ * See `macrotaskYield` below for what that yield is actually made of, and why it
27
+ * is not the `setTimeout(0)` it used to be.
28
+ */
29
+ import type { PdfiumBinding } from './pdfium.js';
30
+ import type { PdfiumWasmSource, WorkerRequest, WorkerResponse } from './protocol.js';
31
+ /**
32
+ * The slice of `fetch` this file uses.
33
+ *
34
+ * Declared rather than imported: this package compiles with `lib: ["ES2022"]`
35
+ * and no DOM, deliberately — the pure core must keep loading with neither
36
+ * `react-native` nor a browser present, and a `lib: ["DOM"]` would let a DOM
37
+ * global be referenced anywhere in the package without anyone noticing.
38
+ */
39
+ export interface FetchResponseLike {
40
+ ok: boolean;
41
+ status: number;
42
+ statusText: string;
43
+ arrayBuffer(): Promise<ArrayBuffer>;
44
+ }
45
+ export type FetchLike = (url: string) => Promise<FetchResponseLike>;
46
+ export interface WorkerSessionDeps {
47
+ /** Post one response, transferring any binary payload rather than copying. */
48
+ post(response: WorkerResponse, transfer?: readonly ArrayBuffer[]): void;
49
+ /**
50
+ * Initialise PDFium and hand back the binding. Called at most ONCE, on the
51
+ * `open` message, so a worker that is created and then discarded never
52
+ * compiles the wasm at all.
53
+ */
54
+ loadPdfium(wasm: PdfiumWasmSource): Promise<PdfiumBinding>;
55
+ /** Defaults to `globalThis.fetch`. Injected by the tests. */
56
+ fetch?: FetchLike;
57
+ /**
58
+ * Defaults to `macrotaskYield`. See the header — this is the cancel window.
59
+ *
60
+ * Kept injectable for the tests that need to COUNT yields or drive the drain
61
+ * loop deterministically. Nothing on the shipped path passes it, so the
62
+ * default is what every browser actually runs and what the abort tests in
63
+ * `web-rasterizer.test.ts` exercise.
64
+ */
65
+ yieldToEventLoop?: () => Promise<void>;
66
+ }
67
+ export interface WorkerSession {
68
+ handle(request: WorkerRequest): void;
69
+ /** Close the document and refuse everything after. Idempotent. */
70
+ dispose(): void;
71
+ }
72
+ /**
73
+ * THE CANCEL WINDOW: a port hop, and then a timer turn.
74
+ *
75
+ * WHY A PORT HOP. It is a macrotask that no clamp applies to. Same Chrome
76
+ * worker, same 24 consecutive yields as `timerYield`'s 90.0 ms: 0.2 ms total,
77
+ * mean 0.008 ms.
78
+ *
79
+ * WHY A TIMER TURN AS WELL. Because a port hop ALONE does not drain the timer
80
+ * queue, and the timer queue is where the `cancel` is. Measured on node v24.14.0
81
+ * with K `setTimeout(0)` callbacks already pending when the yield starts, 20
82
+ * trials at each K in {1, 2, 4, 8, 16, 32}: a timer yield runs ALL K of them
83
+ * before it resolves, in every trial; a bare port hop runs ZERO of them, in
84
+ * every trial. A port-only yield would have narrowed the very window this file
85
+ * exists to keep open, and would have done it quietly — the abort test in
86
+ * `web-rasterizer.test.ts` delivers its cancels over a `setTimeout`-backed
87
+ * channel, so a bare hop turns that test flaky rather than loudly wrong.
88
+ *
89
+ * AND THE TIMER TURN IS FREE, which is the whole reason this shape works: the
90
+ * hop RESETS the nesting level Chrome clamps on, because the timer is always
91
+ * scheduled from a port-message task and never from another timer task.
92
+ * Measured in the same worker over the same 24 yields: 0.1-0.3 ms total, mean
93
+ * 0.004-0.013 ms — indistinguishable from the bare hop, and ~280x cheaper than
94
+ * the bare timer it replaces.
95
+ *
96
+ * So this is STRICTLY STRONGER than what it replaces — a hop AND a full timer
97
+ * turn, where there was only a timer turn — at 1/280th the cost, and its worst
98
+ * case (a runtime that clamps the following timer anyway) is exactly the
99
+ * behaviour that shipped before it.
100
+ *
101
+ * Exported for `web-rasterizer.test.ts`, which asserts the drained-timer
102
+ * property above directly. Still not public: nothing in the package's `exports`
103
+ * map reaches `session.ts` except `./worker`, which re-exports nothing.
104
+ */
105
+ export declare const macrotaskYield: () => Promise<void>;
106
+ export declare function createWorkerSession(deps: WorkerSessionDeps): WorkerSession;