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