#include #include #include #include #include #include #include "poppler/Annot.h" #include "goo/GooString.h" #include "goo/gstrtod.h" class TestAnnotations : public QObject { Q_OBJECT public: TestAnnotations(QObject *parent = nullptr) : QObject(parent) { } void saveAndCheck(const std::unique_ptr &doc, const std::function &checkFunction); private slots: void checkQColorPrecision(); void checkFontSizeAndColor(); void checkHighlightFromAndToQuads(); void checkUTF16LEAnnot(); void checkModificationCreationDate(); void checkNonMarkupAnnotations(); void checkDefaultAppearance(); }; /* Is .5f sufficient for 16 bit color channel roundtrip trough save and load on all architectures? */ void TestAnnotations::checkQColorPrecision() { bool precisionOk = true; for (int i = std::numeric_limits::min(); i <= std::numeric_limits::max(); i++) { double normalized = static_cast(i) / static_cast(std::numeric_limits::max()); GooString *serialized = GooString::format("{0:.5f}", normalized); double deserialized = gatof(serialized->c_str()); delete serialized; uint16_t denormalized = std::round(deserialized * std::numeric_limits::max()); if (static_cast(i) != denormalized) { precisionOk = false; break; } } QVERIFY(precisionOk); } void TestAnnotations::checkFontSizeAndColor() { const QString contents = QStringLiteral("foobar"); const std::vector testColors { QColor::fromRgb(0xAB, 0xCD, 0xEF), QColor::fromCmyk(0xAB, 0xBC, 0xCD, 0xDE) }; const QFont testFont(QStringLiteral("Helvetica"), 20); QTemporaryFile tempFile; QVERIFY(tempFile.open()); tempFile.close(); { std::unique_ptr doc { Poppler::Document::load(TESTDATADIR "/unittestcases/UseNone.pdf") }; QVERIFY(doc.get()); std::unique_ptr page { doc->page(0) }; QVERIFY(page.get()); for (const auto &color : testColors) { auto annot = std::make_unique(Poppler::TextAnnotation::InPlace); annot->setBoundary(QRectF(0.0, 0.0, 1.0, 1.0)); annot->setContents(contents); annot->setTextFont(testFont); annot->setTextColor(color); page->addAnnotation(annot.get()); } std::unique_ptr conv(doc->pdfConverter()); QVERIFY(conv.get()); conv->setOutputFileName(tempFile.fileName()); conv->setPDFOptions(Poppler::PDFConverter::WithChanges); QVERIFY(conv->convert()); } { std::unique_ptr doc { Poppler::Document::load(tempFile.fileName()) }; QVERIFY(doc.get()); std::unique_ptr page { doc->page(0) }; QVERIFY(page.get()); auto annots = page->annotations(); QCOMPARE(annots.size(), static_cast(testColors.size())); auto &&annot = annots.constBegin(); for (const auto &color : testColors) { QCOMPARE((*annot)->subType(), Poppler::Annotation::AText); auto textAnnot = static_cast(*annot); QCOMPARE(textAnnot->contents(), contents); QCOMPARE(textAnnot->textFont().pointSize(), testFont.pointSize()); QCOMPARE(static_cast(textAnnot->textColor().spec()), static_cast(color.spec())); QCOMPARE(textAnnot->textColor(), color); if (annot != annots.constEnd()) ++annot; } qDeleteAll(annots); } } namespace Poppler { static bool operator==(const Poppler::HighlightAnnotation::Quad &a, const Poppler::HighlightAnnotation::Quad &b) { // FIXME We do not compare capStart, capEnd and feather since AnnotQuadrilaterals doesn't contain that info and thus // HighlightAnnotationPrivate::fromQuadrilaterals uses default values 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]; } } void TestAnnotations::checkHighlightFromAndToQuads() { std::unique_ptr doc { Poppler::Document::load(TESTDATADIR "/unittestcases/UseNone.pdf") }; std::unique_ptr page { doc->page(0) }; auto ha = std::make_unique(); page->addAnnotation(ha.get()); const QList 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 } }; ha->setHighlightQuads(quads); QCOMPARE(ha->highlightQuads(), quads); } void TestAnnotations::checkUTF16LEAnnot() { std::unique_ptr doc { Poppler::Document::load(TESTDATADIR "/unittestcases/utf16le-annot.pdf") }; QVERIFY(doc.get()); std::unique_ptr page { doc->page(0) }; QVERIFY(page.get()); auto annots = page->annotations(); QCOMPARE(annots.size(), 2); auto annot = annots[1]; QCOMPARE(annot->contents(), QString::fromUtf8("Únîcödé豰")); // clazy:exclude=qstring-allocations qDeleteAll(annots); } void TestAnnotations::saveAndCheck(const std::unique_ptr &doc, const std::function &checkFunction) { // also check that saving yields the same output QTemporaryFile tempFile; QVERIFY(tempFile.open()); tempFile.close(); std::unique_ptr conv(doc->pdfConverter()); conv->setOutputFileName(tempFile.fileName()); conv->setPDFOptions(Poppler::PDFConverter::WithChanges); conv->convert(); std::unique_ptr savedDoc { Poppler::Document::load(tempFile.fileName()) }; std::unique_ptr page { doc->page(0) }; auto annots = page->annotations(); checkFunction(annots.at(1)); qDeleteAll(annots); } void TestAnnotations::checkModificationCreationDate() { std::unique_ptr doc { Poppler::Document::load(TESTDATADIR "/unittestcases/utf16le-annot.pdf") }; QVERIFY(doc.get()); std::unique_ptr page { doc->page(0) }; auto annots = page->annotations(); auto annot = annots.at(1); QCOMPARE(annot->creationDate(), QDateTime()); QCOMPARE(annot->modificationDate(), QDateTime()); const QDateTime dt1(QDate(2020, 8, 7), QTime(18, 34, 56)); annot->setCreationDate(dt1); auto checkFunction1 = [dt1](Poppler::Annotation *a) { QCOMPARE(a->creationDate(), dt1); // setting the creation date updates the modification date QVERIFY(std::abs(a->modificationDate().secsTo(QDateTime::currentDateTime())) < 2); }; checkFunction1(annot); saveAndCheck(doc, checkFunction1); const QDateTime dt2(QDate(2020, 8, 30), QTime(8, 14, 52)); annot->setModificationDate(dt2); auto checkFunction2 = [dt2](Poppler::Annotation *a) { QCOMPARE(a->modificationDate(), dt2); }; checkFunction2(annot); saveAndCheck(doc, checkFunction2); // setting the creation date to empty means "use the modification date" and also updates the modification date // so both creation date and modification date are the same and are now annot->setCreationDate(QDateTime()); auto checkFunction3 = [](Poppler::Annotation *a) { QVERIFY(std::abs(a->creationDate().secsTo(QDateTime::currentDateTime())) < 2); QCOMPARE(a->creationDate(), a->modificationDate()); }; checkFunction3(annot); saveAndCheck(doc, checkFunction3); annot->setModificationDate(QDateTime()); auto checkFunction4 = [](Poppler::Annotation *a) { QCOMPARE(a->creationDate(), QDateTime()); QCOMPARE(a->modificationDate(), QDateTime()); }; checkFunction4(annot); saveAndCheck(doc, checkFunction4); qDeleteAll(annots); } void TestAnnotations::checkNonMarkupAnnotations() { std::unique_ptr doc { Poppler::Document::load(TESTDATADIR "/unittestcases/checkbox_issue_159.pdf") }; QVERIFY(doc.get()); std::unique_ptr page { doc->page(0) }; QVERIFY(page.get()); auto annots = page->annotations(); QCOMPARE(annots.size(), 17); qDeleteAll(annots); } void TestAnnotations::checkDefaultAppearance() { std::unique_ptr roundtripString; { GooString daString { "/Helv 10 Tf 0.1 0.2 0.3 rg" }; const DefaultAppearance da { &daString }; QCOMPARE(da.getFontPtSize(), 10.); QVERIFY(da.getFontName().isName()); QCOMPARE(da.getFontName().getName(), "Helv"); const AnnotColor *color = da.getFontColor(); QVERIFY(color); QCOMPARE(color->getSpace(), AnnotColor::colorRGB); QCOMPARE(color->getValues()[0], 0.1); QCOMPARE(color->getValues()[1], 0.2); QCOMPARE(color->getValues()[2], 0.3); roundtripString.reset(da.toAppearanceString()); } { /* roundtrip through parse/generate/parse shall preserve values */ const DefaultAppearance da { roundtripString.get() }; QCOMPARE(da.getFontPtSize(), 10.); QVERIFY(da.getFontName().isName()); QCOMPARE(da.getFontName().getName(), "Helv"); const AnnotColor *color = da.getFontColor(); QVERIFY(color); QCOMPARE(color->getSpace(), AnnotColor::colorRGB); QCOMPARE(color->getValues()[0], 0.1); QCOMPARE(color->getValues()[1], 0.2); QCOMPARE(color->getValues()[2], 0.3); } { /* parsing bad DA strings must not cause crash */ GooString daString { "/ % Tf 1 2 rg" }; const DefaultAppearance da { &daString }; QVERIFY(!da.getFontName().isName()); } } QTEST_GUILESS_MAIN(TestAnnotations) #include "check_annotations.moc"