@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,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deterministic PageRasterizer with no Skia, no native module and no PDF.
|
|
3
|
+
*
|
|
4
|
+
* It exists to drive the entire pipeline — planner, cadence, epoch swap, cache
|
|
5
|
+
* — in vitest and in a demo app, and to be the fixture that CATCHES
|
|
6
|
+
* REGISTRATION BUGS. The pattern is chosen for that second job:
|
|
7
|
+
*
|
|
8
|
+
* - the checkerboard's cell size is in DOC units, not pixels, so two
|
|
9
|
+
* correctly-placed pieces show one continuous board across their overlap
|
|
10
|
+
* and a piece drawn at the wrong doc rect (or squashed by a src-rect
|
|
11
|
+
* mistake) shows an obvious seam;
|
|
12
|
+
* - the board is INFINITE in doc space and is not clipped to the page box, so
|
|
13
|
+
* a detail piece that overflows the page edge still shows continuous
|
|
14
|
+
* checks and an accidental clip is visible as a blank margin;
|
|
15
|
+
* - each piece carries a 1px border, so a piece that has been inset or
|
|
16
|
+
* stretched shows its border in the wrong place;
|
|
17
|
+
* - each piece encodes its own doc rect into a corner swatch, so a test can
|
|
18
|
+
* read back what a raster BELIEVES it is without any Skia in the loop.
|
|
19
|
+
*
|
|
20
|
+
* Everything here is a pure function of the request, so the same request always
|
|
21
|
+
* produces byte-identical output.
|
|
22
|
+
*/
|
|
23
|
+
import { PdfError } from '../types.js';
|
|
24
|
+
/* ------------------------------------------------------------------ *
|
|
25
|
+
* Page sizes, in PDF points (1pt = 1/72in)
|
|
26
|
+
* ------------------------------------------------------------------ */
|
|
27
|
+
export const LETTER = Object.freeze({ width: 612, height: 792 });
|
|
28
|
+
/** ARCH D, 24x36in — the sheet the base-raster sizing rule is calibrated on. */
|
|
29
|
+
export const ARCH_D = Object.freeze({ width: 1728, height: 2592 });
|
|
30
|
+
/** ARCH E, 36x48in. */
|
|
31
|
+
export const ARCH_E = Object.freeze({ width: 2592, height: 3456 });
|
|
32
|
+
const DEFAULT_CHECKER_DOC_SIZE = 32;
|
|
33
|
+
const DEFAULT_MAX_PIXELS = 64000000;
|
|
34
|
+
function resolveConfig(options) {
|
|
35
|
+
const sizes = options.pageSizes && options.pageSizes.length > 0
|
|
36
|
+
? options.pageSizes
|
|
37
|
+
: [LETTER];
|
|
38
|
+
const rotations = options.pageRotations && options.pageRotations.length > 0
|
|
39
|
+
? options.pageRotations
|
|
40
|
+
: [0];
|
|
41
|
+
const pageCount = options.pageCount ?? options.pageSizes?.length ?? 3;
|
|
42
|
+
const pages = [];
|
|
43
|
+
for (let index = 0; index < pageCount; index++) {
|
|
44
|
+
// Both arrays are non-empty by construction above; the `??` only satisfies
|
|
45
|
+
// noUncheckedIndexedAccess.
|
|
46
|
+
const size = sizes[index % sizes.length] ?? LETTER;
|
|
47
|
+
const rotation = rotations[index % rotations.length] ?? 0;
|
|
48
|
+
pages.push({
|
|
49
|
+
index,
|
|
50
|
+
width: size.width,
|
|
51
|
+
height: size.height,
|
|
52
|
+
rotation,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
id: options.id ?? 'fake',
|
|
57
|
+
pages,
|
|
58
|
+
checkerDocSize: options.checkerDocSize ?? DEFAULT_CHECKER_DOC_SIZE,
|
|
59
|
+
format: options.format ?? 'rgba8888',
|
|
60
|
+
latencyMs: options.latencyMs ?? 0,
|
|
61
|
+
password: options.password,
|
|
62
|
+
fail: options.fail ?? {},
|
|
63
|
+
maxPixels: options.maxPixels ?? DEFAULT_MAX_PIXELS,
|
|
64
|
+
maxConcurrentRenders: Math.max(1, Math.floor(options.maxConcurrentRenders ?? 1) || 1),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/* ------------------------------------------------------------------ *
|
|
68
|
+
* Pixel geometry
|
|
69
|
+
* ------------------------------------------------------------------ */
|
|
70
|
+
const BYTES_PER_PIXEL = 4;
|
|
71
|
+
/**
|
|
72
|
+
* READ OFF THE PIXEL LOOP, NOT ASSUMED — and unfalsifiable by this fake's own
|
|
73
|
+
* tests, which is the honest thing to say about it.
|
|
74
|
+
*
|
|
75
|
+
* `writePixel` takes r, g, b and a as independent arguments and stores all four
|
|
76
|
+
* verbatim; nothing here ever scales a colour channel by an alpha. That is
|
|
77
|
+
* `'straight'` by construction, so that is what it declares.
|
|
78
|
+
*
|
|
79
|
+
* BUT: every `writePixel` call site below passes `a = 255`, and so does
|
|
80
|
+
* `encodeSwatchValue`. The only alpha the fake ever emits besides 255 is the ZERO
|
|
81
|
+
* of the untouched buffer under `background: 'transparent'`, and there the colour
|
|
82
|
+
* channels are zero too. Both `[r, g, b, 255]` and `[0, 0, 0, 0]` are byte-
|
|
83
|
+
* identical under the two encodings, so NO test driven by this fake can tell a
|
|
84
|
+
* wrong declaration here from a right one. The claim is made from the source, and
|
|
85
|
+
* the discriminating pixel evidence lives in `composition.test.ts`, whose
|
|
86
|
+
* fixtures build genuine partial alpha on purpose.
|
|
87
|
+
*
|
|
88
|
+
* If the fake is ever given antialiased edges or a translucent watermark, this
|
|
89
|
+
* becomes a real, checkable claim — and `'straight'` will still be the correct
|
|
90
|
+
* answer as long as `writePixel` keeps storing what it is handed.
|
|
91
|
+
*/
|
|
92
|
+
const FAKE_ALPHA = 'straight';
|
|
93
|
+
/**
|
|
94
|
+
* The pixel size the fake renders a request at.
|
|
95
|
+
*
|
|
96
|
+
* Exported because RasterRequest carries no PixelSize — a backend derives it —
|
|
97
|
+
* and a test that wants to index into the output needs the same arithmetic.
|
|
98
|
+
* Rounding (rather than ceiling) keeps the raster closest to the nominal
|
|
99
|
+
* scale; either is safe, because `PdfRaster.docRect` is the request's doc rect
|
|
100
|
+
* regardless and `fit="fill"` absorbs the sub-pixel difference.
|
|
101
|
+
*/
|
|
102
|
+
export const fakePixelSize = (request) => ({
|
|
103
|
+
width: Math.max(1, Math.round(request.docRect.width * request.scale)),
|
|
104
|
+
height: Math.max(1, Math.round(request.docRect.height * request.scale)),
|
|
105
|
+
});
|
|
106
|
+
/* ------------------------------------------------------------------ *
|
|
107
|
+
* Colour
|
|
108
|
+
* ------------------------------------------------------------------ */
|
|
109
|
+
function hsvToRgb(h, s, v) {
|
|
110
|
+
const c = v * s;
|
|
111
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
112
|
+
const m = v - c;
|
|
113
|
+
let r = c;
|
|
114
|
+
let g = 0;
|
|
115
|
+
let b = 0;
|
|
116
|
+
switch (Math.floor(h / 60) % 6) {
|
|
117
|
+
case 0:
|
|
118
|
+
g = x;
|
|
119
|
+
break;
|
|
120
|
+
case 1:
|
|
121
|
+
r = x;
|
|
122
|
+
g = c;
|
|
123
|
+
break;
|
|
124
|
+
case 2:
|
|
125
|
+
r = 0;
|
|
126
|
+
g = c;
|
|
127
|
+
b = x;
|
|
128
|
+
break;
|
|
129
|
+
case 3:
|
|
130
|
+
r = 0;
|
|
131
|
+
g = x;
|
|
132
|
+
b = c;
|
|
133
|
+
break;
|
|
134
|
+
case 4:
|
|
135
|
+
r = x;
|
|
136
|
+
b = c;
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
b = x;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
return [
|
|
143
|
+
Math.round((r + m) * 255),
|
|
144
|
+
Math.round((g + m) * 255),
|
|
145
|
+
Math.round((b + m) * 255),
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A distinct flat colour per page. Golden-angle hue rotation puts consecutive
|
|
150
|
+
* pages maximally far apart in hue, so page 4 is never mistaken for page 5 in a
|
|
151
|
+
* screenshot of a scrolling document.
|
|
152
|
+
*/
|
|
153
|
+
export function fakePageColor(page) {
|
|
154
|
+
const hue = (((page * 137.508) % 360) + 360) % 360;
|
|
155
|
+
return hsvToRgb(hue, 0.82, 0.86);
|
|
156
|
+
}
|
|
157
|
+
const mix = (a, b, t) => Math.round(a + (b - a) * t);
|
|
158
|
+
function writePixel(out, offset, r, g, b, a, format) {
|
|
159
|
+
if (format === 'bgra8888') {
|
|
160
|
+
out[offset] = b;
|
|
161
|
+
out[offset + 2] = r;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
out[offset] = r;
|
|
165
|
+
out[offset + 2] = b;
|
|
166
|
+
}
|
|
167
|
+
out[offset + 1] = g;
|
|
168
|
+
out[offset + 3] = a;
|
|
169
|
+
}
|
|
170
|
+
/* ------------------------------------------------------------------ *
|
|
171
|
+
* The corner swatch
|
|
172
|
+
* ------------------------------------------------------------------ */
|
|
173
|
+
/**
|
|
174
|
+
* The swatch is RAW BYTES, not a colour: four pixels, each carrying one doc
|
|
175
|
+
* rect component as a 24-bit fixed-point value in channels 0..2 with channel 3
|
|
176
|
+
* held at 255. Because it is written without the format swizzle, the decoder is
|
|
177
|
+
* format-independent — it reads back the same numbers from an RGBA and a BGRA
|
|
178
|
+
* raster.
|
|
179
|
+
*/
|
|
180
|
+
const SWATCH_FIXED_POINT = 16;
|
|
181
|
+
/** Bias so negative doc coordinates survive (±524288pt of range). */
|
|
182
|
+
const SWATCH_BIAS = 0x800000;
|
|
183
|
+
const SWATCH_ORIGIN_X = 1;
|
|
184
|
+
const SWATCH_ROW = 1;
|
|
185
|
+
const SWATCH_PIXELS = 4;
|
|
186
|
+
/** Smallest raster that has room for the swatch inside its border. */
|
|
187
|
+
const SWATCH_MIN_WIDTH = SWATCH_ORIGIN_X + SWATCH_PIXELS + 1;
|
|
188
|
+
const SWATCH_MIN_HEIGHT = SWATCH_ROW + 2;
|
|
189
|
+
function encodeSwatchValue(out, offset, value) {
|
|
190
|
+
const raw = Math.round(value * SWATCH_FIXED_POINT) + SWATCH_BIAS;
|
|
191
|
+
const clamped = Math.max(0, Math.min(0xffffff, raw));
|
|
192
|
+
out[offset] = (clamped >> 16) & 0xff;
|
|
193
|
+
out[offset + 1] = (clamped >> 8) & 0xff;
|
|
194
|
+
out[offset + 2] = clamped & 0xff;
|
|
195
|
+
out[offset + 3] = 255;
|
|
196
|
+
}
|
|
197
|
+
function encodeSwatch(out, rowBytes, width, height, docRect) {
|
|
198
|
+
if (width < SWATCH_MIN_WIDTH || height < SWATCH_MIN_HEIGHT) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const base = SWATCH_ROW * rowBytes + SWATCH_ORIGIN_X * BYTES_PER_PIXEL;
|
|
202
|
+
encodeSwatchValue(out, base, docRect.x);
|
|
203
|
+
encodeSwatchValue(out, base + BYTES_PER_PIXEL, docRect.y);
|
|
204
|
+
encodeSwatchValue(out, base + BYTES_PER_PIXEL * 2, docRect.width);
|
|
205
|
+
encodeSwatchValue(out, base + BYTES_PER_PIXEL * 3, docRect.height);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Read the doc rect a fake raster believes it covers, or null when the raster
|
|
209
|
+
* was too small to carry a swatch.
|
|
210
|
+
*/
|
|
211
|
+
export function decodeFakeDocRect(pixels) {
|
|
212
|
+
const { bytes, width, height, rowBytes } = pixels;
|
|
213
|
+
if (width < SWATCH_MIN_WIDTH || height < SWATCH_MIN_HEIGHT) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const base = SWATCH_ROW * rowBytes + SWATCH_ORIGIN_X * BYTES_PER_PIXEL;
|
|
217
|
+
const read = (component) => {
|
|
218
|
+
const o = base + component * BYTES_PER_PIXEL;
|
|
219
|
+
// In range by the size guard above; `?? 0` satisfies
|
|
220
|
+
// noUncheckedIndexedAccess on TypedArray indexing.
|
|
221
|
+
const raw = ((bytes[o] ?? 0) << 16) |
|
|
222
|
+
((bytes[o + 1] ?? 0) << 8) |
|
|
223
|
+
(bytes[o + 2] ?? 0);
|
|
224
|
+
return (raw - SWATCH_BIAS) / SWATCH_FIXED_POINT;
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
x: read(0),
|
|
228
|
+
y: read(1),
|
|
229
|
+
width: read(2),
|
|
230
|
+
height: read(3),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/* ------------------------------------------------------------------ *
|
|
234
|
+
* Rendering
|
|
235
|
+
* ------------------------------------------------------------------ */
|
|
236
|
+
function renderPixels(request, cfg) {
|
|
237
|
+
const { docRect, scale, page, background } = request;
|
|
238
|
+
const { width, height } = fakePixelSize(request);
|
|
239
|
+
if (width * height > cfg.maxPixels) {
|
|
240
|
+
throw new PdfError('out-of-memory', `Fake rasterizer refused a ${width}x${height} raster ` +
|
|
241
|
+
`(${width * height} pixels > maxPixels ${cfg.maxPixels}).`);
|
|
242
|
+
}
|
|
243
|
+
const rowBytes = width * BYTES_PER_PIXEL;
|
|
244
|
+
const bytes = new Uint8Array(rowBytes * height);
|
|
245
|
+
const [r, g, b] = fakePageColor(page);
|
|
246
|
+
// The pre-fill is load-bearing, and it is the same rule every real backend
|
|
247
|
+
// owes: nothing clears the buffer, and un-painted RGBA is transparent black.
|
|
248
|
+
// 'white' fills with a pale tint of the page colour (still page-identifying,
|
|
249
|
+
// still clearly "paper"); 'transparent' leaves the zeros, which is what
|
|
250
|
+
// makes a missing fill visible as black when the raster is composited.
|
|
251
|
+
if (background === 'white') {
|
|
252
|
+
const tintR = mix(r, 255, 0.85);
|
|
253
|
+
const tintG = mix(g, 255, 0.85);
|
|
254
|
+
const tintB = mix(b, 255, 0.85);
|
|
255
|
+
for (let o = 0; o < bytes.length; o += BYTES_PER_PIXEL) {
|
|
256
|
+
writePixel(bytes, o, tintR, tintG, tintB, 255, cfg.format);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Dark checker cells, in DOC space. Pixel CENTRES are converted to doc
|
|
260
|
+
// coordinates — a corner-based conversion puts the sample exactly on a cell
|
|
261
|
+
// boundary whenever the piece origin happens to align with one, and the
|
|
262
|
+
// parity then flips on a floating-point tie.
|
|
263
|
+
const cell = cfg.checkerDocSize;
|
|
264
|
+
for (let py = 0; py < height; py++) {
|
|
265
|
+
const docY = docRect.y + (py + 0.5) / scale;
|
|
266
|
+
const cellY = Math.floor(docY / cell);
|
|
267
|
+
let offset = py * rowBytes;
|
|
268
|
+
for (let px = 0; px < width; px++, offset += BYTES_PER_PIXEL) {
|
|
269
|
+
const docX = docRect.x + (px + 0.5) / scale;
|
|
270
|
+
const cellX = Math.floor(docX / cell);
|
|
271
|
+
if (((cellX + cellY) & 1) === 1) {
|
|
272
|
+
writePixel(bytes, offset, r, g, b, 255, cfg.format);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// 1px border, opaque black on every background.
|
|
277
|
+
if (width >= 2 && height >= 2) {
|
|
278
|
+
const lastRow = (height - 1) * rowBytes;
|
|
279
|
+
for (let px = 0; px < width; px++) {
|
|
280
|
+
const o = px * BYTES_PER_PIXEL;
|
|
281
|
+
writePixel(bytes, o, 0, 0, 0, 255, cfg.format);
|
|
282
|
+
writePixel(bytes, lastRow + o, 0, 0, 0, 255, cfg.format);
|
|
283
|
+
}
|
|
284
|
+
const lastCol = (width - 1) * BYTES_PER_PIXEL;
|
|
285
|
+
for (let py = 0; py < height; py++) {
|
|
286
|
+
const row = py * rowBytes;
|
|
287
|
+
writePixel(bytes, row, 0, 0, 0, 255, cfg.format);
|
|
288
|
+
writePixel(bytes, row + lastCol, 0, 0, 0, 255, cfg.format);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
encodeSwatch(bytes, rowBytes, width, height, docRect);
|
|
292
|
+
return {
|
|
293
|
+
bytes,
|
|
294
|
+
width,
|
|
295
|
+
height,
|
|
296
|
+
rowBytes,
|
|
297
|
+
format: cfg.format,
|
|
298
|
+
alpha: FAKE_ALPHA,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/* ------------------------------------------------------------------ *
|
|
302
|
+
* Cancellation
|
|
303
|
+
* ------------------------------------------------------------------ */
|
|
304
|
+
/**
|
|
305
|
+
* `signal.reason`, IF THIS PLATFORM HAS IT — and React Native does not.
|
|
306
|
+
*
|
|
307
|
+
* `reason` is spec'd on AbortSignal and present on web and under node, but React
|
|
308
|
+
* Native polyfills the whole class from `abort-controller@3.0.0`
|
|
309
|
+
* (`Libraries/Core/setUpXHR.js` registers
|
|
310
|
+
* `require('abort-controller/dist/abort-controller').AbortController` as the
|
|
311
|
+
* global). That polyfill predates the reason/throwIfAborted addition: its
|
|
312
|
+
* `abort()` takes no argument and its signal exposes only `aborted`, `onabort`
|
|
313
|
+
* and the two EventTarget methods. So on the package's primary target `reason`
|
|
314
|
+
* is `undefined` no matter what the caller aborted with.
|
|
315
|
+
*
|
|
316
|
+
* The runtime `in` check is therefore load-bearing rather than defensive, and it
|
|
317
|
+
* is what keeps this honest without a cast: TypeScript's unlisted-property `in`
|
|
318
|
+
* narrowing gives `signal.reason` the type `unknown`, which is exactly what
|
|
319
|
+
* `PdfError`'s `cause` accepts. Reading it straight through typechecked only
|
|
320
|
+
* because `@types/node` was in the tree and `react-native` was not — the RN
|
|
321
|
+
* types (`src/types/globals.d.ts`) declare the polyfill's real shape and reject
|
|
322
|
+
* it, which is how this gap surfaced.
|
|
323
|
+
*/
|
|
324
|
+
const abortReason = (signal) => 'reason' in signal ? signal.reason : undefined;
|
|
325
|
+
const abortError = (signal) => new PdfError('cancelled', 'Fake rasterizer render was aborted.', {
|
|
326
|
+
cause: signal === undefined ? undefined : abortReason(signal),
|
|
327
|
+
});
|
|
328
|
+
function throwIfAborted(signal) {
|
|
329
|
+
if (signal?.aborted) {
|
|
330
|
+
throw abortError(signal);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function delay(ms, signal) {
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
const onAbort = () => {
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
reject(abortError(signal));
|
|
338
|
+
};
|
|
339
|
+
const timer = setTimeout(() => {
|
|
340
|
+
signal?.removeEventListener('abort', onAbort);
|
|
341
|
+
resolve();
|
|
342
|
+
}, ms);
|
|
343
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/* ------------------------------------------------------------------ *
|
|
347
|
+
* The rasterizer
|
|
348
|
+
* ------------------------------------------------------------------ */
|
|
349
|
+
function renderFailure(cfg, request) {
|
|
350
|
+
const configured = cfg.fail.render;
|
|
351
|
+
if (!configured) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return typeof configured === 'function' ? configured(request) : configured;
|
|
355
|
+
}
|
|
356
|
+
function checkPassword(cfg, source) {
|
|
357
|
+
if (cfg.password === undefined) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (source.password === undefined) {
|
|
361
|
+
throw new PdfError('password-required', 'Fake document is protected.');
|
|
362
|
+
}
|
|
363
|
+
if (source.password !== cfg.password) {
|
|
364
|
+
throw new PdfError('password-incorrect', 'Fake document password rejected.');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function createHandle(cfg) {
|
|
368
|
+
let closed = false;
|
|
369
|
+
const requirePage = (page) => {
|
|
370
|
+
const geometry = cfg.pages[page];
|
|
371
|
+
if (!geometry) {
|
|
372
|
+
throw new PdfError('not-found', `Fake document has ${cfg.pages.length} pages; asked for ${page}.`);
|
|
373
|
+
}
|
|
374
|
+
return geometry;
|
|
375
|
+
};
|
|
376
|
+
const requireOpen = () => {
|
|
377
|
+
if (closed) {
|
|
378
|
+
throw new PdfError('backend-failure', 'Fake rasterizer handle is closed.');
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
pageCount: cfg.pages.length,
|
|
383
|
+
pageGeometry: requirePage,
|
|
384
|
+
async render(request, signal) {
|
|
385
|
+
throwIfAborted(signal);
|
|
386
|
+
requireOpen();
|
|
387
|
+
requirePage(request.page);
|
|
388
|
+
if (cfg.latencyMs > 0) {
|
|
389
|
+
await delay(cfg.latencyMs, signal);
|
|
390
|
+
}
|
|
391
|
+
// Re-check after the await: a real backend that finished its synchronous
|
|
392
|
+
// draw during this window would still be expected to drop the result.
|
|
393
|
+
throwIfAborted(signal);
|
|
394
|
+
requireOpen();
|
|
395
|
+
const failure = renderFailure(cfg, request);
|
|
396
|
+
if (failure) {
|
|
397
|
+
throw new PdfError(failure, `Fake rasterizer failed page ${request.page} by configuration.`);
|
|
398
|
+
}
|
|
399
|
+
return renderPixels(request, cfg);
|
|
400
|
+
},
|
|
401
|
+
close() {
|
|
402
|
+
closed = true;
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
export function createFakeRasterizer(options = {}) {
|
|
407
|
+
const cfg = resolveConfig(options);
|
|
408
|
+
const capabilities = {
|
|
409
|
+
annotations: true,
|
|
410
|
+
// Honest: the pixel loop is synchronous and uninterruptible. The only
|
|
411
|
+
// window in which the signal can land is the artificial latency, so with no
|
|
412
|
+
// latency configured the fake is exactly as interruptible as PDFKit —
|
|
413
|
+
// which is to say, between requests only.
|
|
414
|
+
interruptibleRender: cfg.latencyMs > 0,
|
|
415
|
+
text: false,
|
|
416
|
+
search: false,
|
|
417
|
+
links: false,
|
|
418
|
+
maxConcurrentRenders: cfg.maxConcurrentRenders,
|
|
419
|
+
};
|
|
420
|
+
return {
|
|
421
|
+
id: cfg.id,
|
|
422
|
+
capabilities,
|
|
423
|
+
open(source) {
|
|
424
|
+
return new Promise(resolve => {
|
|
425
|
+
if (cfg.fail.open) {
|
|
426
|
+
throw new PdfError(cfg.fail.open, `Fake rasterizer failed open() by configuration (${cfg.fail.open}).`);
|
|
427
|
+
}
|
|
428
|
+
checkPassword(cfg, source);
|
|
429
|
+
resolve(createHandle(cfg));
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rasterizer pieces that are the same on every platform.
|
|
3
|
+
*
|
|
4
|
+
* `getDefaultRasterizer` is NOT here — it is the one thing that differs, and it
|
|
5
|
+
* lives in `./index.native.ts` and `./index.web.ts`. Those two files re-export
|
|
6
|
+
* everything below, so a platform entry point only ever imports one of them.
|
|
7
|
+
*
|
|
8
|
+
* Note that a relative import written with an explicit `.js` extension is
|
|
9
|
+
* resolved LITERALLY by Metro — its platform-extension search appends to the
|
|
10
|
+
* whole request string, so `./index.js` looks for `index.js.native.js` and then
|
|
11
|
+
* falls through to the real `index.js`. That is why the platform split is done
|
|
12
|
+
* by naming the file at the import site (and by the package's `exports`
|
|
13
|
+
* conditions at the root) rather than by trusting `./index.js` to resolve to
|
|
14
|
+
* `./index.native.js`. It is also why `export * from './index.js'` inside
|
|
15
|
+
* `index.native.ts` is not circular.
|
|
16
|
+
*
|
|
17
|
+
* The deterministic fake backend is deliberately NOT re-exported here: it is
|
|
18
|
+
* reachable only through the `./testing` subpath, so it never lands in an app
|
|
19
|
+
* bundle that merely imports the package.
|
|
20
|
+
*
|
|
21
|
+
* NEITHER IS THE INGEST SEAM, ANY MORE. `export * from './ingest.js'` used to
|
|
22
|
+
* sit here, which gave `ingestRaster` / `imageFromPixels` / `rasterByteLength`
|
|
23
|
+
* two public homes and made `./rasterizer` a Skia-touching subpath. They live on
|
|
24
|
+
* `./skia` now — a BREAKING move for anyone importing them from here, and the
|
|
25
|
+
* point of it is that "which subpath needs a Skia runtime" has one answer. What
|
|
26
|
+
* this subpath still is: the platform backend registry and the shared backend
|
|
27
|
+
* helpers, which touch no Skia at all.
|
|
28
|
+
*/
|
|
29
|
+
import type { PageRasterizer } from '../types.js';
|
|
30
|
+
/**
|
|
31
|
+
* A PageRasterizer that exists only to fail informatively.
|
|
32
|
+
*
|
|
33
|
+
* The failure is deferred to `open()` on purpose. Throwing at import time would
|
|
34
|
+
* take the pure core, the Skia ingest path and the fake backend down with it,
|
|
35
|
+
* and all three are fully usable today — a host that injects its own
|
|
36
|
+
* `PageRasterizer` never touches this object at all.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createMissingBackendRasterizer(id: string, missing: string): PageRasterizer;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rasterizer pieces that are the same on every platform.
|
|
3
|
+
*
|
|
4
|
+
* `getDefaultRasterizer` is NOT here — it is the one thing that differs, and it
|
|
5
|
+
* lives in `./index.native.ts` and `./index.web.ts`. Those two files re-export
|
|
6
|
+
* everything below, so a platform entry point only ever imports one of them.
|
|
7
|
+
*
|
|
8
|
+
* Note that a relative import written with an explicit `.js` extension is
|
|
9
|
+
* resolved LITERALLY by Metro — its platform-extension search appends to the
|
|
10
|
+
* whole request string, so `./index.js` looks for `index.js.native.js` and then
|
|
11
|
+
* falls through to the real `index.js`. That is why the platform split is done
|
|
12
|
+
* by naming the file at the import site (and by the package's `exports`
|
|
13
|
+
* conditions at the root) rather than by trusting `./index.js` to resolve to
|
|
14
|
+
* `./index.native.js`. It is also why `export * from './index.js'` inside
|
|
15
|
+
* `index.native.ts` is not circular.
|
|
16
|
+
*
|
|
17
|
+
* The deterministic fake backend is deliberately NOT re-exported here: it is
|
|
18
|
+
* reachable only through the `./testing` subpath, so it never lands in an app
|
|
19
|
+
* bundle that merely imports the package.
|
|
20
|
+
*
|
|
21
|
+
* NEITHER IS THE INGEST SEAM, ANY MORE. `export * from './ingest.js'` used to
|
|
22
|
+
* sit here, which gave `ingestRaster` / `imageFromPixels` / `rasterByteLength`
|
|
23
|
+
* two public homes and made `./rasterizer` a Skia-touching subpath. They live on
|
|
24
|
+
* `./skia` now — a BREAKING move for anyone importing them from here, and the
|
|
25
|
+
* point of it is that "which subpath needs a Skia runtime" has one answer. What
|
|
26
|
+
* this subpath still is: the platform backend registry and the shared backend
|
|
27
|
+
* helpers, which touch no Skia at all.
|
|
28
|
+
*/
|
|
29
|
+
import { PdfError } from '../types.js';
|
|
30
|
+
const NO_CAPABILITIES = {
|
|
31
|
+
annotations: false,
|
|
32
|
+
interruptibleRender: false,
|
|
33
|
+
text: false,
|
|
34
|
+
search: false,
|
|
35
|
+
links: false,
|
|
36
|
+
maxConcurrentRenders: 1,
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* A PageRasterizer that exists only to fail informatively.
|
|
40
|
+
*
|
|
41
|
+
* The failure is deferred to `open()` on purpose. Throwing at import time would
|
|
42
|
+
* take the pure core, the Skia ingest path and the fake backend down with it,
|
|
43
|
+
* and all three are fully usable today — a host that injects its own
|
|
44
|
+
* `PageRasterizer` never touches this object at all.
|
|
45
|
+
*/
|
|
46
|
+
export function createMissingBackendRasterizer(id, missing) {
|
|
47
|
+
const message = `${missing} @reekon-tools/react-native-pdf-canvas ships three things that ` +
|
|
48
|
+
'are known to work with no native module at all: the pure core, the Skia ' +
|
|
49
|
+
'ingest path and a deterministic fake backend. Both native backends exist ' +
|
|
50
|
+
'and are proven on device, so seeing this means the module is genuinely ' +
|
|
51
|
+
'absent from the build rather than unimplemented — on iOS check that ' +
|
|
52
|
+
'`PdfCanvas` is in ios/Podfile.lock and re-run `pod install`; on Android ' +
|
|
53
|
+
'check that autolinking registered `PdfCanvasPackage` (this package ships ' +
|
|
54
|
+
'the `packageImportPath` it needs, because the binding lives outside ' +
|
|
55
|
+
'`src/main`). The web rasterizer EXISTS but must be configured, because ' +
|
|
56
|
+
'how a worker is built and where `pdfium.wasm` lives are your bundler’s ' +
|
|
57
|
+
'business and not something this package can discover — see ' +
|
|
58
|
+
'`configureWebPdfium`. Otherwise pass a `rasterizer` explicitly — either ' +
|
|
59
|
+
'your own PageRasterizer, or `createFakeRasterizer()` from ' +
|
|
60
|
+
"'@reekon-tools/react-native-pdf-canvas/testing'. See README.md, " +
|
|
61
|
+
'"Backends".';
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
capabilities: NO_CAPABILITIES,
|
|
65
|
+
open() {
|
|
66
|
+
return Promise.reject(new PdfError('backend-failure', message));
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native default rasterizer.
|
|
3
|
+
*
|
|
4
|
+
* BOTH PLATFORMS ARE WIRED. `createAndroidRasterizer` and `createIosRasterizer`
|
|
5
|
+
* are pure and injectable, and this file is the only place a real native module
|
|
6
|
+
* is resolved. On any build where the module is absent — a host that skipped
|
|
7
|
+
* autolinking, a platform neither branch knows — the self-describing placeholder
|
|
8
|
+
* is returned exactly as before, failing at `open()` rather than at import, so
|
|
9
|
+
* the pure core, the Skia ingest path and the fake backend all keep working.
|
|
10
|
+
*
|
|
11
|
+
* ONE NATIVE MODULE NAME FOR BOTH PLATFORMS (`PdfCanvas`), one wire format, and
|
|
12
|
+
* therefore one lookup. What differs is which adapter wraps it, because the
|
|
13
|
+
* capabilities and the alpha encoding differ — see `./native-bridge.ts`.
|
|
14
|
+
*/
|
|
15
|
+
import type { PageRasterizer } from '../types.js';
|
|
16
|
+
export * from './index.js';
|
|
17
|
+
export * from './android.js';
|
|
18
|
+
export * from './ios.js';
|
|
19
|
+
export declare const getDefaultRasterizer: () => PageRasterizer;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native default rasterizer.
|
|
3
|
+
*
|
|
4
|
+
* BOTH PLATFORMS ARE WIRED. `createAndroidRasterizer` and `createIosRasterizer`
|
|
5
|
+
* are pure and injectable, and this file is the only place a real native module
|
|
6
|
+
* is resolved. On any build where the module is absent — a host that skipped
|
|
7
|
+
* autolinking, a platform neither branch knows — the self-describing placeholder
|
|
8
|
+
* is returned exactly as before, failing at `open()` rather than at import, so
|
|
9
|
+
* the pure core, the Skia ingest path and the fake backend all keep working.
|
|
10
|
+
*
|
|
11
|
+
* ONE NATIVE MODULE NAME FOR BOTH PLATFORMS (`PdfCanvas`), one wire format, and
|
|
12
|
+
* therefore one lookup. What differs is which adapter wraps it, because the
|
|
13
|
+
* capabilities and the alpha encoding differ — see `./native-bridge.ts`.
|
|
14
|
+
*/
|
|
15
|
+
import { createAndroidRasterizer } from './android.js';
|
|
16
|
+
import { createIosRasterizer } from './ios.js';
|
|
17
|
+
import { createMissingBackendRasterizer } from './index.js';
|
|
18
|
+
export * from './index.js';
|
|
19
|
+
export * from './android.js';
|
|
20
|
+
export * from './ios.js';
|
|
21
|
+
const MISSING = createMissingBackendRasterizer('missing-native', 'The PdfCanvas native module was not found. On iOS that means the pod is ' +
|
|
22
|
+
'not in the build (run `pod install` after adding the package, and check ' +
|
|
23
|
+
'that `PdfCanvas` appears in Podfile.lock); on Android it means ' +
|
|
24
|
+
'autolinking did not register PdfCanvasPackage.');
|
|
25
|
+
/**
|
|
26
|
+
* `require`, not `import`, and wrapped in a try.
|
|
27
|
+
*
|
|
28
|
+
* A static `import` of `react-native` would make this module unloadable under
|
|
29
|
+
* vitest and in a browser bundle, taking the pure core down with it — the exact
|
|
30
|
+
* failure the placeholder exists to avoid. Metro resolves a `require` call
|
|
31
|
+
* statically, so the RN bundle is unaffected.
|
|
32
|
+
*
|
|
33
|
+
* `NativeModules` rather than `TurboModuleRegistry`: this module has no codegen
|
|
34
|
+
* spec, and `TurboModuleRegistry.get` returns null for a legacy module on the
|
|
35
|
+
* old architecture. `NativeModules` resolves under both, because the new
|
|
36
|
+
* architecture's interop layer backfills it — measured on Android inside a
|
|
37
|
+
* bridgeless RN 0.86 app where `global.__turboModuleProxy` was `undefined` and
|
|
38
|
+
* `NativeModules.PdfCanvas` still had every method.
|
|
39
|
+
*/
|
|
40
|
+
function loadNativeModule() {
|
|
41
|
+
try {
|
|
42
|
+
const rn = require('react-native');
|
|
43
|
+
const os = rn.Platform?.OS;
|
|
44
|
+
if (os !== 'android' && os !== 'ios')
|
|
45
|
+
return null;
|
|
46
|
+
const found = rn.NativeModules?.['PdfCanvas'];
|
|
47
|
+
if (!found)
|
|
48
|
+
return null;
|
|
49
|
+
return { os, module: found };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolved ONCE, at module scope.
|
|
57
|
+
*
|
|
58
|
+
* Stable reference on purpose — a rasterizer identity change is a reason to
|
|
59
|
+
* reopen a document, so the default must not be a fresh object per call.
|
|
60
|
+
*
|
|
61
|
+
* IT ALSO NEGOTIATES THE TRANSPORT, which is why "once" matters more than
|
|
62
|
+
* tidiness: `createNativeRasterizer` probes the JSI path at construction, and on
|
|
63
|
+
* iOS that probe is what installs `__pdfCanvasTakePixels` into this runtime. A
|
|
64
|
+
* fresh rasterizer per call would re-run the probe, and each run parks a slot.
|
|
65
|
+
*
|
|
66
|
+
* NAME COLLISION, deliberate and unresolved here: `react/usePdfDocument.ts`
|
|
67
|
+
* exports a registry accessor with this same name and a DIFFERENT meaning
|
|
68
|
+
* (`PageRasterizer | null`, whatever `setDefaultRasterizer` last installed).
|
|
69
|
+
* The root index must therefore not `export *` from this module. Do NOT wire
|
|
70
|
+
* the MISSING placeholder into that registry either: an empty registry produces
|
|
71
|
+
* "No PageRasterizer available", which is a better first error than a backend
|
|
72
|
+
* that opens and then always fails.
|
|
73
|
+
*/
|
|
74
|
+
const resolved = loadNativeModule();
|
|
75
|
+
const DEFAULT = resolved === null
|
|
76
|
+
? MISSING
|
|
77
|
+
: resolved.os === 'ios'
|
|
78
|
+
? createIosRasterizer(resolved.module)
|
|
79
|
+
: createAndroidRasterizer(resolved.module);
|
|
80
|
+
export const getDefaultRasterizer = () => DEFAULT;
|