File indexing completed on 2024-05-12 04:19:58

0001 /*
0002 Gwenview: an image viewer
0003 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
0004 
0005 This program is free software; you can redistribute it and/or
0006 modify it under the terms of the GNU General Public License
0007 as published by the Free Software Foundation; either version 2
0008 of the License, or (at your option) any later version.
0009 
0010 This program is distributed in the hope that it will be useful,
0011 but WITHOUT ANY WARRANTY; without even the implied warranty of
0012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013 GNU General Public License for more details.
0014 
0015 You should have received a copy of the GNU General Public License
0016 along with this program; if not, write to the Free Software
0017 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
0018 
0019 */
0020 // Qt
0021 #include <QConicalGradient>
0022 #include <QImage>
0023 #include <QPainter>
0024 #include <QTest>
0025 
0026 // KF
0027 #include <KIO/StatJob>
0028 #include <KJobUiDelegate>
0029 #include <kio_version.h>
0030 
0031 // KDCraw
0032 #include <KDCRAW/KDcraw>
0033 
0034 // Local
0035 #include "../lib/abstractimageoperation.h"
0036 #include "../lib/document/abstractdocumenteditor.h"
0037 #include "../lib/document/documentfactory.h"
0038 #include "../lib/document/documentjob.h"
0039 #include "../lib/imagemetainfomodel.h"
0040 #include "../lib/imageutils.h"
0041 #include "../lib/transformimageoperation.h"
0042 #include "testutils.h"
0043 
0044 #include <exiv2/exif.hpp>
0045 
0046 #include "documenttest.h"
0047 
0048 QTEST_MAIN(DocumentTest)
0049 
0050 using namespace Gwenview;
0051 
0052 static void waitUntilMetaInfoLoaded(Document::Ptr doc)
0053 {
0054     while (doc->loadingState() < Document::MetaInfoLoaded) {
0055         QTest::qWait(100);
0056     }
0057 }
0058 
0059 static bool waitUntilJobIsDone(DocumentJob *job)
0060 {
0061     JobWatcher watcher(job);
0062     watcher.wait();
0063     return watcher.error() == KJob::NoError;
0064 }
0065 
0066 void DocumentTest::initTestCase()
0067 {
0068     qRegisterMetaType<QUrl>("QUrl");
0069 }
0070 
0071 void DocumentTest::init()
0072 {
0073     DocumentFactory::instance()->clearCache();
0074 }
0075 
0076 void DocumentTest::testLoad()
0077 {
0078     QFETCH(QString, fileName);
0079     QFETCH(QByteArray, expectedFormat);
0080     QFETCH(int, expectedKindInt);
0081     QFETCH(bool, expectedIsAnimated);
0082     QFETCH(QImage, expectedImage);
0083     QFETCH(int, maxHeight); // number of lines to test. -1 to test all lines
0084 
0085     auto expectedKind = MimeTypeUtils::Kind(expectedKindInt);
0086 
0087     QUrl url = urlForTestFile(fileName);
0088 
0089     // testing RAW loading. For raw, QImage directly won't work -> load it using KDCRaw
0090     QByteArray mFormatHint = url.fileName().section('.', -1).toLocal8Bit().toLower();
0091     if (KDcrawIface::KDcraw::rawFilesList().contains(QString(mFormatHint))) {
0092         if (!KDcrawIface::KDcraw::loadEmbeddedPreview(expectedImage, url.toLocalFile())) {
0093             QSKIP(
0094                 "Not running this test: failed to get expectedImage. Try running ./fetch_testing_raw.sh\
0095  in the tests/data directory and then rerun the tests.");
0096         }
0097     }
0098 
0099     if (expectedKind != MimeTypeUtils::KIND_SVG_IMAGE) {
0100         if (expectedImage.isNull()) {
0101             QSKIP("Not running this test: QImage failed to load the test image");
0102         }
0103     }
0104 
0105     Document::Ptr doc = DocumentFactory::instance()->load(url);
0106     QSignalSpy spy(doc.data(), SIGNAL(isAnimatedUpdated()));
0107     doc->waitUntilLoaded();
0108     QCOMPARE(doc->loadingState(), Document::Loaded);
0109 
0110     QCOMPARE(doc->kind(), expectedKind);
0111     QCOMPARE(doc->isAnimated(), expectedIsAnimated);
0112     QCOMPARE(spy.count(), doc->isAnimated() ? 1 : 0);
0113     if (doc->kind() == MimeTypeUtils::KIND_RASTER_IMAGE) {
0114         QImage image = doc->image();
0115         if (maxHeight > -1) {
0116             QRect poiRect(0, 0, image.width(), maxHeight);
0117             image = image.copy(poiRect);
0118             expectedImage = expectedImage.copy(poiRect);
0119         }
0120         QCOMPARE(image, expectedImage);
0121         QCOMPARE(QString(doc->format()), QString(expectedFormat));
0122     }
0123 }
0124 
0125 static void testLoad_newRow(const char *fileName,
0126                             const QByteArray &format,
0127                             MimeTypeUtils::Kind kind = MimeTypeUtils::KIND_RASTER_IMAGE,
0128                             bool isAnimated = false,
0129                             int maxHeight = -1)
0130 {
0131     QTest::newRow(fileName) << fileName << QByteArray(format) << int(kind) << isAnimated << QImage(pathForTestFile(fileName), format) << maxHeight;
0132 }
0133 
0134 void DocumentTest::testLoad_data()
0135 {
0136     QTest::addColumn<QString>("fileName");
0137     QTest::addColumn<QByteArray>("expectedFormat");
0138     QTest::addColumn<int>("expectedKindInt");
0139     QTest::addColumn<bool>("expectedIsAnimated");
0140     QTest::addColumn<QImage>("expectedImage");
0141     QTest::addColumn<int>("maxHeight");
0142 
0143     testLoad_newRow("test.png", "png");
0144     testLoad_newRow("160216_no_size_before_decoding.eps", "eps");
0145     testLoad_newRow("160382_corrupted.jpeg", "jpeg", MimeTypeUtils::KIND_RASTER_IMAGE, false, 55);
0146     testLoad_newRow("1x10k.png", "png");
0147     testLoad_newRow("1x10k.jpg", "jpeg");
0148     testLoad_newRow("test.xcf", "xcf");
0149     testLoad_newRow("188191_does_not_load.tga", "tga");
0150     testLoad_newRow("289819_does_not_load.png", "png");
0151     testLoad_newRow("png-with-jpeg-extension.jpg", "png");
0152     testLoad_newRow("jpg-with-gif-extension.gif", "jpeg");
0153 
0154     // RAW preview
0155     testLoad_newRow("CANON-EOS350D-02.CR2", "cr2", MimeTypeUtils::KIND_RASTER_IMAGE, false);
0156     testLoad_newRow("dsc_0093.nef", "nef", MimeTypeUtils::KIND_RASTER_IMAGE, false);
0157 
0158     // SVG
0159     testLoad_newRow("test.svg", "", MimeTypeUtils::KIND_SVG_IMAGE);
0160     // FIXME: Test svgz
0161 
0162     // Animated
0163     testLoad_newRow("4frames.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, true);
0164     testLoad_newRow("1frame.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, false);
0165     testLoad_newRow("185523_1frame_with_graphic_control_extension.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, false);
0166 }
0167 
0168 void DocumentTest::testLoadTwoPasses()
0169 {
0170     QUrl url = urlForTestFile("test.png");
0171     QImage image;
0172     bool ok = image.load(url.toLocalFile());
0173     QVERIFY2(ok, "Could not load 'test.png'");
0174     Document::Ptr doc = DocumentFactory::instance()->load(url);
0175     waitUntilMetaInfoLoaded(doc);
0176     QVERIFY2(doc->image().isNull(), "Image shouldn't have been loaded at this time");
0177     QCOMPARE(doc->format().data(), "png");
0178     doc->waitUntilLoaded();
0179     QCOMPARE(image, doc->image());
0180 }
0181 
0182 void DocumentTest::testLoadEmpty()
0183 {
0184     QUrl url = urlForTestFile("empty.png");
0185     Document::Ptr doc = DocumentFactory::instance()->load(url);
0186     while (doc->loadingState() <= Document::KindDetermined) {
0187         QTest::qWait(100);
0188     }
0189     QCOMPARE(doc->loadingState(), Document::LoadingFailed);
0190 }
0191 
0192 #define NEW_ROW(fileName) QTest::newRow(fileName) << fileName
0193 void DocumentTest::testLoadDownSampled_data()
0194 {
0195     QTest::addColumn<QString>("fileName");
0196 
0197     NEW_ROW("orient6.jpg");
0198     NEW_ROW("1x10k.jpg");
0199 }
0200 #undef NEW_ROW
0201 
0202 void DocumentTest::testLoadDownSampled()
0203 {
0204     // Note: for now we only support down sampling on jpeg, do not use test.png
0205     // here
0206     QFETCH(QString, fileName);
0207     QUrl url = urlForTestFile(fileName);
0208     QImage image;
0209     bool ok = image.load(url.toLocalFile());
0210     QVERIFY2(ok, "Could not load test image");
0211     Document::Ptr doc = DocumentFactory::instance()->load(url);
0212 
0213     QSignalSpy downSampledImageReadySpy(doc.data(), SIGNAL(downSampledImageReady()));
0214     QSignalSpy loadingFailedSpy(doc.data(), SIGNAL(loadingFailed(QUrl)));
0215     QSignalSpy loadedSpy(doc.data(), SIGNAL(loaded(QUrl)));
0216     bool ready = doc->prepareDownSampledImageForZoom(0.2);
0217     QVERIFY2(!ready, "There should not be a down sampled image at this point");
0218 
0219     while (downSampledImageReadySpy.isEmpty() && loadingFailedSpy.isEmpty() && loadedSpy.isEmpty()) {
0220         QTest::qWait(100);
0221     }
0222     QImage downSampledImage = doc->downSampledImageForZoom(0.2);
0223     QVERIFY2(!downSampledImage.isNull(), "Down sampled image should not be null");
0224 
0225     QSize expectedSize = doc->size() / 2;
0226     if (expectedSize.isEmpty()) {
0227         expectedSize = image.size();
0228     }
0229     QCOMPARE(downSampledImage.size(), expectedSize);
0230 }
0231 
0232 /**
0233  * Down sampling is not supported on png. We should get a complete image
0234  * instead.
0235  */
0236 void DocumentTest::testLoadDownSampledPng()
0237 {
0238     QUrl url = urlForTestFile("test.png");
0239     QImage image;
0240     bool ok = image.load(url.toLocalFile());
0241     QVERIFY2(ok, "Could not load test image");
0242     Document::Ptr doc = DocumentFactory::instance()->load(url);
0243 
0244     LoadingStateSpy stateSpy(doc);
0245     connect(doc.data(), &Document::loaded, &stateSpy, &LoadingStateSpy::readState);
0246 
0247     bool ready = doc->prepareDownSampledImageForZoom(0.2);
0248     QVERIFY2(!ready, "There should not be a down sampled image at this point");
0249 
0250     doc->waitUntilLoaded();
0251 
0252     QCOMPARE(stateSpy.mCallCount, 1);
0253     QCOMPARE(stateSpy.mState, Document::Loaded);
0254 }
0255 
0256 void DocumentTest::testLoadRemote()
0257 {
0258     QUrl url = setUpRemoteTestDir("test.png");
0259     if (!url.isValid()) {
0260         QSKIP("Not running this test: failed to setup remote test dir.");
0261     }
0262     url = url.adjusted(QUrl::StripTrailingSlash);
0263     url.setPath(url.path() + '/' + "test.png");
0264 
0265     QVERIFY2(KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatNoDetails)->exec(), "test url not found");
0266 
0267     Document::Ptr doc = DocumentFactory::instance()->load(url);
0268     doc->waitUntilLoaded();
0269     QImage image = doc->image();
0270     QCOMPARE(image.width(), 150);
0271     QCOMPARE(image.height(), 100);
0272 }
0273 
0274 void DocumentTest::testLoadAnimated()
0275 {
0276     QUrl srcUrl = urlForTestFile("40frames.gif");
0277     Document::Ptr doc = DocumentFactory::instance()->load(srcUrl);
0278     QSignalSpy spy(doc.data(), SIGNAL(imageRectUpdated(QRect)));
0279     doc->waitUntilLoaded();
0280     QVERIFY(doc->isAnimated());
0281 
0282     // Test we receive only one imageRectUpdated() until animation is started
0283     // (the imageRectUpdated() is triggered by the loading of the first image)
0284     QTest::qWait(1000);
0285     QCOMPARE(spy.count(), 1);
0286 
0287     // Test we now receive some imageRectUpdated()
0288     doc->startAnimation();
0289     QTest::qWait(1000);
0290     int count = spy.count();
0291     doc->stopAnimation();
0292     QVERIFY2(count > 0, "No imageRectUpdated() signal received");
0293 
0294     // Test we do not receive imageRectUpdated() anymore
0295     QTest::qWait(1000);
0296     QCOMPARE(count, spy.count());
0297 
0298     // Start again, we should receive imageRectUpdated() again
0299     doc->startAnimation();
0300     QTest::qWait(1000);
0301     QVERIFY2(spy.count() > count, "No imageRectUpdated() signal received after restarting");
0302 }
0303 
0304 void DocumentTest::testPrepareDownSampledAfterFailure()
0305 {
0306     QUrl url = urlForTestFile("empty.png");
0307     Document::Ptr doc = DocumentFactory::instance()->load(url);
0308 
0309     doc->waitUntilLoaded();
0310     QCOMPARE(doc->loadingState(), Document::LoadingFailed);
0311 
0312     bool ready = doc->prepareDownSampledImageForZoom(0.25);
0313     QVERIFY2(!ready, "Down sampled image should not be ready");
0314 }
0315 
0316 void DocumentTest::testSaveRemote()
0317 {
0318     QUrl dstUrl = setUpRemoteTestDir();
0319     if (!dstUrl.isValid()) {
0320         QSKIP("Not running this test: failed to setup remote test dir.");
0321     }
0322 
0323     QUrl srcUrl = urlForTestFile("test.png");
0324     Document::Ptr doc = DocumentFactory::instance()->load(srcUrl);
0325     doc->waitUntilLoaded();
0326 
0327     dstUrl = dstUrl.adjusted(QUrl::StripTrailingSlash);
0328     dstUrl.setPath(dstUrl.path() + '/' + "testSaveRemote.png");
0329     QVERIFY(waitUntilJobIsDone(doc->save(dstUrl, "png")));
0330 }
0331 
0332 /**
0333  * Check that deleting a document while it is loading does not crash
0334  */
0335 void DocumentTest::testDeleteWhileLoading()
0336 {
0337     {
0338         QUrl url = urlForTestFile("test.png");
0339         QImage image;
0340         bool ok = image.load(url.toLocalFile());
0341         QVERIFY2(ok, "Could not load 'test.png'");
0342         Document::Ptr doc = DocumentFactory::instance()->load(url);
0343     }
0344     DocumentFactory::instance()->clearCache();
0345     // Wait two seconds. If the test fails we will get a segfault while waiting
0346     QTest::qWait(2000);
0347 }
0348 
0349 void DocumentTest::testLoadRotated()
0350 {
0351     QUrl url = urlForTestFile("orient6.jpg");
0352     QImage image;
0353     bool ok = image.load(url.toLocalFile());
0354     QVERIFY2(ok, "Could not load 'orient6.jpg'");
0355     QTransform matrix = ImageUtils::transformMatrix(ROT_90);
0356     image = image.transformed(matrix);
0357 
0358     Document::Ptr doc = DocumentFactory::instance()->load(url);
0359     doc->waitUntilLoaded();
0360     QCOMPARE(image, doc->image());
0361 
0362     // RAW preview on rotated image
0363     url = urlForTestFile("dsd_1838.nef");
0364     if (!KDcrawIface::KDcraw::loadEmbeddedPreview(image, url.toLocalFile())) {
0365         QSKIP(
0366             "Not running this test: failed to get image. Try running ./fetch_testing_raw.sh\
0367  in the tests/data directory and then rerun the tests.");
0368     }
0369     matrix = ImageUtils::transformMatrix(ROT_270);
0370     image = image.transformed(matrix);
0371 
0372     doc = DocumentFactory::instance()->load(url);
0373     doc->waitUntilLoaded();
0374     QCOMPARE(image, doc->image());
0375 }
0376 
0377 /**
0378  * Checks that asking the DocumentFactory the same document twice in a row does
0379  * not load it twice
0380  */
0381 void DocumentTest::testMultipleLoads()
0382 {
0383     QUrl url = urlForTestFile("orient6.jpg");
0384     Document::Ptr doc1 = DocumentFactory::instance()->load(url);
0385     Document::Ptr doc2 = DocumentFactory::instance()->load(url);
0386 
0387     QCOMPARE(doc1.data(), doc2.data());
0388 }
0389 
0390 void DocumentTest::testSaveAs()
0391 {
0392     QUrl url = urlForTestFile("orient6.jpg");
0393     DocumentFactory *factory = DocumentFactory::instance();
0394     Document::Ptr doc = factory->load(url);
0395     QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl, QUrl)));
0396     QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged()));
0397     QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(QUrl)));
0398     doc->startLoadingFullImage();
0399 
0400     QUrl destUrl = urlForTestOutputFile("result.png");
0401     QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png")));
0402     QCOMPARE(doc->format().data(), "png");
0403     QCOMPARE(doc->url(), destUrl);
0404     QCOMPARE(doc->metaInfo()->getValueForKey("General.Name"), destUrl.fileName());
0405 
0406     QVERIFY2(doc->loadingState() == Document::Loaded, "Document is supposed to finish loading before saving");
0407 
0408     QTest::qWait(100); // saved() is emitted asynchronously
0409     QCOMPARE(savedSpy.count(), 1);
0410     QVariantList args = savedSpy.takeFirst();
0411     QCOMPARE(args.at(0).toUrl(), url);
0412     QCOMPARE(args.at(1).toUrl(), destUrl);
0413 
0414     QImage image("result.png", "png");
0415     QCOMPARE(doc->image(), image);
0416 
0417     QVERIFY(!DocumentFactory::instance()->hasUrl(url));
0418     QVERIFY(DocumentFactory::instance()->hasUrl(destUrl));
0419 
0420     QCOMPARE(modifiedDocumentListChangedSpy.count(), 0); // No changes were made
0421 
0422     QCOMPARE(documentChangedSpy.count(), 1);
0423     args = documentChangedSpy.takeFirst();
0424     QCOMPARE(args.at(0).toUrl(), destUrl);
0425 }
0426 
0427 void DocumentTest::testLosslessSave()
0428 {
0429     QUrl url1 = urlForTestFile("orient6.jpg");
0430     Document::Ptr doc = DocumentFactory::instance()->load(url1);
0431     doc->startLoadingFullImage();
0432 
0433     QUrl url2 = urlForTestOutputFile("orient1.jpg");
0434     QVERIFY(waitUntilJobIsDone(doc->save(url2, "jpeg")));
0435 
0436     QImage image1;
0437     QVERIFY(image1.load(url1.toLocalFile()));
0438 
0439     QImage image2;
0440     QVERIFY(image2.load(url2.toLocalFile()));
0441 
0442     QCOMPARE(image1, image2);
0443 }
0444 
0445 void DocumentTest::testLosslessRotate()
0446 {
0447     // Generate test image
0448     QImage image1(200, 96, QImage::Format_RGB32);
0449     {
0450         QPainter painter(&image1);
0451         QConicalGradient gradient(QPointF(100, 48), 100);
0452         gradient.setColorAt(0, Qt::white);
0453         gradient.setColorAt(1, Qt::blue);
0454         painter.fillRect(image1.rect(), gradient);
0455     }
0456 
0457     QUrl url1 = urlForTestOutputFile("lossless1.jpg");
0458     QVERIFY(image1.save(url1.toLocalFile(), "jpeg"));
0459 
0460     // Load it as a Gwenview document
0461     Document::Ptr doc = DocumentFactory::instance()->load(url1);
0462     doc->waitUntilLoaded();
0463 
0464     // Rotate one time
0465     QVERIFY(doc->editor());
0466     doc->editor()->applyTransformation(ROT_90);
0467 
0468     // Save it
0469     QUrl url2 = urlForTestOutputFile("lossless2.jpg");
0470     waitUntilJobIsDone(doc->save(url2, "jpeg"));
0471 
0472     // Load the saved image
0473     doc = DocumentFactory::instance()->load(url2);
0474     doc->waitUntilLoaded();
0475 
0476     // Rotate the other way
0477     QVERIFY(doc->editor());
0478     doc->editor()->applyTransformation(ROT_270);
0479     waitUntilJobIsDone(doc->save(url2, "jpeg"));
0480 
0481     // Compare the saved images
0482     QVERIFY(image1.load(url1.toLocalFile()));
0483     QImage image2;
0484     QVERIFY(image2.load(url2.toLocalFile()));
0485 
0486     QCOMPARE(image1, image2);
0487 }
0488 
0489 void DocumentTest::testModifyAndSaveAs()
0490 {
0491     QVariantList args;
0492     class TestOperation : public AbstractImageOperation
0493     {
0494     public:
0495         void redo() override
0496         {
0497             QImage image(10, 10, QImage::Format_ARGB32);
0498             image.fill(QColor(Qt::white).rgb());
0499             document()->editor()->setImage(image);
0500             finish(true);
0501         }
0502     };
0503     QUrl url = urlForTestFile("orient6.jpg");
0504     DocumentFactory *factory = DocumentFactory::instance();
0505     Document::Ptr doc = factory->load(url);
0506 
0507     QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl, QUrl)));
0508     QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged()));
0509     QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(QUrl)));
0510 
0511     doc->waitUntilLoaded();
0512     QVERIFY(!doc->isModified());
0513     QCOMPARE(modifiedDocumentListChangedSpy.count(), 0);
0514 
0515     // Modify image
0516     QVERIFY(doc->editor());
0517     auto op = new TestOperation;
0518     op->applyToDocument(doc);
0519     QTest::qWait(100);
0520     QVERIFY(doc->isModified());
0521     QCOMPARE(modifiedDocumentListChangedSpy.count(), 1);
0522     modifiedDocumentListChangedSpy.clear();
0523     QList<QUrl> lst = factory->modifiedDocumentList();
0524     QCOMPARE(lst.count(), 1);
0525     QCOMPARE(lst.first(), url);
0526     QCOMPARE(documentChangedSpy.count(), 1);
0527     args = documentChangedSpy.takeFirst();
0528     QCOMPARE(args.at(0).toUrl(), url);
0529 
0530     // Save it under a new name
0531     QUrl destUrl = urlForTestOutputFile("modify.png");
0532     QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png")));
0533 
0534     // Wait a bit because save() will clear the undo stack when back to the
0535     // event loop
0536     QTest::qWait(100);
0537     QVERIFY(!doc->isModified());
0538 
0539     QVERIFY(!factory->hasUrl(url));
0540     QVERIFY(factory->hasUrl(destUrl));
0541     QCOMPARE(modifiedDocumentListChangedSpy.count(), 1);
0542     QVERIFY(DocumentFactory::instance()->modifiedDocumentList().isEmpty());
0543 
0544     QCOMPARE(documentChangedSpy.count(), 2);
0545     QList<QUrl> modifiedUrls = QList<QUrl>() << url << destUrl;
0546     QVERIFY(modifiedUrls.contains(url));
0547     QVERIFY(modifiedUrls.contains(destUrl));
0548 }
0549 
0550 void DocumentTest::testMetaInfoJpeg()
0551 {
0552     QUrl url = urlForTestFile("orient6.jpg");
0553     Document::Ptr doc = DocumentFactory::instance()->load(url);
0554 
0555     // We cleared the cache, so the document should not be loaded
0556     Q_ASSERT(doc->loadingState() <= Document::KindDetermined);
0557 
0558     // Wait until we receive the metaInfoUpdated() signal
0559     QSignalSpy metaInfoUpdatedSpy(doc.data(), SIGNAL(metaInfoUpdated()));
0560     while (metaInfoUpdatedSpy.isEmpty()) {
0561         QTest::qWait(100);
0562     }
0563 
0564     // Extract an exif key
0565     QString value = doc->metaInfo()->getValueForKey("Exif.Image.Make");
0566     QCOMPARE(value, QString::fromUtf8("Canon"));
0567 }
0568 
0569 void DocumentTest::testMetaInfoBmp()
0570 {
0571     QUrl url = urlForTestOutputFile("metadata.bmp");
0572     const int width = 200;
0573     const int height = 100;
0574     QImage image(width, height, QImage::Format_ARGB32);
0575     image.fill(Qt::black);
0576     image.save(url.toLocalFile(), "BMP");
0577 
0578     Document::Ptr doc = DocumentFactory::instance()->load(url);
0579     QSignalSpy metaInfoUpdatedSpy(doc.data(), SIGNAL(metaInfoUpdated()));
0580     waitUntilMetaInfoLoaded(doc);
0581 
0582     Q_ASSERT(metaInfoUpdatedSpy.count() >= 1);
0583 
0584     QString value = doc->metaInfo()->getValueForKey("General.ImageSize");
0585     QString expectedValue = QStringLiteral("%1x%2").arg(width).arg(height);
0586     QCOMPARE(value, expectedValue);
0587 }
0588 
0589 void DocumentTest::testForgetModifiedDocument()
0590 {
0591     QSignalSpy spy(DocumentFactory::instance(), SIGNAL(modifiedDocumentListChanged()));
0592     DocumentFactory::instance()->forget(QUrl("file://does/not/exist.png"));
0593     QCOMPARE(spy.count(), 0);
0594 
0595     // Generate test image
0596     QImage image1(200, 96, QImage::Format_RGB32);
0597     {
0598         QPainter painter(&image1);
0599         QConicalGradient gradient(QPointF(100, 48), 100);
0600         gradient.setColorAt(0, Qt::white);
0601         gradient.setColorAt(1, Qt::blue);
0602         painter.fillRect(image1.rect(), gradient);
0603     }
0604 
0605     QUrl url = urlForTestOutputFile("testForgetModifiedDocument.png");
0606     QVERIFY(image1.save(url.toLocalFile(), "png"));
0607 
0608     // Load it as a Gwenview document
0609     Document::Ptr doc = DocumentFactory::instance()->load(url);
0610     doc->waitUntilLoaded();
0611 
0612     // Modify it
0613     auto op = new TransformImageOperation(ROT_90);
0614     op->applyToDocument(doc);
0615     QTest::qWait(100);
0616 
0617     QCOMPARE(spy.count(), 1);
0618 
0619     QList<QUrl> lst = DocumentFactory::instance()->modifiedDocumentList();
0620     QCOMPARE(lst.length(), 1);
0621     QCOMPARE(lst.first(), url);
0622 
0623     // Forget it
0624     DocumentFactory::instance()->forget(url);
0625 
0626     QCOMPARE(spy.count(), 2);
0627     lst = DocumentFactory::instance()->modifiedDocumentList();
0628     QVERIFY(lst.isEmpty());
0629 }
0630 
0631 void DocumentTest::testModifiedAndSavedSignals()
0632 {
0633     TransformImageOperation *op;
0634 
0635     QUrl url = urlForTestFile("orient6.jpg");
0636     Document::Ptr doc = DocumentFactory::instance()->load(url);
0637     QSignalSpy modifiedSpy(doc.data(), SIGNAL(modified(QUrl)));
0638     QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl, QUrl)));
0639     doc->waitUntilLoaded();
0640 
0641     QCOMPARE(modifiedSpy.count(), 0);
0642     QCOMPARE(savedSpy.count(), 0);
0643 
0644     op = new TransformImageOperation(ROT_90);
0645     op->applyToDocument(doc);
0646     QTest::qWait(100);
0647     QCOMPARE(modifiedSpy.count(), 1);
0648 
0649     op = new TransformImageOperation(ROT_90);
0650     op->applyToDocument(doc);
0651     QTest::qWait(100);
0652     QCOMPARE(modifiedSpy.count(), 2);
0653 
0654     doc->undoStack()->undo();
0655     QTest::qWait(100);
0656     QCOMPARE(modifiedSpy.count(), 3);
0657 
0658     doc->undoStack()->undo();
0659     QTest::qWait(100);
0660     QCOMPARE(savedSpy.count(), 1);
0661 }
0662 
0663 class TestJob : public DocumentJob
0664 {
0665 public:
0666     TestJob(QString *str, char ch)
0667         : mStr(str)
0668         , mCh(ch)
0669     {
0670     }
0671 
0672 protected:
0673     void doStart() override
0674     {
0675         *mStr += mCh;
0676         emitResult();
0677     }
0678 
0679 private:
0680     QString *mStr;
0681     char mCh;
0682 };
0683 
0684 void DocumentTest::testJobQueue()
0685 {
0686     QUrl url = urlForTestFile("orient6.jpg");
0687     Document::Ptr doc = DocumentFactory::instance()->load(url);
0688     QSignalSpy spy(doc.data(), SIGNAL(busyChanged(QUrl, bool)));
0689 
0690     QString str;
0691     doc->enqueueJob(new TestJob(&str, 'a'));
0692     doc->enqueueJob(new TestJob(&str, 'b'));
0693     doc->enqueueJob(new TestJob(&str, 'c'));
0694     QVERIFY(doc->isBusy());
0695     QEventLoop loop;
0696     connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit);
0697     loop.exec();
0698     QVERIFY(!doc->isBusy());
0699     QCOMPARE(spy.count(), 2);
0700     QVariantList row = spy.takeFirst();
0701     QCOMPARE(row.at(0).toUrl(), url);
0702     QVERIFY(row.at(1).toBool());
0703     row = spy.takeFirst();
0704     QCOMPARE(row.at(0).toUrl(), url);
0705     QVERIFY(!row.at(1).toBool());
0706     QCOMPARE(str, QStringLiteral("abc"));
0707 }
0708 
0709 class TestCheckDocumentEditorJob : public DocumentJob
0710 {
0711 public:
0712     TestCheckDocumentEditorJob(int *hasEditor)
0713         : mHasEditor(hasEditor)
0714     {
0715         *mHasEditor = -1;
0716     }
0717 
0718 protected:
0719     void doStart() override
0720     {
0721         document()->waitUntilLoaded();
0722         *mHasEditor = checkDocumentEditor() ? 1 : 0;
0723         emitResult();
0724     }
0725 
0726 private:
0727     int *mHasEditor;
0728 };
0729 
0730 class TestUiDelegate : public KJobUiDelegate
0731 {
0732 public:
0733     TestUiDelegate(bool *showErrorMessageCalled)
0734         : mShowErrorMessageCalled(showErrorMessageCalled)
0735     {
0736         setAutoErrorHandlingEnabled(true);
0737         *mShowErrorMessageCalled = false;
0738     }
0739 
0740     void showErrorMessage() override
0741     {
0742         // qDebug();
0743         *mShowErrorMessageCalled = true;
0744     }
0745 
0746 private:
0747     bool *mShowErrorMessageCalled;
0748 };
0749 
0750 /**
0751  * Test that an error is reported when a DocumentJob fails because there is no
0752  * document editor available
0753  */
0754 void DocumentTest::testCheckDocumentEditor()
0755 {
0756     int hasEditor;
0757     bool showErrorMessageCalled;
0758     QEventLoop loop;
0759     Document::Ptr doc;
0760     TestCheckDocumentEditorJob *job;
0761 
0762     doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg"));
0763 
0764     job = new TestCheckDocumentEditorJob(&hasEditor);
0765     job->setUiDelegate(new TestUiDelegate(&showErrorMessageCalled));
0766     doc->enqueueJob(job);
0767     connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit);
0768     loop.exec();
0769     QVERIFY(!showErrorMessageCalled);
0770     QCOMPARE(hasEditor, 1);
0771 
0772     doc = DocumentFactory::instance()->load(urlForTestFile("test.svg"));
0773 
0774     job = new TestCheckDocumentEditorJob(&hasEditor);
0775     job->setUiDelegate(new TestUiDelegate(&showErrorMessageCalled));
0776     doc->enqueueJob(job);
0777     connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit);
0778     loop.exec();
0779     QVERIFY(showErrorMessageCalled);
0780     QCOMPARE(hasEditor, 0);
0781 }
0782 
0783 /**
0784  * An operation should only pushed to the document undo stack if it succeed
0785  */
0786 void DocumentTest::testUndoStackPush()
0787 {
0788     class SuccessOperation : public AbstractImageOperation
0789     {
0790     protected:
0791         void redo() override
0792         {
0793             QMetaObject::invokeMethod(
0794                 this,
0795                 [this]() {
0796                     finish(true);
0797                 },
0798                 Qt::QueuedConnection);
0799         }
0800     };
0801 
0802     class FailureOperation : public AbstractImageOperation
0803     {
0804     protected:
0805         void redo() override
0806         {
0807             QMetaObject::invokeMethod(
0808                 this,
0809                 [this]() {
0810                     finish(false);
0811                 },
0812                 Qt::QueuedConnection);
0813         }
0814     };
0815 
0816     AbstractImageOperation *op;
0817     Document::Ptr doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg"));
0818 
0819     // A successful operation should be added to the undo stack
0820     op = new SuccessOperation;
0821     op->applyToDocument(doc);
0822     QTest::qWait(100);
0823     QVERIFY(!doc->undoStack()->isClean());
0824 
0825     // Reset
0826     doc->undoStack()->undo();
0827     QVERIFY(doc->undoStack()->isClean());
0828 
0829     // A failed operation should not be added to the undo stack
0830     op = new FailureOperation;
0831     op->applyToDocument(doc);
0832     QTest::qWait(100);
0833     QVERIFY(doc->undoStack()->isClean());
0834 }
0835 
0836 void DocumentTest::testUndoRedo()
0837 {
0838     class SuccessOperation : public AbstractImageOperation
0839     {
0840     public:
0841         int mRedoCount = 0;
0842         int mUndoCount = 0;
0843 
0844     protected:
0845         void redo() override
0846         {
0847             mRedoCount++;
0848             finish(true);
0849         }
0850 
0851         void undo() override
0852         {
0853             mUndoCount++;
0854             finish(true);
0855         }
0856     };
0857 
0858     Document::Ptr doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg"));
0859     QSignalSpy modifiedSpy(doc.data(), &Document::modified);
0860     QSignalSpy savedSpy(doc.data(), &Document::saved);
0861 
0862     auto op = new SuccessOperation;
0863     QCOMPARE(op->mRedoCount, 0);
0864     QCOMPARE(op->mUndoCount, 0);
0865 
0866     // Apply (redo) operation
0867     op->applyToDocument(doc);
0868     QVERIFY(modifiedSpy.wait());
0869     QCOMPARE(op->mRedoCount, 1);
0870     QCOMPARE(op->mUndoCount, 0);
0871     QCOMPARE(doc->undoStack()->count(), 1);
0872     QVERIFY(!doc->undoStack()->isClean());
0873 
0874     // Undo operation
0875     doc->undoStack()->undo();
0876     QVERIFY(savedSpy.wait());
0877     QCOMPARE(op->mRedoCount, 1);
0878     QCOMPARE(op->mUndoCount, 1);
0879     QCOMPARE(doc->undoStack()->count(), 1);
0880     QVERIFY(doc->undoStack()->isClean());
0881 
0882     // Redo operation
0883     doc->undoStack()->redo();
0884     QVERIFY(modifiedSpy.wait());
0885     QCOMPARE(op->mRedoCount, 2);
0886     QCOMPARE(op->mUndoCount, 1);
0887     QCOMPARE(doc->undoStack()->count(), 1);
0888     QVERIFY(!doc->undoStack()->isClean());
0889 
0890     // Undo operation again
0891     doc->undoStack()->undo();
0892     QVERIFY(savedSpy.wait());
0893     QCOMPARE(op->mRedoCount, 2);
0894     QCOMPARE(op->mUndoCount, 2);
0895     QCOMPARE(doc->undoStack()->count(), 1);
0896     QVERIFY(doc->undoStack()->isClean());
0897 }
0898 
0899 #include "moc_documenttest.cpp"