@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,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.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": "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"
@@ -12,17 +12,41 @@
12
12
  "access": "public"
13
13
  },
14
14
  "type": "module",
15
- "sideEffects": false,
15
+ "//sideEffects": "NOT `false`. The four entry files below are `export *` re-exports plus a top-level resolver install, and a bundler applies `sideEffects` to the IMPORTED module: with `false`, Rollup never marks a module that declares no used binding as executed and drops the call (that is what broke every PDF canvas on the production web build while dev — esbuild, entry-point code always kept — stayed green). `src/**` is listed so the property is testable against source; see `__tests__/tree-shaking.test.ts`. `web-init` is not listed: its install is inside an exported function.",
16
+ "sideEffects": [
17
+ "./dist/index.web.js",
18
+ "./dist/index.native.js",
19
+ "./dist/skia/index.web.js",
20
+ "./dist/skia/index.native.js",
21
+ "./src/index.web.ts",
22
+ "./src/index.native.ts",
23
+ "./src/skia/index.web.ts",
24
+ "./src/skia/index.native.ts"
25
+ ],
16
26
  "//files-scripts": "`scripts` ships because the peer-floor check runs from a CONSUMER'S postinstall, before/without any build of `dist`.",
17
- "//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.",
18
28
  "files": [
19
29
  "dist",
20
30
  "ios",
21
- "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",
22
44
  "scripts",
23
45
  "react-native.config.js",
24
46
  "PdfCanvas.podspec",
25
- "README.md"
47
+ "README.md",
48
+ "LICENSE",
49
+ "NOTICE"
26
50
  ],
27
51
  "bin": {
28
52
  "pdf-canvas-check-skia": "./scripts/check-skia-peer.mjs"
@@ -91,11 +115,17 @@
91
115
  "check-format": "prettier --check .",
92
116
  "check-exports": "attw --pack . --profile esm-only",
93
117
  "check-peers": "node scripts/check-skia-peer.mjs",
94
- "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"
95
124
  },
96
125
  "keywords": [
97
126
  "react-native",
98
127
  "pdf",
128
+ "pdfium",
99
129
  "skia",
100
130
  "reanimated"
101
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