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

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 #include "document.h"
0021 #include "document_p.h"
0022 
0023 // Qt
0024 #include <QApplication>
0025 #include <QImage>
0026 #include <QUndoStack>
0027 #include <QUrl>
0028 
0029 // KF
0030 #include <KJobUiDelegate>
0031 #include <KLocalizedString>
0032 
0033 // Exiv2
0034 #include <exiv2/exiv2.hpp>
0035 
0036 // Local
0037 #include "documentjob.h"
0038 #include "gvdebug.h"
0039 #include "gwenview_lib_debug.h"
0040 #include "imagemetainfomodel.h"
0041 #include "loadingdocumentimpl.h"
0042 #include "loadingjob.h"
0043 #include "savejob.h"
0044 
0045 namespace Gwenview
0046 {
0047 #undef ENABLE_LOG
0048 #undef LOG
0049 // #define ENABLE_LOG
0050 #ifdef ENABLE_LOG
0051 #define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
0052 #else
0053 #define LOG(x) ;
0054 #endif
0055 
0056 #ifdef ENABLE_LOG
0057 
0058 static void logQueue(DocumentPrivate *d)
0059 {
0060 #define PREFIX "  QUEUE: "
0061     if (!d->mCurrentJob) {
0062         Q_ASSERT(d->mJobQueue.isEmpty());
0063         qDebug(PREFIX "No current job, no pending jobs");
0064         return;
0065     }
0066     qCDebug(GWENVIEW_LIB_LOG) << PREFIX "Current job:" << d->mCurrentJob.data();
0067     if (d->mJobQueue.isEmpty()) {
0068         qDebug(PREFIX "No pending jobs");
0069         return;
0070     }
0071     qDebug(PREFIX "%d pending job(s):", d->mJobQueue.size());
0072     for (DocumentJob *job : qAsConst(d->mJobQueue)) {
0073         Q_ASSERT(job);
0074         qCDebug(GWENVIEW_LIB_LOG) << PREFIX "-" << job;
0075     }
0076 #undef PREFIX
0077 }
0078 
0079 #define LOG_QUEUE(msg, d)                                                                                                                                      \
0080     LOG(msg);                                                                                                                                                  \
0081     logQueue(d)
0082 
0083 #else
0084 
0085 #define LOG_QUEUE(msg, d)
0086 
0087 #endif
0088 
0089 //- DocumentPrivate ---------------------------------------
0090 void DocumentPrivate::scheduleImageLoading(int invertedZoom)
0091 {
0092     auto impl = qobject_cast<LoadingDocumentImpl *>(mImpl);
0093     Q_ASSERT(impl);
0094     impl->loadImage(invertedZoom);
0095 }
0096 
0097 void DocumentPrivate::scheduleImageDownSampling(int invertedZoom)
0098 {
0099     LOG("invertedZoom=" << invertedZoom);
0100     auto job = qobject_cast<DownSamplingJob *>(mCurrentJob.data());
0101     if (job && job->mInvertedZoom == invertedZoom) {
0102         LOG("Current job is already doing it");
0103         return;
0104     }
0105 
0106     // Remove any previously scheduled downsampling job
0107     DocumentJobQueue::Iterator it;
0108     for (it = mJobQueue.begin(); it != mJobQueue.end(); ++it) {
0109         auto job = qobject_cast<DownSamplingJob *>(*it);
0110         if (!job) {
0111             continue;
0112         }
0113         if (job->mInvertedZoom == invertedZoom) {
0114             // Already scheduled, nothing to do
0115             LOG("Already scheduled");
0116             return;
0117         } else {
0118             LOG("Removing downsampling job");
0119             mJobQueue.erase(it);
0120             delete job;
0121         }
0122     }
0123     q->enqueueJob(new DownSamplingJob(invertedZoom));
0124 }
0125 
0126 void DocumentPrivate::downSampleImage(int invertedZoom)
0127 {
0128     mDownSampledImageMap[invertedZoom] = mImage.scaled(mImage.size() / invertedZoom, Qt::KeepAspectRatio, Qt::FastTransformation);
0129     if (mDownSampledImageMap[invertedZoom].size().isEmpty()) {
0130         mDownSampledImageMap[invertedZoom] = mImage;
0131     }
0132     Q_EMIT q->downSampledImageReady();
0133 }
0134 
0135 //- DownSamplingJob ---------------------------------------
0136 void DownSamplingJob::doStart()
0137 {
0138     DocumentPrivate *d = document()->d;
0139     d->downSampleImage(mInvertedZoom);
0140     setError(NoError);
0141     emitResult();
0142 }
0143 
0144 //- Document ----------------------------------------------
0145 qreal Document::maxDownSampledZoom()
0146 {
0147     return 0.5;
0148 }
0149 
0150 Document::Document(const QUrl &url)
0151     : QObject()
0152     , d(new DocumentPrivate)
0153 {
0154     d->q = this;
0155     d->mImpl = nullptr;
0156     d->mUrl = url;
0157     d->mKeepRawData = false;
0158 }
0159 
0160 Document::~Document()
0161 {
0162     // We do not want undo stack to emit signals, forcing us to emit signals
0163     // ourself while we are being destroyed.
0164     disconnect(&d->mUndoStack, nullptr, this, nullptr);
0165 
0166     delete d->mImpl;
0167     delete d;
0168 }
0169 
0170 void Document::reload()
0171 {
0172     d->mSize = QSize();
0173     d->mImage = QImage();
0174     d->mDownSampledImageMap.clear();
0175     d->mExiv2Image.reset();
0176     d->mKind = MimeTypeUtils::KIND_UNKNOWN;
0177     d->mFormat = QByteArray();
0178     d->mImageMetaInfoModel.setUrl(d->mUrl);
0179     d->mImageMetaInfoModel.setDates(d->mUrl);
0180     d->mImageMetaInfoModel.setMimeType(d->mUrl);
0181     d->mImageMetaInfoModel.setFileSize(d->mUrl);
0182     d->mUndoStack.clear();
0183     d->mErrorString.clear();
0184     d->mCmsProfile = nullptr;
0185 
0186     switchToImpl(new LoadingDocumentImpl(this));
0187 }
0188 
0189 const QImage &Document::image() const
0190 {
0191     return d->mImage;
0192 }
0193 
0194 /**
0195  * invertedZoom is the biggest power of 2 for which zoom < 1/invertedZoom.
0196  * Example:
0197  * zoom = 0.4 == 1/2.5 => invertedZoom = 2 (1/2.5 < 1/2)
0198  * zoom = 0.2 == 1/5   => invertedZoom = 4 (1/5   < 1/4)
0199  */
0200 inline int invertedZoomForZoom(qreal zoom)
0201 {
0202     int invertedZoom;
0203     for (invertedZoom = 1; zoom < 1. / (invertedZoom * 4); invertedZoom *= 2) { }
0204     return invertedZoom;
0205 }
0206 
0207 const QImage &Document::downSampledImageForZoom(qreal zoom) const
0208 {
0209     static const QImage sNullImage;
0210 
0211     int invertedZoom = invertedZoomForZoom(zoom);
0212     if (invertedZoom == 1) {
0213         return d->mImage;
0214     }
0215 
0216     if (!d->mDownSampledImageMap.contains(invertedZoom)) {
0217         if (!d->mImage.isNull()) {
0218             // Special case: if we have the full image and the down sampled
0219             // image would be too small, return the original image.
0220             const QSize downSampledSize = d->mImage.size() / invertedZoom;
0221             if (downSampledSize.isEmpty()) {
0222                 return d->mImage;
0223             }
0224         }
0225         return sNullImage;
0226     }
0227 
0228     return d->mDownSampledImageMap[invertedZoom];
0229 }
0230 
0231 Document::LoadingState Document::loadingState() const
0232 {
0233     return d->mImpl->loadingState();
0234 }
0235 
0236 void Document::switchToImpl(AbstractDocumentImpl *impl)
0237 {
0238     Q_ASSERT(impl);
0239     LOG("old impl:" << d->mImpl << "new impl:" << impl);
0240     if (d->mImpl) {
0241         d->mImpl->deleteLater();
0242     }
0243     d->mImpl = impl;
0244 
0245     connect(d->mImpl, &AbstractDocumentImpl::metaInfoLoaded, this, &Document::emitMetaInfoLoaded);
0246     connect(d->mImpl, &AbstractDocumentImpl::loaded, this, &Document::emitLoaded);
0247     connect(d->mImpl, &AbstractDocumentImpl::loadingFailed, this, &Document::emitLoadingFailed);
0248     connect(d->mImpl, &AbstractDocumentImpl::imageRectUpdated, this, &Document::imageRectUpdated);
0249     connect(d->mImpl, &AbstractDocumentImpl::isAnimatedUpdated, this, &Document::isAnimatedUpdated);
0250     d->mImpl->init();
0251 }
0252 
0253 void Document::setImageInternal(const QImage &image)
0254 {
0255     d->mImage = image;
0256     d->mDownSampledImageMap.clear();
0257 
0258     // If we didn't get the image size before decoding the full image, set it
0259     // now
0260     setSize(d->mImage.size());
0261 }
0262 
0263 QUrl Document::url() const
0264 {
0265     return d->mUrl;
0266 }
0267 
0268 QByteArray Document::rawData() const
0269 {
0270     return d->mImpl->rawData();
0271 }
0272 
0273 bool Document::keepRawData() const
0274 {
0275     return d->mKeepRawData;
0276 }
0277 
0278 void Document::setKeepRawData(bool value)
0279 {
0280     d->mKeepRawData = value;
0281 }
0282 
0283 void Document::waitUntilLoaded()
0284 {
0285     startLoadingFullImage();
0286     while (true) {
0287         LoadingState state = loadingState();
0288         if (state == Loaded || state == LoadingFailed) {
0289             return;
0290         }
0291         qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
0292     }
0293 }
0294 
0295 DocumentJob *Document::save(const QUrl &url, const QByteArray &format)
0296 {
0297     waitUntilLoaded();
0298     DocumentJob *job = d->mImpl->save(url, format);
0299     if (!job) {
0300         qCWarning(GWENVIEW_LIB_LOG) << "Implementation does not support saving!";
0301         setErrorString(i18nc("@info", "Gwenview cannot save this kind of documents."));
0302         return nullptr;
0303     }
0304     job->setProperty("oldUrl", d->mUrl);
0305     job->setProperty("newUrl", url);
0306     connect(job, &DocumentJob::result, this, &Document::slotSaveResult);
0307     enqueueJob(job);
0308     return job;
0309 }
0310 
0311 void Document::slotSaveResult(KJob *job)
0312 {
0313     if (job->error()) {
0314         setErrorString(job->errorString());
0315     } else {
0316         d->mUndoStack.setClean();
0317         auto saveJob = static_cast<SaveJob *>(job);
0318         d->mUrl = saveJob->newUrl();
0319         d->mImageMetaInfoModel.setUrl(d->mUrl);
0320         d->mImageMetaInfoModel.setDates(d->mUrl);
0321         d->mImageMetaInfoModel.setFileSize(d->mUrl);
0322         Q_EMIT saved(saveJob->oldUrl(), d->mUrl);
0323     }
0324 }
0325 
0326 QByteArray Document::format() const
0327 {
0328     return d->mFormat;
0329 }
0330 
0331 void Document::setFormat(const QByteArray &format)
0332 {
0333     d->mFormat = format;
0334     Q_EMIT metaInfoUpdated();
0335 }
0336 
0337 MimeTypeUtils::Kind Document::kind() const
0338 {
0339     return d->mKind;
0340 }
0341 
0342 void Document::setKind(MimeTypeUtils::Kind kind)
0343 {
0344     d->mKind = kind;
0345     Q_EMIT kindDetermined(d->mUrl);
0346 }
0347 
0348 QSize Document::size() const
0349 {
0350     return d->mSize;
0351 }
0352 
0353 bool Document::hasAlphaChannel() const
0354 {
0355     if (d->mImage.isNull()) {
0356         return false;
0357     } else {
0358         return d->mImage.hasAlphaChannel();
0359     }
0360 }
0361 
0362 int Document::memoryUsage() const
0363 {
0364     // FIXME: Take undo stack into account
0365     int usage = d->mImage.sizeInBytes();
0366     usage += rawData().length();
0367     return usage;
0368 }
0369 
0370 void Document::setSize(const QSize &size)
0371 {
0372     if (size == d->mSize) {
0373         return;
0374     }
0375     d->mSize = size;
0376     d->mImageMetaInfoModel.setImageSize(size);
0377     Q_EMIT metaInfoUpdated();
0378 }
0379 
0380 bool Document::isModified() const
0381 {
0382     return !d->mUndoStack.isClean();
0383 }
0384 
0385 AbstractDocumentEditor *Document::editor()
0386 {
0387     return d->mImpl->editor();
0388 }
0389 
0390 void Document::setExiv2Image(std::unique_ptr<Exiv2::Image> image)
0391 {
0392     d->mExiv2Image = std::move(image);
0393     d->mImageMetaInfoModel.setExiv2Image(d->mExiv2Image.get());
0394     Q_EMIT metaInfoUpdated();
0395 }
0396 
0397 void Document::setDownSampledImage(const QImage &image, int invertedZoom)
0398 {
0399     Q_ASSERT(!d->mDownSampledImageMap.contains(invertedZoom));
0400     d->mDownSampledImageMap[invertedZoom] = image;
0401     Q_EMIT downSampledImageReady();
0402 }
0403 
0404 QString Document::errorString() const
0405 {
0406     return d->mErrorString;
0407 }
0408 
0409 void Document::setErrorString(const QString &string)
0410 {
0411     d->mErrorString = string;
0412 }
0413 
0414 ImageMetaInfoModel *Document::metaInfo() const
0415 {
0416     return &d->mImageMetaInfoModel;
0417 }
0418 
0419 void Document::startLoadingFullImage()
0420 {
0421     LoadingState state = loadingState();
0422     if (state <= MetaInfoLoaded) {
0423         // Schedule full image loading
0424         auto job = new LoadingJob;
0425         job->uiDelegate()->setAutoWarningHandlingEnabled(false);
0426         job->uiDelegate()->setAutoErrorHandlingEnabled(false);
0427         enqueueJob(job);
0428         d->scheduleImageLoading(1);
0429     } else if (state == Loaded) {
0430         return;
0431     } else if (state == LoadingFailed) {
0432         qCWarning(GWENVIEW_LIB_LOG) << "Can't load full image: loading has already failed";
0433     }
0434 }
0435 
0436 bool Document::prepareDownSampledImageForZoom(qreal zoom)
0437 {
0438     if (zoom >= maxDownSampledZoom()) {
0439         qCWarning(GWENVIEW_LIB_LOG) << "No need to call prepareDownSampledImageForZoom if zoom >= " << maxDownSampledZoom();
0440         return true;
0441     }
0442 
0443     int invertedZoom = invertedZoomForZoom(zoom);
0444     if (d->mDownSampledImageMap.contains(invertedZoom)) {
0445         LOG("downSampledImageForZoom=" << zoom << "invertedZoom=" << invertedZoom << "ready");
0446         return true;
0447     }
0448 
0449     LOG("downSampledImageForZoom=" << zoom << "invertedZoom=" << invertedZoom << "not ready");
0450     if (loadingState() == LoadingFailed) {
0451         qCWarning(GWENVIEW_LIB_LOG) << "Image has failed to load, not doing anything";
0452         return false;
0453     } else if (loadingState() == Loaded) {
0454         d->scheduleImageDownSampling(invertedZoom);
0455         return false;
0456     }
0457 
0458     // Schedule down sampled image loading
0459     d->scheduleImageLoading(invertedZoom);
0460 
0461     return false;
0462 }
0463 
0464 void Document::emitMetaInfoLoaded()
0465 {
0466     Q_EMIT metaInfoLoaded(d->mUrl);
0467 }
0468 
0469 void Document::emitLoaded()
0470 {
0471     Q_EMIT loaded(d->mUrl);
0472 }
0473 
0474 void Document::emitLoadingFailed()
0475 {
0476     Q_EMIT loadingFailed(d->mUrl);
0477 }
0478 
0479 QUndoStack *Document::undoStack() const
0480 {
0481     return &d->mUndoStack;
0482 }
0483 
0484 void Document::imageOperationCompleted()
0485 {
0486     if (d->mUndoStack.isClean()) {
0487         // If user just undid all his changes this does not really correspond
0488         // to a save, but it's similar enough as far as Document users are
0489         // concerned
0490         Q_EMIT saved(d->mUrl, d->mUrl);
0491     } else {
0492         Q_EMIT modified(d->mUrl);
0493     }
0494 }
0495 
0496 bool Document::isEditable() const
0497 {
0498     return d->mImpl->isEditable();
0499 }
0500 
0501 bool Document::isAnimated() const
0502 {
0503     return d->mImpl->isAnimated();
0504 }
0505 
0506 void Document::startAnimation()
0507 {
0508     return d->mImpl->startAnimation();
0509 }
0510 
0511 void Document::stopAnimation()
0512 {
0513     return d->mImpl->stopAnimation();
0514 }
0515 
0516 void Document::enqueueJob(DocumentJob *job)
0517 {
0518     LOG("job=" << job);
0519     job->setDocument(Ptr(this));
0520     connect(job, &LoadingJob::finished, this, &Document::slotJobFinished);
0521     if (d->mCurrentJob) {
0522         d->mJobQueue.enqueue(job);
0523     } else {
0524         d->mCurrentJob = job;
0525         LOG("Starting first job");
0526         job->start();
0527         Q_EMIT busyChanged(d->mUrl, true);
0528     }
0529     LOG_QUEUE("Job added", d);
0530 }
0531 
0532 void Document::slotJobFinished(KJob *job)
0533 {
0534     LOG("job=" << job);
0535     GV_RETURN_IF_FAIL(job == d->mCurrentJob.data());
0536 
0537     if (d->mJobQueue.isEmpty()) {
0538         LOG("All done");
0539         d->mCurrentJob.clear();
0540         Q_EMIT busyChanged(d->mUrl, false);
0541         Q_EMIT allTasksDone();
0542     } else {
0543         LOG("Starting next job");
0544         d->mCurrentJob = d->mJobQueue.dequeue();
0545         GV_RETURN_IF_FAIL(d->mCurrentJob);
0546         d->mCurrentJob.data()->start();
0547     }
0548     LOG_QUEUE("Removed done job", d);
0549 }
0550 
0551 bool Document::isBusy() const
0552 {
0553     return !d->mJobQueue.isEmpty();
0554 }
0555 
0556 QSvgRenderer *Document::svgRenderer() const
0557 {
0558     return d->mImpl->svgRenderer();
0559 }
0560 
0561 void Document::setCmsProfile(const Cms::Profile::Ptr &ptr)
0562 {
0563     d->mCmsProfile = ptr;
0564 }
0565 
0566 Cms::Profile::Ptr Document::cmsProfile() const
0567 {
0568     return d->mCmsProfile;
0569 }
0570 
0571 } // namespace
0572 
0573 #include "moc_document.cpp"
0574 
0575 #include "moc_document_p.cpp"