@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,110 @@
1
+ // The value types the core speaks, mirroring `src/types.ts`.
2
+ //
3
+ // Every rect is in PAGE-LOCAL DOC SPACE: PDF points, y-down, origin at the
4
+ // page's top-left, already post-rotation. That is exactly the space PDFium's
5
+ // display matrix emits, which is why a PDFium core is a pass-through and needs
6
+ // no transform of its own — the one thing the CoreGraphics core had to build by
7
+ // hand (see the retired `PdfCanvasPageTransform.h`) does not exist here.
8
+ #pragma once
9
+
10
+ #include <atomic>
11
+ #include <cstdint>
12
+ #include <string>
13
+ #include <vector>
14
+
15
+ namespace pdfcanvas {
16
+
17
+ enum class Background {
18
+ /// Fill the raster opaque white before drawing.
19
+ White,
20
+ /// Leave it cleared to transparent black before drawing. PDFium composites
21
+ /// onto whatever is there and never clears, so both backgrounds are an
22
+ /// explicit fill and only the value differs.
23
+ Transparent,
24
+ };
25
+
26
+ /// A page's intrinsic size in PDF points, POST-ROTATION.
27
+ ///
28
+ /// `FPDF_GetPageSizeByIndexF` already applies `/Rotate` (measured: a 200x100
29
+ /// MediaBox with `/Rotate 90` reports 100x200), and the render applies it too,
30
+ /// so from this package's vantage point there is no residual rotation left for
31
+ /// anyone to apply. `rotation` is therefore always 0 here. It exists because
32
+ /// `PageGeometry.rotation` is on the TS contract, where it is informational.
33
+ struct PageGeometry {
34
+ int index = 0;
35
+ double width = 0;
36
+ double height = 0;
37
+ int rotation = 0;
38
+ };
39
+
40
+ /// One raster to produce, mirroring `RasterRequest` in `src/types.ts`.
41
+ struct RasterRequest {
42
+ int page = 0;
43
+ double x = 0;
44
+ double y = 0;
45
+ double width = 0;
46
+ double height = 0;
47
+ /// Device pixels per PDF point.
48
+ double scale = 1;
49
+ /// Draw PDF-embedded annotations (and form fields) into the raster.
50
+ bool annotations = true;
51
+ Background background = Background::White;
52
+
53
+ /// Destination width in device pixels: ROUND, not ceil, and at least 1 —
54
+ /// the rule `planner.ts`, the fake backend and both retired native cores all
55
+ /// share. The planner declares the pixel size the cache is keyed on, so a
56
+ /// backend that rounded the other way would hand back a raster of a
57
+ /// different size than the plan promised.
58
+ int pixelWidth() const;
59
+ int pixelHeight() const;
60
+ };
61
+
62
+ /// Where a document's bytes come from. Exactly one of `path` / `bytes` is used;
63
+ /// a non-empty path wins.
64
+ struct Source {
65
+ std::string path;
66
+ std::vector<uint8_t> bytes;
67
+ /// Present means "the caller supplied one", including the empty string —
68
+ /// PDFium reports the same error for "no password" and "wrong password", and
69
+ /// only the caller knows which it was.
70
+ bool hasPassword = false;
71
+ std::string password;
72
+ };
73
+
74
+ /// A cancellation flag, written by whoever owns the render's token and read by
75
+ /// the render at five checkpoints — including MID-RENDER, through PDFium's
76
+ /// progressive API. That last one is what neither retired core could offer:
77
+ /// `CGContextDrawPDFPage` and `PdfRenderer.render` were uninterruptible once
78
+ /// entered, and `RasterPolicy.minEpochIntervalMs` exists because of it.
79
+ class Cancellation {
80
+ public:
81
+ void cancel() noexcept { cancelled_.store(true, std::memory_order_relaxed); }
82
+ bool isCancelled() const noexcept {
83
+ return cancelled_.load(std::memory_order_relaxed);
84
+ }
85
+
86
+ /// TESTING AFFORDANCE. Arms the flag to flip on the N-th poll of `poll()`,
87
+ /// which is what the render's mid-render checkpoint calls. It is the only
88
+ /// deterministic way to land a cancel INSIDE a render rather than before or
89
+ /// after it, and it is a no-op unless armed.
90
+ void cancelAfterPolls(int polls) noexcept {
91
+ pollsUntilCancel_.store(polls, std::memory_order_relaxed);
92
+ }
93
+
94
+ /// The render's view of the flag: `isCancelled()`, plus the poll counter.
95
+ bool poll() noexcept {
96
+ const int remaining = pollsUntilCancel_.load(std::memory_order_relaxed);
97
+ if (remaining > 0) {
98
+ if (pollsUntilCancel_.fetch_sub(1, std::memory_order_relaxed) == 1) {
99
+ cancel();
100
+ }
101
+ }
102
+ return isCancelled();
103
+ }
104
+
105
+ private:
106
+ std::atomic<bool> cancelled_{false};
107
+ std::atomic<int> pollsUntilCancel_{0};
108
+ };
109
+
110
+ } // namespace pdfcanvas
@@ -0,0 +1,22 @@
1
+ # The core's source list and include directory, for every build that compiles
2
+ # it: the Android library (android/src/main/cpp/CMakeLists.txt), the host test
3
+ # suite (native/CMakeLists.txt). The CocoaPods build lists the same files via
4
+ # `source_files` globs in PdfCanvas.podspec, because CocoaPods does not read
5
+ # CMake. Keep the two in step: a file added here and not there compiles on two
6
+ # platforms and is undefined on the third.
7
+ #
8
+ # `include()` this, then compile `${PDFCANVAS_CORE_SOURCES}` with
9
+ # `${PDFCANVAS_CORE_INCLUDE_DIR}` on the include path and the PDFium headers
10
+ # reachable as `fpdfview.h`.
11
+
12
+ set(PDFCANVAS_CORE_DIR "${CMAKE_CURRENT_LIST_DIR}")
13
+ set(PDFCANVAS_CORE_INCLUDE_DIR "${PDFCANVAS_CORE_DIR}/include")
14
+ set(PDFCANVAS_CORE_SOURCES
15
+ "${PDFCANVAS_CORE_DIR}/src/document.cpp"
16
+ "${PDFCANVAS_CORE_DIR}/src/error.cpp"
17
+ "${PDFCANVAS_CORE_DIR}/src/jsi.cpp"
18
+ "${PDFCANVAS_CORE_DIR}/src/library.cpp"
19
+ "${PDFCANVAS_CORE_DIR}/src/pixels.cpp"
20
+ "${PDFCANVAS_CORE_DIR}/src/service.cpp"
21
+ "${PDFCANVAS_CORE_DIR}/src/slots.cpp"
22
+ )
@@ -0,0 +1,516 @@
1
+ #include "pdfcanvas/document.h"
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+ #include <cstdint>
6
+ #include <cstdlib>
7
+ #include <limits>
8
+ #include <mutex>
9
+ #include <string>
10
+ #include <sys/stat.h>
11
+ #include <vector>
12
+
13
+ #include "fpdf_formfill.h"
14
+ #include "fpdf_progressive.h"
15
+ #include "fpdfview.h"
16
+ #include "pdfcanvas/library.h"
17
+ #include "pdfcanvas/slots.h"
18
+
19
+ namespace pdfcanvas {
20
+
21
+ namespace {
22
+
23
+ /* ------------------------------------------------------------------ *
24
+ * Constants
25
+ * ------------------------------------------------------------------ */
26
+
27
+ /// ARGB. Both values are symmetric under an R/B swap, which is what makes
28
+ /// filling BEFORE a reverse-byte-order render correct rather than lucky:
29
+ /// `FPDFBitmap_FillRect` writes in the bitmap's own BGRA order.
30
+ constexpr FPDF_DWORD kOpaqueWhite = 0xFFFFFFFFu;
31
+ constexpr FPDF_DWORD kTransparent = 0x00000000u;
32
+
33
+ /// PDFium's ints are 32-bit; a request past this cannot be expressed at all.
34
+ constexpr long long kMaxInt32 = (std::numeric_limits<int>::max)();
35
+
36
+ /* ------------------------------------------------------------------ *
37
+ * Errors
38
+ * ------------------------------------------------------------------ */
39
+
40
+ /// `FPDF_GetLastError()` after a failed load -> the frozen error union.
41
+ ///
42
+ /// ONLY MEANINGFUL IMMEDIATELY AFTER A FAILED DOCUMENT LOAD (measured in the
43
+ /// web backend: a later unrelated failure leaves the stale value in place), so
44
+ /// this is consulted in exactly one place. `passwordSupplied` is what tells the
45
+ /// two password outcomes apart: PDFium returns FPDF_ERR_PASSWORD both for an
46
+ /// encrypted document opened with no password and for one opened with the
47
+ /// wrong password, and the distinction matters — one means "prompt", the other
48
+ /// "you got it wrong".
49
+ PdfError loadError(unsigned long error, bool passwordSupplied, const std::string& what) {
50
+ ErrorCode code;
51
+ const char* name;
52
+ switch (error) {
53
+ case FPDF_ERR_FILE:
54
+ code = ErrorCode::NotFound;
55
+ name = "FILE";
56
+ break;
57
+ case FPDF_ERR_FORMAT:
58
+ // Both a truncated document and an eight-byte non-PDF report this.
59
+ code = ErrorCode::Corrupt;
60
+ name = "FORMAT";
61
+ break;
62
+ case FPDF_ERR_PASSWORD:
63
+ code = passwordSupplied ? ErrorCode::PasswordIncorrect : ErrorCode::PasswordRequired;
64
+ name = "PASSWORD";
65
+ break;
66
+ case FPDF_ERR_SECURITY:
67
+ // An encryption scheme this PDFium build cannot handle. Not corruption
68
+ // and not a wrong password — nothing the user can do about it.
69
+ code = ErrorCode::Unsupported;
70
+ name = "SECURITY";
71
+ break;
72
+ case FPDF_ERR_PAGE:
73
+ code = ErrorCode::Corrupt;
74
+ name = "PAGE";
75
+ break;
76
+ default:
77
+ code = ErrorCode::BackendFailure;
78
+ name = "UNKNOWN";
79
+ break;
80
+ }
81
+ return PdfError(code, "PDFium could not open " + what + " (FPDF_GetLastError = " +
82
+ std::to_string(error) + " " + name +
83
+ (passwordSupplied ? ", a password was supplied" : "") + ").");
84
+ }
85
+
86
+ /// What the file system can say that PDFium's single FPDF_ERR_FILE cannot: the
87
+ /// four failures "no such file", "a directory", "unreadable" and "empty" all
88
+ /// want different messages, because a host showing "corrupt" for a path that
89
+ /// simply does not exist sends somebody looking at the wrong thing.
90
+ void preflightPath(const std::string& path) {
91
+ if (path.empty()) {
92
+ throw PdfError(ErrorCode::NotFound, "No document path was given.");
93
+ }
94
+ struct stat st {};
95
+ if (::stat(path.c_str(), &st) != 0) {
96
+ throw PdfError(ErrorCode::NotFound, "No file at " + path + ".");
97
+ }
98
+ if ((st.st_mode & S_IFMT) == S_IFDIR) {
99
+ throw PdfError(ErrorCode::NotFound, path + " is a directory, not a PDF.");
100
+ }
101
+ if (st.st_size == 0) {
102
+ throw PdfError(ErrorCode::Corrupt, path + " is empty (0 bytes).");
103
+ }
104
+ }
105
+
106
+ void checkCancelled(Cancellation* signal, const char* stage) {
107
+ if (signal != nullptr && signal->isCancelled()) {
108
+ throw PdfError(ErrorCode::Cancelled, std::string("Render cancelled (") + stage + ").");
109
+ }
110
+ }
111
+
112
+ /* ------------------------------------------------------------------ *
113
+ * Geometry
114
+ * ------------------------------------------------------------------ */
115
+
116
+ /// Read every page's size without loading a single page.
117
+ ///
118
+ /// `FPDF_GetPageSizeByIndexF` works off the page DICTIONARY — no content
119
+ /// stream is parsed — so the cost is proportional to the page tree and not to
120
+ /// what is drawn on it. It is already post-rotation (measured: a 200x100
121
+ /// MediaBox with `/Rotate 90` reports 100x200; the CropBox intersected with the
122
+ /// MediaBox, after rotation — precisely what `PageGeometry` is defined as).
123
+ ///
124
+ /// A page that cannot be measured fails the OPEN rather than being papered
125
+ /// over with a zero size: a `PageGeometry` is what the layout turns into the
126
+ /// document's whole coordinate space, so a wrong one is every page after it in
127
+ /// the wrong place.
128
+ std::vector<PageGeometry> readGeometry(FPDF_DOCUMENT document) {
129
+ const int pageCount = FPDF_GetPageCount(document);
130
+ std::vector<PageGeometry> pages;
131
+ pages.reserve(static_cast<size_t>(std::max(0, pageCount)));
132
+ for (int index = 0; index < pageCount; index++) {
133
+ FS_SIZEF size{};
134
+ if (!FPDF_GetPageSizeByIndexF(document, index, &size)) {
135
+ throw PdfError(ErrorCode::Corrupt, "PDFium could not read the size of page " +
136
+ std::to_string(index) + " of " +
137
+ std::to_string(pageCount) + ".");
138
+ }
139
+ PageGeometry geometry;
140
+ geometry.index = index;
141
+ geometry.width = size.width;
142
+ geometry.height = size.height;
143
+ // Always 0: PDFium pre-applies `/Rotate` to both the size above and the
144
+ // render, so there is no residual rotation for a caller to apply. Reading
145
+ // the raw value would need `FPDF_LoadPage`, which parses the content
146
+ // stream — the one thing `pageGeometry` is forbidden to do.
147
+ geometry.rotation = 0;
148
+ pages.push_back(geometry);
149
+ }
150
+ return pages;
151
+ }
152
+
153
+ /* ------------------------------------------------------------------ *
154
+ * The progressive pause
155
+ * ------------------------------------------------------------------ */
156
+
157
+ /// PDFium's progressive renderer calls `NeedToPauseNow` between batches of page
158
+ /// objects. Answering "yes" only when the render has been cancelled means a
159
+ /// live render never pauses at all, and a superseded one stops within one batch
160
+ /// — which is what makes `interruptibleRender: true` honest for the first time
161
+ /// on a native backend.
162
+ struct PauseAdapter {
163
+ IFSDK_PAUSE pause{};
164
+ Cancellation* signal = nullptr;
165
+
166
+ explicit PauseAdapter(Cancellation* s) : signal(s) {
167
+ pause.version = 1;
168
+ pause.user = this;
169
+ pause.NeedToPauseNow = [](IFSDK_PAUSE* self) -> FPDF_BOOL {
170
+ auto* adapter = static_cast<PauseAdapter*>(self->user);
171
+ return adapter->signal != nullptr && adapter->signal->poll() ? 1 : 0;
172
+ };
173
+ }
174
+ };
175
+
176
+ } // namespace
177
+
178
+ /* ------------------------------------------------------------------ *
179
+ * Impl
180
+ * ------------------------------------------------------------------ */
181
+
182
+ struct Document::Impl {
183
+ FPDF_DOCUMENT document = nullptr;
184
+ FPDF_FORMHANDLE form = nullptr;
185
+ FPDF_FORMFILLINFO formInfo{};
186
+ /// The bytes a memory document reads from. `FPDF_LoadMemDocument64` does
187
+ /// not copy: PDFium reads lazily for the whole life of the document, so this
188
+ /// is freed after `FPDF_CloseDocument` and never before.
189
+ std::vector<uint8_t> bytes;
190
+ bool closed = false;
191
+
192
+ struct LoadedPage {
193
+ int index = -1;
194
+ FPDF_PAGE page = nullptr;
195
+ uint64_t lastUse = 0;
196
+ };
197
+ std::vector<LoadedPage> cache;
198
+ uint64_t useCounter = 0;
199
+
200
+ /// Requires the library lock.
201
+ FPDF_PAGE loadPage(int index) {
202
+ for (auto& entry : cache) {
203
+ if (entry.index == index) {
204
+ entry.lastUse = ++useCounter;
205
+ return entry.page;
206
+ }
207
+ }
208
+ if (cache.size() >= static_cast<size_t>(kPageCacheCapacity)) {
209
+ auto oldest = std::min_element(
210
+ cache.begin(), cache.end(),
211
+ [](const LoadedPage& a, const LoadedPage& b) { return a.lastUse < b.lastUse; });
212
+ unloadPage(*oldest);
213
+ cache.erase(oldest);
214
+ }
215
+ // `FPDF_LoadPage` is the call that parses the content stream; an epoch is
216
+ // several tiles of the same page, so the cache above turns N parses into
217
+ // one.
218
+ FPDF_PAGE page = FPDF_LoadPage(document, index);
219
+ if (page == nullptr) {
220
+ // Not `FPDF_GetLastError` — it is stale here. The index was already
221
+ // bounds-checked against the geometry read at open, so a failure at this
222
+ // point is a page whose content PDFium refuses.
223
+ throw PdfError(ErrorCode::Corrupt,
224
+ "PDFium could not load page " + std::to_string(index) +
225
+ ". The page exists (its size was read at open) but its "
226
+ "content could not be parsed.");
227
+ }
228
+ if (form != nullptr) {
229
+ // Required for FPDF_FFLDraw to know the page's widgets; balanced by
230
+ // FORM_OnBeforeClosePage in unloadPage.
231
+ FORM_OnAfterLoadPage(page, form);
232
+ }
233
+ cache.push_back({index, page, ++useCounter});
234
+ return page;
235
+ }
236
+
237
+ void unloadPage(LoadedPage& entry) {
238
+ if (entry.page == nullptr) return;
239
+ if (form != nullptr) FORM_OnBeforeClosePage(entry.page, form);
240
+ FPDF_ClosePage(entry.page);
241
+ entry.page = nullptr;
242
+ }
243
+
244
+ void closeAll() {
245
+ if (closed) return;
246
+ closed = true;
247
+ for (auto& entry : cache) unloadPage(entry);
248
+ cache.clear();
249
+ if (form != nullptr) {
250
+ FPDFDOC_ExitFormFillEnvironment(form);
251
+ form = nullptr;
252
+ }
253
+ if (document != nullptr) {
254
+ FPDF_CloseDocument(document);
255
+ document = nullptr;
256
+ }
257
+ // AFTER the close, never before.
258
+ bytes.clear();
259
+ bytes.shrink_to_fit();
260
+ }
261
+ };
262
+
263
+ /* ------------------------------------------------------------------ *
264
+ * Open / close
265
+ * ------------------------------------------------------------------ */
266
+
267
+ Document::Document(std::unique_ptr<Impl> impl) : impl_(std::move(impl)) {}
268
+
269
+ Document::~Document() {
270
+ std::lock_guard<std::mutex> lock(Library::mutex());
271
+ impl_->closeAll();
272
+ }
273
+
274
+ std::unique_ptr<Document> Document::open(const Source& source) {
275
+ const bool fromPath = !source.path.empty();
276
+ if (fromPath) {
277
+ preflightPath(source.path);
278
+ } else if (source.bytes.empty()) {
279
+ throw PdfError(ErrorCode::Corrupt, "The document is empty (0 bytes).");
280
+ }
281
+
282
+ auto impl = std::make_unique<Impl>();
283
+ std::lock_guard<std::mutex> lock(Library::mutex());
284
+ Library::ensureInitialized();
285
+
286
+ const char* password = source.hasPassword ? source.password.c_str() : nullptr;
287
+ if (fromPath) {
288
+ impl->document = FPDF_LoadDocument(source.path.c_str(), password);
289
+ } else {
290
+ impl->bytes = source.bytes;
291
+ impl->document = FPDF_LoadMemDocument64(impl->bytes.data(), impl->bytes.size(), password);
292
+ }
293
+ if (impl->document == nullptr) {
294
+ throw loadError(FPDF_GetLastError(), source.hasPassword,
295
+ fromPath ? source.path : "the document bytes");
296
+ }
297
+
298
+ std::vector<PageGeometry> pages;
299
+ try {
300
+ pages = readGeometry(impl->document);
301
+ } catch (...) {
302
+ impl->closeAll();
303
+ throw;
304
+ }
305
+
306
+ // The form-fill environment is what draws WIDGET annotations — form fields
307
+ // — which `FPDF_RenderPageBitmap` skips even with `FPDF_ANNOT`. Version 1 of
308
+ // the info struct, with every callback null: PDFium null-checks them all,
309
+ // and this build has no V8, so nothing here ever needs to answer a
310
+ // JavaScript action. Opened eagerly because it is cheap and because a page
311
+ // must be registered with it at load time, before anyone knows whether an
312
+ // annotations render will come.
313
+ impl->formInfo = FPDF_FORMFILLINFO{};
314
+ impl->formInfo.version = 1;
315
+ impl->form = FPDFDOC_InitFormFillEnvironment(impl->document, &impl->formInfo);
316
+ if (impl->form != nullptr) {
317
+ // No field highlight. Chrome and Acrobat tint fillable fields light blue
318
+ // on screen; under an annotation canvas that wash would read as content,
319
+ // and it is not what a printed or flattened copy of the page looks like.
320
+ FPDF_SetFormFieldHighlightColor(impl->form, FPDF_FORMFIELD_UNKNOWN, 0xFFE4DD);
321
+ FPDF_SetFormFieldHighlightAlpha(impl->form, 0);
322
+ }
323
+
324
+ std::unique_ptr<Document> document(new Document(std::move(impl)));
325
+ document->pages_ = std::move(pages);
326
+ return document;
327
+ }
328
+
329
+ void Document::close() {
330
+ std::lock_guard<std::mutex> lock(Library::mutex());
331
+ impl_->closeAll();
332
+ }
333
+
334
+ bool Document::isClosed() const noexcept { return impl_->closed; }
335
+
336
+ const PageGeometry& Document::geometry(int index) const {
337
+ if (index < 0 || static_cast<size_t>(index) >= pages_.size()) {
338
+ throw PdfError(ErrorCode::NotFound, "Document has " + std::to_string(pages_.size()) +
339
+ " pages; asked for " + std::to_string(index) + ".");
340
+ }
341
+ return pages_[static_cast<size_t>(index)];
342
+ }
343
+
344
+ /* ------------------------------------------------------------------ *
345
+ * Render
346
+ * ------------------------------------------------------------------ */
347
+
348
+ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal,
349
+ PixelSink* sink) {
350
+ checkCancelled(signal, "before start");
351
+ if (impl_->closed) {
352
+ throw PdfError(ErrorCode::BackendFailure, "Document handle is closed.");
353
+ }
354
+ const PageGeometry& geometry = this->geometry(request.page);
355
+
356
+ if (!std::isfinite(request.scale) || request.scale <= 0) {
357
+ throw PdfError(ErrorCode::BackendFailure,
358
+ "Scale must be a positive finite number, got " + std::to_string(request.scale) + ".");
359
+ }
360
+ if (!std::isfinite(request.x) || !std::isfinite(request.y) || !(request.width > 0) ||
361
+ !(request.height > 0)) {
362
+ throw PdfError(ErrorCode::BackendFailure,
363
+ "Doc rect must be finite and have positive extent, got " +
364
+ std::to_string(request.width) + "x" + std::to_string(request.height) +
365
+ " at (" + std::to_string(request.x) + ", " + std::to_string(request.y) + ").");
366
+ }
367
+
368
+ const int pxW = request.pixelWidth();
369
+ const int pxH = request.pixelHeight();
370
+ const long long pixels = static_cast<long long>(pxW) * pxH;
371
+ // THE RECT MECHANISM — negative offsets, not a matrix. PDFium draws the WHOLE
372
+ // page at the target scale slid by (-x*s, -y*s) into a bitmap that is only
373
+ // the tile's size; everything outside is clipped by the bitmap itself.
374
+ // `-round(x*s)`, not `round(-x*s)`: the tile's origin inside a hypothetical
375
+ // full-page render is `round(x*s)`, and negating the rounded origin makes the
376
+ // two the same integer by construction. The web backend measured this
377
+ // byte-identical to the matrix form on every rect it tried.
378
+ const long long startX = -std::llround(request.x * request.scale);
379
+ const long long startY = -std::llround(request.y * request.scale);
380
+ const long long pageW = std::llround(geometry.width * request.scale);
381
+ const long long pageH = std::llround(geometry.height * request.scale);
382
+ if (pixels > kMaxRasterPixels) {
383
+ throw PdfError(ErrorCode::OutOfMemory,
384
+ "Refused a " + std::to_string(pxW) + "x" + std::to_string(pxH) + " raster (" +
385
+ std::to_string(pixels) + " pixels > " + std::to_string(kMaxRasterPixels) + ").");
386
+ }
387
+ if (pageW > kMaxInt32 || pageH > kMaxInt32 || std::llabs(startX) > kMaxInt32 ||
388
+ std::llabs(startY) > kMaxInt32) {
389
+ throw PdfError(ErrorCode::OutOfMemory,
390
+ "A " + std::to_string(pxW) + "x" + std::to_string(pxH) + " raster of a " +
391
+ std::to_string(pageW) + "x" + std::to_string(pageH) +
392
+ " page cannot be expressed in PDFium's 32-bit ints. Lower "
393
+ "`RasterPolicy.maxPieceDimension` or `maxRasterPixels`.");
394
+ }
395
+ const int rowBytes = pxW * 4;
396
+ const size_t byteCount = static_cast<size_t>(rowBytes) * static_cast<size_t>(pxH);
397
+
398
+ std::lock_guard<std::mutex> lock(Library::mutex());
399
+ // The wait above is where a cancel most often lands — a superseded detail
400
+ // raster typically queues behind a base raster — so the signal is re-read on
401
+ // the far side of it.
402
+ checkCancelled(signal, "after lock");
403
+ if (impl_->closed) {
404
+ throw PdfError(ErrorCode::BackendFailure, "Document handle is closed.");
405
+ }
406
+
407
+ FPDF_PAGE page = impl_->loadPage(request.page);
408
+
409
+ // RESERVE BEFORE DRAWING, and fall back before drawing too. A sink refusal
410
+ // here costs nothing, which is the point of deciding it before the raster
411
+ // exists.
412
+ HeapSink heap;
413
+ PixelSink* destination = sink;
414
+ uint8_t* buffer = destination != nullptr ? destination->reserve(byteCount) : nullptr;
415
+ if (buffer == nullptr) {
416
+ destination = &heap;
417
+ buffer = heap.reserve(byteCount);
418
+ }
419
+ if (buffer == nullptr) {
420
+ throw PdfError(ErrorCode::OutOfMemory, "Could not allocate " + std::to_string(byteCount) +
421
+ " bytes for a " + std::to_string(pxW) + "x" +
422
+ std::to_string(pxH) + " raster.");
423
+ }
424
+
425
+ FPDF_BITMAP bitmap = FPDFBitmap_CreateEx(pxW, pxH, FPDFBitmap_BGRA, buffer, rowBytes);
426
+ if (bitmap == nullptr) {
427
+ destination->abandon();
428
+ throw PdfError(ErrorCode::BackendFailure, "PDFium refused a " + std::to_string(pxW) + "x" +
429
+ std::to_string(pxH) + " bitmap over the sink's buffer.");
430
+ }
431
+
432
+ // THE FILL IS NOT OPTIONAL. PDFium composites onto the destination and never
433
+ // clears it; a zeroed RGBA buffer is transparent BLACK, which under the app's
434
+ // annotation layers reads as a black page rather than a blank one, and ingest
435
+ // cannot tell "the PDF painted black here" from "nothing painted here".
436
+ if (!FPDFBitmap_FillRect(bitmap, 0, 0, pxW, pxH,
437
+ request.background == Background::White ? kOpaqueWhite : kTransparent)) {
438
+ FPDFBitmap_Destroy(bitmap);
439
+ destination->abandon();
440
+ throw PdfError(ErrorCode::BackendFailure,
441
+ "PDFium refused to clear the destination before rendering page " +
442
+ std::to_string(request.page) + ".");
443
+ }
444
+
445
+ // RGBA out (Skia's universally-accepted colour type) instead of PDFium's
446
+ // native BGRA; annotations only when asked. NO `FPDF_LCD_TEXT`: subpixel text
447
+ // is calibrated to a specific pixel grid, and every tile here is scaled and
448
+ // composited by Skia afterwards.
449
+ const int flags = FPDF_REVERSE_BYTE_ORDER | (request.annotations ? FPDF_ANNOT : 0);
450
+ const int sx = static_cast<int>(startX);
451
+ const int sy = static_cast<int>(startY);
452
+ const int pw = static_cast<int>(pageW);
453
+ const int ph = static_cast<int>(pageH);
454
+
455
+ auto abandonWith = [&](PdfError error) -> PdfError {
456
+ FPDFBitmap_Destroy(bitmap);
457
+ destination->abandon();
458
+ return error;
459
+ };
460
+
461
+ if (signal != nullptr && signal->isCancelled()) {
462
+ throw abandonWith(PdfError(ErrorCode::Cancelled, "Render cancelled (before render)."));
463
+ }
464
+
465
+ // THE PROGRESSIVE API, so a cancel lands mid-draw. `Start` returns
466
+ // TOBECONTINUED when the pause adapter asked to stop, which it does only
467
+ // once the signal is set; a live render runs to DONE in one call.
468
+ PauseAdapter pause(signal);
469
+ int status = FPDF_RenderPageBitmap_Start(bitmap, page, sx, sy, pw, ph, /*rotate=*/0, flags,
470
+ &pause.pause);
471
+ while (status == FPDF_RENDER_TOBECONTINUED) {
472
+ if (signal != nullptr && signal->isCancelled()) {
473
+ FPDF_RenderPage_Close(page);
474
+ throw abandonWith(PdfError(ErrorCode::Cancelled, "Render cancelled (mid-render)."));
475
+ }
476
+ status = FPDF_RenderPage_Continue(page, &pause.pause);
477
+ }
478
+ FPDF_RenderPage_Close(page);
479
+ if (status == FPDF_RENDER_FAILED) {
480
+ throw abandonWith(PdfError(ErrorCode::BackendFailure,
481
+ "PDFium failed to render page " + std::to_string(request.page) + "."));
482
+ }
483
+
484
+ // Form fields are WIDGET annotations, and `FPDF_RenderPageBitmap` skips
485
+ // widgets even with FPDF_ANNOT (they belong to the form-fill environment).
486
+ // This is the pass that draws them — the filled-in value, the checkbox
487
+ // state — with the same offsets, so they land exactly where the page put
488
+ // them.
489
+ if (request.annotations && impl_->form != nullptr) {
490
+ FPDF_FFLDraw(impl_->form, bitmap, page, sx, sy, pw, ph, /*rotate=*/0, flags);
491
+ }
492
+
493
+ FPDFBitmap_Destroy(bitmap);
494
+
495
+ // Post-check. This saves no drawing — the pixels are already finished — it
496
+ // only stops a superseded raster being handed back and ingested into Skia,
497
+ // which is the expensive half on the JS side. THE ABANDON IS NOT OPTIONAL: a
498
+ // slot-backed raster is about to be published to a process-wide map where
499
+ // nothing else will ever look for it.
500
+ if (signal != nullptr && signal->isCancelled()) {
501
+ destination->abandon();
502
+ throw PdfError(ErrorCode::Cancelled, "Render cancelled (after render).");
503
+ }
504
+
505
+ RasterPixels out;
506
+ out.width = pxW;
507
+ out.height = pxH;
508
+ out.rowBytes = rowBytes;
509
+ out.slot = destination->publish();
510
+ if (destination == &heap) {
511
+ out.bytes = heap.takeBytes();
512
+ }
513
+ return out;
514
+ }
515
+
516
+ } // namespace pdfcanvas
@@ -0,0 +1,29 @@
1
+ #include "pdfcanvas/error.h"
2
+
3
+ namespace pdfcanvas {
4
+
5
+ const char* wireName(ErrorCode code) {
6
+ switch (code) {
7
+ case ErrorCode::NotFound:
8
+ return "not-found";
9
+ case ErrorCode::PasswordRequired:
10
+ return "password-required";
11
+ case ErrorCode::PasswordIncorrect:
12
+ return "password-incorrect";
13
+ case ErrorCode::Corrupt:
14
+ return "corrupt";
15
+ case ErrorCode::Unsupported:
16
+ return "unsupported";
17
+ case ErrorCode::Cancelled:
18
+ return "cancelled";
19
+ case ErrorCode::OutOfMemory:
20
+ return "out-of-memory";
21
+ case ErrorCode::BackendFailure:
22
+ return "backend-failure";
23
+ }
24
+ // No `default:` above, so a new member is a compiler warning rather than a
25
+ // silent fall-through to this line.
26
+ return "backend-failure";
27
+ }
28
+
29
+ } // namespace pdfcanvas