@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,743 @@
|
|
|
1
|
+
#import "PdfCanvasModule.h"
|
|
2
|
+
|
|
3
|
+
// `<React/RCTBridgeModule.h>` only FORWARD-DECLARES `RCTBridge`, so the category
|
|
4
|
+
// below and every `self.bridge` message need the real header. Compiling without it
|
|
5
|
+
// fails as "cannot define category for undefined class", which reads like a missing
|
|
6
|
+
// pod dependency rather than a missing import.
|
|
7
|
+
#import <React/RCTBridge.h>
|
|
8
|
+
#import <React/RCTLog.h>
|
|
9
|
+
// `ObjCInteropTurboModule` — a PUBLIC pod header, exposed by React-NativeModulesApple
|
|
10
|
+
// as `ReactCommon/RCTInteropTurboModule.h`. See the note above `getTurboModule:` for
|
|
11
|
+
// why this module builds the interop wrapper itself instead of letting the manager do
|
|
12
|
+
// it.
|
|
13
|
+
#import <ReactCommon/RCTInteropTurboModule.h>
|
|
14
|
+
#import <ReactCommon/RCTTurboModule.h>
|
|
15
|
+
|
|
16
|
+
#import <atomic>
|
|
17
|
+
|
|
18
|
+
#import <PdfCanvasCancellation.h>
|
|
19
|
+
#import <PdfCanvasDocumentPool.h>
|
|
20
|
+
#import <PdfCanvasErrorCode.h>
|
|
21
|
+
#import <PdfCanvasPageGeometry.h>
|
|
22
|
+
#import <PdfCanvasRasterPixels.h>
|
|
23
|
+
#import <PdfCanvasRasterRequest.h>
|
|
24
|
+
#import <PdfCanvasRasterizer.h>
|
|
25
|
+
|
|
26
|
+
#import <PdfCanvasSlots.h>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ------------------------------------------------------------------------
|
|
30
|
+
* HOW THIS MODULE GETS A `jsi::Runtime`, AND THE TWO ROUTES THAT DO NOT WORK
|
|
31
|
+
* ------------------------------------------------------------------------
|
|
32
|
+
*
|
|
33
|
+
* The JSI transport needs a runtime to install a host function into. There are four
|
|
34
|
+
* plausible ways to get one and only two of them work, so both the choice and the
|
|
35
|
+
* rejects are recorded here — each reject was tried on a real device.
|
|
36
|
+
*
|
|
37
|
+
* 1. `installJSIBindingsWithRuntime:callInvoker:` — WHAT THIS FILE USES. React
|
|
38
|
+
* Native calls it with a live `jsi::Runtime &` on the JS thread, at module
|
|
39
|
+
* construction, BEFORE JS can call any method on the module. That last part is
|
|
40
|
+
* what makes it better than anything the JS side could trigger: the host function
|
|
41
|
+
* exists before the first `installJsiTransport()` can look for it, so no render
|
|
42
|
+
* can race the decision. It is the same hook `RCTBlobManager` uses.
|
|
43
|
+
*
|
|
44
|
+
* THE CATCH, and it is why `getTurboModule:` is implemented below:
|
|
45
|
+
* `RCTTurboModuleManager` only calls it for a module that responds to
|
|
46
|
+
* `getTurboModule:` (`provideTurboModule:`, around line 408 of
|
|
47
|
+
* RCTTurboModuleManager.mm). A plain `RCT_EXPORT_MODULE` class goes down
|
|
48
|
+
* `provideLegacyModule:` instead and is never asked. So this module returns an
|
|
49
|
+
* `ObjCInteropTurboModule` — exactly what the interop layer would have built for
|
|
50
|
+
* it — which changes nothing about how its methods are marshalled and everything
|
|
51
|
+
* about whether it is offered the runtime.
|
|
52
|
+
*
|
|
53
|
+
* 2. `self.bridge.runtime` — the FALLBACK, for the old architecture, where
|
|
54
|
+
* `getTurboModule:` is never called and `RCTCxxBridge` really does answer.
|
|
55
|
+
*
|
|
56
|
+
* IT DOES NOT WORK UNDER BRIDGELESS, MEASURED. `RCTInstance` does create an
|
|
57
|
+
* `RCTBridgeProxy` with `runtime:_reactInstance->getJavaScriptContext()` and
|
|
58
|
+
* `RCTTurboModuleManager` does assign it to a module's `bridge` property — but
|
|
59
|
+
* `RCTBridgeProxy` is an `NSProxy` that overrides `methodSignatureForSelector:`
|
|
60
|
+
* and `forwardInvocation:` and does NOT override `respondsToSelector:`, so a
|
|
61
|
+
* `respondsToSelector:@selector(runtime)` guard answers NO for a method the proxy
|
|
62
|
+
* genuinely implements. On device this produced exactly one line —
|
|
63
|
+
* "[PdfCanvas] No bridge runtime is reachable" — and a session that rendered
|
|
64
|
+
* every tile through base64. Dropping the guard and messaging it anyway would
|
|
65
|
+
* work, and is not what this does: a bare message to an `NSProxy` whose
|
|
66
|
+
* `forwardInvocation:` logs an error and returns garbage is not a thing to do on
|
|
67
|
+
* the strength of a reading of one version's source.
|
|
68
|
+
*
|
|
69
|
+
* (`@shopify/react-native-skia`'s `SkiaManager.mm` uses this route with an
|
|
70
|
+
* `if (bridge.runtime)` guard. That is not evidence it works under bridgeless —
|
|
71
|
+
* on the new architecture skia installs through its own codegen'd spec and the
|
|
72
|
+
* bridge path is its legacy branch.)
|
|
73
|
+
*
|
|
74
|
+
* 3. `RCTCallInvokerModule` + `callInvoker->invokeSync` — REJECTED, and it would
|
|
75
|
+
* DEADLOCK. `RuntimeSchedulerCallInvoker::invokeSync` calls
|
|
76
|
+
* `RuntimeScheduler_Modern::executeNowOnTheSameThread`, whose re-entrancy guard is
|
|
77
|
+
* a `thread_local` pointer set only INSIDE itself. A blocking synchronous native
|
|
78
|
+
* method runs on the JS thread but not inside that function, so the guard is null
|
|
79
|
+
* and it falls through to `executeSynchronouslyOnSameThread_CAN_DEADLOCK`, which
|
|
80
|
+
* schedules onto the JS thread and waits — while being the JS thread.
|
|
81
|
+
*
|
|
82
|
+
* 4. `callInvoker->invokeAsync` at construction — REJECTED on ordering. It would be
|
|
83
|
+
* safe, but the lambda runs after the current JS turn, and
|
|
84
|
+
* `NativeModules.PdfCanvas` is touched and `installJsiTransport()` called within
|
|
85
|
+
* one turn. The probe would find nothing and every session would fall back.
|
|
86
|
+
*/
|
|
87
|
+
@interface RCTBridge (PdfCanvasRuntime)
|
|
88
|
+
- (void *)runtime;
|
|
89
|
+
@end
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Mirrors `kPdfCanvasMaxPoolSize`, and is the width of the render queue.
|
|
93
|
+
*
|
|
94
|
+
* The queue is sized to the ceiling rather than to a document's pool because one
|
|
95
|
+
* module serves every open document. A render whose document has fewer lanes blocks
|
|
96
|
+
* on that document's own semaphore, which is where the real limit lives.
|
|
97
|
+
*/
|
|
98
|
+
static const NSInteger kPdfCanvasQueueWidth = 4;
|
|
99
|
+
|
|
100
|
+
@implementation PdfCanvasModule {
|
|
101
|
+
/** JS handle -> rasterizer. Read on the queue, written on the bridge thread. */
|
|
102
|
+
NSMutableDictionary<NSNumber *, PdfCanvasRasterizer *> *_handles;
|
|
103
|
+
/** (handle, token) -> signal, so a token is only meaningful with its handle. */
|
|
104
|
+
NSMutableDictionary<NSNumber *, PdfCanvasCancellation *> *_signals;
|
|
105
|
+
/** Guards both dictionaries. */
|
|
106
|
+
NSLock *_lock;
|
|
107
|
+
NSOperationQueue *_renderQueue;
|
|
108
|
+
NSInteger _nextHandle;
|
|
109
|
+
/**
|
|
110
|
+
* Whether renders return a slot instead of a base64 string.
|
|
111
|
+
*
|
|
112
|
+
* FALSE UNTIL PROVEN. It is never set by this class deciding the fast path looks
|
|
113
|
+
* available; it is set by JS after a byte-for-byte round trip of
|
|
114
|
+
* `PdfCanvasSlotsProbeBytes`. Atomic because it is written on the JS thread (by
|
|
115
|
+
* `enableJsiTransport`, and by `invalidate`) and read on a render worker.
|
|
116
|
+
*/
|
|
117
|
+
std::atomic<bool> _jsiEnabled;
|
|
118
|
+
std::atomic<bool> _invalidated;
|
|
119
|
+
/**
|
|
120
|
+
* Set once by `installJSIBindingsWithRuntime:callInvoker:`, which is the only
|
|
121
|
+
* thing that ever sees a live runtime on the new architecture.
|
|
122
|
+
*
|
|
123
|
+
* A flag rather than the pointer: the host function is installed there and then,
|
|
124
|
+
* so all `installJsiTransport` needs to know is whether that already happened.
|
|
125
|
+
* Keeping a `jsi::Runtime *` around would be a pointer with no owner and no way to
|
|
126
|
+
* learn it had been torn down.
|
|
127
|
+
*/
|
|
128
|
+
BOOL _hostFunctionInstalled;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@synthesize bridge = _bridge;
|
|
132
|
+
|
|
133
|
+
RCT_EXPORT_MODULE(PdfCanvas)
|
|
134
|
+
|
|
135
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
136
|
+
// Nothing here touches UIKit at construction. Returning YES would move module
|
|
137
|
+
// setup to the main thread for no reason and log a warning about it.
|
|
138
|
+
return NO;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
- (instancetype)init {
|
|
142
|
+
self = [super init];
|
|
143
|
+
if (self != nil) {
|
|
144
|
+
_handles = [NSMutableDictionary dictionary];
|
|
145
|
+
_signals = [NSMutableDictionary dictionary];
|
|
146
|
+
_lock = [[NSLock alloc] init];
|
|
147
|
+
_nextHandle = 1;
|
|
148
|
+
_jsiEnabled = false;
|
|
149
|
+
_invalidated = false;
|
|
150
|
+
_renderQueue = [[NSOperationQueue alloc] init];
|
|
151
|
+
// WIDTH-LIMITED, mirroring Android's `Executors.newFixedThreadPool`. An
|
|
152
|
+
// unbounded concurrent `dispatch_queue` would spawn a thread per queued render
|
|
153
|
+
// — and every render beyond a document's pool size does nothing but block on
|
|
154
|
+
// that pool's semaphore, so those threads would be pure cost.
|
|
155
|
+
_renderQueue.maxConcurrentOperationCount = kPdfCanvasQueueWidth;
|
|
156
|
+
_renderQueue.name = @"tools.reekon.pdfcanvas.render";
|
|
157
|
+
_renderQueue.qualityOfService = NSQualityOfServiceUserInitiated;
|
|
158
|
+
}
|
|
159
|
+
return self;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A reload drops the JS side without ever calling `close`, so every open document
|
|
164
|
+
* would otherwise keep its parsed pages until the process dies. Fast Refresh makes
|
|
165
|
+
* that happen dozens of times a day.
|
|
166
|
+
*/
|
|
167
|
+
- (void)invalidate {
|
|
168
|
+
// FIRST, before anything else, so no render that has not yet STARTED can ask for
|
|
169
|
+
// a slot.
|
|
170
|
+
//
|
|
171
|
+
// IT DOES NOT DIVERT A RENDER ALREADY IN FLIGHT, and that is the same bounded
|
|
172
|
+
// leak Android documents: a worker already inside `CGContextDrawPDFPage` holds a
|
|
173
|
+
// sink, and it will publish its raster after the `PdfCanvasSlotsReleaseAll`
|
|
174
|
+
// below has run past. Nothing takes it, and it is freed by the NEXT invalidate.
|
|
175
|
+
// At most one raster per busy lane, dev-only, reclaimed on the following reload.
|
|
176
|
+
_jsiEnabled = false;
|
|
177
|
+
_invalidated = true;
|
|
178
|
+
|
|
179
|
+
[_lock lock];
|
|
180
|
+
NSArray<PdfCanvasRasterizer *> *open = _handles.allValues;
|
|
181
|
+
[_handles removeAllObjects];
|
|
182
|
+
[_signals removeAllObjects];
|
|
183
|
+
[_lock unlock];
|
|
184
|
+
|
|
185
|
+
for (PdfCanvasRasterizer *rasterizer in open) {
|
|
186
|
+
// Non-blocking even with a render in flight — a lease owns a CoreFoundation
|
|
187
|
+
// reference to its document, so this cannot free one out from under a worker.
|
|
188
|
+
// See `PdfCanvasDocumentPool`.
|
|
189
|
+
[rasterizer close];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// THE QUEUE IS DELIBERATELY NOT CANCELLED. `cancelAllOperations` looks like the
|
|
193
|
+
// tidy thing to do and it strands promises: an `NSOperation` cancelled before it
|
|
194
|
+
// starts never runs its block, so the `resolve`/`reject` pair inside it is never
|
|
195
|
+
// called, and on the JS side that is an `await` that hangs for the life of the app
|
|
196
|
+
// rather than an error anyone can see.
|
|
197
|
+
//
|
|
198
|
+
// Letting the queued operations run costs nothing and settles every one of them.
|
|
199
|
+
// Each holds a strong reference to its rasterizer, which the loop above has just
|
|
200
|
+
// closed, so it fails fast with `backend-failure` — and a render that has already
|
|
201
|
+
// STARTED was never cancellable anyway, because `CGContextDrawPDFPage` is
|
|
202
|
+
// uninterruptible once entered. (Android reaches the same conclusion from the other
|
|
203
|
+
// side: its binding catches `RejectedExecutionException` for tasks submitted after
|
|
204
|
+
// `shutdownNow`, precisely so those promises settle.)
|
|
205
|
+
|
|
206
|
+
// A reload also strands any slot whose promise resolved but whose JS
|
|
207
|
+
// continuation never ran. Each one is a whole raster — megabytes — held in C++
|
|
208
|
+
// with nothing left that could ever take it.
|
|
209
|
+
PdfCanvasSlotsReleaseAll();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ---------------------------------------------------------------- *
|
|
213
|
+
* transport
|
|
214
|
+
* ---------------------------------------------------------------- */
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Installs the JSI host function and parks one probe raster for JS to take.
|
|
218
|
+
*
|
|
219
|
+
* SYNCHRONOUS on purpose, and that is load-bearing twice over. MEASURED by reading
|
|
220
|
+
* RN 0.86.2's own source: `ObjCTurboModule::performMethodInvocation` runs a sync
|
|
221
|
+
* method through `nativeMethodCallInvoker_->invokeSync`, and
|
|
222
|
+
* `ModuleNativeMethodCallInvoker::invokeSync` is `work()` — called inline, on the
|
|
223
|
+
* calling thread, which for a JS-initiated call is the JS thread. That is the only
|
|
224
|
+
* thread where `bridge.runtime` is a live pointer rather than a value that may
|
|
225
|
+
* already have been torn down. It also means the whole probe — install, take,
|
|
226
|
+
* enable — completes before the JS module that called it finishes evaluating, so no
|
|
227
|
+
* render can race the decision.
|
|
228
|
+
*
|
|
229
|
+
* IT RETURNS AN OBJECT, AND MUST. Same measurement: for a non-void, non-promise
|
|
230
|
+
* return the interop layer does `void *rawResult; [inv getReturnValue:&rawResult];
|
|
231
|
+
* result = (__bridge id)rawResult;`. A method declared to return `double` or `BOOL`
|
|
232
|
+
* would have its bits reinterpreted as an object pointer. `RCT_EXPORT_BLOCKING_
|
|
233
|
+
* SYNCHRONOUS_METHOD` defaults the return type to `id` for exactly this reason;
|
|
234
|
+
* the `NSNumber` below is that default honoured rather than fought.
|
|
235
|
+
*
|
|
236
|
+
* @return the probe slot id, or -1 if the fast path is unavailable. Never throws:
|
|
237
|
+
* every failure here is a fallback, not an error.
|
|
238
|
+
*/
|
|
239
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(installJsiTransport) {
|
|
240
|
+
@try {
|
|
241
|
+
if (_invalidated) {
|
|
242
|
+
return @(-1);
|
|
243
|
+
}
|
|
244
|
+
// NORMALLY ALREADY DONE. On the new architecture
|
|
245
|
+
// `installJSIBindingsWithRuntime:callInvoker:` ran at construction, so all this
|
|
246
|
+
// method has to do is park a probe raster. The branch below is the old
|
|
247
|
+
// architecture, where that hook is never called and `RCTCxxBridge` genuinely
|
|
248
|
+
// does answer `-runtime`.
|
|
249
|
+
if (!_hostFunctionInstalled) {
|
|
250
|
+
RCTBridge *bridge = self.bridge;
|
|
251
|
+
void *runtime =
|
|
252
|
+
[bridge respondsToSelector:@selector(runtime)] ? bridge.runtime : NULL;
|
|
253
|
+
if (runtime == NULL) {
|
|
254
|
+
RCTLogWarn(
|
|
255
|
+
@"[PdfCanvas] No JS runtime was reachable — neither "
|
|
256
|
+
@"installJSIBindingsWithRuntime: nor bridge.runtime — so every tile "
|
|
257
|
+
@"will cross the bridge as base64. That is correct but roughly 85x "
|
|
258
|
+
@"slower per tile. bridge=%@",
|
|
259
|
+
bridge == nil ? @"nil" : NSStringFromClass([bridge class]));
|
|
260
|
+
return @(-1);
|
|
261
|
+
}
|
|
262
|
+
_hostFunctionInstalled = PdfCanvasSlotsInstall(runtime);
|
|
263
|
+
if (!_hostFunctionInstalled) {
|
|
264
|
+
return @(-1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return @(PdfCanvasSlotsStoreProbe());
|
|
268
|
+
} @catch (NSException *exception) {
|
|
269
|
+
// Includes a `respondsToSelector:` on an `NSProxy` that declines to answer it.
|
|
270
|
+
// Every failure here is a fallback, not an error.
|
|
271
|
+
return @(-1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Turns the fast transport on, once JS has verified the probe bytes.
|
|
277
|
+
*
|
|
278
|
+
* Synchronous for the same reason as `installJsiTransport`: an async call would be
|
|
279
|
+
* ordered before any render, but "ordered before" is a property of the bridge queue
|
|
280
|
+
* and this decision is too cheap to make it depend on one.
|
|
281
|
+
*
|
|
282
|
+
* @return the flag as it now stands, so the caller reports what is really live
|
|
283
|
+
* rather than what it asked for.
|
|
284
|
+
*/
|
|
285
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(enableJsiTransport:(BOOL)enabled) {
|
|
286
|
+
const bool live = enabled && !_invalidated;
|
|
287
|
+
_jsiEnabled = live;
|
|
288
|
+
return @(live);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ---------------------------------------------------------------- *
|
|
292
|
+
* open
|
|
293
|
+
* ---------------------------------------------------------------- */
|
|
294
|
+
|
|
295
|
+
RCT_EXPORT_METHOD(open
|
|
296
|
+
: (NSDictionary *)source poolSize
|
|
297
|
+
: (double)poolSize resolve
|
|
298
|
+
: (RCTPromiseResolveBlock)resolve reject
|
|
299
|
+
: (RCTPromiseRejectBlock)reject) {
|
|
300
|
+
// DRAINED HERE, on the calling thread, before the queue is handed anything. On
|
|
301
|
+
// iOS a `NSDictionary` from the bridge is a real Foundation object rather than a
|
|
302
|
+
// shell over C++ memory — unlike Android's `ReadableNativeMap`, which is only
|
|
303
|
+
// contractually valid for the duration of the method — so this is defensive
|
|
304
|
+
// rather than required. It is still the right shape: it keeps the two bindings
|
|
305
|
+
// reading the same way, and it means a malformed request fails before a worker
|
|
306
|
+
// has allocated anything.
|
|
307
|
+
NSString *uri = [source[@"uri"] isKindOfClass:NSString.class] ? source[@"uri"] : nil;
|
|
308
|
+
NSString *base64 =
|
|
309
|
+
[source[@"base64"] isKindOfClass:NSString.class] ? source[@"base64"] : nil;
|
|
310
|
+
NSString *password =
|
|
311
|
+
[source[@"password"] isKindOfClass:NSString.class] ? source[@"password"] : nil;
|
|
312
|
+
const NSInteger lanes = [self clampPoolSize:poolSize];
|
|
313
|
+
|
|
314
|
+
if (_invalidated) {
|
|
315
|
+
[self reject:reject code:PdfCanvasErrorCodeBackendFailure
|
|
316
|
+
message:@"The PdfCanvas module has been invalidated."
|
|
317
|
+
underlying:nil];
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
__weak PdfCanvasModule *weakSelf = self;
|
|
322
|
+
[_renderQueue addOperationWithBlock:^{
|
|
323
|
+
PdfCanvasModule *strongSelf = weakSelf;
|
|
324
|
+
if (strongSelf == nil) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
NSError *error = nil;
|
|
328
|
+
PdfCanvasRasterizer *rasterizer = nil;
|
|
329
|
+
if (base64 != nil) {
|
|
330
|
+
NSData *data = [[NSData alloc] initWithBase64EncodedString:base64
|
|
331
|
+
options:0];
|
|
332
|
+
if (data == nil) {
|
|
333
|
+
[strongSelf reject:reject
|
|
334
|
+
code:PdfCanvasErrorCodeCorrupt
|
|
335
|
+
message:@"The document's base64 payload could not be decoded."
|
|
336
|
+
underlying:nil];
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
rasterizer = [PdfCanvasRasterizer openData:data
|
|
340
|
+
password:password
|
|
341
|
+
poolSize:lanes
|
|
342
|
+
error:&error];
|
|
343
|
+
} else {
|
|
344
|
+
NSURL *url = [strongSelf resolveURI:uri error:&error];
|
|
345
|
+
if (url != nil) {
|
|
346
|
+
rasterizer = [PdfCanvasRasterizer openFileURL:url
|
|
347
|
+
password:password
|
|
348
|
+
poolSize:lanes
|
|
349
|
+
error:&error];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (rasterizer == nil) {
|
|
353
|
+
[strongSelf rejectWithError:reject error:error];
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// EVERYTHING THAT CAN STILL FAIL HAPPENS BEFORE THE RASTERIZER IS PUBLISHED.
|
|
358
|
+
// Registering it first and then throwing while building the result would leave
|
|
359
|
+
// an open document — parsed pages, a mapped file — that JS has no handle for
|
|
360
|
+
// and could never close. Android's binding carries the same ordering and the
|
|
361
|
+
// same `published` flag for the same reason.
|
|
362
|
+
NSMutableArray<NSDictionary *> *pages =
|
|
363
|
+
[NSMutableArray arrayWithCapacity:(NSUInteger)rasterizer.pageCount];
|
|
364
|
+
for (NSInteger i = 0; i < rasterizer.pageCount; i++) {
|
|
365
|
+
PdfCanvasPageGeometry *geometry = [rasterizer pageGeometryAtIndex:i
|
|
366
|
+
error:&error];
|
|
367
|
+
if (geometry == nil) {
|
|
368
|
+
[rasterizer close];
|
|
369
|
+
[strongSelf rejectWithError:reject error:error];
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
[pages addObject:@{
|
|
373
|
+
@"index" : @(geometry.index),
|
|
374
|
+
// DOUBLES, not ints. A PDF point is not an integer — A4 is 595.276 wide —
|
|
375
|
+
// and Android reports ints only because `PdfRenderer` gives it ints.
|
|
376
|
+
// `PageGeometry.width` in `src/types.ts` is a `number`, so nothing has to
|
|
377
|
+
// change on the JS side to carry the extra precision.
|
|
378
|
+
@"width" : @(geometry.widthPoints),
|
|
379
|
+
@"height" : @(geometry.heightPoints),
|
|
380
|
+
@"rotation" : @(geometry.rotationDegrees),
|
|
381
|
+
}];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
NSNumber *handle = nil;
|
|
385
|
+
[strongSelf->_lock lock];
|
|
386
|
+
if (strongSelf->_invalidated) {
|
|
387
|
+
[strongSelf->_lock unlock];
|
|
388
|
+
[rasterizer close];
|
|
389
|
+
[strongSelf reject:reject
|
|
390
|
+
code:PdfCanvasErrorCodeBackendFailure
|
|
391
|
+
message:@"The PdfCanvas module was invalidated while opening."
|
|
392
|
+
underlying:nil];
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
handle = @(strongSelf->_nextHandle++);
|
|
396
|
+
strongSelf->_handles[handle] = rasterizer;
|
|
397
|
+
[strongSelf->_lock unlock];
|
|
398
|
+
|
|
399
|
+
resolve(@{
|
|
400
|
+
@"handle" : handle,
|
|
401
|
+
@"pageCount" : @(rasterizer.pageCount),
|
|
402
|
+
@"pages" : pages,
|
|
403
|
+
// A CONSTANT on this platform, and part of the shared wire format rather
|
|
404
|
+
// than an Android extension: a host should not have to know which platform
|
|
405
|
+
// it is on to ask whether a `/Rotate` page will draw upright.
|
|
406
|
+
@"rotationSupport" : PdfCanvasRasterizer.rotationSupport,
|
|
407
|
+
});
|
|
408
|
+
}];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Accepts a plain path or a `file://` URL.
|
|
413
|
+
*
|
|
414
|
+
* NO `content://` EQUIVALENT TO HANDLE, which is the one place this is simpler than
|
|
415
|
+
* Android: there, a document-provider URI has no path the process can open and has
|
|
416
|
+
* to be copied into the cache first, because `PdfRenderer` needs a seekable
|
|
417
|
+
* descriptor and a provider may hand back a pipe. On iOS a picked document arrives
|
|
418
|
+
* as a `file://` URL that is either already in the app's container (the usual case,
|
|
419
|
+
* when the host asked for a copy) or security-scoped — and a security-scoped URL is
|
|
420
|
+
* the HOST's to keep open, which `PdfCanvasPreflight` says in its unreadable-file
|
|
421
|
+
* message rather than guessing at here.
|
|
422
|
+
*/
|
|
423
|
+
- (nullable NSURL *)resolveURI:(nullable NSString *)uri
|
|
424
|
+
error:(NSError *_Nullable *_Nullable)error {
|
|
425
|
+
if (uri.length == 0) {
|
|
426
|
+
PdfCanvasFail(error, PdfCanvasErrorCodeNotFound, @"No document uri was given.",
|
|
427
|
+
nil);
|
|
428
|
+
return nil;
|
|
429
|
+
}
|
|
430
|
+
if ([uri hasPrefix:@"file://"]) {
|
|
431
|
+
NSURL *url = [NSURL URLWithString:uri];
|
|
432
|
+
if (url == nil || url.path.length == 0) {
|
|
433
|
+
// A path with characters that need percent-encoding — a space, a `#` — makes
|
|
434
|
+
// `URLWithString:` return nil. Falling back to treating the whole string as a
|
|
435
|
+
// path is wrong (it would keep the scheme), so the encoded form is tried.
|
|
436
|
+
NSString *encoded = [uri
|
|
437
|
+
stringByAddingPercentEncodingWithAllowedCharacters:
|
|
438
|
+
NSCharacterSet.URLPathAllowedCharacterSet];
|
|
439
|
+
url = encoded == nil ? nil : [NSURL URLWithString:encoded];
|
|
440
|
+
}
|
|
441
|
+
if (url == nil || url.path.length == 0) {
|
|
442
|
+
PdfCanvasFail(
|
|
443
|
+
error, PdfCanvasErrorCodeNotFound,
|
|
444
|
+
[NSString stringWithFormat:@"Could not parse %@ as a file URL.", uri],
|
|
445
|
+
nil);
|
|
446
|
+
return nil;
|
|
447
|
+
}
|
|
448
|
+
return [NSURL fileURLWithPath:url.path];
|
|
449
|
+
}
|
|
450
|
+
if ([uri containsString:@"://"]) {
|
|
451
|
+
// Handed on as-is so preflight can produce the "this backend does no
|
|
452
|
+
// networking" message, which is more useful than a parse failure here.
|
|
453
|
+
NSURL *url = [NSURL URLWithString:uri];
|
|
454
|
+
if (url != nil) {
|
|
455
|
+
return url;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return [NSURL fileURLWithPath:uri];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* ---------------------------------------------------------------- *
|
|
462
|
+
* render
|
|
463
|
+
* ---------------------------------------------------------------- */
|
|
464
|
+
|
|
465
|
+
RCT_EXPORT_METHOD(render
|
|
466
|
+
: (double)handle request
|
|
467
|
+
: (NSDictionary *)request token
|
|
468
|
+
: (double)token resolve
|
|
469
|
+
: (RCTPromiseResolveBlock)resolve reject
|
|
470
|
+
: (RCTPromiseRejectBlock)reject) {
|
|
471
|
+
NSNumber *handleKey = @((NSInteger)handle);
|
|
472
|
+
[_lock lock];
|
|
473
|
+
PdfCanvasRasterizer *rasterizer = _handles[handleKey];
|
|
474
|
+
[_lock unlock];
|
|
475
|
+
if (rasterizer == nil) {
|
|
476
|
+
[self reject:reject
|
|
477
|
+
code:PdfCanvasErrorCodeBackendFailure
|
|
478
|
+
message:[NSString stringWithFormat:@"No open document for handle %ld.",
|
|
479
|
+
(long)handle]
|
|
480
|
+
underlying:nil];
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
PdfCanvasRasterRequest *req = [[PdfCanvasRasterRequest alloc]
|
|
485
|
+
initWithPage:[request[@"page"] integerValue]
|
|
486
|
+
docX:[request[@"x"] doubleValue]
|
|
487
|
+
docY:[request[@"y"] doubleValue]
|
|
488
|
+
docWidth:[request[@"width"] doubleValue]
|
|
489
|
+
docHeight:[request[@"height"] doubleValue]
|
|
490
|
+
scale:[request[@"scale"] doubleValue]
|
|
491
|
+
annotations:request[@"annotations"] == nil
|
|
492
|
+
? YES
|
|
493
|
+
: [request[@"annotations"] boolValue]
|
|
494
|
+
background:[@"transparent" isEqual:request[@"background"]]
|
|
495
|
+
? PdfCanvasBackgroundTransparent
|
|
496
|
+
: PdfCanvasBackgroundWhite];
|
|
497
|
+
|
|
498
|
+
NSNumber *signalKey = [self signalKeyForHandle:(NSInteger)handle
|
|
499
|
+
token:(NSInteger)token];
|
|
500
|
+
// Registered BEFORE the operation runs, and synchronously on the calling thread.
|
|
501
|
+
// That ordering is what lets `cancel` be a pure lookup: calls from one JS module
|
|
502
|
+
// are executed in the order JS made them, and JS cannot cancel a token before
|
|
503
|
+
// the render that issued it, so by the time any cancel for this key arrives this
|
|
504
|
+
// entry already exists.
|
|
505
|
+
PdfCanvasCancellation *signal = [[PdfCanvasCancellation alloc] init];
|
|
506
|
+
[_lock lock];
|
|
507
|
+
_signals[signalKey] = signal;
|
|
508
|
+
[_lock unlock];
|
|
509
|
+
|
|
510
|
+
__weak PdfCanvasModule *weakSelf = self;
|
|
511
|
+
[_renderQueue addOperationWithBlock:^{
|
|
512
|
+
PdfCanvasModule *strongSelf = weakSelf;
|
|
513
|
+
if (strongSelf == nil) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
PdfCanvasRasterPixels *pixels = nil;
|
|
517
|
+
BOOL handedOff = NO;
|
|
518
|
+
@try {
|
|
519
|
+
// Read ONCE. The flag is atomic and `invalidate` clears it, so reading it
|
|
520
|
+
// twice could ask for a slot and then decide not to look for one — which is
|
|
521
|
+
// exactly how a filled slot is stranded.
|
|
522
|
+
const bool fast = strongSelf->_jsiEnabled;
|
|
523
|
+
id<PdfCanvasPixelSink> sink =
|
|
524
|
+
fast ? [[PdfCanvasSlotSink alloc] init] : nil;
|
|
525
|
+
|
|
526
|
+
NSError *error = nil;
|
|
527
|
+
pixels = [rasterizer render:req cancellation:signal sink:sink error:&error];
|
|
528
|
+
if (pixels == nil) {
|
|
529
|
+
[strongSelf rejectWithError:reject error:error];
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:5];
|
|
534
|
+
// ONE of the two, never both — `native-bridge.ts` branches on which key is
|
|
535
|
+
// present, and sending both would mean paying for the encode this whole path
|
|
536
|
+
// exists to delete.
|
|
537
|
+
//
|
|
538
|
+
// The branch is on WHERE THE PIXELS ARE, not on what was asked for: a sink
|
|
539
|
+
// may refuse its reservation, and that tile still has to go out.
|
|
540
|
+
if (pixels.isInNativeSlot) {
|
|
541
|
+
// A double, because that is what a bridge number is on the way to JS.
|
|
542
|
+
// Slots are consumed within one render, so the 2^53 exact-integer ceiling
|
|
543
|
+
// is not reachable in any session that could also survive the file
|
|
544
|
+
// descriptors.
|
|
545
|
+
result[@"pixelsSlot"] = @((double)pixels.slot);
|
|
546
|
+
} else {
|
|
547
|
+
result[@"base64"] = [pixels.bytes base64EncodedStringWithOptions:0];
|
|
548
|
+
}
|
|
549
|
+
result[@"width"] = @(pixels.width);
|
|
550
|
+
result[@"height"] = @(pixels.height);
|
|
551
|
+
// Passed through, never recomputed as width*4 on either side of the bridge.
|
|
552
|
+
// A wrong stride shears the image rather than failing.
|
|
553
|
+
result[@"rowBytes"] = @(pixels.rowBytes);
|
|
554
|
+
result[@"format"] = pixels.format;
|
|
555
|
+
resolve(result);
|
|
556
|
+
// ONLY NOW. Everything that could fail is behind us and the slot id has
|
|
557
|
+
// reached JS, so from here the ArrayBuffer's take is what frees it and this
|
|
558
|
+
// thread must not.
|
|
559
|
+
handedOff = YES;
|
|
560
|
+
} @catch (NSException *exception) {
|
|
561
|
+
[strongSelf reject:reject
|
|
562
|
+
code:PdfCanvasErrorCodeBackendFailure
|
|
563
|
+
message:[NSString stringWithFormat:@"%@: %@", exception.name,
|
|
564
|
+
exception.reason]
|
|
565
|
+
underlying:nil];
|
|
566
|
+
} @finally {
|
|
567
|
+
// THE ABANDON PATHS, and each one strands a whole raster of C++ memory
|
|
568
|
+
// without this:
|
|
569
|
+
//
|
|
570
|
+
// 1. Building the result dictionary throws (memory pressure), or
|
|
571
|
+
// `resolve` throws, or the promise was already settled.
|
|
572
|
+
// 2. NOT the render failing after it filled the slot: `pixels` is nil on
|
|
573
|
+
// that path, so this cannot release it. The rasterizer owns that one —
|
|
574
|
+
// its post-draw cancellation check abandons its own sink before
|
|
575
|
+
// returning.
|
|
576
|
+
//
|
|
577
|
+
// Also not covered, and the two differ by WHEN the reload lands: a slot id
|
|
578
|
+
// that reaches JS whose continuation never runs is reclaimed by
|
|
579
|
+
// `PdfCanvasSlotsReleaseAll` in `invalidate`; a slot published AFTER that —
|
|
580
|
+
// this worker was already mid-draw when the reload started — waits for the
|
|
581
|
+
// NEXT one. See the note at `invalidate`.
|
|
582
|
+
if (pixels != nil && !handedOff) {
|
|
583
|
+
[pixels releaseSlot];
|
|
584
|
+
}
|
|
585
|
+
[strongSelf->_lock lock];
|
|
586
|
+
[strongSelf->_signals removeObjectForKey:signalKey];
|
|
587
|
+
[strongSelf->_lock unlock];
|
|
588
|
+
}
|
|
589
|
+
}];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Best-effort, and the JS side is told so via `capabilities.interruptibleRender:
|
|
594
|
+
* false`. This can stop a queued render starting and can discard a finished one; it
|
|
595
|
+
* cannot interrupt `CGContextDrawPDFPage`.
|
|
596
|
+
*
|
|
597
|
+
* A PURE LOOKUP, and deliberately not a speculative insert. "No signal registered"
|
|
598
|
+
* is far more commonly what a cancel that arrives AFTER its render finished and
|
|
599
|
+
* cleaned up looks like than a cancel that beat its render across the bridge — and
|
|
600
|
+
* a superseded raster epoch produces a stream of the former. Planting an entry for
|
|
601
|
+
* each would grow the dictionary for the life of the module. Android's binding
|
|
602
|
+
* carries the same fix and the same note.
|
|
603
|
+
*/
|
|
604
|
+
RCT_EXPORT_METHOD(cancel : (double)handle token : (double)token) {
|
|
605
|
+
NSNumber *key = [self signalKeyForHandle:(NSInteger)handle
|
|
606
|
+
token:(NSInteger)token];
|
|
607
|
+
[_lock lock];
|
|
608
|
+
PdfCanvasCancellation *signal = _signals[key];
|
|
609
|
+
[_lock unlock];
|
|
610
|
+
[signal cancel];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
RCT_EXPORT_METHOD(close : (double)handle) {
|
|
614
|
+
NSNumber *key = @((NSInteger)handle);
|
|
615
|
+
[_lock lock];
|
|
616
|
+
PdfCanvasRasterizer *rasterizer = _handles[key];
|
|
617
|
+
[_handles removeObjectForKey:key];
|
|
618
|
+
[_lock unlock];
|
|
619
|
+
// Non-blocking even with a render in flight. See `PdfCanvasDocumentPool`.
|
|
620
|
+
[rasterizer close];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Exposed for wire parity with Android, where the answer is a device probe. */
|
|
624
|
+
RCT_EXPORT_METHOD(rotationSupport
|
|
625
|
+
: (RCTPromiseResolveBlock)resolve reject
|
|
626
|
+
: (RCTPromiseRejectBlock)reject) {
|
|
627
|
+
resolve(PdfCanvasRasterizer.rotationSupport);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Slots currently parked, for a host that wants to see the transport is not
|
|
632
|
+
* leaking. Diagnostic only — nothing in the package reads it.
|
|
633
|
+
*/
|
|
634
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(pendingRasterSlots) {
|
|
635
|
+
return @(PdfCanvasSlotsCount());
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/*
|
|
639
|
+
* WHY THERE IS NO `isBackendPoisoned`.
|
|
640
|
+
*
|
|
641
|
+
* Android exposes one because a `PdfRenderer` constructor that throws permanently
|
|
642
|
+
* poisons PDFium for the whole process on API 24, and a host may want to know it
|
|
643
|
+
* has happened. CoreGraphics has no equivalent behaviour — MEASURED: a `%PDF-`
|
|
644
|
+
* header over garbage returns NULL and a subsequent open of a good document
|
|
645
|
+
* succeeds, repeatedly, in the same process (`FailureTests.swift`,
|
|
646
|
+
* `testHeaderOnlyGarbageIsCorruptAndPoisonsNothing`). A method that always answered
|
|
647
|
+
* "no" would be a method a host might one day trust to mean something.
|
|
648
|
+
*/
|
|
649
|
+
|
|
650
|
+
/* ---------------------------------------------------------------- *
|
|
651
|
+
* helpers
|
|
652
|
+
* ---------------------------------------------------------------- */
|
|
653
|
+
|
|
654
|
+
- (NSInteger)clampPoolSize:(double)requested {
|
|
655
|
+
if (!(requested >= 1)) {
|
|
656
|
+
return 1;
|
|
657
|
+
}
|
|
658
|
+
const NSInteger size = (NSInteger)requested;
|
|
659
|
+
return size > kPdfCanvasQueueWidth ? kPdfCanvasQueueWidth : size;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
- (NSNumber *)signalKeyForHandle:(NSInteger)handle token:(NSInteger)token {
|
|
663
|
+
return @(((int64_t)handle << 32) | (token & 0xffffffffLL));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* The error CODE is the promise's rejection code, verbatim from
|
|
668
|
+
* `PdfCanvasErrorCodeWire` — which is the TypeScript `PdfErrorCode` union member.
|
|
669
|
+
* React Native surfaces it as `error.code`, and
|
|
670
|
+
* `src/rasterizer/native-bridge.ts` reconstructs a `PdfError` from it with no
|
|
671
|
+
* translation table in between.
|
|
672
|
+
*/
|
|
673
|
+
- (void)reject:(RCTPromiseRejectBlock)reject
|
|
674
|
+
code:(PdfCanvasErrorCode)code
|
|
675
|
+
message:(NSString *)message
|
|
676
|
+
underlying:(nullable NSError *)underlying {
|
|
677
|
+
reject(PdfCanvasErrorCodeWire(code), message,
|
|
678
|
+
underlying ?: PdfCanvasMakeError(code, message, nil));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
- (void)rejectWithError:(RCTPromiseRejectBlock)reject
|
|
682
|
+
error:(nullable NSError *)error {
|
|
683
|
+
if (error == nil) {
|
|
684
|
+
[self reject:reject
|
|
685
|
+
code:PdfCanvasErrorCodeBackendFailure
|
|
686
|
+
message:@"The rasterizer failed without reporting an error."
|
|
687
|
+
underlying:nil];
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
reject(PdfCanvasErrorCodeWire(PdfCanvasErrorCodeOf(error)),
|
|
691
|
+
error.localizedDescription, error);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/* ---------------------------------------------------------------- *
|
|
695
|
+
* The runtime
|
|
696
|
+
* ---------------------------------------------------------------- */
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Returns the SAME wrapper the interop layer would have built for this module.
|
|
700
|
+
*
|
|
701
|
+
* `ObjCInteropTurboModule` is what `RCTTurboModuleManager provideLegacyModule:`
|
|
702
|
+
* constructs for any `RCT_EXPORT_MODULE` class, and it reads this class's exported
|
|
703
|
+
* methods off the `__rct_export__` metadata at runtime — so returning it changes
|
|
704
|
+
* NOTHING about how `open`, `render`, `cancel`, `close` or the two synchronous
|
|
705
|
+
* methods are marshalled. What it changes is which branch of the manager this module
|
|
706
|
+
* takes: responding to `getTurboModule:` moves it from `provideLegacyModule:` to
|
|
707
|
+
* `provideTurboModule:`, and only the latter offers
|
|
708
|
+
* `installJSIBindingsWithRuntime:callInvoker:`.
|
|
709
|
+
*
|
|
710
|
+
* NO CODEGEN, deliberately. A generated spec would mean a `codegenConfig` in
|
|
711
|
+
* `package.json`, a spec file, and a JS side that reaches the module through
|
|
712
|
+
* `TurboModuleRegistry` instead of `NativeModules` — and `NativeModules` is what
|
|
713
|
+
* works on BOTH architectures and what `src/rasterizer/index.native.ts` does for
|
|
714
|
+
* Android too. Paying for codegen to obtain a hook the interop wrapper can be handed
|
|
715
|
+
* directly is the wrong trade.
|
|
716
|
+
*/
|
|
717
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
718
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
719
|
+
return std::make_shared<facebook::react::ObjCInteropTurboModule>(params);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* The one place a live `jsi::Runtime` is handed to this module.
|
|
724
|
+
*
|
|
725
|
+
* Called on the JS thread at module construction, before JS can call any method
|
|
726
|
+
* here — so the host function exists before the first `installJsiTransport()` looks
|
|
727
|
+
* for it, and no render can race the decision. See the file header for the three
|
|
728
|
+
* routes that do not work.
|
|
729
|
+
*/
|
|
730
|
+
- (void)installJSIBindingsWithRuntime:(facebook::jsi::Runtime &)runtime
|
|
731
|
+
callInvoker:
|
|
732
|
+
(const std::shared_ptr<facebook::react::CallInvoker> &)
|
|
733
|
+
callInvoker {
|
|
734
|
+
_hostFunctionInstalled = PdfCanvasSlotsInstall(&runtime);
|
|
735
|
+
if (!_hostFunctionInstalled) {
|
|
736
|
+
RCTLogWarn(
|
|
737
|
+
@"[PdfCanvas] The JS runtime refused __pdfCanvasTakePixels, so every "
|
|
738
|
+
@"tile will cross the bridge as base64. That is correct but roughly 85x "
|
|
739
|
+
@"slower per tile.");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
@end
|