diff --git a/lib/document/loadingdocumentimpl.cpp b/lib/document/loadingdocumentimpl.cpp index 8531b9df..2ac3a84d 100644 --- a/lib/document/loadingdocumentimpl.cpp +++ b/lib/document/loadingdocumentimpl.cpp @@ -1,463 +1,482 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "loadingdocumentimpl.moc" // STL #include // Qt #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include // Local #include "animateddocumentloadedimpl.h" #include "document.h" #include "documentloadedimpl.h" #include "emptydocumentimpl.h" #include "exiv2imageloader.h" #include "imageutils.h" #include "jpegcontent.h" #include "jpegdocumentloadedimpl.h" #include "orientation.h" #include "svgdocumentloadedimpl.h" #include "urlutils.h" #include "videodocumentloadedimpl.h" namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) kDebug() << x #else #define LOG(x) ; #endif const int HEADER_SIZE = 256; struct LoadingDocumentImplPrivate { LoadingDocumentImpl* q; QPointer mTransferJob; QFuture mMetaInfoFuture; QFutureWatcher mMetaInfoFutureWatcher; QFuture mImageDataFuture; QFutureWatcher mImageDataFutureWatcher; // If != 0, this means we need to load an image at zoom = // 1/mImageDataInvertedZoom int mImageDataInvertedZoom; bool mMetaInfoLoaded; bool mAnimated; bool mDownSampledImageLoaded; + QByteArray mFormatHint; QByteArray mData; QByteArray mFormat; QSize mImageSize; Exiv2::Image::AutoPtr mExiv2Image; std::auto_ptr mJpegContent; QImage mImage; /** * Determine kind of document and switch to an implementation if it is not * necessary to download more data. * @return true if switched to another implementation. */ bool determineKind() { QString mimeType; const KUrl& url = q->document()->url(); if (KProtocolInfo::determineMimetypeFromExtension(url.protocol())) { mimeType = KMimeType::findByNameAndContent(url.fileName(), mData)->name(); } else { mimeType = KMimeType::findByContent(mData)->name(); } MimeTypeUtils::Kind kind = MimeTypeUtils::mimeTypeKind(mimeType); LOG("mimeType:" << mimeType); LOG("kind:" << kind); q->setDocumentKind(kind); switch (kind) { case MimeTypeUtils::KIND_RASTER_IMAGE: case MimeTypeUtils::KIND_SVG_IMAGE: return false; case MimeTypeUtils::KIND_VIDEO: q->switchToImpl(new VideoDocumentLoadedImpl(q->document())); return true; default: q->setDocumentErrorString( i18nc("@info", "Gwenview cannot display documents of type %1.", mimeType) ); emit q->loadingFailed(); q->switchToImpl(new EmptyDocumentImpl(q->document())); return true; } } void startLoading() { Q_ASSERT(!mMetaInfoLoaded); switch (q->document()->kind()) { case MimeTypeUtils::KIND_RASTER_IMAGE: + // The hint is used to: + // - Speed up loadMetaInfo(): QImageReader will try to decode the + // image using plugins matching this format first. + // - Avoid breakage: Because of a bug in Qt TGA image plugin, some + // PNG were incorrectly identified as PCX! See: + // https://bugs.kde.org/show_bug.cgi?id=289819 + // + mFormatHint = q->document()->url().fileName() + .section('.', -1).toAscii().toLower(); mMetaInfoFuture = QtConcurrent::run(this, &LoadingDocumentImplPrivate::loadMetaInfo); mMetaInfoFutureWatcher.setFuture(mMetaInfoFuture); break; case MimeTypeUtils::KIND_SVG_IMAGE: q->switchToImpl(new SvgDocumentLoadedImpl(q->document(), mData)); break; case MimeTypeUtils::KIND_VIDEO: break; default: kWarning() << "We should not reach this point!"; break; } } void startImageDataLoading() { LOG(""); Q_ASSERT(mMetaInfoLoaded); Q_ASSERT(mImageDataInvertedZoom != 0); Q_ASSERT(!mImageDataFuture.isRunning()); mImageDataFuture = QtConcurrent::run(this, &LoadingDocumentImplPrivate::loadImageData); mImageDataFutureWatcher.setFuture(mImageDataFuture); } bool loadMetaInfo() { QBuffer buffer; buffer.setBuffer(&mData); buffer.open(QIODevice::ReadOnly); - QImageReader reader(&buffer); + QImageReader reader(&buffer, mFormatHint); + if (!reader.canRead()) { + return false; + } mFormat = reader.format(); + if (mFormat == "jpg") { + // if mFormatHint was "jpg", then mFormat is "jpg", but the rest of + // Gwenview code assumes JPEG images have "jpeg" format. + mFormat = "jpeg"; + } if (mFormat.isEmpty()) { + kError() << "mFormat.isEmpty(): this should not happen!"; return false; } Exiv2ImageLoader loader; if (loader.load(mData)) { mExiv2Image = loader.popImage(); } if (mFormat == "jpeg" && mExiv2Image.get()) { mJpegContent.reset(new JpegContent()); if (!mJpegContent->loadFromData(mData, mExiv2Image.get())) { return false; } // Use the size from JpegContent, as its correctly transposed if the // image has been rotated mImageSize = mJpegContent->size(); } else { mImageSize = reader.size(); } LOG("mImageSize" << mImageSize); return true; } void loadImageData() { QBuffer buffer; buffer.setBuffer(&mData); buffer.open(QIODevice::ReadOnly); - QImageReader reader(&buffer); + QImageReader reader(&buffer, mFormat); LOG("mImageDataInvertedZoom=" << mImageDataInvertedZoom); if (mImageSize.isValid() && mImageDataInvertedZoom != 1 && reader.supportsOption(QImageIOHandler::ScaledSize) ) { // Do not use mImageSize here: QImageReader needs a non-transposed // image size QSize size = reader.size() / mImageDataInvertedZoom; if (!size.isEmpty()) { LOG("Setting scaled size to" << size); reader.setScaledSize(size); } else { LOG("Not setting scaled size as it is empty" << size); } } bool ok = reader.read(&mImage); if (!ok) { LOG("QImageReader::read() failed"); return; } if (mJpegContent.get()) { Gwenview::Orientation orientation = mJpegContent->orientation(); QMatrix matrix = ImageUtils::transformMatrix(orientation); mImage = mImage.transformed(matrix); } if (reader.supportsAnimation() && reader.nextImageDelay() > 0 // Assume delay == 0 <=> only one frame ) { /* * QImageReader is not really helpful to detect animated gif: * - QImageReader::imageCount() returns 0 * - QImageReader::nextImageDelay() may return something > 0 if the * image consists of only one frame but includes a "Graphic * Control Extension" (usually only present if we have an * animation) (Bug #185523) * * Decoding the next frame is the only reliable way I found to * detect an animated gif */ LOG("May be an animated image. delay:" << reader.nextImageDelay()); QImage nextImage; if (reader.read(&nextImage)) { LOG("Really an animated image (more than one frame)"); mAnimated = true; } else { kWarning() << q->document()->url() << "is not really an animated image (only one frame)"; } } } }; LoadingDocumentImpl::LoadingDocumentImpl(Document* document) : AbstractDocumentImpl(document) , d(new LoadingDocumentImplPrivate) { d->q = this; d->mMetaInfoLoaded = false; d->mAnimated = false; d->mDownSampledImageLoaded = false; d->mImageDataInvertedZoom = 0; connect(&d->mMetaInfoFutureWatcher, SIGNAL(finished()), SLOT(slotMetaInfoLoaded())); connect(&d->mImageDataFutureWatcher, SIGNAL(finished()), SLOT(slotImageLoaded())); } LoadingDocumentImpl::~LoadingDocumentImpl() { LOG(""); // Disconnect watchers to make sure they do not trigger further work d->mMetaInfoFutureWatcher.disconnect(); d->mImageDataFutureWatcher.disconnect(); d->mMetaInfoFutureWatcher.waitForFinished(); d->mImageDataFutureWatcher.waitForFinished(); if (d->mTransferJob) { d->mTransferJob->kill(); } delete d; } void LoadingDocumentImpl::init() { KUrl url = document()->url(); if (UrlUtils::urlIsFastLocalFile(url)) { // Load file content directly QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly)) { setDocumentErrorString(i18nc("@info", "Could not open file %1", url.toLocalFile())); emit loadingFailed(); switchToImpl(new EmptyDocumentImpl(document())); return; } d->mData = file.read(HEADER_SIZE); if (d->determineKind()) { return; } d->mData += file.readAll(); d->startLoading(); } else { // Transfer file via KIO d->mTransferJob = KIO::get(document()->url()); connect(d->mTransferJob, SIGNAL(data(KIO::Job*, QByteArray)), SLOT(slotDataReceived(KIO::Job*, QByteArray))); connect(d->mTransferJob, SIGNAL(result(KJob*)), SLOT(slotTransferFinished(KJob*))); d->mTransferJob->start(); } } void LoadingDocumentImpl::loadImage(int invertedZoom) { if (d->mImageDataInvertedZoom == invertedZoom) { LOG("Already loading an image at invertedZoom=" << invertedZoom); return; } if (d->mImageDataInvertedZoom == 1) { LOG("Ignoring request: we are loading a full image"); return; } d->mImageDataFutureWatcher.waitForFinished(); d->mImageDataInvertedZoom = invertedZoom; if (d->mMetaInfoLoaded) { // Do not test on mMetaInfoFuture.isRunning() here: it might not have // started if we are downloading the image from a remote url d->startImageDataLoading(); } } void LoadingDocumentImpl::slotDataReceived(KIO::Job* job, const QByteArray& chunk) { d->mData.append(chunk); if (document()->kind() == MimeTypeUtils::KIND_UNKNOWN && d->mData.length() >= HEADER_SIZE) { if (d->determineKind()) { job->kill(); return; } } } void LoadingDocumentImpl::slotTransferFinished(KJob* job) { if (job->error()) { setDocumentErrorString(job->errorString()); emit loadingFailed(); switchToImpl(new EmptyDocumentImpl(document())); return; } d->startLoading(); } bool LoadingDocumentImpl::isEditable() const { return d->mDownSampledImageLoaded; } Document::LoadingState LoadingDocumentImpl::loadingState() const { if (!document()->image().isNull()) { return Document::Loaded; } else if (d->mMetaInfoLoaded) { return Document::MetaInfoLoaded; } else if (document()->kind() != MimeTypeUtils::KIND_UNKNOWN) { return Document::KindDetermined; } else { return Document::Loading; } } void LoadingDocumentImpl::slotMetaInfoLoaded() { LOG(""); Q_ASSERT(!d->mMetaInfoFuture.isRunning()); if (!d->mMetaInfoFuture.result()) { setDocumentErrorString( i18nc("@info", "Loading meta information failed.") ); emit loadingFailed(); switchToImpl(new EmptyDocumentImpl(document())); return; } setDocumentFormat(d->mFormat); setDocumentImageSize(d->mImageSize); setDocumentExiv2Image(d->mExiv2Image); d->mMetaInfoLoaded = true; emit metaInfoLoaded(); // Start image loading if necessary // We test if mImageDataFuture is not already running because code connected to // metaInfoLoaded() signal could have called loadImage() if (!d->mImageDataFuture.isRunning() && d->mImageDataInvertedZoom != 0) { d->startImageDataLoading(); } } void LoadingDocumentImpl::slotImageLoaded() { LOG(""); if (d->mImage.isNull()) { setDocumentErrorString( i18nc("@info", "Loading image failed.") ); emit loadingFailed(); switchToImpl(new EmptyDocumentImpl(document())); return; } if (d->mAnimated) { if (d->mImage.size() == d->mImageSize) { // We already decoded the first frame at the right size, let's show // it setDocumentImage(d->mImage); } switchToImpl(new AnimatedDocumentLoadedImpl( document(), d->mData)); return; } if (d->mImageDataInvertedZoom != 1 && d->mImage.size() != d->mImageSize) { LOG("Loaded a down sampled image"); d->mDownSampledImageLoaded = true; // We loaded a down sampled image setDocumentDownSampledImage(d->mImage, d->mImageDataInvertedZoom); return; } LOG("Loaded a full image"); setDocumentImage(d->mImage); DocumentLoadedImpl* impl; if (d->mJpegContent.get()) { impl = new JpegDocumentLoadedImpl( document(), d->mJpegContent.release()); } else { impl = new DocumentLoadedImpl( document(), d->mData); } switchToImpl(impl); } } // namespace diff --git a/tests/auto/documenttest.cpp b/tests/auto/documenttest.cpp index f0afe33e..b103bdfc 100644 --- a/tests/auto/documenttest.cpp +++ b/tests/auto/documenttest.cpp @@ -1,774 +1,776 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Qt #include #include #include // KDE #include #include #include #include // Local #include "../lib/abstractimageoperation.h" #include "../lib/document/abstractdocumenteditor.h" #include "../lib/document/documentjob.h" #include "../lib/document/documentfactory.h" #include "../lib/imagemetainfomodel.h" #include "../lib/imageutils.h" #include "../lib/transformimageoperation.h" #include "testutils.h" #include #include "documenttest.moc" QTEST_KDEMAIN(DocumentTest, GUI) using namespace Gwenview; static void waitUntilMetaInfoLoaded(Document::Ptr doc) { while (doc->loadingState() < Document::MetaInfoLoaded) { QTest::qWait(100); } } static bool waitUntilJobIsDone(DocumentJob* job) { JobWatcher watcher(job); watcher.wait(); return watcher.error() == KJob::NoError; } void DocumentTest::initTestCase() { qRegisterMetaType("KUrl"); } void DocumentTest::init() { DocumentFactory::instance()->clearCache(); } void DocumentTest::testLoad() { QFETCH(QString, fileName); QFETCH(QByteArray, expectedFormat); QFETCH(int, expectedKindInt); QFETCH(bool, expectedIsAnimated); QFETCH(QImage, expectedImage); MimeTypeUtils::Kind expectedKind = MimeTypeUtils::Kind(expectedKindInt); KUrl url = urlForTestFile(fileName); if (expectedKind != MimeTypeUtils::KIND_SVG_IMAGE) { QVERIFY2(!expectedImage.isNull(), "Could not load test image"); } Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy spy(doc.data(), SIGNAL(isAnimatedUpdated())); doc->startLoadingFullImage(); doc->waitUntilLoaded(); QCOMPARE(doc->loadingState(), Document::Loaded); - QCOMPARE(expectedKind, doc->kind()); - QCOMPARE(expectedIsAnimated, doc->isAnimated()); + QCOMPARE(doc->kind(), expectedKind); + QCOMPARE(doc->isAnimated(), expectedIsAnimated); QCOMPARE(spy.count(), doc->isAnimated() ? 1 : 0); if (doc->kind() == MimeTypeUtils::KIND_RASTER_IMAGE) { - QCOMPARE(expectedImage, doc->image()); - QCOMPARE(expectedFormat, doc->format()); + QCOMPARE(doc->image(), expectedImage); + QCOMPARE(QString(doc->format()), QString(expectedFormat)); } } #define NEW_ROW(fileName, format, kind, isAnimated) QTest::newRow(fileName) << fileName << QByteArray(format) << int(kind) << isAnimated << QImage(pathForTestFile(fileName)) void DocumentTest::testLoad_data() { QTest::addColumn("fileName"); QTest::addColumn("expectedFormat"); QTest::addColumn("expectedKindInt"); QTest::addColumn("expectedIsAnimated"); QTest::addColumn("expectedImage"); NEW_ROW("test.png", "png", MimeTypeUtils::KIND_RASTER_IMAGE, false); NEW_ROW("160216_no_size_before_decoding.eps", "eps", MimeTypeUtils::KIND_RASTER_IMAGE, false); NEW_ROW("160382_corrupted.jpeg", "jpeg", MimeTypeUtils::KIND_RASTER_IMAGE, false); NEW_ROW("test.svg", "", MimeTypeUtils::KIND_SVG_IMAGE, false); // FIXME: Test svgz NEW_ROW("1x10k.png", "png", MimeTypeUtils::KIND_RASTER_IMAGE, false); NEW_ROW("1x10k.jpg", "jpeg", MimeTypeUtils::KIND_RASTER_IMAGE, false); NEW_ROW("4frames.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, true); NEW_ROW("1frame.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, false); NEW_ROW("185523_1frame_with_graphic_control_extension.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, false); NEW_ROW("test.xcf", "xcf", MimeTypeUtils::KIND_RASTER_IMAGE, false); NEW_ROW("188191_does_not_load.tga", "tga", MimeTypeUtils::KIND_RASTER_IMAGE, false); + NEW_ROW("289819_does_not_load.png", + "png", MimeTypeUtils::KIND_RASTER_IMAGE, false); } #undef NEW_ROW void DocumentTest::testLoadTwoPasses() { KUrl url = urlForTestFile("test.png"); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load 'test.png'"); Document::Ptr doc = DocumentFactory::instance()->load(url); waitUntilMetaInfoLoaded(doc); QVERIFY2(doc->image().isNull(), "Image shouldn't have been loaded at this time"); QCOMPARE(doc->format().data(), "png"); doc->startLoadingFullImage(); doc->waitUntilLoaded(); QCOMPARE(image, doc->image()); } void DocumentTest::testLoadEmpty() { KUrl url = urlForTestFile("empty.png"); Document::Ptr doc = DocumentFactory::instance()->load(url); while (doc->loadingState() <= Document::KindDetermined) { QTest::qWait(100); } QCOMPARE(doc->loadingState(), Document::LoadingFailed); } #define NEW_ROW(fileName) QTest::newRow(fileName) << fileName void DocumentTest::testLoadDownSampled_data() { QTest::addColumn("fileName"); NEW_ROW("orient6.jpg"); NEW_ROW("1x10k.jpg"); } #undef NEW_ROW void DocumentTest::testLoadDownSampled() { // Note: for now we only support down sampling on jpeg, do not use test.png // here QFETCH(QString, fileName); KUrl url = urlForTestFile(fileName); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load test image"); Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy downSampledImageReadySpy(doc.data(), SIGNAL(downSampledImageReady())); QSignalSpy loadingFailedSpy(doc.data(), SIGNAL(loadingFailed(KUrl))); QSignalSpy loadedSpy(doc.data(), SIGNAL(loaded(KUrl))); bool ready = doc->prepareDownSampledImageForZoom(0.2); QVERIFY2(!ready, "There should not be a down sampled image at this point"); while (downSampledImageReadySpy.count() == 0 && loadingFailedSpy.count() == 0 && loadedSpy.count() == 0) { QTest::qWait(100); } QImage downSampledImage = doc->downSampledImageForZoom(0.2); QVERIFY2(!downSampledImage.isNull(), "Down sampled image should not be null"); QSize expectedSize = doc->size() / 4; if (expectedSize.isEmpty()) { expectedSize = image.size(); } QCOMPARE(downSampledImage.size(), expectedSize); } /** * Down sampling is not supported on png. We should get a complete image * instead. */ void DocumentTest::testLoadDownSampledPng() { KUrl url = urlForTestFile("test.png"); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load test image"); Document::Ptr doc = DocumentFactory::instance()->load(url); LoadingStateSpy stateSpy(doc); connect(doc.data(), SIGNAL(loaded(KUrl)), &stateSpy, SLOT(readState())); bool ready = doc->prepareDownSampledImageForZoom(0.2); QVERIFY2(!ready, "There should not be a down sampled image at this point"); doc->waitUntilLoaded(); QCOMPARE(stateSpy.mCallCount, 1); QCOMPARE(stateSpy.mState, Document::Loaded); } void DocumentTest::testLoadRemote() { KUrl url = setUpRemoteTestDir("test.png"); if (!url.isValid()) { return; } url.addPath("test.png"); QVERIFY2(KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0), "test url not found"); Document::Ptr doc = DocumentFactory::instance()->load(url); doc->startLoadingFullImage(); doc->waitUntilLoaded(); QImage image = doc->image(); QCOMPARE(image.width(), 150); QCOMPARE(image.height(), 100); } void DocumentTest::testLoadAnimated() { KUrl srcUrl = urlForTestFile("40frames.gif"); Document::Ptr doc = DocumentFactory::instance()->load(srcUrl); QSignalSpy spy(doc.data(), SIGNAL(imageRectUpdated(QRect))); doc->startLoadingFullImage(); doc->waitUntilLoaded(); QVERIFY(doc->isAnimated()); // Test we receive only one imageRectUpdated() until animation is started // (the imageRectUpdated() is triggered by the loading of the first image) QTest::qWait(1000); QCOMPARE(spy.count(), 1); // Test we now receive some imageRectUpdated() doc->startAnimation(); QTest::qWait(1000); int count = spy.count(); doc->stopAnimation(); QVERIFY2(count > 0, "No imageRectUpdated() signal received"); // Test we do not receive imageRectUpdated() anymore QTest::qWait(1000); QCOMPARE(count, spy.count()); // Start again, we should receive imageRectUpdated() again doc->startAnimation(); QTest::qWait(1000); QVERIFY2(spy.count() > count, "No imageRectUpdated() signal received after restarting"); } void DocumentTest::testSaveRemote() { KUrl dstUrl = setUpRemoteTestDir(); if (!dstUrl.isValid()) { return; } KUrl srcUrl = urlForTestFile("test.png"); Document::Ptr doc = DocumentFactory::instance()->load(srcUrl); doc->startLoadingFullImage(); doc->waitUntilLoaded(); dstUrl.addPath("testSaveRemote.png"); QVERIFY(waitUntilJobIsDone(doc->save(dstUrl, "png"))); } /** * Check that deleting a document while it is loading does not crash */ void DocumentTest::testDeleteWhileLoading() { { KUrl url = urlForTestFile("test.png"); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load 'test.png'"); Document::Ptr doc = DocumentFactory::instance()->load(url); } DocumentFactory::instance()->clearCache(); // Wait two seconds. If the test fails we will get a segfault while waiting QTest::qWait(2000); } void DocumentTest::testLoadRotated() { KUrl url = urlForTestFile("orient6.jpg"); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load 'orient6.jpg'"); QMatrix matrix = ImageUtils::transformMatrix(ROT_90); image = image.transformed(matrix); Document::Ptr doc = DocumentFactory::instance()->load(url); doc->startLoadingFullImage(); doc->waitUntilLoaded(); QCOMPARE(image, doc->image()); } /** * Checks that asking the DocumentFactory the same document twice in a row does * not load it twice */ void DocumentTest::testMultipleLoads() { KUrl url = urlForTestFile("orient6.jpg"); Document::Ptr doc1 = DocumentFactory::instance()->load(url); Document::Ptr doc2 = DocumentFactory::instance()->load(url); QCOMPARE(doc1.data(), doc2.data()); } void DocumentTest::testSaveAs() { KUrl url = urlForTestFile("orient6.jpg"); DocumentFactory* factory = DocumentFactory::instance(); Document::Ptr doc = factory->load(url); QSignalSpy savedSpy(doc.data(), SIGNAL(saved(KUrl, KUrl))); QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged())); QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(KUrl))); doc->startLoadingFullImage(); KUrl destUrl = urlForTestOutputFile("result.png"); QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png"))); QCOMPARE(doc->format().data(), "png"); QCOMPARE(doc->url(), destUrl); QCOMPARE(doc->metaInfo()->getValueForKey("General.Name"), destUrl.fileName()); QVERIFY2(doc->loadingState() == Document::Loaded, "Document is supposed to finish loading before saving" ); QTest::qWait(100); // saved() is emitted asynchronously QCOMPARE(savedSpy.count(), 1); QVariantList args = savedSpy.takeFirst(); QCOMPARE(args.at(0).value(), url); QCOMPARE(args.at(1).value(), destUrl); QImage image("result.png", "png"); QCOMPARE(doc->image(), image); QVERIFY(!DocumentFactory::instance()->hasUrl(url)); QVERIFY(DocumentFactory::instance()->hasUrl(destUrl)); QCOMPARE(modifiedDocumentListChangedSpy.count(), 0); // No changes were made QCOMPARE(documentChangedSpy.count(), 1); args = documentChangedSpy.takeFirst(); QCOMPARE(args.at(0).value(), destUrl); } void DocumentTest::testLosslessSave() { KUrl url1 = urlForTestFile("orient6.jpg"); Document::Ptr doc = DocumentFactory::instance()->load(url1); doc->startLoadingFullImage(); KUrl url2 = urlForTestOutputFile("orient1.jpg"); QVERIFY(waitUntilJobIsDone(doc->save(url2, "jpeg"))); QImage image1; QVERIFY(image1.load(url1.toLocalFile())); QImage image2; QVERIFY(image2.load(url2.toLocalFile())); QCOMPARE(image1, image2); } void DocumentTest::testLosslessRotate() { // Generate test image QImage image1(200, 96, QImage::Format_RGB32); { QPainter painter(&image1); QConicalGradient gradient(QPointF(100, 48), 100); gradient.setColorAt(0, Qt::white); gradient.setColorAt(1, Qt::blue); painter.fillRect(image1.rect(), gradient); } KUrl url1 = urlForTestOutputFile("lossless1.jpg"); QVERIFY(image1.save(url1.toLocalFile(), "jpeg")); // Load it as a Gwenview document Document::Ptr doc = DocumentFactory::instance()->load(url1); doc->startLoadingFullImage(); doc->waitUntilLoaded(); // Rotate one time QVERIFY(doc->editor()); doc->editor()->applyTransformation(ROT_90); // Save it KUrl url2 = urlForTestOutputFile("lossless2.jpg"); waitUntilJobIsDone(doc->save(url2, "jpeg")); // Load the saved image doc = DocumentFactory::instance()->load(url2); doc->startLoadingFullImage(); doc->waitUntilLoaded(); // Rotate the other way QVERIFY(doc->editor()); doc->editor()->applyTransformation(ROT_270); waitUntilJobIsDone(doc->save(url2, "jpeg")); // Compare the saved images QVERIFY(image1.load(url1.toLocalFile())); QImage image2; QVERIFY(image2.load(url2.toLocalFile())); QCOMPARE(image1, image2); } void DocumentTest::testModifyAndSaveAs() { QVariantList args; class TestOperation : public AbstractImageOperation { public: void redo() { QImage image(10, 10, QImage::Format_ARGB32); image.fill(QColor(Qt::white).rgb()); document()->editor()->setImage(image); finish(true); } }; KUrl url = urlForTestFile("orient6.jpg"); DocumentFactory* factory = DocumentFactory::instance(); Document::Ptr doc = factory->load(url); QSignalSpy savedSpy(doc.data(), SIGNAL(saved(KUrl, KUrl))); QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged())); QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(KUrl))); doc->startLoadingFullImage(); doc->waitUntilLoaded(); QVERIFY(!doc->isModified()); QCOMPARE(modifiedDocumentListChangedSpy.count(), 0); // Modify image QVERIFY(doc->editor()); TestOperation* op = new TestOperation; op->applyToDocument(doc); QVERIFY(doc->isModified()); QCOMPARE(modifiedDocumentListChangedSpy.count(), 1); modifiedDocumentListChangedSpy.clear(); QList lst = factory->modifiedDocumentList(); QCOMPARE(lst.count(), 1); QCOMPARE(lst.first(), url); QCOMPARE(documentChangedSpy.count(), 1); args = documentChangedSpy.takeFirst(); QCOMPARE(args.at(0).value(), url); // Save it under a new name KUrl destUrl = urlForTestOutputFile("modify.png"); QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png"))); // Wait a bit because save() will clear the undo stack when back to the // event loop QTest::qWait(100); QVERIFY(!doc->isModified()); QVERIFY(!factory->hasUrl(url)); QVERIFY(factory->hasUrl(destUrl)); QCOMPARE(modifiedDocumentListChangedSpy.count(), 1); QVERIFY(DocumentFactory::instance()->modifiedDocumentList().isEmpty()); QCOMPARE(documentChangedSpy.count(), 2); KUrl::List modifiedUrls = KUrl::List() << url << destUrl; QVERIFY(modifiedUrls.contains(url)); QVERIFY(modifiedUrls.contains(destUrl)); } void DocumentTest::testMetaInfoJpeg() { KUrl url = urlForTestFile("orient6.jpg"); Document::Ptr doc = DocumentFactory::instance()->load(url); // We cleared the cache, so the document should not be loaded Q_ASSERT(doc->loadingState() <= Document::KindDetermined); // Wait until we receive the metaInfoUpdated() signal QSignalSpy metaInfoUpdatedSpy(doc.data(), SIGNAL(metaInfoUpdated())); while (metaInfoUpdatedSpy.count() == 0) { QTest::qWait(100); } // Extract an exif key QString value = doc->metaInfo()->getValueForKey("Exif.Image.Make"); QCOMPARE(value, QString::fromUtf8("Canon")); } void DocumentTest::testMetaInfoBmp() { KUrl url = urlForTestOutputFile("metadata.bmp"); const int width = 200; const int height = 100; QImage image(width, height, QImage::Format_ARGB32); image.fill(Qt::black); image.save(url.toLocalFile(), "BMP"); Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy metaInfoUpdatedSpy(doc.data(), SIGNAL(metaInfoUpdated())); waitUntilMetaInfoLoaded(doc); Q_ASSERT(metaInfoUpdatedSpy.count() >= 1); QString value = doc->metaInfo()->getValueForKey("General.ImageSize"); QString expectedValue = QString("%1x%2").arg(width).arg(height); QCOMPARE(value, expectedValue); } void DocumentTest::testForgetModifiedDocument() { QSignalSpy spy(DocumentFactory::instance(), SIGNAL(modifiedDocumentListChanged())); DocumentFactory::instance()->forget(KUrl("file://does/not/exist.png")); QCOMPARE(spy.count(), 0); // Generate test image QImage image1(200, 96, QImage::Format_RGB32); { QPainter painter(&image1); QConicalGradient gradient(QPointF(100, 48), 100); gradient.setColorAt(0, Qt::white); gradient.setColorAt(1, Qt::blue); painter.fillRect(image1.rect(), gradient); } KUrl url = urlForTestOutputFile("testForgetModifiedDocument.png"); QVERIFY(image1.save(url.toLocalFile(), "png")); // Load it as a Gwenview document Document::Ptr doc = DocumentFactory::instance()->load(url); doc->startLoadingFullImage(); doc->waitUntilLoaded(); // Modify it TransformImageOperation* op = new TransformImageOperation(ROT_90); op->applyToDocument(doc); QTest::qWait(100); QCOMPARE(spy.count(), 1); QList lst = DocumentFactory::instance()->modifiedDocumentList(); QCOMPARE(lst.length(), 1); QCOMPARE(lst.first(), url); // Forget it DocumentFactory::instance()->forget(url); QCOMPARE(spy.count(), 2); lst = DocumentFactory::instance()->modifiedDocumentList(); QVERIFY(lst.isEmpty()); } void DocumentTest::testModifiedAndSavedSignals() { TransformImageOperation* op; KUrl url = urlForTestFile("orient6.jpg"); Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy modifiedSpy(doc.data(), SIGNAL(modified(KUrl))); QSignalSpy savedSpy(doc.data(), SIGNAL(saved(KUrl, KUrl))); doc->startLoadingFullImage(); doc->waitUntilLoaded(); QCOMPARE(modifiedSpy.count(), 0); QCOMPARE(savedSpy.count(), 0); op = new TransformImageOperation(ROT_90); op->applyToDocument(doc); QTest::qWait(100); QCOMPARE(modifiedSpy.count(), 1); op = new TransformImageOperation(ROT_90); op->applyToDocument(doc); QTest::qWait(100); QCOMPARE(modifiedSpy.count(), 2); doc->undoStack()->undo(); QCOMPARE(modifiedSpy.count(), 3); doc->undoStack()->undo(); QCOMPARE(savedSpy.count(), 1); } class TestJob : public DocumentJob { public: TestJob(QString* str, char ch) : mStr(str) , mCh(ch) {} protected: virtual void doStart() { *mStr += mCh; emitResult(); } private: QString* mStr; char mCh; }; void DocumentTest::testJobQueue() { KUrl url = urlForTestFile("orient6.jpg"); Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy spy(doc.data(), SIGNAL(busyChanged(KUrl, bool))); QString str; doc->enqueueJob(new TestJob(&str, 'a')); doc->enqueueJob(new TestJob(&str, 'b')); doc->enqueueJob(new TestJob(&str, 'c')); QVERIFY(doc->isBusy()); QEventLoop loop; connect(doc.data(), SIGNAL(allTasksDone()), &loop, SLOT(quit())); loop.exec(); QVERIFY(!doc->isBusy()); QCOMPARE(spy.count(), 2); QVariantList row = spy.takeFirst(); QCOMPARE(row.at(0).value(), url); QVERIFY(row.at(1).toBool()); row = spy.takeFirst(); QCOMPARE(row.at(0).value(), url); QVERIFY(!row.at(1).toBool()); QCOMPARE(str, QString("abc")); } class TestCheckDocumentEditorJob : public DocumentJob { public: TestCheckDocumentEditorJob(int* hasEditor) : mHasEditor(hasEditor) { *mHasEditor = -1; } protected: virtual void doStart() { document()->waitUntilLoaded(); *mHasEditor = checkDocumentEditor() ? 1 : 0; emitResult(); } private: int* mHasEditor; }; class TestUiDelegate : public KJobUiDelegate { public: TestUiDelegate(bool* showErrorMessageCalled) : mShowErrorMessageCalled(showErrorMessageCalled) { setAutoErrorHandlingEnabled(true); *mShowErrorMessageCalled = false; } virtual void showErrorMessage() { kDebug(); *mShowErrorMessageCalled = true; } private: bool* mShowErrorMessageCalled; }; /** * Test that an error is reported when a DocumentJob fails because there is no * document editor available */ void DocumentTest::testCheckDocumentEditor() { int hasEditor; bool showErrorMessageCalled; QEventLoop loop; Document::Ptr doc; TestCheckDocumentEditorJob* job; doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg")); job = new TestCheckDocumentEditorJob(&hasEditor); job->setUiDelegate(new TestUiDelegate(&showErrorMessageCalled)); doc->enqueueJob(job); connect(doc.data(), SIGNAL(allTasksDone()), &loop, SLOT(quit())); loop.exec(); QVERIFY(!showErrorMessageCalled); QCOMPARE(hasEditor, 1); doc = DocumentFactory::instance()->load(urlForTestFile("test.svg")); job = new TestCheckDocumentEditorJob(&hasEditor); job->setUiDelegate(new TestUiDelegate(&showErrorMessageCalled)); doc->enqueueJob(job); connect(doc.data(), SIGNAL(allTasksDone()), &loop, SLOT(quit())); loop.exec(); QVERIFY(showErrorMessageCalled); QCOMPARE(hasEditor, 0); } /** * An operation should only pushed to the document undo stack if it succeed */ void DocumentTest::testUndoStackPush() { class SuccessOperation : public AbstractImageOperation { protected: virtual void redo() { QMetaObject::invokeMethod(this, "finish", Qt::QueuedConnection, Q_ARG(bool, true)); } }; class FailureOperation : public AbstractImageOperation { protected: virtual void redo() { QMetaObject::invokeMethod(this, "finish", Qt::QueuedConnection, Q_ARG(bool, false)); } }; AbstractImageOperation* op; Document::Ptr doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg")); // A successful operation should be added to the undo stack op = new SuccessOperation; op->applyToDocument(doc); QTest::qWait(100); QVERIFY(!doc->undoStack()->isClean()); // Reset doc->undoStack()->undo(); QVERIFY(doc->undoStack()->isClean()); // A failed operation should not be added to the undo stack op = new FailureOperation; op->applyToDocument(doc); QTest::qWait(100); QVERIFY(doc->undoStack()->isClean()); } diff --git a/tests/data/289819_does_not_load.png b/tests/data/289819_does_not_load.png new file mode 100644 index 00000000..fb7b5753 Binary files /dev/null and b/tests/data/289819_does_not_load.png differ