@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.
- package/PdfCanvas.podspec +80 -0
- package/README.md +1391 -0
- package/android/build.gradle +175 -0
- package/android/consumer-rules.pro +3 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +2 -0
- package/android/gradlew +248 -0
- package/android/gradlew.bat +98 -0
- package/android/settings.gradle +34 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
- package/android/src/jsi/cpp/CMakeLists.txt +33 -0
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
- package/android/tools/compile-gate.sh +289 -0
- package/dist/cache.d.ts +161 -0
- package/dist/cache.js +389 -0
- package/dist/clock.d.ts +21 -0
- package/dist/clock.js +31 -0
- package/dist/controller.d.ts +131 -0
- package/dist/controller.js +1002 -0
- package/dist/geometry.d.ts +96 -0
- package/dist/geometry.js +248 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +50 -0
- package/dist/index.native.d.ts +21 -0
- package/dist/index.native.js +23 -0
- package/dist/index.web.d.ts +24 -0
- package/dist/index.web.js +26 -0
- package/dist/layout.d.ts +78 -0
- package/dist/layout.js +152 -0
- package/dist/peer-check.d.ts +83 -0
- package/dist/peer-check.js +183 -0
- package/dist/planner.d.ts +35 -0
- package/dist/planner.js +530 -0
- package/dist/policy.d.ts +14 -0
- package/dist/policy.js +168 -0
- package/dist/rasterizer/android.d.ts +121 -0
- package/dist/rasterizer/android.js +126 -0
- package/dist/rasterizer/buffer.d.ts +18 -0
- package/dist/rasterizer/buffer.js +18 -0
- package/dist/rasterizer/fake.d.ts +92 -0
- package/dist/rasterizer/fake.js +433 -0
- package/dist/rasterizer/index.d.ts +38 -0
- package/dist/rasterizer/index.js +69 -0
- package/dist/rasterizer/index.native.d.ts +19 -0
- package/dist/rasterizer/index.native.js +80 -0
- package/dist/rasterizer/index.web.d.ts +63 -0
- package/dist/rasterizer/index.web.js +75 -0
- package/dist/rasterizer/ingest.d.ts +180 -0
- package/dist/rasterizer/ingest.js +307 -0
- package/dist/rasterizer/ios.d.ts +84 -0
- package/dist/rasterizer/ios.js +155 -0
- package/dist/rasterizer/native-bridge.d.ts +261 -0
- package/dist/rasterizer/native-bridge.js +413 -0
- package/dist/rasterizer/web/client.d.ts +84 -0
- package/dist/rasterizer/web/client.js +359 -0
- package/dist/rasterizer/web/engine.d.ts +69 -0
- package/dist/rasterizer/web/engine.js +308 -0
- package/dist/rasterizer/web/index.d.ts +26 -0
- package/dist/rasterizer/web/index.js +21 -0
- package/dist/rasterizer/web/pdfium.d.ts +213 -0
- package/dist/rasterizer/web/pdfium.js +170 -0
- package/dist/rasterizer/web/protocol.d.ts +138 -0
- package/dist/rasterizer/web/protocol.js +37 -0
- package/dist/rasterizer/web/session.d.ts +106 -0
- package/dist/rasterizer/web/session.js +413 -0
- package/dist/rasterizer/web/worker.d.ts +44 -0
- package/dist/rasterizer/web/worker.js +76 -0
- package/dist/react/PdfContentView.d.ts +98 -0
- package/dist/react/PdfContentView.js +189 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +3 -0
- package/dist/react/usePdfDocument.d.ts +134 -0
- package/dist/react/usePdfDocument.js +275 -0
- package/dist/react/usePdfLayer.d.ts +94 -0
- package/dist/react/usePdfLayer.js +388 -0
- package/dist/skia/index.d.ts +26 -0
- package/dist/skia/index.js +25 -0
- package/dist/skia/index.native.d.ts +14 -0
- package/dist/skia/index.native.js +16 -0
- package/dist/skia/index.web.d.ts +12 -0
- package/dist/skia/index.web.js +14 -0
- package/dist/skia/resolve.native.d.ts +18 -0
- package/dist/skia/resolve.native.js +22 -0
- package/dist/skia/resolve.web.d.ts +41 -0
- package/dist/skia/resolve.web.js +62 -0
- package/dist/skia/skia-api.d.ts +94 -0
- package/dist/skia/skia-api.js +113 -0
- package/dist/testing/index.d.ts +27 -0
- package/dist/testing/index.js +28 -0
- package/dist/testing/scenes.d.ts +100 -0
- package/dist/testing/scenes.js +267 -0
- package/dist/types.d.ts +611 -0
- package/dist/types.js +21 -0
- package/dist/web-init.d.ts +68 -0
- package/dist/web-init.js +64 -0
- package/ios/Package.swift +58 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
- package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
- package/package.json +144 -0
- package/react-native.config.js +40 -0
- package/scripts/check-skia-peer.mjs +86 -0
- package/scripts/skia-peer-check.mjs +363 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
package tools.reekon.pdfcanvas.rn;
|
|
2
|
+
|
|
3
|
+
import android.content.ContentResolver;
|
|
4
|
+
import android.net.Uri;
|
|
5
|
+
import android.os.ParcelFileDescriptor;
|
|
6
|
+
import android.util.Base64;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.bridge.JavaScriptContextHolder;
|
|
9
|
+
import com.facebook.react.bridge.Promise;
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
12
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
13
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
14
|
+
import com.facebook.react.bridge.WritableArray;
|
|
15
|
+
import com.facebook.react.bridge.WritableMap;
|
|
16
|
+
import com.facebook.react.bridge.Arguments;
|
|
17
|
+
|
|
18
|
+
import java.io.File;
|
|
19
|
+
import java.io.FileOutputStream;
|
|
20
|
+
import java.io.IOException;
|
|
21
|
+
import java.io.InputStream;
|
|
22
|
+
import java.util.Map;
|
|
23
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
24
|
+
import java.util.concurrent.ExecutorService;
|
|
25
|
+
import java.util.concurrent.Executors;
|
|
26
|
+
import java.util.concurrent.RejectedExecutionException;
|
|
27
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
28
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
29
|
+
|
|
30
|
+
import tools.reekon.pdfcanvas.jsi.PdfCanvasPixelBridge;
|
|
31
|
+
import tools.reekon.pdfcanvas.PageGeometry;
|
|
32
|
+
import tools.reekon.pdfcanvas.PdfCanvasRasterizer;
|
|
33
|
+
import tools.reekon.pdfcanvas.PdfErrorCode;
|
|
34
|
+
import tools.reekon.pdfcanvas.PdfPlatformQuirks;
|
|
35
|
+
import tools.reekon.pdfcanvas.PdfRasterException;
|
|
36
|
+
import tools.reekon.pdfcanvas.RasterCancellation;
|
|
37
|
+
import tools.reekon.pdfcanvas.RasterPixels;
|
|
38
|
+
import tools.reekon.pdfcanvas.RasterRequest;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* MARSHALLING ONLY.
|
|
42
|
+
*
|
|
43
|
+
* Every line here converts a type, resolves a URI, picks a thread or rejects a
|
|
44
|
+
* promise. Not one of them decides anything about rasterizing — that all lives
|
|
45
|
+
* in {@code tools.reekon.pdfcanvas}, which has no React imports and is proven on
|
|
46
|
+
* device by {@code ./gradlew connectedDebugAndroidTest} with no app in the
|
|
47
|
+
* picture. Keeping the split sharp is the point: this file cannot be tested
|
|
48
|
+
* without building a whole React Native app, so it must not be somewhere
|
|
49
|
+
* behaviour hides.
|
|
50
|
+
*
|
|
51
|
+
* <h3>The transport</h3>
|
|
52
|
+
*
|
|
53
|
+
* TWO OF THEM, chosen at runtime, and the fast one has to earn it.
|
|
54
|
+
*
|
|
55
|
+
* <p>BASE64 is the original and is still the fallback. It is the honest cost of a
|
|
56
|
+
* classic bridge module: neither the legacy bridge nor new-architecture codegen
|
|
57
|
+
* has a binary type. Measured on an LG V60, one 1024x1024 tile costs 19.88 ms to
|
|
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.
|
|
70
|
+
*
|
|
71
|
+
* <p>THE SLOT IS ASKED FOR BEFORE THE RASTER EXISTS, which is what makes that
|
|
72
|
+
* possible: the bitmap is alive only inside the rasterizer, so the decision of
|
|
73
|
+
* where the pixels land has to travel down rather than the pixels travelling up.
|
|
74
|
+
* The consequence is that this file, not the rasterizer, owns every path where a
|
|
75
|
+
* FILLED slot is abandoned — and each one must release it or leak a whole raster
|
|
76
|
+
* of native memory. They are enumerated at {@link #render}.
|
|
77
|
+
*
|
|
78
|
+
* <p>It is selected by {@link #installJsiTransport} + {@link #enableJsiTransport},
|
|
79
|
+
* never assumed: install returns a probe slot, JS takes it and checks the bytes
|
|
80
|
+
* against {@link PdfCanvasPixelBridge#PROBE_BYTES}, and only a JS-side match
|
|
81
|
+
* turns {@link #jsiEnabled} on. A build with no {@code libpdfcanvasjsi.so}, a
|
|
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.
|
|
86
|
+
*
|
|
87
|
+
* <h3>Threading</h3>
|
|
88
|
+
*
|
|
89
|
+
* {@code @ReactMethod} runs on the bridge's native-modules thread, and
|
|
90
|
+
* {@code PdfRenderer.render} is a long synchronous call, so every render is
|
|
91
|
+
* handed to an executor sized to the renderer pool. Rendering on the caller's
|
|
92
|
+
* thread would block every other native module for the length of an ARCH-D
|
|
93
|
+
* raster.
|
|
94
|
+
*
|
|
95
|
+
* EVERY {@code ReadableMap} IS DRAINED ON THE CALLING THREAD, before the
|
|
96
|
+
* executor is handed anything. On the legacy bridge a {@code ReadableNativeMap}
|
|
97
|
+
* is a thin Java shell over C++ memory that is only contractually valid for the
|
|
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.
|
|
111
|
+
*/
|
|
112
|
+
public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
113
|
+
|
|
114
|
+
public static final String NAME = "PdfCanvas";
|
|
115
|
+
|
|
116
|
+
/** Mirrors the native pool ceiling; see PdfRendererPool.MAX_POOL_SIZE. */
|
|
117
|
+
private static final int MAX_POOL_SIZE = 4;
|
|
118
|
+
|
|
119
|
+
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
|
+
/**
|
|
128
|
+
* Slot ids for the JSI transport. Monotonic and deliberately NOT keyed by
|
|
129
|
+
* handle: a slot is written and taken within one render, and reusing ids would
|
|
130
|
+
* let a late take collect a live raster.
|
|
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.
|
|
141
|
+
*/
|
|
142
|
+
private static final AtomicLong nextSlot = new AtomicLong(1);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether renders return a slot instead of a base64 string.
|
|
146
|
+
*
|
|
147
|
+
* Volatile because it is written on the JS thread (by {@link
|
|
148
|
+
* #enableJsiTransport}, and by a render worker that just watched the native
|
|
149
|
+
* store fail) and read on the bridge thread by every {@link #render}.
|
|
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}.
|
|
154
|
+
*/
|
|
155
|
+
private volatile boolean jsiEnabled = false;
|
|
156
|
+
|
|
157
|
+
public PdfCanvasModule(ReactApplicationContext context) {
|
|
158
|
+
super(context);
|
|
159
|
+
this.reactContext = context;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@Override
|
|
163
|
+
public String getName() {
|
|
164
|
+
return NAME;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@Override
|
|
168
|
+
public void invalidate() {
|
|
169
|
+
// FIRST, before the executor is touched, so no render that has not yet
|
|
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.
|
|
189
|
+
jsiEnabled = false;
|
|
190
|
+
// A reload drops the JS side without ever calling close(), so every open
|
|
191
|
+
// document would otherwise keep its file descriptors and parsed pages until
|
|
192
|
+
// the process dies. Fast Refresh makes that happen dozens of times a day.
|
|
193
|
+
for (PdfCanvasRasterizer rasterizer : handles.values()) {
|
|
194
|
+
rasterizer.close();
|
|
195
|
+
}
|
|
196
|
+
handles.clear();
|
|
197
|
+
signals.clear();
|
|
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();
|
|
204
|
+
}
|
|
205
|
+
super.invalidate();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ---------------------------------------------------------------- *
|
|
209
|
+
* transport
|
|
210
|
+
* ---------------------------------------------------------------- */
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Installs the JSI host function and parks one probe raster for JS to take.
|
|
214
|
+
*
|
|
215
|
+
* SYNCHRONOUS on purpose, and that is load-bearing twice over. A blocking
|
|
216
|
+
* synchronous {@code @ReactMethod} executes ON THE JS THREAD, which is the
|
|
217
|
+
* only thread where {@code JavaScriptContextHolder.get()} is a live pointer
|
|
218
|
+
* rather than a value that may already have been torn down. It also means the
|
|
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.
|
|
237
|
+
*
|
|
238
|
+
* @return the probe slot id, or -1 if the fast path is unavailable. Never
|
|
239
|
+
* throws: every failure here is a fallback, not an error.
|
|
240
|
+
*/
|
|
241
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
242
|
+
public double installJsiTransport() {
|
|
243
|
+
try {
|
|
244
|
+
if (!PdfCanvasPixelBridge.isLoaded()) {
|
|
245
|
+
return -1;
|
|
246
|
+
}
|
|
247
|
+
JavaScriptContextHolder holder = getReactApplicationContext().getJavaScriptContextHolder();
|
|
248
|
+
if (holder == null || holder.get() == 0) {
|
|
249
|
+
return -1;
|
|
250
|
+
}
|
|
251
|
+
if (!PdfCanvasPixelBridge.nativeInstall(holder.get())) {
|
|
252
|
+
return -1;
|
|
253
|
+
}
|
|
254
|
+
long slot = nextSlot.getAndIncrement();
|
|
255
|
+
PdfCanvasPixelBridge.nativeStore(slot, PdfCanvasPixelBridge.PROBE_BYTES);
|
|
256
|
+
return (double) slot;
|
|
257
|
+
} catch (Throwable t) {
|
|
258
|
+
return -1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
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
|
+
*/
|
|
272
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
273
|
+
public boolean enableJsiTransport(boolean enabled) {
|
|
274
|
+
jsiEnabled = enabled && PdfCanvasPixelBridge.isLoaded();
|
|
275
|
+
return jsiEnabled;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* ---------------------------------------------------------------- *
|
|
279
|
+
* open
|
|
280
|
+
* ---------------------------------------------------------------- */
|
|
281
|
+
|
|
282
|
+
@ReactMethod
|
|
283
|
+
public void open(ReadableMap source, int poolSize, Promise promise) {
|
|
284
|
+
// UNVERIFIED FIX (1 of 3): drained HERE, on the bridge thread, not inside
|
|
285
|
+
// the executor lambda. `source` is a ReadableNativeMap whose backing native
|
|
286
|
+
// data is only guaranteed valid for the duration of this call.
|
|
287
|
+
final String password = source.hasKey("password") ? source.getString("password") : null;
|
|
288
|
+
final String base64 = source.hasKey("base64") ? source.getString("base64") : null;
|
|
289
|
+
final String uri = source.hasKey("uri") ? source.getString("uri") : null;
|
|
290
|
+
final int clamped = Math.max(1, Math.min(MAX_POOL_SIZE, poolSize));
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
renderExecutor.execute(
|
|
294
|
+
() -> {
|
|
295
|
+
PdfCanvasRasterizer rasterizer = null;
|
|
296
|
+
boolean published = false;
|
|
297
|
+
try {
|
|
298
|
+
if (base64 != null) {
|
|
299
|
+
byte[] data = Base64.decode(base64, Base64.DEFAULT);
|
|
300
|
+
rasterizer =
|
|
301
|
+
PdfCanvasRasterizer.openBytes(
|
|
302
|
+
reactContext.getCacheDir(), data, password, clamped);
|
|
303
|
+
} else {
|
|
304
|
+
rasterizer = PdfCanvasRasterizer.openFile(resolve(uri), password, clamped);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// UNVERIFIED FIX (2 of 3): everything that can still throw happens
|
|
308
|
+
// BEFORE the rasterizer is published to `handles` and before the
|
|
309
|
+
// promise is resolved. It used to be registered immediately after
|
|
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".
|
|
315
|
+
WritableArray pages = Arguments.createArray();
|
|
316
|
+
for (int i = 0; i < rasterizer.pageCount(); i++) {
|
|
317
|
+
PageGeometry g = rasterizer.pageGeometry(i);
|
|
318
|
+
WritableMap page = Arguments.createMap();
|
|
319
|
+
page.putInt("index", g.index);
|
|
320
|
+
page.putInt("width", g.widthPoints);
|
|
321
|
+
page.putInt("height", g.heightPoints);
|
|
322
|
+
page.putInt("rotation", g.rotationDegrees);
|
|
323
|
+
pages.pushMap(page);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
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
|
+
result.putInt("handle", handle);
|
|
337
|
+
handles.put(handle, rasterizer);
|
|
338
|
+
published = true;
|
|
339
|
+
promise.resolve(result);
|
|
340
|
+
} catch (PdfRasterException e) {
|
|
341
|
+
reject(promise, e);
|
|
342
|
+
} catch (Throwable t) {
|
|
343
|
+
reject(promise, PdfErrorCode.BACKEND_FAILURE, t);
|
|
344
|
+
} finally {
|
|
345
|
+
if (rasterizer != null && !published) {
|
|
346
|
+
rasterizer.close();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
} catch (RejectedExecutionException e) {
|
|
351
|
+
// invalidate() shut the executor down. Without this the task never runs
|
|
352
|
+
// and the promise never settles, which on the JS side is an `await` that
|
|
353
|
+
// hangs for the life of the app rather than an error anyone can see.
|
|
354
|
+
reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Accepts a plain path, a {@code file://} URI, or a {@code content://} URI.
|
|
360
|
+
*
|
|
361
|
+
* A content URI has no path this process can open, so it is copied into the
|
|
362
|
+
* cache first. That copy is deliberate and unavoidable: {@code PdfRenderer}
|
|
363
|
+
* needs a SEEKABLE descriptor, and a document provider is free to hand back a
|
|
364
|
+
* pipe. It is also why a host that already has a file path should pass one.
|
|
365
|
+
*/
|
|
366
|
+
private File resolve(String uri) throws PdfRasterException {
|
|
367
|
+
if (uri == null || uri.isEmpty()) {
|
|
368
|
+
throw new PdfRasterException(PdfErrorCode.NOT_FOUND, "No document uri was given.");
|
|
369
|
+
}
|
|
370
|
+
if (uri.startsWith("content://")) {
|
|
371
|
+
return copyContentUri(Uri.parse(uri));
|
|
372
|
+
}
|
|
373
|
+
if (uri.startsWith("file://")) {
|
|
374
|
+
return new File(Uri.parse(uri).getPath());
|
|
375
|
+
}
|
|
376
|
+
return new File(uri);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private File copyContentUri(Uri uri) throws PdfRasterException {
|
|
380
|
+
ContentResolver resolver = reactContext.getContentResolver();
|
|
381
|
+
File out;
|
|
382
|
+
try {
|
|
383
|
+
out = File.createTempFile("pdfcanvas-content-", ".pdf", reactContext.getCacheDir());
|
|
384
|
+
try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
|
|
385
|
+
InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
|
|
386
|
+
FileOutputStream fos = new FileOutputStream(out)) {
|
|
387
|
+
byte[] buffer = new byte[64 * 1024];
|
|
388
|
+
int n;
|
|
389
|
+
while ((n = in.read(buffer)) > 0) {
|
|
390
|
+
fos.write(buffer, 0, n);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (SecurityException e) {
|
|
394
|
+
throw new PdfRasterException(
|
|
395
|
+
PdfErrorCode.NOT_FOUND, "No permission to read " + uri + ".", e);
|
|
396
|
+
} catch (IOException e) {
|
|
397
|
+
throw new PdfRasterException(PdfErrorCode.NOT_FOUND, "Could not read " + uri + ".", e);
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ---------------------------------------------------------------- *
|
|
403
|
+
* render
|
|
404
|
+
* ---------------------------------------------------------------- */
|
|
405
|
+
|
|
406
|
+
@ReactMethod
|
|
407
|
+
public void render(int handle, ReadableMap request, int token, Promise promise) {
|
|
408
|
+
PdfCanvasRasterizer rasterizer = handles.get(handle);
|
|
409
|
+
if (rasterizer == null) {
|
|
410
|
+
promise.reject(
|
|
411
|
+
PdfErrorCode.BACKEND_FAILURE.wire(), "No open document for handle " + handle + ".");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// UNVERIFIED FIX (1 of 3, second half): the whole request is turned into a
|
|
416
|
+
// plain Java object HERE, on the bridge thread. Reading `request` inside the
|
|
417
|
+
// executor lambda meant touching a ReadableNativeMap after the
|
|
418
|
+
// @ReactMethod had returned and its native backing was free to go.
|
|
419
|
+
final RasterRequest req;
|
|
420
|
+
try {
|
|
421
|
+
req =
|
|
422
|
+
new RasterRequest(
|
|
423
|
+
request.getInt("page"),
|
|
424
|
+
(float) request.getDouble("x"),
|
|
425
|
+
(float) request.getDouble("y"),
|
|
426
|
+
(float) request.getDouble("width"),
|
|
427
|
+
(float) request.getDouble("height"),
|
|
428
|
+
(float) request.getDouble("scale"),
|
|
429
|
+
!request.hasKey("annotations") || request.getBoolean("annotations"),
|
|
430
|
+
"transparent".equals(request.getString("background"))
|
|
431
|
+
? RasterRequest.BACKGROUND_TRANSPARENT
|
|
432
|
+
: RasterRequest.BACKGROUND_WHITE);
|
|
433
|
+
} 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
|
+
reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
long key = signalKey(handle, token);
|
|
442
|
+
// Registered BEFORE the executor runs, and synchronously on the bridge
|
|
443
|
+
// thread. That ordering is what lets cancel() be a pure lookup: bridge calls
|
|
444
|
+
// for one module are executed in the order JS made them, and JS cannot
|
|
445
|
+
// cancel a token before the render() that issued it, so by the time any
|
|
446
|
+
// cancel for this key arrives this entry already exists.
|
|
447
|
+
final RasterCancellation signal = new RasterCancellation();
|
|
448
|
+
signals.put(key, signal);
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
renderExecutor.execute(
|
|
452
|
+
() -> {
|
|
453
|
+
RasterPixels pixels = null;
|
|
454
|
+
boolean handedOff = false;
|
|
455
|
+
try {
|
|
456
|
+
// Read ONCE. `jsiEnabled` is volatile and invalidate() clears it,
|
|
457
|
+
// so reading it twice could ask for a slot and then decide not to
|
|
458
|
+
// look for one — which is exactly how a filled slot is stranded.
|
|
459
|
+
final boolean fast = jsiEnabled;
|
|
460
|
+
pixels =
|
|
461
|
+
fast
|
|
462
|
+
? rasterizer.renderToNativeSlot(req, signal, nextSlot.getAndIncrement())
|
|
463
|
+
: rasterizer.render(req, signal);
|
|
464
|
+
|
|
465
|
+
WritableMap result = Arguments.createMap();
|
|
466
|
+
// ONE of the two, never both — `android.ts` branches on which key
|
|
467
|
+
// is present, and sending both would mean paying for the encode
|
|
468
|
+
// this whole path exists to delete.
|
|
469
|
+
//
|
|
470
|
+
// The branch is on WHERE THE PIXELS ARE, not on what was asked
|
|
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);
|
|
479
|
+
} else {
|
|
480
|
+
result.putString("base64", Base64.encodeToString(pixels.bytes, Base64.NO_WRAP));
|
|
481
|
+
}
|
|
482
|
+
result.putInt("width", pixels.width);
|
|
483
|
+
result.putInt("height", pixels.height);
|
|
484
|
+
// Passed through, never recomputed as width*4 on either side of
|
|
485
|
+
// the bridge. A wrong stride shears the image rather than failing.
|
|
486
|
+
result.putInt("rowBytes", pixels.rowBytes);
|
|
487
|
+
result.putString("format", pixels.format);
|
|
488
|
+
promise.resolve(result);
|
|
489
|
+
// ONLY NOW. Everything that could throw is behind us and the slot
|
|
490
|
+
// id has reached JS, so from here the ArrayBuffer's take is what
|
|
491
|
+
// frees it and this thread must not.
|
|
492
|
+
handedOff = true;
|
|
493
|
+
} catch (PdfRasterException e) {
|
|
494
|
+
reject(promise, e);
|
|
495
|
+
} catch (Throwable t) {
|
|
496
|
+
reject(promise, PdfErrorCode.BACKEND_FAILURE, t);
|
|
497
|
+
} finally {
|
|
498
|
+
// THE ABANDON PATHS, all three of them, and each one leaks a whole
|
|
499
|
+
// raster of native memory without this:
|
|
500
|
+
//
|
|
501
|
+
// 1. Arguments.createMap() or a put* throws (OOM, or a bridge
|
|
502
|
+
// torn down under us) after the slot was filled.
|
|
503
|
+
// 2. promise.resolve throws, or the promise was already settled.
|
|
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();
|
|
517
|
+
}
|
|
518
|
+
signals.remove(key);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
} catch (RejectedExecutionException e) {
|
|
522
|
+
// invalidate() shut the executor down between the handle lookup and here.
|
|
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);
|
|
526
|
+
reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Best-effort, and the JS side is told so via
|
|
532
|
+
* {@code capabilities.interruptibleRender: false}. This can stop a queued
|
|
533
|
+
* render starting and can discard a finished one; it cannot interrupt
|
|
534
|
+
* {@code PdfRenderer.render}.
|
|
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.
|
|
552
|
+
*/
|
|
553
|
+
@ReactMethod
|
|
554
|
+
public void cancel(int handle, int token) {
|
|
555
|
+
RasterCancellation signal = signals.get(signalKey(handle, token));
|
|
556
|
+
if (signal != null) {
|
|
557
|
+
signal.cancel();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
@ReactMethod
|
|
562
|
+
public void close(int handle) {
|
|
563
|
+
PdfCanvasRasterizer rasterizer = handles.remove(handle);
|
|
564
|
+
if (rasterizer != null) {
|
|
565
|
+
rasterizer.close();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
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
|
+
/* ---------------------------------------------------------------- */
|
|
607
|
+
|
|
608
|
+
private static long signalKey(int handle, int token) {
|
|
609
|
+
return (((long) handle) << 32) | (token & 0xffffffffL);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* The error CODE is the promise's rejection code, verbatim from
|
|
614
|
+
* {@link PdfErrorCode#wire()} — which is the TypeScript {@code PdfErrorCode}
|
|
615
|
+
* union member. React Native surfaces it as {@code error.code}, and
|
|
616
|
+
* {@code src/rasterizer/android.ts} reconstructs a {@code PdfError} from it
|
|
617
|
+
* with no translation table in between.
|
|
618
|
+
*/
|
|
619
|
+
private static void reject(Promise promise, PdfRasterException e) {
|
|
620
|
+
promise.reject(e.code().wire(), e.getMessage(), e);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private static void reject(Promise promise, PdfErrorCode code, Throwable t) {
|
|
624
|
+
promise.reject(code.wire(), String.valueOf(t.getMessage()), t);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package tools.reekon.pdfcanvas.rn;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage;
|
|
4
|
+
import com.facebook.react.bridge.NativeModule;
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
7
|
+
|
|
8
|
+
import java.util.ArrayList;
|
|
9
|
+
import java.util.Collections;
|
|
10
|
+
import java.util.List;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Autolinking entry point.
|
|
14
|
+
*
|
|
15
|
+
* No view managers: the package owns no canvas. Pages come back as pixels and
|
|
16
|
+
* the HOST draws them inside its own Skia scene — that is the whole premise of
|
|
17
|
+
* `@reekon-tools/react-native-pdf-canvas`, and a native view here would quietly
|
|
18
|
+
* contradict it.
|
|
19
|
+
*/
|
|
20
|
+
public class PdfCanvasPackage implements ReactPackage {
|
|
21
|
+
|
|
22
|
+
@Override
|
|
23
|
+
public List<NativeModule> createNativeModules(ReactApplicationContext context) {
|
|
24
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
25
|
+
modules.add(new PdfCanvasModule(context));
|
|
26
|
+
return modules;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Override
|
|
30
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext context) {
|
|
31
|
+
return Collections.emptyList();
|
|
32
|
+
}
|
|
33
|
+
}
|