@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,451 @@
1
+ #include "fixtures.h"
2
+
3
+ #include <array>
4
+ #include <cstdio>
5
+ #include <cstring>
6
+ #include <filesystem>
7
+ #include <fstream>
8
+ #include <stdexcept>
9
+ #include <string>
10
+
11
+ namespace fixtures {
12
+
13
+ namespace {
14
+
15
+ /* ------------------------------------------------------------------ *
16
+ * The writer
17
+ * ------------------------------------------------------------------ */
18
+
19
+ /// Assembles objects and writes a correct xref table, which every reader prefers.
20
+ class Writer {
21
+ public:
22
+ Writer() {
23
+ write("%PDF-1.4\n");
24
+ // A binary comment line marks the file as binary, per the spec's advice.
25
+ const uint8_t marker[] = {0x25, 0xE2, 0xE3, 0xCF, 0xD3, 0x0A};
26
+ out_.insert(out_.end(), marker, marker + sizeof(marker));
27
+ }
28
+
29
+ void write(const std::string& s) { out_.insert(out_.end(), s.begin(), s.end()); }
30
+ void write(const Bytes& b) { out_.insert(out_.end(), b.begin(), b.end()); }
31
+
32
+ int count() const { return static_cast<int>(offsets_.size()); }
33
+
34
+ /// Returns the object number, 1-based.
35
+ int add(const std::string& body) { return add(Bytes(body.begin(), body.end())); }
36
+
37
+ int add(const Bytes& body) {
38
+ const int number = count() + 1;
39
+ offsets_.push_back(out_.size());
40
+ write(std::to_string(number) + " 0 obj\n");
41
+ write(body);
42
+ write("\nendobj\n");
43
+ return number;
44
+ }
45
+
46
+ Bytes finish(int rootObject, const std::string& extraTrailer = "") {
47
+ const size_t xref = out_.size();
48
+ const size_t size = offsets_.size() + 1;
49
+ write("xref\n0 " + std::to_string(size) + "\n");
50
+ write("0000000000 65535 f \n");
51
+ for (size_t offset : offsets_) {
52
+ char line[32];
53
+ std::snprintf(line, sizeof(line), "%010zu 00000 n \n", offset);
54
+ write(std::string(line));
55
+ }
56
+ write("trailer\n<< /Size " + std::to_string(size) + " /Root " + std::to_string(rootObject) +
57
+ " 0 R " + extraTrailer + " >>\nstartxref\n" + std::to_string(xref) + "\n%%EOF\n");
58
+ return out_;
59
+ }
60
+
61
+ private:
62
+ Bytes out_;
63
+ std::vector<size_t> offsets_;
64
+ };
65
+
66
+ std::string boxString(const Box& box) {
67
+ return "[" + std::to_string(box.x0) + " " + std::to_string(box.y0) + " " +
68
+ std::to_string(box.x1) + " " + std::to_string(box.y1) + "]";
69
+ }
70
+
71
+ std::string streamObject(const std::string& content, const std::string& extraDict = "") {
72
+ return "<< /Length " + std::to_string(content.size()) + extraDict + " >>\nstream\n" + content +
73
+ "endstream";
74
+ }
75
+
76
+ std::string fmt(double value) {
77
+ char buffer[32];
78
+ std::snprintf(buffer, sizeof(buffer), "%g", value);
79
+ return buffer;
80
+ }
81
+
82
+ } // namespace
83
+
84
+ /* ------------------------------------------------------------------ *
85
+ * Single-page fixtures
86
+ * ------------------------------------------------------------------ */
87
+
88
+ Bytes page(Box mediaBox, const Box* cropBox, int rotate, const std::string& content) {
89
+ Writer w;
90
+ const int contents = w.add(streamObject(content));
91
+ // COMPUTED, not hardcoded: /Parent has to name the page tree, which is
92
+ // written next-but-one.
93
+ const int pagesNumber = w.count() + 2;
94
+ std::string dict = "<< /Type /Page /Parent " + std::to_string(pagesNumber) +
95
+ " 0 R /MediaBox " + boxString(mediaBox);
96
+ if (cropBox != nullptr) dict += " /CropBox " + boxString(*cropBox);
97
+ if (rotate != 0) dict += " /Rotate " + std::to_string(rotate);
98
+ dict += " /Contents " + std::to_string(contents) + " 0 R /Resources << >> >>";
99
+ const int pageObj = w.add(dict);
100
+ const int pages = w.add("<< /Type /Pages /Kids [" + std::to_string(pageObj) + " 0 R] /Count 1 >>");
101
+ const int catalog = w.add("<< /Type /Catalog /Pages " + std::to_string(pages) + " 0 R >>");
102
+ if (pages != pagesNumber) throw std::logic_error("pages object number drifted");
103
+ return w.finish(catalog);
104
+ }
105
+
106
+ Bytes quadrants(int width, int height, int rotate) {
107
+ const int hw = width / 2;
108
+ const int hh = height / 2;
109
+ const std::string content = "1 0 0 rg 0 0 " + std::to_string(hw) + " " + std::to_string(hh) +
110
+ " re f\n0 1 0 rg " + std::to_string(hw) + " 0 " + std::to_string(hw) +
111
+ " " + std::to_string(hh) + " re f\n0 0 1 rg 0 " + std::to_string(hh) +
112
+ " " + std::to_string(hw) + " " + std::to_string(hh) +
113
+ " re f\n0 0 0 rg " + std::to_string(hw) + " " + std::to_string(hh) +
114
+ " " + std::to_string(hw) + " " + std::to_string(hh) + " re f\n";
115
+ return page({0, 0, width, height}, nullptr, rotate, content);
116
+ }
117
+
118
+ Bytes grid(int width, int height, int pitch) {
119
+ std::string content = "1 1 1 rg 0 0 " + std::to_string(width) + " " + std::to_string(height) +
120
+ " re f\n0 0 0 RG 0.5 w\n";
121
+ for (int x = 0; x <= width; x += pitch) {
122
+ content += std::to_string(x) + " 0 m " + std::to_string(x) + " " + std::to_string(height) + " l S\n";
123
+ }
124
+ for (int y = 0; y <= height; y += pitch) {
125
+ content += "0 " + std::to_string(y) + " m " + std::to_string(width) + " " + std::to_string(y) + " l S\n";
126
+ }
127
+ content += "1 0 0 RG 2 w 0 0 m " + std::to_string(width) + " " + std::to_string(height) + " l S " +
128
+ std::to_string(width) + " 0 m 0 " + std::to_string(height) + " l S\n";
129
+ return page({0, 0, width, height}, nullptr, 0, content);
130
+ }
131
+
132
+ Bytes rotated(int degrees) {
133
+ // PDF user space is y-UP from the bottom-left, so the top-left 112x100 of
134
+ // the page is the rect (0, 692) to (112, 792).
135
+ const std::string content = "1 1 1 rg 0 0 612 792 re f\n1 0 0 rg 0 692 112 100 re f\n0 0 1 rg 0 0 112 100 re f\n";
136
+ return page({0, 0, kLetterW, kLetterH}, nullptr, degrees, content);
137
+ }
138
+
139
+ Bytes offsetCropBox() {
140
+ // CropBox (100, 150) to (300, 350): 200 x 200 points, top-left at (100, 350).
141
+ const std::string content =
142
+ "0.5 0.5 0.5 rg 0 0 400 400 re f\n1 0 0 rg 100 310 40 40 re f\n0 0 1 rg 260 150 40 40 re f\n";
143
+ const Box crop{100, 150, 300, 350};
144
+ return page({0, 0, 400, 400}, &crop, 0, content);
145
+ }
146
+
147
+ Bytes cropBoxOverhangingMediaBox() {
148
+ const Box crop{50, 50, 400, 400};
149
+ return page({0, 0, 200, 200}, &crop, 0, "0.5 0.5 0.5 rg 0 0 200 200 re f\n");
150
+ }
151
+
152
+ Bytes antialiasedWhiteEdge() {
153
+ const std::string content =
154
+ "1 1 1 rg\n50 10 m\n72.09 10 90 27.91 90 50 c\n90 72.09 72.09 90 50 90 c\n"
155
+ "27.91 90 10 72.09 10 50 c\n10 27.91 27.91 10 50 10 c\nf\n";
156
+ return page({0, 0, 100, 100}, nullptr, 0, content);
157
+ }
158
+
159
+ Bytes annotatedPage() {
160
+ Writer w;
161
+ const std::string content = "1 1 1 rg 0 0 200 200 re f\n1 0 0 rg 10 10 30 30 re f\n";
162
+ const int contents = w.add(streamObject(content));
163
+ // The annotation and its appearance stream are written BEFORE the page
164
+ // dictionary that names them, so /Annots is built from the object number
165
+ // they actually got.
166
+ const std::string appearance = "0 1 0 rg 0 0 100 100 re f\n";
167
+ const int ap = w.add(streamObject(appearance, " /Type /XObject /Subtype /Form /BBox [0 0 100 100]"));
168
+ const int annot = w.add("<< /Type /Annot /Subtype /Square /Rect [50 50 150 150] /F 4 /AP << /N " +
169
+ std::to_string(ap) + " 0 R >> >>");
170
+ const int pagesNumber = w.count() + 2;
171
+ const int pageObj = w.add("<< /Type /Page /Parent " + std::to_string(pagesNumber) +
172
+ " 0 R /MediaBox [0 0 200 200] /Annots [" + std::to_string(annot) +
173
+ " 0 R] /Contents " + std::to_string(contents) + " 0 R /Resources << >> >>");
174
+ const int pages = w.add("<< /Type /Pages /Kids [" + std::to_string(pageObj) + " 0 R] /Count 1 >>");
175
+ const int catalog = w.add("<< /Type /Catalog /Pages " + std::to_string(pages) + " 0 R >>");
176
+ if (pages != pagesNumber) throw std::logic_error("pages object number drifted");
177
+ return w.finish(catalog);
178
+ }
179
+
180
+ Bytes formFieldPage() {
181
+ Writer w;
182
+ const std::string content = "1 1 1 rg 0 0 200 200 re f\n1 0 0 rg 10 10 30 30 re f\n";
183
+ const int contents = w.add(streamObject(content));
184
+ // A merged field/widget dictionary — the common single-widget shape — whose
185
+ // normal appearance paints a solid blue box. `/FT /Tx` with a value and a
186
+ // default appearance so PDFium treats it as a real text field rather than an
187
+ // orphan widget.
188
+ const std::string appearance = "0 0 1 rg 0 0 100 50 re f\n";
189
+ const int ap = w.add(streamObject(appearance, " /Type /XObject /Subtype /Form /BBox [0 0 100 50]"));
190
+ const int pagesNumber = w.count() + 3; // widget, page, then the tree
191
+ const int pageNumber = w.count() + 2;
192
+ const int widget = w.add("<< /Type /Annot /Subtype /Widget /FT /Tx /T (field) /V (value) /Ff 0 "
193
+ "/Rect [50 50 150 100] /F 4 /P " + std::to_string(pageNumber) +
194
+ " 0 R /DA (/Helv 12 Tf 0 g) /MK << /BG [0 0 1] >> /AP << /N " +
195
+ std::to_string(ap) + " 0 R >> >>");
196
+ const int pageObj = w.add("<< /Type /Page /Parent " + std::to_string(pagesNumber) +
197
+ " 0 R /MediaBox [0 0 200 200] /Annots [" + std::to_string(widget) +
198
+ " 0 R] /Contents " + std::to_string(contents) + " 0 R /Resources << >> >>");
199
+ const int pages = w.add("<< /Type /Pages /Kids [" + std::to_string(pageObj) + " 0 R] /Count 1 >>");
200
+ const int catalog = w.add("<< /Type /Catalog /Pages " + std::to_string(pages) +
201
+ " 0 R /AcroForm << /Fields [" + std::to_string(widget) +
202
+ " 0 R] /DA (/Helv 0 Tf 0 g) /NeedAppearances false >> >>");
203
+ if (pages != pagesNumber || pageObj != pageNumber) throw std::logic_error("object numbers drifted");
204
+ return w.finish(catalog);
205
+ }
206
+
207
+ /* ------------------------------------------------------------------ *
208
+ * Multi-page fixtures
209
+ * ------------------------------------------------------------------ */
210
+
211
+ Bytes multiPage(const std::vector<PageSpec>& specs) {
212
+ Writer w;
213
+ std::vector<int> contentObjects;
214
+ for (const auto& spec : specs) contentObjects.push_back(w.add(streamObject(spec.content)));
215
+ const int treeNumber = static_cast<int>(contentObjects.size() + specs.size()) + 1;
216
+ std::vector<int> pageObjects;
217
+ for (size_t i = 0; i < specs.size(); i++) {
218
+ std::string dict = "<< /Type /Page /Parent " + std::to_string(treeNumber) + " 0 R /MediaBox [0 0 " +
219
+ std::to_string(specs[i].width) + " " + std::to_string(specs[i].height) + "]";
220
+ if (specs[i].rotate != 0) dict += " /Rotate " + std::to_string(specs[i].rotate);
221
+ dict += " /Contents " + std::to_string(contentObjects[i]) + " 0 R /Resources << >> >>";
222
+ pageObjects.push_back(w.add(dict));
223
+ }
224
+ std::string kids;
225
+ for (int number : pageObjects) kids += std::to_string(number) + " 0 R ";
226
+ const int tree = w.add("<< /Type /Pages /Kids [" + kids + "] /Count " + std::to_string(specs.size()) + " >>");
227
+ if (tree != treeNumber) throw std::logic_error("tree object number drifted");
228
+ const int catalog = w.add("<< /Type /Catalog /Pages " + std::to_string(tree) + " 0 R >>");
229
+ return w.finish(catalog);
230
+ }
231
+
232
+ Bytes mixedSizeDocument() {
233
+ std::vector<PageSpec> specs;
234
+ const std::pair<int, int> sizes[] = {{kLetterW, kLetterH}, {kArchDW, kArchDH}, {200, 900}};
235
+ for (const auto& size : sizes) {
236
+ specs.push_back({size.first, size.second, 0,
237
+ "0.5 0.5 0.5 rg 0 0 " + std::to_string(size.first) + " " +
238
+ std::to_string(size.second) + " re f\n"});
239
+ }
240
+ return multiPage(specs);
241
+ }
242
+
243
+ Bytes coloredPages(const std::vector<std::array<double, 3>>& colors, int width, int height) {
244
+ std::vector<PageSpec> specs;
245
+ for (const auto& color : colors) {
246
+ specs.push_back({width, height, 0,
247
+ fmt(color[0]) + " " + fmt(color[1]) + " " + fmt(color[2]) + " rg 0 0 " +
248
+ std::to_string(width) + " " + std::to_string(height) + " re f\n"});
249
+ }
250
+ return multiPage(specs);
251
+ }
252
+
253
+ /* ------------------------------------------------------------------ *
254
+ * Not-PDFs
255
+ * ------------------------------------------------------------------ */
256
+
257
+ Bytes headerOnlyGarbage() {
258
+ std::string s = "%PDF-1.7\n";
259
+ for (int i = 0; i < 200; i++) s += "garbage line " + std::to_string(i) + "\n";
260
+ return Bytes(s.begin(), s.end());
261
+ }
262
+
263
+ Bytes notAPdfAtAll() {
264
+ std::string s;
265
+ for (int i = 0; i < 200; i++) s += "this is a text file, line " + std::to_string(i) + "\n";
266
+ s += "%PDF-1.7 too late to count\n";
267
+ return Bytes(s.begin(), s.end());
268
+ }
269
+
270
+ /* ------------------------------------------------------------------ *
271
+ * Encryption (standard security handler, R=2, V=1, RC4-40)
272
+ * ------------------------------------------------------------------ */
273
+
274
+ const char* const kUserPassword = "secret";
275
+ /// Non-empty, and that is load-bearing: with an empty owner password every
276
+ /// reader's owner-authentication path (Algorithm 7) recovers the user password
277
+ /// from /O and opens the document with no password at all.
278
+ const char* const kOwnerPassword = "owner";
279
+
280
+ namespace {
281
+
282
+ const uint8_t kPad[32] = {0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E,
283
+ 0x56, 0xFF, 0xFA, 0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68,
284
+ 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A};
285
+
286
+ Bytes padded(const std::string& password) {
287
+ Bytes out(32, 0);
288
+ const size_t n = std::min<size_t>(password.size(), 32);
289
+ for (size_t i = 0; i < n; i++) out[i] = static_cast<uint8_t>(password[i]);
290
+ for (size_t i = n; i < 32; i++) out[i] = kPad[i - n];
291
+ return out;
292
+ }
293
+
294
+ Bytes rc4(const Bytes& key, const Bytes& data) {
295
+ std::array<int, 256> s{};
296
+ for (int i = 0; i < 256; i++) s[i] = i;
297
+ for (int i = 0, j = 0; i < 256; i++) {
298
+ j = (j + s[i] + key[i % key.size()]) & 0xFF;
299
+ std::swap(s[i], s[j]);
300
+ }
301
+ Bytes out(data.size());
302
+ for (size_t k = 0, i = 0, j = 0; k < data.size(); k++) {
303
+ i = (i + 1) & 0xFF;
304
+ j = (j + s[i]) & 0xFF;
305
+ std::swap(s[i], s[j]);
306
+ out[k] = static_cast<uint8_t>(data[k] ^ s[(s[i] + s[j]) & 0xFF]);
307
+ }
308
+ return out;
309
+ }
310
+
311
+ /// MD5, written out rather than pulled from a platform library, so the fixture
312
+ /// is the same bytes on every host.
313
+ Bytes md5(const Bytes& message) {
314
+ static const uint32_t s[64] = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
315
+ 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
316
+ 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
317
+ 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};
318
+ static const uint32_t k[64] = {
319
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
320
+ 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
321
+ 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
322
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
323
+ 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
324
+ 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
325
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
326
+ 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391};
327
+
328
+ Bytes msg = message;
329
+ const uint64_t bitLength = static_cast<uint64_t>(message.size()) * 8;
330
+ msg.push_back(0x80);
331
+ while (msg.size() % 64 != 56) msg.push_back(0);
332
+ for (int i = 0; i < 8; i++) msg.push_back(static_cast<uint8_t>(bitLength >> (8 * i)));
333
+
334
+ uint32_t a0 = 0x67452301, b0 = 0xefcdab89, c0 = 0x98badcfe, d0 = 0x10325476;
335
+ for (size_t chunk = 0; chunk < msg.size(); chunk += 64) {
336
+ uint32_t m[16];
337
+ for (int i = 0; i < 16; i++) {
338
+ const size_t o = chunk + static_cast<size_t>(i) * 4;
339
+ m[i] = static_cast<uint32_t>(msg[o]) | (static_cast<uint32_t>(msg[o + 1]) << 8) |
340
+ (static_cast<uint32_t>(msg[o + 2]) << 16) | (static_cast<uint32_t>(msg[o + 3]) << 24);
341
+ }
342
+ uint32_t a = a0, b = b0, c = c0, d = d0;
343
+ for (int i = 0; i < 64; i++) {
344
+ uint32_t f;
345
+ int g;
346
+ switch (i / 16) {
347
+ case 0: f = (b & c) | (~b & d); g = i; break;
348
+ case 1: f = (d & b) | (~d & c); g = (5 * i + 1) % 16; break;
349
+ case 2: f = b ^ c ^ d; g = (3 * i + 5) % 16; break;
350
+ default: f = c ^ (b | ~d); g = (7 * i) % 16; break;
351
+ }
352
+ f = f + a + k[i] + m[g];
353
+ a = d;
354
+ d = c;
355
+ c = b;
356
+ b = b + ((f << s[i]) | (f >> (32 - s[i])));
357
+ }
358
+ a0 += a; b0 += b; c0 += c; d0 += d;
359
+ }
360
+ Bytes out;
361
+ for (uint32_t word : {a0, b0, c0, d0}) {
362
+ for (int i = 0; i < 4; i++) out.push_back(static_cast<uint8_t>(word >> (8 * i)));
363
+ }
364
+ return out;
365
+ }
366
+
367
+ Bytes concat(std::initializer_list<Bytes> parts) {
368
+ Bytes out;
369
+ for (const auto& part : parts) out.insert(out.end(), part.begin(), part.end());
370
+ return out;
371
+ }
372
+
373
+ std::string hex(const Bytes& bytes) {
374
+ std::string out;
375
+ char buffer[3];
376
+ for (uint8_t b : bytes) {
377
+ std::snprintf(buffer, sizeof(buffer), "%02X", b);
378
+ out += buffer;
379
+ }
380
+ return out;
381
+ }
382
+
383
+ } // namespace
384
+
385
+ Bytes encrypted() {
386
+ const std::string contentString = "1 0 0 rg 0 0 200 200 re f\n";
387
+ const Bytes content(contentString.begin(), contentString.end());
388
+
389
+ const Bytes fileId = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
390
+ 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10};
391
+ const int32_t permissions = -1;
392
+ const Bytes pBytes = {static_cast<uint8_t>(permissions), static_cast<uint8_t>(permissions >> 8),
393
+ static_cast<uint8_t>(permissions >> 16), static_cast<uint8_t>(permissions >> 24)};
394
+
395
+ // Algorithm 3: /O is the padded USER password, RC4'd with the MD5 of the
396
+ // padded OWNER password.
397
+ Bytes ownerDigest = md5(padded(kOwnerPassword));
398
+ const Bytes ownerKey(ownerDigest.begin(), ownerDigest.begin() + 5);
399
+ const Bytes o = rc4(ownerKey, padded(kUserPassword));
400
+
401
+ // Algorithm 2: the file encryption key.
402
+ Bytes fileDigest = md5(concat({padded(kUserPassword), o, pBytes, fileId}));
403
+ const Bytes fileKey(fileDigest.begin(), fileDigest.begin() + 5);
404
+
405
+ // Algorithm 4 (R=2): /U is the pad string RC4'd with the file key.
406
+ const Bytes u = rc4(fileKey, Bytes(kPad, kPad + 32));
407
+
408
+ // Algorithm 1: per-object key. Object 1 is the content stream.
409
+ const Bytes extra = {1, 0, 0, 0, 0};
410
+ Bytes objectDigest = md5(concat({fileKey, extra}));
411
+ const Bytes objectKey(objectDigest.begin(), objectDigest.begin() + 10);
412
+ const Bytes encryptedContent = rc4(objectKey, content);
413
+
414
+ Writer w;
415
+ Bytes streamObj;
416
+ const std::string head = "<< /Length " + std::to_string(encryptedContent.size()) + " >>\nstream\n";
417
+ streamObj.insert(streamObj.end(), head.begin(), head.end());
418
+ streamObj.insert(streamObj.end(), encryptedContent.begin(), encryptedContent.end());
419
+ const std::string tail = "\nendstream";
420
+ streamObj.insert(streamObj.end(), tail.begin(), tail.end());
421
+ const int contents = w.add(streamObj);
422
+ const int pageObj = w.add("<< /Type /Page /Parent 3 0 R /MediaBox [0 0 200 200] /Contents " +
423
+ std::to_string(contents) + " 0 R /Resources << >> >>");
424
+ const int pages = w.add("<< /Type /Pages /Kids [" + std::to_string(pageObj) + " 0 R] /Count 1 >>");
425
+ const int catalog = w.add("<< /Type /Catalog /Pages " + std::to_string(pages) + " 0 R >>");
426
+ const int encrypt = w.add("<< /Filter /Standard /V 1 /R 2 /O <" + hex(o) + "> /U <" + hex(u) +
427
+ "> /P " + std::to_string(permissions) + " >>");
428
+ if (pages != 3) throw std::logic_error("pages object is not 3");
429
+ return w.finish(catalog, "/Encrypt " + std::to_string(encrypt) + " 0 R /ID [<" + hex(fileId) +
430
+ "> <" + hex(fileId) + ">]");
431
+ }
432
+
433
+ /* ------------------------------------------------------------------ *
434
+ * Files
435
+ * ------------------------------------------------------------------ */
436
+
437
+ std::string tempDir() {
438
+ const auto dir = std::filesystem::temp_directory_path() / "pdfcanvas-native-tests";
439
+ std::filesystem::create_directories(dir);
440
+ return dir.string();
441
+ }
442
+
443
+ std::string writeTemp(const Bytes& bytes, const std::string& name) {
444
+ const auto path = std::filesystem::path(tempDir()) / name;
445
+ std::ofstream out(path, std::ios::binary | std::ios::trunc);
446
+ out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
447
+ out.close();
448
+ return path.string();
449
+ }
450
+
451
+ } // namespace fixtures
@@ -0,0 +1,96 @@
1
+ // PDFs built in memory, so a test has no asset to ship and the content is known
2
+ // exactly rather than eyeballed.
3
+ //
4
+ // A PORT OF THE RETIRED `TestPdfs.swift` / `TestPdfs.java`, now the ONE copy:
5
+ // every platform is proven against the same bytes, so a rendering disagreement
6
+ // can only be a rendering disagreement. Every fixture is hand-written PDF
7
+ // syntax — the bytes are the input, and nothing about what they mean depends on
8
+ // the code under test.
9
+ #pragma once
10
+
11
+ #include <cstdint>
12
+ #include <string>
13
+ #include <vector>
14
+
15
+ namespace fixtures {
16
+
17
+ using Bytes = std::vector<uint8_t>;
18
+
19
+ constexpr int kLetterW = 612;
20
+ constexpr int kLetterH = 792;
21
+ /// ARCH-D, the sheet size this package exists for.
22
+ constexpr int kArchDW = 1728;
23
+ constexpr int kArchDH = 2592;
24
+
25
+ struct Box {
26
+ int x0, y0, x1, y1;
27
+ };
28
+
29
+ /// Four saturated quadrants in PDF user space (y-up): red bottom-left, green
30
+ /// bottom-right, blue top-left, black top-right. Saturated is what makes a
31
+ /// channel-order test possible at all; quadrants make a stride error visible;
32
+ /// and because the four differ in both axes, a y-flip and an x/y transposition
33
+ /// each move half the pixels.
34
+ Bytes quadrants(int width, int height, int rotate = 0);
35
+
36
+ /// Detail at every spatial frequency: a `pitch`-point grid catches a scale
37
+ /// error, the two diagonals catch a transposition. Also the shape a real
38
+ /// drawing has — hundreds of stroked lines.
39
+ Bytes grid(int width, int height, int pitch = 8);
40
+
41
+ /// A 612x792 page carrying `/Rotate degrees`, red filling the UNROTATED
42
+ /// top-left 112x100 points and blue the unrotated bottom-left.
43
+ Bytes rotated(int degrees);
44
+
45
+ /// MediaBox 0 0 400 400, CropBox 100 150 300 350, a red marker at the crop
46
+ /// box's own top-left and a blue one at its bottom-right.
47
+ Bytes offsetCropBox();
48
+
49
+ /// A CropBox that overhangs its MediaBox, so the intersection is testable.
50
+ Bytes cropBoxOverhangingMediaBox();
51
+
52
+ /// An antialiased opaque-white circle on an empty page: rendered transparent
53
+ /// this decides the alpha encoding.
54
+ Bytes antialiasedWhiteEdge();
55
+
56
+ /// A page whose only GREEN is inside a `/Square` annotation's appearance
57
+ /// stream. Content: white page with a red marker.
58
+ Bytes annotatedPage();
59
+
60
+ /// A page whose only BLUE is a text-field WIDGET's appearance stream, reachable
61
+ /// through the document's AcroForm. Content: white page with a red marker.
62
+ Bytes formFieldPage();
63
+
64
+ /// A single page with explicit boxes, rotation and content.
65
+ Bytes page(Box mediaBox, const Box* cropBox, int rotate, const std::string& content);
66
+
67
+ struct PageSpec {
68
+ int width;
69
+ int height;
70
+ int rotate;
71
+ std::string content;
72
+ };
73
+ Bytes multiPage(const std::vector<PageSpec>& pages);
74
+
75
+ /// Letter, ARCH-D, and 200x900 — three sizes, so geometry is per page.
76
+ Bytes mixedSizeDocument();
77
+ /// One flat colour per page.
78
+ Bytes coloredPages(const std::vector<std::array<double, 3>>& colors, int width, int height);
79
+
80
+ /// A valid `%PDF-` header and nothing else that parses.
81
+ Bytes headerOnlyGarbage();
82
+ /// No `%PDF-` header anywhere near the front.
83
+ Bytes notAPdfAtAll();
84
+
85
+ /// A 200x200 red page encrypted with the standard security handler (R=2, V=1,
86
+ /// RC4-40), user password `kUserPassword`, owner password `kOwnerPassword`.
87
+ extern const char* const kUserPassword;
88
+ extern const char* const kOwnerPassword;
89
+ Bytes encrypted();
90
+
91
+ /// Writes bytes to a fresh file under the system temp dir and returns its path.
92
+ std::string writeTemp(const Bytes& bytes, const std::string& name);
93
+ /// The temp directory fixtures write into.
94
+ std::string tempDir();
95
+
96
+ } // namespace fixtures
@@ -0,0 +1,122 @@
1
+ // A hundred lines of test harness, so the core takes no test dependency.
2
+ //
3
+ // `TEST(name) { ... }` registers a case; `CHECK*` record a failure and keep
4
+ // going within the case (so one report names every wrong pixel, not the first);
5
+ // `REQUIRE*` abort the case. `EXPECT_ERROR(expr, code)` is the one that earns
6
+ // its place: every failure mode in this package is a `PdfError` with a code a
7
+ // UI can switch on, and "it threw" is not the same as "it threw the right one".
8
+ #pragma once
9
+
10
+ #include <cmath>
11
+ #include <functional>
12
+ #include <sstream>
13
+ #include <string>
14
+ #include <vector>
15
+
16
+ #include "pdfcanvas/error.h"
17
+
18
+ namespace harness {
19
+
20
+ struct Failure {
21
+ std::string file;
22
+ int line;
23
+ std::string message;
24
+ };
25
+
26
+ struct Context {
27
+ std::vector<Failure> failures;
28
+ void fail(const char* file, int line, std::string message) {
29
+ failures.push_back({file, line, std::move(message)});
30
+ }
31
+ };
32
+
33
+ Context& current();
34
+
35
+ struct Case {
36
+ const char* name;
37
+ std::function<void()> body;
38
+ };
39
+
40
+ std::vector<Case>& cases();
41
+
42
+ struct Registrar {
43
+ Registrar(const char* name, void (*body)()) { cases().push_back({name, body}); }
44
+ };
45
+
46
+ /// Aborts the current case. Caught by the runner.
47
+ struct Abort {};
48
+
49
+ template <typename T>
50
+ std::string show(const T& value) {
51
+ std::ostringstream out;
52
+ out << value;
53
+ return out.str();
54
+ }
55
+
56
+ } // namespace harness
57
+
58
+ #define TEST(name) \
59
+ static void name(); \
60
+ static ::harness::Registrar registrar_##name(#name, &name); \
61
+ static void name()
62
+
63
+ #define CHECK(cond) \
64
+ do { \
65
+ if (!(cond)) ::harness::current().fail(__FILE__, __LINE__, "CHECK failed: " #cond); \
66
+ } while (0)
67
+
68
+ #define CHECK_MSG(cond, msg) \
69
+ do { \
70
+ if (!(cond)) \
71
+ ::harness::current().fail(__FILE__, __LINE__, \
72
+ std::string("CHECK failed: " #cond " — ") + (msg)); \
73
+ } while (0)
74
+
75
+ #define CHECK_EQ(a, b) \
76
+ do { \
77
+ const auto _va = (a); \
78
+ const auto _vb = (b); \
79
+ if (!(_va == _vb)) \
80
+ ::harness::current().fail(__FILE__, __LINE__, \
81
+ std::string("CHECK_EQ failed: " #a " == " #b " (") + \
82
+ ::harness::show(_va) + " vs " + ::harness::show(_vb) + ")"); \
83
+ } while (0)
84
+
85
+ #define CHECK_NEAR(a, b, tol) \
86
+ do { \
87
+ const double _va = static_cast<double>(a); \
88
+ const double _vb = static_cast<double>(b); \
89
+ if (std::fabs(_va - _vb) > (tol)) \
90
+ ::harness::current().fail(__FILE__, __LINE__, \
91
+ std::string("CHECK_NEAR failed: " #a " ~ " #b " (") + \
92
+ ::harness::show(_va) + " vs " + ::harness::show(_vb) + ")"); \
93
+ } while (0)
94
+
95
+ #define REQUIRE(cond) \
96
+ do { \
97
+ if (!(cond)) { \
98
+ ::harness::current().fail(__FILE__, __LINE__, "REQUIRE failed: " #cond); \
99
+ throw ::harness::Abort{}; \
100
+ } \
101
+ } while (0)
102
+
103
+ /// The expression must throw a `PdfError` whose code is `expected`.
104
+ #define EXPECT_ERROR(expr, expected) \
105
+ do { \
106
+ bool _threw = false; \
107
+ try { \
108
+ (void)(expr); \
109
+ } catch (const ::pdfcanvas::PdfError& _error) { \
110
+ _threw = true; \
111
+ if (_error.code() != (expected)) \
112
+ ::harness::current().fail( \
113
+ __FILE__, __LINE__, \
114
+ std::string("EXPECT_ERROR: " #expr " threw ") + \
115
+ ::pdfcanvas::wireName(_error.code()) + " (" + _error.message() + \
116
+ "), expected " + ::pdfcanvas::wireName(expected)); \
117
+ } \
118
+ if (!_threw) \
119
+ ::harness::current().fail(__FILE__, __LINE__, \
120
+ std::string("EXPECT_ERROR: " #expr " did not throw; expected ") + \
121
+ ::pdfcanvas::wireName(expected)); \
122
+ } while (0)