@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,611 @@
1
+ /**
2
+ * The type contract for @reekon-tools/react-native-pdf-canvas.
3
+ *
4
+ * THE ONE RULE: the transform decides WHEN to rasterize, never WHAT to draw.
5
+ *
6
+ * Every rect in this file is in DOC SPACE — PDF points, y-down, origin at the
7
+ * page's top-left. That is exactly what PDFium's CPDF_Page::GetDisplayMatrix()
8
+ * emits, so a PDFium backend is a pass-through and every other backend adapts
9
+ * to it. There is deliberately no `coordinateSystem` option: two conventions in
10
+ * one package is how device pixel ratio gets applied twice.
11
+ *
12
+ * Nothing here imports React, react-native, or reanimated. The only external
13
+ * type is SkImage, and it is type-only.
14
+ */
15
+ import type { SkImage } from '@shopify/react-native-skia';
16
+ /** A rect in doc space: PDF points, y-down, origin top-left. */
17
+ export interface DocRect {
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ }
23
+ export interface DocSize {
24
+ width: number;
25
+ height: number;
26
+ }
27
+ /** Device-pixel size. Distinct from DocSize so the two cannot be swapped. */
28
+ export interface PixelSize {
29
+ width: number;
30
+ height: number;
31
+ }
32
+ export type PageRotation = 0 | 90 | 180 | 270;
33
+ /** A page's intrinsic geometry, in PDF points, already accounting for /Rotate. */
34
+ export interface PageGeometry {
35
+ index: number;
36
+ /** CropBox width in points, after rotation is applied. */
37
+ width: number;
38
+ /** CropBox height in points, after rotation is applied. */
39
+ height: number;
40
+ rotation: PageRotation;
41
+ }
42
+ /**
43
+ * What the host tells us about where it is looking. Pushed at settle rate; the
44
+ * package never reads a shared value to obtain it.
45
+ *
46
+ * `scale` is DEVICE PIXELS PER PDF POINT — i.e. zoom * pixelRatio, already
47
+ * combined. The package never derives it, because a host may wrap the scene in
48
+ * a further transform the package cannot see.
49
+ */
50
+ export interface ViewportSnapshot {
51
+ visibleDocRect: DocRect;
52
+ scale: number;
53
+ }
54
+ export type PdfSource = {
55
+ uri: string;
56
+ password?: string;
57
+ } | {
58
+ data: Uint8Array;
59
+ password?: string;
60
+ };
61
+ export type PdfErrorCode = 'not-found' | 'password-required' | 'password-incorrect' | 'corrupt' | 'unsupported' | 'cancelled' | 'out-of-memory' | 'backend-failure';
62
+ export declare class PdfError extends Error {
63
+ readonly code: PdfErrorCode;
64
+ constructor(code: PdfErrorCode, message: string, options?: {
65
+ cause?: unknown;
66
+ });
67
+ }
68
+ /**
69
+ * Byte order of a raster. The three backends produce different orderings and
70
+ * the descriptor travels with the bytes rather than being assumed:
71
+ * PDFium emits BGRA natively (or RGBA with FPDF_REVERSE_BYTE_ORDER), Android
72
+ * Bitmap ARGB_8888 is RGBA in memory, CGBitmapContext is configured explicitly.
73
+ */
74
+ export type PixelFormat = 'rgba8888' | 'bgra8888';
75
+ /**
76
+ * Whether a raster's colour channels have been multiplied by its alpha channel.
77
+ *
78
+ * SAME STANDING AS `PixelFormat`, and required for the same reason: the engines
79
+ * genuinely disagree, so the descriptor travels with the bytes rather than being
80
+ * assumed. Getting the byte order wrong swaps red and blue; getting this wrong
81
+ * darkens or brightens every partially-transparent pixel, which is subtler and
82
+ * therefore worse.
83
+ *
84
+ * `'premultiplied'` — each channel is already scaled by alpha, so a 62%-covered
85
+ * white edge is stored `[158, 158, 158, 158]`. Android's `Bitmap` ARGB_8888 is
86
+ * this by default (`isPremultiplied()` is true unless a caller opts out) and so
87
+ * is a `CGBitmapContext` created `kCGImageAlphaPremultipliedFirst`.
88
+ *
89
+ * `'straight'` — the channels carry the colour and alpha carries the coverage
90
+ * independently, so the same edge is stored `[255, 255, 255, 158]`. MEASURED:
91
+ * PDFium reads `[255, 255, 255, 157]` and pdf.js `[255, 255, 255, 160]` for an
92
+ * antialiased opaque-white edge over a transparent page; premultiplied would
93
+ * have been `~[157, 157, 157, 157]`. A canvas `getImageData` buffer is likewise
94
+ * straight (a 50% grey patch reads `[128, 128, 128, 128]`).
95
+ *
96
+ * NOT DEFAULTABLE, on purpose. Two of the three engines this package knows about
97
+ * emit `'straight'` and one emits `'premultiplied'`, so there is no default that
98
+ * is right more often than it is wrong — and a default is exactly how a global
99
+ * `AlphaType.Premul` constant survived in `ingest.ts` until the first backend
100
+ * that renders `background: 'transparent'` would have been silently wrong.
101
+ *
102
+ * INVISIBLE UNDER `background: 'white'`. A fully opaque pixel is byte-identical
103
+ * in both encodings, which is why this can be stated wrongly for a long time
104
+ * without anything going visibly bad. It only bites on a transparent
105
+ * background — which is in the `RasterRequest` contract.
106
+ */
107
+ export type AlphaEncoding = 'premultiplied' | 'straight';
108
+ /**
109
+ * How a backend's bytes reached JS.
110
+ *
111
+ * Not a capability in the "can it do X" sense — it is a fact about the build and
112
+ * the device, resolved at runtime, and the reason it is surfaced at all is that
113
+ * the difference between these two is ~85x on a tile. A silent fallback is how a
114
+ * transport rewrite ships as no change whatsoever, so both
115
+ * `RasterizerCapabilities.transport` and `RasterPhaseTimings.transport` carry it:
116
+ * the first says what was negotiated, the second says what each tile actually
117
+ * used.
118
+ *
119
+ * - `base64` — a string across the bridge, decoded in JS. Works everywhere.
120
+ * - `jsi-arraybuffer` — an ArrayBuffer wrapping native memory, no string and no
121
+ * decode.
122
+ * - `direct` — the backend produced the bytes in JS already (the fake one).
123
+ */
124
+ export type RasterTransport = 'base64' | 'jsi-arraybuffer' | 'direct';
125
+ /**
126
+ * What a backend spent getting its pixels as far as `RasterPixels`.
127
+ *
128
+ * OPTIONAL AND PURELY OBSERVATIONAL. Nothing in the package reads these to make
129
+ * a decision; the controller copies them into a `raster-phases` diagnostic and
130
+ * that is all. A backend that reports nothing is not degraded in any way.
131
+ *
132
+ * It exists because the cost of a tile is split across two modules that never
133
+ * see each other: the BACKEND owns the transport (a bridge hop, a base64
134
+ * decode), and the CONTROLLER owns the ingest (`Skia.Data.fromBytes` +
135
+ * `Skia.Image.MakeImage`). Subtracting one total from another is how the
136
+ * three terms got conflated in the first place, and the difference matters
137
+ * enormously — a transport rewrite deletes the first two and cannot touch the
138
+ * third.
139
+ */
140
+ export interface RasterPhaseTimings {
141
+ /**
142
+ * The transport round trip, INCLUDING whatever the backend did at the far end
143
+ * of it.
144
+ *
145
+ * On Android this is `await native.render(...)`: bridge marshalling in both
146
+ * directions plus the ~27.5 ms p50 of Java work (open, render, extract,
147
+ * base64-encode) that the instrumented `PdfCanvasPhaseTimingTest` measures
148
+ * separately. Subtracting the Java figure from this one is what isolates
149
+ * marshalling.
150
+ *
151
+ * CONTAMINATED BY CONCURRENCY, and unavoidably so: this is wall time across an
152
+ * `await`, so with more than one render lane it also contains whatever the JS
153
+ * thread did for the OTHER lanes in the meantime. `decodeMs` and `ingestMs`
154
+ * are synchronous blocks and carry no such contamination.
155
+ */
156
+ bridgeMs: number;
157
+ /** Decoding the transport payload into bytes. Zero for a backend with none. */
158
+ decodeMs: number;
159
+ /**
160
+ * Characters of transport payload — the base64 string's length on Android.
161
+ * Zero when a backend moves bytes without stringifying them, which is the
162
+ * whole point of measuring it.
163
+ */
164
+ payloadChars: number;
165
+ /**
166
+ * Whether `bytes` is already exactly its own ArrayBuffer.
167
+ *
168
+ * When false, ingest must copy the whole buffer before Skia sees it (see
169
+ * `isTightBuffer`). Reported rather than inferred so a transport change can be
170
+ * held to it.
171
+ */
172
+ tight: boolean;
173
+ /**
174
+ * Which transport produced these bytes.
175
+ *
176
+ * OPTIONAL, because it was added after the field shipped and a backend that
177
+ * has only ever had one transport has nothing to disambiguate. Present on
178
+ * Android, where two transports coexist and the negotiated one can differ from
179
+ * the one a given tile got — a store that failed mid-session falls the whole
180
+ * rasterizer back, and the tile that triggered it reports `base64` while the
181
+ * capability still says otherwise until the next negotiation.
182
+ */
183
+ transport?: RasterTransport;
184
+ }
185
+ export interface RasterPixels {
186
+ bytes: Uint8Array;
187
+ width: number;
188
+ height: number;
189
+ /** Required. Skia.Image.MakeImage needs it and a wrong value shears the image. */
190
+ rowBytes: number;
191
+ format: PixelFormat;
192
+ /**
193
+ * Required, and deliberately not optional — see `AlphaEncoding`. Every backend
194
+ * states what it produces so none can inherit a default that is wrong for it.
195
+ */
196
+ alpha: AlphaEncoding;
197
+ /**
198
+ * ADDITIVE AND OPTIONAL. `RasterPixels` is the shipped seam that the web
199
+ * backend and the fake rasterizer also implement, so this had to cost them
200
+ * nothing: omitting it is the documented normal case, and no consumer branches
201
+ * on its presence except the controller's diagnostic emit.
202
+ */
203
+ timings?: RasterPhaseTimings;
204
+ }
205
+ export interface RasterRequest {
206
+ page: number;
207
+ /** The doc-space rect to render. Backends render exactly this rect. */
208
+ docRect: DocRect;
209
+ /** Device pixels per PDF point. */
210
+ scale: number;
211
+ /**
212
+ * Whether PDF-embedded annotations are drawn into the raster.
213
+ *
214
+ * TRUE for file rasters (thumbnails, print) — that is what the outgoing
215
+ * react-native-pdf-thumbnail did via PDFKit, and the app depends on it.
216
+ * FALSE for tiles, because the Skia layer draws the app's own annotations and
217
+ * they would double-draw.
218
+ */
219
+ annotations: boolean;
220
+ background: 'white' | 'transparent';
221
+ }
222
+ export interface RasterizerCapabilities {
223
+ /** Can render PDF-embedded annotations into the raster. */
224
+ annotations: boolean;
225
+ /** Honours the abort signal mid-render, not merely between requests. */
226
+ interruptibleRender: boolean;
227
+ text: boolean;
228
+ search: boolean;
229
+ links: boolean;
230
+ /**
231
+ * How many `render` calls this backend can usefully have in flight at once.
232
+ *
233
+ * A PROPERTY OF THE BACKEND, never a constant in the controller. Android's
234
+ * ceiling is the `PdfRendererPool` size — each slot is one more `PdfRenderer`
235
+ * on its own dup'd file descriptor, and a request beyond that blocks on the
236
+ * pool's queue rather than doing anything. A backend with one document handle
237
+ * and no pool declares 1 and is scheduled exactly as it is today.
238
+ *
239
+ * OPTIONAL, DEFAULTING TO 1, and that default is the safe one: an unaware
240
+ * backend keeps the strictly-sequential behaviour it was written against. The
241
+ * controller reads it as `min(bound, plan.items.length)`.
242
+ *
243
+ * IT IS NOT A PROMISE OF SPEEDUP, AND ON ANDROID IT IS A PROMISE OF THE
244
+ * OPPOSITE. Measured on an LG V60 (Snapdragon 865, API 33): AOSP serialises
245
+ * `PdfRenderer` behind a process-wide PDFium lock, so `openPage` throughput is
246
+ * FLAT at 0.084 / 0.073 / 0.074 ops/ms across 1 / 2 / 4 threads while an
247
+ * identical pure-Java workload scales 2.8x — and the lock CONVOYS, so
248
+ * contending for it makes each call slower rather than merely unparallel. The
249
+ * Android backend therefore declares 1. It used to declare 2 to PIPELINE its
250
+ * base64 decode against the next tile's native work; the JSI transport deleted
251
+ * that decode, and re-measuring on the JSI path put every lane count above 1
252
+ * behind it on real documents. See `AndroidRasterizerOptions.poolSize` for the
253
+ * numbers.
254
+ *
255
+ * A backend whose rasterizing genuinely parallelises should still declare it.
256
+ * The lane machinery is not Android-specific and the controller has no opinion;
257
+ * this field is where a backend states what it can actually do.
258
+ */
259
+ maxConcurrentRenders?: number;
260
+ /**
261
+ * How this backend moves pixels into JS, as NEGOTIATED — resolved once when
262
+ * the rasterizer is constructed, not per render.
263
+ *
264
+ * OPTIONAL and observational. Nothing in the package branches on it; it exists
265
+ * so a host can SEE which path is live, because the fallback is invisible from
266
+ * the outside and the two differ by roughly 85x per tile. A backend that omits
267
+ * it is not making any claim.
268
+ */
269
+ transport?: RasterTransport;
270
+ }
271
+ /**
272
+ * The backend seam. Three implementations: platform rasterizers on native, a
273
+ * PDFium worker on web, a deterministic fake for tests. Injected per document,
274
+ * defaulting to the platform module.
275
+ */
276
+ export interface PageRasterizer {
277
+ /** Stable identifier, surfaced in diagnostics so a bug report names a backend. */
278
+ readonly id: string;
279
+ readonly capabilities: RasterizerCapabilities;
280
+ open(source: PdfSource): Promise<RasterizerHandle>;
281
+ }
282
+ export interface RasterizerHandle {
283
+ readonly pageCount: number;
284
+ /** Cheap: must not load a page. */
285
+ pageGeometry(index: number): PageGeometry;
286
+ render(request: RasterRequest, signal?: AbortSignal): Promise<RasterPixels>;
287
+ close(): void;
288
+ }
289
+ export type RasterRole = 'base' | 'detail';
290
+ /**
291
+ * One rasterized piece, ready to draw.
292
+ *
293
+ * INVARIANT: `docRect` is exactly the rect the bytes were rendered for. It is
294
+ * never inset. Adjacent detail pieces OVERLAP rather than abut, because
295
+ * RN Skia's <Image> has no src-rect prop — both draw paths hardcode src to the
296
+ * whole image, so drawing an inset rect would squash the full bitmap into it
297
+ * and misregister every piece against the layers above.
298
+ */
299
+ export interface PdfRaster {
300
+ readonly key: string;
301
+ readonly page: number;
302
+ readonly role: RasterRole;
303
+ readonly image: SkImage;
304
+ readonly docRect: DocRect;
305
+ /** Device pixels per PDF point this raster was rendered at. */
306
+ readonly scale: number;
307
+ readonly epoch: number;
308
+ /**
309
+ * What this raster costs while it is MOUNTED, which is ~2x its pixels.
310
+ *
311
+ * Two allocations, not one. `SkImages::RasterFromData(info, data, rowBytes)`
312
+ * — what `Skia.Image.MakeImage` calls — SHARES the `SkData` and keeps it alive
313
+ * for the image's whole life, and the first time that image is drawn on a GPU
314
+ * canvas Skia uploads a budgeted texture of the same dimensions. Both are
315
+ * resident for as long as the raster is in the tree.
316
+ *
317
+ * `rasterByteLength` books both copies. See its comment for what that does to
318
+ * the meaning of `cacheBudgetBytes`.
319
+ */
320
+ readonly bytes: number;
321
+ }
322
+ export interface PdfPageContent {
323
+ readonly page: number;
324
+ /** Where this page sits in doc space, per the layout function. */
325
+ readonly pageRect: DocRect;
326
+ /**
327
+ * The whole page at fit scale, as one or more pieces whose union is exactly
328
+ * `pageRect`.
329
+ *
330
+ * ANTI-BLANK INVARIANT: never EMPTIED once populated. It was "never nulled
331
+ * once rastered" while this was a single raster; tiling the base makes it a
332
+ * set, and the promise is the same one — for a page inside the live window
333
+ * there is always something drawable under the detail layer. (A page that
334
+ * leaves the window by more than the base radius is deliberately emptied; that
335
+ * is the promise being kept for the pages you are LOOKING at.)
336
+ *
337
+ * Usually length 1. It splits for the same reason detail does — see
338
+ * `RasterPolicy.maxPieceDimension`, which bounds the in-flight transient of a
339
+ * single raster, and `maxTextureDimension`, which a piece may never exceed.
340
+ */
341
+ readonly base: readonly PdfRaster[];
342
+ /** Sharp overlays for the current viewport. Usually length 1. */
343
+ readonly detail: readonly PdfRaster[];
344
+ /** Previous detail set, held across the swap and released next commit. */
345
+ readonly retiring: readonly PdfRaster[];
346
+ }
347
+ /**
348
+ * A NEW FROZEN OBJECT PER EPOCH. Identity change is the swap signal.
349
+ *
350
+ * A "referentially stable handle mutated in place" cannot work: RN Skia's
351
+ * shallowEq is reference equality on every key but children, and prepareUpdate
352
+ * returns null on equality — the new SkImages would never enter the Skia tree,
353
+ * the raster would complete, memory would be spent, and nothing on screen would
354
+ * change. That commit is the expensive thing the cadence policy exists to make
355
+ * rare.
356
+ */
357
+ export interface PdfContent {
358
+ readonly pages: readonly PdfPageContent[];
359
+ readonly epoch: number;
360
+ /** True while any planned raster for the current viewport is still missing. */
361
+ readonly settling: boolean;
362
+ }
363
+ /** Where pages sit in doc space. The package has no opinion. */
364
+ export type PdfPageLayout = (pages: readonly DocSize[]) => DocRect[];
365
+ /**
366
+ * Every constant the cadence depends on. All of these are DERIVED, not
367
+ * measured — see docs. A consumer with letter-size text documents on a DPR-1
368
+ * display swaps two fields rather than forking the package.
369
+ */
370
+ export interface RasterPolicy {
371
+ /**
372
+ * Multiplier on the fit-zoom display scale for the whole-page base raster.
373
+ *
374
+ * NOT a fixed max-edge cap. A 2048px cap is calibrated for Letter pages; on
375
+ * an ARCH-D sheet it yields 0.790 px/pt against a fit display scale of 0.677,
376
+ * so the base is drawn MINIFIED — the aliasing regime, and unfixable because
377
+ * ImageCmd::draw hardcodes kStrict_SrcRectConstraint, which disables mipmaps.
378
+ */
379
+ baseQuality: number;
380
+ /** Hard ceiling on base raster pixels, after baseQuality. */
381
+ baseMaxPixels: number;
382
+ /** Fraction of the visible rect added on each side of a detail raster. */
383
+ detailOverflow: number;
384
+ /**
385
+ * HARDWARE ceiling on a piece's pixel DIMENSION, in device px. A piece may
386
+ * never exceed it: past the limit the texture upload FAILS and the piece draws
387
+ * nothing. 4096 is the only near-universal value.
388
+ *
389
+ * Distinct from `maxPieceDimension` below, and the two must not be merged.
390
+ * This one is a property of the device; that one is a choice about memory.
391
+ */
392
+ maxTextureDimension: number;
393
+ /**
394
+ * MEMORY policy on a piece's pixel DIMENSION, in device px, applied to the
395
+ * base layer and the detail layer alike.
396
+ *
397
+ * Nothing breaks when a piece exceeds it — this is not a limit the hardware
398
+ * has an opinion about. It exists to bound the TRANSIENT: one piece's bytes
399
+ * exist simultaneously as a platform bitmap, a JS `Uint8Array` and an
400
+ * `SkData`, so the whole-flow peak scales with the largest SINGLE raster in
401
+ * flight, not with the epoch's total. Splitting the same total area into more,
402
+ * smaller pieces leaves the resident bytes unchanged and cuts that peak.
403
+ *
404
+ * The effective cap is `min(maxPieceDimension, maxTextureDimension)`.
405
+ */
406
+ maxPieceDimension: number;
407
+ /** Hard ceiling on a single raster request, in pixels. */
408
+ maxRasterPixels: number;
409
+ /**
410
+ * Re-raster when currentScale / epochScale leaves [1/band, band].
411
+ * 1.414 at >=200ppi effective; 1.2 on DPR-1, where pixels are visible before
412
+ * any magnification.
413
+ */
414
+ sharpnessBand: number;
415
+ /**
416
+ * pdf.js's MOVEMENT_THRESHOLD, adopted verbatim. Re-raster when the padding
417
+ * remaining AHEAD of the motion is less than this fraction of the padding
418
+ * behind it.
419
+ */
420
+ movementThreshold: number;
421
+ /** Quiet-timer delay after a scale change (continuous input only). */
422
+ quietZoomMs: number;
423
+ /** Quiet-timer delay after a pan-only change. */
424
+ quietPanMs: number;
425
+ /** Force one raster this long after the last committed one under continued input. */
426
+ maxStaleMs: number;
427
+ /**
428
+ * Floor between epochs, enforced on BOTH paths.
429
+ *
430
+ * PDFKit's draw(with:to:) and Android's PdfRenderer.render are synchronous and
431
+ * uninterruptible once entered, so an abort only prevents the NEXT piece
432
+ * starting. Without a floor a user pinch-nudging three times while reading a
433
+ * dimension never sees a detail raster at all — every render is superseded
434
+ * before it lands. Default to the measured p95 raster time.
435
+ */
436
+ minEpochIntervalMs: number;
437
+ /** Soft cap on cached raster bytes. NOT a memory budget — see docs. */
438
+ cacheBudgetBytes: number;
439
+ /** Keep the previous detail set mounted after its replacement paints. */
440
+ retainPreviousDetail: boolean;
441
+ }
442
+ /** One raster the planner wants to exist. */
443
+ export interface RasterPlanItem {
444
+ key: string;
445
+ page: number;
446
+ role: RasterRole;
447
+ docRect: DocRect;
448
+ scale: number;
449
+ pixels: PixelSize;
450
+ }
451
+ export interface RasterPlan {
452
+ items: RasterPlanItem[];
453
+ /** Scale the detail items were planned at, for the sharpness predicate. */
454
+ detailScale: number;
455
+ /**
456
+ * Bounding rect of everything this plan INTENDS to cover — the detail items
457
+ * plus `baseCoveredDocRect`. Null when the plan covers nothing.
458
+ *
459
+ * It is what the plan wanted, not what a run of it achieved, so it is NOT what
460
+ * an epoch records: see `EpochState.coveredRects`.
461
+ */
462
+ coveredDocRect: DocRect | null;
463
+ /**
464
+ * The part of `coveredDocRect` that no detail ITEM accounts for: the pages
465
+ * whose base is already at least as dense as the detail would be, and which
466
+ * therefore get no detail raster at all (see `planRasters`).
467
+ *
468
+ * Reported separately because the controller rebuilds the epoch's coverage
469
+ * from the rasters it actually PRODUCED, and there is no produced raster to
470
+ * rebuild this half from — the base is standing in for the detail here. A hole
471
+ * in the base itself is caught by the controller's per-piece base check
472
+ * instead, which forces a 'layout' epoch.
473
+ */
474
+ baseCoveredDocRect: DocRect | null;
475
+ }
476
+ /** What the planner needs to know about the current epoch to decide. */
477
+ export interface EpochState {
478
+ detailScale: number;
479
+ /**
480
+ * Where this epoch was RESPONSIBLE for coverage: the doc rect of every page it
481
+ * planned.
482
+ *
483
+ * Coverage is judged over these regions and nowhere else, because most of the
484
+ * doc plane is not page. `continuousVertical` leaves a 12pt gutter between
485
+ * pages and centres narrower ones, so a viewport that spans two pages — the
486
+ * ordinary reading posture — always has doc area in it that NO raster can ever
487
+ * cover. Judging the whole visible rect would report that as a hole and fire
488
+ * 'coverage' on every settle for the life of the document.
489
+ *
490
+ * It is also what answers "am I looking somewhere this epoch never planned":
491
+ * the pages BEYOND these rects are the ones a scroll reaches, and their bounds
492
+ * is the containment test that used to be made against the covered rect.
493
+ */
494
+ pageRects: readonly DocRect[];
495
+ /**
496
+ * EXACTLY the doc rects this epoch has detail-quality coverage of — one entry
497
+ * per raster that was produced, plus the plan's `baseCoveredDocRect` where a
498
+ * base is standing in for detail.
499
+ *
500
+ * A SET, not the bounding rect, and that is the whole point. A tiled epoch
501
+ * that loses one piece (backend error, budget refusal) keeps its bounding rect
502
+ * intact — remove any single cell of a 2x2 grid and the other three still span
503
+ * it — so a bounding-rect predicate reports full coverage over a hole and
504
+ * never re-fires for it. `rectsCover` is an exact union test and does not.
505
+ *
506
+ * Empty means nothing is covered, which fires 'coverage' unconditionally.
507
+ */
508
+ coveredRects: readonly DocRect[];
509
+ committedAtMs: number;
510
+ }
511
+ export interface PdfController {
512
+ /**
513
+ * Continuous input — wheel, trackpad, momentum. Restarts a quiet timer.
514
+ * Cheap and idempotent: an identical plan is a no-op, because any state
515
+ * change costs a full re-record of the CONSUMER'S entire scene.
516
+ */
517
+ hint(viewport: ViewportSnapshot): void;
518
+ /**
519
+ * Discrete end — gesture lift-off, or an animation's completion callback.
520
+ * Evaluates and dispatches immediately, subject to minEpochIntervalMs.
521
+ *
522
+ * This MUST be called by the host. Settle cannot be sniffed: a withDecay
523
+ * fling at 2000px/s animates for ~38 seconds at reanimated's default
524
+ * deceleration, changing the shared value every frame, and `_animation` is
525
+ * cleared only on interruption — never on natural completion.
526
+ */
527
+ settle(viewport: ViewportSnapshot): void;
528
+ /** Gesture begin. Suppresses the continuous path entirely. */
529
+ suppress(): void;
530
+ /** Gesture end. Does not itself dispatch; call settle() for that. */
531
+ resume(): void;
532
+ }
533
+ export type PdfDiagnostic = {
534
+ type: 'raster';
535
+ key: string;
536
+ ms: number;
537
+ pixels: number;
538
+ backend: string;
539
+ }
540
+ /**
541
+ * The same tile as `raster`, broken into the terms a transport change can and
542
+ * cannot remove. Emitted ONLY when the backend reported `RasterPixels.timings`
543
+ * — so the fake backend, and therefore the whole vitest suite, never sees it.
544
+ *
545
+ * Note that `raster.ms` is NOT `totalMs`: `raster` is emitted BEFORE ingest
546
+ * runs, so it covers bridge + decode and stops there. This one is the whole
547
+ * per-tile cost the JS thread can see.
548
+ */
549
+ | {
550
+ type: 'raster-phases';
551
+ key: string;
552
+ backend: string;
553
+ /** Transport round trip including the native work. See RasterPhaseTimings. */
554
+ bridgeMs: number;
555
+ /** The payload decode — a hand-rolled base64 loop on Android today. */
556
+ decodeMs: number;
557
+ /**
558
+ * The `ingest` call and nothing else — `Skia.Data.fromBytes` +
559
+ * `Skia.Image.MakeImage`. The cache put is deliberately outside it.
560
+ *
561
+ * THE TERM NO TRANSPORT CHANGE CAN DELETE. `fromBytes` is
562
+ * `SkData::MakeWithCopy` and the image retains that copy; both survive any
563
+ * rewrite of how the bytes reached JS.
564
+ */
565
+ ingestMs: number;
566
+ totalMs: number;
567
+ payloadChars: number;
568
+ /** Decoded raster bytes, i.e. what a zero-copy transport would move. */
569
+ byteLength: number;
570
+ /** False means ingest had to copy the buffer before Skia saw it. */
571
+ tight: boolean;
572
+ } | {
573
+ type: 'cache-hit';
574
+ key: string;
575
+ } | {
576
+ type: 'evict';
577
+ key: string;
578
+ bytes: number;
579
+ reason: 'budget' | 'page-window' | 'epoch';
580
+ } | {
581
+ type: 'epoch';
582
+ epoch: number;
583
+ items: number;
584
+ reason: EpochReason;
585
+ /**
586
+ * Planned pieces this epoch did NOT mount — a backend failure or a budget
587
+ * refusal. The field probe could only see `settling`, which is one bit for
588
+ * three different situations; this is the number that says which.
589
+ */
590
+ missing: number;
591
+ } | {
592
+ type: 'skip';
593
+ reason: 'min-interval' | 'identical-plan' | 'suppressed';
594
+ }
595
+ /**
596
+ * A re-evaluation the CONTROLLER armed for itself, because an epoch committed
597
+ * with a hole in it and no host input is guaranteed to arrive. See
598
+ * `commit`'s coverage note in `controller.ts`.
599
+ */
600
+ | {
601
+ type: 'repair';
602
+ missing: number;
603
+ inMs: number;
604
+ attempt: number;
605
+ } | {
606
+ type: 'error';
607
+ key: string;
608
+ error: PdfError;
609
+ };
610
+ export type EpochReason = 'initial' | 'coverage' | 'sharpness' | 'look-ahead' | 'stale' | 'layout';
611
+ export type Sampling = 'linear' | 'nearest' | 'cubic';
package/dist/types.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The type contract for @reekon-tools/react-native-pdf-canvas.
3
+ *
4
+ * THE ONE RULE: the transform decides WHEN to rasterize, never WHAT to draw.
5
+ *
6
+ * Every rect in this file is in DOC SPACE — PDF points, y-down, origin at the
7
+ * page's top-left. That is exactly what PDFium's CPDF_Page::GetDisplayMatrix()
8
+ * emits, so a PDFium backend is a pass-through and every other backend adapts
9
+ * to it. There is deliberately no `coordinateSystem` option: two conventions in
10
+ * one package is how device pixel ratio gets applied twice.
11
+ *
12
+ * Nothing here imports React, react-native, or reanimated. The only external
13
+ * type is SkImage, and it is type-only.
14
+ */
15
+ export class PdfError extends Error {
16
+ constructor(code, message, options) {
17
+ super(message, options);
18
+ this.name = 'PdfError';
19
+ this.code = code;
20
+ }
21
+ }