@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.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 (121) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/PdfCanvas.podspec +68 -39
  4. package/README.md +213 -1316
  5. package/android/build.gradle +67 -54
  6. package/android/consumer-rules.pro +7 -0
  7. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
  8. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
  9. package/android/src/main/cpp/CMakeLists.txt +54 -0
  10. package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
  11. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
  12. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
  13. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
  14. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
  15. package/android/tools/compile-gate.sh +81 -101
  16. package/dist/controller.d.ts +14 -0
  17. package/dist/controller.js +4 -3
  18. package/dist/rasterizer/index.native.d.ts +8 -12
  19. package/dist/rasterizer/index.native.js +15 -21
  20. package/dist/rasterizer/native-bridge.d.ts +51 -75
  21. package/dist/rasterizer/native-bridge.js +49 -33
  22. package/dist/rasterizer/native.d.ts +45 -0
  23. package/dist/rasterizer/native.js +123 -0
  24. package/dist/react/usePdfDocument.d.ts +5 -5
  25. package/dist/react/usePdfLayer.d.ts +10 -1
  26. package/dist/react/usePdfLayer.js +10 -2
  27. package/dist/testing/scenes.d.ts +3 -3
  28. package/dist/testing/scenes.js +4 -4
  29. package/dist/types.d.ts +36 -42
  30. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  31. package/native/.clangd +5 -0
  32. package/native/CMakeLists.txt +89 -0
  33. package/native/core/include/pdfcanvas/document.h +90 -0
  34. package/native/core/include/pdfcanvas/error.h +47 -0
  35. package/native/core/include/pdfcanvas/jsi.h +18 -0
  36. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  37. package/native/core/include/pdfcanvas/library.h +28 -0
  38. package/native/core/include/pdfcanvas/pixels.h +92 -0
  39. package/native/core/include/pdfcanvas/service.h +87 -0
  40. package/native/core/include/pdfcanvas/slots.h +113 -0
  41. package/native/core/include/pdfcanvas/types.h +110 -0
  42. package/native/core/pdfcanvas-core.cmake +22 -0
  43. package/native/core/src/document.cpp +516 -0
  44. package/native/core/src/error.cpp +29 -0
  45. package/native/core/src/jsi.cpp +66 -0
  46. package/native/core/src/library.cpp +52 -0
  47. package/native/core/src/pixels.cpp +45 -0
  48. package/native/core/src/service.cpp +156 -0
  49. package/native/core/src/slots.cpp +139 -0
  50. package/native/tests/fixtures.cpp +451 -0
  51. package/native/tests/fixtures.h +96 -0
  52. package/native/tests/harness.h +122 -0
  53. package/native/tests/main.cpp +64 -0
  54. package/native/tests/pixels_util.h +126 -0
  55. package/native/tests/test_document.cpp +485 -0
  56. package/native/tests/test_service.cpp +130 -0
  57. package/native/tests/test_slots.cpp +250 -0
  58. package/package.json +27 -7
  59. package/react-native.config.js +5 -2
  60. package/scripts/fetch-pdfium.mjs +487 -0
  61. package/scripts/pdfium-manifest.json +46 -0
  62. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  63. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  64. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  65. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  66. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  67. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  68. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  69. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  72. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  73. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  74. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  75. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  76. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  77. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  78. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  79. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  80. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  81. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  82. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  83. package/dist/rasterizer/android.d.ts +0 -121
  84. package/dist/rasterizer/android.js +0 -126
  85. package/dist/rasterizer/ios.d.ts +0 -84
  86. package/dist/rasterizer/ios.js +0 -155
  87. package/ios/Package.swift +0 -58
  88. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  89. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  90. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  91. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  92. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  93. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  94. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  95. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  98. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  99. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  100. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  101. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  102. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  103. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  104. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  105. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  109. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  110. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  111. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  112. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  113. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  114. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  115. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  116. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  117. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  118. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  119. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  120. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  121. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -0,0 +1,66 @@
