576 lines
14 KiB
C++
576 lines
14 KiB
C++
|
/*
|
||
|
Gwenview: an image viewer
|
||
|
Copyright 2007 Aurélien Gâteau <agateau@kde.org>
|
||
|
|
||
|
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.
|
||
|
|
||
|
*/
|
||
|
#include "document.h"
|
||
|
#include "document_p.h"
|
||
|
|
||
|
// Qt
|
||
|
#include <QApplication>
|
||
|
#include <QImage>
|
||
|
#include <QUndoStack>
|
||
|
#include <QUrl>
|
||
|
|
||
|
// KF
|
||
|
#include <KJobUiDelegate>
|
||
|
#include <KLocalizedString>
|
||
|
|
||
|
// Exiv2
|
||
|
#include <exiv2/exiv2.hpp>
|
||
|
|
||
|
// Local
|
||
|
#include "documentjob.h"
|
||
|
#include "gvdebug.h"
|
||
|
#include "gwenview_lib_debug.h"
|
||
|
#include "imagemetainfomodel.h"
|
||
|
#include "loadingdocumentimpl.h"
|
||
|
#include "loadingjob.h"
|
||
|
#include "savejob.h"
|
||
|
|
||
|
namespace Gwenview
|
||
|
{
|
||
|
#undef ENABLE_LOG
|
||
|
#undef LOG
|
||
|
// #define ENABLE_LOG
|
||
|
#ifdef ENABLE_LOG
|
||
|
#define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
|
||
|
#else
|
||
|
#define LOG(x) ;
|
||
|
#endif
|
||
|
|
||
|
#ifdef ENABLE_LOG
|
||
|
|
||
|
static void logQueue(DocumentPrivate *d)
|
||
|
{
|
||
|
#define PREFIX " QUEUE: "
|
||
|
if (!d->mCurrentJob) {
|
||
|
Q_ASSERT(d->mJobQueue.isEmpty());
|
||
|
qDebug(PREFIX "No current job, no pending jobs");
|
||
|
return;
|
||
|
}
|
||
|
qCDebug(GWENVIEW_LIB_LOG) << PREFIX "Current job:" << d->mCurrentJob.data();
|
||
|
if (d->mJobQueue.isEmpty()) {
|
||
|
qDebug(PREFIX "No pending jobs");
|
||
|
return;
|
||
|
}
|
||
|
qDebug(PREFIX "%d pending job(s):", d->mJobQueue.size());
|
||
|
for (DocumentJob *job : qAsConst(d->mJobQueue)) {
|
||
|
Q_ASSERT(job);
|
||
|
qCDebug(GWENVIEW_LIB_LOG) << PREFIX "-" << job;
|
||
|
}
|
||
|
#undef PREFIX
|
||
|
}
|
||
|
|
||
|
#define LOG_QUEUE(msg, d) \
|
||
|
LOG(msg); \
|
||
|
logQueue(d)
|
||
|
|
||
|
#else
|
||
|
|
||
|
#define LOG_QUEUE(msg, d)
|
||
|
|
||
|
#endif
|
||
|
|
||
|
//- DocumentPrivate ---------------------------------------
|
||
|
void DocumentPrivate::scheduleImageLoading(int invertedZoom)
|
||
|
{
|
||
|
auto impl = qobject_cast<LoadingDocumentImpl *>(mImpl);
|
||
|
Q_ASSERT(impl);
|
||
|
impl->loadImage(invertedZoom);
|
||
|
}
|
||
|
|
||
|
void DocumentPrivate::scheduleImageDownSampling(int invertedZoom)
|
||
|
{
|
||
|
LOG("invertedZoom=" << invertedZoom);
|
||
|
auto job = qobject_cast<DownSamplingJob *>(mCurrentJob.data());
|
||
|
if (job && job->mInvertedZoom == invertedZoom) {
|
||
|
LOG("Current job is already doing it");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Remove any previously scheduled downsampling job
|
||
|
DocumentJobQueue::Iterator it;
|
||
|
for (it = mJobQueue.begin(); it != mJobQueue.end(); ++it) {
|
||
|
auto job = qobject_cast<DownSamplingJob *>(*it);
|
||
|
if (!job) {
|
||
|
continue;
|
||
|
}
|
||
|
if (job->mInvertedZoom == invertedZoom) {
|
||
|
// Already scheduled, nothing to do
|
||
|
LOG("Already scheduled");
|
||
|
return;
|
||
|
} else {
|
||
|
LOG("Removing downsampling job");
|
||
|
mJobQueue.erase(it);
|
||
|
delete job;
|
||
|
}
|
||
|
}
|
||
|
q->enqueueJob(new DownSamplingJob(invertedZoom));
|
||
|
}
|
||
|
|
||
|
void DocumentPrivate::downSampleImage(int invertedZoom)
|
||
|
{
|
||
|
mDownSampledImageMap[invertedZoom] = mImage.scaled(mImage.size() / invertedZoom, Qt::KeepAspectRatio, Qt::FastTransformation);
|
||
|
if (mDownSampledImageMap[invertedZoom].size().isEmpty()) {
|
||
|
mDownSampledImageMap[invertedZoom] = mImage;
|
||
|
}
|
||
|
Q_EMIT q->downSampledImageReady();
|
||
|
}
|
||
|
|
||
|
//- DownSamplingJob ---------------------------------------
|
||
|
void DownSamplingJob::doStart()
|
||
|
{
|
||
|
DocumentPrivate *d = document()->d;
|
||
|
d->downSampleImage(mInvertedZoom);
|
||
|
setError(NoError);
|
||
|
emitResult();
|
||
|
}
|
||
|
|
||
|
//- Document ----------------------------------------------
|
||
|
qreal Document::maxDownSampledZoom()
|
||
|
{
|
||
|
return 0.5;
|
||
|
}
|
||
|
|
||
|
Document::Document(const QUrl &url)
|
||
|
: QObject()
|
||
|
, d(new DocumentPrivate)
|
||
|
{
|
||
|
d->q = this;
|
||
|
d->mImpl = nullptr;
|
||
|
d->mUrl = url;
|
||
|
d->mKeepRawData = false;
|
||
|
}
|
||
|
|
||
|
Document::~Document()
|
||
|
{
|
||
|
// We do not want undo stack to emit signals, forcing us to emit signals
|
||
|
// ourself while we are being destroyed.
|
||
|
disconnect(&d->mUndoStack, nullptr, this, nullptr);
|
||
|
|
||
|
delete d->mImpl;
|
||
|
delete d;
|
||
|
}
|
||
|
|
||
|
void Document::reload()
|
||
|
{
|
||
|
d->mSize = QSize();
|
||
|
d->mImage = QImage();
|
||
|
d->mDownSampledImageMap.clear();
|
||
|
d->mExiv2Image.reset();
|
||
|
d->mKind = MimeTypeUtils::KIND_UNKNOWN;
|
||
|
d->mFormat = QByteArray();
|
||
|
d->mImageMetaInfoModel.setUrl(d->mUrl);
|
||
|
d->mImageMetaInfoModel.setDates(d->mUrl);
|
||
|
d->mImageMetaInfoModel.setMimeType(d->mUrl);
|
||
|
d->mImageMetaInfoModel.setFileSize(d->mUrl);
|
||
|
d->mUndoStack.clear();
|
||
|
d->mErrorString.clear();
|
||
|
d->mCmsProfile = nullptr;
|
||
|
|
||
|
switchToImpl(new LoadingDocumentImpl(this));
|
||
|
}
|
||
|
|
||
|
const QImage &Document::image() const
|
||
|
{
|
||
|
return d->mImage;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* invertedZoom is the biggest power of 2 for which zoom < 1/invertedZoom.
|
||
|
* Example:
|
||
|
* zoom = 0.4 == 1/2.5 => invertedZoom = 2 (1/2.5 < 1/2)
|
||
|
* zoom = 0.2 == 1/5 => invertedZoom = 4 (1/5 < 1/4)
|
||
|
*/
|
||
|
inline int invertedZoomForZoom(qreal zoom)
|
||
|
{
|
||
|
int invertedZoom;
|
||
|
for (invertedZoom = 1; zoom < 1. / (invertedZoom * 4); invertedZoom *= 2) { }
|
||
|
return invertedZoom;
|
||
|
}
|
||
|
|
||
|
const QImage &Document::downSampledImageForZoom(qreal zoom) const
|
||
|
{
|
||
|
static const QImage sNullImage;
|
||
|
|
||
|
int invertedZoom = invertedZoomForZoom(zoom);
|
||
|
if (invertedZoom == 1) {
|
||
|
return d->mImage;
|
||
|
}
|
||
|
|
||
|
if (!d->mDownSampledImageMap.contains(invertedZoom)) {
|
||
|
if (!d->mImage.isNull()) {
|
||
|
// Special case: if we have the full image and the down sampled
|
||
|
// image would be too small, return the original image.
|
||
|
const QSize downSampledSize = d->mImage.size() / invertedZoom;
|
||
|
if (downSampledSize.isEmpty()) {
|
||
|
return d->mImage;
|
||
|
}
|
||
|
}
|
||
|
return sNullImage;
|
||
|
}
|
||
|
|
||
|
return d->mDownSampledImageMap[invertedZoom];
|
||
|
}
|
||
|
|
||
|
Document::LoadingState Document::loadingState() const
|
||
|
{
|
||
|
return d->mImpl->loadingState();
|
||
|
}
|
||
|
|
||
|
void Document::switchToImpl(AbstractDocumentImpl *impl)
|
||
|
{
|
||
|
Q_ASSERT(impl);
|
||
|
LOG("old impl:" << d->mImpl << "new impl:" << impl);
|
||
|
if (d->mImpl) {
|
||
|
d->mImpl->deleteLater();
|
||
|
}
|
||
|
d->mImpl = impl;
|
||
|
|
||
|
connect(d->mImpl, &AbstractDocumentImpl::metaInfoLoaded, this, &Document::emitMetaInfoLoaded);
|
||
|
connect(d->mImpl, &AbstractDocumentImpl::loaded, this, &Document::emitLoaded);
|
||
|
connect(d->mImpl, &AbstractDocumentImpl::loadingFailed, this, &Document::emitLoadingFailed);
|
||
|
connect(d->mImpl, &AbstractDocumentImpl::imageRectUpdated, this, &Document::imageRectUpdated);
|
||
|
connect(d->mImpl, &AbstractDocumentImpl::isAnimatedUpdated, this, &Document::isAnimatedUpdated);
|
||
|
d->mImpl->init();
|
||
|
}
|
||
|
|
||
|
void Document::setImageInternal(const QImage &image)
|
||
|
{
|
||
|
d->mImage = image;
|
||
|
d->mDownSampledImageMap.clear();
|
||
|
|
||
|
// If we didn't get the image size before decoding the full image, set it
|
||
|
// now
|
||
|
setSize(d->mImage.size());
|
||
|
}
|
||
|
|
||
|
QUrl Document::url() const
|
||
|
{
|
||
|
return d->mUrl;
|
||
|
}
|
||
|
|
||
|
QByteArray Document::rawData() const
|
||
|
{
|
||
|
return d->mImpl->rawData();
|
||
|
}
|
||
|
|
||
|
bool Document::keepRawData() const
|
||
|
{
|
||
|
return d->mKeepRawData;
|
||
|
}
|
||
|
|
||
|
void Document::setKeepRawData(bool value)
|
||
|
{
|
||
|
d->mKeepRawData = value;
|
||
|
}
|
||
|
|
||
|
void Document::waitUntilLoaded()
|
||
|
{
|
||
|
startLoadingFullImage();
|
||
|
while (true) {
|
||
|
LoadingState state = loadingState();
|
||
|
if (state == Loaded || state == LoadingFailed) {
|
||
|
return;
|
||
|
}
|
||
|
qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
DocumentJob *Document::save(const QUrl &url, const QByteArray &format)
|
||
|
{
|
||
|
waitUntilLoaded();
|
||
|
DocumentJob *job = d->mImpl->save(url, format);
|
||
|
if (!job) {
|
||
|
qCWarning(GWENVIEW_LIB_LOG) << "Implementation does not support saving!";
|
||
|
setErrorString(i18nc("@info", "Gwenview cannot save this kind of documents."));
|
||
|
return nullptr;
|
||
|
}
|
||
|
job->setProperty("oldUrl", d->mUrl);
|
||
|
job->setProperty("newUrl", url);
|
||
|
connect(job, &DocumentJob::result, this, &Document::slotSaveResult);
|
||
|
enqueueJob(job);
|
||
|
return job;
|
||
|
}
|
||
|
|
||
|
void Document::slotSaveResult(KJob *job)
|
||
|
{
|
||
|
if (job->error()) {
|
||
|
setErrorString(job->errorString());
|
||
|
} else {
|
||
|
d->mUndoStack.setClean();
|
||
|
auto saveJob = static_cast<SaveJob *>(job);
|
||
|
d->mUrl = saveJob->newUrl();
|
||
|
d->mImageMetaInfoModel.setUrl(d->mUrl);
|
||
|
d->mImageMetaInfoModel.setDates(d->mUrl);
|
||
|
d->mImageMetaInfoModel.setFileSize(d->mUrl);
|
||
|
Q_EMIT saved(saveJob->oldUrl(), d->mUrl);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
QByteArray Document::format() const
|
||
|
{
|
||
|
return d->mFormat;
|
||
|
}
|
||
|
|
||
|
void Document::setFormat(const QByteArray &format)
|
||
|
{
|
||
|
d->mFormat = format;
|
||
|
Q_EMIT metaInfoUpdated();
|
||
|
}
|
||
|
|
||
|
MimeTypeUtils::Kind Document::kind() const
|
||
|
{
|
||
|
return d->mKind;
|
||
|
}
|
||
|
|
||
|
void Document::setKind(MimeTypeUtils::Kind kind)
|
||
|
{
|
||
|
d->mKind = kind;
|
||
|
Q_EMIT kindDetermined(d->mUrl);
|
||
|
}
|
||
|
|
||
|
QSize Document::size() const
|
||
|
{
|
||
|
return d->mSize;
|
||
|
}
|
||
|
|
||
|
bool Document::hasAlphaChannel() const
|
||
|
{
|
||
|
if (d->mImage.isNull()) {
|
||
|
return false;
|
||
|
} else {
|
||
|
return d->mImage.hasAlphaChannel();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
int Document::memoryUsage() const
|
||
|
{
|
||
|
// FIXME: Take undo stack into account
|
||
|
int usage = d->mImage.sizeInBytes();
|
||
|
usage += rawData().length();
|
||
|
return usage;
|
||
|
}
|
||
|
|
||
|
void Document::setSize(const QSize &size)
|
||
|
{
|
||
|
if (size == d->mSize) {
|
||
|
return;
|
||
|
}
|
||
|
d->mSize = size;
|
||
|
d->mImageMetaInfoModel.setImageSize(size);
|
||
|
Q_EMIT metaInfoUpdated();
|
||
|
}
|
||
|
|
||
|
bool Document::isModified() const
|
||
|
{
|
||
|
return !d->mUndoStack.isClean();
|
||
|
}
|
||
|
|
||
|
AbstractDocumentEditor *Document::editor()
|
||
|
{
|
||
|
return d->mImpl->editor();
|
||
|
}
|
||
|
|
||
|
void Document::setExiv2Image(std::unique_ptr<Exiv2::Image> image)
|
||
|
{
|
||
|
d->mExiv2Image = std::move(image);
|
||
|
d->mImageMetaInfoModel.setExiv2Image(d->mExiv2Image.get());
|
||
|
Q_EMIT metaInfoUpdated();
|
||
|
}
|
||
|
|
||
|
void Document::setDownSampledImage(const QImage &image, int invertedZoom)
|
||
|
{
|
||
|
Q_ASSERT(!d->mDownSampledImageMap.contains(invertedZoom));
|
||
|
d->mDownSampledImageMap[invertedZoom] = image;
|
||
|
Q_EMIT downSampledImageReady();
|
||
|
}
|
||
|
|
||
|
QString Document::errorString() const
|
||
|
{
|
||
|
return d->mErrorString;
|
||
|
}
|
||
|
|
||
|
void Document::setErrorString(const QString &string)
|
||
|
{
|
||
|
d->mErrorString = string;
|
||
|
}
|
||
|
|
||
|
ImageMetaInfoModel *Document::metaInfo() const
|
||
|
{
|
||
|
return &d->mImageMetaInfoModel;
|
||
|
}
|
||
|
|
||
|
void Document::startLoadingFullImage()
|
||
|
{
|
||
|
LoadingState state = loadingState();
|
||
|
if (state <= MetaInfoLoaded) {
|
||
|
// Schedule full image loading
|
||
|
auto job = new LoadingJob;
|
||
|
job->uiDelegate()->setAutoWarningHandlingEnabled(false);
|
||
|
job->uiDelegate()->setAutoErrorHandlingEnabled(false);
|
||
|
enqueueJob(job);
|
||
|
d->scheduleImageLoading(1);
|
||
|
} else if (state == Loaded) {
|
||
|
return;
|
||
|
} else if (state == LoadingFailed) {
|
||
|
qCWarning(GWENVIEW_LIB_LOG) << "Can't load full image: loading has already failed";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool Document::prepareDownSampledImageForZoom(qreal zoom)
|
||
|
{
|
||
|
if (zoom >= maxDownSampledZoom()) {
|
||
|
qCWarning(GWENVIEW_LIB_LOG) << "No need to call prepareDownSampledImageForZoom if zoom >= " << maxDownSampledZoom();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int invertedZoom = invertedZoomForZoom(zoom);
|
||
|
if (d->mDownSampledImageMap.contains(invertedZoom)) {
|
||
|
LOG("downSampledImageForZoom=" << zoom << "invertedZoom=" << invertedZoom << "ready");
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
LOG("downSampledImageForZoom=" << zoom << "invertedZoom=" << invertedZoom << "not ready");
|
||
|
if (loadingState() == LoadingFailed) {
|
||
|
qCWarning(GWENVIEW_LIB_LOG) << "Image has failed to load, not doing anything";
|
||
|
return false;
|
||
|
} else if (loadingState() == Loaded) {
|
||
|
d->scheduleImageDownSampling(invertedZoom);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Schedule down sampled image loading
|
||
|
d->scheduleImageLoading(invertedZoom);
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
void Document::emitMetaInfoLoaded()
|
||
|
{
|
||
|
Q_EMIT metaInfoLoaded(d->mUrl);
|
||
|
}
|
||
|
|
||
|
void Document::emitLoaded()
|
||
|
{
|
||
|
Q_EMIT loaded(d->mUrl);
|
||
|
}
|
||
|
|
||
|
void Document::emitLoadingFailed()
|
||
|
{
|
||
|
Q_EMIT loadingFailed(d->mUrl);
|
||
|
}
|
||
|
|
||
|
QUndoStack *Document::undoStack() const
|
||
|
{
|
||
|
return &d->mUndoStack;
|
||
|
}
|
||
|
|
||
|
void Document::imageOperationCompleted()
|
||
|
{
|
||
|
if (d->mUndoStack.isClean()) {
|
||
|
// If user just undid all his changes this does not really correspond
|
||
|
// to a save, but it's similar enough as far as Document users are
|
||
|
// concerned
|
||
|
Q_EMIT saved(d->mUrl, d->mUrl);
|
||
|
} else {
|
||
|
Q_EMIT modified(d->mUrl);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool Document::isEditable() const
|
||
|
{
|
||
|
return d->mImpl->isEditable();
|
||
|
}
|
||
|
|
||
|
bool Document::isAnimated() const
|
||
|
{
|
||
|
return d->mImpl->isAnimated();
|
||
|
}
|
||
|
|
||
|
void Document::startAnimation()
|
||
|
{
|
||
|
return d->mImpl->startAnimation();
|
||
|
}
|
||
|
|
||
|
void Document::stopAnimation()
|
||
|
{
|
||
|
return d->mImpl->stopAnimation();
|
||
|
}
|
||
|
|
||
|
void Document::enqueueJob(DocumentJob *job)
|
||
|
{
|
||
|
LOG("job=" << job);
|
||
|
job->setDocument(Ptr(this));
|
||
|
connect(job, &LoadingJob::finished, this, &Document::slotJobFinished);
|
||
|
if (d->mCurrentJob) {
|
||
|
d->mJobQueue.enqueue(job);
|
||
|
} else {
|
||
|
d->mCurrentJob = job;
|
||
|
LOG("Starting first job");
|
||
|
job->start();
|
||
|
Q_EMIT busyChanged(d->mUrl, true);
|
||
|
}
|
||
|
LOG_QUEUE("Job added", d);
|
||
|
}
|
||
|
|
||
|
void Document::slotJobFinished(KJob *job)
|
||
|
{
|
||
|
LOG("job=" << job);
|
||
|
GV_RETURN_IF_FAIL(job == d->mCurrentJob.data());
|
||
|
|
||
|
if (d->mJobQueue.isEmpty()) {
|
||
|
LOG("All done");
|
||
|
d->mCurrentJob.clear();
|
||
|
Q_EMIT busyChanged(d->mUrl, false);
|
||
|
Q_EMIT allTasksDone();
|
||
|
} else {
|
||
|
LOG("Starting next job");
|
||
|
d->mCurrentJob = d->mJobQueue.dequeue();
|
||
|
GV_RETURN_IF_FAIL(d->mCurrentJob);
|
||
|
d->mCurrentJob.data()->start();
|
||
|
}
|
||
|
LOG_QUEUE("Removed done job", d);
|
||
|
}
|
||
|
|
||
|
bool Document::isBusy() const
|
||
|
{
|
||
|
return !d->mJobQueue.isEmpty();
|
||
|
}
|
||
|
|
||
|
QSvgRenderer *Document::svgRenderer() const
|
||
|
{
|
||
|
return d->mImpl->svgRenderer();
|
||
|
}
|
||
|
|
||
|
void Document::setCmsProfile(const Cms::Profile::Ptr &ptr)
|
||
|
{
|
||
|
d->mCmsProfile = ptr;
|
||
|
}
|
||
|
|
||
|
Cms::Profile::Ptr Document::cmsProfile() const
|
||
|
{
|
||
|
return d->mCmsProfile;
|
||
|
}
|
||
|
|
||
|
} // namespace
|
||
|
|
||
|
#include "moc_document.cpp"
|
||
|
|
||
|
#include "moc_document_p.cpp"
|