@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.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +68 -39
- package/README.md +213 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/index.native.d.ts +5 -0
- package/dist/index.native.js +5 -0
- package/dist/index.web.d.ts +10 -0
- package/dist/index.web.js +10 -0
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/skia/index.native.d.ts +7 -3
- package/dist/skia/index.native.js +7 -3
- package/dist/skia/index.web.d.ts +4 -2
- package/dist/skia/index.web.js +4 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +38 -8
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +487 -0
- package/scripts/pdfium-manifest.json +46 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -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
|