@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,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;
|