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