@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 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 { openPdfiumDocument } from './engine.js';
|
|
30
|
+
import { PdfError } from '../../types.js';
|
|
31
|
+
/**
|
|
32
|
+
* One macrotask turn, the plain way. THE FALLBACK, and half of the default.
|
|
33
|
+
*
|
|
34
|
+
* Correct, and slow the moment it repeats: Chrome clamps a timer whose NESTING
|
|
35
|
+
* LEVEL exceeds 5 to a 4 ms floor, and a chain of these only ever nests deeper,
|
|
36
|
+
* because each one is scheduled from inside the task the previous one's timer
|
|
37
|
+
* started. Measured in real headless Chrome inside a real `{type: 'module'}`
|
|
38
|
+
* worker (harness `/c/tmp/mc/web`, 24 consecutive yields): the first six are
|
|
39
|
+
* free (0.10 0.00 0.00 0.00 0.00 0.00 ms) and every one after costs 4.0-6.2 ms
|
|
40
|
+
* — 90.0 ms total, mean 3.75 ms. `drain` yields once per tile, so a 4-10 tile
|
|
41
|
+
* detail epoch burnt ~0-20 ms of pure worker idle and a 32-piece base grid
|
|
42
|
+
* ~130 ms.
|
|
43
|
+
*/
|
|
44
|
+
const timerYield = () => new Promise(resolve => {
|
|
45
|
+
setTimeout(resolve, 0);
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* ONE channel for the whole module, built on first use and never torn down.
|
|
49
|
+
*
|
|
50
|
+
* `undefined` = not tried yet; `null` = this runtime has no usable
|
|
51
|
+
* `MessageChannel`, so `macrotaskYield` is timer-only. A fresh channel per yield
|
|
52
|
+
* would allocate two ports per tile, which is a cost of exactly the kind this
|
|
53
|
+
* change exists to remove, so it is built once.
|
|
54
|
+
*/
|
|
55
|
+
let sharedChannel;
|
|
56
|
+
function openYieldChannel() {
|
|
57
|
+
const Ctor = globalThis.MessageChannel;
|
|
58
|
+
// ABSENT ONLY IN OLD OR EXOTIC RUNTIMES. `MessageChannel` is a global in every
|
|
59
|
+
// browser, in `DedicatedWorkerGlobalScope`, and in Node >= 15 (re-exported
|
|
60
|
+
// from `worker_threads`) — measured `typeof MessageChannel === 'function'` on
|
|
61
|
+
// node v24.14.0, which is what vitest runs here. It is NOT in React Native's
|
|
62
|
+
// Hermes global scope, but nothing on the native path reaches this file:
|
|
63
|
+
// `session.ts` is web-only, behind the `browser`/`default` export condition.
|
|
64
|
+
// So the fallback is for a runtime nobody has named, not for a platform we
|
|
65
|
+
// ship to — and it is a fallback rather than a throw because a missing yield
|
|
66
|
+
// would break rendering, not just slow it down.
|
|
67
|
+
if (typeof Ctor !== 'function')
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
const { port1, port2 } = new Ctor();
|
|
71
|
+
const waiters = [];
|
|
72
|
+
port1.onmessage = () => {
|
|
73
|
+
// FIFO, and `wake` posts exactly one message per waiter, so message N
|
|
74
|
+
// wakes waiter N. Concurrent yields therefore cannot cross-resolve: they
|
|
75
|
+
// are matched by POSITION, not by identity. (`drain` is single-flight per
|
|
76
|
+
// session, so the case that makes this reachable at all is two sessions in
|
|
77
|
+
// one worker — the channel is per MODULE, not per session.) Even a
|
|
78
|
+
// mismatch would still be a correct yield, since every waiter is asking
|
|
79
|
+
// for the same thing and nothing is carried across, but matching by
|
|
80
|
+
// position means the question never has to be argued.
|
|
81
|
+
waiters.shift()?.();
|
|
82
|
+
// NODE ONLY, AND LOAD-BEARING. A `MessagePort` with a live `onmessage`
|
|
83
|
+
// refs the libuv loop, so an always-armed module-scope channel keeps the
|
|
84
|
+
// process alive forever — measured: `node` with nothing open but an armed
|
|
85
|
+
// port never exits (killed by a timeout at exit 124). Unref-ing
|
|
86
|
+
// permanently is the opposite failure: the loop then drains and exits with
|
|
87
|
+
// a yield still outstanding, stranding `drain` — also measured, the
|
|
88
|
+
// posted message is simply never delivered. So the port is ref'd exactly
|
|
89
|
+
// while a yield is in flight and unref'd the moment none is.
|
|
90
|
+
if (waiters.length === 0)
|
|
91
|
+
port1.unref?.();
|
|
92
|
+
};
|
|
93
|
+
port1.unref?.();
|
|
94
|
+
return {
|
|
95
|
+
waiters,
|
|
96
|
+
wake: () => {
|
|
97
|
+
port1.ref?.();
|
|
98
|
+
port2.postMessage(0);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// A runtime that has the constructor but refuses to build one. Nothing is
|
|
104
|
+
// known to do this; falling back still beats throwing out of a render.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* THE CANCEL WINDOW: a port hop, and then a timer turn.
|
|
110
|
+
*
|
|
111
|
+
* WHY A PORT HOP. It is a macrotask that no clamp applies to. Same Chrome
|
|
112
|
+
* worker, same 24 consecutive yields as `timerYield`'s 90.0 ms: 0.2 ms total,
|
|
113
|
+
* mean 0.008 ms.
|
|
114
|
+
*
|
|
115
|
+
* WHY A TIMER TURN AS WELL. Because a port hop ALONE does not drain the timer
|
|
116
|
+
* queue, and the timer queue is where the `cancel` is. Measured on node v24.14.0
|
|
117
|
+
* with K `setTimeout(0)` callbacks already pending when the yield starts, 20
|
|
118
|
+
* trials at each K in {1, 2, 4, 8, 16, 32}: a timer yield runs ALL K of them
|
|
119
|
+
* before it resolves, in every trial; a bare port hop runs ZERO of them, in
|
|
120
|
+
* every trial. A port-only yield would have narrowed the very window this file
|
|
121
|
+
* exists to keep open, and would have done it quietly — the abort test in
|
|
122
|
+
* `web-rasterizer.test.ts` delivers its cancels over a `setTimeout`-backed
|
|
123
|
+
* channel, so a bare hop turns that test flaky rather than loudly wrong.
|
|
124
|
+
*
|
|
125
|
+
* AND THE TIMER TURN IS FREE, which is the whole reason this shape works: the
|
|
126
|
+
* hop RESETS the nesting level Chrome clamps on, because the timer is always
|
|
127
|
+
* scheduled from a port-message task and never from another timer task.
|
|
128
|
+
* Measured in the same worker over the same 24 yields: 0.1-0.3 ms total, mean
|
|
129
|
+
* 0.004-0.013 ms — indistinguishable from the bare hop, and ~280x cheaper than
|
|
130
|
+
* the bare timer it replaces.
|
|
131
|
+
*
|
|
132
|
+
* So this is STRICTLY STRONGER than what it replaces — a hop AND a full timer
|
|
133
|
+
* turn, where there was only a timer turn — at 1/280th the cost, and its worst
|
|
134
|
+
* case (a runtime that clamps the following timer anyway) is exactly the
|
|
135
|
+
* behaviour that shipped before it.
|
|
136
|
+
*
|
|
137
|
+
* Exported for `web-rasterizer.test.ts`, which asserts the drained-timer
|
|
138
|
+
* property above directly. Still not public: nothing in the package's `exports`
|
|
139
|
+
* map reaches `session.ts` except `./worker`, which re-exports nothing.
|
|
140
|
+
*/
|
|
141
|
+
export const macrotaskYield = async () => {
|
|
142
|
+
if (sharedChannel === undefined)
|
|
143
|
+
sharedChannel = openYieldChannel();
|
|
144
|
+
const channel = sharedChannel;
|
|
145
|
+
if (channel !== null) {
|
|
146
|
+
await new Promise(resolve => {
|
|
147
|
+
channel.waiters.push(resolve);
|
|
148
|
+
channel.wake();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
await timerYield();
|
|
152
|
+
};
|
|
153
|
+
const resolveFetch = (injected) => {
|
|
154
|
+
if (injected !== undefined)
|
|
155
|
+
return injected;
|
|
156
|
+
const found = globalThis.fetch;
|
|
157
|
+
if (typeof found !== 'function') {
|
|
158
|
+
throw new PdfError('unsupported', 'This runtime has no global `fetch`, so a document given as a `uri` ' +
|
|
159
|
+
'cannot be loaded. Pass the document as `{data}` instead.');
|
|
160
|
+
}
|
|
161
|
+
return found;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Anything thrown, as the two fields the wire carries.
|
|
165
|
+
*
|
|
166
|
+
* A `PdfError` keeps its code — that is the whole point of the union being
|
|
167
|
+
* frozen and shared. Anything else is a bug or a WASM abort, and becomes
|
|
168
|
+
* `backend-failure` with its message preserved, because a swallowed message here
|
|
169
|
+
* is a support ticket that says only "rendering failed".
|
|
170
|
+
*/
|
|
171
|
+
function describe(error) {
|
|
172
|
+
if (error instanceof PdfError) {
|
|
173
|
+
return { code: error.code, message: error.message };
|
|
174
|
+
}
|
|
175
|
+
const message = error?.message ?? String(error);
|
|
176
|
+
return { code: 'backend-failure', message: String(message) };
|
|
177
|
+
}
|
|
178
|
+
export function createWorkerSession(deps) {
|
|
179
|
+
const yieldToEventLoop = deps.yieldToEventLoop ?? macrotaskYield;
|
|
180
|
+
let opening = null;
|
|
181
|
+
let document = null;
|
|
182
|
+
let disposed = false;
|
|
183
|
+
const queue = [];
|
|
184
|
+
/**
|
|
185
|
+
* Ids the main thread has given up on.
|
|
186
|
+
*
|
|
187
|
+
* A SET AND NOT A FLAG ON THE JOB, because a `cancel` can arrive before its
|
|
188
|
+
* `render` does — the main thread aborts on its own clock and both messages
|
|
189
|
+
* are just messages. An entry is consumed when the matching job is dequeued;
|
|
190
|
+
* one that never matches is dropped when the session is disposed.
|
|
191
|
+
*/
|
|
192
|
+
const cancelled = new Set();
|
|
193
|
+
/**
|
|
194
|
+
* The job `drain` is currently awaiting, or null.
|
|
195
|
+
*
|
|
196
|
+
* Exists so `cancel` can tell a job that is STILL CANCELLABLE (queued, or in
|
|
197
|
+
* flight across the `requireDocument` await) from one that has already been
|
|
198
|
+
* answered. Without it every cancel for an already-finished render left a
|
|
199
|
+
* permanent entry in `cancelled`: the deletes all live on the dequeue path,
|
|
200
|
+
* so an id that never gets dequeued again is never removed, and a long-lived
|
|
201
|
+
* document's set grows with every abort a gesture produces.
|
|
202
|
+
*/
|
|
203
|
+
let inFlight = null;
|
|
204
|
+
let draining = false;
|
|
205
|
+
const postError = (id, error) => {
|
|
206
|
+
const { code, message } = describe(error);
|
|
207
|
+
deps.post({ kind: 'error', id, code, message });
|
|
208
|
+
};
|
|
209
|
+
async function readSource(request) {
|
|
210
|
+
if ('bytes' in request.source) {
|
|
211
|
+
return new Uint8Array(request.source.bytes);
|
|
212
|
+
}
|
|
213
|
+
const url = request.source.url;
|
|
214
|
+
let response;
|
|
215
|
+
try {
|
|
216
|
+
response = await resolveFetch(deps.fetch)(url);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// A network failure, a CORS refusal or a bad scheme. All indistinguishable
|
|
220
|
+
// from JS by design, so the message carries the URL and nothing pretends
|
|
221
|
+
// to know which it was.
|
|
222
|
+
throw new PdfError('backend-failure', `Could not fetch the document from ${url}: ` +
|
|
223
|
+
`${error?.message ?? String(error)}.`, { cause: error });
|
|
224
|
+
}
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
throw new PdfError(
|
|
227
|
+
// 404 and 410 are the two that mean the document is not there. An
|
|
228
|
+
// expired Firebase download token is a 403, which is a real failure but
|
|
229
|
+
// not a missing document, so it stays `backend-failure` and says so.
|
|
230
|
+
response.status === 404 || response.status === 410
|
|
231
|
+
? 'not-found'
|
|
232
|
+
: 'backend-failure', `Fetching the document from ${url} returned ` +
|
|
233
|
+
`${response.status} ${response.statusText}.`);
|
|
234
|
+
}
|
|
235
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
236
|
+
}
|
|
237
|
+
async function open(request) {
|
|
238
|
+
// Both awaits before any PDFium call, and in this order for a reason worth
|
|
239
|
+
// keeping: the wasm compile and the document fetch are independent, so they
|
|
240
|
+
// run concurrently rather than one after the other.
|
|
241
|
+
const [binding, bytes] = await Promise.all([
|
|
242
|
+
deps.loadPdfium(request.wasm),
|
|
243
|
+
readSource(request),
|
|
244
|
+
]);
|
|
245
|
+
if (disposed) {
|
|
246
|
+
throw new PdfError('cancelled', 'The session was closed while opening.');
|
|
247
|
+
}
|
|
248
|
+
return openPdfiumDocument(binding, bytes, request.password);
|
|
249
|
+
}
|
|
250
|
+
async function requireDocument() {
|
|
251
|
+
if (opening === null) {
|
|
252
|
+
throw new PdfError('backend-failure', 'A render arrived before the document was opened.');
|
|
253
|
+
}
|
|
254
|
+
return opening;
|
|
255
|
+
}
|
|
256
|
+
async function drain() {
|
|
257
|
+
if (draining)
|
|
258
|
+
return;
|
|
259
|
+
draining = true;
|
|
260
|
+
try {
|
|
261
|
+
while (queue.length > 0 && !disposed) {
|
|
262
|
+
const job = queue.shift();
|
|
263
|
+
if (job === undefined)
|
|
264
|
+
break;
|
|
265
|
+
if (cancelled.delete(job.id))
|
|
266
|
+
continue;
|
|
267
|
+
inFlight = job.id;
|
|
268
|
+
// `finally`, not a trailing assignment: the body below leaves by
|
|
269
|
+
// `continue`, by `return` and by throwing, and an `inFlight` left
|
|
270
|
+
// pointing at a finished job would let a late cancel be recorded for an
|
|
271
|
+
// id nothing can consume — the same unbounded growth from the other end.
|
|
272
|
+
try {
|
|
273
|
+
let target;
|
|
274
|
+
try {
|
|
275
|
+
target = await requireDocument();
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
postError(job.id, error);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
// Re-checked after the await: both of these can become true while the
|
|
282
|
+
// open is in flight, and rendering into a disposed session would be a
|
|
283
|
+
// use-after-free rather than a wasted tile.
|
|
284
|
+
if (disposed)
|
|
285
|
+
return;
|
|
286
|
+
if (cancelled.delete(job.id))
|
|
287
|
+
continue;
|
|
288
|
+
try {
|
|
289
|
+
const raster = target.render(job.request);
|
|
290
|
+
const buffer = raster.bytes.buffer;
|
|
291
|
+
deps.post({
|
|
292
|
+
kind: 'rendered',
|
|
293
|
+
id: job.id,
|
|
294
|
+
bytes: buffer,
|
|
295
|
+
width: raster.width,
|
|
296
|
+
height: raster.height,
|
|
297
|
+
rowBytes: raster.rowBytes,
|
|
298
|
+
format: raster.format,
|
|
299
|
+
alpha: raster.alpha,
|
|
300
|
+
}, [buffer]);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
postError(job.id, error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
inFlight = null;
|
|
308
|
+
}
|
|
309
|
+
// THE CANCEL WINDOW. See the header.
|
|
310
|
+
//
|
|
311
|
+
// UNCONDITIONAL, AND `if (queue.length > 0)` IS NOT AN OPTIMISATION HERE
|
|
312
|
+
// — it is a correctness bug. AN EMPTY QUEUE DOES NOT MEAN NOTHING IS
|
|
313
|
+
// COMING: messages arrive one per event-loop turn, so the queue is fed
|
|
314
|
+
// SERIALLY and `drain` sees a queue of exactly ONE almost every time it
|
|
315
|
+
// looks. Measured in a real Chrome `{type:'module'}` worker
|
|
316
|
+
// (`/c/tmp/mc/web`): three renders posted back-to-back from the main
|
|
317
|
+
// thread were RECEIVED at t=0, t=60 and t=120 ms with a 60 ms render
|
|
318
|
+
// between each — one delivery per turn, never a batch, and identically
|
|
319
|
+
// so under all three candidate yields.
|
|
320
|
+
//
|
|
321
|
+
// Skip the yield when that queue is empty and `drain` returns after
|
|
322
|
+
// every single tile, `draining` goes false, and the next render is then
|
|
323
|
+
// rendered synchronously inside its own message handler — with the
|
|
324
|
+
// `cancel` still sitting untouched behind it. This trailing yield is
|
|
325
|
+
// what holds `draining` true for one macrotask, so everything that
|
|
326
|
+
// arrives in that turn is batched and re-checked against `cancelled`
|
|
327
|
+
// before any of it is rendered.
|
|
328
|
+
//
|
|
329
|
+
// MEASURED, not argued: adding that guard turns 'renders only the tile
|
|
330
|
+
// that was not aborted' red (3 tiles rendered instead of 1) and leaves
|
|
331
|
+
// the other 348 tests green — the same signature as deleting this line
|
|
332
|
+
// outright. The turn it "wastes" after the genuinely last tile costs
|
|
333
|
+
// 0.008 ms now that the yield is not a clamped timer, which is the whole
|
|
334
|
+
// of what audit item A9 was worth. 'yields after the last job too, so a
|
|
335
|
+
// serially fed queue can still be cancelled' pins this.
|
|
336
|
+
await yieldToEventLoop();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
draining = false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function dispose() {
|
|
344
|
+
if (disposed)
|
|
345
|
+
return;
|
|
346
|
+
disposed = true;
|
|
347
|
+
queue.length = 0;
|
|
348
|
+
cancelled.clear();
|
|
349
|
+
document?.close();
|
|
350
|
+
document = null;
|
|
351
|
+
// An open still in flight closes itself when it lands — see the `disposed`
|
|
352
|
+
// check in the resolve handler. Swallow its rejection so a discarded worker
|
|
353
|
+
// never produces an unhandled rejection.
|
|
354
|
+
opening?.catch(() => { });
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
handle(request) {
|
|
358
|
+
if (disposed)
|
|
359
|
+
return;
|
|
360
|
+
switch (request.kind) {
|
|
361
|
+
case 'open': {
|
|
362
|
+
if (opening !== null) {
|
|
363
|
+
postError(request.id, new PdfError('backend-failure', 'This worker already has a document open. A worker owns ' +
|
|
364
|
+
'exactly one document, for the whole of its life.'));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
opening = open(request);
|
|
368
|
+
opening.then(opened => {
|
|
369
|
+
document = opened;
|
|
370
|
+
if (disposed) {
|
|
371
|
+
// Lost the race with a close. Nothing has been posted, so the
|
|
372
|
+
// main thread never learns about a document it already
|
|
373
|
+
// abandoned — but the heap allocation is real and must go.
|
|
374
|
+
opened.close();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
deps.post({
|
|
378
|
+
kind: 'opened',
|
|
379
|
+
id: request.id,
|
|
380
|
+
pageCount: opened.pageCount,
|
|
381
|
+
// A copy, because the array crosses a structured-clone boundary
|
|
382
|
+
// and the engine's own is readonly on purpose.
|
|
383
|
+
pages: opened.pages.map(page => ({ ...page })),
|
|
384
|
+
});
|
|
385
|
+
}, error => {
|
|
386
|
+
postError(request.id, error);
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
case 'render': {
|
|
391
|
+
queue.push(request);
|
|
392
|
+
void drain();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
case 'cancel': {
|
|
396
|
+
// Only if something can still consume it. A cancel that arrives after
|
|
397
|
+
// its render was answered has nothing to match, and recording it
|
|
398
|
+
// anyway is what made this set grow without bound.
|
|
399
|
+
if (inFlight === request.id ||
|
|
400
|
+
queue.some(job => job.id === request.id)) {
|
|
401
|
+
cancelled.add(request.id);
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
case 'close': {
|
|
406
|
+
dispose();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
dispose,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@reekon-tools/react-native-pdf-canvas/worker` — the module worker entry.
|
|
3
|
+
*
|
|
4
|
+
* THE ONLY MODULE IN THIS PACKAGE THAT NAMES `@embedpdf/pdfium`. It is an
|
|
5
|
+
* OPTIONAL peer dependency: a consumer who ships native only never resolves this
|
|
6
|
+
* file, and nothing else in the package imports it — not even for a type (see
|
|
7
|
+
* `PdfiumWasmModule` in `./pdfium.ts`).
|
|
8
|
+
*
|
|
9
|
+
* It is also the file that PROVES the hand-written binding matches the real
|
|
10
|
+
* module. `bindPdfium` takes a structural type, which is what keeps `./engine.ts`
|
|
11
|
+
* pure and testable; the risk of a structural type is silent drift, and the
|
|
12
|
+
* answer to that is right below — this file passes the genuine
|
|
13
|
+
* `WrappedPdfiumModule` to it and `tsc --noEmit` checks every name, arity and
|
|
14
|
+
* return type. A renamed export or a changed signature in a `@embedpdf/pdfium`
|
|
15
|
+
* bump fails the BUILD, not the browser.
|
|
16
|
+
*
|
|
17
|
+
* ------------------------------------------------------------------------
|
|
18
|
+
* HOW A HOST REACHES IT
|
|
19
|
+
* ------------------------------------------------------------------------
|
|
20
|
+
*
|
|
21
|
+
* import PdfCanvasWorker from
|
|
22
|
+
* '@reekon-tools/react-native-pdf-canvas/worker?worker'; // Vite
|
|
23
|
+
*
|
|
24
|
+
* configureWebPdfium({
|
|
25
|
+
* createWorker: () => new PdfCanvasWorker(),
|
|
26
|
+
* wasm: {url: pdfiumWasmUrl},
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* Any other way of producing a MODULE worker is equally fine — the package takes
|
|
30
|
+
* a factory and never builds a URL of its own. See `./client.ts`.
|
|
31
|
+
*
|
|
32
|
+
* ------------------------------------------------------------------------
|
|
33
|
+
* WHY THERE IS ALMOST NOTHING HERE
|
|
34
|
+
* ------------------------------------------------------------------------
|
|
35
|
+
*
|
|
36
|
+
* Because a worker entry is the one part of this backend that cannot run under
|
|
37
|
+
* vitest in node — there is no DOM `Worker`, and `node:worker_threads` speaks a
|
|
38
|
+
* different API. So everything that could hold a bug lives in `./session.ts` and
|
|
39
|
+
* `./engine.ts`, which take their dependencies as arguments and are driven in
|
|
40
|
+
* node against the REAL PDFium module by an in-process worker stub. What is left
|
|
41
|
+
* unexercised is exactly the glue below: two `self` bindings and the wasm
|
|
42
|
+
* options.
|
|
43
|
+
*/
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@reekon-tools/react-native-pdf-canvas/worker` — the module worker entry.
|
|
3
|
+
*
|
|
4
|
+
* THE ONLY MODULE IN THIS PACKAGE THAT NAMES `@embedpdf/pdfium`. It is an
|
|
5
|
+
* OPTIONAL peer dependency: a consumer who ships native only never resolves this
|
|
6
|
+
* file, and nothing else in the package imports it — not even for a type (see
|
|
7
|
+
* `PdfiumWasmModule` in `./pdfium.ts`).
|
|
8
|
+
*
|
|
9
|
+
* It is also the file that PROVES the hand-written binding matches the real
|
|
10
|
+
* module. `bindPdfium` takes a structural type, which is what keeps `./engine.ts`
|
|
11
|
+
* pure and testable; the risk of a structural type is silent drift, and the
|
|
12
|
+
* answer to that is right below — this file passes the genuine
|
|
13
|
+
* `WrappedPdfiumModule` to it and `tsc --noEmit` checks every name, arity and
|
|
14
|
+
* return type. A renamed export or a changed signature in a `@embedpdf/pdfium`
|
|
15
|
+
* bump fails the BUILD, not the browser.
|
|
16
|
+
*
|
|
17
|
+
* ------------------------------------------------------------------------
|
|
18
|
+
* HOW A HOST REACHES IT
|
|
19
|
+
* ------------------------------------------------------------------------
|
|
20
|
+
*
|
|
21
|
+
* import PdfCanvasWorker from
|
|
22
|
+
* '@reekon-tools/react-native-pdf-canvas/worker?worker'; // Vite
|
|
23
|
+
*
|
|
24
|
+
* configureWebPdfium({
|
|
25
|
+
* createWorker: () => new PdfCanvasWorker(),
|
|
26
|
+
* wasm: {url: pdfiumWasmUrl},
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* Any other way of producing a MODULE worker is equally fine — the package takes
|
|
30
|
+
* a factory and never builds a URL of its own. See `./client.ts`.
|
|
31
|
+
*
|
|
32
|
+
* ------------------------------------------------------------------------
|
|
33
|
+
* WHY THERE IS ALMOST NOTHING HERE
|
|
34
|
+
* ------------------------------------------------------------------------
|
|
35
|
+
*
|
|
36
|
+
* Because a worker entry is the one part of this backend that cannot run under
|
|
37
|
+
* vitest in node — there is no DOM `Worker`, and `node:worker_threads` speaks a
|
|
38
|
+
* different API. So everything that could hold a bug lives in `./session.ts` and
|
|
39
|
+
* `./engine.ts`, which take their dependencies as arguments and are driven in
|
|
40
|
+
* node against the REAL PDFium module by an in-process worker stub. What is left
|
|
41
|
+
* unexercised is exactly the glue below: two `self` bindings and the wasm
|
|
42
|
+
* options.
|
|
43
|
+
*/
|
|
44
|
+
import { init } from '@embedpdf/pdfium';
|
|
45
|
+
import { bindPdfium } from './pdfium.js';
|
|
46
|
+
import { createWorkerSession } from './session.js';
|
|
47
|
+
/**
|
|
48
|
+
* Compile the wasm and initialise the library.
|
|
49
|
+
*
|
|
50
|
+
* `{url}` goes through Emscripten's `locateFile` so it can stream-compile;
|
|
51
|
+
* `{binary}` skips the fetch entirely and is what the node tests use. Neither
|
|
52
|
+
* form is guessed at — `DEFAULT_PDFIUM_WASM_URL` in `@embedpdf/pdfium` points at
|
|
53
|
+
* a jsdelivr copy pinned to ITS version, and silently fetching a different build
|
|
54
|
+
* than the JS bindings expect is precisely the failure the host-supplied locator
|
|
55
|
+
* exists to prevent.
|
|
56
|
+
*
|
|
57
|
+
* `PDFiumExt_Init()` is `@embedpdf/pdfium`'s initialiser and stands in for
|
|
58
|
+
* `FPDF_InitLibrary`. Once per module instance, before any other call.
|
|
59
|
+
*/
|
|
60
|
+
async function loadPdfium(wasm) {
|
|
61
|
+
const module = await init('binary' in wasm ? { wasmBinary: wasm.binary } : { locateFile: () => wasm.url });
|
|
62
|
+
module.PDFiumExt_Init();
|
|
63
|
+
return bindPdfium(module);
|
|
64
|
+
}
|
|
65
|
+
const session = createWorkerSession({
|
|
66
|
+
post: (response, transfer) => {
|
|
67
|
+
self.postMessage(response, transfer);
|
|
68
|
+
},
|
|
69
|
+
loadPdfium,
|
|
70
|
+
});
|
|
71
|
+
self.addEventListener('message', event => {
|
|
72
|
+
const data = event?.data;
|
|
73
|
+
if (data === undefined || data === null)
|
|
74
|
+
return;
|
|
75
|
+
session.handle(data);
|
|
76
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE component.
|
|
3
|
+
*
|
|
4
|
+
* It draws whatever `content` says, at the doc-space rects `content` carries, and
|
|
5
|
+
* it does nothing else. No gestures, no viewport, no shared values, no `<Canvas>`.
|
|
6
|
+
* It is meant to be mounted inside SOMEBODY ELSE'S `<Group transform={...}>`:
|
|
7
|
+
*
|
|
8
|
+
* <Canvas>
|
|
9
|
+
* <Group transform={worldTransform}>
|
|
10
|
+
* <PdfContentView content={content} />
|
|
11
|
+
* ...the host's own annotation layers...
|
|
12
|
+
* </Group>
|
|
13
|
+
* </Canvas>
|
|
14
|
+
*
|
|
15
|
+
* THE ONE RULE: the transform decides WHEN to rasterize, never WHAT to draw. Every
|
|
16
|
+
* number this component hands to Skia is a plain JS number read off a frozen
|
|
17
|
+
* `PdfContent`, so a pan or a zoom re-records the host's scene without this
|
|
18
|
+
* subtree changing at all.
|
|
19
|
+
*/
|
|
20
|
+
import type { ReactNode } from 'react';
|
|
21
|
+
import type { GroupProps } from '@shopify/react-native-skia';
|
|
22
|
+
import type { PdfContent, Sampling } from '../types.js';
|
|
23
|
+
/**
|
|
24
|
+
* The blend mode the Skia DOM actually accepts — the uncapitalised string union
|
|
25
|
+
* (`'srcOver'`, `'multiply'`, …), not the numeric `BlendMode` enum. Derived from
|
|
26
|
+
* `GroupProps` rather than restated so it cannot drift from the renderer.
|
|
27
|
+
*/
|
|
28
|
+
export type PdfBlendMode = NonNullable<GroupProps['blendMode']>;
|
|
29
|
+
export interface PdfContentViewProps {
|
|
30
|
+
/**
|
|
31
|
+
* The frozen snapshot to draw. A new object identity per epoch is the swap
|
|
32
|
+
* signal; see the `PdfContent` doc in types.ts for why a mutated handle cannot
|
|
33
|
+
* work.
|
|
34
|
+
*/
|
|
35
|
+
content: PdfContent;
|
|
36
|
+
/** Forwarded to the wrapping `<Group>`. */
|
|
37
|
+
zIndex?: number;
|
|
38
|
+
/**
|
|
39
|
+
* How the rasters are resampled when the host transform does not land them at
|
|
40
|
+
* exactly 1:1.
|
|
41
|
+
*
|
|
42
|
+
* MIPMAPS DO NOT WORK HERE AND MUST NOT BE ATTEMPTED. RN Skia's `ImageCmd::draw`
|
|
43
|
+
* hardcodes `kStrict_SrcRectConstraint`, and Skia's own `SkCanvas.h` says of
|
|
44
|
+
* that constraint that it "disables mipmaps and anisotropic filtering" — the
|
|
45
|
+
* sampler is forbidden from reading outside the src rect, which is exactly what
|
|
46
|
+
* a mip level requires. Passing `MipmapMode.Linear` here does not error; it is
|
|
47
|
+
* silently ignored, and you get the same aliasing plus the memory of the mip
|
|
48
|
+
* chain.
|
|
49
|
+
*
|
|
50
|
+
* The consequence is structural: MINIFICATION CANNOT BE FIXED BY SAMPLING. It is
|
|
51
|
+
* fixed by never rastering a layer that will be drawn minified — which is why
|
|
52
|
+
* the base layer is sized from the fit display scale times `baseQuality` rather
|
|
53
|
+
* than from a fixed pixel cap. Minification is a scheduling problem here, not a
|
|
54
|
+
* filtering one.
|
|
55
|
+
*
|
|
56
|
+
* @default 'linear'
|
|
57
|
+
*/
|
|
58
|
+
sampling?: Sampling;
|
|
59
|
+
/**
|
|
60
|
+
* Colour of the sheet drawn under each page.
|
|
61
|
+
*
|
|
62
|
+
* Defaults to `null`, meaning NOTHING is drawn under an unrastered page. That
|
|
63
|
+
* default is deliberate: painting opaque white under a PDF with a transparent
|
|
64
|
+
* background is a real bug, and the consumer has very often already painted a
|
|
65
|
+
* surface (a `<Fill>`, a page-shadow layer, a themed canvas) that this rect
|
|
66
|
+
* would cover.
|
|
67
|
+
*
|
|
68
|
+
* @default null
|
|
69
|
+
*/
|
|
70
|
+
paperColor?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Forwarded to the wrapping `<Group>`.
|
|
73
|
+
*
|
|
74
|
+
* CAVEAT: Skia group opacity is folded into each child's paint, not composited
|
|
75
|
+
* once. Because the base and detail layers deliberately OVERLAP, a value below 1
|
|
76
|
+
* lets the base show through the detail and the seams become visible. For a
|
|
77
|
+
* uniform fade, wrap this component in your own `<Group layer={paint}>` so the
|
|
78
|
+
* whole stack composites once.
|
|
79
|
+
*/
|
|
80
|
+
opacity?: number;
|
|
81
|
+
/** Forwarded to the wrapping `<Group>`. */
|
|
82
|
+
blendMode?: PdfBlendMode;
|
|
83
|
+
/**
|
|
84
|
+
* Paint MODIFIER declarations only — `<ColorFilter>` / `<ImageFilter>` /
|
|
85
|
+
* `<Shader>` / `<PathEffect>` nodes (e.g. an invert filter for a dark-mode PDF).
|
|
86
|
+
*
|
|
87
|
+
* They are emitted FIRST, before any drawing, because RN Skia collects
|
|
88
|
+
* declarations as they are visited and the first drawing consumes them.
|
|
89
|
+
*
|
|
90
|
+
* Do NOT pass a `<Paint>`. A Paint child is a *second* paint, so the node is
|
|
91
|
+
* drawn TWICE — once with the declared paint and once with the inherited one —
|
|
92
|
+
* and `paintDeclarations.clear()` runs on that first drawing, so the extra paint
|
|
93
|
+
* would apply to the base raster and to none of the detail rasters. In `__DEV__`
|
|
94
|
+
* this is warned about.
|
|
95
|
+
*/
|
|
96
|
+
children?: ReactNode;
|
|
97
|
+
}
|
|
98
|
+
export declare function PdfContentView({ content, zIndex, sampling, paperColor, opacity, blendMode, children, }: PdfContentViewProps): import("react").JSX.Element;
|