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 | |
14 | class TestAnnotations : public QObject |
15 | { |
16 | Q_OBJECT |
17 | public: |
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? */ |
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 | 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 | |
49 | void 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 | |
107 | namespace Poppler { |
108 | static 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 | |
116 | void 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 | |
130 | void 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 | |
145 | void 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 | |
163 | void 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 | |
210 | void 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 | |
222 | void 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 | |
260 | QTEST_GUILESS_MAIN(TestAnnotations) |
261 | |
262 | #include "check_annotations.moc" |
263 | |