@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.
- package/PdfCanvas.podspec +80 -0
- package/README.md +1391 -0
- package/android/build.gradle +175 -0
- package/android/consumer-rules.pro +3 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +2 -0
- package/android/gradlew +248 -0
- package/android/gradlew.bat +98 -0
- package/android/settings.gradle +34 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
- package/android/src/jsi/cpp/CMakeLists.txt +33 -0
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
- package/android/tools/compile-gate.sh +289 -0
- package/dist/cache.d.ts +161 -0
- package/dist/cache.js +389 -0
- package/dist/clock.d.ts +21 -0
- package/dist/clock.js +31 -0
- package/dist/controller.d.ts +131 -0
- package/dist/controller.js +1002 -0
- package/dist/geometry.d.ts +96 -0
- package/dist/geometry.js +248 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +50 -0
- package/dist/index.native.d.ts +21 -0
- package/dist/index.native.js +23 -0
- package/dist/index.web.d.ts +24 -0
- package/dist/index.web.js +26 -0
- package/dist/layout.d.ts +78 -0
- package/dist/layout.js +152 -0
- package/dist/peer-check.d.ts +83 -0
- package/dist/peer-check.js +183 -0
- package/dist/planner.d.ts +35 -0
- package/dist/planner.js +530 -0
- package/dist/policy.d.ts +14 -0
- package/dist/policy.js +168 -0
- package/dist/rasterizer/android.d.ts +121 -0
- package/dist/rasterizer/android.js +126 -0
- package/dist/rasterizer/buffer.d.ts +18 -0
- package/dist/rasterizer/buffer.js +18 -0
- package/dist/rasterizer/fake.d.ts +92 -0
- package/dist/rasterizer/fake.js +433 -0
- package/dist/rasterizer/index.d.ts +38 -0
- package/dist/rasterizer/index.js +69 -0
- package/dist/rasterizer/index.native.d.ts +19 -0
- package/dist/rasterizer/index.native.js +80 -0
- package/dist/rasterizer/index.web.d.ts +63 -0
- package/dist/rasterizer/index.web.js +75 -0
- package/dist/rasterizer/ingest.d.ts +180 -0
- package/dist/rasterizer/ingest.js +307 -0
- package/dist/rasterizer/ios.d.ts +84 -0
- package/dist/rasterizer/ios.js +155 -0
- package/dist/rasterizer/native-bridge.d.ts +261 -0
- package/dist/rasterizer/native-bridge.js +413 -0
- package/dist/rasterizer/web/client.d.ts +84 -0
- package/dist/rasterizer/web/client.js +359 -0
- package/dist/rasterizer/web/engine.d.ts +69 -0
- package/dist/rasterizer/web/engine.js +308 -0
- package/dist/rasterizer/web/index.d.ts +26 -0
- package/dist/rasterizer/web/index.js +21 -0
- package/dist/rasterizer/web/pdfium.d.ts +213 -0
- package/dist/rasterizer/web/pdfium.js +170 -0
- package/dist/rasterizer/web/protocol.d.ts +138 -0
- package/dist/rasterizer/web/protocol.js +37 -0
- package/dist/rasterizer/web/session.d.ts +106 -0
- package/dist/rasterizer/web/session.js +413 -0
- package/dist/rasterizer/web/worker.d.ts +44 -0
- package/dist/rasterizer/web/worker.js +76 -0
- package/dist/react/PdfContentView.d.ts +98 -0
- package/dist/react/PdfContentView.js +189 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +3 -0
- package/dist/react/usePdfDocument.d.ts +134 -0
- package/dist/react/usePdfDocument.js +275 -0
- package/dist/react/usePdfLayer.d.ts +94 -0
- package/dist/react/usePdfLayer.js +388 -0
- package/dist/skia/index.d.ts +26 -0
- package/dist/skia/index.js +25 -0
- package/dist/skia/index.native.d.ts +14 -0
- package/dist/skia/index.native.js +16 -0
- package/dist/skia/index.web.d.ts +12 -0
- package/dist/skia/index.web.js +14 -0
- package/dist/skia/resolve.native.d.ts +18 -0
- package/dist/skia/resolve.native.js +22 -0
- package/dist/skia/resolve.web.d.ts +41 -0
- package/dist/skia/resolve.web.js +62 -0
- package/dist/skia/skia-api.d.ts +94 -0
- package/dist/skia/skia-api.js +113 -0
- package/dist/testing/index.d.ts +27 -0
- package/dist/testing/index.js +28 -0
- package/dist/testing/scenes.d.ts +100 -0
- package/dist/testing/scenes.js +267 -0
- package/dist/types.d.ts +611 -0
- package/dist/types.js +21 -0
- package/dist/web-init.d.ts +68 -0
- package/dist/web-init.js +64 -0
- package/ios/Package.swift +58 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
- package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
- package/package.json +144 -0
- package/react-native.config.js +40 -0
- package/scripts/check-skia-peer.mjs +86 -0
- 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;
|