1
+ #include "pdfcanvas/jsi.h"
2
+
3
+ #include "pdfcanvas/jsi_config.h"
4
+ #include "pdfcanvas/slots.h"
5
+
6
+ #if PDFCANVAS_HAS_JSI
7
+ #include <jsi/jsi.h>
8
+ #include <string>
9
+ #endif
10
+
11
+ namespace pdfcanvas {
12
+
13
+ #if PDFCANVAS_HAS_JSI
14
+
15
+ bool installTakePixels(void* runtimePointer) {
16
+ if (runtimePointer == nullptr) return false;
17
+ auto& runtime = *static_cast<facebook::jsi::Runtime*>(runtimePointer);
18
+ try {
19
+ auto name = facebook::jsi::PropNameID::forAscii(runtime, "__pdfCanvasTakePixels");
20
+ auto take = facebook::jsi::Function::createFromHostFunction(
21
+ runtime, name, 1,
22
+ [](facebook::jsi::Runtime& rt, const facebook::jsi::Value&,
23
+ const facebook::jsi::Value* args, size_t count) -> facebook::jsi::Value {
24
+ if (count < 1 || !args[0].isNumber()) {
25
+ throw facebook::jsi::JSError(
26
+ rt, "__pdfCanvasTakePixels(slot): slot must be a number.");
27
+ }
28
+ const auto slot = static_cast<int64_t>(args[0].asNumber());
29
+ std::shared_ptr<SlotBuffer> buffer = slots::take(slot);
30
+ if (buffer == nullptr) {
31
+ // Every slot is taken exactly once, so this means the render that
32
+ // filled it was torn down (a reload drains every slot) or the same
33
+ // slot was taken twice. Named rather than silently empty: an empty
34
+ // ArrayBuffer would reach Skia and draw a black tile.
35
+ throw facebook::jsi::JSError(
36
+ rt, "__pdfCanvasTakePixels: no raster is waiting in slot " +
37
+ std::to_string(slot) +
38
+ ". It was already taken, or the module was invalidated.");
39
+ }
40
+ // The ArrayBuffer BORROWS this buffer and the shared_ptr keeps it
41
+ // alive; the slot map has already dropped its reference, so JS is now
42
+ // the sole owner and the GC is what frees it.
43
+ return facebook::jsi::Value(
44
+ rt, facebook::jsi::ArrayBuffer(rt, std::move(buffer)));
45
+ });
46
+ runtime.global().setProperty(runtime, name, take);
47
+ return true;
48
+ } catch (...) {
49
+ // A runtime that refuses to install a host function is a runtime this
50
+ // package will not use. The JS side falls back to base64.
51
+ return false;
52
+ }
53
+ }
54
+
55
+ #else
56
+
57
+ bool installTakePixels(void* runtimePointer) {
58
+ // No JSI in this build. The honest answer, not a stub: there is no runtime
59
+ // to install into and nothing that could call the host function.
60
+ (void)runtimePointer;
61
+ return false;
62
+ }
63
+
64
+ #endif
65
+
66
+ } // namespace pdfcanvas
@@ -0,0 +1,52 @@
1
+ #include "pdfcanvas/library.h"
2
+
3
+ #include <mutex>
4
+
5
+ #include "fpdfview.h"
6
+
7
+ namespace pdfcanvas {
8
+
9
+ std::mutex& Library::mutex() {
10
+ static std::mutex m;
11
+ return m;
12
+ }
13
+
14
+ const char** Library::userFontPaths() {
15
+ #if defined(__ANDROID__)
16
+ // PDFium's Android font mapper scans /system/fonts on its own; these are the
17
+ // folders a vendor build may add, and passing them is harmless where the
18
+ // mapper ignores them.
19
+ static const char* paths[] = {"/system/fonts", "/system/font", "/data/fonts",
20
+ "/product/fonts", nullptr};
21
+ return paths;
22
+ #elif defined(__APPLE__)
23
+ // PDFium's Apple mapper adds /System/Library/Fonts itself. On iOS the
24
+ // system's own fonts sit in subfolders of it, and the folder scan is
25
+ // recursive, so a PDF that does not embed Helvetica gets the real one where
26
+ // the sandbox allows the read and PDFium's built-in Foxit Sans where it does
27
+ // not. Both are legible; only the former is pixel-identical to Preview.
28
+ static const char* paths[] = {"/System/Library/Fonts", "/Library/Fonts", nullptr};
29
+ return paths;
30
+ #else
31
+ return nullptr;
32
+ #endif
33
+ }
34
+
35
+ void Library::ensureInitialized() {
36
+ static std::once_flag once;
37
+ std::call_once(once, [] {
38
+ FPDF_LIBRARY_CONFIG config{};
39
+ // Version 2: the newest the header calls stable. m_pIsolate / the embedder
40
+ // slot only matter for a V8 build, which this is not.
41
+ config.version = 2;
42
+ config.m_pUserFontPaths = userFontPaths();
43
+ config.m_pIsolate = nullptr;
44
+ config.m_v8EmbedderSlot = 0;
45
+ FPDF_InitLibraryWithConfig(&config);
46
+ // FPDF_DestroyLibrary is deliberately never called: the library lives for
47
+ // the process, and tearing it down while a late render might still be
48
+ // holding a page is exactly the kind of exit-time crash nobody can debug.
49
+ });
50
+ }
51
+
52
+ } // namespace pdfcanvas
@@ -0,0 +1,45 @@
1
+ #include "pdfcanvas/pixels.h"
2
+ #include "pdfcanvas/types.h"
3
+
4
+ #include <cmath>
5
+ #include <limits>
6
+ #include <new>
7
+
8
+ namespace pdfcanvas {
9
+
10
+ namespace {
11
+
12
+ /// `llround`, which is round-half-away-from-zero — what JavaScript's
13
+ /// `Math.round` is for the positive values that reach here (a non-positive
14
+ /// extent is rejected before this is read). `(int)(v + 0.5)` differs at exactly
15
+ /// representable halves and is the version that silently disagrees with the
16
+ /// planner.
17
+ int pixelExtent(double points, double scale) {
18
+ if (!std::isfinite(points) || !std::isfinite(scale)) return 1;
19
+ const double product = points * scale;
20
+ if (product >= static_cast<double>((std::numeric_limits<int>::max)())) {
21
+ return (std::numeric_limits<int>::max)();
22
+ }
23
+ const long long rounded = std::llround(product);
24
+ return rounded < 1 ? 1 : static_cast<int>(rounded);
25
+ }
26
+
27
+ } // namespace
28
+
29
+ int RasterRequest::pixelWidth() const { return pixelExtent(width, scale); }
30
+ int RasterRequest::pixelHeight() const { return pixelExtent(height, scale); }
31
+
32
+ uint8_t* HeapSink::reserve(size_t length) {
33
+ if (reserved_ || length == 0) return nullptr;
34
+ reserved_ = true;
35
+ try {
36
+ // Value-initialised: zeroed, per the sink contract.
37
+ bytes_.assign(length, 0);
38
+ } catch (const std::bad_alloc&) {
39
+ bytes_.clear();
40
+ return nullptr;
41
+ }
42
+ return bytes_.data();
43
+ }
44
+
45
+ } // namespace pdfcanvas
@@ -0,0 +1,156 @@
1
+ #include "pdfcanvas/service.h"
2
+
3
+ #include <string>
4
+
5
+ #include "pdfcanvas/slots.h"
6
+
7
+ namespace pdfcanvas {
8
+
9
+ int64_t Service::signalKey(int handle, int64_t token) noexcept {
10
+ return (static_cast<int64_t>(handle) << 32) | (token & 0xffffffffLL);
11
+ }
12
+
13
+ Service::Opened Service::open(const Source& source) {
14
+ {
15
+ std::lock_guard<std::mutex> lock(mutex_);
16
+ if (invalidated_) {
17
+ throw PdfError(ErrorCode::BackendFailure, "The PdfCanvas module has been invalidated.");
18
+ }
19
+ }
20
+ // Opened OUTSIDE the service mutex: a parse of a large document must not
21
+ // block a cancel or a close of some other document.
22
+ std::shared_ptr<Document> document = Document::open(source);
23
+
24
+ std::lock_guard<std::mutex> lock(mutex_);
25
+ if (invalidated_) {
26
+ // Invalidated while opening. Nothing has been handed to a caller yet, so
27
+ // closing here is the whole cleanup.
28
+ document->close();
29
+ throw PdfError(ErrorCode::BackendFailure, "The PdfCanvas module was invalidated while opening.");
30
+ }
31
+ Opened opened;
32
+ opened.handle = nextHandle_++;
33
+ opened.pages = document->pages();
34
+ documents_.emplace(opened.handle, std::move(document));
35
+ return opened;
36
+ }
37
+
38
+ std::shared_ptr<Document> Service::documentFor(int handle) {
39
+ std::lock_guard<std::mutex> lock(mutex_);
40
+ auto found = documents_.find(handle);
41
+ return found == documents_.end() ? nullptr : found->second;
42
+ }
43
+
44
+ void Service::registerCancellation(int handle, int64_t token) {
45
+ std::lock_guard<std::mutex> lock(mutex_);
46
+ signals_[signalKey(handle, token)] = std::make_shared<Cancellation>();
47
+ }
48
+
49
+ RasterPixels Service::render(int handle, const RasterRequest& request, int64_t token,
50
+ bool useSlot) {
51
+ const int64_t key = signalKey(handle, token);
52
+ std::shared_ptr<Cancellation> signal;
53
+ std::shared_ptr<Document> document;
54
+ {
55
+ std::lock_guard<std::mutex> lock(mutex_);
56
+ auto found = signals_.find(key);
57
+ if (found != signals_.end()) {
58
+ signal = found->second;
59
+ } else {
60
+ // Not registered ahead of time: a caller that dispatched without the
61
+ // register step. It still gets a signal, so a cancel that arrives while
62
+ // this render runs is honoured.
63
+ signal = std::make_shared<Cancellation>();
64
+ signals_.emplace(key, signal);
65
+ }
66
+ auto doc = documents_.find(handle);
67
+ document = doc == documents_.end() ? nullptr : doc->second;
68
+ }
69
+
70
+ // Unregistered on EVERY path out, including a throw — otherwise a superseded
71
+ // epoch, which produces a stream of cancelled renders, grows the map for the
72
+ // life of the module.
73
+ struct Unregister {
74
+ Service* service;
75
+ int64_t key;
76
+ ~Unregister() {
77
+ std::lock_guard<std::mutex> lock(service->mutex_);
78
+ service->signals_.erase(key);
79
+ }
80
+ } unregister{this, key};
81
+
82
+ if (document == nullptr) {
83
+ throw PdfError(ErrorCode::BackendFailure,
84
+ "No open document for handle " + std::to_string(handle) + ".");
85
+ }
86
+
87
+ // Read ONCE per render: whether to reach for a slot. The sink may still
88
+ // refuse, in which case the raster comes back heap-backed and the caller
89
+ // branches on where the pixels ARE, not on what was asked for.
90
+ if (useSlot) {
91
+ SlotSink sink;
92
+ return document->render(request, signal.get(), &sink);
93
+ }
94
+ return document->render(request, signal.get(), nullptr);
95
+ }
96
+
97
+ void Service::cancel(int handle, int64_t token) {
98
+ std::shared_ptr<Cancellation> signal;
99
+ {
100
+ std::lock_guard<std::mutex> lock(mutex_);
101
+ auto found = signals_.find(signalKey(handle, token));
102
+ if (found == signals_.end()) return;
103
+ signal = found->second;
104
+ }
105
+ // A pure lookup, deliberately not a speculative insert. "No signal
106
+ // registered" is far more commonly a cancel that arrived AFTER its render
107
+ // finished than one that beat its render here, and planting an entry for
108
+ // each would grow the map forever.
109
+ signal->cancel();
110
+ }
111
+
112
+ void Service::close(int handle) {
113
+ std::shared_ptr<Document> document;
114
+ {
115
+ std::lock_guard<std::mutex> lock(mutex_);
116
+ auto found = documents_.find(handle);
117
+ if (found == documents_.end()) return;
118
+ document = std::move(found->second);
119
+ documents_.erase(found);
120
+ }
121
+ // Outside the service mutex: this blocks until an in-flight render on the
122
+ // document releases the library lock, and a cancel for that render must be
123
+ // able to get in meanwhile.
124
+ document->close();
125
+ }
126
+
127
+ void Service::invalidate() {
128
+ std::vector<std::shared_ptr<Document>> open;
129
+ {
130
+ std::lock_guard<std::mutex> lock(mutex_);
131
+ invalidated_ = true;
132
+ for (auto& entry : documents_) open.push_back(std::move(entry.second));
133
+ documents_.clear();
134
+ // Every in-flight render observes its signal at the next checkpoint and
135
+ // stops, so `close()` below waits for at most one progressive batch each.
136
+ for (auto& entry : signals_) entry.second->cancel();
137
+ signals_.clear();
138
+ }
139
+ for (auto& document : open) document->close();
140
+ // A reload also strands any slot whose promise resolved but whose JS
141
+ // continuation never ran. Each one is a whole raster — megabytes — held in
142
+ // C++ with nothing left that could ever take it.
143
+ slots::releaseAll();
144
+ }
145
+
146
+ bool Service::isInvalidated() const {
147
+ std::lock_guard<std::mutex> lock(mutex_);
148
+ return invalidated_;
149
+ }
150
+
151
+ size_t Service::openCount() const {
152
+ std::lock_guard<std::mutex> lock(mutex_);
153
+ return documents_.size();
154
+ }
155
+
156
+ } // namespace pdfcanvas
@@ -0,0 +1,139 @@
1
+ #include "pdfcanvas/slots.h"
2
+
3
+ #include <atomic>
4
+ #include <cstring>
5
+ #include <mutex>
6
+ #include <new>
7
+ #include <unordered_map>
8
+
9
+ namespace pdfcanvas {
10
+
11
+ namespace {
12
+
13
+ /// Slots are published on a render worker and taken on the JS thread, so the
14
+ /// map is guarded. The lock is held only around the map operation itself —
15
+ /// never across an allocation, never across a fill, never across a JSI call.
16
+ std::mutex& slotsMutex() {
17
+ static std::mutex m;
18
+ return m;
19
+ }
20
+
21
+ std::unordered_map<int64_t, std::shared_ptr<SlotBuffer>>& slotMap() {
22
+ static std::unordered_map<int64_t, std::shared_ptr<SlotBuffer>> map;
23
+ return map;
24
+ }
25
+
26
+ /// Slot ids, from one counter per PROCESS, starting at 1 because 0 is the
27
+ /// heap-backed sentinel on the wire. Process-wide rather than per-module for
28
+ /// the reason both retired stores gave: React Native builds a fresh set of
29
+ /// native modules on every reload while the outgoing one may still have
30
+ /// rasters parked, and a per-module counter would hand the same id to two
31
+ /// modules — a wrong image, not an error.
32
+ std::atomic<int64_t> g_nextSlot{1};
33
+
34
+ void publishBuffer(int64_t slot, std::shared_ptr<SlotBuffer> buffer) {
35
+ std::lock_guard<std::mutex> lock(slotsMutex());
36
+ // Assignment, not emplace-and-check: the id came from the process-wide
37
+ // counter a moment ago, so a collision is unrepresentable.
38
+ slotMap()[slot] = std::move(buffer);
39
+ }
40
+
41
+ } // namespace
42
+
43
+ namespace slots {
44
+
45
+ const std::array<uint8_t, 8>& probeBytes() {
46
+ // Chosen, not arbitrary. 0x00 and 0xFF pin both ends of the range, and
47
+ // 0xFD/0xFE/0xFF sit above 0x7F where a signed-byte slip in a Java copy
48
+ // would show up.
49
+ static const std::array<uint8_t, 8> bytes{0x00, 0x01, 0x02, 0xFD,
50
+ 0xFE, 0xFF, 0x2A, 0x07};
51
+ return bytes;
52
+ }
53
+
54
+ int64_t storeProbe() {
55
+ SlotSink sink;
56
+ uint8_t* bytes = sink.reserve(probeBytes().size());
57
+ if (bytes == nullptr) return -1;
58
+ std::memcpy(bytes, probeBytes().data(), probeBytes().size());
59
+ return sink.publish();
60
+ }
61
+
62
+ std::shared_ptr<SlotBuffer> take(int64_t slot) {
63
+ std::lock_guard<std::mutex> lock(slotsMutex());
64
+ auto& map = slotMap();
65
+ auto found = map.find(slot);
66
+ if (found == map.end()) return nullptr;
67
+ std::shared_ptr<SlotBuffer> buffer = std::move(found->second);
68
+ map.erase(found);
69
+ return buffer;
70
+ }
71
+
72
+ bool release(int64_t slot) {
73
+ // Freed outside the lock, by `take`'s returned shared_ptr going out of scope
74
+ // here rather than inside the critical section.
75
+ return take(slot) != nullptr;
76
+ }
77
+
78
+ void releaseAll() {
79
+ std::unordered_map<int64_t, std::shared_ptr<SlotBuffer>> drained;
80
+ {
81
+ std::lock_guard<std::mutex> lock(slotsMutex());
82
+ drained.swap(slotMap());
83
+ }
84
+ // Dropping several megabytes of buffers is not something to do while the JS
85
+ // thread might be waiting on the same mutex.
86
+ }
87
+
88
+ size_t count() {
89
+ std::lock_guard<std::mutex> lock(slotsMutex());
90
+ return slotMap().size();
91
+ }
92
+
93
+ } // namespace slots
94
+
95
+ /* ------------------------------------------------------------------ *
96
+ * SlotSink
97
+ * ------------------------------------------------------------------ */
98
+
99
+ uint8_t* SlotSink::reserve(size_t length) {
100
+ if (reserved_ || length == 0) return nullptr;
101
+ reserved_ = true;
102
+ try {
103
+ buffer_ = std::make_shared<SlotBuffer>(length);
104
+ } catch (const std::bad_alloc&) {
105
+ // Reported as a refusal rather than propagated: the rasterizer falls
106
+ // straight through to a heap sink and the tile still goes out.
107
+ buffer_ = nullptr;
108
+ return nullptr;
109
+ }
110
+ // Allocated next to the map it goes into, so a collision at publish cannot
111
+ // happen. Held HERE and nowhere else until publish — see the header.
112
+ slot_ = g_nextSlot.fetch_add(1, std::memory_order_relaxed);
113
+ return buffer_->data();
114
+ }
115
+
116
+ int64_t SlotSink::publish() {
117
+ if (buffer_ == nullptr || published_) return 0;
118
+ publishBuffer(slot_, buffer_);
119
+ published_ = true;
120
+ // The map holds the only reference this object needs to care about from
121
+ // here on: a successful take is then the sole owner, so the buffer dies with
122
+ // the JS ArrayBuffer rather than with this sink.
123
+ buffer_ = nullptr;
124
+ return slot_;
125
+ }
126
+
127
+ void SlotSink::abandon() {
128
+ if (published_) {
129
+ // Already in the map. Taking it back out is what frees it — and finding
130
+ // nothing is the normal case when JS has already redeemed it.
131
+ slots::take(slot_);
132
+ published_ = false;
133
+ slot_ = 0;
134
+ return;
135
+ }
136
+ buffer_ = nullptr;
137
+ }
138
+
139
+ } // namespace pdfcanvas