@reekon-tools/react-native-pdf-canvas 0.1.0 → 0.2.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 (129) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/PdfCanvas.podspec +68 -39
  4. package/README.md +213 -1316
  5. package/android/build.gradle +67 -54
  6. package/android/consumer-rules.pro +7 -0
  7. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
  8. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
  9. package/android/src/main/cpp/CMakeLists.txt +54 -0
  10. package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
  11. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
  12. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
  13. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
  14. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
  15. package/android/tools/compile-gate.sh +81 -101
  16. package/dist/controller.d.ts +14 -0
  17. package/dist/controller.js +4 -3
  18. package/dist/index.native.d.ts +5 -0
  19. package/dist/index.native.js +5 -0
  20. package/dist/index.web.d.ts +10 -0
  21. package/dist/index.web.js +10 -0
  22. package/dist/rasterizer/index.native.d.ts +8 -12
  23. package/dist/rasterizer/index.native.js +15 -21
  24. package/dist/rasterizer/native-bridge.d.ts +51 -75
  25. package/dist/rasterizer/native-bridge.js +49 -33
  26. package/dist/rasterizer/native.d.ts +45 -0
  27. package/dist/rasterizer/native.js +123 -0
  28. package/dist/react/usePdfDocument.d.ts +5 -5
  29. package/dist/react/usePdfLayer.d.ts +10 -1
  30. package/dist/react/usePdfLayer.js +10 -2
  31. package/dist/skia/index.native.d.ts +7 -3
  32. package/dist/skia/index.native.js +7 -3
  33. package/dist/skia/index.web.d.ts +4 -2
  34. package/dist/skia/index.web.js +4 -2
  35. package/dist/testing/scenes.d.ts +3 -3
  36. package/dist/testing/scenes.js +4 -4
  37. package/dist/types.d.ts +36 -42
  38. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  39. package/native/.clangd +5 -0
  40. package/native/CMakeLists.txt +89 -0
  41. package/native/core/include/pdfcanvas/document.h +90 -0
  42. package/native/core/include/pdfcanvas/error.h +47 -0
  43. package/native/core/include/pdfcanvas/jsi.h +18 -0
  44. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  45. package/native/core/include/pdfcanvas/library.h +28 -0
  46. package/native/core/include/pdfcanvas/pixels.h +92 -0
  47. package/native/core/include/pdfcanvas/service.h +87 -0
  48. package/native/core/include/pdfcanvas/slots.h +113 -0
  49. package/native/core/include/pdfcanvas/types.h +110 -0
  50. package/native/core/pdfcanvas-core.cmake +22 -0
  51. package/native/core/src/document.cpp +516 -0
  52. package/native/core/src/error.cpp +29 -0
  53. package/native/core/src/jsi.cpp +66 -0
  54. package/native/core/src/library.cpp +52 -0
  55. package/native/core/src/pixels.cpp +45 -0
  56. package/native/core/src/service.cpp +156 -0
  57. package/native/core/src/slots.cpp +139 -0
  58. package/native/tests/fixtures.cpp +451 -0
  59. package/native/tests/fixtures.h +96 -0
  60. package/native/tests/harness.h +122 -0
  61. package/native/tests/main.cpp +64 -0
  62. package/native/tests/pixels_util.h +126 -0
  63. package/native/tests/test_document.cpp +485 -0
  64. package/native/tests/test_service.cpp +130 -0
  65. package/native/tests/test_slots.cpp +250 -0
  66. package/package.json +38 -8
  67. package/react-native.config.js +5 -2
  68. package/scripts/fetch-pdfium.mjs +487 -0
  69. package/scripts/pdfium-manifest.json +46 -0
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  72. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  73. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  74. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  75. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  76. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  77. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  78. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  79. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  80. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  81. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  82. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  83. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  84. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  85. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  86. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  87. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  88. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  89. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  90. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  91. package/dist/rasterizer/android.d.ts +0 -121
  92. package/dist/rasterizer/android.js +0 -126
  93. package/dist/rasterizer/ios.d.ts +0 -84
  94. package/dist/rasterizer/ios.js +0 -155
  95. package/ios/Package.swift +0 -58
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  98. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  99. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  100. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  101. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  102. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  103. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  104. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  105. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  109. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  110. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  111. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  112. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  113. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  114. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  115. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  116. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  117. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  118. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  119. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  120. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  121. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  122. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  123. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  124. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  125. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  126. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  127. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  128. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  129. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -1,20 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import java.lang.annotation.ElementType;
4
- import java.lang.annotation.Retention;
5
- import java.lang.annotation.RetentionPolicy;
6
- import java.lang.annotation.Target;
7
-
8
- /**
9
- * Marks a test that deliberately triggers the API 24 PDFium poisoning and can
10
- * therefore only be the LAST thing its process does.
11
- *
12
- * `android/build.gradle` passes `notAnnotation` for this type, so
13
- * `connectedDebugAndroidTest` never runs it by accident. It is run on its own —
14
- * the command is in {@code PdfCanvasPoisonGuardTest}'s javadoc — which is the
15
- * only way to observe the guard doing its job, because observing it requires the
16
- * platform to actually be broken.
17
- */
18
- @Retention(RetentionPolicy.RUNTIME)
19
- @Target({ElementType.METHOD, ElementType.TYPE})
20
- public @interface PoisonsTheProcess {}
@@ -1,33 +0,0 @@
1
- # The raster transport's native half. See pdfcanvas-jsi.cpp for what it does.
2
- #
3
- # PDFCANVAS_WITH_JSI is passed ON only by the autolinked build, where
4
- # ReactAndroid's prefab publishes jsi/jsi.h and libjsi.so. The standalone build
5
- # (`cd android && ./gradlew connectedDebugAndroidTest`) passes nothing, has no
6
- # React Native anywhere on its classpath, and still compiles this library — the
7
- # slot storage and its JNI surface are React-free, which is what lets the device
8
- # suite prove the transport's byte fidelity with no app in the picture.
9
-
10
- cmake_minimum_required(VERSION 3.13)
11
- project(pdfcanvasjsi)
12
-
13
- set(CMAKE_CXX_STANDARD 17)
14
- set(CMAKE_CXX_STANDARD_REQUIRED ON)
15
-
16
- add_library(pdfcanvasjsi SHARED pdfcanvas-jsi.cpp)
17
-
18
- # jnigraphics is the NDK's Bitmap accessor (AndroidBitmap_getInfo /
19
- # AndroidBitmap_lockPixels), which is how the one-copy path reads a raster
20
- # straight out of the Bitmap's own pixel memory. Part of the platform since
21
- # API 8 and present on every device this package supports, so it is linked in
22
- # BOTH configurations — the standalone build needs it too, and that is what lets
23
- # the instrumented suite prove the fill's byte fidelity with no app in the
24
- # picture.
25
- target_link_libraries(pdfcanvasjsi jnigraphics)
26
-
27
- if(PDFCANVAS_WITH_JSI)
28
- target_compile_definitions(pdfcanvasjsi PRIVATE PDFCANVAS_WITH_JSI=1)
29
- # CONFIG mode against the react-android AAR's prefab package. `jsi` is the
30
- # only target needed: nothing here touches TurboModules, fbjni or folly.
31
- find_package(ReactAndroid REQUIRED CONFIG)
32
- target_link_libraries(pdfcanvasjsi ReactAndroid::jsi)
33
- endif()
@@ -1,469 +0,0 @@
1
- /*
2
- * The raster transport: a Java `byte[]` becomes a JS `ArrayBuffer` with no
3
- * base64 anywhere in between.
4
- *
5
- * WHY THIS FILE EXISTS. The measured split for one 1024x1024 tile on an LG V60
6
- * was 100.07 ms of bridge round trip, 1005.46 ms of `decodeBase64` in Hermes and
7
- * 4.09 ms of Skia ingest. The decode is 90.6% of the cost and it is a
8
- * 5.6-million-iteration interpreted loop that exists only to undo an encode that
9
- * happened 3 ms earlier on the other side of the bridge. Deleting BOTH is what
10
- * this file does; the Skia ingest is untouched and is expected to survive.
11
- *
12
- * HOW MANY COPIES. TWO ENTRY POINTS, and they differ by exactly one copy. The
13
- * count below is of FULL-RASTER copies; the ArrayBuffer hand-off is a pointer
14
- * wrap in both cases and is not one of them.
15
- *
16
- * `nativeStoreFromBitmap` — ONE copy. The `SlotBuffer` is allocated here, then
17
- * `AndroidBitmap_lockPixels` gives the Bitmap's own pixel memory and one
18
- * `memcpy` moves it in. The Java heap is never involved: no `byte[]`, and
19
- * nothing for ART's large-object space to collect afterwards. This is the
20
- * path `PdfCanvasRasterizer.extract` takes when a slot is asked for.
21
- *
22
- * `nativeStore` — TWO copies, and it is kept because the base64 fallback needs
23
- * a `byte[]` anyway. `Bitmap.copyPixelsToBuffer` has already copied the
24
- * raster into a Java array (copy 1) before this is called; here
25
- * `GetByteArrayRegion` copies it again into the `SlotBuffer` (copy 2).
26
- * THIS COMMENT USED TO CLAIM "exactly one" FOR THIS PATH. It undercounted:
27
- * it was counting from the `byte[]`, as though the array had arrived free.
28
- *
29
- * Both run on the render executor thread, the same worker that just did
30
- * `PdfRenderer.render`, so their cost is paid in parallel with whatever JS is
31
- * doing. Then `__pdfCanvasTakePixels` hands the `SlotBuffer` to
32
- * `jsi::ArrayBuffer(runtime, shared_ptr<MutableBuffer>)`: Hermes retains the
33
- * shared_ptr and points the ArrayBuffer's storage AT the vector. No copy.
34
- *
35
- * WHY THE FILL IS NOT DONE FROM JAVA THROUGH A DIRECT ByteBuffer. That was the
36
- * obvious shape — `NewDirectByteBuffer` over the `SlotBuffer` and let Java
37
- * `copyPixelsToBuffer` into it — and it opens a use-after-free this one does not.
38
- * A slot in `g_slots` is reachable by `nativeReleaseAll`, which runs on ANOTHER
39
- * thread (`PdfCanvasModule.invalidate`, i.e. every Fast Refresh) and frees the
40
- * vector. Handing the address to Java means the slot must be published BEFORE the
41
- * fill, so a reload landing mid-copy frees memory a render worker is still
42
- * writing into: silent heap corruption, weeks from the change that caused it.
43
- * Filling inside one JNI call keeps the shape every slot has always had —
44
- * allocate, fill, publish under the lock, in that order — so the buffer is
45
- * unreachable by anything else until it is finished.
46
- *
47
- * WHO FREES IT. The `SlotBuffer` is a `std::shared_ptr`. The slot map drops its
48
- * reference the instant the take succeeds, so from that moment the only owner is
49
- * the JS ArrayBuffer, and the vector is freed when Hermes collects it. A slot
50
- * that is stored and never taken (a JS teardown between the promise resolving
51
- * and its continuation running) is freed by `nativeReleaseAll`, which
52
- * `PdfCanvasModule.invalidate()` calls.
53
- *
54
- * TWO BUILDS, ONE FILE — the same split `build.gradle` already draws:
55
- *
56
- * PDFCANVAS_WITH_JSI defined — the autolinked build. ReactAndroid's prefab
57
- * supplies `jsi/jsi.h`, and the host function
58
- * is installed onto the JS global.
59
- * PDFCANVAS_WITH_JSI undefined — the standalone build
60
- * (`./gradlew connectedDebugAndroidTest`).
61
- * There is no React Native on the classpath and
62
- * no libjsi.so to link, so `nativeInstall`
63
- * reports false and the slot storage is
64
- * compiled on its own.
65
- *
66
- * That second configuration is not a stub for its own sake: it is what lets
67
- * `PdfCanvasJsiTransportTest` prove ON DEVICE that the bytes which become the
68
- * ArrayBuffer are byte-identical to what the base64 path would have produced.
69
- * The JNI copy is the only step that could shear a raster, and it is the step
70
- * both builds share.
71
- *
72
- * PLAIN JNI, NO FBJNI. The whole surface is four static methods on one class and
73
- * one host function; fbjni would add a dependency and a registration dance to
74
- * express `GetByteArrayRegion`.
75
- */
76
-
77
- #include <android/bitmap.h>
78
- #include <jni.h>
79
-
80
- #include <cstddef>
81
- #include <cstdint>
82
- #include <cstring>
83
- #include <memory>
84
- #include <mutex>
85
- #include <unordered_map>
86
- #include <vector>
87
-
88
- #ifdef PDFCANVAS_WITH_JSI
89
- #include <jsi/jsi.h>
90
- #endif
91
-
92
- namespace {
93
-
94
- #ifdef PDFCANVAS_WITH_JSI
95
- using SlotBufferBase = facebook::jsi::MutableBuffer;
96
- #else
97
- /**
98
- * The two methods `jsi::MutableBuffer` declares, restated so the standalone
99
- * build compiles the identical storage code with no JSI header in sight. If
100
- * these ever drift from jsi.h the autolinked build stops compiling, which is the
101
- * failure mode worth having.
102
- */
103
- struct SlotBufferBase {
104
- virtual ~SlotBufferBase() = default;
105
- virtual size_t size() const = 0;
106
- virtual uint8_t* data() = 0;
107
- };
108
- #endif
109
-
110
- /**
111
- * One raster's bytes, owned outright.
112
- *
113
- * NOT TAKEN, AND MEASURED BEFORE IT IS: `std::vector<uint8_t> bytes_(size)`
114
- * VALUE-initialises, so constructing this memsets the whole raster to zero and
115
- * then a fill immediately overwrites every byte of it. That is a second
116
- * full-raster pass per tile, on the same serialised render executor as the copy
117
- * it precedes, and a `unique_ptr<uint8_t[]>` from `new uint8_t[size]` would
118
- * delete it outright.
119
- *
120
- * It stays because it buys a real invariant for free: a published slot can never
121
- * hand JS memory that was not written. Every fill path today writes all `size`
122
- * bytes BEFORE publishing, so the invariant is already structural rather than
123
- * incidental — but "already structural" is the sort of thing that stops being
124
- * true quietly, and the failure mode is a tile of somebody else's heap. Take it
125
- * only with `PdfCanvasPhaseTimingTest`'s LANES-JSI-DIRECT row measured on both
126
- * sides of the change.
127
- */
128
- class SlotBuffer final : public SlotBufferBase {
129
- public:
130
- explicit SlotBuffer(size_t size) : bytes_(size) {}
131
-
132
- size_t size() const override { return bytes_.size(); }
133
- uint8_t* data() override { return bytes_.data(); }
134
-
135
- private:
136
- std::vector<uint8_t> bytes_;
137
- };
138
-
139
- /**
140
- * Slots are written on the render executor and read on the JS thread, so the map
141
- * is guarded. The lock is held only around the map operation itself — never
142
- * across the allocation or the copy, and never across a JSI call.
143
- */
144
- std::mutex g_slotsMutex;
145
- std::unordered_map<int64_t, std::shared_ptr<SlotBuffer>> g_slots;
146
-
147
- /**
148
- * Publishes a finished buffer, or refuses.
149
- *
150
- * REFUSES rather than overwrites when the id is already occupied. Slot ids are
151
- * monotonic per PROCESS (`PdfCanvasModule.nextSlot` is static for exactly this
152
- * reason), so a collision cannot happen by design and can only mean the id
153
- * allocator has been duplicated — in which case overwriting hands one module
154
- * ANOTHER module's raster, which is a wrong image rather than an error. `false`
155
- * here means the caller falls back, and the buffer is freed by its shared_ptr.
156
- *
157
- * Separate from the fill so the lock is held only around the map operation,
158
- * never across the allocation or the memcpy.
159
- */
160
- bool publishSlot(int64_t slot, std::shared_ptr<SlotBuffer> buffer) {
161
- std::lock_guard<std::mutex> lock(g_slotsMutex);
162
- if (g_slots.find(slot) != g_slots.end()) {
163
- return false;
164
- }
165
- g_slots.emplace(slot, std::move(buffer));
166
- return true;
167
- }
168
-
169
- std::shared_ptr<SlotBuffer> takeSlot(int64_t slot) {
170
- std::lock_guard<std::mutex> lock(g_slotsMutex);
171
- auto found = g_slots.find(slot);
172
- if (found == g_slots.end()) {
173
- return nullptr;
174
- }
175
- std::shared_ptr<SlotBuffer> buffer = std::move(found->second);
176
- g_slots.erase(found);
177
- return buffer;
178
- }
179
-
180
- /**
181
- * Raises a Java throwable, so a C++ failure leaves this JNI method as an
182
- * exception the caller can catch rather than as an unwind into ART.
183
- */
184
- void throwJava(JNIEnv* env, const char* className, const char* message) {
185
- if (env->ExceptionCheck()) {
186
- return;
187
- }
188
- jclass clazz = env->FindClass(className);
189
- if (clazz == nullptr) {
190
- // FindClass has already set its own NoClassDefFoundError; nothing better to
191
- // report and nothing that may throw from here.
192
- return;
193
- }
194
- env->ThrowNew(clazz, message);
195
- }
196
-
197
- } // namespace
198
-
199
- extern "C" {
200
-
201
- JNIEXPORT void JNICALL
202
- Java_tools_reekon_pdfcanvas_jsi_PdfCanvasPixelBridge_nativeStore(
203
- JNIEnv* env,
204
- jclass,
205
- jlong slot,
206
- jbyteArray data) {
207
- if (data == nullptr) {
208
- throwJava(
209
- env, "java/lang/NullPointerException", "nativeStore: bytes is null.");
210
- return;
211
- }
212
- const jsize length = env->GetArrayLength(data);
213
- // ALLOCATES, so it can raise a C++ exception: `std::vector` throws
214
- // `std::bad_alloc` for a 4 MiB raster under memory pressure, and the map node
215
- // allocation can too. (`nativeStoreFromBitmap` allocates as well, but reports
216
- // every failure of its own as `false` rather than as a throwable — see there.)
217
- //
218
- // A C++ exception MUST NOT unwind out of a JNI method: the ART frame below is
219
- // not exception-aware, so the unwind reaches `std::terminate` and aborts the
220
- // process. It is converted to a Java throwable here because that is what this
221
- // method's callers are written to catch — `PdfCanvasModule.installJsiTransport`
222
- // catches `Throwable` and returns -1, which declines the whole transport and
223
- // leaves base64 in place. So the promised fallback is real.
224
- try {
225
- auto buffer = std::make_shared<SlotBuffer>(static_cast<size_t>(length));
226
- if (length > 0) {
227
- // GetByteArrayRegion, not GetPrimitiveArrayCritical: the critical variants
228
- // suspend GC for their duration and this is a 4 MiB copy on a worker
229
- // thread, which is exactly the shape of call that should not be holding the
230
- // collector still.
231
- env->GetByteArrayRegion(
232
- data, 0, length, reinterpret_cast<jbyte*>(buffer->data()));
233
- }
234
- std::lock_guard<std::mutex> lock(g_slotsMutex);
235
- // Left as an overwriting assignment, deliberately: this is the FALLBACK
236
- // transport and the instrumented suite proves its current behaviour. It does
237
- // not go through publishSlot's refusal, which is new-path-only.
238
- g_slots[static_cast<int64_t>(slot)] = std::move(buffer);
239
- } catch (...) {
240
- throwJava(
241
- env,
242
- "java/lang/OutOfMemoryError",
243
- "nativeStore: could not park the raster in native memory.");
244
- }
245
- }
246
-
247
- /**
248
- * THE ONE-COPY PATH: a `Bitmap`'s pixels straight into a `SlotBuffer`.
249
- *
250
- * Deletes, per tile, one full-raster `new byte[]` on the ART heap and one
251
- * full-raster memcpy out of it. What is left is the single `memcpy` below, which
252
- * is the same copy `Bitmap.copyPixelsToBuffer` would have performed — it too is
253
- * a `memcpy` from `bitmap.getPixels()` through Skia's `AutoBufferPointer`.
254
- *
255
- * FAILS BY RETURNING FALSE, NEVER BY THROWING. Every refusal here is a tile that
256
- * goes out on the `byte[]` path instead, which is the transport that has always
257
- * worked; `PdfCanvasRasterizer.extract` treats false as "use the heap" and does
258
- * not distinguish the reasons. So an allocation failure becomes a slower correct
259
- * tile rather than a `PdfErrorCode`, and if the heap allocation then fails too,
260
- * THAT is what maps to `OUT_OF_MEMORY` — the same mapping as before this path
261
- * existed.
262
- *
263
- * WHY byteCount IS PASSED IN AND ALSO CROSS-CHECKED. Java has already validated
264
- * `rowBytes >= width*4` and `byteCount >= rowBytes*height` against the Bitmap's
265
- * own reported layout, and that validation must stay where it is (before any
266
- * allocation). `AndroidBitmap_getInfo` reports the stride a SECOND time, from the
267
- * NDK side, and a disagreement between the two would mean copying a number of
268
- * bytes the source does not have — a short read at best, a shear at worst. So
269
- * `stride * height` must equal the count Java asked for, exactly; a mismatch is
270
- * refused rather than reconciled, because guessing which of the two is right is
271
- * how a plausible-looking skew ships.
272
- *
273
- * The format is checked for the same reason. `Bitmap.Config.ARGB_8888` is named
274
- * for a packed int and laid out R,G,B,A in memory, which is what the NDK calls
275
- * `ANDROID_BITMAP_FORMAT_RGBA_8888` and what `RasterPixels.FORMAT_RGBA_8888`
276
- * declares to the TS side. Asserting it here is what stops that agreement from
277
- * being an assumption.
278
- */
279
- JNIEXPORT jboolean JNICALL
280
- Java_tools_reekon_pdfcanvas_jsi_PdfCanvasPixelBridge_nativeStoreFromBitmap(
281
- JNIEnv* env,
282
- jclass,
283
- jlong slot,
284
- jobject bitmap,
285
- jint width,
286
- jint height,
287
- jint byteCount) {
288
- if (bitmap == nullptr || width <= 0 || height <= 0 || byteCount <= 0) {
289
- return JNI_FALSE;
290
- }
291
-
292
- AndroidBitmapInfo info;
293
- if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
294
- return JNI_FALSE;
295
- }
296
- if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
297
- return JNI_FALSE;
298
- }
299
- // The shape the caller is about to DECLARE to the TS side, against the shape
300
- // the NDK says this bitmap has. They cannot differ today — the bitmap was
301
- // created at exactly pxW x pxH — and checking costs two comparisons, which
302
- // is a better price than a raster that is described as one size and holds
303
- // another.
304
- if (info.width != static_cast<uint32_t>(width) ||
305
- info.height != static_cast<uint32_t>(height)) {
306
- return JNI_FALSE;
307
- }
308
- // In 64-bit arithmetic on purpose: stride and height are uint32_t, and a
309
- // 32-bit product would wrap before it could disagree with byteCount.
310
- const uint64_t reported =
311
- static_cast<uint64_t>(info.stride) * static_cast<uint64_t>(info.height);
312
- if (reported != static_cast<uint64_t>(byteCount)) {
313
- return JNI_FALSE;
314
- }
315
-
316
- std::shared_ptr<SlotBuffer> buffer;
317
- try {
318
- buffer = std::make_shared<SlotBuffer>(static_cast<size_t>(byteCount));
319
- } catch (...) {
320
- // `std::bad_alloc` for a 4 MiB raster under pressure. A C++ exception must
321
- // never unwind out of a JNI method — the ART frame below is not
322
- // exception-aware and the unwind reaches `std::terminate` — so it stops here
323
- // and becomes a `false`.
324
- return JNI_FALSE;
325
- }
326
-
327
- void* pixels = nullptr;
328
- if (AndroidBitmap_lockPixels(env, bitmap, &pixels) != ANDROID_BITMAP_RESULT_SUCCESS ||
329
- pixels == nullptr) {
330
- // Nothing to unlock: a failed lock is not balanced by an unlock. `buffer`
331
- // is freed by its shared_ptr on the way out and no slot was published.
332
- return JNI_FALSE;
333
- }
334
- std::memcpy(buffer->data(), pixels, static_cast<size_t>(byteCount));
335
- // Balanced immediately, and before anything that could throw or return early.
336
- // A leaked lock would pin the Bitmap's pixels for the life of the process.
337
- AndroidBitmap_unlockPixels(env, bitmap);
338
-
339
- try {
340
- // PUBLISHED LAST. Until this line the buffer is reachable only from this
341
- // stack frame, so `nativeReleaseAll` on another thread cannot free memory
342
- // this method is still writing into. See the header note.
343
- if (!publishSlot(static_cast<int64_t>(slot), std::move(buffer))) {
344
- return JNI_FALSE;
345
- }
346
- } catch (...) {
347
- // The map node allocation can throw too, and it is inside the lock.
348
- return JNI_FALSE;
349
- }
350
- return JNI_TRUE;
351
- }
352
-
353
- JNIEXPORT void JNICALL
354
- Java_tools_reekon_pdfcanvas_jsi_PdfCanvasPixelBridge_nativeRelease(
355
- JNIEnv*,
356
- jclass,
357
- jlong slot) {
358
- takeSlot(static_cast<int64_t>(slot));
359
- }
360
-
361
- JNIEXPORT void JNICALL
362
- Java_tools_reekon_pdfcanvas_jsi_PdfCanvasPixelBridge_nativeReleaseAll(
363
- JNIEnv*,
364
- jclass) {
365
- std::unordered_map<int64_t, std::shared_ptr<SlotBuffer>> drained;
366
- {
367
- std::lock_guard<std::mutex> lock(g_slotsMutex);
368
- drained.swap(g_slots);
369
- }
370
- // Freed outside the lock. Dropping several megabytes of vectors is not
371
- // something to do while the JS thread might be waiting on the same mutex.
372
- }
373
-
374
- JNIEXPORT jint JNICALL
375
- Java_tools_reekon_pdfcanvas_jsi_PdfCanvasPixelBridge_nativeSlotCount(
376
- JNIEnv*,
377
- jclass) {
378
- std::lock_guard<std::mutex> lock(g_slotsMutex);
379
- return static_cast<jint>(g_slots.size());
380
- }
381
-
382
- /**
383
- * The test seam, and the reason the standalone build compiles this file at all.
384
- *
385
- * Takes a slot back out as a Java `byte[]` through the SAME storage the
386
- * ArrayBuffer would have wrapped. An instrumented test can therefore render a
387
- * tile, store it, take it back, and compare against `Base64.decode` of the same
388
- * pixels — proving the transport moves the bytes the base64 path moved, without
389
- * needing a JS runtime or a whole app around it.
390
- */
391
- JNIEXPORT jbyteArray JNICALL
392
- Java_tools_reekon_pdfcanvas_jsi_PdfCanvasPixelBridge_nativeTakeForTest(
393
- JNIEnv* env,
394
- jclass,
395
- jlong slot) {
396
- std::shared_ptr<SlotBuffer> buffer = takeSlot(static_cast<int64_t>(slot));
397
- if (buffer == nullptr) {
398
- return nullptr;
399
- }
400
- const jsize length = static_cast<jsize>(buffer->size());
401
- jbyteArray out = env->NewByteArray(length);
402
- if (out == nullptr) {
403
- return nullptr;
404
- }
405
- if (length > 0) {
406
- env->SetByteArrayRegion(
407
- out, 0, length, reinterpret_cast<const jbyte*>(buffer->data()));
408
- }
409
- return out;
410
- }
411
-
412
- JNIEXPORT jboolean JNICALL
413
- Java_tools_reekon_pdfcanvas_jsi_PdfCanvasPixelBridge_nativeInstall(
414
- JNIEnv*,
415
- jclass,
416
- jlong runtimePointer) {
417
- #ifdef PDFCANVAS_WITH_JSI
418
- if (runtimePointer == 0) {
419
- return JNI_FALSE;
420
- }
421
- auto& runtime = *reinterpret_cast<facebook::jsi::Runtime*>(runtimePointer);
422
- try {
423
- auto name =
424
- facebook::jsi::PropNameID::forAscii(runtime, "__pdfCanvasTakePixels");
425
- auto take = facebook::jsi::Function::createFromHostFunction(
426
- runtime,
427
- name,
428
- 1,
429
- [](facebook::jsi::Runtime& rt,
430
- const facebook::jsi::Value&,
431
- const facebook::jsi::Value* args,
432
- size_t count) -> facebook::jsi::Value {
433
- if (count < 1 || !args[0].isNumber()) {
434
- throw facebook::jsi::JSError(
435
- rt, "__pdfCanvasTakePixels(slot): slot must be a number.");
436
- }
437
- const auto slot = static_cast<int64_t>(args[0].asNumber());
438
- std::shared_ptr<SlotBuffer> buffer = takeSlot(slot);
439
- if (buffer == nullptr) {
440
- // Every slot is taken exactly once, so this means the render that
441
- // filled it was torn down (a reload drains every slot) or the same
442
- // slot was taken twice. Named rather than silently empty: an empty
443
- // ArrayBuffer would reach Skia and draw a black tile.
444
- throw facebook::jsi::JSError(
445
- rt,
446
- "__pdfCanvasTakePixels: no raster is waiting in slot " +
447
- std::to_string(slot) +
448
- ". It was already taken, or the module was invalidated.");
449
- }
450
- // The ArrayBuffer BORROWS this vector and the shared_ptr keeps it
451
- // alive; the slot map has already dropped its reference, so JS is now
452
- // the sole owner and the GC is what frees it.
453
- return facebook::jsi::Value(
454
- rt, facebook::jsi::ArrayBuffer(rt, std::move(buffer)));
455
- });
456
- runtime.global().setProperty(runtime, name, take);
457
- return JNI_TRUE;
458
- } catch (...) {
459
- // A runtime that refuses to install a host function is a runtime this
460
- // package will not use. The JS side falls back to base64.
461
- return JNI_FALSE;
462
- }
463
- #else
464
- (void)runtimePointer;
465
- return JNI_FALSE;
466
- #endif
467
- }
468
-
469
- } // extern "C"
@@ -1,140 +0,0 @@
1
- package tools.reekon.pdfcanvas.jsi;
2
-
3
- import android.graphics.Bitmap;
4
-
5
- /**
6
- * The Java face of the raster transport — see {@code android/src/jsi/cpp/pdfcanvas-jsi.cpp}.
7
- *
8
- * <h3>Why this is its own source set</h3>
9
- *
10
- * It is compiled by BOTH builds. {@code src/main} is the React-free rasterizer
11
- * core and {@code src/reactnative} is the React binding; this is a third thing —
12
- * React-free like the core, but native, and only half of it is reachable without
13
- * a React Native runtime. Keeping it separate is what lets the standalone build
14
- * (which has no ReactAndroid, no libjsi.so, and is the build that runs the
15
- * instrumented suite) compile the slot storage and prove its byte fidelity,
16
- * while the autolinked build additionally gets {@link #nativeInstall}.
17
- *
18
- * <h3>The contract</h3>
19
- *
20
- * A slot is a monotonically increasing id chosen by the caller. The pixels reach
21
- * C++-owned memory on whatever thread stores them — the render executor, never
22
- * the JS thread — and are taken exactly once, either by the JS host function
23
- * {@code __pdfCanvasTakePixels} (which wraps them in a zero-copy ArrayBuffer) or
24
- * by {@link #nativeTakeForTest}. Taking removes the slot; a slot that is never
25
- * taken is freed by {@link #nativeReleaseAll}.
26
- *
27
- * <p>TWO WAYS IN, and they are not equivalent.
28
- * {@link #nativeStoreFromBitmap} is the one-copy path: it allocates the native
29
- * buffer and fills it from the {@code Bitmap}'s own pixels, so no {@code byte[]}
30
- * is ever allocated. {@link #nativeStore} is the two-copy path, kept because the
31
- * base64 fallback needs that {@code byte[]} regardless. Prefer the former; use
32
- * the latter when the bytes are already in a Java array.
33
- *
34
- * <h3>Degrading</h3>
35
- *
36
- * {@link #isLoaded()} is false when {@code libpdfcanvasjsi.so} is absent from the
37
- * APK — an older app binary, a build that skipped the native build, an ABI with
38
- * no artifact. Every caller must treat that as "use base64", because rendering
39
- * correctly matters more than rendering fast. Nothing here throws to say so.
40
- */
41
- public final class PdfCanvasPixelBridge {
42
-
43
- /**
44
- * The self-test payload.
45
- *
46
- * Stored by the module during install and taken by JS before the transport is
47
- * enabled, so the first bytes to cross are ones both sides already agree on.
48
- * It contains 0x00, 0xFF and a byte above 0x7F on purpose: Java's `byte` is
49
- * signed, and a sign-extension slip in the JNI copy or in the ArrayBuffer view
50
- * shows up here rather than as a discoloured tile.
51
- */
52
- public static final byte[] PROBE_BYTES = {
53
- 0x00, 0x01, 0x02, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, 0x2A, 0x07
54
- };
55
-
56
- private static final boolean LOADED = load();
57
-
58
- private PdfCanvasPixelBridge() {}
59
-
60
- private static boolean load() {
61
- try {
62
- System.loadLibrary("pdfcanvasjsi");
63
- return true;
64
- } catch (Throwable t) {
65
- // UnsatisfiedLinkError on a build without the native library, and
66
- // deliberately not rethrown: the package must still render.
67
- return false;
68
- }
69
- }
70
-
71
- /** Whether {@code libpdfcanvasjsi.so} loaded. False means base64. */
72
- public static boolean isLoaded() {
73
- return LOADED;
74
- }
75
-
76
- /**
77
- * Installs {@code globalThis.__pdfCanvasTakePixels} into the JSI runtime.
78
- *
79
- * MUST be called on the JS thread with a live runtime pointer. Returns false
80
- * — never throws — when the library was built without JSI (the standalone
81
- * build), when the pointer is null, or when the runtime refuses the host
82
- * function.
83
- */
84
- public static native boolean nativeInstall(long runtimePointer);
85
-
86
- /**
87
- * Copies {@code bytes} into C++-owned memory under {@code slot}.
88
- *
89
- * The bytes are already on the Java heap by the time this is called, so this
90
- * is the SECOND full-raster copy of that raster. Use
91
- * {@link #nativeStoreFromBitmap} where the source is still a {@code Bitmap}.
92
- */
93
- public static native void nativeStore(long slot, byte[] bytes);
94
-
95
- /**
96
- * Copies a {@code Bitmap}'s pixels DIRECTLY into C++-owned memory under
97
- * {@code slot} — one memcpy, and not one byte of Java heap.
98
- *
99
- * <p>{@code width}, {@code height} and {@code byteCount} are the caller's
100
- * already-validated view of the bitmap, and every one of them is cross-checked
101
- * against what {@code AndroidBitmap_getInfo} reports for the SAME bitmap. A
102
- * disagreement is a refusal rather than a reconciliation: copying a length the
103
- * source does not have, or describing a raster as a shape it is not, would
104
- * shear the image — which reads as a plausible skew rather than an error.
105
- *
106
- * <p>RETURNS FALSE INSTEAD OF THROWING, for every failure — a native
107
- * allocation that could not be had, a bitmap that would not lock, a format or
108
- * stride that did not match, an id already occupied. False means "the raster
109
- * is NOT in the slot; fall back to the {@code byte[]} path", and no slot is
110
- * left behind to release. The caller must not treat it as an error: the
111
- * fallback is the transport that has always worked.
112
- *
113
- * <p>MUST NOT be called when {@link #isLoaded()} is false — like every native
114
- * method here, that is an {@code UnsatisfiedLinkError}, not a {@code false}.
115
- *
116
- * @param bitmap a mutable, non-recycled {@code ARGB_8888} bitmap. It is locked
117
- * for the duration of the copy and unlocked before this returns.
118
- */
119
- public static native boolean nativeStoreFromBitmap(
120
- long slot, Bitmap bitmap, int width, int height, int byteCount);
121
-
122
- /** Drops one slot if it is still there. */
123
- public static native void nativeRelease(long slot);
124
-
125
- /** Drops every slot. Called from {@code PdfCanvasModule.invalidate()}. */
126
- public static native void nativeReleaseAll();
127
-
128
- /** Slots currently waiting to be taken. For assertions, not for logic. */
129
- public static native int nativeSlotCount();
130
-
131
- /**
132
- * Takes a slot back out as a Java array, through the identical storage the
133
- * ArrayBuffer would have wrapped. Null when the slot is empty.
134
- *
135
- * The instrumented suite's only way to check the transport without a JS
136
- * runtime, which is why it ships rather than living in androidTest: the JNI
137
- * copy under test is the production one.
138
- */
139
- public static native byte[] nativeTakeForTest(long slot);
140
- }