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