1#include <cmath>
2#include <memory>
3#include <sstream>
4
5#include <QtTest/QTest>
6#include <QTemporaryFile>
7
8#include <poppler-qt6.h>
9
10#include "poppler/Annot.h"
11#include "goo/GooString.h"
12#include "goo/gstrtod.h"
13
14class TestAnnotations : public QObject
15{
16 Q_OBJECT
17public:
18 explicit TestAnnotations(QObject *parent = nullptr) : QObject(parent) { }
19
20 void saveAndCheck(const std::unique_ptr<Poppler::Document> &doc, const std::function<void(Poppler::Annotation *a)> &checkFunction);
21
22private slots:
23 void checkQColorPrecision();
24 void checkFontSizeAndColor();
25 void checkHighlightFromAndToQuads();
26 void checkUTF16LEAnnot();
27 void checkModificationCreationDate();
28 void checkNonMarkupAnnotations();
29 void checkDefaultAppearance();
30};
31
32/* Is .5f sufficient for 16 bit color channel roundtrip trough save and load on all architectures? */
33void TestAnnotations::checkQColorPrecision()
34{
35 bool precisionOk = true;
36 for (int i = std::numeric_limits<uint16_t>::min(); i <= std::numeric_limits<uint16_t>::max(); i++) {
37 double normalized = static_cast<uint16_t>(i) / static_cast<double>(std::numeric_limits<uint16_t>::max());
38 const std::unique_ptr<GooString> serialized = GooString::format(fmt: "{0:.5f}", normalized);
39 double deserialized = gatof(nptr: serialized->c_str());
40 uint16_t denormalized = std::round(x: deserialized * std::numeric_limits<uint16_t>::max());
41 if (static_cast<uint16_t>(i) != denormalized) {
42 precisionOk = false;
43 break;
44 }
45 }
46 QVERIFY(precisionOk);
47}
48
49void TestAnnotations::checkFontSizeAndColor()
50{
51 const QString contents = QStringLiteral("foobar");
52 const std::vector<QColor> testColors { QColor::fromRgb(r: 0xAB, g: 0xCD, b: 0xEF), QColor::fromCmyk(c: 0xAB, m: 0xBC, y: 0xCD, k: 0xDE) };
53 const QFont testFont(QStringLiteral("Helvetica"), 20);
54
55 QTemporaryFile tempFile;
56 QVERIFY(tempFile.open());
57 tempFile.close();
58
59 {
60 std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/UseNone.pdf") };
61 QVERIFY(doc.get());
62
63 std::unique_ptr<Poppler::Page> page { doc->page(index: 0) };
64 QVERIFY(page.get());
65
66 for (const auto &color : testColors) {
67 auto annot = std::make_unique<Poppler::TextAnnotation>(args: Poppler::TextAnnotation::InPlace);
68 annot->setBoundary(QRectF(0.0, 0.0, 1.0, 1.0));
69 annot->setContents(contents);
70 annot->setTextFont(testFont);
71 annot->setTextColor(color);
72 page->addAnnotation(ann: annot.get());
73 }
74
75 std::unique_ptr<Poppler::PDFConverter> conv(doc->pdfConverter());
76 QVERIFY(conv.get());
77 conv->setOutputFileName(tempFile.fileName());
78 conv->setPDFOptions(Poppler::PDFConverter::WithChanges);
79 QVERIFY(conv->convert());
80 }
81
82 {
83 std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(filePath: tempFile.fileName()) };
84 QVERIFY(doc.get());
85
86 std::unique_ptr<Poppler::Page> page { doc->page(index: 0) };
87 QVERIFY(page.get());
88
89 auto annots = page->annotations();
90 QCOMPARE(annots.size(), static_cast<int>(testColors.size()));
91
92 auto &&annot = annots.cbegin();
93 for (const auto &color : testColors) {
94 QCOMPARE((*annot)->subType(), Poppler::Annotation::AText);
95 auto textAnnot = static_cast<Poppler::TextAnnotation *>(annot->get());
96 QCOMPARE(textAnnot->contents(), contents);
97 QCOMPARE(textAnnot->textFont().pointSize(), testFont.pointSize());
98 QCOMPARE(static_cast<int>(textAnnot->textColor().spec()), static_cast<int>(color.spec()));
99 QCOMPARE(textAnnot->textColor(), color);
100 if (annot != annots.cend()) {
101 ++annot;
102 }
103 }
104 }
105}
106
107namespace Poppler {
108static bool operator==(const Poppler::HighlightAnnotation::Quad &a, const Poppler::HighlightAnnotation::Quad &b)
109{
110 // FIXME We do not compare capStart, capEnd and feather since AnnotQuadrilaterals doesn't contain that info and thus
111 // HighlightAnnotationPrivate::fromQuadrilaterals uses default values
112 return a.points[0] == b.points[0] && a.points[1] == b.points[1] && a.points[2] == b.points[2] && a.points[3] == b.points[3];
113}
114}
115
116void TestAnnotations::checkHighlightFromAndToQuads()
117{
118 std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/UseNone.pdf") };
119
120 std::unique_ptr<Poppler::Page> page { doc->page(index: 0) };
121
122 auto ha = std::make_unique<Poppler::HighlightAnnotation>();
123 page->addAnnotation(ann: ha.get());
124
125 const QList<Poppler::HighlightAnnotation::Quad> quads = { { .points: { { 0, 0.1 }, { 0.2, 0.3 }, { 0.4, 0.5 }, { 0.6, 0.7 } }, .capStart: false, .capEnd: false, .feather: 0 }, { .points: { { 0.8, 0.9 }, { 0.1, 0.2 }, { 0.3, 0.4 }, { 0.5, 0.6 } }, .capStart: true, .capEnd: false, .feather: 0.4 } };
126 ha->setHighlightQuads(quads);
127 QCOMPARE(ha->highlightQuads(), quads);
128}
129
130void TestAnnotations::checkUTF16LEAnnot()
131{
132 std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/utf16le-annot.pdf") };
133 QVERIFY(doc.get());
134
135 std::unique_ptr<Poppler::Page> page { doc->page(index: 0) };
136 QVERIFY(page.get());
137
138 auto annots = page->annotations();
139 QCOMPARE(annots.size(), 2);
140
141 const auto &annot = annots[1];
142 QCOMPARE(annot->contents(), QString::fromUtf8("Únîcödé豰")); // clazy:exclude=qstring-allocations
143}
144
145void TestAnnotations::saveAndCheck(const std::unique_ptr<Poppler::Document> &doc, const std::function<void(Poppler::Annotation *a)> &checkFunction)
146{
147 // also check that saving yields the same output
148 QTemporaryFile tempFile;
149 QVERIFY(tempFile.open());
150 tempFile.close();
151
152 std::unique_ptr<Poppler::PDFConverter> conv(doc->pdfConverter());
153 conv->setOutputFileName(tempFile.fileName());
154 conv->setPDFOptions(Poppler::PDFConverter::WithChanges);
155 conv->convert();
156
157 std::unique_ptr<Poppler::Document> savedDoc { Poppler::Document::load(filePath: tempFile.fileName()) };
158 std::unique_ptr<Poppler::Page> page { doc->page(index: 0) };
159 auto annots = page->annotations();
160 checkFunction(annots.at(n: 1).get());
161}
162
163void TestAnnotations::checkModificationCreationDate()
164{
165 std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/utf16le-annot.pdf") };
166 QVERIFY(doc.get());
167
168 std::unique_ptr<Poppler::Page> page { doc->page(index: 0) };
169
170 auto annots = page->annotations();
171 const auto &annot = annots.at(n: 1);
172 QCOMPARE(annot->creationDate(), QDateTime());
173 QCOMPARE(annot->modificationDate(), QDateTime());
174
175 const QDateTime dt1(QDate(2020, 8, 7), QTime(18, 34, 56));
176 annot->setCreationDate(dt1);
177 auto checkFunction1 = [dt1](Poppler::Annotation *a) {
178 QCOMPARE(a->creationDate(), dt1);
179 // setting the creation date updates the modification date
180 QVERIFY(std::abs(a->modificationDate().secsTo(QDateTime::currentDateTime())) < 2);
181 };
182 checkFunction1(annot.get());
183 saveAndCheck(doc, checkFunction: checkFunction1);
184
185 const QDateTime dt2(QDate(2020, 8, 30), QTime(8, 14, 52));
186 annot->setModificationDate(dt2);
187 auto checkFunction2 = [dt2](Poppler::Annotation *a) { QCOMPARE(a->modificationDate(), dt2); };
188 checkFunction2(annot.get());
189 saveAndCheck(doc, checkFunction: checkFunction2);
190
191 // setting the creation date to empty means "use the modification date" and also updates the modification date
192 // so both creation date and modification date are the same and are now
193 annot->setCreationDate(QDateTime());
194 auto checkFunction3 = [](Poppler::Annotation *a) {
195 QVERIFY(std::abs(a->creationDate().secsTo(QDateTime::currentDateTime())) < 2);
196 QCOMPARE(a->creationDate(), a->modificationDate());
197 };
198 checkFunction3(annot.get());
199 saveAndCheck(doc, checkFunction: checkFunction3);
200
201 annot->setModificationDate(QDateTime());
202 auto checkFunction4 = [](Poppler::Annotation *a) {
203 QCOMPARE(a->creationDate(), QDateTime());
204 QCOMPARE(a->modificationDate(), QDateTime());
205 };
206 checkFunction4(annot.get());
207 saveAndCheck(doc, checkFunction: checkFunction4);
208}
209
210void TestAnnotations::checkNonMarkupAnnotations()
211{
212 std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/checkbox_issue_159.pdf") };
213 QVERIFY(doc.get());
214
215 std::unique_ptr<Poppler::Page> page { doc->page(index: 0) };
216 QVERIFY(page.get());
217
218 auto annots = page->annotations();
219 QCOMPARE(annots.size(), 17);
220}
221
222void TestAnnotations::checkDefaultAppearance()
223{
224 std::unique_ptr<GooString> roundtripString;
225 {
226 GooString daString { "/Helv 10 Tf 0.1 0.2 0.3 rg" };
227 const DefaultAppearance da { &daString };
228 QCOMPARE(da.getFontPtSize(), 10.);
229 QVERIFY(da.getFontName().isName());
230 QCOMPARE(da.getFontName().getName(), "Helv");
231 const AnnotColor *color = da.getFontColor();
232 QVERIFY(color);
233 QCOMPARE(color->getSpace(), AnnotColor::colorRGB);
234 QCOMPARE(color->getValues()[0], 0.1);
235 QCOMPARE(color->getValues()[1], 0.2);
236 QCOMPARE(color->getValues()[2], 0.3);
237 roundtripString = std::make_unique<GooString>(args: da.toAppearanceString());
238 }
239 {
240 /* roundtrip through parse/generate/parse shall preserve values */
241 const DefaultAppearance da { roundtripString.get() };
242 QCOMPARE(da.getFontPtSize(), 10.);
243 QVERIFY(da.getFontName().isName());
244 QCOMPARE(da.getFontName().getName(), "Helv");
245 const AnnotColor *color = da.getFontColor();
246 QVERIFY(color);
247 QCOMPARE(color->getSpace(), AnnotColor::colorRGB);
248 QCOMPARE(color->getValues()[0], 0.1);
249 QCOMPARE(color->getValues()[1], 0.2);
250 QCOMPARE(color->getValues()[2], 0.3);
251 }
252 {
253 /* parsing bad DA strings must not cause crash */
254 GooString daString { "/ % Tf 1 2 rg" };
255 const DefaultAppearance da { &daString };
256 QVERIFY(!da.getFontName().isName());
257 }
258}
259
260QTEST_GUILESS_MAIN(TestAnnotations)
261
262#include "check_annotations.moc"
263

source code of poppler/qt6/tests/check_annotations.cpp