@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +68 -39
- package/README.md +213 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +487 -0
- package/scripts/pdfium-manifest.json +46 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#include <chrono>
|
|
2
|
+
#include <cstdio>
|
|
3
|
+
#include <cstring>
|
|
4
|
+
#include <exception>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
#include "harness.h"
|
|
8
|
+
|
|
9
|
+
namespace harness {
|
|
10
|
+
|
|
11
|
+
Context& current() {
|
|
12
|
+
static Context context;
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
std::vector<Case>& cases() {
|
|
17
|
+
static std::vector<Case> all;
|
|
18
|
+
return all;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
} // namespace harness
|
|
22
|
+
|
|
23
|
+
int main(int argc, char** argv) {
|
|
24
|
+
const char* filter = nullptr;
|
|
25
|
+
for (int i = 1; i < argc; i++) {
|
|
26
|
+
if (std::strcmp(argv[i], "--filter") == 0 && i + 1 < argc) filter = argv[++i];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
int ran = 0;
|
|
30
|
+
int failed = 0;
|
|
31
|
+
for (const auto& test : harness::cases()) {
|
|
32
|
+
if (filter != nullptr && std::strstr(test.name, filter) == nullptr) continue;
|
|
33
|
+
ran++;
|
|
34
|
+
harness::current().failures.clear();
|
|
35
|
+
const auto started = std::chrono::steady_clock::now();
|
|
36
|
+
try {
|
|
37
|
+
test.body();
|
|
38
|
+
} catch (const harness::Abort&) {
|
|
39
|
+
// Already recorded.
|
|
40
|
+
} catch (const pdfcanvas::PdfError& error) {
|
|
41
|
+
harness::current().fail("<runner>", 0,
|
|
42
|
+
std::string("unexpected PdfError ") +
|
|
43
|
+
pdfcanvas::wireName(error.code()) + ": " + error.message());
|
|
44
|
+
} catch (const std::exception& error) {
|
|
45
|
+
harness::current().fail("<runner>", 0,
|
|
46
|
+
std::string("unexpected exception: ") + error.what());
|
|
47
|
+
}
|
|
48
|
+
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
49
|
+
std::chrono::steady_clock::now() - started)
|
|
50
|
+
.count();
|
|
51
|
+
if (harness::current().failures.empty()) {
|
|
52
|
+
std::printf("PASS %-64s %5lld ms\n", test.name, static_cast<long long>(ms));
|
|
53
|
+
} else {
|
|
54
|
+
failed++;
|
|
55
|
+
std::printf("FAIL %s\n", test.name);
|
|
56
|
+
for (const auto& failure : harness::current().failures) {
|
|
57
|
+
std::printf(" %s:%d: %s\n", failure.file.c_str(), failure.line,
|
|
58
|
+
failure.message.c_str());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
std::printf("---\n%d cases, %d failed\n", ran, failed);
|
|
63
|
+
return failed == 0 && ran > 0 ? 0 : 1;
|
|
64
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Reading pixels back, and the vocabulary every rendering test is written in.
|
|
2
|
+
//
|
|
3
|
+
// `RasterPixels` is indexed from the TOP-LEFT, because that is what doc space
|
|
4
|
+
// is. The fixtures are y-UP in PDF space, so a quadrant that is "bottom-left"
|
|
5
|
+
// in the PDF reads at the bottom-left of the raster too — the flip is PDFium's,
|
|
6
|
+
// not the test's, and a test that reasoned in PDF coordinates would be testing
|
|
7
|
+
// its own arithmetic against the library's.
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <algorithm>
|
|
11
|
+
#include <cstdlib>
|
|
12
|
+
#include <string>
|
|
13
|
+
|
|
14
|
+
#include "harness.h"
|
|
15
|
+
#include "pdfcanvas/document.h"
|
|
16
|
+
|
|
17
|
+
namespace px {
|
|
18
|
+
|
|
19
|
+
struct Pixel {
|
|
20
|
+
int r, g, b, a;
|
|
21
|
+
bool operator==(const Pixel& o) const { return r == o.r && g == o.g && b == o.b && a == o.a; }
|
|
22
|
+
/// Within `tolerance` on every channel. Antialiasing makes exact equality
|
|
23
|
+
/// wrong near an edge.
|
|
24
|
+
bool near(const Pixel& o, int tolerance = 4) const {
|
|
25
|
+
return std::abs(r - o.r) <= tolerance && std::abs(g - o.g) <= tolerance &&
|
|
26
|
+
std::abs(b - o.b) <= tolerance && std::abs(a - o.a) <= tolerance;
|
|
27
|
+
}
|
|
28
|
+
std::string str() const {
|
|
29
|
+
return "[" + std::to_string(r) + "," + std::to_string(g) + "," + std::to_string(b) + "," +
|
|
30
|
+
std::to_string(a) + "]";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
inline const Pixel kRed{255, 0, 0, 255};
|
|
35
|
+
inline const Pixel kGreen{0, 255, 0, 255};
|
|
36
|
+
inline const Pixel kBlue{0, 0, 255, 255};
|
|
37
|
+
inline const Pixel kBlack{0, 0, 0, 255};
|
|
38
|
+
inline const Pixel kWhite{255, 255, 255, 255};
|
|
39
|
+
inline const Pixel kClear{0, 0, 0, 0};
|
|
40
|
+
inline const Pixel kGrey{128, 128, 128, 255};
|
|
41
|
+
|
|
42
|
+
inline Pixel at(const pdfcanvas::RasterPixels& raster, int x, int y) {
|
|
43
|
+
return {raster.channel(x, y, 0), raster.channel(x, y, 1), raster.channel(x, y, 2),
|
|
44
|
+
raster.channel(x, y, 3)};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// A coarse letter map, so a failure in the log says what happened.
|
|
48
|
+
inline std::string map(const pdfcanvas::RasterPixels& raster, int cols = 16, int rows = 16) {
|
|
49
|
+
std::string out;
|
|
50
|
+
for (int row = 0; row < rows; row++) {
|
|
51
|
+
for (int col = 0; col < cols; col++) {
|
|
52
|
+
const int x = std::min(raster.width - 1, (col * raster.width) / cols + raster.width / (cols * 2));
|
|
53
|
+
const int y = std::min(raster.height - 1, (row * raster.height) / rows + raster.height / (rows * 2));
|
|
54
|
+
const Pixel p = at(raster, x, y);
|
|
55
|
+
if (p.a == 0) out += '_';
|
|
56
|
+
else if (p.r > 200 && p.g > 200 && p.b > 200) out += 'W';
|
|
57
|
+
else if (p.r > 200 && p.g < 60 && p.b < 60) out += 'r';
|
|
58
|
+
else if (p.g > 200 && p.r < 60 && p.b < 60) out += 'g';
|
|
59
|
+
else if (p.b > 200 && p.r < 60 && p.g < 60) out += 'b';
|
|
60
|
+
else if (p.r < 60 && p.g < 60 && p.b < 60) out += 'k';
|
|
61
|
+
else out += '.';
|
|
62
|
+
}
|
|
63
|
+
out += '\n';
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
struct Diff {
|
|
69
|
+
int differing = 0;
|
|
70
|
+
int maxDelta = 0;
|
|
71
|
+
int total = 0;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/// Compares `a` against the region of `b` starting at (offsetX, offsetY).
|
|
75
|
+
inline Diff compare(const pdfcanvas::RasterPixels& a, const pdfcanvas::RasterPixels& b,
|
|
76
|
+
int offsetX = 0, int offsetY = 0) {
|
|
77
|
+
Diff diff;
|
|
78
|
+
for (int y = 0; y < a.height; y++) {
|
|
79
|
+
for (int x = 0; x < a.width; x++) {
|
|
80
|
+
const Pixel pa = at(a, x, y);
|
|
81
|
+
const Pixel pb = at(b, x + offsetX, y + offsetY);
|
|
82
|
+
const int d = std::max({std::abs(pa.r - pb.r), std::abs(pa.g - pb.g), std::abs(pa.b - pb.b),
|
|
83
|
+
std::abs(pa.a - pb.a)});
|
|
84
|
+
if (d != 0) diff.differing++;
|
|
85
|
+
diff.maxDelta = std::max(diff.maxDelta, d);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
diff.total = a.width * a.height;
|
|
89
|
+
return diff;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
inline pdfcanvas::RasterRequest request(int page, double x, double y, double width, double height,
|
|
93
|
+
double scale, bool annotations = false,
|
|
94
|
+
pdfcanvas::Background background = pdfcanvas::Background::White) {
|
|
95
|
+
pdfcanvas::RasterRequest r;
|
|
96
|
+
r.page = page;
|
|
97
|
+
r.x = x;
|
|
98
|
+
r.y = y;
|
|
99
|
+
r.width = width;
|
|
100
|
+
r.height = height;
|
|
101
|
+
r.scale = scale;
|
|
102
|
+
r.annotations = annotations;
|
|
103
|
+
r.background = background;
|
|
104
|
+
return r;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Renders the whole of `page` at `scale` to the heap.
|
|
108
|
+
inline pdfcanvas::RasterPixels renderWhole(pdfcanvas::Document& document, int page = 0,
|
|
109
|
+
double scale = 1, bool annotations = false,
|
|
110
|
+
pdfcanvas::Background background = pdfcanvas::Background::White) {
|
|
111
|
+
const auto& geometry = document.geometry(page);
|
|
112
|
+
return document.render(request(page, 0, 0, geometry.width, geometry.height, scale, annotations, background),
|
|
113
|
+
nullptr, nullptr);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
} // namespace px
|
|
117
|
+
|
|
118
|
+
#define CHECK_PIXEL(raster, x, y, expected) \
|
|
119
|
+
do { \
|
|
120
|
+
const ::px::Pixel _actual = ::px::at((raster), (x), (y)); \
|
|
121
|
+
if (!_actual.near(expected)) \
|
|
122
|
+
::harness::current().fail(__FILE__, __LINE__, \
|
|
123
|
+
"(" + std::to_string(x) + "," + std::to_string(y) + ") is " + \
|
|
124
|
+
_actual.str() + ", expected " + (expected).str() + "\n" + \
|
|
125
|
+
::px::map(raster)); \
|
|
126
|
+
} while (0)
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
// What the core actually draws.
|
|
2
|
+
//
|
|
3
|
+
// Every test here names the bug it catches, because every one of these failure
|
|
4
|
+
// modes produces a plausible image rather than an error: a channel swap, a
|
|
5
|
+
// stride shear, a y-flip, a page ignored, an annotation drawn when it was not
|
|
6
|
+
// asked for, a transparent page composited as black.
|
|
7
|
+
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <thread>
|
|
10
|
+
|
|
11
|
+
#include "fixtures.h"
|
|
12
|
+
#include "harness.h"
|
|
13
|
+
#include "pdfcanvas/document.h"
|
|
14
|
+
#include "pixels_util.h"
|
|
15
|
+
|
|
16
|
+
using namespace pdfcanvas;
|
|
17
|
+
using px::kBlack;
|
|
18
|
+
using px::kBlue;
|
|
19
|
+
using px::kClear;
|
|
20
|
+
using px::kGreen;
|
|
21
|
+
using px::kGrey;
|
|
22
|
+
using px::kRed;
|
|
23
|
+
using px::kWhite;
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
|
|
27
|
+
std::unique_ptr<Document> openFixture(const fixtures::Bytes& bytes, const std::string& name,
|
|
28
|
+
const char* password = nullptr) {
|
|
29
|
+
Source source;
|
|
30
|
+
source.path = fixtures::writeTemp(bytes, name);
|
|
31
|
+
if (password != nullptr) {
|
|
32
|
+
source.hasPassword = true;
|
|
33
|
+
source.password = password;
|
|
34
|
+
}
|
|
35
|
+
return Document::open(source);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
} // namespace
|
|
39
|
+
|
|
40
|
+
/* ------------------------------------------------------------------ *
|
|
41
|
+
* The doc-space contract
|
|
42
|
+
* ------------------------------------------------------------------ */
|
|
43
|
+
|
|
44
|
+
/// The four quadrants land where doc space says, which pins the y-flip, the
|
|
45
|
+
/// channel order (RGBA, via FPDF_REVERSE_BYTE_ORDER) and the stride at once.
|
|
46
|
+
TEST(quadrantsLandWhereDocSpaceSaysTheyShould) {
|
|
47
|
+
auto document = openFixture(fixtures::quadrants(200, 100), "quadrants.pdf");
|
|
48
|
+
auto raster = px::renderWhole(*document, 0, 2);
|
|
49
|
+
CHECK_EQ(raster.width, 400);
|
|
50
|
+
CHECK_EQ(raster.height, 200);
|
|
51
|
+
CHECK_EQ(raster.rowBytes, 1600);
|
|
52
|
+
CHECK_PIXEL(raster, 20, 20, kBlue);
|
|
53
|
+
CHECK_PIXEL(raster, 380, 20, kBlack);
|
|
54
|
+
CHECK_PIXEL(raster, 20, 180, kRed);
|
|
55
|
+
CHECK_PIXEL(raster, 380, 180, kGreen);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// A tile is the same region cropped from a whole-page render — THE PROPERTY
|
|
59
|
+
/// THE ENTIRE DETAIL LAYER RESTS ON. The tile's origin and size are
|
|
60
|
+
/// deliberately not round numbers.
|
|
61
|
+
///
|
|
62
|
+
/// `maxDelta <= 1`, not 0: PDFium's AGG rasterizer accumulates antialiased
|
|
63
|
+
/// coverage in device space, so a shallow diagonal edge can come out one level
|
|
64
|
+
/// different when the bitmap origin moves — the web backend measured the same
|
|
65
|
+
/// (24 of 9,600 pixels, always by exactly one level) and spike S1 measured it
|
|
66
|
+
/// on Android's PdfRenderer, which is the same engine. A registration error
|
|
67
|
+
/// moves whole pixels of this high-frequency pattern and cannot hide under it.
|
|
68
|
+
TEST(aTileIsIdenticalToThatRegionOfAWholePageRender) {
|
|
69
|
+
auto document = openFixture(fixtures::grid(200, 100), "grid-tile.pdf");
|
|
70
|
+
const double scale = 4.0;
|
|
71
|
+
auto whole = px::renderWhole(*document, 0, scale);
|
|
72
|
+
auto tile = document->render(px::request(0, 37, 23, 64, 41, scale), nullptr, nullptr);
|
|
73
|
+
CHECK_EQ(tile.width, 256);
|
|
74
|
+
CHECK_EQ(tile.height, 164);
|
|
75
|
+
const auto diff = px::compare(tile, whole, static_cast<int>(37 * scale), static_cast<int>(23 * scale));
|
|
76
|
+
const std::string summary = std::to_string(diff.differing) + "/" + std::to_string(diff.total) +
|
|
77
|
+
" pixels differ, maxDelta " + std::to_string(diff.maxDelta);
|
|
78
|
+
CHECK_MSG(diff.maxDelta <= 1, summary);
|
|
79
|
+
CHECK_MSG(diff.differing * 100 < diff.total * 2, summary);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Two tiles at the same scale agree over their overlap, with heights that
|
|
83
|
+
/// round in OPPOSITE directions.
|
|
84
|
+
TEST(rastersAtTheSameScaleAgreeWhereTheyOverlap) {
|
|
85
|
+
auto document = openFixture(fixtures::grid(200, 200), "grid-overlap.pdf");
|
|
86
|
+
const double scale = 4.0;
|
|
87
|
+
auto a = document->render(px::request(0, 10, 10, 50.1, 25.1, scale), nullptr, nullptr);
|
|
88
|
+
auto b = document->render(px::request(0, 10, 20, 50.1, 25.4, scale), nullptr, nullptr);
|
|
89
|
+
CHECK(a.height != b.height);
|
|
90
|
+
const int overlapRows = static_cast<int>((35.1 - 20) * scale) - 1;
|
|
91
|
+
int differing = 0;
|
|
92
|
+
for (int row = 0; row < overlapRows; row++) {
|
|
93
|
+
for (int x = 0; x < a.width; x++) {
|
|
94
|
+
if (!(px::at(a, x, row + static_cast<int>(10 * scale)) == px::at(b, x, row))) differing++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
CHECK_EQ(differing, 0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// The rounding rule is `round`, at least 1 — the planner's rule.
|
|
101
|
+
TEST(pixelExtentsRoundAndFloorAtOne) {
|
|
102
|
+
CHECK_EQ(px::request(0, 0, 0, 128.125, 128.1, 4).pixelWidth(), 513);
|
|
103
|
+
CHECK_EQ(px::request(0, 0, 0, 128.125, 128.1, 4).pixelHeight(), 512);
|
|
104
|
+
CHECK_EQ(px::request(0, 0, 0, 0.01, 0.01, 1).pixelWidth(), 1);
|
|
105
|
+
CHECK_EQ(px::request(0, 0, 0, 0.01, 0.01, 1).pixelHeight(), 1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Cost is flat in scale: the engine clips rather than rasterizing the whole
|
|
109
|
+
/// page and discarding it. Not a threshold in milliseconds — a shape assertion.
|
|
110
|
+
TEST(costIsFlatInScale) {
|
|
111
|
+
auto document = openFixture(fixtures::grid(fixtures::kArchDW, fixtures::kArchDH), "grid-archd.pdf");
|
|
112
|
+
const double side = 256;
|
|
113
|
+
auto best = [&](double scale) {
|
|
114
|
+
double best = 1e9;
|
|
115
|
+
for (int i = 0; i < 5; i++) {
|
|
116
|
+
const auto started = std::chrono::steady_clock::now();
|
|
117
|
+
(void)document->render(px::request(0, 100, 100, side / scale, side / scale, scale), nullptr, nullptr);
|
|
118
|
+
const double ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
|
|
119
|
+
best = std::min(best, ms);
|
|
120
|
+
}
|
|
121
|
+
return best;
|
|
122
|
+
};
|
|
123
|
+
(void)best(1);
|
|
124
|
+
const double one = best(1);
|
|
125
|
+
const double eight = best(8);
|
|
126
|
+
CHECK_MSG(eight < one * 8 + 10,
|
|
127
|
+
"256px at 8x took " + std::to_string(eight) + "ms against " + std::to_string(one) + "ms at 1x");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ------------------------------------------------------------------ *
|
|
131
|
+
* Backgrounds and alpha
|
|
132
|
+
* ------------------------------------------------------------------ */
|
|
133
|
+
|
|
134
|
+
TEST(whiteBackgroundFillsEverythingTheContentDoesNotPaint) {
|
|
135
|
+
auto document = openFixture(fixtures::page({0, 0, 100, 100}, nullptr, 0, "1 0 0 rg 0 0 40 40 re f\n"),
|
|
136
|
+
"sparse.pdf");
|
|
137
|
+
auto raster = px::renderWhole(*document, 0, 1, false, Background::White);
|
|
138
|
+
CHECK_PIXEL(raster, 90, 10, kWhite);
|
|
139
|
+
CHECK_PIXEL(raster, 50, 50, kWhite);
|
|
140
|
+
CHECK_PIXEL(raster, 10, 90, kRed);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
TEST(transparentBackgroundLeavesUnpaintedAreaClear) {
|
|
144
|
+
auto document = openFixture(fixtures::page({0, 0, 100, 100}, nullptr, 0, "1 0 0 rg 0 0 40 40 re f\n"),
|
|
145
|
+
"sparse-clear.pdf");
|
|
146
|
+
auto raster = px::renderWhole(*document, 0, 1, false, Background::Transparent);
|
|
147
|
+
CHECK_PIXEL(raster, 90, 10, kClear);
|
|
148
|
+
CHECK_PIXEL(raster, 10, 90, kRed);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// ALPHA IS STRAIGHT, measured off the bytes. A partially covered opaque-white
|
|
152
|
+
/// edge reads `[255,255,255,a]`; premultiplied would be `[a,a,a,a]`. The TS
|
|
153
|
+
/// adapter declares `'straight'` for both native platforms on the strength of
|
|
154
|
+
/// this — the retired cores were both premultiplied, and getting it wrong is
|
|
155
|
+
/// invisible under a white background and wrong under a transparent one.
|
|
156
|
+
TEST(alphaIsStraight) {
|
|
157
|
+
auto document = openFixture(fixtures::antialiasedWhiteEdge(), "aa-edge.pdf");
|
|
158
|
+
auto raster = px::renderWhole(*document, 0, 1, false, Background::Transparent);
|
|
159
|
+
int partial = 0;
|
|
160
|
+
int straight = 0;
|
|
161
|
+
for (int y = 0; y < raster.height; y++) {
|
|
162
|
+
for (int x = 0; x < raster.width; x++) {
|
|
163
|
+
const px::Pixel p = px::at(raster, x, y);
|
|
164
|
+
if (p.a > 10 && p.a < 245) {
|
|
165
|
+
partial++;
|
|
166
|
+
if (p.r >= 250 && p.g >= 250 && p.b >= 250) straight++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
CHECK_MSG(partial > 20, "an antialiased circle must have partially covered edge pixels; got " +
|
|
171
|
+
std::to_string(partial));
|
|
172
|
+
CHECK_EQ(partial, straight);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ------------------------------------------------------------------ *
|
|
176
|
+
* Annotations and forms
|
|
177
|
+
* ------------------------------------------------------------------ */
|
|
178
|
+
|
|
179
|
+
/// The green is INSIDE an annotation's appearance stream and nowhere else, so
|
|
180
|
+
/// green in the raster can only have come from compositing the annotation.
|
|
181
|
+
TEST(annotationsAreDrawnOnlyWhenAsked) {
|
|
182
|
+
auto document = openFixture(fixtures::annotatedPage(), "annotated.pdf");
|
|
183
|
+
auto with = px::renderWhole(*document, 0, 1, true);
|
|
184
|
+
auto without = px::renderWhole(*document, 0, 1, false);
|
|
185
|
+
// The annotation covers PDF (50,50)-(150,150), i.e. doc (50,50)-(150,150).
|
|
186
|
+
CHECK_PIXEL(with, 100, 100, kGreen);
|
|
187
|
+
CHECK_PIXEL(without, 100, 100, kWhite);
|
|
188
|
+
// The content stream is unaffected by the flag.
|
|
189
|
+
CHECK_PIXEL(with, 25, 175, kRed);
|
|
190
|
+
CHECK_PIXEL(without, 25, 175, kRed);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// FORM FIELDS ARE WIDGET ANNOTATIONS, and `FPDF_RenderPageBitmap` skips them
|
|
194
|
+
/// even with FPDF_ANNOT — they are drawn by `FPDF_FFLDraw` through the form-fill
|
|
195
|
+
/// environment. Without that second pass a filled-in form renders as a blank
|
|
196
|
+
/// form, which is exactly the "annotations are missing" report this rewrite
|
|
197
|
+
/// exists to close.
|
|
198
|
+
TEST(formFieldsAreDrawnWithAnnotations) {
|
|
199
|
+
auto document = openFixture(fixtures::formFieldPage(), "form.pdf");
|
|
200
|
+
auto with = px::renderWhole(*document, 0, 1, true);
|
|
201
|
+
auto without = px::renderWhole(*document, 0, 1, false);
|
|
202
|
+
// The widget covers PDF (50,50)-(150,100): doc y 100..150.
|
|
203
|
+
const px::Pixel inside = px::at(with, 100, 125);
|
|
204
|
+
CHECK_MSG(!inside.near(kWhite), "the widget must paint something at its centre; got " + inside.str() +
|
|
205
|
+
"\n" + px::map(with));
|
|
206
|
+
CHECK_PIXEL(without, 100, 125, kWhite);
|
|
207
|
+
CHECK_PIXEL(with, 25, 175, kRed);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Annotations tile exactly like content: a tile of the annotated region equals
|
|
211
|
+
/// that region of the whole-page render.
|
|
212
|
+
TEST(annotationTilesRegisterLikeContent) {
|
|
213
|
+
auto document = openFixture(fixtures::annotatedPage(), "annotated-tile.pdf");
|
|
214
|
+
auto whole = px::renderWhole(*document, 0, 3, true);
|
|
215
|
+
auto tile = document->render(px::request(0, 40, 40, 70, 55, 3, true), nullptr, nullptr);
|
|
216
|
+
const auto diff = px::compare(tile, whole, 120, 120);
|
|
217
|
+
CHECK_EQ(diff.differing, 0);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ------------------------------------------------------------------ *
|
|
221
|
+
* Geometry
|
|
222
|
+
* ------------------------------------------------------------------ */
|
|
223
|
+
|
|
224
|
+
TEST(pageGeometryIsPostRotation) {
|
|
225
|
+
auto document = openFixture(fixtures::rotated(90), "rotated-90.pdf");
|
|
226
|
+
CHECK_EQ(document->pageCount(), 1);
|
|
227
|
+
CHECK_NEAR(document->geometry(0).width, 792, 0.01);
|
|
228
|
+
CHECK_NEAR(document->geometry(0).height, 612, 0.01);
|
|
229
|
+
CHECK_EQ(document->geometry(0).rotation, 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// `/Rotate 90` is clockwise: the unrotated top-left marker (red) lands at the
|
|
233
|
+
/// displayed TOP-RIGHT and the unrotated bottom-left (blue) at the top-left.
|
|
234
|
+
TEST(rotatedPagesDrawUpright) {
|
|
235
|
+
{
|
|
236
|
+
auto document = openFixture(fixtures::rotated(90), "rotated-90-draw.pdf");
|
|
237
|
+
auto raster = px::renderWhole(*document, 0, 0.5);
|
|
238
|
+
CHECK_EQ(raster.width, 396);
|
|
239
|
+
CHECK_EQ(raster.height, 306);
|
|
240
|
+
CHECK_PIXEL(raster, 380, 20, kRed);
|
|
241
|
+
CHECK_PIXEL(raster, 20, 20, kBlue);
|
|
242
|
+
CHECK_PIXEL(raster, 200, 200, kWhite);
|
|
243
|
+
}
|
|
244
|
+
{
|
|
245
|
+
auto document = openFixture(fixtures::rotated(180), "rotated-180-draw.pdf");
|
|
246
|
+
auto raster = px::renderWhole(*document, 0, 0.5);
|
|
247
|
+
CHECK_EQ(raster.width, 306);
|
|
248
|
+
CHECK_EQ(raster.height, 396);
|
|
249
|
+
// Red (unrotated top-left) is now bottom-right; blue (bottom-left) is top-right.
|
|
250
|
+
CHECK_PIXEL(raster, 290, 380, kRed);
|
|
251
|
+
CHECK_PIXEL(raster, 290, 20, kBlue);
|
|
252
|
+
}
|
|
253
|
+
{
|
|
254
|
+
auto document = openFixture(fixtures::rotated(270), "rotated-270-draw.pdf");
|
|
255
|
+
auto raster = px::renderWhole(*document, 0, 0.5);
|
|
256
|
+
CHECK_EQ(raster.width, 396);
|
|
257
|
+
CHECK_PIXEL(raster, 20, 290, kRed);
|
|
258
|
+
CHECK_PIXEL(raster, 380, 290, kBlue);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
TEST(rotationIsReportedPerPage) {
|
|
263
|
+
auto document = openFixture(
|
|
264
|
+
fixtures::multiPage({{200, 100, 0, "1 0 0 rg 0 0 40 40 re f\n"},
|
|
265
|
+
{200, 100, 90, "1 0 0 rg 0 0 40 40 re f\n"},
|
|
266
|
+
{200, 100, 270, "1 0 0 rg 0 0 40 40 re f\n"}}),
|
|
267
|
+
"rotated-pages.pdf");
|
|
268
|
+
CHECK_NEAR(document->geometry(0).width, 200, 0.01);
|
|
269
|
+
CHECK_NEAR(document->geometry(1).width, 100, 0.01);
|
|
270
|
+
CHECK_NEAR(document->geometry(1).height, 200, 0.01);
|
|
271
|
+
CHECK_NEAR(document->geometry(2).width, 100, 0.01);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// The crop box origin is subtracted: with MediaBox 0 0 400 400 and CropBox
|
|
275
|
+
/// 100 150 300 350, the page is 200x200 and the red marker sits at doc (0,0).
|
|
276
|
+
TEST(cropBoxOriginIsSubtracted) {
|
|
277
|
+
auto document = openFixture(fixtures::offsetCropBox(), "cropbox.pdf");
|
|
278
|
+
CHECK_NEAR(document->geometry(0).width, 200, 0.01);
|
|
279
|
+
CHECK_NEAR(document->geometry(0).height, 200, 0.01);
|
|
280
|
+
auto raster = px::renderWhole(*document, 0, 1);
|
|
281
|
+
CHECK_PIXEL(raster, 20, 20, kRed);
|
|
282
|
+
CHECK_PIXEL(raster, 180, 180, kBlue);
|
|
283
|
+
CHECK_PIXEL(raster, 100, 100, kGrey);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
TEST(cropBoxIsIntersectedWithMediaBox) {
|
|
287
|
+
auto document = openFixture(fixtures::cropBoxOverhangingMediaBox(), "cropbox-overhang.pdf");
|
|
288
|
+
CHECK_NEAR(document->geometry(0).width, 150, 0.01);
|
|
289
|
+
CHECK_NEAR(document->geometry(0).height, 150, 0.01);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
TEST(eachPageReportsItsOwnSize) {
|
|
293
|
+
auto document = openFixture(fixtures::mixedSizeDocument(), "mixed.pdf");
|
|
294
|
+
CHECK_EQ(document->pageCount(), 3);
|
|
295
|
+
CHECK_NEAR(document->geometry(0).width, fixtures::kLetterW, 0.01);
|
|
296
|
+
CHECK_NEAR(document->geometry(1).width, fixtures::kArchDW, 0.01);
|
|
297
|
+
CHECK_NEAR(document->geometry(1).height, fixtures::kArchDH, 0.01);
|
|
298
|
+
CHECK_NEAR(document->geometry(2).width, 200, 0.01);
|
|
299
|
+
CHECK_NEAR(document->geometry(2).height, 900, 0.01);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
TEST(eachPageRendersItsOwnContent) {
|
|
303
|
+
auto document = openFixture(fixtures::coloredPages({{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}, 100, 100),
|
|
304
|
+
"colored.pdf");
|
|
305
|
+
CHECK_PIXEL(px::renderWhole(*document, 0), 50, 50, kRed);
|
|
306
|
+
CHECK_PIXEL(px::renderWhole(*document, 1), 50, 50, kGreen);
|
|
307
|
+
CHECK_PIXEL(px::renderWhole(*document, 2), 50, 50, kBlue);
|
|
308
|
+
// Back to the first, which the page cache (capacity 3) still holds; and
|
|
309
|
+
// then a fourth pattern that forces an eviction and a reload.
|
|
310
|
+
CHECK_PIXEL(px::renderWhole(*document, 0), 50, 50, kRed);
|
|
311
|
+
auto four = openFixture(fixtures::coloredPages({{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}, 50, 50),
|
|
312
|
+
"colored-4.pdf");
|
|
313
|
+
for (int round = 0; round < 3; round++) {
|
|
314
|
+
CHECK_PIXEL(px::renderWhole(*four, 0), 25, 25, kRed);
|
|
315
|
+
CHECK_PIXEL(px::renderWhole(*four, 1), 25, 25, kGreen);
|
|
316
|
+
CHECK_PIXEL(px::renderWhole(*four, 2), 25, 25, kBlue);
|
|
317
|
+
CHECK_PIXEL(px::renderWhole(*four, 3), 25, 25, kBlack);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
TEST(openBytesMatchesOpenFile) {
|
|
322
|
+
const auto bytes = fixtures::grid(120, 80);
|
|
323
|
+
auto fromFile = openFixture(bytes, "bytes-vs-file.pdf");
|
|
324
|
+
Source memory;
|
|
325
|
+
memory.bytes = bytes;
|
|
326
|
+
auto fromMemory = Document::open(memory);
|
|
327
|
+
auto a = px::renderWhole(*fromFile, 0, 2);
|
|
328
|
+
auto b = px::renderWhole(*fromMemory, 0, 2);
|
|
329
|
+
CHECK_EQ(px::compare(a, b).differing, 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ------------------------------------------------------------------ *
|
|
333
|
+
* Failures
|
|
334
|
+
* ------------------------------------------------------------------ */
|
|
335
|
+
|
|
336
|
+
TEST(openFailuresAreDistinguishable) {
|
|
337
|
+
Source missing;
|
|
338
|
+
missing.path = fixtures::tempDir() + "/does-not-exist.pdf";
|
|
339
|
+
EXPECT_ERROR(Document::open(missing), ErrorCode::NotFound);
|
|
340
|
+
|
|
341
|
+
Source directory;
|
|
342
|
+
directory.path = fixtures::tempDir();
|
|
343
|
+
EXPECT_ERROR(Document::open(directory), ErrorCode::NotFound);
|
|
344
|
+
|
|
345
|
+
Source empty;
|
|
346
|
+
empty.path = fixtures::writeTemp({}, "empty.pdf");
|
|
347
|
+
EXPECT_ERROR(Document::open(empty), ErrorCode::Corrupt);
|
|
348
|
+
|
|
349
|
+
Source emptyBytes;
|
|
350
|
+
EXPECT_ERROR(Document::open(emptyBytes), ErrorCode::Corrupt);
|
|
351
|
+
|
|
352
|
+
Source notPdf;
|
|
353
|
+
notPdf.path = fixtures::writeTemp(fixtures::notAPdfAtAll(), "not-a-pdf.pdf");
|
|
354
|
+
EXPECT_ERROR(Document::open(notPdf), ErrorCode::Corrupt);
|
|
355
|
+
|
|
356
|
+
Source garbage;
|
|
357
|
+
garbage.path = fixtures::writeTemp(fixtures::headerOnlyGarbage(), "header-only.pdf");
|
|
358
|
+
EXPECT_ERROR(Document::open(garbage), ErrorCode::Corrupt);
|
|
359
|
+
|
|
360
|
+
// And a good document still opens afterwards: nothing poisons the process,
|
|
361
|
+
// which is the API-24 `PdfRenderer` behaviour the old preflight existed for.
|
|
362
|
+
auto fine = openFixture(fixtures::quadrants(40, 40), "after-garbage.pdf");
|
|
363
|
+
CHECK_EQ(fine->pageCount(), 1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
TEST(encryptedDocuments) {
|
|
367
|
+
const auto bytes = fixtures::encrypted();
|
|
368
|
+
Source locked;
|
|
369
|
+
locked.path = fixtures::writeTemp(bytes, "encrypted.pdf");
|
|
370
|
+
EXPECT_ERROR(Document::open(locked), ErrorCode::PasswordRequired);
|
|
371
|
+
|
|
372
|
+
Source wrong = locked;
|
|
373
|
+
wrong.hasPassword = true;
|
|
374
|
+
wrong.password = "nope";
|
|
375
|
+
EXPECT_ERROR(Document::open(wrong), ErrorCode::PasswordIncorrect);
|
|
376
|
+
|
|
377
|
+
Source right = locked;
|
|
378
|
+
right.hasPassword = true;
|
|
379
|
+
right.password = fixtures::kUserPassword;
|
|
380
|
+
auto document = Document::open(right);
|
|
381
|
+
auto raster = px::renderWhole(*document, 0, 0.5);
|
|
382
|
+
CHECK_PIXEL(raster, 50, 50, kRed);
|
|
383
|
+
|
|
384
|
+
// The owner password opens it too — that is what an owner password is for.
|
|
385
|
+
Source owner = locked;
|
|
386
|
+
owner.hasPassword = true;
|
|
387
|
+
owner.password = fixtures::kOwnerPassword;
|
|
388
|
+
CHECK_EQ(Document::open(owner)->pageCount(), 1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
TEST(outOfRangePageIsNotFound) {
|
|
392
|
+
auto document = openFixture(fixtures::quadrants(40, 40), "range.pdf");
|
|
393
|
+
EXPECT_ERROR(document->geometry(1), ErrorCode::NotFound);
|
|
394
|
+
EXPECT_ERROR(document->geometry(-1), ErrorCode::NotFound);
|
|
395
|
+
EXPECT_ERROR(document->render(px::request(1, 0, 0, 10, 10, 1), nullptr, nullptr), ErrorCode::NotFound);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
TEST(degenerateRequestsAreRefused) {
|
|
399
|
+
auto document = openFixture(fixtures::quadrants(40, 40), "degenerate.pdf");
|
|
400
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 0, 10, 1), nullptr, nullptr), ErrorCode::BackendFailure);
|
|
401
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 10, -1, 1), nullptr, nullptr), ErrorCode::BackendFailure);
|
|
402
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 10, 10, 0), nullptr, nullptr), ErrorCode::BackendFailure);
|
|
403
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 10, 10, -2), nullptr, nullptr), ErrorCode::BackendFailure);
|
|
404
|
+
EXPECT_ERROR(document->render(px::request(0, 1.0 / 0.0, 0, 10, 10, 1), nullptr, nullptr), ErrorCode::BackendFailure);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
TEST(anAbsurdRasterIsRefused) {
|
|
408
|
+
auto document = openFixture(fixtures::quadrants(40, 40), "absurd.pdf");
|
|
409
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 40, 40, 300), nullptr, nullptr), ErrorCode::OutOfMemory);
|
|
410
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 1, 1, 1e12), nullptr, nullptr), ErrorCode::OutOfMemory);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
TEST(renderingAfterCloseFails) {
|
|
414
|
+
auto document = openFixture(fixtures::quadrants(40, 40), "closed.pdf");
|
|
415
|
+
document->close();
|
|
416
|
+
document->close();
|
|
417
|
+
CHECK(document->isClosed());
|
|
418
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 10, 10, 1), nullptr, nullptr), ErrorCode::BackendFailure);
|
|
419
|
+
// Geometry is precomputed and still readable — a closed handle can still
|
|
420
|
+
// describe its pages, which is what a layout holding on to sizes needs.
|
|
421
|
+
CHECK_NEAR(document->geometry(0).width, 40, 0.01);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* ------------------------------------------------------------------ *
|
|
425
|
+
* Cancellation
|
|
426
|
+
* ------------------------------------------------------------------ */
|
|
427
|
+
|
|
428
|
+
TEST(aCancelBeforeStartStopsTheRender) {
|
|
429
|
+
auto document = openFixture(fixtures::quadrants(40, 40), "cancel-before.pdf");
|
|
430
|
+
Cancellation signal;
|
|
431
|
+
signal.cancel();
|
|
432
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, 40, 40, 1), &signal, nullptr), ErrorCode::Cancelled);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// A cancel that lands DURING the draw stops it. The affordance flips the flag
|
|
436
|
+
/// on the second time PDFium asks whether to pause, which is inside the render
|
|
437
|
+
/// and nowhere else — the property neither retired core could offer.
|
|
438
|
+
TEST(aCancelMidRenderStopsTheDrawAndLeavesThePageUsable) {
|
|
439
|
+
auto document = openFixture(fixtures::grid(fixtures::kArchDW, fixtures::kArchDH, 4), "cancel-mid.pdf");
|
|
440
|
+
Cancellation signal;
|
|
441
|
+
signal.cancelAfterPolls(2);
|
|
442
|
+
EXPECT_ERROR(document->render(px::request(0, 0, 0, fixtures::kArchDW, fixtures::kArchDH, 1), &signal, nullptr),
|
|
443
|
+
ErrorCode::Cancelled);
|
|
444
|
+
CHECK(signal.isCancelled());
|
|
445
|
+
// The progressive render was closed properly: the same page renders again.
|
|
446
|
+
// (6,2) sits between the 4pt grid lines and off both red diagonals — one of
|
|
447
|
+
// which passes through the doc-space origin, so (2,2) would be red.
|
|
448
|
+
auto raster = document->render(px::request(0, 0, 0, 100, 100, 1), nullptr, nullptr);
|
|
449
|
+
CHECK_EQ(raster.width, 100);
|
|
450
|
+
CHECK_PIXEL(raster, 6, 2, kWhite);
|
|
451
|
+
// On the antialiased edge of the 2pt red diagonal: reddish, not exactly red.
|
|
452
|
+
const px::Pixel onDiagonal = px::at(raster, 2, 2);
|
|
453
|
+
CHECK_MSG(onDiagonal.r > 200 && onDiagonal.g < 120, "expected the red diagonal at (2,2), got " + onDiagonal.str());
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
TEST(anUncancelledSignalIsTransparent) {
|
|
457
|
+
auto document = openFixture(fixtures::quadrants(40, 40), "cancel-none.pdf");
|
|
458
|
+
Cancellation signal;
|
|
459
|
+
auto raster = document->render(px::request(0, 0, 0, 40, 40, 1), &signal, nullptr);
|
|
460
|
+
CHECK_EQ(raster.width, 40);
|
|
461
|
+
CHECK(!signal.isCancelled());
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/// Closing while a render is in flight on another thread is safe: close waits
|
|
465
|
+
/// for the library lock, and the render completes or cancels first.
|
|
466
|
+
TEST(closeWhileRenderingIsSafe) {
|
|
467
|
+
auto document = openFixture(fixtures::grid(fixtures::kArchDW, fixtures::kArchDH, 4), "close-inflight.pdf");
|
|
468
|
+
Cancellation signal;
|
|
469
|
+
std::exception_ptr failure;
|
|
470
|
+
std::thread worker([&] {
|
|
471
|
+
try {
|
|
472
|
+
(void)document->render(px::request(0, 0, 0, fixtures::kArchDW, fixtures::kArchDH, 2), &signal, nullptr);
|
|
473
|
+
} catch (const PdfError&) {
|
|
474
|
+
// Cancelled or closed under us: both are correct outcomes.
|
|
475
|
+
} catch (...) {
|
|
476
|
+
failure = std::current_exception();
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
480
|
+
signal.cancel();
|
|
481
|
+
document->close();
|
|
482
|
+
worker.join();
|
|
483
|
+
CHECK(!failure);
|
|
484
|
+
CHECK(document->isClosed());
|
|
485
|
+
}
|