@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +77 -40
- package/README.md +218 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +559 -0
- package/scripts/pdfium-manifest.json +48 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#import "PdfCanvasModule.h"
|
|
2
2
|
|
|
3
3
|
// `<React/RCTBridgeModule.h>` only FORWARD-DECLARES `RCTBridge`, so the category
|
|
4
|
-
// below and every `self.bridge` message need the real header.
|
|
5
|
-
// fails as "cannot define category for undefined class", which reads like a missing
|
|
6
|
-
// pod dependency rather than a missing import.
|
|
4
|
+
// below and every `self.bridge` message need the real header.
|
|
7
5
|
#import <React/RCTBridge.h>
|
|
8
6
|
#import <React/RCTLog.h>
|
|
9
7
|
// `ObjCInteropTurboModule` — a PUBLIC pod header, exposed by React-NativeModulesApple
|
|
@@ -14,117 +12,71 @@
|
|
|
14
12
|
#import <ReactCommon/RCTTurboModule.h>
|
|
15
13
|
|
|
16
14
|
#import <atomic>
|
|
15
|
+
#import <memory>
|
|
16
|
+
#import <string>
|
|
17
|
+
#import <vector>
|
|
17
18
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#import <PdfCanvasRasterPixels.h>
|
|
23
|
-
#import <PdfCanvasRasterRequest.h>
|
|
24
|
-
#import <PdfCanvasRasterizer.h>
|
|
25
|
-
|
|
26
|
-
#import <PdfCanvasSlots.h>
|
|
19
|
+
#include "pdfcanvas/error.h"
|
|
20
|
+
#include "pdfcanvas/jsi.h"
|
|
21
|
+
#include "pdfcanvas/service.h"
|
|
22
|
+
#include "pdfcanvas/slots.h"
|
|
27
23
|
|
|
28
24
|
/**
|
|
25
|
+
* MARSHALLING ONLY, over the C++ core in `native/core`.
|
|
26
|
+
*
|
|
27
|
+
* This file used to sit on a CoreGraphics/PDFKit rasterizer of its own. It now
|
|
28
|
+
* sits on the same PDFium core Android does — same release, same tiles, byte for
|
|
29
|
+
* byte — and the handle map, the (handle, token) cancellation registry and the
|
|
30
|
+
* invalidate ordering it used to carry live in `pdfcanvas::Service`, once, in the
|
|
31
|
+
* language both bindings call. What is left here: NSDictionary in, NSDictionary
|
|
32
|
+
* out, a worker queue, a transport flag, and how a runtime pointer is obtained.
|
|
33
|
+
*
|
|
29
34
|
* ------------------------------------------------------------------------
|
|
30
35
|
* HOW THIS MODULE GETS A `jsi::Runtime`, AND THE TWO ROUTES THAT DO NOT WORK
|
|
31
36
|
* ------------------------------------------------------------------------
|
|
32
37
|
*
|
|
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
38
|
* 1. `installJSIBindingsWithRuntime:callInvoker:` — WHAT THIS FILE USES. React
|
|
38
39
|
* 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.
|
|
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:
|
|
40
|
+
* construction, BEFORE JS can call any method on the module. THE CATCH:
|
|
45
41
|
* `RCTTurboModuleManager` only calls it for a module that responds to
|
|
46
|
-
* `getTurboModule
|
|
47
|
-
*
|
|
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.
|
|
42
|
+
* `getTurboModule:`, so this module returns an `ObjCInteropTurboModule` —
|
|
43
|
+
* exactly what the interop layer would have built for it.
|
|
55
44
|
*
|
|
56
|
-
*
|
|
57
|
-
* `RCTBridgeProxy`
|
|
58
|
-
*
|
|
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.
|
|
45
|
+
* 2. `self.bridge.runtime` — the FALLBACK, for the old architecture. It does NOT
|
|
46
|
+
* work under bridgeless (measured): `RCTBridgeProxy` is an `NSProxy` that does
|
|
47
|
+
* not override `respondsToSelector:`, so the guard answers NO.
|
|
68
48
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* on the new architecture skia installs through its own codegen'd spec and the
|
|
72
|
-
* bridge path is its legacy branch.)
|
|
49
|
+
* 3. `RCTCallInvokerModule` + `invokeSync` — REJECTED; it deadlocks from a
|
|
50
|
+
* blocking synchronous method (`executeSynchronouslyOnSameThread_CAN_DEADLOCK`).
|
|
73
51
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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.
|
|
52
|
+
* 4. `invokeAsync` at construction — REJECTED on ordering: `installJsiTransport()`
|
|
53
|
+
* is called within the same JS turn and would find nothing.
|
|
86
54
|
*/
|
|
87
55
|
@interface RCTBridge (PdfCanvasRuntime)
|
|
88
56
|
- (void *)runtime;
|
|
89
57
|
@end
|
|
90
58
|
|
|
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
59
|
@implementation PdfCanvasModule {
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
60
|
+
/**
|
|
61
|
+
* SHARED with every queued block. A block that is mid-render when a reload
|
|
62
|
+
* lands keeps the service alive until it returns, so a torn-down module can
|
|
63
|
+
* never free memory a worker is still writing into.
|
|
64
|
+
*/
|
|
65
|
+
std::shared_ptr<pdfcanvas::Service> _service;
|
|
66
|
+
/**
|
|
67
|
+
* ONE lane. PDFium is not thread-safe and the core serialises every call
|
|
68
|
+
* behind a process-wide lock, so a second worker would only queue behind the
|
|
69
|
+
* first while holding a promise open. (CoreGraphics parallelised; PDFium does
|
|
70
|
+
* not, and the iOS default was already one.)
|
|
71
|
+
*/
|
|
107
72
|
NSOperationQueue *_renderQueue;
|
|
108
|
-
NSInteger _nextHandle;
|
|
109
73
|
/**
|
|
110
|
-
* Whether renders return a slot instead of a base64 string.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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.
|
|
74
|
+
* Whether renders return a slot instead of a base64 string. FALSE UNTIL
|
|
75
|
+
* PROVEN by the JS-side probe round trip. Atomic: written on the JS thread,
|
|
76
|
+
* read on the worker.
|
|
116
77
|
*/
|
|
117
78
|
std::atomic<bool> _jsiEnabled;
|
|
118
79
|
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
80
|
BOOL _hostFunctionInstalled;
|
|
129
81
|
}
|
|
130
82
|
|
|
@@ -133,26 +85,17 @@ static const NSInteger kPdfCanvasQueueWidth = 4;
|
|
|
133
85
|
RCT_EXPORT_MODULE(PdfCanvas)
|
|
134
86
|
|
|
135
87
|
+ (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
88
|
return NO;
|
|
139
89
|
}
|
|
140
90
|
|
|
141
91
|
- (instancetype)init {
|
|
142
92
|
self = [super init];
|
|
143
93
|
if (self != nil) {
|
|
144
|
-
|
|
145
|
-
_signals = [NSMutableDictionary dictionary];
|
|
146
|
-
_lock = [[NSLock alloc] init];
|
|
147
|
-
_nextHandle = 1;
|
|
94
|
+
_service = std::make_shared<pdfcanvas::Service>();
|
|
148
95
|
_jsiEnabled = false;
|
|
149
96
|
_invalidated = false;
|
|
150
97
|
_renderQueue = [[NSOperationQueue alloc] init];
|
|
151
|
-
|
|
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;
|
|
98
|
+
_renderQueue.maxConcurrentOperationCount = 1;
|
|
156
99
|
_renderQueue.name = @"tools.reekon.pdfcanvas.render";
|
|
157
100
|
_renderQueue.qualityOfService = NSQualityOfServiceUserInitiated;
|
|
158
101
|
}
|
|
@@ -160,53 +103,51 @@ RCT_EXPORT_MODULE(PdfCanvas)
|
|
|
160
103
|
}
|
|
161
104
|
|
|
162
105
|
/**
|
|
163
|
-
* A reload drops the JS side without ever calling `close
|
|
164
|
-
*
|
|
165
|
-
*
|
|
106
|
+
* A reload drops the JS side without ever calling `close`. The service closes
|
|
107
|
+
* every document (waiting for an in-flight render to reach its next
|
|
108
|
+
* cancellation checkpoint), cancels every registered signal and drains every
|
|
109
|
+
* parked slot — rasters whose promise resolved but whose JS continuation never
|
|
110
|
+
* ran.
|
|
111
|
+
*
|
|
112
|
+
* THE QUEUE IS DELIBERATELY NOT CANCELLED. A cancelled `NSOperation` never runs
|
|
113
|
+
* its block, so its resolve/reject pair is never called and the JS `await` hangs
|
|
114
|
+
* for the life of the app. Letting queued operations run costs nothing: each
|
|
115
|
+
* fails fast with `backend-failure` against an invalidated service.
|
|
166
116
|
*/
|
|
167
117
|
- (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
118
|
_jsiEnabled = false;
|
|
177
119
|
_invalidated = true;
|
|
120
|
+
_service->invalidate();
|
|
121
|
+
}
|
|
178
122
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
}
|
|
123
|
+
/* ---------------------------------------------------------------- *
|
|
124
|
+
* helpers
|
|
125
|
+
* ---------------------------------------------------------------- */
|
|
191
126
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
127
|
+
static NSString *PdfCanvasResolvePath(NSString *uri);
|
|
128
|
+
|
|
129
|
+
static NSString *WireCode(pdfcanvas::ErrorCode code) {
|
|
130
|
+
return [NSString stringWithUTF8String:pdfcanvas::wireName(code)];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static NSError *ErrorFor(pdfcanvas::ErrorCode code, NSString *message) {
|
|
134
|
+
return [NSError errorWithDomain:@"tools.reekon.pdfcanvas"
|
|
135
|
+
code:static_cast<NSInteger>(code)
|
|
136
|
+
userInfo:@{NSLocalizedDescriptionKey : message ?: @"PDF Canvas failed."}];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The error CODE is the promise's rejection code, verbatim from
|
|
141
|
+
* `pdfcanvas::wireName` — the TypeScript `PdfErrorCode` union member. React Native
|
|
142
|
+
* surfaces it as `error.code`, and `native-bridge.ts` reconstructs a `PdfError`
|
|
143
|
+
* from it with no translation table in between.
|
|
144
|
+
*/
|
|
145
|
+
static void RejectWith(RCTPromiseRejectBlock reject, pdfcanvas::ErrorCode code, NSString *message) {
|
|
146
|
+
reject(WireCode(code), message, ErrorFor(code, message));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static void RejectWithPdfError(RCTPromiseRejectBlock reject, const pdfcanvas::PdfError &error) {
|
|
150
|
+
RejectWith(reject, error.code(), [NSString stringWithUTF8String:error.message().c_str()]);
|
|
210
151
|
}
|
|
211
152
|
|
|
212
153
|
/* ---------------------------------------------------------------- *
|
|
@@ -216,246 +157,154 @@ RCT_EXPORT_MODULE(PdfCanvas)
|
|
|
216
157
|
/**
|
|
217
158
|
* Installs the JSI host function and parks one probe raster for JS to take.
|
|
218
159
|
*
|
|
219
|
-
* SYNCHRONOUS on purpose
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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.
|
|
160
|
+
* SYNCHRONOUS on purpose: a blocking synchronous method runs inline on the JS
|
|
161
|
+
* thread — the only thread where `bridge.runtime` is a live pointer — and
|
|
162
|
+
* completes before the JS module that called it finishes evaluating, so no render
|
|
163
|
+
* can race the decision. IT RETURNS AN OBJECT, AND MUST: the interop layer reads a
|
|
164
|
+
* non-void, non-promise return as `id`, and a `double` would be reinterpreted as
|
|
165
|
+
* a pointer.
|
|
235
166
|
*
|
|
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.
|
|
167
|
+
* @return the probe slot id, or -1 if the fast path is unavailable. Never throws.
|
|
238
168
|
*/
|
|
239
169
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(installJsiTransport) {
|
|
240
170
|
@try {
|
|
241
171
|
if (_invalidated) {
|
|
242
172
|
return @(-1);
|
|
243
173
|
}
|
|
244
|
-
// NORMALLY ALREADY DONE
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// architecture, where that hook is never called and `RCTCxxBridge` genuinely
|
|
248
|
-
// does answer `-runtime`.
|
|
174
|
+
// NORMALLY ALREADY DONE by installJSIBindingsWithRuntime: on the new
|
|
175
|
+
// architecture. The branch below is the old architecture, where that hook is
|
|
176
|
+
// never called and `RCTCxxBridge` genuinely does answer `-runtime`.
|
|
249
177
|
if (!_hostFunctionInstalled) {
|
|
250
178
|
RCTBridge *bridge = self.bridge;
|
|
251
|
-
void *runtime =
|
|
252
|
-
[bridge respondsToSelector:@selector(runtime)] ? bridge.runtime : NULL;
|
|
179
|
+
void *runtime = [bridge respondsToSelector:@selector(runtime)] ? bridge.runtime : NULL;
|
|
253
180
|
if (runtime == NULL) {
|
|
254
|
-
RCTLogWarn(
|
|
255
|
-
|
|
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]));
|
|
181
|
+
RCTLogWarn(@"[PdfCanvas] No JS runtime was reachable, so every tile will cross the "
|
|
182
|
+
@"bridge as base64. That is correct but roughly 85x slower per tile.");
|
|
260
183
|
return @(-1);
|
|
261
184
|
}
|
|
262
|
-
_hostFunctionInstalled =
|
|
185
|
+
_hostFunctionInstalled = pdfcanvas::installTakePixels(runtime);
|
|
263
186
|
if (!_hostFunctionInstalled) {
|
|
264
187
|
return @(-1);
|
|
265
188
|
}
|
|
266
189
|
}
|
|
267
|
-
return @(
|
|
190
|
+
return @(pdfcanvas::slots::storeProbe());
|
|
268
191
|
} @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
192
|
return @(-1);
|
|
272
193
|
}
|
|
273
194
|
}
|
|
274
195
|
|
|
275
|
-
/**
|
|
276
|
-
|
|
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) {
|
|
196
|
+
/** Turns the fast transport on, once JS has verified the probe bytes. */
|
|
197
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(enableJsiTransport : (BOOL)enabled) {
|
|
286
198
|
const bool live = enabled && !_invalidated;
|
|
287
199
|
_jsiEnabled = live;
|
|
288
200
|
return @(live);
|
|
289
201
|
}
|
|
290
202
|
|
|
203
|
+
/** Slots currently parked, for a host checking that the transport does not leak. */
|
|
204
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(pendingRasterSlots) {
|
|
205
|
+
return @(static_cast<NSInteger>(pdfcanvas::slots::count()));
|
|
206
|
+
}
|
|
207
|
+
|
|
291
208
|
/* ---------------------------------------------------------------- *
|
|
292
209
|
* open
|
|
293
210
|
* ---------------------------------------------------------------- */
|
|
294
211
|
|
|
295
212
|
RCT_EXPORT_METHOD(open
|
|
296
|
-
: (NSDictionary *)source
|
|
297
|
-
: (double)poolSize resolve
|
|
213
|
+
: (NSDictionary *)source resolve
|
|
298
214
|
: (RCTPromiseResolveBlock)resolve reject
|
|
299
215
|
: (RCTPromiseRejectBlock)reject) {
|
|
300
|
-
// DRAINED HERE, on the calling thread, before the queue is handed anything
|
|
301
|
-
// iOS
|
|
302
|
-
//
|
|
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.
|
|
216
|
+
// DRAINED HERE, on the calling thread, before the queue is handed anything —
|
|
217
|
+
// defensive on iOS (an NSDictionary from the bridge is a real object), and the
|
|
218
|
+
// same shape as the Android binding.
|
|
307
219
|
NSString *uri = [source[@"uri"] isKindOfClass:NSString.class] ? source[@"uri"] : nil;
|
|
308
|
-
NSString *base64 =
|
|
309
|
-
|
|
310
|
-
NSString *password =
|
|
311
|
-
[source[@"password"] isKindOfClass:NSString.class] ? source[@"password"] : nil;
|
|
312
|
-
const NSInteger lanes = [self clampPoolSize:poolSize];
|
|
220
|
+
NSString *base64 = [source[@"base64"] isKindOfClass:NSString.class] ? source[@"base64"] : nil;
|
|
221
|
+
NSString *password = [source[@"password"] isKindOfClass:NSString.class] ? source[@"password"] : nil;
|
|
313
222
|
|
|
314
223
|
if (_invalidated) {
|
|
315
|
-
|
|
316
|
-
message:@"The PdfCanvas module has been invalidated."
|
|
317
|
-
underlying:nil];
|
|
224
|
+
RejectWith(reject, pdfcanvas::ErrorCode::BackendFailure, @"The PdfCanvas module has been invalidated.");
|
|
318
225
|
return;
|
|
319
226
|
}
|
|
320
227
|
|
|
321
|
-
|
|
228
|
+
std::shared_ptr<pdfcanvas::Service> service = _service;
|
|
322
229
|
[_renderQueue addOperationWithBlock:^{
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
230
|
+
pdfcanvas::Source src;
|
|
231
|
+
if (password != nil) {
|
|
232
|
+
src.hasPassword = true;
|
|
233
|
+
src.password = password.UTF8String;
|
|
326
234
|
}
|
|
327
|
-
NSError *error = nil;
|
|
328
|
-
PdfCanvasRasterizer *rasterizer = nil;
|
|
329
235
|
if (base64 != nil) {
|
|
330
|
-
NSData *data = [[NSData alloc] initWithBase64EncodedString:base64
|
|
331
|
-
options:0];
|
|
236
|
+
NSData *data = [[NSData alloc] initWithBase64EncodedString:base64 options:0];
|
|
332
237
|
if (data == nil) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
message:@"The document's base64 payload could not be decoded."
|
|
336
|
-
underlying:nil];
|
|
238
|
+
RejectWith(reject, pdfcanvas::ErrorCode::Corrupt,
|
|
239
|
+
@"The document's base64 payload could not be decoded.");
|
|
337
240
|
return;
|
|
338
241
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
poolSize:lanes
|
|
342
|
-
error:&error];
|
|
242
|
+
const auto *bytes = static_cast<const uint8_t *>(data.bytes);
|
|
243
|
+
src.bytes.assign(bytes, bytes + data.length);
|
|
343
244
|
} else {
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
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];
|
|
245
|
+
NSString *path = PdfCanvasResolvePath(uri);
|
|
246
|
+
if (path == nil) {
|
|
247
|
+
RejectWith(reject, pdfcanvas::ErrorCode::NotFound,
|
|
248
|
+
[NSString stringWithFormat:@"Could not resolve %@ to a file path.", uri ?: @"(nil)"]);
|
|
370
249
|
return;
|
|
371
250
|
}
|
|
372
|
-
|
|
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
|
-
}];
|
|
251
|
+
src.path = path.fileSystemRepresentation;
|
|
382
252
|
}
|
|
383
253
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
254
|
+
try {
|
|
255
|
+
const pdfcanvas::Service::Opened opened = service->open(src);
|
|
256
|
+
NSMutableArray<NSDictionary *> *pages = [NSMutableArray arrayWithCapacity:opened.pages.size()];
|
|
257
|
+
for (const auto &page : opened.pages) {
|
|
258
|
+
[pages addObject:@{
|
|
259
|
+
@"index" : @(page.index),
|
|
260
|
+
// DOUBLES: a PDF point is not an integer — A4 is 595.276 wide.
|
|
261
|
+
@"width" : @(page.width),
|
|
262
|
+
@"height" : @(page.height),
|
|
263
|
+
@"rotation" : @(page.rotation),
|
|
264
|
+
}];
|
|
265
|
+
}
|
|
266
|
+
resolve(@{
|
|
267
|
+
@"handle" : @(opened.handle),
|
|
268
|
+
@"pageCount" : @(static_cast<NSInteger>(opened.pages.size())),
|
|
269
|
+
@"pages" : pages,
|
|
270
|
+
});
|
|
271
|
+
} catch (const pdfcanvas::PdfError &error) {
|
|
272
|
+
RejectWithPdfError(reject, error);
|
|
273
|
+
} catch (const std::exception &error) {
|
|
274
|
+
RejectWith(reject, pdfcanvas::ErrorCode::BackendFailure,
|
|
275
|
+
[NSString stringWithUTF8String:error.what()]);
|
|
394
276
|
}
|
|
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
277
|
}];
|
|
409
278
|
}
|
|
410
279
|
|
|
411
280
|
/**
|
|
412
|
-
* Accepts a plain path or a `file://` URL.
|
|
281
|
+
* Accepts a plain path or a `file://` URL and returns a file-system path.
|
|
413
282
|
*
|
|
414
|
-
* NO `content://` EQUIVALENT TO HANDLE,
|
|
415
|
-
*
|
|
416
|
-
*
|
|
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.
|
|
283
|
+
* NO `content://` EQUIVALENT TO HANDLE, unlike Android. On iOS a picked document
|
|
284
|
+
* arrives as a `file://` URL that is either already in the app's container or
|
|
285
|
+
* security-scoped — and a security-scoped URL is the HOST's to keep open.
|
|
422
286
|
*/
|
|
423
|
-
|
|
424
|
-
error:(NSError *_Nullable *_Nullable)error {
|
|
287
|
+
static NSString *PdfCanvasResolvePath(NSString *uri) {
|
|
425
288
|
if (uri.length == 0) {
|
|
426
|
-
PdfCanvasFail(error, PdfCanvasErrorCodeNotFound, @"No document uri was given.",
|
|
427
|
-
nil);
|
|
428
289
|
return nil;
|
|
429
290
|
}
|
|
430
291
|
if ([uri hasPrefix:@"file://"]) {
|
|
431
292
|
NSURL *url = [NSURL URLWithString:uri];
|
|
432
293
|
if (url == nil || url.path.length == 0) {
|
|
433
|
-
// A path with characters that need percent-encoding — a space, a `#` —
|
|
434
|
-
// `URLWithString:` return nil
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
stringByAddingPercentEncodingWithAllowedCharacters:
|
|
438
|
-
NSCharacterSet.URLPathAllowedCharacterSet];
|
|
294
|
+
// A path with characters that need percent-encoding — a space, a `#` —
|
|
295
|
+
// makes `URLWithString:` return nil; try the encoded form.
|
|
296
|
+
NSString *encoded =
|
|
297
|
+
[uri stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet];
|
|
439
298
|
url = encoded == nil ? nil : [NSURL URLWithString:encoded];
|
|
440
299
|
}
|
|
441
|
-
|
|
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];
|
|
300
|
+
return url.path.length > 0 ? url.path : nil;
|
|
449
301
|
}
|
|
450
302
|
if ([uri containsString:@"://"]) {
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
if (url != nil) {
|
|
455
|
-
return url;
|
|
456
|
-
}
|
|
303
|
+
// Not a file. The core's preflight reports it as not-found with the path in
|
|
304
|
+
// the message, which is more useful than a parse failure here.
|
|
305
|
+
return uri;
|
|
457
306
|
}
|
|
458
|
-
return
|
|
307
|
+
return uri;
|
|
459
308
|
}
|
|
460
309
|
|
|
461
310
|
/* ---------------------------------------------------------------- *
|
|
@@ -468,227 +317,95 @@ RCT_EXPORT_METHOD(render
|
|
|
468
317
|
: (double)token resolve
|
|
469
318
|
: (RCTPromiseResolveBlock)resolve reject
|
|
470
319
|
: (RCTPromiseRejectBlock)reject) {
|
|
471
|
-
|
|
472
|
-
[
|
|
473
|
-
|
|
474
|
-
[
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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;
|
|
320
|
+
pdfcanvas::RasterRequest req;
|
|
321
|
+
req.page = [request[@"page"] intValue];
|
|
322
|
+
req.x = [request[@"x"] doubleValue];
|
|
323
|
+
req.y = [request[@"y"] doubleValue];
|
|
324
|
+
req.width = [request[@"width"] doubleValue];
|
|
325
|
+
req.height = [request[@"height"] doubleValue];
|
|
326
|
+
req.scale = [request[@"scale"] doubleValue];
|
|
327
|
+
req.annotations = request[@"annotations"] == nil ? true : [request[@"annotations"] boolValue];
|
|
328
|
+
req.background = [@"transparent" isEqual:request[@"background"]] ? pdfcanvas::Background::Transparent
|
|
329
|
+
: pdfcanvas::Background::White;
|
|
330
|
+
const int nativeHandle = static_cast<int>(handle);
|
|
331
|
+
const int64_t nativeToken = static_cast<int64_t>(token);
|
|
332
|
+
|
|
333
|
+
// Registered BEFORE the operation runs, synchronously on the calling thread.
|
|
334
|
+
// That ordering is what lets `cancel` be a pure lookup: JS cannot cancel a token
|
|
335
|
+
// before the render that issued it, so by the time any cancel for this token
|
|
336
|
+
// arrives the signal already exists.
|
|
337
|
+
_service->registerCancellation(nativeHandle, nativeToken);
|
|
338
|
+
|
|
339
|
+
std::shared_ptr<pdfcanvas::Service> service = _service;
|
|
340
|
+
// Read ONCE, here: `invalidate` clears it, and a worker that read it twice could
|
|
341
|
+
// ask for a slot and then decide not to look for one.
|
|
342
|
+
const bool fast = _jsiEnabled;
|
|
511
343
|
[_renderQueue addOperationWithBlock:^{
|
|
512
|
-
|
|
513
|
-
if (strongSelf == nil) {
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
PdfCanvasRasterPixels *pixels = nil;
|
|
344
|
+
int64_t slot = 0;
|
|
517
345
|
BOOL handedOff = NO;
|
|
518
346
|
@try {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (pixels == nil) {
|
|
529
|
-
[strongSelf rejectWithError:reject error:error];
|
|
347
|
+
pdfcanvas::RasterPixels pixels;
|
|
348
|
+
try {
|
|
349
|
+
pixels = service->render(nativeHandle, req, nativeToken, fast);
|
|
350
|
+
} catch (const pdfcanvas::PdfError &error) {
|
|
351
|
+
RejectWithPdfError(reject, error);
|
|
352
|
+
return;
|
|
353
|
+
} catch (const std::exception &error) {
|
|
354
|
+
RejectWith(reject, pdfcanvas::ErrorCode::BackendFailure,
|
|
355
|
+
[NSString stringWithUTF8String:error.what()]);
|
|
530
356
|
return;
|
|
531
357
|
}
|
|
358
|
+
slot = pixels.slot;
|
|
532
359
|
|
|
533
360
|
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:5];
|
|
534
361
|
// ONE of the two, never both — `native-bridge.ts` branches on which key is
|
|
535
|
-
// present
|
|
536
|
-
//
|
|
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.
|
|
362
|
+
// present. The branch is on WHERE THE PIXELS ARE, not on what was asked for:
|
|
363
|
+
// the store may have refused and the tile still has to go out.
|
|
364
|
+
if (pixels.inSlot()) {
|
|
545
365
|
result[@"pixelsSlot"] = @((double)pixels.slot);
|
|
546
366
|
} else {
|
|
547
|
-
|
|
367
|
+
NSData *data = [NSData dataWithBytesNoCopy:pixels.bytes.data()
|
|
368
|
+
length:pixels.bytes.size()
|
|
369
|
+
freeWhenDone:NO];
|
|
370
|
+
result[@"base64"] = [data base64EncodedStringWithOptions:0];
|
|
548
371
|
}
|
|
549
372
|
result[@"width"] = @(pixels.width);
|
|
550
373
|
result[@"height"] = @(pixels.height);
|
|
551
374
|
// Passed through, never recomputed as width*4 on either side of the bridge.
|
|
552
|
-
// A wrong stride shears the image rather than failing.
|
|
553
375
|
result[@"rowBytes"] = @(pixels.rowBytes);
|
|
554
|
-
result[@"format"] =
|
|
376
|
+
result[@"format"] = @"rgba8888";
|
|
555
377
|
resolve(result);
|
|
556
|
-
// ONLY NOW
|
|
557
|
-
//
|
|
558
|
-
// thread must not.
|
|
378
|
+
// ONLY NOW: the slot id has reached JS, and the ArrayBuffer's take is what
|
|
379
|
+
// frees it from here on.
|
|
559
380
|
handedOff = YES;
|
|
560
381
|
} @catch (NSException *exception) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
message:[NSString stringWithFormat:@"%@: %@", exception.name,
|
|
564
|
-
exception.reason]
|
|
565
|
-
underlying:nil];
|
|
382
|
+
RejectWith(reject, pdfcanvas::ErrorCode::BackendFailure,
|
|
383
|
+
[NSString stringWithFormat:@"%@: %@", exception.name, exception.reason]);
|
|
566
384
|
} @finally {
|
|
567
|
-
// THE ABANDON
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
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];
|
|
385
|
+
// THE ABANDON PATH: a slot was filled and then the result could not be built
|
|
386
|
+
// or the promise could not be settled. Without this a whole raster of native
|
|
387
|
+
// memory sits in the store until the next invalidate.
|
|
388
|
+
if (slot != 0 && !handedOff) {
|
|
389
|
+
pdfcanvas::slots::release(slot);
|
|
584
390
|
}
|
|
585
|
-
[strongSelf->_lock lock];
|
|
586
|
-
[strongSelf->_signals removeObjectForKey:signalKey];
|
|
587
|
-
[strongSelf->_lock unlock];
|
|
588
391
|
}
|
|
589
392
|
}];
|
|
590
393
|
}
|
|
591
394
|
|
|
592
395
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
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.
|
|
396
|
+
* Genuinely mid-render now, not best-effort: the core polls the signal from
|
|
397
|
+
* PDFium's progressive renderer between batches of page objects, so a superseded
|
|
398
|
+
* tile stops drawing within one batch. A pure lookup — a cancel for a token whose
|
|
399
|
+
* render already finished finds nothing and does nothing.
|
|
603
400
|
*/
|
|
604
401
|
RCT_EXPORT_METHOD(cancel : (double)handle token : (double)token) {
|
|
605
|
-
|
|
606
|
-
token:(NSInteger)token];
|
|
607
|
-
[_lock lock];
|
|
608
|
-
PdfCanvasCancellation *signal = _signals[key];
|
|
609
|
-
[_lock unlock];
|
|
610
|
-
[signal cancel];
|
|
402
|
+
_service->cancel(static_cast<int>(handle), static_cast<int64_t>(token));
|
|
611
403
|
}
|
|
612
404
|
|
|
613
405
|
RCT_EXPORT_METHOD(close : (double)handle) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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);
|
|
406
|
+
// Blocks only if a render on this document is in flight, and then only until
|
|
407
|
+
// that render reaches its next cancellation checkpoint.
|
|
408
|
+
_service->close(static_cast<int>(handle));
|
|
692
409
|
}
|
|
693
410
|
|
|
694
411
|
/* ---------------------------------------------------------------- *
|
|
@@ -696,23 +413,12 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(pendingRasterSlots) {
|
|
|
696
413
|
* ---------------------------------------------------------------- */
|
|
697
414
|
|
|
698
415
|
/**
|
|
699
|
-
* Returns the SAME wrapper the interop layer would have built for this module
|
|
700
|
-
*
|
|
701
|
-
* `
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
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.
|
|
416
|
+
* Returns the SAME wrapper the interop layer would have built for this module, so
|
|
417
|
+
* the methods above are marshalled exactly as a plain `RCT_EXPORT_MODULE` class's
|
|
418
|
+
* are — but `RCTTurboModuleManager` routes the module through `provideTurboModule:`,
|
|
419
|
+
* the only branch that offers `installJSIBindingsWithRuntime:callInvoker:`.
|
|
420
|
+
* NO CODEGEN, deliberately: `NativeModules` is what works on BOTH architectures
|
|
421
|
+
* and what `src/rasterizer/index.native.ts` does for Android too.
|
|
716
422
|
*/
|
|
717
423
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
718
424
|
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
@@ -720,23 +426,15 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(pendingRasterSlots) {
|
|
|
720
426
|
}
|
|
721
427
|
|
|
722
428
|
/**
|
|
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.
|
|
429
|
+
* The one place a live `jsi::Runtime` is handed to this module: called on the JS
|
|
430
|
+
* thread at module construction, before JS can call any method here.
|
|
729
431
|
*/
|
|
730
432
|
- (void)installJSIBindingsWithRuntime:(facebook::jsi::Runtime &)runtime
|
|
731
|
-
callInvoker:
|
|
732
|
-
|
|
733
|
-
callInvoker {
|
|
734
|
-
_hostFunctionInstalled = PdfCanvasSlotsInstall(&runtime);
|
|
433
|
+
callInvoker:(const std::shared_ptr<facebook::react::CallInvoker> &)callInvoker {
|
|
434
|
+
_hostFunctionInstalled = pdfcanvas::installTakePixels(&runtime);
|
|
735
435
|
if (!_hostFunctionInstalled) {
|
|
736
|
-
RCTLogWarn(
|
|
737
|
-
|
|
738
|
-
@"tile will cross the bridge as base64. That is correct but roughly 85x "
|
|
739
|
-
@"slower per tile.");
|
|
436
|
+
RCTLogWarn(@"[PdfCanvas] The JS runtime refused __pdfCanvasTakePixels, so every tile will "
|
|
437
|
+
@"cross the bridge as base64. That is correct but roughly 85x slower per tile.");
|
|
740
438
|
}
|
|
741
439
|
}
|
|
742
440
|
|