@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,308 @@
1
+ /**
2
+ * The PDFium half of the web backend: open a document, measure its pages,
3
+ * render an arbitrary doc-space rect.
4
+ *
5
+ * PURE IN THE SAME SENSE `../android.ts` IS. It takes a `PdfiumBinding` and
6
+ * imports nothing else — no worker, no DOM, no `@embedpdf/pdfium`. Which means
7
+ * the interesting half of this backend (rect registration, stride, the
8
+ * background fill, `/Rotate`, the error codes) is exercised in vitest under
9
+ * plain node against the REAL WASM module, not against a mock of it. See
10
+ * `__tests__/web-rasterizer.test.ts`.
11
+ *
12
+ * ------------------------------------------------------------------------
13
+ * THE RECT MECHANISM — negative offsets, not a matrix
14
+ * ------------------------------------------------------------------------
15
+ *
16
+ * PDFium's public way to draw part of a page is to describe the WHOLE page at
17
+ * the target scale and then slide it: `FPDF_RenderPageBitmap(bitmap, page,
18
+ * -x*s, -y*s, pageW*s, pageH*s, 0, flags)` into a bitmap that is only the tile's
19
+ * size. Everything outside the bitmap is clipped away by the bitmap itself.
20
+ *
21
+ * The alternative, `FPDF_RenderPageBitmapWithMatrix` with
22
+ * `FS_MATRIX{a:s, b:0, c:0, d:s, e:-x*s, f:-y*s}` and an `FS_RECTF(0,0,w,h)`
23
+ * clip, was MEASURED to be byte-identical to this on every rect tested —
24
+ * including a rect whose device offset is fractional (x*s = 33.6999…), where the
25
+ * two might plausibly have rounded differently and do not. The matrix form also
26
+ * has a trap this one does not: passing a NULL clip renders NOTHING AT ALL
27
+ * (measured: an all-white bitmap, no error). The simpler mechanism with no null
28
+ * case is the one that ships.
29
+ *
30
+ * ------------------------------------------------------------------------
31
+ * WHY THE OFFSET IS `-round(x*s)` AND NOT `round(-x*s)`
32
+ * ------------------------------------------------------------------------
33
+ *
34
+ * Because the tile's origin inside a hypothetical full-page render is
35
+ * `round(x*s)`, and the registration property is that the tile equals that
36
+ * region exactly. Negating the rounded origin makes those two the same integer
37
+ * by construction. `Math.round(-0.5)` is `-0` while `-Math.round(0.5)` is `-1`,
38
+ * so the two spellings genuinely differ on half-pixel boundaries — which is
39
+ * exactly where a misregistration would be least likely to be noticed and most
40
+ * likely to be real.
41
+ */
42
+ import { FPDF_ANNOT, FPDF_BITMAP_BGRA, FPDF_REVERSE_BYTE_ORDER, loadError, } from './pdfium.js';
43
+ import { PdfError } from '../../types.js';
44
+ /**
45
+ * MEASURED, not inherited from the other backends. An antialiased opaque-white
46
+ * edge over a transparent page reads `[255, 255, 255, 50]` and
47
+ * `[255, 255, 255, 178]` out of PDFium; premultiplied would have been
48
+ * `[50, 50, 50, 50]` and `[178, 178, 178, 178]`. So the web backend declares
49
+ * `'straight'` and Android declares `'premultiplied'`, which is the entire
50
+ * reason `RasterPixels.alpha` is a required field rather than a constant in
51
+ * `ingest.ts`.
52
+ *
53
+ * INVISIBLE UNDER `background: 'white'` — every pixel is opaque there and the
54
+ * two encodings are byte-identical — so this can only ever be caught by a test
55
+ * that renders `background: 'transparent'`. There is one.
56
+ */
57
+ const PDFIUM_ALPHA = 'straight';
58
+ /** See `FPDF_REVERSE_BYTE_ORDER`: the flag is always passed, so this follows. */
59
+ const PDFIUM_FORMAT = 'rgba8888';
60
+ /** ARGB. Both of the two values used are symmetric under an R/B swap, which is
61
+ * what makes filling BEFORE a reverse-byte-order render correct rather than
62
+ * lucky: `FPDFBitmap_FillRect` writes in the bitmap's own BGRA order. */
63
+ const OPAQUE_WHITE = 0xffffffff;
64
+ const TRANSPARENT = 0x00000000;
65
+ /** PDFium's ints are 32-bit; a request past this cannot be expressed at all. */
66
+ const MAX_INT32 = 0x7fffffff;
67
+ const ROTATIONS = [0, 90, 180, 270];
68
+ /* ------------------------------------------------------------------ *
69
+ * Open
70
+ * ------------------------------------------------------------------ */
71
+ /**
72
+ * Read every page's size and rotation without loading a single page.
73
+ *
74
+ * This is what makes `RasterizerHandle.pageGeometry` synchronous. Both calls
75
+ * work off the page DICTIONARY — no content stream is parsed — so the cost is
76
+ * proportional to the page tree and not to what is drawn on it.
77
+ *
78
+ * A page that cannot be measured fails the OPEN rather than being papered over
79
+ * with a zero size. A `PageGeometry` is what the layout function turns into the
80
+ * document's whole coordinate space; a wrong one there is not a missing page,
81
+ * it is every page after it in the wrong place.
82
+ */
83
+ function readGeometry(api, document, pageCount) {
84
+ const sizeOut = api.malloc(8);
85
+ try {
86
+ const pages = [];
87
+ for (let index = 0; index < pageCount; index++) {
88
+ if (!api.FPDF_GetPageSizeByIndexF(document, index, sizeOut)) {
89
+ throw new PdfError('corrupt', `PDFium could not read the size of page ${index} of ${pageCount}.`);
90
+ }
91
+ const rotationIndex = api.EPDF_GetPageRotationByIndex(document, index);
92
+ pages.push({
93
+ index,
94
+ // ALREADY POST-ROTATION, and measured to be so — see
95
+ // `FPDF_GetPageSizeByIndexF`. Swapping width and height here for a
96
+ // 90/270 page would rotate it twice.
97
+ width: api.getFloat(sizeOut),
98
+ height: api.getFloat(sizeOut + 4),
99
+ // -1 is PDFium's "no such page", which the size read above has already
100
+ // ruled out; anything else outside 0..3 would be a new return
101
+ // convention. Both fall back to 0 rather than failing the open: an
102
+ // unrotated page whose dimensions are right still draws correctly, and
103
+ // `PageGeometry.rotation` is informational to this package.
104
+ rotation: ROTATIONS[rotationIndex] ?? 0,
105
+ });
106
+ }
107
+ return pages;
108
+ }
109
+ finally {
110
+ api.free(sizeOut);
111
+ }
112
+ }
113
+ /**
114
+ * Open a document from bytes.
115
+ *
116
+ * THE BYTES ARE COPIED INTO THE WASM HEAP AND STAY THERE. `FPDF_LoadMemDocument`
117
+ * does not take ownership and does not copy: PDFium reads the buffer lazily for
118
+ * the whole life of the document, so freeing it early is a use-after-free inside
119
+ * the WASM heap — which does not crash, it produces wrong pixels. `close()`
120
+ * frees it, after `FPDF_CloseDocument` and never before.
121
+ */
122
+ export function openPdfiumDocument(api, bytes, password) {
123
+ if (bytes.byteLength === 0) {
124
+ throw new PdfError('corrupt', 'The document is empty (0 bytes).');
125
+ }
126
+ const dataPointer = api.malloc(bytes.byteLength);
127
+ if (dataPointer === 0) {
128
+ throw new PdfError('out-of-memory', `Could not allocate ${bytes.byteLength} bytes in the PDFium heap for ` +
129
+ 'the document.');
130
+ }
131
+ api.heapU8().set(bytes, dataPointer);
132
+ const document = api.FPDF_LoadMemDocument(dataPointer, bytes.byteLength, password ?? '');
133
+ if (document === 0) {
134
+ const error = api.FPDF_GetLastError();
135
+ api.free(dataPointer);
136
+ throw loadError(error, password !== undefined && password !== '');
137
+ }
138
+ let pages;
139
+ const pageCount = api.FPDF_GetPageCount(document);
140
+ try {
141
+ pages = readGeometry(api, document, pageCount);
142
+ }
143
+ catch (error) {
144
+ api.FPDF_CloseDocument(document);
145
+ api.free(dataPointer);
146
+ throw error;
147
+ }
148
+ return createDocument(api, document, dataPointer, pages);
149
+ }
150
+ /* ------------------------------------------------------------------ *
151
+ * Render
152
+ * ------------------------------------------------------------------ */
153
+ function createDocument(api, document, dataPointer, pages) {
154
+ let closed = false;
155
+ /**
156
+ * ONE loaded page, cached.
157
+ *
158
+ * `FPDF_LoadPage` is the call that parses the content stream, and an epoch is
159
+ * several tiles of the same page — so caching turns N parses into one. It is
160
+ * capped at ONE rather than being an LRU on purpose: a parsed page lives in
161
+ * the Emscripten heap, and that heap only ever grows (it never returns pages
162
+ * to the OS), so "how many parsed pages may be resident" is a permanent floor
163
+ * on the tab's memory and not a cache-tuning question. Alternating between two
164
+ * pages costs a reparse; holding both costs the session.
165
+ */
166
+ let loadedIndex = -1;
167
+ let loadedPage = 0;
168
+ const releasePage = () => {
169
+ if (loadedPage !== 0) {
170
+ api.FPDF_ClosePage(loadedPage);
171
+ loadedPage = 0;
172
+ loadedIndex = -1;
173
+ }
174
+ };
175
+ const requirePage = (index) => {
176
+ const geometry = pages[index];
177
+ if (geometry === undefined) {
178
+ throw new PdfError('not-found', `Document has ${pages.length} pages; asked for ${index}.`);
179
+ }
180
+ return geometry;
181
+ };
182
+ const loadPage = (index) => {
183
+ if (loadedIndex === index && loadedPage !== 0)
184
+ return loadedPage;
185
+ releasePage();
186
+ const page = api.FPDF_LoadPage(document, index);
187
+ if (page === 0) {
188
+ // Not `FPDF_GetLastError` — see `PDFIUM_ERR`, it is stale here. The index
189
+ // was already bounds-checked against the geometry read at open, so a
190
+ // failure at this point is a page whose content PDFium refuses.
191
+ throw new PdfError('corrupt', `PDFium could not load page ${index}. The page exists (its size was ` +
192
+ 'read at open) but its content could not be parsed.');
193
+ }
194
+ loadedIndex = index;
195
+ loadedPage = page;
196
+ return page;
197
+ };
198
+ return {
199
+ pageCount: pages.length,
200
+ pages,
201
+ render(request) {
202
+ if (closed) {
203
+ throw new PdfError('backend-failure', 'The document is closed.');
204
+ }
205
+ const geometry = requirePage(request.page);
206
+ const { docRect, scale } = request;
207
+ if (!Number.isFinite(scale) || scale <= 0) {
208
+ throw new PdfError('backend-failure', `Scale must be a positive finite number, got ${scale}.`);
209
+ }
210
+ if (!Number.isFinite(docRect.x) ||
211
+ !Number.isFinite(docRect.y) ||
212
+ !(docRect.width > 0) ||
213
+ !(docRect.height > 0)) {
214
+ throw new PdfError('backend-failure', `Doc rect must be finite and have positive extent, got ` +
215
+ `${docRect.width}x${docRect.height} at ` +
216
+ `(${docRect.x}, ${docRect.y}).`);
217
+ }
218
+ // At least one pixel: a sub-pixel rect is a legitimate ask at low zoom,
219
+ // and a zero-sized bitmap is not a thing PDFium (or Skia) accepts.
220
+ const width = Math.max(1, Math.round(docRect.width * scale));
221
+ const height = Math.max(1, Math.round(docRect.height * scale));
222
+ // See the header: the negation of the ROUNDED origin, not the rounding of
223
+ // the negated one.
224
+ const startX = -Math.round(docRect.x * scale);
225
+ const startY = -Math.round(docRect.y * scale);
226
+ const pageWidth = Math.round(geometry.width * scale);
227
+ const pageHeight = Math.round(geometry.height * scale);
228
+ if (width * height > MAX_INT32 / 4 ||
229
+ pageWidth > MAX_INT32 ||
230
+ pageHeight > MAX_INT32) {
231
+ throw new PdfError('out-of-memory', `A ${width}x${height} raster of a ${pageWidth}x${pageHeight} page ` +
232
+ 'cannot be expressed in PDFium’s 32-bit ints. Lower ' +
233
+ '`RasterPolicy.maxPieceDimension` or `maxRasterPixels`.');
234
+ }
235
+ const bitmap = api.FPDFBitmap_CreateEx(width, height, FPDF_BITMAP_BGRA, 0, 0);
236
+ if (bitmap === 0) {
237
+ throw new PdfError('out-of-memory', `PDFium could not allocate a ${width}x${height} bitmap ` +
238
+ `(${width * height * 4} bytes).`);
239
+ }
240
+ try {
241
+ // BEFORE THE RENDER, ALWAYS, AND NOT OPTIONAL. PDFium composites onto
242
+ // the destination and never clears it; a freshly allocated buffer is
243
+ // transparent black, which under the app's annotation layers reads as a
244
+ // BLACK page rather than a blank one. Ingest cannot repair this — it
245
+ // has no way to tell "the PDF painted black here" from "nothing painted
246
+ // here".
247
+ // The return is CHECKED, not discarded: the comment above says the fill
248
+ // is not optional, and a silently-failed fill is exactly the failure it
249
+ // describes — a black page that ingest cannot tell from painted black.
250
+ const filled = api.FPDFBitmap_FillRect(bitmap, 0, 0, width, height, request.background === 'white' ? OPAQUE_WHITE : TRANSPARENT);
251
+ if (!filled) {
252
+ throw new PdfError('backend-failure', `PDFium refused to clear the ${width}x${height} destination ` +
253
+ `bitmap before rendering page ${request.page}. Rendering into it ` +
254
+ 'would composite onto uninitialised memory.');
255
+ }
256
+ const page = loadPage(request.page);
257
+ api.FPDF_RenderPageBitmap(bitmap, page, startX, startY, pageWidth, pageHeight,
258
+ // Device rotation, NOT the page's `/Rotate`. PDFium applies the
259
+ // page's own rotation itself, and `geometry` is already post-rotation
260
+ // — passing anything but 0 here would rotate a second time.
261
+ 0, FPDF_REVERSE_BYTE_ORDER | (request.annotations ? FPDF_ANNOT : 0));
262
+ // READ, NEVER ASSUMED. Measured tight (`width * 4`) at 1, 3, 7, 13, 1023
263
+ // and 1025 px — but "tight today" is not "tight always", and a wrong
264
+ // stride shears the image instead of failing.
265
+ const rowBytes = api.FPDFBitmap_GetStride(bitmap);
266
+ const buffer = api.FPDFBitmap_GetBuffer(bitmap);
267
+ if (buffer === 0 || rowBytes < width * 4) {
268
+ throw new PdfError('backend-failure', `PDFium reported an unusable bitmap: buffer ${buffer}, stride ` +
269
+ `${rowBytes} for a ${width}px row.`);
270
+ }
271
+ // `slice` and not `subarray`: this must LEAVE the WASM heap. A view
272
+ // would be invalidated by the next allocation (Emscripten replaces
273
+ // HEAPU8 wholesale when memory grows) and would be neither tight nor
274
+ // transferable. The copy is the price of getting the pixels out, and
275
+ // it is the only one — the result owns an exactly-sized ArrayBuffer,
276
+ // so `isTightBuffer` holds and ingest's `tighten()` has nothing to do.
277
+ //
278
+ // The WHOLE stride of the last row, not Skia's `computeByteSize`
279
+ // minimum. The two are the same for a tight stride (which is every
280
+ // width measured) and differ only by the final row's padding; carrying
281
+ // it costs nothing and keeps `bytes[y * rowBytes + …]` in range for
282
+ // every row, which is how every reader here and in the tests indexes.
283
+ const bytes = api.heapU8().slice(buffer, buffer + rowBytes * height);
284
+ return {
285
+ bytes,
286
+ width,
287
+ height,
288
+ rowBytes,
289
+ format: PDFIUM_FORMAT,
290
+ alpha: PDFIUM_ALPHA,
291
+ };
292
+ }
293
+ finally {
294
+ api.FPDFBitmap_Destroy(bitmap);
295
+ }
296
+ },
297
+ close() {
298
+ if (closed)
299
+ return;
300
+ closed = true;
301
+ releasePage();
302
+ api.FPDF_CloseDocument(document);
303
+ // AFTER the close, never before: PDFium reads this buffer for the whole
304
+ // life of the document.
305
+ api.free(dataPointer);
306
+ },
307
+ };
308
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The web backend's MAIN-THREAD surface.
3
+ *
4
+ * `./worker.ts` is deliberately NOT re-exported. It is an ENTRY POINT, not a
5
+ * module a host imports for its values — it binds itself to `self` at
6
+ * evaluation, and it is the only file that names `@embedpdf/pdfium`, so pulling
7
+ * it in here would drag an optional peer into every web bundle that merely
8
+ * touches the rasterizer registry. It is reached through the package's
9
+ * `./worker` subpath, by a bundler, as a worker.
10
+ *
11
+ * `./engine.ts` and `./session.ts` are re-exported because they are the seam a
12
+ * different host might want: a caller who already owns a PDFium module (a
13
+ * Node-side thumbnail service, an Electron main process) can drive
14
+ * `openPdfiumDocument` directly with no worker at all, which is exactly what the
15
+ * tests do.
16
+ */
17
+ export { createWebRasterizer } from './client.js';
18
+ export type { WebRasterizerOptions, WorkerLike } from './client.js';
19
+ export { openPdfiumDocument } from './engine.js';
20
+ export type { PdfiumDocument, PdfiumRasterBytes } from './engine.js';
21
+ export { bindPdfium, loadError, mapLoadError, FPDF_ANNOT, FPDF_BITMAP_BGRA, FPDF_REVERSE_BYTE_ORDER, PDFIUM_ERR, } from './pdfium.js';
22
+ export type { PdfiumBinding, PdfiumHeap, PdfiumWasmModule } from './pdfium.js';
23
+ export { createWorkerSession } from './session.js';
24
+ export type { FetchLike, FetchResponseLike, WorkerSession, WorkerSessionDeps, } from './session.js';
25
+ export { isWorkerResponse } from './protocol.js';
26
+ export type { CancelRequest, CloseRequest, ErrorResponse, OpenRequest, OpenedResponse, PdfiumDocumentSource, PdfiumWasmSource, RenderRequest, RenderedResponse, WorkerRequest, WorkerResponse, } from './protocol.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The web backend's MAIN-THREAD surface.
3
+ *
4
+ * `./worker.ts` is deliberately NOT re-exported. It is an ENTRY POINT, not a
5
+ * module a host imports for its values — it binds itself to `self` at
6
+ * evaluation, and it is the only file that names `@embedpdf/pdfium`, so pulling
7
+ * it in here would drag an optional peer into every web bundle that merely
8
+ * touches the rasterizer registry. It is reached through the package's
9
+ * `./worker` subpath, by a bundler, as a worker.
10
+ *
11
+ * `./engine.ts` and `./session.ts` are re-exported because they are the seam a
12
+ * different host might want: a caller who already owns a PDFium module (a
13
+ * Node-side thumbnail service, an Electron main process) can drive
14
+ * `openPdfiumDocument` directly with no worker at all, which is exactly what the
15
+ * tests do.
16
+ */
17
+ export { createWebRasterizer } from './client.js';
18
+ export { openPdfiumDocument } from './engine.js';
19
+ export { bindPdfium, loadError, mapLoadError, FPDF_ANNOT, FPDF_BITMAP_BGRA, FPDF_REVERSE_BYTE_ORDER, PDFIUM_ERR, } from './pdfium.js';
20
+ export { createWorkerSession } from './session.js';
21
+ export { isWorkerResponse } from './protocol.js';
@@ -0,0 +1,213 @@
1
+ /**
2
+ * The PDFium surface this package uses, and the adapter that produces it.
3
+ *
4
+ * ------------------------------------------------------------------------
5
+ * WHY A STRUCTURAL INTERFACE INSTEAD OF `@embedpdf/pdfium`'s OWN TYPE
6
+ * ------------------------------------------------------------------------
7
+ *
8
+ * Same discipline as `AndroidPdfNativeModule` in `../android.ts`, and for the
9
+ * same two reasons.
10
+ *
11
+ * 1. THE ENGINE STAYS PURE AND TESTABLE. `./engine.ts` takes a
12
+ * `PdfiumBinding` and never names the package, so the whole open/geometry/
13
+ * render/error path runs in vitest against the real WASM module — or
14
+ * against a stub — with no worker, no DOM and no bundler in the loop.
15
+ * 2. THE PUBLISHED TYPES CARRY NO PEER. `@embedpdf/pdfium` is an OPTIONAL peer
16
+ * dependency: a consumer who only ships native must not have their build
17
+ * break on a missing module referenced from our `.d.ts`. Nothing in this
18
+ * file imports it, not even as a type. `./worker.ts` is the one module that
19
+ * names it at all, and it exports nothing typed by it.
20
+ *
21
+ * The obvious risk of a hand-written structural type is that it drifts from the
22
+ * real module and nobody notices until runtime. It cannot here: `./worker.ts`
23
+ * calls `bindPdfium(await init(...))` with the genuine `WrappedPdfiumModule`,
24
+ * and that file IS type-checked by `tsc --noEmit`. A wrong arity or a renamed
25
+ * export fails the build.
26
+ *
27
+ * ------------------------------------------------------------------------
28
+ * EVERY CONSTANT AND SEMANTIC BELOW WAS MEASURED AGAINST 2.15.0, NOT READ
29
+ * ------------------------------------------------------------------------
30
+ *
31
+ * The notes that say "measured" mean a script that called the real WASM module
32
+ * under node and printed the answer. See each one.
33
+ */
34
+ import { PdfError } from '../../types.js';
35
+ import type { PdfErrorCode } from '../../types.js';
36
+ /**
37
+ * `FPDFBitmap_BGRA`. The only four-channel format PDFium's public bitmap API
38
+ * offers, and the one every render path below creates.
39
+ *
40
+ * It describes the BUFFER, not the render: adding `FPDF_REVERSE_BYTE_ORDER` to
41
+ * the render flags leaves the bitmap tagged BGRA while the bytes come out RGBA.
42
+ * That is not a contradiction to fix — the flag is documented as acting on the
43
+ * composite, and it is why `renderPage` derives `PixelFormat` from the FLAGS it
44
+ * passed rather than from the bitmap format it asked for.
45
+ */
46
+ export declare const FPDF_BITMAP_BGRA = 4;
47
+ /** Draw PDF-embedded annotations. Answers `RasterRequest.annotations`. */
48
+ export declare const FPDF_ANNOT = 1;
49
+ /**
50
+ * Write RGBA instead of BGRA.
51
+ *
52
+ * MEASURED: with the flag, a `0.2 0.4 0.9 rg` fill reads `[51, 102, 230, 255]`;
53
+ * without it, `[230, 102, 51, 255]`. Same bitmap format, swapped R and B.
54
+ *
55
+ * THE WEB BACKEND ALWAYS PASSES IT, and that is a deliberate choice about the
56
+ * INGEST side rather than about PDFium. `imageFromPixels` maps `'bgra8888'` to
57
+ * `ColorType.BGRA_8888`, which CanvasKit exposes but which a GPU-backed build
58
+ * may still refuse from `MakeImage` — and the recovery for that is a full extra
59
+ * pass over the buffer swizzling R and B in JS. RGBA is the configuration every
60
+ * Skia backend accepts, PDFium produces it for free inside the composite it was
61
+ * already doing, and the JS-side fallback pass never runs.
62
+ */
63
+ export declare const FPDF_REVERSE_BYTE_ORDER = 16;
64
+ /**
65
+ * `FPDF_GetLastError()`'s codes.
66
+ *
67
+ * ONLY MEANINGFUL IMMEDIATELY AFTER A FAILED DOCUMENT LOAD. MEASURED: after
68
+ * loading an encrypted document without a password (which sets 4), a subsequent
69
+ * `FPDF_GetPageSizeByIndexF` for an out-of-range index returns false and leaves
70
+ * the error at 4 — the stale value from the earlier call. So this table is
71
+ * consulted in exactly one place, `mapLoadError`, and every other failure in
72
+ * this backend is decided by its own bounds check or return value.
73
+ */
74
+ export declare const PDFIUM_ERR: {
75
+ readonly SUCCESS: 0;
76
+ readonly UNKNOWN: 1;
77
+ readonly FILE: 2;
78
+ readonly FORMAT: 3;
79
+ readonly PASSWORD: 4;
80
+ readonly SECURITY: 5;
81
+ readonly PAGE: 6;
82
+ };
83
+ /**
84
+ * The WASM heap, reached through FUNCTIONS rather than a captured view.
85
+ *
86
+ * `heapU8()` is a call and must stay one. Emscripten REPLACES `Module.HEAPU8`
87
+ * whenever the heap grows, so a reference captured at bind time detaches the
88
+ * moment a large bitmap is allocated and every read after that is out of a dead
89
+ * buffer. This is the single easiest way to write a web PDF backend that works
90
+ * on small documents and silently produces garbage on big ones.
91
+ */
92
+ export interface PdfiumHeap {
93
+ heapU8(): Uint8Array;
94
+ malloc(byteLength: number): number;
95
+ free(pointer: number): void;
96
+ /** Reads an `FS_SIZEF` component — PDFium's float32. */
97
+ getFloat(pointer: number): number;
98
+ }
99
+ /**
100
+ * Exactly the PDFium entry points this backend calls. Nothing speculative:
101
+ * text, search, links and form filling are all absent because none of them is
102
+ * implemented, and `RasterizerCapabilities` says so.
103
+ */
104
+ export interface PdfiumBinding extends PdfiumHeap {
105
+ /**
106
+ * Does NOT copy — the buffer must stay allocated and untouched for the whole
107
+ * life of the document. `./engine.ts` owns that lifetime and frees the
108
+ * pointer in `close()`.
109
+ */
110
+ FPDF_LoadMemDocument(dataPointer: number, byteLength: number, password: string): number;
111
+ FPDF_CloseDocument(document: number): void;
112
+ FPDF_GetLastError(): number;
113
+ FPDF_GetPageCount(document: number): number;
114
+ /**
115
+ * Writes an `FS_SIZEF` (two float32s) and does NOT parse the page's content
116
+ * stream — PDFium builds a bare `CPDF_Page` from the page dictionary and reads
117
+ * its dimensions. That is what makes `RasterizerHandle.pageGeometry`
118
+ * synchronous and cheap: every page is measured once at open.
119
+ *
120
+ * MEASURED, and it is the whole reason `/Rotate` needs no arithmetic here: for
121
+ * a 200x100 MediaBox with `/Rotate 90` it reports 100x200, and for a 280x360
122
+ * CropBox with `/Rotate 270` it reports 360x280. CropBox, intersected with
123
+ * MediaBox, AFTER rotation — which is precisely what `PageGeometry.width` and
124
+ * `.height` are defined as.
125
+ *
126
+ * Contrast `EPDF_GetPageSizeByIndexNormalized`, which sounds like the one you
127
+ * want and is the opposite: it normalises the rotation AWAY and reports
128
+ * 200x100 for the same rotated page.
129
+ */
130
+ FPDF_GetPageSizeByIndexF(document: number, index: number, sizeOut: number): boolean;
131
+ /**
132
+ * `@embedpdf/pdfium`'s extension. Returns PDFium's rotation INDEX — 0, 1, 2 or
133
+ * 3 for 0/90/180/270 — or -1 for an index the document does not have
134
+ * (measured). The plain `FPDFPage_GetRotation` would need a loaded page, which
135
+ * is exactly what `pageGeometry` is forbidden to do.
136
+ */
137
+ EPDF_GetPageRotationByIndex(document: number, index: number): number;
138
+ FPDF_LoadPage(document: number, index: number): number;
139
+ FPDF_ClosePage(page: number): void;
140
+ /**
141
+ * `firstScan: 0` lets PDFium allocate; `stride: 0` lets it choose. Returns 0
142
+ * when the allocation fails, which is the only out-of-memory signal there is
143
+ * (measured: a 100000x100000 request returns 0 rather than throwing).
144
+ */
145
+ FPDFBitmap_CreateEx(width: number, height: number, format: number, firstScan: number, stride: number): number;
146
+ FPDFBitmap_Destroy(bitmap: number): void;
147
+ /** `color` is ARGB, so 0xFFFFFFFF is opaque white and 0 is transparent. */
148
+ FPDFBitmap_FillRect(bitmap: number, left: number, top: number, width: number, height: number, color: number): boolean;
149
+ FPDFBitmap_GetBuffer(bitmap: number): number;
150
+ FPDFBitmap_GetStride(bitmap: number): number;
151
+ FPDF_RenderPageBitmap(bitmap: number, page: number, startX: number, startY: number, sizeX: number, sizeY: number, rotate: number, flags: number): void;
152
+ }
153
+ /**
154
+ * The shape of `@embedpdf/pdfium`'s `WrappedPdfiumModule`, restated as the
155
+ * subset this package touches.
156
+ *
157
+ * `PdfiumRuntimeMethods.wasmExports` is where `malloc` / `free` live; the FPDF_*
158
+ * entries are `cwrap`ped onto the wrapper object itself.
159
+ */
160
+ export interface PdfiumWasmModule {
161
+ pdfium: {
162
+ HEAPU8: Uint8Array;
163
+ /**
164
+ * `number | bigint` because Emscripten's `getValue` reads `i64` too. This
165
+ * backend only ever asks for `'float'`, so the bigint arm is unreachable —
166
+ * but the declaration is the real one, and `bindPdfium` narrows rather than
167
+ * asserting. (This is the drift check working: the first hand-written
168
+ * signature said `number`, and `tsc` refused the real module.)
169
+ */
170
+ getValue(pointer: number, type: string): number | bigint;
171
+ wasmExports: {
172
+ malloc(byteLength: number): number;
173
+ free(pointer: number): void;
174
+ };
175
+ };
176
+ /** Initialises the library. Must be called once before anything else. */
177
+ PDFiumExt_Init(): null;
178
+ FPDF_LoadMemDocument(dataPointer: number, byteLength: number, password: string): number;
179
+ FPDF_CloseDocument(document: number): null;
180
+ FPDF_GetLastError(): number;
181
+ FPDF_GetPageCount(document: number): number;
182
+ FPDF_GetPageSizeByIndexF(document: number, index: number, sizeOut: number): boolean;
183
+ EPDF_GetPageRotationByIndex(document: number, index: number): number;
184
+ FPDF_LoadPage(document: number, index: number): number;
185
+ FPDF_ClosePage(page: number): null;
186
+ FPDFBitmap_CreateEx(width: number, height: number, format: number, firstScan: number, stride: number): number;
187
+ FPDFBitmap_Destroy(bitmap: number): null;
188
+ FPDFBitmap_FillRect(bitmap: number, left: number, top: number, width: number, height: number, color: number): boolean;
189
+ FPDFBitmap_GetBuffer(bitmap: number): number;
190
+ FPDFBitmap_GetStride(bitmap: number): number;
191
+ FPDF_RenderPageBitmap(bitmap: number, page: number, startX: number, startY: number, sizeX: number, sizeY: number, rotate: number, flags: number): null;
192
+ }
193
+ /**
194
+ * Turn an initialised `@embedpdf/pdfium` module into a `PdfiumBinding`.
195
+ *
196
+ * Deliberately mechanical. The only judgement in here is `heapU8`, which reads
197
+ * `HEAPU8` off the module on EVERY call rather than closing over it — see
198
+ * `PdfiumHeap`.
199
+ */
200
+ export declare function bindPdfium(module: PdfiumWasmModule): PdfiumBinding;
201
+ /**
202
+ * `FPDF_GetLastError()` after a failed load -> the frozen `PdfErrorCode` union.
203
+ *
204
+ * `passwordSupplied` is what tells the two password outcomes apart, and it has
205
+ * to: MEASURED, PDFium returns 4 both for an encrypted document opened with no
206
+ * password and for one opened with the WRONG password. There is no code that
207
+ * distinguishes them, so the caller's own knowledge does — and the distinction
208
+ * matters, because one of them means "prompt the user" and the other means
209
+ * "tell them they got it wrong".
210
+ */
211
+ export declare function mapLoadError(error: number, passwordSupplied: boolean): PdfErrorCode;
212
+ /** The load failure, as the error a host can branch on. */
213
+ export declare function loadError(error: number, passwordSupplied: boolean): PdfError;