@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,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.1.1",
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": "MIT",
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. Autolinking reads the podspec out of the INSTALLED package, so a podspec that is not in this list means `pod install` finds nothing, `PdfCanvas` never appears in Podfile.lock, and `NativeModules.PdfCanvas` is undefined at runtime with no build error anywhere. `ios/Package.swift` and `ios/Tests` ship as part of `ios` and cost a few KB; they are what lets a consumer run the core's host tests against the exact version they installed.",
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
  ],
@@ -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 CoreGraphics/PDFKit backend landed. If it ever needs
23
- * to be null again, the message in `src/rasterizer/index.ts` is what a host sees.
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