@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
package/dist/policy.js ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * The cadence constants, in one place, each with the reasoning that produced
3
+ * it. None of these are measured on this codebase yet — where a value is a
4
+ * placeholder for a measurement, the comment says so out loud.
5
+ */
6
+ export const DEFAULT_POLICY = Object.freeze({
7
+ /**
8
+ * 1.0 — the base is rendered at exactly the fit-zoom display scale, so at
9
+ * fit zoom it is pixel-for-pixel and never magnified or minified. Anything
10
+ * above 1.0 buys sharpness only while zoomed in, which is the DETAIL layer's
11
+ * job; anything below draws the page minified, which is the aliasing regime
12
+ * (kStrict_SrcRectConstraint disables mipmaps, so it cannot be filtered
13
+ * away). A consumer whose base must survive a 2x zoom without the detail
14
+ * layer landing raises this to ~1.5 and pays for it in `baseMaxPixels`.
15
+ */
16
+ baseQuality: 1.0,
17
+ /**
18
+ * 4 MP. The base is the anti-blank mechanism and is NEVER released, so it is
19
+ * resident for the life of the document — one per visible page. At RGBA8888
20
+ * that is 16 MB per page, which is a sixth of `cacheBudgetBytes` and leaves
21
+ * room for the two or three pages a continuous layout keeps live. It also
22
+ * comfortably clears every real fit-zoom target: ARCH-D on a 1170x2532
23
+ * phone needs 2.05 MP, so the clamp does not bind on the case that matters
24
+ * and only catches the pathological sheet.
25
+ */
26
+ baseMaxPixels: 4000000,
27
+ /**
28
+ * 0.25 — a quarter of the visible extent added on EACH side, so a detail
29
+ * raster is 1.5x the viewport per axis, 2.25x its area. That is the price of
30
+ * a flick that outruns the quiet timer: a pan of up to a quarter viewport
31
+ * stays sharp with no new raster at all. Larger multiplies raster cost
32
+ * quadratically for padding that is usually thrown away.
33
+ */
34
+ detailOverflow: 0.25,
35
+ /**
36
+ * 4096 is the only near-universal GPU texture limit — it is the floor across
37
+ * the GLES3 and Metal feature sets this ships to, and exceeding it does not
38
+ * degrade, it fails the texture upload and draws nothing. Devices reporting
39
+ * 8192 or 16384 exist; planning to the floor costs one extra piece on a
40
+ * desktop-sized viewport and removes an entire class of blank-page report.
41
+ */
42
+ maxTextureDimension: 4096,
43
+ /**
44
+ * 1024 device px — a MEMORY policy, not a hardware one, and the single biggest
45
+ * low-end lever in the package.
46
+ *
47
+ * Measured on the un-tiled detail raster this replaced: 1755x3798 (25.4 MiB of
48
+ * pixels) had a ~237 MiB whole-flow transient peak across the Java, native and
49
+ * JS heaps, because one piece's bytes exist at once as a platform bitmap, a
50
+ * base64 string on the bridge, a JS `Uint8Array` and an `SkData`. Tiling the
51
+ * same area at 1024 drops that peak to ~29.7 MiB; at 512 to ~7.4 MiB. The
52
+ * RESIDENT bytes do not change — it is the same pixels either way.
53
+ *
54
+ * Why not 512, then: every piece is a separate uninterruptible platform render
55
+ * with its own fixed overhead, and each interior seam costs an overlap strip,
56
+ * so halving again quadruples the piece count for a peak that is already well
57
+ * under any device's budget. 1024 is also comfortably inside
58
+ * `maxTextureDimension` on every device, so the two caps do not fight.
59
+ *
60
+ * Raise it on a big-memory tablet; lower it to 512 on a low-RAM Android device
61
+ * where the transient, not the total, is what gets the app killed.
62
+ */
63
+ maxPieceDimension: 1024,
64
+ /**
65
+ * 24 MP for a single detail request, matching the cap the consuming app
66
+ * already established for its own PDF rendering. It is a TIME budget as much
67
+ * as a memory one: PDFKit's draw and Android's PdfRenderer.render are
68
+ * synchronous and uninterruptible once entered, so an oversized request is
69
+ * an un-abortable stall on whichever thread it runs on.
70
+ */
71
+ maxRasterPixels: 24000000,
72
+ /**
73
+ * sqrt(2). Re-raster once the on-screen scale has drifted a full half-octave
74
+ * from what the bytes were rendered at — i.e. tolerate up to 41% magnifi-
75
+ * cation (mild softness) or 29% minification before spending a raster.
76
+ * Halving this to 1.2 is correct on a DPR-1 display, where individual pixels
77
+ * are resolvable before any magnification is applied.
78
+ */
79
+ sharpnessBand: 1.414,
80
+ /**
81
+ * pdf.js's MOVEMENT_THRESHOLD, adopted verbatim rather than re-derived. Fire
82
+ * when the padding remaining ahead of the motion falls below half the
83
+ * padding already consumed behind it — a directional look-ahead that starts
84
+ * the next raster while the current one still covers the screen.
85
+ */
86
+ movementThreshold: 0.5,
87
+ /**
88
+ * 250 ms after a scale change. Zoom is coarse and self-correcting — users
89
+ * overshoot and come back — so rastering mid-pinch is usually wasted, and
90
+ * the base layer covers the gap at any scale. Long enough that a two-finger
91
+ * adjust-and-settle produces one raster, not three.
92
+ */
93
+ quietZoomMs: 250,
94
+ /**
95
+ * 120 ms after a pan-only change, roughly half the zoom delay. Pan is
96
+ * monotonic: content that scrolls off does not come back, so waiting buys
97
+ * nothing, and `detailOverflow` has already covered the first quarter
98
+ * viewport of travel. Comfortably above one 60Hz frame so a wheel notch
99
+ * train still coalesces.
100
+ */
101
+ quietPanMs: 120,
102
+ /**
103
+ * 700 ms. Under continued input the quiet timer never fires, so without this
104
+ * a slow continuous drag shows the base layer indefinitely. Chosen to sit
105
+ * above `minEpochIntervalMs` plus a typical raster, so the forced raster is
106
+ * never itself immediately superseded.
107
+ */
108
+ maxStaleMs: 700,
109
+ /**
110
+ * 300 ms — A PLACEHOLDER for the measured p95 raster time on the target
111
+ * device, which has not been measured yet. It should be replaced with a real
112
+ * number, not tuned by feel: the floor exists because an abort cannot
113
+ * interrupt a synchronous platform render, so setting it below the actual
114
+ * raster duration means every raster is superseded before it lands and the
115
+ * detail layer never appears at all.
116
+ */
117
+ minEpochIntervalMs: 300,
118
+ /**
119
+ * 96 MB. A SOFT cap on cached raster bytes used to pick eviction victims —
120
+ * not a memory limit, because the base rasters and the live epoch are pinned
121
+ * regardless.
122
+ *
123
+ * THE VALUE IS UNCHANGED AND ITS MEANING HAS TIGHTENED. A raster now books ~2x
124
+ * its pixels (`PdfRaster.bytes`, and `rasterByteLength` for why), so the same
125
+ * 96 MB covers half as many pixels as it nominally did before. That is the
126
+ * point: it was always covering half as many, it just did not know, so
127
+ * eviction was picking victims on numbers that were wrong by 2x. A 4 MP base
128
+ * is 32 MB against this budget, not 16 MB.
129
+ */
130
+ cacheBudgetBytes: 96 * 1024 * 1024,
131
+ /**
132
+ * false. The retiring set is already held across the swap, which is what
133
+ * prevents a one-frame flash; keeping it mounted beyond that doubles the
134
+ * resident detail bytes to defend against a case the base layer already
135
+ * covers. Consumers on a big-memory tablet may prefer true.
136
+ */
137
+ retainPreviousDetail: false,
138
+ });
139
+ /**
140
+ * Merge a partial override over the defaults. An explicitly-`undefined` field
141
+ * is IGNORED rather than allowed to erase a default — `{quietPanMs: maybe}`
142
+ * with an unset `maybe` is the common way a host would otherwise nuke a
143
+ * constant and get a zero-delay raster storm.
144
+ */
145
+ export function resolvePolicy(partial) {
146
+ if (!partial)
147
+ return DEFAULT_POLICY;
148
+ // Spelled out field by field rather than looped: a keyed loop needs an index
149
+ // signature RasterPolicy does not have, and every escape from that is a
150
+ // cast. `??` gives the ignore-undefined behaviour for free, and correctly
151
+ // keeps an explicit `false` for retainPreviousDetail.
152
+ return Object.freeze({
153
+ baseQuality: partial.baseQuality ?? DEFAULT_POLICY.baseQuality,
154
+ baseMaxPixels: partial.baseMaxPixels ?? DEFAULT_POLICY.baseMaxPixels,
155
+ detailOverflow: partial.detailOverflow ?? DEFAULT_POLICY.detailOverflow,
156
+ maxTextureDimension: partial.maxTextureDimension ?? DEFAULT_POLICY.maxTextureDimension,
157
+ maxPieceDimension: partial.maxPieceDimension ?? DEFAULT_POLICY.maxPieceDimension,
158
+ maxRasterPixels: partial.maxRasterPixels ?? DEFAULT_POLICY.maxRasterPixels,
159
+ sharpnessBand: partial.sharpnessBand ?? DEFAULT_POLICY.sharpnessBand,
160
+ movementThreshold: partial.movementThreshold ?? DEFAULT_POLICY.movementThreshold,
161
+ quietZoomMs: partial.quietZoomMs ?? DEFAULT_POLICY.quietZoomMs,
162
+ quietPanMs: partial.quietPanMs ?? DEFAULT_POLICY.quietPanMs,
163
+ maxStaleMs: partial.maxStaleMs ?? DEFAULT_POLICY.maxStaleMs,
164
+ minEpochIntervalMs: partial.minEpochIntervalMs ?? DEFAULT_POLICY.minEpochIntervalMs,
165
+ cacheBudgetBytes: partial.cacheBudgetBytes ?? DEFAULT_POLICY.cacheBudgetBytes,
166
+ retainPreviousDetail: partial.retainPreviousDetail ?? DEFAULT_POLICY.retainPreviousDetail,
167
+ });
168
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The TypeScript half of the Android backend.
3
+ *
4
+ * THIN, AND IT USED TO BE THE WHOLE THING. Everything that is not
5
+ * Android-specific — error mapping, page-geometry marshalling, the transport
6
+ * negotiation, the base64 decode, the cancellation wiring, the phase timings —
7
+ * moved to `./native-bridge.ts` when the iOS backend landed, because the two
8
+ * platforms speak the identical wire format and the spec's claim for the iOS step
9
+ * is that the seam is a seam. What is left here is the four things that are
10
+ * genuinely Android's: the id, the capabilities, the alpha encoding, and what
11
+ * `PdfSource` it can accept.
12
+ *
13
+ * The names exported from `./native-bridge.ts` are re-exported below under their
14
+ * original `Android*` spellings, so nothing that imported them from here breaks.
15
+ *
16
+ * PURE ON PURPOSE. Nothing here imports `react-native`, which is what lets the
17
+ * whole adapter be tested in vitest against a fake native module.
18
+ * `./index.native.ts` is the only file that resolves the real one, and it does
19
+ * nothing except hand it to `createAndroidRasterizer`.
20
+ *
21
+ * The native side is `android/src/main/java/tools/reekon/pdfcanvas/`, a
22
+ * React-free Android library that is proven on device by
23
+ * `./gradlew connectedDebugAndroidTest`. That split is the reason a bug here is
24
+ * a bug in marshalling and never a bug in rasterizing.
25
+ */
26
+ import type { NativeRasterizerHandle, NativeRotationSupport, NativeOpenResult, NativePdfPage, NativeRenderRequest, NativeRenderResult, NativeSource, NativeTransport, PdfCanvasNativeModule } from './native-bridge.js';
27
+ import type { PageRasterizer } from '../types.js';
28
+ export { TRANSPORT_PROBE_BYTES, bytesFromResult, decodeBase64, lookupTakePixels, negotiateTransport, toPageGeometry, } from './native-bridge.js';
29
+ export type { NativeRasterizerHandle, NativeRotationSupport, NativeOpenResult, NativePdfPage, NativeRenderRequest, NativeRenderResult, NativeSource, NativeTransport, PdfCanvasNativeModule, TakePixels, } from './native-bridge.js';
30
+ /**
31
+ * ALIASES, NOT COPIES. These were the shipped names before the wire format was
32
+ * shared with iOS, and a consumer that imported one should not have to care that
33
+ * a second backend now speaks it too.
34
+ */
35
+ export type AndroidRotationSupport = NativeRotationSupport;
36
+ export type AndroidNativePage = NativePdfPage;
37
+ export type AndroidNativeOpenResult = NativeOpenResult;
38
+ export type AndroidNativeRenderResult = NativeRenderResult;
39
+ export type AndroidNativeRenderRequest = NativeRenderRequest;
40
+ export type AndroidNativeSource = NativeSource;
41
+ export type AndroidPdfNativeModule = PdfCanvasNativeModule;
42
+ export type AndroidTransport = NativeTransport;
43
+ export type AndroidRasterizerHandle = NativeRasterizerHandle;
44
+ export interface AndroidRasterizerOptions {
45
+ /**
46
+ * Concurrent renders. Clamped to [1, 4] natively — each slot is one more
47
+ * `PdfRenderer` holding a file descriptor and a parsed document.
48
+ *
49
+ * ONE. It is also what the rasterizer reports as
50
+ * `RasterizerCapabilities.maxConcurrentRenders`, so raising it does two things
51
+ * at once: it opens more native renderers AND it widens the controller's lane
52
+ * bound. Both of those are measured below and both come out negative.
53
+ *
54
+ * THE POOL DOES NOT PARALLELISE PDFium. AOSP serialises `PdfRenderer` behind a
55
+ * process-wide PDFium lock, and the lock does not merely serialise, it CONVOYS.
56
+ * On an LG V60 (Snapdragon 865, 8 cores, API 33) at the field report's scale of
57
+ * 131.25 device px/pt with 1024 px tiles, over a real field-measure drawing,
58
+ * four threads inflate the per-tile PDFium phases `openPage` 16.2x,
59
+ * `PdfRenderer.render` 10.9x and `recycle+close` 33.6x, for a whole-tile
60
+ * inflation of 5.84x, against 1.1x-5.3x for every non-PDFium phase. On a dense
61
+ * electrical schematic it is worse still: `render` 30.9x, `openPage` 11.2x.
62
+ * A pure-Java control on the same threads scales 2.8x. So the native calls do
63
+ * not overlap, and contending for them makes each one dramatically slower.
64
+ *
65
+ * NOTHING ABOUT THIS CARRIES OVER TO iOS, and the iOS option says so on its own
66
+ * doc comment: the lock being measured here is AOSP's, in
67
+ * `android_graphics_pdf_PdfRenderer.cpp`, and CoreGraphics has no counterpart
68
+ * to it that anyone has measured.
69
+ *
70
+ * WHY THIS WAS 2, AND WHY THAT REASON IS GONE. The second slot never existed to
71
+ * rasterize in parallel; it existed to PIPELINE, hiding the JS thread's
72
+ * `decodeBase64` (~1005 ms per tile, 90.6% of the tile) behind the next tile's
73
+ * native work. The JSI transport deleted that cost outright — the JS thread's
74
+ * whole share is now a pointer wrap — so there is nothing left to hide. Worse,
75
+ * deleting the base64 ENCODE from the Java side deleted the only phase that was
76
+ * genuinely parallel: on the real drawing a tile was 72.7% parallelisable work
77
+ * (19.3 ms of encode inside 30.6 ms) and is now 35.6% (4.6 ms of bitmap fill,
78
+ * pixel copy and JNI store inside 13.0 ms), with the PDFium-serialised part
79
+ * risen from 25.5% to 60.2%. Amdahl caps that at 1.67x before the convoy is
80
+ * counted, and the convoy is what turns the remaining headroom negative.
81
+ *
82
+ * MEASURED, on the JSI transport, by `laneScheduleOnTheJsiTransport` in
83
+ * `PdfCanvasPhaseTimingTest` — N lanes over one shared cursor of 6 plan items,
84
+ * which is the schedule the controller actually runs, 21 interleaved rounds per
85
+ * lane count. Mean epoch wall clock relative to one lane, higher is better:
86
+ *
87
+ * - real field-measure drawing, 4 independent passes:
88
+ * 2 lanes 0.82x / 0.81x / 0.95x / 0.76x,
89
+ * 3 lanes 0.54x / 0.61x / 0.85x / 0.58x,
90
+ * 4 lanes 0.59x / 0.58x / 0.92x / 0.55x. One lane is never beaten.
91
+ * - dense electrical schematic: 2 lanes 0.48x, 3 lanes 0.41x, 4 lanes 0.38x.
92
+ * - THE TAIL IS WHERE IT HURTS MOST, and the tail is what a user sees as a
93
+ * stutter. Worst-of-21 on the drawing: 126-172 ms at one lane against
94
+ * 177-238 ms at two. On the schematic: 134 ms against 321 ms.
95
+ *
96
+ * ONE IS ALSO THE CHEAPER OBJECT: one file descriptor and one parsed document
97
+ * instead of two, which on a large drawing is the larger of the two costs.
98
+ *
99
+ * DO NOT READ THE SYNTHETIC FIXTURE'S NUMBERS AS AGREEING OR DISAGREEING. With
100
+ * no `customer.pdf` pushed, `PdfCanvasPhaseTimingTest` falls back to a
101
+ * vector-rects-and-lines page whose `PdfRenderer.render` is 0.8 ms rather than
102
+ * 6.5 ms; that shrinks the serialised fraction, and two lanes duly measure
103
+ * 1.06x-1.25x there. The fixture warns that it is an optimistic bound, and this
104
+ * is exactly the decision it would have inverted. Push a real drawing before
105
+ * re-running this.
106
+ */
107
+ poolSize?: number;
108
+ id?: string;
109
+ /**
110
+ * How to find the installed JSI host function. Injected only by tests; the
111
+ * default reads `globalThis`.
112
+ */
113
+ lookupTakePixels?: () => import('./native-bridge.js').TakePixels | null;
114
+ }
115
+ /**
116
+ * Wraps a native module in a `PageRasterizer`.
117
+ *
118
+ * The native module is INJECTED rather than imported so this file stays pure.
119
+ * `getDefaultRasterizer()` in `./index.native.ts` supplies the real one.
120
+ */
121
+ export declare function createAndroidRasterizer(native: AndroidPdfNativeModule, options?: AndroidRasterizerOptions): PageRasterizer;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The TypeScript half of the Android backend.
3
+ *
4
+ * THIN, AND IT USED TO BE THE WHOLE THING. Everything that is not
5
+ * Android-specific — error mapping, page-geometry marshalling, the transport
6
+ * negotiation, the base64 decode, the cancellation wiring, the phase timings —
7
+ * moved to `./native-bridge.ts` when the iOS backend landed, because the two
8
+ * platforms speak the identical wire format and the spec's claim for the iOS step
9
+ * is that the seam is a seam. What is left here is the four things that are
10
+ * genuinely Android's: the id, the capabilities, the alpha encoding, and what
11
+ * `PdfSource` it can accept.
12
+ *
13
+ * The names exported from `./native-bridge.ts` are re-exported below under their
14
+ * original `Android*` spellings, so nothing that imported them from here breaks.
15
+ *
16
+ * PURE ON PURPOSE. Nothing here imports `react-native`, which is what lets the
17
+ * whole adapter be tested in vitest against a fake native module.
18
+ * `./index.native.ts` is the only file that resolves the real one, and it does
19
+ * nothing except hand it to `createAndroidRasterizer`.
20
+ *
21
+ * The native side is `android/src/main/java/tools/reekon/pdfcanvas/`, a
22
+ * React-free Android library that is proven on device by
23
+ * `./gradlew connectedDebugAndroidTest`. That split is the reason a bug here is
24
+ * a bug in marshalling and never a bug in rasterizing.
25
+ */
26
+ import { createNativeRasterizer } from './native-bridge.js';
27
+ import { PdfError } from '../types.js';
28
+ export { TRANSPORT_PROBE_BYTES, bytesFromResult, decodeBase64, lookupTakePixels, negotiateTransport, toPageGeometry, } from './native-bridge.js';
29
+ /* ------------------------------------------------------------------ *
30
+ * What is actually Android's
31
+ * ------------------------------------------------------------------ */
32
+ /**
33
+ * A CONSTANT AND NOT PART OF THE WIRE RESULT, unlike `format`.
34
+ *
35
+ * `format` is per-render because the Java side could in principle hand back
36
+ * either byte order; the alpha encoding cannot vary, because it is a property of
37
+ * `Bitmap` itself. `PdfCanvasRasterizer.extract` reads an ARGB_8888 Bitmap, and
38
+ * Android bitmaps are PREMULTIPLIED unless somebody calls
39
+ * `setPremultiplied(false)` — which that code deliberately never does, since
40
+ * un-premultiplying would make a `background: 'transparent'` raster composite
41
+ * wrongly rather than fail visibly. So there is nothing for the native side to
42
+ * report and no reason to widen the wire format to carry it.
43
+ *
44
+ * IT MATCHES iOS AND THAT IS A COINCIDENCE OF MECHANISM, not a shared choice.
45
+ * `CGBitmapContext` has no non-premultiplied 8-bit RGBA configuration at all —
46
+ * Apple's supported-formats table offers premultiplied or alpha-none and nothing
47
+ * between — so `ios.ts` declares the same value for a completely different
48
+ * reason. PDFium and pdf.js, meanwhile, both emit STRAIGHT alpha (measured — see
49
+ * `AlphaEncoding`), which is the whole reason `RasterPixels.alpha` exists rather
50
+ * than a single `AlphaType.Premul` in `ingest.ts`. Do not "align" any of these
51
+ * four values with each other.
52
+ */
53
+ const ANDROID_ALPHA = 'premultiplied';
54
+ const CAPABILITIES = {
55
+ /**
56
+ * True in the sense the field means — annotations ARE drawn into the raster.
57
+ *
58
+ * What is NOT true, and has no field to say so: they cannot be turned OFF.
59
+ * `PdfRenderer.Page.render` takes a render mode and no annotation switch, and
60
+ * PDFium's `FPDF_ANNOT` flag is unreachable through the platform class at
61
+ * every API level. So `RasterRequest.annotations: false` — which is what the
62
+ * controller passes for every tile — is recorded and ignored, and an app that
63
+ * draws its own copy of a PDF-embedded annotation will see it twice on
64
+ * Android. The fix is a PDFium backend, not a different mode here.
65
+ *
66
+ * iOS DOES HONOUR THE FALSE CASE, which is worth knowing when a bug report
67
+ * says annotations appear twice on one platform only: `ios.ts` picks
68
+ * `CGContextDrawPDFPage` for `false` and PDFKit for `true`, and the two really
69
+ * do differ in what they composite.
70
+ */
71
+ annotations: true,
72
+ /**
73
+ * False, and honestly so. `PdfRenderer.Page.render` is synchronous and
74
+ * uninterruptible once entered; an abort can only stop the NEXT piece
75
+ * starting, or discard a finished raster that is no longer wanted.
76
+ */
77
+ interruptibleRender: false,
78
+ text: false,
79
+ search: false,
80
+ links: false,
81
+ // Overwritten per rasterizer with the pool size it was actually opened with —
82
+ // see `createNativeRasterizer`. The constant carries the conservative value so
83
+ // a partially-constructed object is never the fast one.
84
+ maxConcurrentRenders: 1,
85
+ };
86
+ /**
87
+ * Mirrors `PdfRendererPool.MAX_POOL_SIZE` on the Java side, which is where the
88
+ * clamp is actually enforced (`PdfRendererPool.open`). Restating it here is not
89
+ * a second source of truth: it is what makes the DECLARED capability match the
90
+ * pool the native side will really hand out, rather than promising four lanes to
91
+ * a pool that silently opened with four... or with one.
92
+ */
93
+ const MAX_POOL_SIZE = 4;
94
+ const clampPoolSize = (size) => Math.max(1, Math.min(MAX_POOL_SIZE, Math.floor(size) || 1));
95
+ function toNativeSource(source) {
96
+ const password = source.password;
97
+ if ('uri' in source) {
98
+ return password === undefined
99
+ ? { uri: source.uri }
100
+ : { uri: source.uri, password };
101
+ }
102
+ throw new PdfError('unsupported', 'Passing a document as bytes is not wired through the Android binding yet. ' +
103
+ 'The native side supports it (PdfCanvasRasterizer.openBytes spools to a ' +
104
+ 'file, because PdfRenderer needs a seekable descriptor), but sending the ' +
105
+ 'bytes over the bridge costs a base64 round trip of the whole document. ' +
106
+ 'Pass a `uri` instead.');
107
+ }
108
+ /**
109
+ * Wraps a native module in a `PageRasterizer`.
110
+ *
111
+ * The native module is INJECTED rather than imported so this file stays pure.
112
+ * `getDefaultRasterizer()` in `./index.native.ts` supplies the real one.
113
+ */
114
+ export function createAndroidRasterizer(native, options = {}) {
115
+ return createNativeRasterizer(native, {
116
+ id: options.id ?? 'android-pdfrenderer',
117
+ capabilities: CAPABILITIES,
118
+ alpha: ANDROID_ALPHA,
119
+ poolSize: clampPoolSize(options.poolSize ?? 1),
120
+ label: 'Android',
121
+ toNativeSource,
122
+ ...(options.lookupTakePixels === undefined
123
+ ? {}
124
+ : { lookupTakePixels: options.lookupTakePixels }),
125
+ });
126
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The tight-buffer predicate, stated ONCE.
3
+ *
4
+ * `Skia.Data.fromBytes` on native reads `array.getProperty("buffer")` and copies
5
+ * the WHOLE ArrayBuffer, ignoring `byteOffset` and `byteLength`
6
+ * (JsiSkDataFactory.h). So a Uint8Array that is a VIEW into something larger
7
+ * ingests shifted bytes and shears the image, and `ingest.ts` copies it to a
8
+ * tight buffer first (`tighten`).
9
+ *
10
+ * This predicate is also the thing a new TRANSPORT has to satisfy to make that
11
+ * copy disappear: a native path that hands back an ArrayBuffer which is exactly
12
+ * the raster and nothing more is tight by construction, and `tighten` becomes a
13
+ * no-op. That makes the predicate a fact worth REPORTING (the backend puts it in
14
+ * `RasterPhaseTimings.tight`), not merely a branch — which is why it lives in
15
+ * its own pure module rather than inline in `tighten`, where the transport work
16
+ * could not see it without importing Skia.
17
+ */
18
+ export declare const isTightBuffer: (bytes: Uint8Array) => boolean;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The tight-buffer predicate, stated ONCE.
3
+ *
4
+ * `Skia.Data.fromBytes` on native reads `array.getProperty("buffer")` and copies
5
+ * the WHOLE ArrayBuffer, ignoring `byteOffset` and `byteLength`
6
+ * (JsiSkDataFactory.h). So a Uint8Array that is a VIEW into something larger
7
+ * ingests shifted bytes and shears the image, and `ingest.ts` copies it to a
8
+ * tight buffer first (`tighten`).
9
+ *
10
+ * This predicate is also the thing a new TRANSPORT has to satisfy to make that
11
+ * copy disappear: a native path that hands back an ArrayBuffer which is exactly
12
+ * the raster and nothing more is tight by construction, and `tighten` becomes a
13
+ * no-op. That makes the predicate a fact worth REPORTING (the backend puts it in
14
+ * `RasterPhaseTimings.tight`), not merely a branch — which is why it lives in
15
+ * its own pure module rather than inline in `tighten`, where the transport work
16
+ * could not see it without importing Skia.
17
+ */
18
+ export const isTightBuffer = (bytes) => bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength;
@@ -0,0 +1,92 @@
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 type { DocRect, DocSize, PageRasterizer, PageRotation, PdfErrorCode, PixelFormat, PixelSize, RasterPixels, RasterRequest } from '../types.js';
24
+ export declare const LETTER: DocSize;
25
+ /** ARCH D, 24x36in — the sheet the base-raster sizing rule is calibrated on. */
26
+ export declare const ARCH_D: DocSize;
27
+ /** ARCH E, 36x48in. */
28
+ export declare const ARCH_E: DocSize;
29
+ export interface FakeFailureModes {
30
+ /** `open()` rejects with this code. */
31
+ open?: PdfErrorCode;
32
+ /**
33
+ * `render()` rejects with this code. The function form lets a test fail one
34
+ * page, one scale or one call and let the rest succeed.
35
+ */
36
+ render?: PdfErrorCode | ((request: RasterRequest) => PdfErrorCode | null);
37
+ }
38
+ export interface FakeRasterizerOptions {
39
+ /** Surfaced as `PageRasterizer.id` and in diagnostics. */
40
+ id?: string;
41
+ /** Cycled if `pageCount` exceeds its length. Defaults to a single LETTER. */
42
+ pageSizes?: readonly DocSize[];
43
+ /** Defaults to `pageSizes.length`, or 3. */
44
+ pageCount?: number;
45
+ /**
46
+ * Cycled, defaults to 0. Informational only: per the contract, page width and
47
+ * height are already post-rotation, so the fake does not swap them.
48
+ */
49
+ pageRotations?: readonly PageRotation[];
50
+ /** Checker cell size in DOC units. */
51
+ checkerDocSize?: number;
52
+ format?: PixelFormat;
53
+ /** Artificial delay on `render()` only; `open()` always resolves promptly. */
54
+ latencyMs?: number;
55
+ /** When set, `open()` enforces it against `source.password`. */
56
+ password?: string;
57
+ fail?: FakeFailureModes;
58
+ /** Refuse absurd requests so a planner bug is a test failure, not an OOM. */
59
+ maxPixels?: number;
60
+ /**
61
+ * Reported as `RasterizerCapabilities.maxConcurrentRenders`.
62
+ *
63
+ * DEFAULTS TO 1, which is the honest answer for the pixel loop below: it is
64
+ * synchronous, so a second `render` cannot make progress while the first is
65
+ * running whatever the caller does. Raise it in a test or a demo that wants
66
+ * the controller's LANES exercised — with `latencyMs` set, the fake's awaits
67
+ * genuinely interleave and the schedule is what is under test, not the pixels.
68
+ */
69
+ maxConcurrentRenders?: number;
70
+ }
71
+ /**
72
+ * The pixel size the fake renders a request at.
73
+ *
74
+ * Exported because RasterRequest carries no PixelSize — a backend derives it —
75
+ * and a test that wants to index into the output needs the same arithmetic.
76
+ * Rounding (rather than ceiling) keeps the raster closest to the nominal
77
+ * scale; either is safe, because `PdfRaster.docRect` is the request's doc rect
78
+ * regardless and `fit="fill"` absorbs the sub-pixel difference.
79
+ */
80
+ export declare const fakePixelSize: (request: RasterRequest) => PixelSize;
81
+ /**
82
+ * A distinct flat colour per page. Golden-angle hue rotation puts consecutive
83
+ * pages maximally far apart in hue, so page 4 is never mistaken for page 5 in a
84
+ * screenshot of a scrolling document.
85
+ */
86
+ export declare function fakePageColor(page: number): [number, number, number];
87
+ /**
88
+ * Read the doc rect a fake raster believes it covers, or null when the raster
89
+ * was too small to carry a swatch.
90
+ */
91
+ export declare function decodeFakeDocRect(pixels: RasterPixels): DocRect | null;
92
+ export declare function createFakeRasterizer(options?: FakeRasterizerOptions): PageRasterizer;