@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.1
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.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +77 -40
- package/README.md +218 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +559 -0
- package/scripts/pdfium-manifest.json +48 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -5,6 +5,7 @@ import android.net.Uri;
|
|
|
5
5
|
import android.os.ParcelFileDescriptor;
|
|
6
6
|
import android.util.Base64;
|
|
7
7
|
|
|
8
|
+
import com.facebook.react.bridge.Arguments;
|
|
8
9
|
import com.facebook.react.bridge.JavaScriptContextHolder;
|
|
9
10
|
import com.facebook.react.bridge.Promise;
|
|
10
11
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
@@ -13,150 +14,87 @@ import com.facebook.react.bridge.ReactMethod;
|
|
|
13
14
|
import com.facebook.react.bridge.ReadableMap;
|
|
14
15
|
import com.facebook.react.bridge.WritableArray;
|
|
15
16
|
import com.facebook.react.bridge.WritableMap;
|
|
16
|
-
import com.facebook.react.bridge.Arguments;
|
|
17
17
|
|
|
18
18
|
import java.io.File;
|
|
19
19
|
import java.io.FileOutputStream;
|
|
20
20
|
import java.io.IOException;
|
|
21
21
|
import java.io.InputStream;
|
|
22
|
-
import java.util.Map;
|
|
23
|
-
import java.util.concurrent.ConcurrentHashMap;
|
|
24
22
|
import java.util.concurrent.ExecutorService;
|
|
25
23
|
import java.util.concurrent.Executors;
|
|
26
24
|
import java.util.concurrent.RejectedExecutionException;
|
|
27
|
-
import java.util.concurrent.atomic.AtomicInteger;
|
|
28
|
-
import java.util.concurrent.atomic.AtomicLong;
|
|
29
25
|
|
|
30
|
-
import tools.reekon.pdfcanvas.
|
|
31
|
-
import tools.reekon.pdfcanvas.PageGeometry;
|
|
32
|
-
import tools.reekon.pdfcanvas.PdfCanvasRasterizer;
|
|
26
|
+
import tools.reekon.pdfcanvas.PdfCanvasNative;
|
|
33
27
|
import tools.reekon.pdfcanvas.PdfErrorCode;
|
|
34
|
-
import tools.reekon.pdfcanvas.PdfPlatformQuirks;
|
|
35
28
|
import tools.reekon.pdfcanvas.PdfRasterException;
|
|
36
|
-
import tools.reekon.pdfcanvas.RasterCancellation;
|
|
37
|
-
import tools.reekon.pdfcanvas.RasterPixels;
|
|
38
|
-
import tools.reekon.pdfcanvas.RasterRequest;
|
|
39
29
|
|
|
40
30
|
/**
|
|
41
31
|
* MARSHALLING ONLY.
|
|
42
32
|
*
|
|
43
|
-
* Every line here converts a type, resolves a URI, picks a thread or
|
|
44
|
-
* promise. Not one of them decides anything about rasterizing —
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
33
|
+
* Every line here converts a type, resolves a URI, picks a thread or settles a
|
|
34
|
+
* promise. Not one of them decides anything about rasterizing, and — new since
|
|
35
|
+
* the PDFium core — not one of them keeps state about documents either: the
|
|
36
|
+
* handle map, the (handle, token) cancellation registry and the invalidate
|
|
37
|
+
* ordering all live in {@code pdfcanvas::Service} in C++, shared with the iOS
|
|
38
|
+
* binding. What this class owns is a service pointer, a worker thread and a
|
|
39
|
+
* transport flag.
|
|
50
40
|
*
|
|
51
41
|
* <h3>The transport</h3>
|
|
52
42
|
*
|
|
53
43
|
* TWO OF THEM, chosen at runtime, and the fast one has to earn it.
|
|
54
44
|
*
|
|
55
|
-
* <p>BASE64 is the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* encode here, ~72.6 ms of string marshalling, and <b>1005.46 ms</b> of
|
|
59
|
-
* {@code decodeBase64} in Hermes — 90.6% of the tile, spent undoing an encode
|
|
60
|
-
* that finished a moment earlier.
|
|
61
|
-
*
|
|
62
|
-
* <p>JSI ARRAYBUFFER is the fast one. The raster is written STRAIGHT from the
|
|
63
|
-
* {@code Bitmap} into C++-owned memory on this render's worker thread — see
|
|
64
|
-
* {@link PdfCanvasRasterizer#renderToNativeSlot}, which asks
|
|
65
|
-
* {@link PdfCanvasPixelBridge#nativeStoreFromBitmap} to do it in one memcpy — the
|
|
66
|
-
* promise resolves with a {@code pixelsSlot} number instead of a string, and JS
|
|
67
|
-
* calls the installed {@code __pdfCanvasTakePixels(slot)} host function to
|
|
68
|
-
* receive an ArrayBuffer that WRAPS that memory. No string, no decode, nothing
|
|
69
|
-
* copied on the JS thread, and no {@code byte[]} on the Java heap at all.
|
|
45
|
+
* <p>BASE64 is the fallback. Neither the legacy bridge nor new-architecture
|
|
46
|
+
* codegen has a binary type; a 1024x1024 tile measured 1005 ms of
|
|
47
|
+
* {@code decodeBase64} in Hermes on an LG V60.
|
|
70
48
|
*
|
|
71
|
-
* <p>
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* of native memory. They are enumerated at {@link #render}.
|
|
49
|
+
* <p>JSI ARRAYBUFFER is the fast one. PDFium writes the raster STRAIGHT into
|
|
50
|
+
* C++-owned memory (a slot), the promise resolves with a {@code pixelsSlot}
|
|
51
|
+
* number, and JS calls the installed {@code __pdfCanvasTakePixels(slot)} host
|
|
52
|
+
* function to receive an ArrayBuffer that WRAPS that memory. Zero copies, on
|
|
53
|
+
* Android as on iOS — the one-copy `Bitmap` path is gone with `PdfRenderer`.
|
|
77
54
|
*
|
|
78
55
|
* <p>It is selected by {@link #installJsiTransport} + {@link #enableJsiTransport},
|
|
79
56
|
* never assumed: install returns a probe slot, JS takes it and checks the bytes
|
|
80
|
-
* against {@link
|
|
81
|
-
*
|
|
82
|
-
* runtime that refuses the host function, or a Hermes that will not make an
|
|
83
|
-
* ArrayBuffer out of a {@code MutableBuffer} all leave the flag off and every
|
|
84
|
-
* render goes back to base64. Rendering correctly is worth more than rendering
|
|
85
|
-
* fast.
|
|
57
|
+
* against {@link PdfCanvasNative#PROBE_BYTES}, and only a JS-side match turns
|
|
58
|
+
* {@link #jsiEnabled} on.
|
|
86
59
|
*
|
|
87
60
|
* <h3>Threading</h3>
|
|
88
61
|
*
|
|
89
|
-
* {@code @ReactMethod} runs on the bridge's native-modules thread
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
62
|
+
* {@code @ReactMethod} runs on the bridge's native-modules thread and a render
|
|
63
|
+
* is a long synchronous call, so every open and render is handed to ONE worker.
|
|
64
|
+
* One, because PDFium is not thread-safe and the core serialises every call
|
|
65
|
+
* behind a process-wide lock anyway; a second worker would only queue behind
|
|
66
|
+
* the first while holding a promise open.
|
|
94
67
|
*
|
|
95
|
-
* EVERY {@code ReadableMap} IS DRAINED ON THE CALLING THREAD, before the
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* duration of the {@code @ReactMethod} call; reading it from a worker after the
|
|
99
|
-
* method has returned is a use-after-free that presents as garbage values or a
|
|
100
|
-
* native crash, and never as an exception you could map to a
|
|
101
|
-
* {@code PdfErrorCode}. So each method marshals to plain Java values first and
|
|
102
|
-
* the lambda closes over those.
|
|
103
|
-
*
|
|
104
|
-
* <h3>UNVERIFIED</h3>
|
|
105
|
-
*
|
|
106
|
-
* Nothing in this file has ever been compiled or run — that needs a build of a
|
|
107
|
-
* consuming app. The three fixes marked {@code UNVERIFIED FIX} below were made
|
|
108
|
-
* by inspection against the bridge's documented contracts, not observed. Treat
|
|
109
|
-
* them, and the rest of this file, as unreviewed code that happens to be
|
|
110
|
-
* type-correct.
|
|
68
|
+
* <p>EVERY {@code ReadableMap} IS DRAINED ON THE CALLING THREAD, before the
|
|
69
|
+
* worker is handed anything: on the legacy bridge a {@code ReadableNativeMap} is
|
|
70
|
+
* only valid for the duration of the {@code @ReactMethod} call.
|
|
111
71
|
*/
|
|
112
72
|
public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
113
73
|
|
|
114
74
|
public static final String NAME = "PdfCanvas";
|
|
115
75
|
|
|
116
|
-
/** Mirrors the native pool ceiling; see PdfRendererPool.MAX_POOL_SIZE. */
|
|
117
|
-
private static final int MAX_POOL_SIZE = 4;
|
|
118
|
-
|
|
119
76
|
private final ReactApplicationContext reactContext;
|
|
120
|
-
private final Map<Integer, PdfCanvasRasterizer> handles = new ConcurrentHashMap<>();
|
|
121
|
-
/** Keyed by (handle, token) so a token is only meaningful with its handle. */
|
|
122
|
-
private final Map<Long, RasterCancellation> signals = new ConcurrentHashMap<>();
|
|
123
|
-
private final AtomicInteger nextHandle = new AtomicInteger(1);
|
|
124
|
-
private final ExecutorService renderExecutor =
|
|
125
|
-
Executors.newFixedThreadPool(MAX_POOL_SIZE);
|
|
126
|
-
|
|
127
77
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* STATIC, because the map it keys is static. The slots live in one
|
|
133
|
-
* process-wide {@code std::unordered_map} in {@code pdfcanvas-jsi.cpp}, so a
|
|
134
|
-
* per-instance counter starting at 1 would hand the SAME id to two module
|
|
135
|
-
* instances — and RN builds a fresh set of native modules for every reload
|
|
136
|
-
* while the outgoing one may still have rasters parked. The second store then
|
|
137
|
-
* silently replaces the first module's buffer, and that module's take returns
|
|
138
|
-
* ANOTHER tile's pixels: a wrong image, not an error. One counter per process
|
|
139
|
-
* is what makes "taken exactly once" true of the storage rather than of a
|
|
140
|
-
* single module.
|
|
78
|
+
* The C++ service. 0 when the native library failed to load, in which case
|
|
79
|
+
* every method rejects with {@code backend-failure} and says so. Never freed —
|
|
80
|
+
* see {@link PdfCanvasNative}.
|
|
141
81
|
*/
|
|
142
|
-
private
|
|
82
|
+
private final long service;
|
|
83
|
+
private final ExecutorService worker = Executors.newSingleThreadExecutor();
|
|
143
84
|
|
|
144
85
|
/**
|
|
145
86
|
* Whether renders return a slot instead of a base64 string.
|
|
146
87
|
*
|
|
147
|
-
* Volatile
|
|
148
|
-
* #
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* FALSE UNTIL PROVEN. It is never set by this class deciding the fast path
|
|
152
|
-
* looks available; it is set by JS after a byte-for-byte round trip of {@link
|
|
153
|
-
* PdfCanvasPixelBridge#PROBE_BYTES}.
|
|
88
|
+
* Volatile: written on the JS thread by {@link #enableJsiTransport} and
|
|
89
|
+
* {@link #invalidate}, read on the worker by every render. FALSE UNTIL
|
|
90
|
+
* PROVEN by the JS-side probe round trip.
|
|
154
91
|
*/
|
|
155
92
|
private volatile boolean jsiEnabled = false;
|
|
156
93
|
|
|
157
94
|
public PdfCanvasModule(ReactApplicationContext context) {
|
|
158
95
|
super(context);
|
|
159
96
|
this.reactContext = context;
|
|
97
|
+
this.service = PdfCanvasNative.isLoaded() ? PdfCanvasNative.createService() : 0L;
|
|
160
98
|
}
|
|
161
99
|
|
|
162
100
|
@Override
|
|
@@ -166,45 +104,37 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
166
104
|
|
|
167
105
|
@Override
|
|
168
106
|
public void invalidate() {
|
|
169
|
-
// FIRST,
|
|
170
|
-
// STARTED can ask for a slot.
|
|
171
|
-
//
|
|
172
|
-
// IT NO LONGER DIVERTS A RENDER ALREADY IN FLIGHT, and it used to. The flag
|
|
173
|
-
// was read AFTER the raster came back (the old putPixelsSlot), so clearing
|
|
174
|
-
// it here made an in-flight worker fall back to base64 and strand nothing.
|
|
175
|
-
// render() now reads it BEFORE the draw, because the fill happens inside the
|
|
176
|
-
// rasterizer while the Bitmap is still alive — so a worker already inside
|
|
177
|
-
// PdfRenderer.render (uninterruptible; shutdownNow only sets the flag) holds
|
|
178
|
-
// a slot id already, and will publish its raster after the nativeReleaseAll
|
|
179
|
-
// below has run past. Nothing takes it, and it is freed by the NEXT
|
|
180
|
-
// invalidate() rather than by this one.
|
|
181
|
-
//
|
|
182
|
-
// Bounded — at most one raster per busy lane, dev-only (Fast Refresh), and
|
|
183
|
-
// reclaimed on the following reload. NOT closed by re-reading the flag after
|
|
184
|
-
// the render: negotiateTransport in a SECOND JS runtime calls
|
|
185
|
-
// enableJsiTransport(false) on this same shared flag while the first
|
|
186
|
-
// runtime's renders are in flight, so freeing on a cleared flag would hand
|
|
187
|
-
// that runtime an unredeemable slot instead of a tile. Closing it properly
|
|
188
|
-
// needs a teardown epoch the rasterizer can see, which is not this change.
|
|
107
|
+
// FIRST, so no render that has not yet STARTED asks for a slot.
|
|
189
108
|
jsiEnabled = false;
|
|
190
109
|
// A reload drops the JS side without ever calling close(), so every open
|
|
191
|
-
// document would otherwise keep its
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
renderExecutor.shutdownNow();
|
|
199
|
-
// A reload also strands any slot whose promise resolved but whose JS
|
|
200
|
-
// continuation never ran. Each one is a whole raster — megabytes — held in
|
|
201
|
-
// C++ with nothing left that could ever take it.
|
|
202
|
-
if (PdfCanvasPixelBridge.isLoaded()) {
|
|
203
|
-
PdfCanvasPixelBridge.nativeReleaseAll();
|
|
110
|
+
// document would otherwise keep its parsed pages until the process dies.
|
|
111
|
+
// The C++ side closes every handle (waiting for an in-flight render to hit
|
|
112
|
+
// its next cancellation checkpoint), cancels every registered signal and
|
|
113
|
+
// drains every parked slot — rasters whose promise resolved but whose JS
|
|
114
|
+
// continuation never ran.
|
|
115
|
+
if (service != 0L) {
|
|
116
|
+
PdfCanvasNative.invalidateService(service);
|
|
204
117
|
}
|
|
118
|
+
// After the service is invalidated, so a task that was queued behind an
|
|
119
|
+
// in-flight render fails fast with backend-failure rather than rendering
|
|
120
|
+
// into a torn-down world. Tasks that never start leave their promise
|
|
121
|
+
// unsettled — dev-only (Fast Refresh), and the same as before.
|
|
122
|
+
worker.shutdownNow();
|
|
205
123
|
super.invalidate();
|
|
206
124
|
}
|
|
207
125
|
|
|
126
|
+
private boolean requireService(Promise promise) {
|
|
127
|
+
if (service != 0L) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
promise.reject(
|
|
131
|
+
PdfErrorCode.BACKEND_FAILURE.wire(),
|
|
132
|
+
"libpdfcanvas.so did not load. The APK is missing the native library for this ABI "
|
|
133
|
+
+ "(or the libpdfium.so it links); check that the app was rebuilt after adding "
|
|
134
|
+
+ "@reekon-tools/react-native-pdf-canvas.");
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
208
138
|
/* ---------------------------------------------------------------- *
|
|
209
139
|
* transport
|
|
210
140
|
* ---------------------------------------------------------------- */
|
|
@@ -212,28 +142,10 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
212
142
|
/**
|
|
213
143
|
* Installs the JSI host function and parks one probe raster for JS to take.
|
|
214
144
|
*
|
|
215
|
-
* SYNCHRONOUS on purpose
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* whole probe — install, take, enable — completes before the JS module that
|
|
220
|
-
* called it finishes evaluating, so no render can ever race the decision.
|
|
221
|
-
*
|
|
222
|
-
* <h3>What the probe covers, and what it does not</h3>
|
|
223
|
-
*
|
|
224
|
-
* It round-trips {@link PdfCanvasPixelBridge#PROBE_BYTES} through
|
|
225
|
-
* {@code nativeStore} — the {@code byte[]} store — because a probe needs bytes
|
|
226
|
-
* both sides already agree on and there is no Bitmap at install time. So it
|
|
227
|
-
* proves the JS HALF: the library loaded, the host function is installed,
|
|
228
|
-
* Hermes will build an ArrayBuffer over a {@code MutableBuffer}, and byte
|
|
229
|
-
* values survive the view unmangled.
|
|
230
|
-
*
|
|
231
|
-
* <p>It does NOT exercise the fill every subsequent raster uses.
|
|
232
|
-
* {@code nativeStoreFromBitmap} is a different memcpy from a different source,
|
|
233
|
-
* and its fidelity is proven on device by
|
|
234
|
-
* {@code PdfCanvasJsiTransportTest.directFillBytesEqualTheHeapPathForARealRaster}
|
|
235
|
-
* instead — in the standalone build, with no app around it. Worth knowing when
|
|
236
|
-
* reading a bug report: a passing probe says nothing about the fill.
|
|
145
|
+
* SYNCHRONOUS on purpose: a blocking synchronous {@code @ReactMethod} executes
|
|
146
|
+
* ON THE JS THREAD, the only thread where {@code JavaScriptContextHolder.get()}
|
|
147
|
+
* is a live pointer, and it completes before the JS module that called it
|
|
148
|
+
* finishes evaluating, so no render can race the decision.
|
|
237
149
|
*
|
|
238
150
|
* @return the probe slot id, or -1 if the fast path is unavailable. Never
|
|
239
151
|
* throws: every failure here is a fallback, not an error.
|
|
@@ -241,116 +153,89 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
241
153
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
242
154
|
public double installJsiTransport() {
|
|
243
155
|
try {
|
|
244
|
-
if (
|
|
156
|
+
if (service == 0L) {
|
|
245
157
|
return -1;
|
|
246
158
|
}
|
|
247
159
|
JavaScriptContextHolder holder = getReactApplicationContext().getJavaScriptContextHolder();
|
|
248
160
|
if (holder == null || holder.get() == 0) {
|
|
249
161
|
return -1;
|
|
250
162
|
}
|
|
251
|
-
if (!
|
|
163
|
+
if (!PdfCanvasNative.installJsi(holder.get())) {
|
|
252
164
|
return -1;
|
|
253
165
|
}
|
|
254
|
-
|
|
255
|
-
PdfCanvasPixelBridge.nativeStore(slot, PdfCanvasPixelBridge.PROBE_BYTES);
|
|
256
|
-
return (double) slot;
|
|
166
|
+
return (double) PdfCanvasNative.storeProbe();
|
|
257
167
|
} catch (Throwable t) {
|
|
258
168
|
return -1;
|
|
259
169
|
}
|
|
260
170
|
}
|
|
261
171
|
|
|
262
|
-
/**
|
|
263
|
-
* Turns the fast transport on, once JS has verified the probe bytes.
|
|
264
|
-
*
|
|
265
|
-
* Synchronous for the same reason as {@link #installJsiTransport}: an async
|
|
266
|
-
* call would be ordered before any render, but "ordered before" is a property
|
|
267
|
-
* of the bridge queue, and this decision is too cheap to make it depend on one.
|
|
268
|
-
*
|
|
269
|
-
* @return the flag as it now stands, so the caller reports what is really live
|
|
270
|
-
* rather than what it asked for.
|
|
271
|
-
*/
|
|
172
|
+
/** Turns the fast transport on, once JS has verified the probe bytes. */
|
|
272
173
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
273
174
|
public boolean enableJsiTransport(boolean enabled) {
|
|
274
|
-
jsiEnabled = enabled &&
|
|
175
|
+
jsiEnabled = enabled && service != 0L;
|
|
275
176
|
return jsiEnabled;
|
|
276
177
|
}
|
|
277
178
|
|
|
179
|
+
/** Slots currently parked, for a host checking that the transport does not leak. */
|
|
180
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
181
|
+
public double pendingRasterSlots() {
|
|
182
|
+
return service == 0L ? 0 : PdfCanvasNative.slotCount();
|
|
183
|
+
}
|
|
184
|
+
|
|
278
185
|
/* ---------------------------------------------------------------- *
|
|
279
186
|
* open
|
|
280
187
|
* ---------------------------------------------------------------- */
|
|
281
188
|
|
|
282
189
|
@ReactMethod
|
|
283
|
-
public void open(ReadableMap source,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
190
|
+
public void open(ReadableMap source, Promise promise) {
|
|
191
|
+
if (!requireService(promise)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Drained HERE, on the bridge thread, not inside the worker lambda.
|
|
287
195
|
final String password = source.hasKey("password") ? source.getString("password") : null;
|
|
288
196
|
final String base64 = source.hasKey("base64") ? source.getString("base64") : null;
|
|
289
197
|
final String uri = source.hasKey("uri") ? source.getString("uri") : null;
|
|
290
|
-
final int clamped = Math.max(1, Math.min(MAX_POOL_SIZE, poolSize));
|
|
291
198
|
|
|
292
199
|
try {
|
|
293
|
-
|
|
200
|
+
worker.execute(
|
|
294
201
|
() -> {
|
|
295
|
-
PdfCanvasRasterizer rasterizer = null;
|
|
296
|
-
boolean published = false;
|
|
297
202
|
try {
|
|
203
|
+
final double[] opened;
|
|
298
204
|
if (base64 != null) {
|
|
299
205
|
byte[] data = Base64.decode(base64, Base64.DEFAULT);
|
|
300
|
-
|
|
301
|
-
PdfCanvasRasterizer.openBytes(
|
|
302
|
-
reactContext.getCacheDir(), data, password, clamped);
|
|
206
|
+
opened = PdfCanvasNative.open(service, null, data, password);
|
|
303
207
|
} else {
|
|
304
|
-
|
|
208
|
+
opened =
|
|
209
|
+
PdfCanvasNative.open(service, resolve(uri).getAbsolutePath(), null, password);
|
|
305
210
|
}
|
|
306
211
|
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// opening, so a throw in the geometry loop (or an OOM building the
|
|
311
|
-
// result map) rejected the promise while leaving an open rasterizer
|
|
312
|
-
// — file descriptors and parsed pages — that JS had no handle for
|
|
313
|
-
// and could never close. The `published` flag is what makes the
|
|
314
|
-
// finally able to tell "handed over" from "still ours".
|
|
212
|
+
// [handle, pageCount, w, h, rot, w, h, rot, ...]
|
|
213
|
+
final int handle = (int) opened[0];
|
|
214
|
+
final int pageCount = (int) opened[1];
|
|
315
215
|
WritableArray pages = Arguments.createArray();
|
|
316
|
-
for (int i = 0; i <
|
|
317
|
-
PageGeometry g = rasterizer.pageGeometry(i);
|
|
216
|
+
for (int i = 0; i < pageCount; i++) {
|
|
318
217
|
WritableMap page = Arguments.createMap();
|
|
319
|
-
page.putInt("index",
|
|
320
|
-
|
|
321
|
-
page.
|
|
322
|
-
page.
|
|
218
|
+
page.putInt("index", i);
|
|
219
|
+
// DOUBLES: a PDF point is not an integer — A4 is 595.276 wide.
|
|
220
|
+
page.putDouble("width", opened[2 + i * 3]);
|
|
221
|
+
page.putDouble("height", opened[3 + i * 3]);
|
|
222
|
+
page.putInt("rotation", (int) opened[4 + i * 3]);
|
|
323
223
|
pages.pushMap(page);
|
|
324
224
|
}
|
|
325
|
-
|
|
326
225
|
WritableMap result = Arguments.createMap();
|
|
327
|
-
result.putInt("pageCount", rasterizer.pageCount());
|
|
328
|
-
result.putArray("pages", pages);
|
|
329
|
-
result.putString(
|
|
330
|
-
"rotationSupport",
|
|
331
|
-
PdfCanvasRasterizer.rotationSupport(reactContext.getCacheDir())
|
|
332
|
-
.name()
|
|
333
|
-
.toLowerCase(java.util.Locale.US));
|
|
334
|
-
|
|
335
|
-
int handle = nextHandle.getAndIncrement();
|
|
336
226
|
result.putInt("handle", handle);
|
|
337
|
-
|
|
338
|
-
|
|
227
|
+
result.putInt("pageCount", pageCount);
|
|
228
|
+
result.putArray("pages", pages);
|
|
339
229
|
promise.resolve(result);
|
|
340
230
|
} catch (PdfRasterException e) {
|
|
341
231
|
reject(promise, e);
|
|
342
232
|
} catch (Throwable t) {
|
|
343
233
|
reject(promise, PdfErrorCode.BACKEND_FAILURE, t);
|
|
344
|
-
} finally {
|
|
345
|
-
if (rasterizer != null && !published) {
|
|
346
|
-
rasterizer.close();
|
|
347
|
-
}
|
|
348
234
|
}
|
|
349
235
|
});
|
|
350
236
|
} catch (RejectedExecutionException e) {
|
|
351
|
-
// invalidate() shut the
|
|
352
|
-
// and the promise never settles
|
|
353
|
-
// hangs for the life of the app rather than an error anyone can see.
|
|
237
|
+
// invalidate() shut the worker down. Without this the task never runs
|
|
238
|
+
// and the promise never settles.
|
|
354
239
|
reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
|
|
355
240
|
}
|
|
356
241
|
}
|
|
@@ -359,9 +244,9 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
359
244
|
* Accepts a plain path, a {@code file://} URI, or a {@code content://} URI.
|
|
360
245
|
*
|
|
361
246
|
* A content URI has no path this process can open, so it is copied into the
|
|
362
|
-
* cache first
|
|
363
|
-
*
|
|
364
|
-
*
|
|
247
|
+
* cache first: PDFium reads a document through its own file access and a
|
|
248
|
+
* document provider is free to hand back a pipe. A host that already has a
|
|
249
|
+
* file path should pass one.
|
|
365
250
|
*/
|
|
366
251
|
private File resolve(String uri) throws PdfRasterException {
|
|
367
252
|
if (uri == null || uri.isEmpty()) {
|
|
@@ -405,216 +290,115 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
405
290
|
|
|
406
291
|
@ReactMethod
|
|
407
292
|
public void render(int handle, ReadableMap request, int token, Promise promise) {
|
|
408
|
-
|
|
409
|
-
if (rasterizer == null) {
|
|
410
|
-
promise.reject(
|
|
411
|
-
PdfErrorCode.BACKEND_FAILURE.wire(), "No open document for handle " + handle + ".");
|
|
293
|
+
if (!requireService(promise)) {
|
|
412
294
|
return;
|
|
413
295
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
// @ReactMethod had returned and its native backing was free to go.
|
|
419
|
-
final RasterRequest req;
|
|
296
|
+
// The whole request is turned into plain values HERE, on the bridge thread.
|
|
297
|
+
final int page;
|
|
298
|
+
final double x, y, width, height, scale;
|
|
299
|
+
final boolean annotations, transparent;
|
|
420
300
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
!request.hasKey("annotations") || request.getBoolean("annotations"),
|
|
430
|
-
"transparent".equals(request.getString("background"))
|
|
431
|
-
? RasterRequest.BACKGROUND_TRANSPARENT
|
|
432
|
-
: RasterRequest.BACKGROUND_WHITE);
|
|
301
|
+
page = request.getInt("page");
|
|
302
|
+
x = request.getDouble("x");
|
|
303
|
+
y = request.getDouble("y");
|
|
304
|
+
width = request.getDouble("width");
|
|
305
|
+
height = request.getDouble("height");
|
|
306
|
+
scale = request.getDouble("scale");
|
|
307
|
+
annotations = !request.hasKey("annotations") || request.getBoolean("annotations");
|
|
308
|
+
transparent = "transparent".equals(request.getString("background"));
|
|
433
309
|
} catch (RuntimeException e) {
|
|
434
|
-
// A malformed request map (missing key, wrong type) now fails here rather
|
|
435
|
-
// than on a worker, which is where it belongs: nothing has been allocated
|
|
436
|
-
// and no signal has been registered yet, so there is nothing to unwind.
|
|
437
310
|
reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
|
|
438
311
|
return;
|
|
439
312
|
}
|
|
440
313
|
|
|
441
|
-
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
// cancel for this key arrives this entry already exists.
|
|
447
|
-
final RasterCancellation signal = new RasterCancellation();
|
|
448
|
-
signals.put(key, signal);
|
|
314
|
+
// Registered BEFORE the worker runs, synchronously on the bridge thread.
|
|
315
|
+
// That ordering is what lets cancel() be a pure lookup: JS cannot cancel a
|
|
316
|
+
// token before the render() that issued it, so by the time any cancel for
|
|
317
|
+
// this token arrives the signal already exists.
|
|
318
|
+
PdfCanvasNative.registerCancellation(service, handle, token);
|
|
449
319
|
|
|
450
320
|
try {
|
|
451
|
-
|
|
321
|
+
worker.execute(
|
|
452
322
|
() -> {
|
|
453
|
-
|
|
323
|
+
long slot = 0L;
|
|
454
324
|
boolean handedOff = false;
|
|
455
325
|
try {
|
|
456
|
-
// Read ONCE.
|
|
457
|
-
//
|
|
458
|
-
// look for one — which is exactly how a filled slot is stranded.
|
|
326
|
+
// Read ONCE. invalidate() clears it, so reading it twice could ask
|
|
327
|
+
// for a slot and then decide not to look for one.
|
|
459
328
|
final boolean fast = jsiEnabled;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
329
|
+
final long[] shape = new long[4];
|
|
330
|
+
final byte[] bytes =
|
|
331
|
+
PdfCanvasNative.render(
|
|
332
|
+
service, handle, page, x, y, width, height, scale, annotations, transparent,
|
|
333
|
+
token, fast, shape);
|
|
334
|
+
slot = shape[0];
|
|
464
335
|
|
|
465
336
|
WritableMap result = Arguments.createMap();
|
|
466
|
-
// ONE of the two, never both — `
|
|
467
|
-
// is present
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// for: renderToNativeSlot falls back to the heap whenever the
|
|
472
|
-
// native store refuses, and that tile still has to go out.
|
|
473
|
-
if (pixels.isInNativeSlot()) {
|
|
474
|
-
// A double, because that is what a WritableMap number is on the
|
|
475
|
-
// way to JS. Slots are consumed within one render, so the 2^53
|
|
476
|
-
// exact-integer ceiling is not reachable in any session that
|
|
477
|
-
// could also survive the file descriptors.
|
|
478
|
-
result.putDouble("pixelsSlot", (double) pixels.slot);
|
|
337
|
+
// ONE of the two, never both — `native-bridge.ts` branches on which
|
|
338
|
+
// key is present. The branch is on WHERE THE PIXELS ARE, not on
|
|
339
|
+
// what was asked for: the store may have refused.
|
|
340
|
+
if (bytes == null) {
|
|
341
|
+
result.putDouble("pixelsSlot", (double) slot);
|
|
479
342
|
} else {
|
|
480
|
-
result.putString("base64", Base64.encodeToString(
|
|
343
|
+
result.putString("base64", Base64.encodeToString(bytes, Base64.NO_WRAP));
|
|
481
344
|
}
|
|
482
|
-
result.putInt("width",
|
|
483
|
-
result.putInt("height",
|
|
484
|
-
// Passed through, never recomputed as width*4 on either side
|
|
485
|
-
|
|
486
|
-
result.
|
|
487
|
-
result.putString("format", pixels.format);
|
|
345
|
+
result.putInt("width", (int) shape[1]);
|
|
346
|
+
result.putInt("height", (int) shape[2]);
|
|
347
|
+
// Passed through, never recomputed as width*4 on either side.
|
|
348
|
+
result.putInt("rowBytes", (int) shape[3]);
|
|
349
|
+
result.putString("format", "rgba8888");
|
|
488
350
|
promise.resolve(result);
|
|
489
|
-
// ONLY NOW
|
|
490
|
-
//
|
|
491
|
-
// frees it and this thread must not.
|
|
351
|
+
// ONLY NOW: the slot id has reached JS, and the ArrayBuffer's take
|
|
352
|
+
// is what frees it from here on.
|
|
492
353
|
handedOff = true;
|
|
493
354
|
} catch (PdfRasterException e) {
|
|
494
355
|
reject(promise, e);
|
|
495
356
|
} catch (Throwable t) {
|
|
496
357
|
reject(promise, PdfErrorCode.BACKEND_FAILURE, t);
|
|
497
358
|
} finally {
|
|
498
|
-
// THE ABANDON
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// 3. NOT the render throwing after it filled the slot: `pixels`
|
|
505
|
-
// is still null on that path, so this cannot release it. The
|
|
506
|
-
// rasterizer owns that one — the post-draw cancellation check
|
|
507
|
-
// releases its own slot before rethrowing.
|
|
508
|
-
//
|
|
509
|
-
// Also NOT covered, and the two cases differ by WHEN the reload
|
|
510
|
-
// lands: a slot id that reaches JS whose continuation never runs
|
|
511
|
-
// is reclaimed by nativeReleaseAll() in invalidate(); a slot
|
|
512
|
-
// published AFTER that nativeReleaseAll — this worker was already
|
|
513
|
-
// mid-draw when the reload started — waits for the NEXT one. See
|
|
514
|
-
// the note at invalidate().
|
|
515
|
-
if (pixels != null && !handedOff) {
|
|
516
|
-
pixels.release();
|
|
359
|
+
// THE ABANDON PATH: a slot was filled and then the result could
|
|
360
|
+
// not be built or the promise could not be settled. Without this
|
|
361
|
+
// a whole raster of native memory sits in the store until the
|
|
362
|
+
// next invalidate.
|
|
363
|
+
if (slot != 0L && !handedOff) {
|
|
364
|
+
PdfCanvasNative.releaseSlot(slot);
|
|
517
365
|
}
|
|
518
|
-
signals.remove(key);
|
|
519
366
|
}
|
|
520
367
|
});
|
|
521
368
|
} catch (RejectedExecutionException e) {
|
|
522
|
-
// invalidate() shut the
|
|
523
|
-
// The task will never run, so its finally will never remove the entry —
|
|
524
|
-
// the same leak the cancel() fix closes, reached from the other side.
|
|
525
|
-
signals.remove(key);
|
|
369
|
+
// invalidate() shut the worker down between the register and here.
|
|
526
370
|
reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
|
|
527
371
|
}
|
|
528
372
|
}
|
|
529
373
|
|
|
530
374
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* UNVERIFIED FIX (3 of 3): a pure LOOKUP. It used to do
|
|
537
|
-
* {@code signals.putIfAbsent(key, cancelled)} when no signal was registered,
|
|
538
|
-
* on the theory that the cancel had beaten its render across the bridge — but
|
|
539
|
-
* "no signal registered" is also, and far more commonly, what a cancel that
|
|
540
|
-
* arrives AFTER its render finished and cleaned up looks like. The two are
|
|
541
|
-
* indistinguishable from here, so every late cancel — and a superseded raster
|
|
542
|
-
* epoch produces a stream of them — planted an entry that nothing ever removed.
|
|
543
|
-
* {@code signals} grew for the life of the module.
|
|
544
|
-
*
|
|
545
|
-
* The speculative branch is not needed anyway: {@link #render} registers its
|
|
546
|
-
* signal synchronously on this same bridge thread before returning, calls from
|
|
547
|
-
* one JS module execute in the order they were made, and a token only exists
|
|
548
|
-
* because a render() issued it. A cancel therefore cannot arrive before the
|
|
549
|
-
* entry it is looking for. If that ordering assumption is ever wrong the cost
|
|
550
|
-
* is one missed cancel on a render that was about to be discarded anyway —
|
|
551
|
-
* which is what "best-effort" already promises — and not unbounded growth.
|
|
375
|
+
* Genuinely mid-render now, not best-effort: the core polls the signal from
|
|
376
|
+
* PDFium's progressive renderer between batches of page objects, so a
|
|
377
|
+
* superseded tile stops drawing within one batch. A pure lookup — a cancel
|
|
378
|
+
* for a token whose render already finished finds nothing and does nothing.
|
|
552
379
|
*/
|
|
553
380
|
@ReactMethod
|
|
554
381
|
public void cancel(int handle, int token) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
signal.cancel();
|
|
382
|
+
if (service != 0L) {
|
|
383
|
+
PdfCanvasNative.cancel(service, handle, token);
|
|
558
384
|
}
|
|
559
385
|
}
|
|
560
386
|
|
|
561
387
|
@ReactMethod
|
|
562
388
|
public void close(int handle) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
rasterizer.close();
|
|
389
|
+
if (service != 0L) {
|
|
390
|
+
PdfCanvasNative.close(service, handle);
|
|
566
391
|
}
|
|
567
392
|
}
|
|
568
393
|
|
|
569
|
-
/** Exposed so a host can warn before showing a rotated page wrongly. */
|
|
570
|
-
@ReactMethod
|
|
571
|
-
public void rotationSupport(Promise promise) {
|
|
572
|
-
promise.resolve(
|
|
573
|
-
PdfCanvasRasterizer.rotationSupport(reactContext.getCacheDir())
|
|
574
|
-
.name()
|
|
575
|
-
.toLowerCase(java.util.Locale.US));
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/** True once this process has hit the API 24 PDFium poisoning. */
|
|
579
|
-
@ReactMethod
|
|
580
|
-
public void isBackendPoisoned(Promise promise) {
|
|
581
|
-
promise.resolve(PdfPlatformQuirks.isBackendPoisoned());
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/*
|
|
585
|
-
* WHY THERE IS NO LONGER A putPixelsSlot().
|
|
586
|
-
*
|
|
587
|
-
* It used to park a finished {@code byte[]} in a slot AFTER the render
|
|
588
|
-
* returned, which is the only place it could: the module had pixels, not a
|
|
589
|
-
* bitmap. That cost a full-raster ART allocation and a full-raster memcpy per
|
|
590
|
-
* tile, both on the render executor — the serialised bottleneck. The slot is
|
|
591
|
-
* now requested BEFORE the raster is drawn (see {@link #render}) so the fill
|
|
592
|
-
* happens once, straight out of the Bitmap.
|
|
593
|
-
*
|
|
594
|
-
* ONE BEHAVIOUR WENT WITH IT, deliberately. That method latched
|
|
595
|
-
* {@code jsiEnabled = false} on any failure, reasoning that a store which threw
|
|
596
|
-
* once will throw again. The new path does not: a refusal is reported as a
|
|
597
|
-
* heap-backed raster that has ALREADY gone out correctly on base64, so there is
|
|
598
|
-
* nothing to salvage by downgrading the whole session. Latching on a transient
|
|
599
|
-
* allocation failure would trade one slow tile for every subsequent tile being
|
|
600
|
-
* slow — and the base64 tile that prompted it costs ~1 s of Hermes on the
|
|
601
|
-
* measured device, which is a cliff worth not falling off twice. A permanent
|
|
602
|
-
* refusal (a format or stride the native side rejects) costs one cheap failed
|
|
603
|
-
* JNI call per tile, not a copy.
|
|
604
|
-
*/
|
|
605
|
-
|
|
606
394
|
/* ---------------------------------------------------------------- */
|
|
607
395
|
|
|
608
|
-
private static long signalKey(int handle, int token) {
|
|
609
|
-
return (((long) handle) << 32) | (token & 0xffffffffL);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
396
|
/**
|
|
613
397
|
* The error CODE is the promise's rejection code, verbatim from
|
|
614
398
|
* {@link PdfErrorCode#wire()} — which is the TypeScript {@code PdfErrorCode}
|
|
615
399
|
* union member. React Native surfaces it as {@code error.code}, and
|
|
616
|
-
* {@code src/rasterizer/
|
|
617
|
-
* with no translation table in between.
|
|
400
|
+
* {@code src/rasterizer/native-bridge.ts} reconstructs a {@code PdfError} from
|
|
401
|
+
* it with no translation table in between.
|
|
618
402
|
*/
|
|
619
403
|
private static void reject(Promise promise, PdfRasterException e) {
|
|
620
404
|
promise.reject(e.code().wire(), e.getMessage(), e);
|