@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,359 @@
1
+ /**
2
+ * The main-thread half of the web backend: a `PageRasterizer` over a worker.
3
+ *
4
+ * ------------------------------------------------------------------------
5
+ * THE CONTRACT WITH THE HOST — a worker FACTORY, not a URL
6
+ * ------------------------------------------------------------------------
7
+ *
8
+ * This package never constructs a `Worker` from a path of its own. Not from a
9
+ * hardcoded URL, and not from `new URL('./worker.js', import.meta.url)` — that
10
+ * form works in Vite and in a native ESM browser load, and does not survive
11
+ * every bundler, every `base`, or an SSR pass where `import.meta.url` is a file
12
+ * path on the server. A library that guesses wrong there fails at RUNTIME in the
13
+ * host's production build, which is the worst place to find out.
14
+ *
15
+ * So the host says how, once:
16
+ *
17
+ * // Vite / Rollup — the form the spike proved on Vite 5.1.0
18
+ * import PdfCanvasWorker from
19
+ * '@reekon-tools/react-native-pdf-canvas/worker?worker';
20
+ * import wasmUrl from '@embedpdf/pdfium/pdfium.wasm?url';
21
+ *
22
+ * configureWebPdfium({
23
+ * createWorker: () => new PdfCanvasWorker(),
24
+ * wasm: {url: wasmUrl},
25
+ * });
26
+ *
27
+ * // Anything else — webpack 5, esbuild, a plain module script
28
+ * createWorker: () =>
29
+ * new Worker(new URL('.../worker.js', import.meta.url), {type: 'module'}),
30
+ *
31
+ * `type: 'module'` is required: the worker entry is ESM and statically imports
32
+ * `@embedpdf/pdfium`. The wasm locator is the host's for the same reason
33
+ * `./web-init.ts`'s CanvasKit locator is — see `PdfiumWasmSource`.
34
+ *
35
+ * ------------------------------------------------------------------------
36
+ * ONE WORKER PER DOCUMENT, TERMINATED ON `close()`
37
+ * ------------------------------------------------------------------------
38
+ *
39
+ * Not one worker per rasterizer, and this is a memory decision rather than a
40
+ * concurrency one. The Emscripten heap GROWS ONE WAY — it never returns pages to
41
+ * the OS — so on web the peak footprint of a session is permanent for the rest
42
+ * of the tab's life. MEASURED: 24 consecutive 512² tiles and 24 consecutive
43
+ * 1024² tiles leave the heap flat at 17.75 MiB, while ONE whole-page 2448x3168
44
+ * render takes it to 32.625 MiB and keeps it there.
45
+ *
46
+ * Terminating the worker is the only way to give that back. So a document's
47
+ * entire PDFium footprint — the parsed document, the page cache, the bitmap high
48
+ * water mark — dies with `close()`, and the cost is one wasm instantiation per
49
+ * document, MEASURED at 19 ms cold and 10 ms warm under node.
50
+ */
51
+ import { isTightBuffer } from '../buffer.js';
52
+ import { isWorkerResponse } from './protocol.js';
53
+ import { monotonicNow } from '../../clock.js';
54
+ import { PdfError } from '../../types.js';
55
+ /**
56
+ * WHAT THIS BACKEND ACTUALLY DOES, and nothing more.
57
+ *
58
+ * `annotations: true` is the one that has to be right rather than optimistic:
59
+ * `usePdfDocument` silently DOWNGRADES a request for annotations when the
60
+ * capability is false, so a lie here does not throw, it draws a different
61
+ * picture. PDFium draws PDF-embedded annotations when `FPDF_ANNOT` is in the
62
+ * render flags and omits them when it is not, and `engine.ts` passes the flag
63
+ * from `RasterRequest.annotations` — so unlike Android, which can only ever draw
64
+ * them, this backend genuinely honours both settings.
65
+ *
66
+ * `text` / `search` / `links` are false because none of them is implemented.
67
+ * PDFium can do all three (`FPDFText_*`, `FPDFLink_*`) and the seam has no place
68
+ * to expose them yet; declaring them true would be claiming an API that does not
69
+ * exist.
70
+ *
71
+ * `interruptibleRender: false`, honestly. `FPDF_RenderPageBitmap` is synchronous
72
+ * and a worker reads one message at a time, so nothing can interrupt a render
73
+ * once it has started. What an abort does do is stop a QUEUED render from
74
+ * starting and free the caller immediately — see `render` below. PDFium's
75
+ * progressive API (`FPDF_RenderPageBitmap_Start` / `FPDF_RenderPage_Continue`
76
+ * with an `IFSDK_PAUSE`) is the mechanism that would make this true, and is not
77
+ * used; whoever wires it up should flip this flag in the same commit.
78
+ *
79
+ * `maxConcurrentRenders: 1` — one worker, one PDFium, one thread. There is
80
+ * nothing for a second lane to overlap with.
81
+ *
82
+ * `transport` is deliberately ABSENT. The union — `base64`, `jsi-arraybuffer`,
83
+ * `direct` — has no member for a structured-clone transfer out of a worker, and
84
+ * the field is purely observational; its own docs say a backend that omits it is
85
+ * making no claim. Inventing a member on a frozen public union to report
86
+ * something nothing branches on is not a trade worth making.
87
+ */
88
+ const CAPABILITIES = {
89
+ annotations: true,
90
+ interruptibleRender: false,
91
+ text: false,
92
+ search: false,
93
+ links: false,
94
+ maxConcurrentRenders: 1,
95
+ };
96
+ /**
97
+ * Request/response over one worker, keyed by an id this side allocates.
98
+ *
99
+ * The worker never initiates: every message it sends answers exactly one id, so
100
+ * there is no subscription model and no ordering requirement. That is what makes
101
+ * an abandoned id (an aborted render) safe to simply forget — the reply arrives,
102
+ * finds no entry, and is dropped.
103
+ */
104
+ function createConnection(worker) {
105
+ const pending = new Map();
106
+ let nextId = 1;
107
+ let dead = null;
108
+ const failAll = (error) => {
109
+ dead = error;
110
+ for (const entry of pending.values())
111
+ entry.reject(error);
112
+ pending.clear();
113
+ };
114
+ const onMessage = (event) => {
115
+ const data = event?.data;
116
+ if (!isWorkerResponse(data)) {
117
+ // Not ours. A worker's message channel is not private, and a message we
118
+ // cannot parse must not be turned into a rejected render belonging to
119
+ // somebody else's id.
120
+ return;
121
+ }
122
+ if (data.kind === 'error' && data.id === null) {
123
+ failAll(new PdfError(data.code, data.message));
124
+ return;
125
+ }
126
+ const id = data.id;
127
+ if (id === null)
128
+ return;
129
+ const entry = pending.get(id);
130
+ if (entry === undefined)
131
+ return;
132
+ pending.delete(id);
133
+ if (data.kind === 'error') {
134
+ entry.reject(new PdfError(data.code, data.message));
135
+ }
136
+ else {
137
+ entry.resolve(data);
138
+ }
139
+ };
140
+ /**
141
+ * A worker that fails to LOAD reports it here and nowhere else — no request
142
+ * of ours ever gets a reply. Without this the first render would hang for
143
+ * ever, which is the single most confusing way for a bundler misconfiguration
144
+ * to present.
145
+ */
146
+ const onError = (event) => {
147
+ const message = event?.message ?? 'unknown worker error';
148
+ failAll(new PdfError('backend-failure', `The PDF worker failed: ${String(message)}. If this is the first ` +
149
+ 'render, check that `createWorker` builds a MODULE worker ' +
150
+ "(`{type: 'module'}`) and that `@embedpdf/pdfium` resolves inside it."));
151
+ };
152
+ worker.addEventListener('message', onMessage);
153
+ worker.addEventListener('error', onError);
154
+ worker.addEventListener('messageerror', onError);
155
+ return {
156
+ nextId: () => nextId++,
157
+ /** Post and await the reply for `id`. The id is the caller's so it can abandon it. */
158
+ request(id, message, transfer) {
159
+ if (dead !== null)
160
+ return Promise.reject(dead);
161
+ return new Promise((resolve, reject) => {
162
+ pending.set(id, { resolve, reject });
163
+ worker.postMessage(message, transfer);
164
+ });
165
+ },
166
+ post(message) {
167
+ if (dead === null)
168
+ worker.postMessage(message);
169
+ },
170
+ /** Forget an id without failing it. Used when a render is aborted. */
171
+ abandon(id) {
172
+ pending.delete(id);
173
+ },
174
+ close(reason) {
175
+ worker.removeEventListener('message', onMessage);
176
+ worker.removeEventListener('error', onError);
177
+ worker.removeEventListener('messageerror', onError);
178
+ failAll(reason);
179
+ worker.terminate();
180
+ },
181
+ };
182
+ }
183
+ /* ------------------------------------------------------------------ *
184
+ * The rasterizer
185
+ * ------------------------------------------------------------------ */
186
+ /**
187
+ * The document bytes, as something safe to TRANSFER.
188
+ *
189
+ * Always a copy, and never the host's own buffer. `postMessage` with a transfer
190
+ * list NEUTERS the source — the caller's `Uint8Array` would silently become
191
+ * zero-length — and `PdfSource.data` belongs to the caller, who is entitled to
192
+ * still have it afterwards (a retry, a second rasterizer, their own cache).
193
+ * `slice()` also fixes the subtler half: a `Uint8Array` that is a VIEW into a
194
+ * larger buffer would otherwise send the whole underlying allocation and PDFium
195
+ * would parse from the wrong offset.
196
+ */
197
+ const detachedCopy = (data) => data.slice().buffer;
198
+ export function createWebRasterizer(options) {
199
+ const id = options.id ?? 'web-pdfium';
200
+ return {
201
+ id,
202
+ capabilities: CAPABILITIES,
203
+ async open(source) {
204
+ const worker = options.createWorker();
205
+ const connection = createConnection(worker);
206
+ const openId = connection.nextId();
207
+ let wire;
208
+ let transfer;
209
+ if ('uri' in source) {
210
+ wire = {
211
+ kind: 'open',
212
+ id: openId,
213
+ wasm: options.wasm,
214
+ // FETCHED IN THE WORKER, not here. Same CORS rules either way, one
215
+ // fewer copy across the boundary, and the main thread never holds a
216
+ // whole document. rock-desktop's Firebase `getDownloadURL` tokens
217
+ // work unchanged.
218
+ source: { url: source.uri },
219
+ password: source.password,
220
+ };
221
+ }
222
+ else {
223
+ const bytes = detachedCopy(source.data);
224
+ wire = {
225
+ kind: 'open',
226
+ id: openId,
227
+ wasm: options.wasm,
228
+ source: { bytes },
229
+ password: source.password,
230
+ };
231
+ // Ours to give away — `detachedCopy` made it for this.
232
+ transfer = [bytes];
233
+ }
234
+ let opened;
235
+ try {
236
+ opened = await connection.request(openId, wire, transfer);
237
+ }
238
+ catch (error) {
239
+ connection.close(error instanceof PdfError
240
+ ? error
241
+ : new PdfError('backend-failure', String(error)));
242
+ throw error;
243
+ }
244
+ if (opened.kind !== 'opened') {
245
+ const failure = new PdfError('backend-failure', `The worker answered an open with a ${opened.kind} message.`);
246
+ connection.close(failure);
247
+ throw failure;
248
+ }
249
+ return createHandle(connection, id, opened.pageCount, opened.pages);
250
+ },
251
+ };
252
+ }
253
+ function createHandle(connection, backendId, pageCount, pages) {
254
+ let closed = false;
255
+ const requirePage = (index) => {
256
+ const geometry = pages[index];
257
+ if (geometry === undefined) {
258
+ throw new PdfError('not-found', `Document has ${pages.length} pages; asked for ${index}.`);
259
+ }
260
+ return geometry;
261
+ };
262
+ return {
263
+ pageCount,
264
+ /**
265
+ * SYNCHRONOUS, and answered entirely from what `open()` already read. No
266
+ * message, no page load — see `readGeometry` in `./engine.ts`, which reads
267
+ * every page's post-rotation size out of the page dictionary at open.
268
+ */
269
+ pageGeometry: requirePage,
270
+ async render(request, signal) {
271
+ if (closed) {
272
+ throw new PdfError('backend-failure', `${backendId} handle is closed.`);
273
+ }
274
+ requirePage(request.page);
275
+ if (signal?.aborted === true) {
276
+ throw new PdfError('cancelled', 'Render was aborted before it started.');
277
+ }
278
+ const id = connection.nextId();
279
+ const startedAt = monotonicNow();
280
+ /**
281
+ * ABORT DOES TWO THINGS, AND ONLY THE FIRST IS GUARANTEED.
282
+ *
283
+ * It frees the CALLER immediately — the promise rejects `cancelled` and
284
+ * the controller stops waiting on a tile it no longer wants. And it posts
285
+ * a `cancel`, which the worker honours if the render has not started yet
286
+ * (see the yield in `./session.ts`) and cannot honour if it has. The id is
287
+ * abandoned either way, so a raster that does arrive is dropped rather
288
+ * than resolving a promise nobody holds.
289
+ *
290
+ * This is `interruptibleRender: false` stated as behaviour: cancellation
291
+ * happens between requests.
292
+ */
293
+ let abort;
294
+ const cancelled = new Promise((_, reject) => {
295
+ abort = () => {
296
+ connection.abandon(id);
297
+ connection.post({ kind: 'cancel', id });
298
+ reject(new PdfError('cancelled', 'Render was aborted.'));
299
+ };
300
+ signal?.addEventListener('abort', abort, { once: true });
301
+ });
302
+ // Nothing awaits this branch when the render wins, and an unhandled
303
+ // rejection on a promise that lost a race is noise in every console.
304
+ cancelled.catch(() => { });
305
+ let response;
306
+ try {
307
+ response = await Promise.race([
308
+ connection.request(id, { kind: 'render', id, request }),
309
+ cancelled,
310
+ ]);
311
+ }
312
+ finally {
313
+ if (abort !== undefined)
314
+ signal?.removeEventListener('abort', abort);
315
+ }
316
+ if (response.kind !== 'rendered') {
317
+ throw new PdfError('backend-failure', `The worker answered a render with a ${response.kind} message.`);
318
+ }
319
+ const bridgeEndedAt = monotonicNow();
320
+ const bytes = new Uint8Array(response.bytes);
321
+ return {
322
+ bytes,
323
+ width: response.width,
324
+ height: response.height,
325
+ // From the worker, which read `FPDFBitmap_GetStride`. Never recomputed
326
+ // as width * 4 — a wrong stride shears the image instead of failing.
327
+ rowBytes: response.rowBytes,
328
+ format: response.format,
329
+ alpha: response.alpha,
330
+ timings: {
331
+ // The whole round trip, INCLUDING PDFium's work at the far end. There
332
+ // is no second clock here: the worker's own render time is not
333
+ // reported, because splitting it would need a clock the two threads
334
+ // share and nothing reads the difference.
335
+ bridgeMs: bridgeEndedAt - startedAt,
336
+ // Structurally zero. The payload crossed as a transferred
337
+ // ArrayBuffer, so there is no string, no decode and nothing to count
338
+ // — which is the headline, not an omission.
339
+ decodeMs: 0,
340
+ payloadChars: 0,
341
+ // Measured rather than asserted, though it is true by construction:
342
+ // the worker `slice()`s the raster out of the WASM heap into its own
343
+ // exactly-sized buffer, and a transfer preserves that.
344
+ tight: isTightBuffer(bytes),
345
+ },
346
+ };
347
+ },
348
+ close() {
349
+ if (closed)
350
+ return;
351
+ closed = true;
352
+ // The `close` message is posted first so an in-process worker can free
353
+ // the document properly; a real one is terminated before it reads it, and
354
+ // terminating is what actually reclaims the WASM heap. See the header.
355
+ connection.post({ kind: 'close' });
356
+ connection.close(new PdfError('cancelled', 'The document was closed mid-render.'));
357
+ },
358
+ };
359
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The PDFium half of the web backend: open a document, measure its pages,
3
+ * render an arbitrary doc-space rect.
4
+ *
5
+ * PURE IN THE SAME SENSE `../android.ts` IS. It takes a `PdfiumBinding` and
6
+ * imports nothing else — no worker, no DOM, no `@embedpdf/pdfium`. Which means
7
+ * the interesting half of this backend (rect registration, stride, the
8
+ * background fill, `/Rotate`, the error codes) is exercised in vitest under
9
+ * plain node against the REAL WASM module, not against a mock of it. See
10
+ * `__tests__/web-rasterizer.test.ts`.
11
+ *
12
+ * ------------------------------------------------------------------------
13
+ * THE RECT MECHANISM — negative offsets, not a matrix
14
+ * ------------------------------------------------------------------------
15
+ *
16
+ * PDFium's public way to draw part of a page is to describe the WHOLE page at
17
+ * the target scale and then slide it: `FPDF_RenderPageBitmap(bitmap, page,
18
+ * -x*s, -y*s, pageW*s, pageH*s, 0, flags)` into a bitmap that is only the tile's
19
+ * size. Everything outside the bitmap is clipped away by the bitmap itself.
20
+ *
21
+ * The alternative, `FPDF_RenderPageBitmapWithMatrix` with
22
+ * `FS_MATRIX{a:s, b:0, c:0, d:s, e:-x*s, f:-y*s}` and an `FS_RECTF(0,0,w,h)`
23
+ * clip, was MEASURED to be byte-identical to this on every rect tested —
24
+ * including a rect whose device offset is fractional (x*s = 33.6999…), where the
25
+ * two might plausibly have rounded differently and do not. The matrix form also
26
+ * has a trap this one does not: passing a NULL clip renders NOTHING AT ALL
27
+ * (measured: an all-white bitmap, no error). The simpler mechanism with no null
28
+ * case is the one that ships.
29
+ *
30
+ * ------------------------------------------------------------------------
31
+ * WHY THE OFFSET IS `-round(x*s)` AND NOT `round(-x*s)`
32
+ * ------------------------------------------------------------------------
33
+ *
34
+ * Because the tile's origin inside a hypothetical full-page render is
35
+ * `round(x*s)`, and the registration property is that the tile equals that
36
+ * region exactly. Negating the rounded origin makes those two the same integer
37
+ * by construction. `Math.round(-0.5)` is `-0` while `-Math.round(0.5)` is `-1`,
38
+ * so the two spellings genuinely differ on half-pixel boundaries — which is
39
+ * exactly where a misregistration would be least likely to be noticed and most
40
+ * likely to be real.
41
+ */
42
+ import type { PdfiumBinding } from './pdfium.js';
43
+ import type { AlphaEncoding, PageGeometry, PixelFormat, RasterRequest } from '../../types.js';
44
+ export interface PdfiumRasterBytes {
45
+ /** Tight: its own exactly-sized ArrayBuffer, ready to transfer. */
46
+ bytes: Uint8Array;
47
+ width: number;
48
+ height: number;
49
+ rowBytes: number;
50
+ format: PixelFormat;
51
+ alpha: AlphaEncoding;
52
+ }
53
+ export interface PdfiumDocument {
54
+ readonly pageCount: number;
55
+ /** Every page, measured once at open. */
56
+ readonly pages: readonly PageGeometry[];
57
+ render(request: RasterRequest): PdfiumRasterBytes;
58
+ close(): void;
59
+ }
60
+ /**
61
+ * Open a document from bytes.
62
+ *
63
+ * THE BYTES ARE COPIED INTO THE WASM HEAP AND STAY THERE. `FPDF_LoadMemDocument`
64
+ * does not take ownership and does not copy: PDFium reads the buffer lazily for
65
+ * the whole life of the document, so freeing it early is a use-after-free inside
66
+ * the WASM heap — which does not crash, it produces wrong pixels. `close()`
67
+ * frees it, after `FPDF_CloseDocument` and never before.
68
+ */
69
+ export declare function openPdfiumDocument(api: PdfiumBinding, bytes: Uint8Array, password?: string): PdfiumDocument;