@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.
Files changed (161) hide show
  1. package/PdfCanvas.podspec +80 -0
  2. package/README.md +1391 -0
  3. package/android/build.gradle +175 -0
  4. package/android/consumer-rules.pro +3 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +2 -0
  8. package/android/gradlew +248 -0
  9. package/android/gradlew.bat +98 -0
  10. package/android/settings.gradle +34 -0
  11. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
  12. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
  13. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
  14. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
  15. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
  16. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
  17. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
  18. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
  19. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
  20. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
  21. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
  22. package/android/src/jsi/cpp/CMakeLists.txt +33 -0
  23. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
  24. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
  25. package/android/src/main/AndroidManifest.xml +7 -0
  26. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
  27. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
  28. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
  29. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
  30. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
  31. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
  32. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
  33. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
  34. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
  35. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
  36. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
  37. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
  38. package/android/tools/compile-gate.sh +289 -0
  39. package/dist/cache.d.ts +161 -0
  40. package/dist/cache.js +389 -0
  41. package/dist/clock.d.ts +21 -0
  42. package/dist/clock.js +31 -0
  43. package/dist/controller.d.ts +131 -0
  44. package/dist/controller.js +1002 -0
  45. package/dist/geometry.d.ts +96 -0
  46. package/dist/geometry.js +248 -0
  47. package/dist/index.d.ts +52 -0
  48. package/dist/index.js +50 -0
  49. package/dist/index.native.d.ts +21 -0
  50. package/dist/index.native.js +23 -0
  51. package/dist/index.web.d.ts +24 -0
  52. package/dist/index.web.js +26 -0
  53. package/dist/layout.d.ts +78 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/peer-check.d.ts +83 -0
  56. package/dist/peer-check.js +183 -0
  57. package/dist/planner.d.ts +35 -0
  58. package/dist/planner.js +530 -0
  59. package/dist/policy.d.ts +14 -0
  60. package/dist/policy.js +168 -0
  61. package/dist/rasterizer/android.d.ts +121 -0
  62. package/dist/rasterizer/android.js +126 -0
  63. package/dist/rasterizer/buffer.d.ts +18 -0
  64. package/dist/rasterizer/buffer.js +18 -0
  65. package/dist/rasterizer/fake.d.ts +92 -0
  66. package/dist/rasterizer/fake.js +433 -0
  67. package/dist/rasterizer/index.d.ts +38 -0
  68. package/dist/rasterizer/index.js +69 -0
  69. package/dist/rasterizer/index.native.d.ts +19 -0
  70. package/dist/rasterizer/index.native.js +80 -0
  71. package/dist/rasterizer/index.web.d.ts +63 -0
  72. package/dist/rasterizer/index.web.js +75 -0
  73. package/dist/rasterizer/ingest.d.ts +180 -0
  74. package/dist/rasterizer/ingest.js +307 -0
  75. package/dist/rasterizer/ios.d.ts +84 -0
  76. package/dist/rasterizer/ios.js +155 -0
  77. package/dist/rasterizer/native-bridge.d.ts +261 -0
  78. package/dist/rasterizer/native-bridge.js +413 -0
  79. package/dist/rasterizer/web/client.d.ts +84 -0
  80. package/dist/rasterizer/web/client.js +359 -0
  81. package/dist/rasterizer/web/engine.d.ts +69 -0
  82. package/dist/rasterizer/web/engine.js +308 -0
  83. package/dist/rasterizer/web/index.d.ts +26 -0
  84. package/dist/rasterizer/web/index.js +21 -0
  85. package/dist/rasterizer/web/pdfium.d.ts +213 -0
  86. package/dist/rasterizer/web/pdfium.js +170 -0
  87. package/dist/rasterizer/web/protocol.d.ts +138 -0
  88. package/dist/rasterizer/web/protocol.js +37 -0
  89. package/dist/rasterizer/web/session.d.ts +106 -0
  90. package/dist/rasterizer/web/session.js +413 -0
  91. package/dist/rasterizer/web/worker.d.ts +44 -0
  92. package/dist/rasterizer/web/worker.js +76 -0
  93. package/dist/react/PdfContentView.d.ts +98 -0
  94. package/dist/react/PdfContentView.js +189 -0
  95. package/dist/react/index.d.ts +6 -0
  96. package/dist/react/index.js +3 -0
  97. package/dist/react/usePdfDocument.d.ts +134 -0
  98. package/dist/react/usePdfDocument.js +275 -0
  99. package/dist/react/usePdfLayer.d.ts +94 -0
  100. package/dist/react/usePdfLayer.js +388 -0
  101. package/dist/skia/index.d.ts +26 -0
  102. package/dist/skia/index.js +25 -0
  103. package/dist/skia/index.native.d.ts +14 -0
  104. package/dist/skia/index.native.js +16 -0
  105. package/dist/skia/index.web.d.ts +12 -0
  106. package/dist/skia/index.web.js +14 -0
  107. package/dist/skia/resolve.native.d.ts +18 -0
  108. package/dist/skia/resolve.native.js +22 -0
  109. package/dist/skia/resolve.web.d.ts +41 -0
  110. package/dist/skia/resolve.web.js +62 -0
  111. package/dist/skia/skia-api.d.ts +94 -0
  112. package/dist/skia/skia-api.js +113 -0
  113. package/dist/testing/index.d.ts +27 -0
  114. package/dist/testing/index.js +28 -0
  115. package/dist/testing/scenes.d.ts +100 -0
  116. package/dist/testing/scenes.js +267 -0
  117. package/dist/types.d.ts +611 -0
  118. package/dist/types.js +21 -0
  119. package/dist/web-init.d.ts +68 -0
  120. package/dist/web-init.js +64 -0
  121. package/ios/Package.swift +58 -0
  122. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
  123. package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
  124. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
  125. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
  126. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
  127. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
  128. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
  129. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
  130. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
  131. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
  132. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
  133. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
  134. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
  135. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
  136. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
  137. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
  138. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
  139. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
  140. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
  141. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
  142. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
  143. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
  144. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
  145. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
  146. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
  147. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
  148. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
  149. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
  150. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
  151. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
  152. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
  153. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
  154. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
  155. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
  156. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
  157. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
  158. package/package.json +144 -0
  159. package/react-native.config.js +40 -0
  160. package/scripts/check-skia-peer.mjs +86 -0
  161. 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;