@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,413 @@
1
+ /**
2
+ * The part of a platform backend that is not platform-specific.
3
+ *
4
+ * TWO NATIVE BACKENDS, ONE WIRE FORMAT. `android.ts` and `ios.ts` are both thin
5
+ * over this file: they choose an `id`, a `RasterizerCapabilities`, an
6
+ * `AlphaEncoding` and how a `PdfSource` becomes a native source, and everything
7
+ * else — error mapping, page-geometry marshalling, the transport negotiation, the
8
+ * base64 decode, the cancellation wiring, the phase timings — is here and is
9
+ * identical. That is not tidiness: the spec's whole claim for the iOS step is
10
+ * "the seam is a seam and not an Android-shaped hole", and the cheapest way to
11
+ * be held to it is for the second backend to be unable to drift.
12
+ *
13
+ * PURE ON PURPOSE, exactly as `android.ts` was before it was split. Nothing here
14
+ * imports `react-native`, which is what lets the whole adapter be tested in
15
+ * vitest against a fake native module. `./index.native.ts` is the only file that
16
+ * resolves a real one.
17
+ *
18
+ * WHY THE WIRE FORMAT IS SHARED RATHER THAN MERELY SIMILAR. The two native
19
+ * modules are written independently — `android/src/.../rn/PdfCanvasModule.java`
20
+ * and `ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm` — so the one thing that
21
+ * can silently diverge is the shape of the maps they resolve with. Sharing this
22
+ * file makes a divergence a compile error on one side or a failing marshalling
23
+ * test on the other, instead of a wrong image on one platform.
24
+ *
25
+ * `globalThis` is the only ambient thing this file reads, and it is universal —
26
+ * the `__pdfCanvasTakePixels` lookup finds nothing under vitest and the adapter
27
+ * carries on.
28
+ */
29
+ import { monotonicNow } from '../clock.js';
30
+ import { isTightBuffer } from './buffer.js';
31
+ import { PdfError } from '../types.js';
32
+ /* ------------------------------------------------------------------ *
33
+ * Errors
34
+ * ------------------------------------------------------------------ */
35
+ const ERROR_CODES = [
36
+ 'not-found',
37
+ 'password-required',
38
+ 'password-incorrect',
39
+ 'corrupt',
40
+ 'unsupported',
41
+ 'cancelled',
42
+ 'out-of-memory',
43
+ 'backend-failure',
44
+ ];
45
+ /**
46
+ * React Native's `promise.reject(code, message)` surfaces `code` as a string
47
+ * property on the rejection, which is why `PdfErrorCode`'s members are the wire
48
+ * format on the native side too (`PdfErrorCode.wire()` in Java,
49
+ * `PdfCanvasErrorCodeWire()` in Objective-C). No translation table exists in
50
+ * between, deliberately: a table is a place for the lists to silently drift
51
+ * apart.
52
+ */
53
+ export function toPdfError(error, fallback) {
54
+ if (error instanceof PdfError)
55
+ return error;
56
+ const code = error?.code;
57
+ const message = error?.message ?? String(error);
58
+ if (typeof code === 'string' && ERROR_CODES.includes(code)) {
59
+ return new PdfError(code, String(message), { cause: error });
60
+ }
61
+ return new PdfError('backend-failure', `${fallback}: ${message}`, {
62
+ cause: error,
63
+ });
64
+ }
65
+ /* ------------------------------------------------------------------ *
66
+ * base64
67
+ * ------------------------------------------------------------------ */
68
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
69
+ const BASE64_LOOKUP = /*#__PURE__*/ (() => {
70
+ const table = new Uint8Array(128).fill(255);
71
+ for (let i = 0; i < BASE64_ALPHABET.length; i++) {
72
+ table[BASE64_ALPHABET.charCodeAt(i)] = i;
73
+ }
74
+ return table;
75
+ })();
76
+ /**
77
+ * Hand-rolled rather than `atob` or `Buffer`.
78
+ *
79
+ * `Buffer` is a node global that RN does not ship, and `atob` — added to Hermes
80
+ * relatively recently — decodes to a STRING, so recovering bytes means a second
81
+ * pass building a Uint8Array from char codes, doubling the peak footprint of a
82
+ * buffer that is already megabytes. This writes straight into the output array,
83
+ * and being pure means the padded-stride and odd-length cases are covered in
84
+ * vitest instead of on a device.
85
+ */
86
+ export function decodeBase64(input) {
87
+ let end = input.length;
88
+ while (end > 0 && input.charCodeAt(end - 1) === 61 /* '=' */)
89
+ end--;
90
+ const out = new Uint8Array(Math.floor((end * 3) / 4));
91
+ let outIndex = 0;
92
+ let accumulator = 0;
93
+ let bits = 0;
94
+ for (let i = 0; i < end; i++) {
95
+ const code = input.charCodeAt(i);
96
+ const value = code < 128 ? BASE64_LOOKUP[code] : undefined;
97
+ if (value === undefined || value === 255) {
98
+ // Whitespace is legal in a base64 payload and is the one thing worth
99
+ // tolerating; anything else means the transport corrupted the raster, and
100
+ // a sheared image is a far worse outcome than a named failure.
101
+ if (code === 10 || code === 13 || code === 32 || code === 9)
102
+ continue;
103
+ throw new PdfError('backend-failure', `Raster payload is not valid base64 at index ${i}.`);
104
+ }
105
+ accumulator = (accumulator << 6) | value;
106
+ bits += 6;
107
+ if (bits >= 8) {
108
+ bits -= 8;
109
+ out[outIndex++] = (accumulator >> bits) & 0xff;
110
+ }
111
+ }
112
+ return outIndex === out.length ? out : out.subarray(0, outIndex).slice();
113
+ }
114
+ /* ------------------------------------------------------------------ *
115
+ * Transport negotiation
116
+ * ------------------------------------------------------------------ */
117
+ /**
118
+ * Mirrors `PdfCanvasPixelBridge.PROBE_BYTES` on the Java side and
119
+ * `kPdfCanvasProbeBytes` in `ios/Sources/PdfCanvasBridge/PdfCanvasSlots.mm`.
120
+ *
121
+ * The three lists are restated rather than derived, which is the point: they are
122
+ * the only places the transport's byte fidelity is asserted, and if one is edited
123
+ * without the others the probe fails and that platform degrades to base64 rather
124
+ * than shipping a shear.
125
+ *
126
+ * The contents are chosen, not arbitrary. 0x00 and 0xFF pin both ends of the
127
+ * range; 0xFD/0xFE/0xFF are above 0x7F, where Java's SIGNED byte and a
128
+ * JS `Uint8Array` disagree unless every step of the copy is right. (On iOS the
129
+ * sign-extension hazard does not exist — the whole path is `uint8_t` — but the
130
+ * probe's other job does: it proves the runtime will build an ArrayBuffer over
131
+ * external memory at all, which is a claim about a JS engine version and not
132
+ * about a language.)
133
+ */
134
+ export const TRANSPORT_PROBE_BYTES = [
135
+ 0x00, 0x01, 0x02, 0xfd, 0xfe, 0xff, 0x2a, 0x07,
136
+ ];
137
+ /**
138
+ * Reads the installed host function off the global.
139
+ *
140
+ * Through `globalThis`, which is universal — unlike `react-native`, whose import
141
+ * is what this whole file is written to avoid. So the pure core still loads
142
+ * under vitest and in a browser: the lookup simply finds nothing.
143
+ */
144
+ export const lookupTakePixels = () => {
145
+ const found = globalThis
146
+ .__pdfCanvasTakePixels;
147
+ return typeof found === 'function' ? found : null;
148
+ };
149
+ const BASE64_TRANSPORT = { kind: 'base64', take: null };
150
+ /**
151
+ * Decides ONCE, at construction, which transport this rasterizer uses — and
152
+ * proves the fast one before choosing it.
153
+ *
154
+ * THE PROBE IS THE POINT. Every step below can fail on a real device for a
155
+ * reason no type system sees: the app binary may predate the native transport
156
+ * (no `installJsiTransport`), the build may lack the native library for this ABI
157
+ * or slice (install returns -1), the runtime may refuse a host function, or the
158
+ * JS engine may not support an ArrayBuffer backed by external memory at all —
159
+ * Hermes does, but "does" is a claim about a version. So instead of inferring
160
+ * availability from any of those, the negotiation moves eight known bytes across
161
+ * the real transport and compares them. Only a byte-for-byte match arms it.
162
+ *
163
+ * Every failure returns `base64`, and none of them throws. A build that cannot
164
+ * do this fast renders exactly as it did before.
165
+ */
166
+ export function negotiateTransport(native, lookup = lookupTakePixels) {
167
+ const install = native.installJsiTransport;
168
+ const enable = native.enableJsiTransport;
169
+ if (typeof install !== 'function' || typeof enable !== 'function') {
170
+ return BASE64_TRANSPORT;
171
+ }
172
+ try {
173
+ const slot = install.call(native);
174
+ if (typeof slot !== 'number' || !Number.isFinite(slot) || slot < 0) {
175
+ return BASE64_TRANSPORT;
176
+ }
177
+ const take = lookup();
178
+ if (take === null) {
179
+ // Installed on the native side but not visible on the global: the runtime
180
+ // this module is evaluating in is not the one that was installed into. The
181
+ // probe slot is reclaimed on the next reload.
182
+ //
183
+ // DISARMED, not merely declined. The enable flag lives on the native
184
+ // MODULE, shared by every rasterizer in the process, so another runtime
185
+ // that negotiated successfully may already have turned it on — and then
186
+ // every render this rasterizer makes comes back as a slot it has no way to
187
+ // redeem, which is a rasterizer that cannot draw at all rather than a slow
188
+ // one. Turning the flag off costs the other runtime its speed and costs
189
+ // this one nothing but a base64 decode.
190
+ enable.call(native, false);
191
+ return BASE64_TRANSPORT;
192
+ }
193
+ const probe = new Uint8Array(take(slot));
194
+ if (probe.length !== TRANSPORT_PROBE_BYTES.length ||
195
+ TRANSPORT_PROBE_BYTES.some((byte, index) => probe[index] !== byte)) {
196
+ enable.call(native, false);
197
+ return BASE64_TRANSPORT;
198
+ }
199
+ return enable.call(native, true)
200
+ ? { kind: 'jsi-arraybuffer', take }
201
+ : BASE64_TRANSPORT;
202
+ }
203
+ catch {
204
+ // Includes the interesting case: a runtime that installs the host function
205
+ // happily and then throws inside `createArrayBuffer`. That failure is only
206
+ // observable by trying, which is what the probe is.
207
+ try {
208
+ enable.call(native, false);
209
+ }
210
+ catch {
211
+ // Disarming a transport that never armed. Nothing to report.
212
+ }
213
+ return BASE64_TRANSPORT;
214
+ }
215
+ }
216
+ /* ------------------------------------------------------------------ *
217
+ * Geometry
218
+ * ------------------------------------------------------------------ */
219
+ const ROTATIONS = [0, 90, 180, 270];
220
+ export function toPageGeometry(page) {
221
+ const rotation = ROTATIONS.includes(page.rotation)
222
+ ? page.rotation
223
+ : 0;
224
+ return {
225
+ index: page.index,
226
+ width: page.width,
227
+ height: page.height,
228
+ rotation,
229
+ };
230
+ }
231
+ /* ------------------------------------------------------------------ *
232
+ * Bytes
233
+ * ------------------------------------------------------------------ */
234
+ /**
235
+ * Turns one native render result into bytes, on whichever transport produced it.
236
+ *
237
+ * Branches on the RESULT rather than on the negotiated transport, because the
238
+ * two can disagree for exactly one render: a native side clears its own flag
239
+ * when a slot store fails and falls that tile back to base64 mid-flight. What
240
+ * arrived is the only thing that can be believed.
241
+ */
242
+ export function bytesFromResult(result, transport) {
243
+ if (result.pixelsSlot !== undefined) {
244
+ if (transport.take === null) {
245
+ throw new PdfError('backend-failure', 'The native module returned a JSI pixel slot, but no ' +
246
+ '__pdfCanvasTakePixels is installed in this runtime.');
247
+ }
248
+ // No copy. The ArrayBuffer wraps the native buffer the render worker
249
+ // filled, and it is exactly the raster and nothing more — so `isTightBuffer`
250
+ // holds by construction and `tighten()` in ingest.ts has nothing to do.
251
+ //
252
+ // WRAPPED, because the take can fail for a reason that is not a bug here: a
253
+ // slot is drained on teardown (a reload, or a second React instance tearing
254
+ // down), and the host function reports that by THROWING — which is right,
255
+ // since an empty ArrayBuffer would reach Skia and draw a black tile. What
256
+ // arrives is a bare `Error` from C++, and every other failure at this seam is
257
+ // a `PdfError` with a code the controller can act on (`decodeBase64`
258
+ // included). Mapping it keeps that one contract true of both transports.
259
+ let buffer;
260
+ try {
261
+ buffer = transport.take(result.pixelsSlot);
262
+ }
263
+ catch (error) {
264
+ throw new PdfError('backend-failure', `Could not redeem raster slot ${result.pixelsSlot}: ` +
265
+ `${error?.message ?? String(error)}`, { cause: error });
266
+ }
267
+ return {
268
+ bytes: new Uint8Array(buffer),
269
+ payloadChars: 0,
270
+ used: 'jsi-arraybuffer',
271
+ };
272
+ }
273
+ if (result.base64 !== undefined) {
274
+ return {
275
+ bytes: decodeBase64(result.base64),
276
+ payloadChars: result.base64.length,
277
+ used: 'base64',
278
+ };
279
+ }
280
+ throw new PdfError('backend-failure', 'The native module returned neither `base64` nor `pixelsSlot`.');
281
+ }
282
+ export function createNativeHandle(native, opened, transport, alpha, label) {
283
+ const pages = opened.pages.map(toPageGeometry);
284
+ let closed = false;
285
+ // Monotonic per handle. The native side keys its cancellation signals on
286
+ // (handle, token), so a token is only ever meaningful alongside its handle.
287
+ let nextToken = 1;
288
+ const requirePage = (index) => {
289
+ const geometry = pages[index];
290
+ if (geometry === undefined) {
291
+ throw new PdfError('not-found', `Document has ${pages.length} pages; asked for ${index}.`);
292
+ }
293
+ return geometry;
294
+ };
295
+ return {
296
+ pageCount: opened.pageCount,
297
+ rotationSupport: opened.rotationSupport,
298
+ pageGeometry: requirePage,
299
+ async render(request, signal) {
300
+ if (closed) {
301
+ throw new PdfError('backend-failure', `${label} rasterizer handle is closed.`);
302
+ }
303
+ requirePage(request.page);
304
+ if (signal?.aborted) {
305
+ throw new PdfError('cancelled', 'Render was aborted before it started.');
306
+ }
307
+ const token = nextToken++;
308
+ // Registered before the call so an abort that lands during the bridge hop
309
+ // still reaches the native signal. Removed in the finally, so a document
310
+ // that renders for hours does not accumulate listeners.
311
+ const onAbort = () => native.cancel(opened.handle, token);
312
+ signal?.addEventListener('abort', onAbort, { once: true });
313
+ // INSTRUMENTATION. Two clock reads and one subtraction per tile, on a path
314
+ // whose cheapest term is already milliseconds — the measurement cost is
315
+ // noise. It is unconditional on purpose: a timing seam that has to be
316
+ // switched on is a timing seam nobody has running when the interesting
317
+ // frame happens.
318
+ const startedAt = monotonicNow();
319
+ let result;
320
+ try {
321
+ result = await native.render(opened.handle, {
322
+ page: request.page,
323
+ x: request.docRect.x,
324
+ y: request.docRect.y,
325
+ width: request.docRect.width,
326
+ height: request.docRect.height,
327
+ scale: request.scale,
328
+ annotations: request.annotations,
329
+ background: request.background,
330
+ }, token);
331
+ }
332
+ catch (error) {
333
+ throw toPdfError(error, `Could not render page ${request.page}`);
334
+ }
335
+ finally {
336
+ signal?.removeEventListener('abort', onAbort);
337
+ }
338
+ const bridgeEndedAt = monotonicNow();
339
+ // On the fast transport this is a slot redemption and an ArrayBuffer view
340
+ // — the same seam that used to hold a 5.6 M-iteration base64 loop, kept in
341
+ // place so the two are measured by the identical clock reads.
342
+ const decoded = bytesFromResult(result, transport);
343
+ const decodeEndedAt = monotonicNow();
344
+ return {
345
+ bytes: decoded.bytes,
346
+ width: result.width,
347
+ height: result.height,
348
+ // Taken from the native result, never recomputed as width * 4. Every
349
+ // device measured so far shows a tight stride at every width tested,
350
+ // including odd and prime ones — but "measured tight today" is not
351
+ // "guaranteed tight", and a wrong stride shears the image instead of
352
+ // failing.
353
+ rowBytes: result.rowBytes,
354
+ format: result.format,
355
+ alpha,
356
+ timings: {
357
+ bridgeMs: bridgeEndedAt - startedAt,
358
+ decodeMs: decodeEndedAt - bridgeEndedAt,
359
+ // Zero on the fast transport, and that zero is the headline: there is
360
+ // no payload to count because nothing was stringified.
361
+ payloadChars: decoded.payloadChars,
362
+ // Measured, not asserted, on BOTH transports. `decodeBase64` allocates
363
+ // its own exact buffer and a redeemed ArrayBuffer is exactly the
364
+ // raster, so this is expected to be TRUE either way — which is the
365
+ // useful thing to have on record: `tighten()`'s copy was never part of
366
+ // the cost being attacked, and the new transport neither gains nor
367
+ // loses there.
368
+ tight: isTightBuffer(decoded.bytes),
369
+ transport: decoded.used,
370
+ },
371
+ };
372
+ },
373
+ close() {
374
+ if (closed)
375
+ return;
376
+ closed = true;
377
+ native.close(opened.handle);
378
+ },
379
+ };
380
+ }
381
+ /**
382
+ * Wraps a native module in a `PageRasterizer`.
383
+ *
384
+ * The native module is INJECTED rather than imported so this file stays pure.
385
+ * `getDefaultRasterizer()` in `./index.native.ts` supplies the real one.
386
+ */
387
+ export function createNativeRasterizer(native, spec) {
388
+ // ONCE, here, and not lazily on first render. The probe is two synchronous
389
+ // bridge calls and an eight-byte round trip, so it costs nothing worth
390
+ // deferring — and doing it now is what lets `capabilities.transport` be true
391
+ // the moment the object exists, rather than becoming true later behind a host
392
+ // that has already read it.
393
+ const transport = negotiateTransport(native, spec.lookupTakePixels ?? lookupTakePixels);
394
+ const capabilities = {
395
+ ...spec.capabilities,
396
+ maxConcurrentRenders: spec.poolSize,
397
+ transport: transport.kind,
398
+ };
399
+ return {
400
+ id: spec.id,
401
+ capabilities,
402
+ async open(source) {
403
+ let opened;
404
+ try {
405
+ opened = await native.open(spec.toNativeSource(source), spec.poolSize);
406
+ }
407
+ catch (error) {
408
+ throw toPdfError(error, 'Could not open the document');
409
+ }
410
+ return createNativeHandle(native, opened, transport, spec.alpha, spec.label);
411
+ },
412
+ };
413
+ }
@@ -0,0 +1,84 @@
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 type { PdfiumWasmSource } from './protocol.js';
52
+ import type { PageRasterizer } from '../../types.js';
53
+ /**
54
+ * The worker surface this file uses, declared rather than imported.
55
+ *
56
+ * This package compiles with `lib: ["ES2022"]` and no DOM — the pure core has to
57
+ * keep loading with neither `react-native` nor a browser present, and pulling
58
+ * `lib.dom` in to name one type would let every DOM global into the whole
59
+ * package unnoticed.
60
+ *
61
+ * A real `Worker` satisfies this structurally. The listener parameter is
62
+ * `unknown` on purpose: method parameters are compared bivariantly, so a
63
+ * `(ev: MessageEvent) => void` listener is accepted, whereas the same shape
64
+ * written as a PROPERTY (`onmessage: …`) would be rejected under
65
+ * `strictFunctionTypes` and force every host to cast.
66
+ */
67
+ export interface WorkerLike {
68
+ postMessage(message: unknown, transfer?: readonly unknown[]): void;
69
+ addEventListener(type: string, listener: (event: unknown) => void): void;
70
+ removeEventListener(type: string, listener: (event: unknown) => void): void;
71
+ terminate(): void;
72
+ }
73
+ export interface WebRasterizerOptions {
74
+ /**
75
+ * Makes one worker. Called once per `open()`, never memoised by this package
76
+ * — see the header for why a document owns its worker outright.
77
+ */
78
+ createWorker: () => WorkerLike;
79
+ /** Where the worker finds `pdfium.wasm`. Always the host's to say. */
80
+ wasm: PdfiumWasmSource;
81
+ /** Surfaced as `PageRasterizer.id` and in every diagnostic. */
82
+ id?: string;
83
+ }
84
+ export declare function createWebRasterizer(options: WebRasterizerOptions): PageRasterizer;