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