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"