@reekon-tools/react-native-pdf-canvas 0.1.0 → 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 (129) 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/index.native.d.ts +5 -0
  19. package/dist/index.native.js +5 -0
  20. package/dist/index.web.d.ts +10 -0
  21. package/dist/index.web.js +10 -0
  22. package/dist/rasterizer/index.native.d.ts +8 -12
  23. package/dist/rasterizer/index.native.js +15 -21
  24. package/dist/rasterizer/native-bridge.d.ts +51 -75
  25. package/dist/rasterizer/native-bridge.js +49 -33
  26. package/dist/rasterizer/native.d.ts +45 -0
  27. package/dist/rasterizer/native.js +123 -0
  28. package/dist/react/usePdfDocument.d.ts +5 -5
  29. package/dist/react/usePdfLayer.d.ts +10 -1
  30. package/dist/react/usePdfLayer.js +10 -2
  31. package/dist/skia/index.native.d.ts +7 -3
  32. package/dist/skia/index.native.js +7 -3
  33. package/dist/skia/index.web.d.ts +4 -2
  34. package/dist/skia/index.web.js +4 -2
  35. package/dist/testing/scenes.d.ts +3 -3
  36. package/dist/testing/scenes.js +4 -4
  37. package/dist/types.d.ts +36 -42
  38. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  39. package/native/.clangd +5 -0
  40. package/native/CMakeLists.txt +89 -0
  41. package/native/core/include/pdfcanvas/document.h +90 -0
  42. package/native/core/include/pdfcanvas/error.h +47 -0
  43. package/native/core/include/pdfcanvas/jsi.h +18 -0
  44. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  45. package/native/core/include/pdfcanvas/library.h +28 -0
  46. package/native/core/include/pdfcanvas/pixels.h +92 -0
  47. package/native/core/include/pdfcanvas/service.h +87 -0
  48. package/native/core/include/pdfcanvas/slots.h +113 -0
  49. package/native/core/include/pdfcanvas/types.h +110 -0
  50. package/native/core/pdfcanvas-core.cmake +22 -0
  51. package/native/core/src/document.cpp +516 -0
  52. package/native/core/src/error.cpp +29 -0
  53. package/native/core/src/jsi.cpp +66 -0
  54. package/native/core/src/library.cpp +52 -0
  55. package/native/core/src/pixels.cpp +45 -0
  56. package/native/core/src/service.cpp +156 -0
  57. package/native/core/src/slots.cpp +139 -0
  58. package/native/tests/fixtures.cpp +451 -0
  59. package/native/tests/fixtures.h +96 -0
  60. package/native/tests/harness.h +122 -0
  61. package/native/tests/main.cpp +64 -0
  62. package/native/tests/pixels_util.h +126 -0
  63. package/native/tests/test_document.cpp +485 -0
  64. package/native/tests/test_service.cpp +130 -0
  65. package/native/tests/test_slots.cpp +250 -0
  66. package/package.json +38 -8
  67. package/react-native.config.js +5 -2
  68. package/scripts/fetch-pdfium.mjs +487 -0
  69. package/scripts/pdfium-manifest.json +46 -0
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  72. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  73. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  74. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  75. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  76. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  77. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  78. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  79. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  80. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  81. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  82. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  83. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  84. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  85. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  86. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  87. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  88. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  89. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  90. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  91. package/dist/rasterizer/android.d.ts +0 -121
  92. package/dist/rasterizer/android.js +0 -126
  93. package/dist/rasterizer/ios.d.ts +0 -84
  94. package/dist/rasterizer/ios.js +0 -155
  95. package/ios/Package.swift +0 -58
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  98. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  99. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  100. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  101. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  102. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  103. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  104. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  105. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  109. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  110. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  111. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  112. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  113. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  114. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  115. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  116. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  117. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  118. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  119. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  120. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  121. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  122. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  123. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  124. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  125. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  126. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  127. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  128. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  129. 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