1 /*
2 Gwenview: an image viewer
3 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
4 
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14 
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 
19 */
20 #include "document.h"
21 #include "document_p.h"
22 
23 // Qt
24 #include <QApplication>
25 #include <QImage>
26 #include <QUndoStack>
27 #include <QUrl>
28 
29 // KF
30 #include <KJobUiDelegate>
31 #include <KLocalizedString>
32 
33 // Exiv2
34 #include <exiv2/exiv2.hpp>
35 
36 // Local
37 #include "documentjob.h"
38 #include "emptydocumentimpl.h"
39 #include "gvdebug.h"
40 #include "gwenview_lib_debug.h"
41 #include "imagemetainfomodel.h"
42 #include "loadingdocumentimpl.h"
43 #include "loadingjob.h"
44 #include "savejob.h"
45 
46 namespace Gwenview
47 {
48 #undef ENABLE_LOG
49 #undef LOG
50 //#define ENABLE_LOG
51 #ifdef ENABLE_LOG
52 #define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
53 #else
54 #define LOG(x) ;
55 #endif
56 
57 #ifdef ENABLE_LOG
58 
logQueue(DocumentPrivate * d)59 static void logQueue(DocumentPrivate *d)
60 {
61 #define PREFIX "  QUEUE: "
62     if (!d->mCurrentJob) {
63         Q_ASSERT(d->mJobQueue.isEmpty());
64         qDebug(PREFIX "No current job, no pending jobs");
65         return;
66     }
67     qCDebug(GWENVIEW_LIB_LOG) << PREFIX "Current job:" << d->mCurrentJob.data();
68     if (d->mJobQueue.isEmpty()) {
69         qDebug(PREFIX "No pending jobs");
70         return;
71     }
72     qDebug(PREFIX "%d pending job(s):", d->mJobQueue.size());
73     for (DocumentJob *job : qAsConst(d->mJobQueue)) {
74         Q_ASSERT(job);
75         qCDebug(GWENVIEW_LIB_LOG) << PREFIX "-" << job;
76     }
77 #undef PREFIX
78 }
79 
80 #define LOG_QUEUE(msg, d)                                                                                                                                      \
81     LOG(msg);                                                                                                                                                  \
82     logQueue(d)
83 
84 #else
85 
86 #define LOG_QUEUE(msg, d)
87 
88 #endif
89 
90 //- DocumentPrivate ---------------------------------------
scheduleImageLoading(int invertedZoom)91 void DocumentPrivate::scheduleImageLoading(int invertedZoom)
92 {
93     auto *impl = qobject_cast<LoadingDocumentImpl *>(mImpl);
94     Q_ASSERT(impl);
95     impl->loadImage(invertedZoom);
96 }
97 
scheduleImageDownSampling(int invertedZoom)98 void DocumentPrivate::scheduleImageDownSampling(int invertedZoom)
99 {
100     LOG("invertedZoom=" << invertedZoom);
101     auto *job = qobject_cast<DownSamplingJob *>(mCurrentJob.data());
102     if (job && job->mInvertedZoom == invertedZoom) {
103         LOG("Current job is already doing it");
104         return;
105     }
106 
107     // Remove any previously scheduled downsampling job
108     DocumentJobQueue::Iterator it;
109     for (it = mJobQueue.begin(); it != mJobQueue.end(); ++it) {
110         auto *job = qobject_cast<DownSamplingJob *>(*it);
111         if (!job) {
112             continue;
113         }
114         if (job->mInvertedZoom == invertedZoom) {
115             // Already scheduled, nothing to do
116             LOG("Already scheduled");
117             return;
118         } else {
119             LOG("Removing downsampling job");
120             mJobQueue.erase(it);
121             delete job;
122         }
123     }
124     q->enqueueJob(new DownSamplingJob(invertedZoom));
125 }
126 
downSampleImage(int invertedZoom)127 void DocumentPrivate::downSampleImage(int invertedZoom)
128 {
129     mDownSampledImageMap[invertedZoom] = mImage.scaled(mImage.size() / invertedZoom, Qt::KeepAspectRatio, Qt::FastTransformation);
130     if (mDownSampledImageMap[invertedZoom].size().isEmpty()) {
131         mDownSampledImageMap[invertedZoom] = mImage;
132     }
133     Q_EMIT q->downSampledImageReady();
134 }
135 
136 //- DownSamplingJob ---------------------------------------
doStart()137 void DownSamplingJob::doStart()
138 {
139     DocumentPrivate *d = document()->d;
140     d->downSampleImage(mInvertedZoom);
141     setError(NoError);
142     emitResult();
143 }
144 
145 //- Document ----------------------------------------------
maxDownSampledZoom()146 qreal Document::maxDownSampledZoom()
147 {
148     return 0.5;
149 }
150 
Document(const QUrl & url)151 Document::Document(const QUrl &url)
152     : QObject()
153     , d(new DocumentPrivate)
154 {
155     d->q = this;
156     d->mImpl = nullptr;
157     d->mUrl = url;
158     d->mKeepRawData = false;
159 }
160 
~Document()161 Document::~Document()
162 {
163     // We do not want undo stack to emit signals, forcing us to emit signals
164     // ourself while we are being destroyed.
165     disconnect(&d->mUndoStack, nullptr, this, nullptr);
166 
167     delete d->mImpl;
168     delete d;
169 }
170 
reload()171 void Document::reload()
172 {
173     d->mSize = QSize();
174     d->mImage = QImage();
175     d->mDownSampledImageMap.clear();
176     d->mExiv2Image.reset();
177     d->mKind = MimeTypeUtils::KIND_UNKNOWN;
178     d->mFormat = QByteArray();
179     d->mImageMetaInfoModel.setUrl(d->mUrl);
180     d->mUndoStack.clear();
181     d->mErrorString.clear();
182     d->mCmsProfile = nullptr;
183 
184     switchToImpl(new LoadingDocumentImpl(this));
185 }
186 
image() const187 const QImage &Document::image() const
188 {
189     return d->mImage;
190 }
191 
192 /**
193  * invertedZoom is the biggest power of 2 for which zoom < 1/invertedZoom.
194  * Example:
195  * zoom = 0.4 == 1/2.5 => invertedZoom = 2 (1/2.5 < 1/2)
196  * zoom = 0.2 == 1/5   => invertedZoom = 4 (1/5   < 1/4)
197  */
invertedZoomForZoom(qreal zoom)198 inline int invertedZoomForZoom(qreal zoom)
199 {
200     int invertedZoom;
201     for (invertedZoom = 1; zoom < 1. / (invertedZoom * 4); invertedZoom *= 2) { }
202     return invertedZoom;
203 }
204 
downSampledImageForZoom(qreal zoom) const205 const QImage &Document::downSampledImageForZoom(qreal zoom) const
206 {
207     static const QImage sNullImage;
208 
209     int invertedZoom = invertedZoomForZoom(zoom);
210     if (invertedZoom == 1) {
211         return d->mImage;
212     }
213 
214     if (!d->mDownSampledImageMap.contains(invertedZoom)) {
215         if (!d->mImage.isNull()) {
216             // Special case: if we have the full image and the down sampled
217             // image would be too small, return the original image.
218             const QSize downSampledSize = d->mImage.size() / invertedZoom;
219             if (downSampledSize.isEmpty()) {
220                 return d->mImage;
221             }
222         }
223         return sNullImage;
224     }
225 
226     return d->mDownSampledImageMap[invertedZoom];
227 }
228 
loadingState() const229 Document::LoadingState Document::loadingState() const
230 {
231     return d->mImpl->loadingState();
232 }
233 
switchToImpl(AbstractDocumentImpl * impl)234 void Document::switchToImpl(AbstractDocumentImpl *impl)
235 {
236     Q_ASSERT(impl);
237     LOG("old impl:" << d->mImpl << "new impl:" << impl);
238     if (d->mImpl) {
239         d->mImpl->deleteLater();
240     }
241     d->mImpl = impl;
242 
243     connect(d->mImpl, &AbstractDocumentImpl::metaInfoLoaded, this, &Document::emitMetaInfoLoaded);
244     connect(d->mImpl, &AbstractDocumentImpl::loaded, this, &Document::emitLoaded);
245     connect(d->mImpl, &AbstractDocumentImpl::loadingFailed, this, &Document::emitLoadingFailed);
246     connect(d->mImpl, &AbstractDocumentImpl::imageRectUpdated, this, &Document::imageRectUpdated);
247     connect(d->mImpl, &AbstractDocumentImpl::isAnimatedUpdated, this, &Document::isAnimatedUpdated);
248     d->mImpl->init();
249 }
250 
setImageInternal(const QImage & image)251 void Document::setImageInternal(const QImage &image)
252 {
253     d->mImage = image;
254     d->mDownSampledImageMap.clear();
255 
256     // If we didn't get the image size before decoding the full image, set it
257     // now
258     setSize(d->mImage.size());
259 }
260 
url() const261 QUrl Document::url() const
262 {
263     return d->mUrl;
264 }
265 
rawData() const266 QByteArray Document::rawData() const
267 {
268     return d->mImpl->rawData();
269 }
270 
keepRawData() const271 bool Document::keepRawData() const
272 {
273     return d->mKeepRawData;
274 }
275 
setKeepRawData(bool value)276 void Document::setKeepRawData(bool value)
277 {
278     d->mKeepRawData = value;
279 }
280 
waitUntilLoaded()281 void Document::waitUntilLoaded()
282 {
283     startLoadingFullImage();
284     while (true) {
285         LoadingState state = loadingState();
286         if (state == Loaded || state == LoadingFailed) {
287             return;
288         }
289         qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
290     }
291 }
292 
save(const QUrl & url,const QByteArray & format)293 DocumentJob *Document::save(const QUrl &url, const QByteArray &format)
294 {
295     waitUntilLoaded();
296     DocumentJob *job = d->mImpl->save(url, format);
297     if (!job) {
298         qCWarning(GWENVIEW_LIB_LOG) << "Implementation does not support saving!";
299         setErrorString(i18nc("@info", "Gwenview cannot save this kind of documents."));
300         return nullptr;
301     }
302     job->setProperty("oldUrl", d->mUrl);
303     job->setProperty("newUrl", url);
304     connect(job, &DocumentJob::result, this, &Document::slotSaveResult);
305     enqueueJob(job);
306     return job;
307 }
308 
slotSaveResult(KJob * job)309 void Document::slotSaveResult(KJob *job)
310 {
311     if (job->error()) {
312         setErrorString(job->errorString());
313     } else {
314         d->mUndoStack.setClean();
315         auto *saveJob = static_cast<SaveJob *>(job);
316         d->mUrl = saveJob->newUrl();
317         d->mImageMetaInfoModel.setUrl(d->mUrl);
318         Q_EMIT saved(saveJob->oldUrl(), d->mUrl);
319     }
320 }
321 
format() const322 QByteArray Document::format() const
323 {
324     return d->mFormat;
325 }
326 
setFormat(const QByteArray & format)327 void Document::setFormat(const QByteArray &format)
328 {
329     d->mFormat = format;
330     Q_EMIT metaInfoUpdated();
331 }
332 
kind() const333 MimeTypeUtils::Kind Document::kind() const
334 {
335     return d->mKind;
336 }
337 
setKind(MimeTypeUtils::Kind kind)338 void Document::setKind(MimeTypeUtils::Kind kind)
339 {
340     d->mKind = kind;
341     Q_EMIT kindDetermined(d->mUrl);
342 }
343 
size() const344 QSize Document::size() const
345 {
346     return d->mSize;
347 }
348 
hasAlphaChannel() const349 bool Document::hasAlphaChannel() const
350 {
351     if (d->mImage.isNull()) {
352         return false;
353     } else {
354         return d->mImage.hasAlphaChannel();
355     }
356 }
357 
memoryUsage() const358 int Document::memoryUsage() const
359 {
360     // FIXME: Take undo stack into account
361     int usage = d->mImage.sizeInBytes();
362     usage += rawData().length();
363     return usage;
364 }
365 
setSize(const QSize & size)366 void Document::setSize(const QSize &size)
367 {
368     if (size == d->mSize) {
369         return;
370     }
371     d->mSize = size;
372     d->mImageMetaInfoModel.setImageSize(size);
373     Q_EMIT metaInfoUpdated();
374 }
375 
isModified() const376 bool Document::isModified() const
377 {
378     return !d->mUndoStack.isClean();
379 }
380 
editor()381 AbstractDocumentEditor *Document::editor()
382 {
383     return d->mImpl->editor();
384 }
385 
setExiv2Image(std::unique_ptr<Exiv2::Image> image)386 void Document::setExiv2Image(std::unique_ptr<Exiv2::Image> image)
387 {
388     d->mExiv2Image = std::move(image);
389     d->mImageMetaInfoModel.setExiv2Image(d->mExiv2Image.get());
390     Q_EMIT metaInfoUpdated();
391 }
392 
setDownSampledImage(const QImage & image,int invertedZoom)393 void Document::setDownSampledImage(const QImage &image, int invertedZoom)
394 {
395     Q_ASSERT(!d->mDownSampledImageMap.contains(invertedZoom));
396     d->mDownSampledImageMap[invertedZoom] = image;
397     Q_EMIT downSampledImageReady();
398 }
399 
errorString() const400 QString Document::errorString() const
401 {
402     return d->mErrorString;
403 }
404 
setErrorString(const QString & string)405 void Document::setErrorString(const QString &string)
406 {
407     d->mErrorString = string;
408 }
409 
metaInfo() const410 ImageMetaInfoModel *Document::metaInfo() const
411 {
412     return &d->mImageMetaInfoModel;
413 }
414 
startLoadingFullImage()415 void Document::startLoadingFullImage()
416 {
417     LoadingState state = loadingState();
418     if (state <= MetaInfoLoaded) {
419         // Schedule full image loading
420         auto *job = new LoadingJob;
421         job->uiDelegate()->setAutoWarningHandlingEnabled(false);
422         job->uiDelegate()->setAutoErrorHandlingEnabled(false);
423         enqueueJob(job);
424         d->scheduleImageLoading(1);
425     } else if (state == Loaded) {
426         return;
427     } else if (state == LoadingFailed) {
428         qCWarning(GWENVIEW_LIB_LOG) << "Can't load full image: loading has already failed";
429     }
430 }
431 
prepareDownSampledImageForZoom(qreal zoom)432 bool Document::prepareDownSampledImageForZoom(qreal zoom)
433 {
434     if (zoom >= maxDownSampledZoom()) {
435         qCWarning(GWENVIEW_LIB_LOG) << "No need to call prepareDownSampledImageForZoom if zoom >= " << maxDownSampledZoom();
436         return true;
437     }
438 
439     int invertedZoom = invertedZoomForZoom(zoom);
440     if (d->mDownSampledImageMap.contains(invertedZoom)) {
441         LOG("downSampledImageForZoom=" << zoom << "invertedZoom=" << invertedZoom << "ready");
442         return true;
443     }
444 
445     LOG("downSampledImageForZoom=" << zoom << "invertedZoom=" << invertedZoom << "not ready");
446     if (loadingState() == LoadingFailed) {
447         qCWarning(GWENVIEW_LIB_LOG) << "Image has failed to load, not doing anything";
448         return false;
449     } else if (loadingState() == Loaded) {
450         d->scheduleImageDownSampling(invertedZoom);
451         return false;
452     }
453 
454     // Schedule down sampled image loading
455     d->scheduleImageLoading(invertedZoom);
456 
457     return false;
458 }
459 
emitMetaInfoLoaded()460 void Document::emitMetaInfoLoaded()
461 {
462     Q_EMIT metaInfoLoaded(d->mUrl);
463 }
464 
emitLoaded()465 void Document::emitLoaded()
466 {
467     Q_EMIT loaded(d->mUrl);
468 }
469 
emitLoadingFailed()470 void Document::emitLoadingFailed()
471 {
472     Q_EMIT loadingFailed(d->mUrl);
473 }
474 
undoStack() const475 QUndoStack *Document::undoStack() const
476 {
477     return &d->mUndoStack;
478 }
479 
imageOperationCompleted()480 void Document::imageOperationCompleted()
481 {
482     if (d->mUndoStack.isClean()) {
483         // If user just undid all his changes this does not really correspond
484         // to a save, but it's similar enough as far as Document users are
485         // concerned
486         Q_EMIT saved(d->mUrl, d->mUrl);
487     } else {
488         Q_EMIT modified(d->mUrl);
489     }
490 }
491 
isEditable() const492 bool Document::isEditable() const
493 {
494     return d->mImpl->isEditable();
495 }
496 
isAnimated() const497 bool Document::isAnimated() const
498 {
499     return d->mImpl->isAnimated();
500 }
501 
startAnimation()502 void Document::startAnimation()
503 {
504     return d->mImpl->startAnimation();
505 }
506 
stopAnimation()507 void Document::stopAnimation()
508 {
509     return d->mImpl->stopAnimation();
510 }
511 
enqueueJob(DocumentJob * job)512 void Document::enqueueJob(DocumentJob *job)
513 {
514     LOG("job=" << job);
515     job->setDocument(Ptr(this));
516     connect(job, &LoadingJob::finished, this, &Document::slotJobFinished);
517     if (d->mCurrentJob) {
518         d->mJobQueue.enqueue(job);
519     } else {
520         d->mCurrentJob = job;
521         LOG("Starting first job");
522         job->start();
523         Q_EMIT busyChanged(d->mUrl, true);
524     }
525     LOG_QUEUE("Job added", d);
526 }
527 
slotJobFinished(KJob * job)528 void Document::slotJobFinished(KJob *job)
529 {
530     LOG("job=" << job);
531     GV_RETURN_IF_FAIL(job == d->mCurrentJob.data());
532 
533     if (d->mJobQueue.isEmpty()) {
534         LOG("All done");
535         d->mCurrentJob.clear();
536         Q_EMIT busyChanged(d->mUrl, false);
537         Q_EMIT allTasksDone();
538     } else {
539         LOG("Starting next job");
540         d->mCurrentJob = d->mJobQueue.dequeue();
541         GV_RETURN_IF_FAIL(d->mCurrentJob);
542         d->mCurrentJob.data()->start();
543     }
544     LOG_QUEUE("Removed done job", d);
545 }
546 
isBusy() const547 bool Document::isBusy() const
548 {
549     return !d->mJobQueue.isEmpty();
550 }
551 
svgRenderer() const552 QSvgRenderer *Document::svgRenderer() const
553 {
554     return d->mImpl->svgRenderer();
555 }
556 
setCmsProfile(const Cms::Profile::Ptr & ptr)557 void Document::setCmsProfile(const Cms::Profile::Ptr &ptr)
558 {
559     d->mCmsProfile = ptr;
560 }
561 
cmsProfile() const562 Cms::Profile::Ptr Document::cmsProfile() const
563 {
564     return d->mCmsProfile;
565 }
566 
567 } // namespace
568