@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.
Files changed (161) hide show
  1. package/PdfCanvas.podspec +80 -0
  2. package/README.md +1391 -0
  3. package/android/build.gradle +175 -0
  4. package/android/consumer-rules.pro +3 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +2 -0
  8. package/android/gradlew +248 -0
  9. package/android/gradlew.bat +98 -0
  10. package/android/settings.gradle +34 -0
  11. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
  12. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
  13. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
  14. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
  15. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
  16. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
  17. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
  18. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
  19. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
  20. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
  21. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
  22. package/android/src/jsi/cpp/CMakeLists.txt +33 -0
  23. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
  24. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
  25. package/android/src/main/AndroidManifest.xml +7 -0
  26. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
  27. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
  28. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
  29. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
  30. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
  31. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
  32. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
  33. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
  34. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
  35. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
  36. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
  37. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
  38. package/android/tools/compile-gate.sh +289 -0
  39. package/dist/cache.d.ts +161 -0
  40. package/dist/cache.js +389 -0
  41. package/dist/clock.d.ts +21 -0
  42. package/dist/clock.js +31 -0
  43. package/dist/controller.d.ts +131 -0
  44. package/dist/controller.js +1002 -0
  45. package/dist/geometry.d.ts +96 -0
  46. package/dist/geometry.js +248 -0
  47. package/dist/index.d.ts +52 -0
  48. package/dist/index.js +50 -0
  49. package/dist/index.native.d.ts +21 -0
  50. package/dist/index.native.js +23 -0
  51. package/dist/index.web.d.ts +24 -0
  52. package/dist/index.web.js +26 -0
  53. package/dist/layout.d.ts +78 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/peer-check.d.ts +83 -0
  56. package/dist/peer-check.js +183 -0
  57. package/dist/planner.d.ts +35 -0
  58. package/dist/planner.js +530 -0
  59. package/dist/policy.d.ts +14 -0
  60. package/dist/policy.js +168 -0
  61. package/dist/rasterizer/android.d.ts +121 -0
  62. package/dist/rasterizer/android.js +126 -0
  63. package/dist/rasterizer/buffer.d.ts +18 -0
  64. package/dist/rasterizer/buffer.js +18 -0
  65. package/dist/rasterizer/fake.d.ts +92 -0
  66. package/dist/rasterizer/fake.js +433 -0
  67. package/dist/rasterizer/index.d.ts +38 -0
  68. package/dist/rasterizer/index.js +69 -0
  69. package/dist/rasterizer/index.native.d.ts +19 -0
  70. package/dist/rasterizer/index.native.js +80 -0
  71. package/dist/rasterizer/index.web.d.ts +63 -0
  72. package/dist/rasterizer/index.web.js +75 -0
  73. package/dist/rasterizer/ingest.d.ts +180 -0
  74. package/dist/rasterizer/ingest.js +307 -0
  75. package/dist/rasterizer/ios.d.ts +84 -0
  76. package/dist/rasterizer/ios.js +155 -0
  77. package/dist/rasterizer/native-bridge.d.ts +261 -0
  78. package/dist/rasterizer/native-bridge.js +413 -0
  79. package/dist/rasterizer/web/client.d.ts +84 -0
  80. package/dist/rasterizer/web/client.js +359 -0
  81. package/dist/rasterizer/web/engine.d.ts +69 -0
  82. package/dist/rasterizer/web/engine.js +308 -0
  83. package/dist/rasterizer/web/index.d.ts +26 -0
  84. package/dist/rasterizer/web/index.js +21 -0
  85. package/dist/rasterizer/web/pdfium.d.ts +213 -0
  86. package/dist/rasterizer/web/pdfium.js +170 -0
  87. package/dist/rasterizer/web/protocol.d.ts +138 -0
  88. package/dist/rasterizer/web/protocol.js +37 -0
  89. package/dist/rasterizer/web/session.d.ts +106 -0
  90. package/dist/rasterizer/web/session.js +413 -0
  91. package/dist/rasterizer/web/worker.d.ts +44 -0
  92. package/dist/rasterizer/web/worker.js +76 -0
  93. package/dist/react/PdfContentView.d.ts +98 -0
  94. package/dist/react/PdfContentView.js +189 -0
  95. package/dist/react/index.d.ts +6 -0
  96. package/dist/react/index.js +3 -0
  97. package/dist/react/usePdfDocument.d.ts +134 -0
  98. package/dist/react/usePdfDocument.js +275 -0
  99. package/dist/react/usePdfLayer.d.ts +94 -0
  100. package/dist/react/usePdfLayer.js +388 -0
  101. package/dist/skia/index.d.ts +26 -0
  102. package/dist/skia/index.js +25 -0
  103. package/dist/skia/index.native.d.ts +14 -0
  104. package/dist/skia/index.native.js +16 -0
  105. package/dist/skia/index.web.d.ts +12 -0
  106. package/dist/skia/index.web.js +14 -0
  107. package/dist/skia/resolve.native.d.ts +18 -0
  108. package/dist/skia/resolve.native.js +22 -0
  109. package/dist/skia/resolve.web.d.ts +41 -0
  110. package/dist/skia/resolve.web.js +62 -0
  111. package/dist/skia/skia-api.d.ts +94 -0
  112. package/dist/skia/skia-api.js +113 -0
  113. package/dist/testing/index.d.ts +27 -0
  114. package/dist/testing/index.js +28 -0
  115. package/dist/testing/scenes.d.ts +100 -0
  116. package/dist/testing/scenes.js +267 -0
  117. package/dist/types.d.ts +611 -0
  118. package/dist/types.js +21 -0
  119. package/dist/web-init.d.ts +68 -0
  120. package/dist/web-init.js +64 -0
  121. package/ios/Package.swift +58 -0
  122. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
  123. package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
  124. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
  125. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
  126. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
  127. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
  128. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
  129. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
  130. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
  131. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
  132. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
  133. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
  134. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
  135. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
  136. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
  137. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
  138. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
  139. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
  140. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
  141. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
  142. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
  143. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
  144. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
  145. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
  146. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
  147. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
  148. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
  149. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
  150. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
  151. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
  152. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
  153. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
  154. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
  155. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
  156. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
  157. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
  158. package/package.json +144 -0
  159. package/react-native.config.js +40 -0
  160. package/scripts/check-skia-peer.mjs +86 -0
  161. 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