@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.
- 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/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +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,130 @@
|
|
|
1
|
+
// The facade both bindings marshal to.
|
|
2
|
+
|
|
3
|
+
#include <thread>
|
|
4
|
+
|
|
5
|
+
#include "fixtures.h"
|
|
6
|
+
#include "harness.h"
|
|
7
|
+
#include "pdfcanvas/service.h"
|
|
8
|
+
#include "pdfcanvas/slots.h"
|
|
9
|
+
#include "pixels_util.h"
|
|
10
|
+
|
|
11
|
+
using namespace pdfcanvas;
|
|
12
|
+
|
|
13
|
+
namespace {
|
|
14
|
+
|
|
15
|
+
Source fileSource(const fixtures::Bytes& bytes, const std::string& name) {
|
|
16
|
+
Source source;
|
|
17
|
+
source.path = fixtures::writeTemp(bytes, name);
|
|
18
|
+
return source;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
} // namespace
|
|
22
|
+
|
|
23
|
+
TEST(serviceOpensRendersAndCloses) {
|
|
24
|
+
Service service;
|
|
25
|
+
const auto opened = service.open(fileSource(fixtures::quadrants(200, 100), "svc-open.pdf"));
|
|
26
|
+
CHECK(opened.handle > 0);
|
|
27
|
+
CHECK_EQ(opened.pages.size(), size_t{1});
|
|
28
|
+
CHECK_NEAR(opened.pages[0].width, 200, 0.01);
|
|
29
|
+
CHECK_EQ(service.openCount(), size_t{1});
|
|
30
|
+
|
|
31
|
+
auto raster = service.render(opened.handle, px::request(0, 0, 0, 200, 100, 1), 1, false);
|
|
32
|
+
CHECK_EQ(raster.width, 200);
|
|
33
|
+
CHECK_PIXEL(raster, 10, 90, px::kRed);
|
|
34
|
+
|
|
35
|
+
service.close(opened.handle);
|
|
36
|
+
CHECK_EQ(service.openCount(), size_t{0});
|
|
37
|
+
EXPECT_ERROR(service.render(opened.handle, px::request(0, 0, 0, 10, 10, 1), 2, false),
|
|
38
|
+
ErrorCode::BackendFailure);
|
|
39
|
+
service.close(opened.handle); // idempotent
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
TEST(handlesAreDistinctAndIndependent) {
|
|
43
|
+
Service service;
|
|
44
|
+
const auto a = service.open(fileSource(fixtures::coloredPages({{1, 0, 0}}, 40, 40), "svc-a.pdf"));
|
|
45
|
+
const auto b = service.open(fileSource(fixtures::coloredPages({{0, 0, 1}}, 40, 40), "svc-b.pdf"));
|
|
46
|
+
CHECK(a.handle != b.handle);
|
|
47
|
+
CHECK_PIXEL(service.render(a.handle, px::request(0, 0, 0, 40, 40, 1), 1, false), 20, 20, px::kRed);
|
|
48
|
+
CHECK_PIXEL(service.render(b.handle, px::request(0, 0, 0, 40, 40, 1), 1, false), 20, 20, px::kBlue);
|
|
49
|
+
service.close(a.handle);
|
|
50
|
+
CHECK_PIXEL(service.render(b.handle, px::request(0, 0, 0, 40, 40, 1), 2, false), 20, 20, px::kBlue);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
TEST(serviceRendersIntoASlotWhenAsked) {
|
|
54
|
+
slots::releaseAll();
|
|
55
|
+
Service service;
|
|
56
|
+
const auto opened = service.open(fileSource(fixtures::grid(100, 100), "svc-slot.pdf"));
|
|
57
|
+
auto heap = service.render(opened.handle, px::request(0, 0, 0, 100, 100, 2), 1, false);
|
|
58
|
+
auto slotted = service.render(opened.handle, px::request(0, 0, 0, 100, 100, 2), 2, true);
|
|
59
|
+
CHECK(!heap.inSlot());
|
|
60
|
+
CHECK(slotted.inSlot());
|
|
61
|
+
auto buffer = slots::take(slotted.slot);
|
|
62
|
+
REQUIRE(buffer != nullptr);
|
|
63
|
+
CHECK(std::vector<uint8_t>(buffer->data(), buffer->data() + buffer->size()) == heap.bytes);
|
|
64
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
TEST(aRegisteredCancellationIsHonouredAndCleanedUp) {
|
|
68
|
+
Service service;
|
|
69
|
+
const auto opened = service.open(fileSource(fixtures::quadrants(40, 40), "svc-cancel.pdf"));
|
|
70
|
+
service.registerCancellation(opened.handle, 7);
|
|
71
|
+
service.cancel(opened.handle, 7);
|
|
72
|
+
EXPECT_ERROR(service.render(opened.handle, px::request(0, 0, 0, 40, 40, 1), 7, false), ErrorCode::Cancelled);
|
|
73
|
+
// The signal was consumed: the same token renders fine afterwards.
|
|
74
|
+
CHECK_EQ(service.render(opened.handle, px::request(0, 0, 0, 40, 40, 1), 7, false).width, 40);
|
|
75
|
+
// A cancel for a token that was never registered is a no-op, not an insert.
|
|
76
|
+
service.cancel(opened.handle, 999);
|
|
77
|
+
service.cancel(12345, 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
TEST(cancelReachesARenderInFlightOnAnotherThread) {
|
|
81
|
+
Service service;
|
|
82
|
+
const auto opened = service.open(
|
|
83
|
+
fileSource(fixtures::grid(fixtures::kArchDW, fixtures::kArchDH, 4), "svc-cancel-inflight.pdf"));
|
|
84
|
+
service.registerCancellation(opened.handle, 1);
|
|
85
|
+
bool cancelled = false;
|
|
86
|
+
bool finished = false;
|
|
87
|
+
std::thread worker([&] {
|
|
88
|
+
try {
|
|
89
|
+
(void)service.render(opened.handle,
|
|
90
|
+
px::request(0, 0, 0, fixtures::kArchDW, fixtures::kArchDH, 3), 1, false);
|
|
91
|
+
finished = true;
|
|
92
|
+
} catch (const PdfError& error) {
|
|
93
|
+
cancelled = error.code() == ErrorCode::Cancelled;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
97
|
+
service.cancel(opened.handle, 1);
|
|
98
|
+
worker.join();
|
|
99
|
+
// Either the render was quick enough to finish, or it was cancelled mid-draw;
|
|
100
|
+
// what must not happen is a third outcome.
|
|
101
|
+
CHECK(cancelled || finished);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
TEST(invalidateClosesEverythingAndRefusesAfterwards) {
|
|
105
|
+
slots::releaseAll();
|
|
106
|
+
Service service;
|
|
107
|
+
const auto a = service.open(fileSource(fixtures::quadrants(40, 40), "svc-inv-a.pdf"));
|
|
108
|
+
const auto b = service.open(fileSource(fixtures::quadrants(40, 40), "svc-inv-b.pdf"));
|
|
109
|
+
(void)service.render(a.handle, px::request(0, 0, 0, 40, 40, 1), 1, true);
|
|
110
|
+
CHECK_EQ(slots::count(), size_t{1});
|
|
111
|
+
CHECK_EQ(service.openCount(), size_t{2});
|
|
112
|
+
|
|
113
|
+
service.invalidate();
|
|
114
|
+
CHECK(service.isInvalidated());
|
|
115
|
+
CHECK_EQ(service.openCount(), size_t{0});
|
|
116
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
117
|
+
EXPECT_ERROR(service.render(b.handle, px::request(0, 0, 0, 40, 40, 1), 2, false), ErrorCode::BackendFailure);
|
|
118
|
+
EXPECT_ERROR(service.open(fileSource(fixtures::quadrants(40, 40), "svc-inv-c.pdf")), ErrorCode::BackendFailure);
|
|
119
|
+
service.invalidate(); // idempotent
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
TEST(openErrorsCarryTheirCodeThroughTheService) {
|
|
123
|
+
Service service;
|
|
124
|
+
Source missing;
|
|
125
|
+
missing.path = fixtures::tempDir() + "/nope.pdf";
|
|
126
|
+
EXPECT_ERROR(service.open(missing), ErrorCode::NotFound);
|
|
127
|
+
Source locked = fileSource(fixtures::encrypted(), "svc-locked.pdf");
|
|
128
|
+
EXPECT_ERROR(service.open(locked), ErrorCode::PasswordRequired);
|
|
129
|
+
CHECK_EQ(service.openCount(), size_t{0});
|
|
130
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// The REAL slot store — the storage the device runs, not a double.
|
|
2
|
+
//
|
|
3
|
+
// Every failure mode here is a leak or a use-after-free rather than a wrong
|
|
4
|
+
// pixel, so a device session does not show any of them: 48 tiles rendered
|
|
5
|
+
// correctly with the store leaking every superseded raster would look exactly
|
|
6
|
+
// like 48 tiles rendered correctly.
|
|
7
|
+
|
|
8
|
+
#include <cstring>
|
|
9
|
+
#include <set>
|
|
10
|
+
#include <thread>
|
|
11
|
+
#include <vector>
|
|
12
|
+
|
|
13
|
+
#include "fixtures.h"
|
|
14
|
+
#include "harness.h"
|
|
15
|
+
#include "pdfcanvas/document.h"
|
|
16
|
+
#include "pdfcanvas/jsi.h"
|
|
17
|
+
#include "pdfcanvas/slots.h"
|
|
18
|
+
#include "pixels_util.h"
|
|
19
|
+
|
|
20
|
+
using namespace pdfcanvas;
|
|
21
|
+
|
|
22
|
+
namespace {
|
|
23
|
+
|
|
24
|
+
struct DrainStore {
|
|
25
|
+
DrainStore() { slots::releaseAll(); }
|
|
26
|
+
~DrainStore() { slots::releaseAll(); }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
std::vector<uint8_t> takeBytes(int64_t slot) {
|
|
30
|
+
auto buffer = slots::take(slot);
|
|
31
|
+
if (buffer == nullptr) return {};
|
|
32
|
+
return std::vector<uint8_t>(buffer->data(), buffer->data() + buffer->size());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
} // namespace
|
|
36
|
+
|
|
37
|
+
TEST(probeBytesMatchTheSharedList) {
|
|
38
|
+
const std::array<uint8_t, 8> expected{0x00, 0x01, 0x02, 0xFD, 0xFE, 0xFF, 0x2A, 0x07};
|
|
39
|
+
CHECK(slots::probeBytes() == expected);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
TEST(theProbeRoundTrips) {
|
|
43
|
+
DrainStore drain;
|
|
44
|
+
const int64_t slot = slots::storeProbe();
|
|
45
|
+
CHECK(slot > 0);
|
|
46
|
+
CHECK_EQ(slots::count(), size_t{1});
|
|
47
|
+
const auto taken = takeBytes(slot);
|
|
48
|
+
CHECK(std::equal(taken.begin(), taken.end(), slots::probeBytes().begin(), slots::probeBytes().end()));
|
|
49
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
TEST(aSlotIsTakenExactlyOnce) {
|
|
53
|
+
DrainStore drain;
|
|
54
|
+
const int64_t slot = slots::storeProbe();
|
|
55
|
+
CHECK(slots::take(slot) != nullptr);
|
|
56
|
+
CHECK(slots::take(slot) == nullptr);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
TEST(slotIdsAreMonotonicAndNeverReused) {
|
|
60
|
+
DrainStore drain;
|
|
61
|
+
std::vector<int64_t> ids;
|
|
62
|
+
for (int i = 0; i < 8; i++) ids.push_back(slots::storeProbe());
|
|
63
|
+
for (size_t i = 1; i < ids.size(); i++) CHECK(ids[i] > ids[i - 1]);
|
|
64
|
+
const int64_t first = ids.front();
|
|
65
|
+
slots::release(first);
|
|
66
|
+
CHECK(slots::storeProbe() > ids.back());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
TEST(releaseDropsOneSlotAndIsIdempotent) {
|
|
70
|
+
DrainStore drain;
|
|
71
|
+
const int64_t a = slots::storeProbe();
|
|
72
|
+
const int64_t b = slots::storeProbe();
|
|
73
|
+
CHECK_EQ(slots::count(), size_t{2});
|
|
74
|
+
CHECK(slots::release(a));
|
|
75
|
+
CHECK(!slots::release(a));
|
|
76
|
+
CHECK_EQ(slots::count(), size_t{1});
|
|
77
|
+
CHECK(slots::take(b) != nullptr);
|
|
78
|
+
CHECK(!slots::release(0));
|
|
79
|
+
CHECK(!slots::release(-1));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
TEST(releaseAllDrainsTheStore) {
|
|
83
|
+
DrainStore drain;
|
|
84
|
+
for (int i = 0; i < 16; i++) slots::storeProbe();
|
|
85
|
+
CHECK_EQ(slots::count(), size_t{16});
|
|
86
|
+
slots::releaseAll();
|
|
87
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
88
|
+
slots::releaseAll();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// RESERVE DOES NOT PUBLISH — the safety property of the whole store. A buffer
|
|
92
|
+
/// reachable from the map before it is filled can be freed by `releaseAll()` on
|
|
93
|
+
/// another thread while PDFium is still writing into it.
|
|
94
|
+
TEST(aReservedBufferIsNotInTheStoreUntilPublished) {
|
|
95
|
+
DrainStore drain;
|
|
96
|
+
SlotSink sink;
|
|
97
|
+
uint8_t* bytes = sink.reserve(4096);
|
|
98
|
+
REQUIRE(bytes != nullptr);
|
|
99
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
100
|
+
slots::releaseAll();
|
|
101
|
+
std::memset(bytes, 0xAB, 4096);
|
|
102
|
+
const int64_t slot = sink.publish();
|
|
103
|
+
CHECK(slot > 0);
|
|
104
|
+
CHECK_EQ(slots::count(), size_t{1});
|
|
105
|
+
const auto taken = takeBytes(slot);
|
|
106
|
+
CHECK_EQ(taken.size(), size_t{4096});
|
|
107
|
+
CHECK_EQ(static_cast<int>(taken.front()), 0xAB);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
TEST(reservedMemoryIsZeroed) {
|
|
111
|
+
SlotSink sink;
|
|
112
|
+
uint8_t* bytes = sink.reserve(8192);
|
|
113
|
+
REQUIRE(bytes != nullptr);
|
|
114
|
+
bool allZero = true;
|
|
115
|
+
for (size_t i = 0; i < 8192; i++) allZero = allZero && bytes[i] == 0;
|
|
116
|
+
CHECK(allZero);
|
|
117
|
+
sink.abandon();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
TEST(aSinkRefusesASecondOrEmptyReservation) {
|
|
121
|
+
DrainStore drain;
|
|
122
|
+
SlotSink sink;
|
|
123
|
+
CHECK(sink.reserve(1024) != nullptr);
|
|
124
|
+
CHECK(sink.reserve(1024) == nullptr);
|
|
125
|
+
SlotSink empty;
|
|
126
|
+
CHECK(empty.reserve(0) == nullptr);
|
|
127
|
+
CHECK_EQ(empty.publish(), int64_t{0});
|
|
128
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
129
|
+
sink.abandon();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
TEST(abandonAfterPublishTakesItBackOut) {
|
|
133
|
+
DrainStore drain;
|
|
134
|
+
SlotSink sink;
|
|
135
|
+
REQUIRE(sink.reserve(64) != nullptr);
|
|
136
|
+
const int64_t slot = sink.publish();
|
|
137
|
+
CHECK_EQ(slots::count(), size_t{1});
|
|
138
|
+
sink.abandon();
|
|
139
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
140
|
+
CHECK(slots::take(slot) == nullptr);
|
|
141
|
+
sink.abandon(); // idempotent
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// THE HEADLINE: bytes taken out of a slot are byte-identical to the same
|
|
145
|
+
/// raster rendered to the heap. PDFium is handed a pointer into the slot buffer
|
|
146
|
+
/// and writes the tile in place, so if the buffer, stride or publish were wrong
|
|
147
|
+
/// the result would be a plausible-looking wrong image rather than an error.
|
|
148
|
+
TEST(slotBytesAreIdenticalToTheHeapPathForARealRaster) {
|
|
149
|
+
DrainStore drain;
|
|
150
|
+
Source source;
|
|
151
|
+
source.path = fixtures::writeTemp(fixtures::grid(400, 300), "slots-fidelity.pdf");
|
|
152
|
+
auto document = Document::open(source);
|
|
153
|
+
const auto req = px::request(0, 37, 23, 64, 41, 4);
|
|
154
|
+
auto heap = document->render(req, nullptr, nullptr);
|
|
155
|
+
CHECK(!heap.inSlot());
|
|
156
|
+
|
|
157
|
+
SlotSink sink;
|
|
158
|
+
auto slotted = document->render(req, nullptr, &sink);
|
|
159
|
+
CHECK(slotted.inSlot());
|
|
160
|
+
CHECK(slotted.bytes.empty());
|
|
161
|
+
CHECK_EQ(slotted.width, heap.width);
|
|
162
|
+
CHECK_EQ(slotted.height, heap.height);
|
|
163
|
+
CHECK_EQ(slotted.rowBytes, heap.rowBytes);
|
|
164
|
+
CHECK_EQ(slots::count(), size_t{1});
|
|
165
|
+
const auto taken = takeBytes(slotted.slot);
|
|
166
|
+
CHECK_EQ(taken.size(), heap.bytes.size());
|
|
167
|
+
CHECK(taken == heap.bytes);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
TEST(slotBytesMatchTheHeapPathOnATransparentBackground) {
|
|
171
|
+
DrainStore drain;
|
|
172
|
+
Source source;
|
|
173
|
+
source.path = fixtures::writeTemp(fixtures::page({0, 0, 100, 100}, nullptr, 0, "1 0 0 rg 0 0 40 40 re f\n"),
|
|
174
|
+
"slots-transparent.pdf");
|
|
175
|
+
auto document = Document::open(source);
|
|
176
|
+
const auto req = px::request(0, 0, 0, 100, 100, 2, false, Background::Transparent);
|
|
177
|
+
auto heap = document->render(req, nullptr, nullptr);
|
|
178
|
+
SlotSink sink;
|
|
179
|
+
auto slotted = document->render(req, nullptr, &sink);
|
|
180
|
+
const auto taken = takeBytes(slotted.slot);
|
|
181
|
+
CHECK(taken == heap.bytes);
|
|
182
|
+
const size_t corner = 10 * static_cast<size_t>(heap.rowBytes) + 190 * 4;
|
|
183
|
+
CHECK_EQ(static_cast<int>(taken[corner + 3]), 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// A raster the render ABANDONS leaves nothing in the store: the path is a
|
|
187
|
+
/// cancellation observed after the draw finished.
|
|
188
|
+
TEST(anAbandonedRasterLeavesNoSlotBehind) {
|
|
189
|
+
DrainStore drain;
|
|
190
|
+
Source source;
|
|
191
|
+
source.path = fixtures::writeTemp(fixtures::grid(200, 200), "slots-abandon.pdf");
|
|
192
|
+
auto document = Document::open(source);
|
|
193
|
+
Cancellation signal;
|
|
194
|
+
// Enough polls to get through the draw, then the post-render check sees it.
|
|
195
|
+
signal.cancelAfterPolls(1);
|
|
196
|
+
SlotSink sink;
|
|
197
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 200, 200, 2), &signal, &sink), ErrorCode::Cancelled);
|
|
198
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// A sink that REFUSES falls the render through to the heap: a slower correct
|
|
202
|
+
/// tile beats a lost one.
|
|
203
|
+
TEST(aRefusedReservationFallsBackToTheHeap) {
|
|
204
|
+
struct Refusing final : PixelSink {
|
|
205
|
+
uint8_t* reserve(size_t) override { return nullptr; }
|
|
206
|
+
int64_t publish() override { return 0; }
|
|
207
|
+
void abandon() override {}
|
|
208
|
+
} refusing;
|
|
209
|
+
Source source;
|
|
210
|
+
source.path = fixtures::writeTemp(fixtures::quadrants(40, 40), "slots-refuse.pdf");
|
|
211
|
+
auto document = Document::open(source);
|
|
212
|
+
auto raster = document->render(px::request(0, 0, 0, 40, 40, 1), nullptr, &refusing);
|
|
213
|
+
CHECK(!raster.inSlot());
|
|
214
|
+
CHECK_EQ(raster.bytes.size(), size_t{40 * 40 * 4});
|
|
215
|
+
CHECK_PIXEL(raster, 5, 35, px::kRed);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
TEST(concurrentPublishAndTakeKeepTheirOwnBytes) {
|
|
219
|
+
DrainStore drain;
|
|
220
|
+
std::vector<int64_t> ids(64, 0);
|
|
221
|
+
std::vector<std::thread> workers;
|
|
222
|
+
for (int i = 0; i < 64; i++) {
|
|
223
|
+
workers.emplace_back([i, &ids] {
|
|
224
|
+
SlotSink sink;
|
|
225
|
+
uint8_t* bytes = sink.reserve(1024);
|
|
226
|
+
if (bytes == nullptr) return;
|
|
227
|
+
std::memset(bytes, i & 0xFF, 1024);
|
|
228
|
+
ids[static_cast<size_t>(i)] = sink.publish();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
for (auto& worker : workers) worker.join();
|
|
232
|
+
CHECK_EQ(std::set<int64_t>(ids.begin(), ids.end()).size(), size_t{64});
|
|
233
|
+
CHECK_EQ(slots::count(), size_t{64});
|
|
234
|
+
int good = 0;
|
|
235
|
+
for (int i = 0; i < 64; i++) {
|
|
236
|
+
const auto taken = takeBytes(ids[static_cast<size_t>(i)]);
|
|
237
|
+
if (taken.size() == 1024 && taken.front() == static_cast<uint8_t>(i & 0xFF)) good++;
|
|
238
|
+
}
|
|
239
|
+
CHECK_EQ(good, 64);
|
|
240
|
+
CHECK_EQ(slots::count(), size_t{0});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// `installTakePixels` reports false in this build, honestly. This is the
|
|
244
|
+
/// configuration that lets every test above run, and a `true` would be a lie
|
|
245
|
+
/// that made the JS side arm a transport it cannot redeem.
|
|
246
|
+
TEST(installDeclinesWithoutAJsiRuntime) {
|
|
247
|
+
CHECK(!installTakePixels(nullptr));
|
|
248
|
+
int dummy = 0;
|
|
249
|
+
CHECK(!installTakePixels(&dummy));
|
|
250
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reekon-tools/react-native-pdf-canvas",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "PDF pages as Skia images, drawn at exact doc-space rects inside somebody else's transform. Owns no canvas, no gestures, no viewport state.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "PDF pages as Skia images, drawn at exact doc-space rects inside somebody else's transform. One PDFium engine on iOS, Android and web. Owns no canvas, no gestures, no viewport state.",
|
|
5
5
|
"author": "REEKON Tools",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://gitlab.com/reekon-tools/software/rock-pro/react-native-pdf-canvas.git"
|
|
@@ -24,15 +24,29 @@
|
|
|
24
24
|
"./src/skia/index.native.ts"
|
|
25
25
|
],
|
|
26
26
|
"//files-scripts": "`scripts` ships because the peer-floor check runs from a CONSUMER'S postinstall, before/without any build of `dist`.",
|
|
27
|
-
"//files": "`*.podspec` SHIPS OR iOS IS SILENTLY ABSENT
|
|
27
|
+
"//files": "`*.podspec` SHIPS OR iOS IS SILENTLY ABSENT: autolinking reads the podspec out of the INSTALLED package. `native` is the C++ core both bindings compile (native/core), its host test suite (native/tests) and the host CMake project — but NOT native/.pdfium (the fetched PDFium binaries, gitignored and re-fetched by scripts/fetch-pdfium.mjs on every platform) and NOT native/build. `scripts` ships because the peer-floor check and the PDFium fetch both run from a CONSUMER'S install: the fetch from build.gradle and from the podspec, before/without any build of `dist`. THE ANDROID ENTRIES ARE ENUMERATED RATHER THAN A BARE `android`, and that is load-bearing: a `files` entry is an ALLOWLIST that beats .gitignore, so `android` swept in build/, .cxx/, .gradle/ and local.properties (this machine's sdk.dir) the moment anyone had run a Gradle build or the compile gate — 869 files and 60 MB unpacked against 0.1.1's 161 and 1.4 MB. `!`-negations fix npm pack but NOT `yarn pack`, which is the packer this repo's packageManager selects, so the only form both honour is naming the paths. Mirror of how `native` is listed: core/tests/CMakeLists/.clangd by name, never the fetched .pdfium or the host build.",
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
30
|
"ios",
|
|
31
|
-
"android",
|
|
31
|
+
"android/build.gradle",
|
|
32
|
+
"android/gradle.properties",
|
|
33
|
+
"android/settings.gradle",
|
|
34
|
+
"android/consumer-rules.pro",
|
|
35
|
+
"android/gradle",
|
|
36
|
+
"android/gradlew",
|
|
37
|
+
"android/gradlew.bat",
|
|
38
|
+
"android/src",
|
|
39
|
+
"android/tools",
|
|
40
|
+
"native/core",
|
|
41
|
+
"native/tests",
|
|
42
|
+
"native/CMakeLists.txt",
|
|
43
|
+
"native/.clangd",
|
|
32
44
|
"scripts",
|
|
33
45
|
"react-native.config.js",
|
|
34
46
|
"PdfCanvas.podspec",
|
|
35
|
-
"README.md"
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"NOTICE"
|
|
36
50
|
],
|
|
37
51
|
"bin": {
|
|
38
52
|
"pdf-canvas-check-skia": "./scripts/check-skia-peer.mjs"
|
|
@@ -101,11 +115,17 @@
|
|
|
101
115
|
"check-format": "prettier --check .",
|
|
102
116
|
"check-exports": "attw --pack . --profile esm-only",
|
|
103
117
|
"check-peers": "node scripts/check-skia-peer.mjs",
|
|
104
|
-
"ci": "yarn run check-format && yarn run typecheck && vitest run && yarn run build"
|
|
118
|
+
"ci": "yarn run check-format && yarn run typecheck && vitest run && yarn run build",
|
|
119
|
+
"pdfium:fetch": "node scripts/fetch-pdfium.mjs",
|
|
120
|
+
"native:configure": "cmake -S native -B native/build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
|
121
|
+
"native:build": "cmake --build native/build",
|
|
122
|
+
"native:test": "node scripts/fetch-pdfium.mjs host && cmake -S native -B native/build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON && cmake --build native/build && ctest --test-dir native/build --output-on-failure",
|
|
123
|
+
"android:gate": "bash android/tools/compile-gate.sh"
|
|
105
124
|
},
|
|
106
125
|
"keywords": [
|
|
107
126
|
"react-native",
|
|
108
127
|
"pdf",
|
|
128
|
+
"pdfium",
|
|
109
129
|
"skia",
|
|
110
130
|
"reanimated"
|
|
111
131
|
],
|
package/react-native.config.js
CHANGED
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
* the failure is silent: the pod is simply absent from `Podfile.lock` and
|
|
20
20
|
* `NativeModules.PdfCanvas` is undefined at runtime.
|
|
21
21
|
*
|
|
22
|
-
* `ios` was `null` until the
|
|
23
|
-
*
|
|
22
|
+
* `ios` was `null` until the first iOS backend landed. If it ever needs to be
|
|
23
|
+
* null again, the message in `src/rasterizer/index.ts` is what a host sees.
|
|
24
|
+
* Note that the podspec FETCHES PDFium while it is evaluated (see the note at
|
|
25
|
+
* its top), so `pod install` needs either network access or the offline knobs
|
|
26
|
+
* `scripts/fetch-pdfium.mjs` documents.
|
|
24
27
|
*/
|
|
25
28
|
const path = require('path');
|
|
26
29
|
|