diff --git a/lib/abstractimageoperation.cpp b/lib/abstractimageoperation.cpp index 396be960..38631f39 100644 --- a/lib/abstractimageoperation.cpp +++ b/lib/abstractimageoperation.cpp @@ -1,64 +1,111 @@ // vim: set tabstop=4 shiftwidth=4 noexpandtab: /* 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 "abstractimageoperation.h" +#include "abstractimageoperation.moc" // Qt // KDE +#include #include // Local #include "document/documentfactory.h" +#include "document/documentjob.h" namespace Gwenview { +class ImageOperationCommand : public QUndoCommand { +public: + ImageOperationCommand(AbstractImageOperation* op) + : mOp(op) + {} + + ~ImageOperationCommand() { + delete mOp; + } + + virtual void undo() { + mOp->undo(); + } + +private: + AbstractImageOperation* mOp; +}; + struct AbstractImageOperationPrivate { + QString mText; KUrl mUrl; }; AbstractImageOperation::AbstractImageOperation() -: QUndoCommand() -, d(new AbstractImageOperationPrivate) { +: d(new AbstractImageOperationPrivate) { } AbstractImageOperation::~AbstractImageOperation() { delete d; } void AbstractImageOperation::applyToDocument(Document::Ptr doc) { d->mUrl = doc->url(); - doc->undoStack()->push(this); + redo(); } Document::Ptr AbstractImageOperation::document() const { Document::Ptr doc = DocumentFactory::instance()->load(d->mUrl); doc->loadFullImage(); return doc; } +void AbstractImageOperation::finish(bool ok) { + if (ok) { + ImageOperationCommand* command = new ImageOperationCommand(this); + command->setText(d->mText); + document()->undoStack()->push(command); + } else { + deleteLater(); + } +} + + +void AbstractImageOperation::finishFromKJob(KJob* job) { + finish(job->error() == KJob::NoError); +} + + +void AbstractImageOperation::setText(const QString& text) { + d->mText = text; +} + + +void AbstractImageOperation::redoAsDocumentJob(DocumentJob* job) { + connect(job, SIGNAL(result(KJob*)), SLOT(finishFromKJob(KJob*))); + document()->enqueueJob(job); +} + + } // namespace diff --git a/lib/abstractimageoperation.h b/lib/abstractimageoperation.h index 65a0e8d4..ddf17a95 100644 --- a/lib/abstractimageoperation.h +++ b/lib/abstractimageoperation.h @@ -1,53 +1,88 @@ // vim: set tabstop=4 shiftwidth=4 noexpandtab: /* 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. */ #ifndef ABSTRACTIMAGEOPERATION_H #define ABSTRACTIMAGEOPERATION_H #include // Qt #include // KDE // Local #include +class KJob; + namespace Gwenview { struct AbstractImageOperationPrivate; -class GWENVIEWLIB_EXPORT AbstractImageOperation : protected QUndoCommand { + +/** + * An operation to apply on a document. This class pushes internal instances of + * QUndoCommand to the document undo stack, but it only does so if the + * operation succeeded. + * + * Class inheriting from this class should: + * - Implement redo() and call finish() or finishFromKJob() when done + * - Implement undo() + * - Define the operation/command text with setText() + */ +class GWENVIEWLIB_EXPORT AbstractImageOperation : public QObject { + Q_OBJECT public: AbstractImageOperation(); virtual ~AbstractImageOperation(); void applyToDocument(Document::Ptr); Document::Ptr document() const; +protected: + virtual void redo() = 0; + virtual void undo() {} + void setText(const QString&); + + /** + * Convenience method which can be called from redo() if the operation is + * implemented as a job + */ + void redoAsDocumentJob(DocumentJob* job); + +protected Q_SLOTS: + void finish(bool ok); + + /** + * Convenience slot which call finish() correctly if job succeeded + */ + void finishFromKJob(KJob* job); + private: AbstractImageOperationPrivate* const d; + + friend class ImageOperationCommand; }; } // namespace #endif /* ABSTRACTIMAGEOPERATION_H */ diff --git a/lib/crop/cropimageoperation.cpp b/lib/crop/cropimageoperation.cpp index 3cb1f9a2..6cedb6be 100644 --- a/lib/crop/cropimageoperation.cpp +++ b/lib/crop/cropimageoperation.cpp @@ -1,97 +1,97 @@ // vim: set tabstop=4 shiftwidth=4 noexpandtab: /* 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 "cropimageoperation.h" // Qt #include #include // KDE #include #include // Local #include "document/document.h" #include "document/documentjob.h" #include "document/abstractdocumenteditor.h" namespace Gwenview { class CropJob : public ThreadedDocumentJob { public: CropJob(const QRect& rect) : mRect(rect) {} virtual void threadedStart() { if (!checkDocumentEditor()) { return; } QImage src = document()->image(); QImage dst(mRect.size(), src.format()); QPainter painter(&dst); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.drawImage(QPoint(0, 0), src, mRect); painter.end(); document()->editor()->setImage(dst); setError(NoError); } private: QRect mRect; }; struct CropImageOperationPrivate { QRect mRect; QImage mOriginalImage; }; CropImageOperation::CropImageOperation(const QRect& rect) : d(new CropImageOperationPrivate) { d->mRect = rect; setText(i18n("Crop")); } CropImageOperation::~CropImageOperation() { delete d; } void CropImageOperation::redo() { d->mOriginalImage = document()->image(); - document()->enqueueJob(new CropJob(d->mRect)); + redoAsDocumentJob(new CropJob(d->mRect)); } void CropImageOperation::undo() { if (!document()->editor()) { kWarning() << "!document->editor()"; return; } document()->editor()->setImage(d->mOriginalImage); } } // namespace diff --git a/lib/redeyereduction/redeyereductionimageoperation.cpp b/lib/redeyereduction/redeyereductionimageoperation.cpp index 112577e6..18005c25 100644 --- a/lib/redeyereduction/redeyereductionimageoperation.cpp +++ b/lib/redeyereduction/redeyereductionimageoperation.cpp @@ -1,163 +1,163 @@ // vim: set tabstop=4 shiftwidth=4 noexpandtab: /* 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 "redeyereductionimageoperation.h" // Stdc #include // Qt #include #include // KDE #include #include // Local #include "ramp.h" #include "document/document.h" #include "document/documentjob.h" #include "document/abstractdocumenteditor.h" #include "paintutils.h" namespace Gwenview { class RedEyeReductionJob : public ThreadedDocumentJob { public: RedEyeReductionJob(const QRectF& rectF) : mRectF(rectF) {} void threadedStart() { if (!checkDocumentEditor()) { return; } QImage img = document()->image(); RedEyeReductionImageOperation::apply(&img, mRectF); document()->editor()->setImage(img); setError(NoError); } private: QRectF mRectF; }; struct RedEyeReductionImageOperationPrivate { QRectF mRectF; QImage mOriginalImage; }; RedEyeReductionImageOperation::RedEyeReductionImageOperation(const QRectF& rectF) : d(new RedEyeReductionImageOperationPrivate) { d->mRectF = rectF; setText(i18n("RedEyeReduction")); } RedEyeReductionImageOperation::~RedEyeReductionImageOperation() { delete d; } void RedEyeReductionImageOperation::redo() { QImage img = document()->image(); QRect rect = PaintUtils::containingRect(d->mRectF); d->mOriginalImage = img.copy(rect); - document()->enqueueJob(new RedEyeReductionJob(d->mRectF)); + redoAsDocumentJob(new RedEyeReductionJob(d->mRectF)); } void RedEyeReductionImageOperation::undo() { if (!document()->editor()) { kWarning() << "!document->editor()"; return; } QImage img = document()->image(); { QPainter painter(&img); painter.setCompositionMode(QPainter::CompositionMode_Source); QRect rect = PaintUtils::containingRect(d->mRectF); painter.drawImage(rect.topLeft(), d->mOriginalImage); } document()->editor()->setImage(img); } /** * This code is inspired from code found in a Paint.net plugin: * http://paintdotnet.forumer.com/viewtopic.php?f=27&t=26193&p=205954&hilit=red+eye#p205954 */ inline qreal computeRedEyeAlpha(const QColor& src) { int hue, sat, value; src.getHsv(&hue, &sat, &value); qreal axs = 1.0; if (hue > 259) { static const Ramp ramp(30, 35, 0., 1.); axs = ramp(sat); } else { const Ramp ramp(hue * 2 + 29, hue * 2 + 40, 0., 1.); axs = ramp(sat); } return qBound(0., double(src.alphaF()) * axs, 1.); } void RedEyeReductionImageOperation::apply(QImage* img, const QRectF& rectF) { const QRect rect = PaintUtils::containingRect(rectF); const qreal radius = rectF.width() / 2; const qreal centerX = rectF.x() + radius; const qreal centerY = rectF.y() + radius; const Ramp radiusRamp(qMin(double(radius * 0.7), double(radius - 1)), radius, 1., 0.); uchar* line = img->scanLine(rect.top()) + rect.left() * 4; for (int y = rect.top(); y < rect.bottom(); ++y, line += img->bytesPerLine()) { QRgb* ptr = (QRgb*)line; for (int x = rect.left(); x < rect.right(); ++x, ++ptr) { const qreal currentRadius = sqrt(pow(y - centerY, 2) + pow(x - centerX, 2)); qreal alpha = radiusRamp(currentRadius); if (qFuzzyCompare(alpha, 0)) { continue; } const QColor src(*ptr); alpha *= computeRedEyeAlpha(src); int r = src.red(); int g = src.green(); int b = src.blue(); QColor dst; // Replace red with green, and blend according to alpha dst.setRed (int((1 - alpha) * r + alpha * g)); dst.setGreen(int((1 - alpha) * g + alpha * g)); dst.setBlue (int((1 - alpha) * b + alpha * b)); *ptr = dst.rgba(); } } } } // namespace diff --git a/lib/resizeimageoperation.cpp b/lib/resizeimageoperation.cpp index 6702ceb3..e7b7b97c 100644 --- a/lib/resizeimageoperation.cpp +++ b/lib/resizeimageoperation.cpp @@ -1,93 +1,93 @@ // vim: set tabstop=4 shiftwidth=4 noexpandtab: /* 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 "resizeimageoperation.h" // Qt #include // KDE #include #include // Local #include "document/abstractdocumenteditor.h" #include "document/document.h" #include "document/documentjob.h" namespace Gwenview { struct ResizeImageOperationPrivate { int mSize; QImage mOriginalImage; }; class ResizeJob : public ThreadedDocumentJob { public: ResizeJob(int size) : mSize(size) {} virtual void threadedStart() { if (!checkDocumentEditor()) { return; } QImage image = document()->image(); image = image.scaled(mSize, mSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); document()->editor()->setImage(image); setError(NoError); } private: int mSize; }; ResizeImageOperation::ResizeImageOperation(int size) : d(new ResizeImageOperationPrivate) { d->mSize = size; setText(i18n("Resize")); } ResizeImageOperation::~ResizeImageOperation() { delete d; } void ResizeImageOperation::redo() { d->mOriginalImage = document()->image(); - document()->enqueueJob(new ResizeJob(d->mSize)); + redoAsDocumentJob(new ResizeJob(d->mSize)); } void ResizeImageOperation::undo() { if (!document()->editor()) { kWarning() << "!document->editor()"; return; } document()->editor()->setImage(d->mOriginalImage); } } // namespace diff --git a/lib/transformimageoperation.cpp b/lib/transformimageoperation.cpp index 08ef17cc..fdfe08c1 100644 --- a/lib/transformimageoperation.cpp +++ b/lib/transformimageoperation.cpp @@ -1,113 +1,113 @@ // vim: set tabstop=4 shiftwidth=4 noexpandtab: /* 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 "transformimageoperation.h" // Qt // KDE #include #include // Local #include "document/abstractdocumenteditor.h" #include "document/documentjob.h" namespace Gwenview { struct TransformImageOperationPrivate { Orientation mOrientation; }; class TransformJob : public ThreadedDocumentJob { public: TransformJob(Orientation orientation) : mOrientation(orientation) {} virtual void threadedStart() { if (!checkDocumentEditor()) { return; } document()->editor()->applyTransformation(mOrientation); setError(NoError); } private: Orientation mOrientation; }; TransformImageOperation::TransformImageOperation(Orientation orientation) : d(new TransformImageOperationPrivate) { d->mOrientation = orientation; switch (d->mOrientation) { case ROT_90: setText(i18n("Rotate Right")); break; case ROT_270: setText(i18n("Rotate Left")); break; case HFLIP: setText(i18n("Mirror")); break; case VFLIP: setText(i18n("Flip")); break; default: // We should not get there because the transformations listed above are // the only one available from the UI. Define a default text nevertheless. setText(i18n("Transform")); break; } } TransformImageOperation::~TransformImageOperation() { delete d; } void TransformImageOperation::redo() { - document()->enqueueJob(new TransformJob(d->mOrientation)); + redoAsDocumentJob(new TransformJob(d->mOrientation)); } void TransformImageOperation::undo() { Orientation orientation; switch (d->mOrientation) { case ROT_90: orientation = ROT_270; break; case ROT_270: orientation = ROT_90; break; default: orientation = d->mOrientation; break; } document()->enqueueJob(new TransformJob(orientation)); } } // namespace diff --git a/tests/documenttest.cpp b/tests/documenttest.cpp index a1bb8c15..641199ca 100644 --- a/tests/documenttest.cpp +++ b/tests/documenttest.cpp @@ -1,701 +1,744 @@ /* 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->loadFullImage(); doc->waitUntilLoaded(); QCOMPARE(doc->loadingState(), Document::Loaded); QCOMPARE(expectedKind, doc->kind()); QCOMPARE(expectedIsAnimated, doc->isAnimated()); QCOMPARE(spy.count(), doc->isAnimated() ? 1 : 0); if (doc->kind() == MimeTypeUtils::KIND_RASTER_IMAGE) { QCOMPARE(expectedImage, doc->image()); QCOMPARE(expectedFormat, doc->format()); } } #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); } #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->loadFullImage(); 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(const KUrl&))); QSignalSpy loadedSpy(doc.data(), SIGNAL(loaded(const 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(const 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() { QString testTarGzPath = pathForTestFile("test.tar.gz"); KUrl url; url.setProtocol("tar"); url.setPath(testTarGzPath + "/test.png"); QVERIFY2(KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0), "test archive not found"); Document::Ptr doc = DocumentFactory::instance()->load(url); doc->loadFullImage(); doc->waitUntilLoaded(); QImage image = doc->image(); QCOMPARE(image.width(), 300); QCOMPARE(image.height(), 200); } void DocumentTest::testLoadAnimated() { KUrl srcUrl = urlForTestFile("40frames.gif"); Document::Ptr doc = DocumentFactory::instance()->load(srcUrl); QSignalSpy spy(doc.data(), SIGNAL(imageRectUpdated(const QRect&))); doc->loadFullImage(); 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 srcUrl = urlForTestFile("test.png"); Document::Ptr doc = DocumentFactory::instance()->load(srcUrl); doc->loadFullImage(); doc->waitUntilLoaded(); KUrl dstUrl; dstUrl.setProtocol("trash"); dstUrl.setPath("/test.png"); QVERIFY(!waitUntilJobIsDone(doc->save(dstUrl, "png"))); if (qgetenv("REMOTE_SFTP_TEST").isEmpty()) { kWarning() << "*** Define the environment variable REMOTE_SFTP_TEST to try saving an image to sftp://localhost/tmp/test.png"; } else { dstUrl.setProtocol("sftp"); dstUrl.setHost("localhost"); dstUrl.setPath("/tmp/test.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->loadFullImage(); 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(const KUrl&, const KUrl&))); QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged())); QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(const KUrl&))); doc->loadFullImage(); KUrl destUrl = urlForTestOutputFile("result.png"); QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png"))); QCOMPARE(doc->format().data(), "png"); QCOMPARE(doc->url(), destUrl); 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->loadFullImage(); 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->loadFullImage(); 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->loadFullImage(); 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(const KUrl&, const KUrl&))); QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged())); QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(const KUrl&))); doc->loadFullImage(); 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->loadFullImage(); 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(const KUrl&))); QSignalSpy savedSpy(doc.data(), SIGNAL(saved(const KUrl&, const KUrl&))); doc->loadFullImage(); 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(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); QCOMPARE(spy.takeFirst().at(0).toBool(), true); QCOMPARE(spy.takeFirst().at(0).toBool(), false); 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/documenttest.h b/tests/documenttest.h index 4cc20520..139cd99b 100644 --- a/tests/documenttest.h +++ b/tests/documenttest.h @@ -1,129 +1,130 @@ /* 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. */ #ifndef DOCUMENTTEST_H #define DOCUMENTTEST_H // Qt #include // KDE #include // Local #include "../lib/document/document.h" class LoadingStateSpy : public QObject { Q_OBJECT public: LoadingStateSpy(const Gwenview::Document::Ptr& doc) : mDocument(doc) , mCallCount(0) { } public Q_SLOTS: void readState() { mCallCount++; mState = mDocument->loadingState(); } public: Gwenview::Document::Ptr mDocument; int mCallCount; Gwenview::Document::LoadingState mState; }; class JobWatcher : public QObject { Q_OBJECT public: JobWatcher(KJob* job) : mJob(job) , mDone(false) , mError(0) { connect(job, SIGNAL(result(KJob*)), SLOT(slotResult(KJob*))); connect(job, SIGNAL(destroyed(QObject*)), SLOT(slotDestroyed())); } void wait() { while (!mDone) { QApplication::processEvents(); } } int error() const { return mError; } private Q_SLOTS: void slotResult(KJob* job) { mError = job->error(); mDone = true; } void slotDestroyed() { kWarning() << "Destroyed"; mError = -1; mDone = true; } private: KJob* mJob; bool mDone; int mError; }; class DocumentTest : public QObject { Q_OBJECT private Q_SLOTS: void testLoad(); void testLoad_data(); void testLoadTwoPasses(); void testLoadEmpty(); void testLoadDownSampled(); void testLoadDownSampled_data(); void testLoadDownSampledPng(); void testLoadRemote(); void testLoadAnimated(); void testDeleteWhileLoading(); void testLoadRotated(); void testMultipleLoads(); void testSaveAs(); void testSaveRemote(); void testLosslessSave(); void testLosslessRotate(); void testModifyAndSaveAs(); void testMetaInfoJpeg(); void testMetaInfoBmp(); void testForgetModifiedDocument(); void testModifiedAndSavedSignals(); void testJobQueue(); void testCheckDocumentEditor(); + void testUndoStackPush(); void initTestCase(); void init(); }; #endif // DOCUMENTTEST_H