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

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003 Gwenview: an image viewer
0004 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
0005 
0006 This program is free software; you can redistribute it and/or
0007 modify it under the terms of the GNU General Public License
0008 as published by the Free Software Foundation; either version 2
0009 of the License, or (at your option) any later version.
0010 
0011 This program is distributed in the hope that it will be useful,
0012 but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 GNU General Public License for more details.
0015 
0016 You should have received a copy of the GNU General Public License
0017 along with this program; if not, write to the Free Software
0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
0019 
0020 */
0021 // Self
0022 #include "loadingdocumentimpl.h"
0023 
0024 // STL
0025 #include <memory>
0026 
0027 // Exiv2
0028 #include <exiv2/exiv2.hpp>
0029 
0030 // Qt
0031 #include <QBuffer>
0032 #include <QByteArray>
0033 #include <QColorSpace>
0034 #include <QFile>
0035 #include <QFuture>
0036 #include <QFutureWatcher>
0037 #include <QImage>
0038 #include <QImageReader>
0039 #include <QPointer>
0040 #include <QUrl>
0041 #include <QtConcurrent>
0042 
0043 // KF
0044 #include <KIO/TransferJob>
0045 #include <KLocalizedString>
0046 #include <KProtocolInfo>
0047 
0048 #ifdef KDCRAW_FOUND
0049 #include <kdcraw/kdcraw.h>
0050 #endif
0051 
0052 // Local
0053 #include "animateddocumentloadedimpl.h"
0054 #include "cms/cmsprofile.h"
0055 #include "document.h"
0056 #include "documentloadedimpl.h"
0057 #include "emptydocumentimpl.h"
0058 #include "exiv2imageloader.h"
0059 #include "gvdebug.h"
0060 #include "gwenview_lib_debug.h"
0061 #include "gwenviewconfig.h"
0062 #include "jpegcontent.h"
0063 #include "jpegdocumentloadedimpl.h"
0064 #include "svgdocumentloadedimpl.h"
0065 #include "urlutils.h"
0066 #include "videodocumentloadedimpl.h"
0067 
0068 namespace Gwenview
0069 {
0070 #undef ENABLE_LOG
0071 #undef LOG
0072 // #define ENABLE_LOG
0073 #ifdef ENABLE_LOG
0074 #define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
0075 #else
0076 #define LOG(x) ;
0077 #endif
0078 
0079 const int HEADER_SIZE = 256;
0080 
0081 struct LoadingDocumentImplPrivate {
0082     LoadingDocumentImpl *q;
0083     QPointer<KIO::TransferJob> mTransferJob;
0084     QFuture<bool> mMetaInfoFuture;
0085     QFutureWatcher<bool> mMetaInfoFutureWatcher;
0086     QFuture<void> mImageDataFuture;
0087     QFutureWatcher<void> mImageDataFutureWatcher;
0088 
0089     // If != 0, this means we need to load an image at zoom =
0090     // 1/mImageDataInvertedZoom
0091     int mImageDataInvertedZoom;
0092 
0093     bool mMetaInfoLoaded;
0094     bool mAnimated;
0095     bool mDownSampledImageLoaded;
0096     QByteArray mFormatHint;
0097     QByteArray mData;
0098     QByteArray mFormat;
0099     QSize mImageSize;
0100     std::unique_ptr<Exiv2::Image> mExiv2Image;
0101     std::unique_ptr<JpegContent> mJpegContent;
0102     QImage mImage;
0103     Cms::Profile::Ptr mCmsProfile;
0104     QMimeType mMimeType;
0105 
0106     /**
0107      * Determine kind of document and switch to an implementation if it is not
0108      * necessary to download more data.
0109      * @return true if switched to another implementation.
0110      */
0111     bool determineKind()
0112     {
0113         const QUrl &url = q->document()->url();
0114         QMimeDatabase db;
0115         if (KProtocolInfo::determineMimetypeFromExtension(url.scheme())) {
0116             mMimeType = db.mimeTypeForFileNameAndData(url.fileName(), mData);
0117         } else {
0118             mMimeType = db.mimeTypeForData(mData);
0119         }
0120 
0121         MimeTypeUtils::Kind kind = MimeTypeUtils::mimeTypeKind(mMimeType.name());
0122         LOG("mimeType:" << mMimeType.name());
0123         LOG("kind:" << kind);
0124         q->setDocumentKind(kind);
0125 
0126         switch (kind) {
0127         case MimeTypeUtils::KIND_RASTER_IMAGE:
0128         case MimeTypeUtils::KIND_SVG_IMAGE:
0129             return false;
0130 
0131         case MimeTypeUtils::KIND_VIDEO:
0132             q->switchToImpl(new VideoDocumentLoadedImpl(q->document()));
0133             return true;
0134 
0135         default:
0136             q->setDocumentErrorString(i18nc("@info", "Gwenview cannot display documents of type %1.", mMimeType.name()));
0137             Q_EMIT q->loadingFailed();
0138             q->switchToImpl(new EmptyDocumentImpl(q->document()));
0139             return true;
0140         }
0141     }
0142 
0143     void startLoading()
0144     {
0145         Q_ASSERT(!mMetaInfoLoaded);
0146 
0147         switch (q->document()->kind()) {
0148         case MimeTypeUtils::KIND_RASTER_IMAGE:
0149             // The hint is used to:
0150             // - Speed up loadMetaInfo(): QImageReader will try to decode the
0151             //   image using plugins matching this format first.
0152             // - Avoid breakage: Because of a bug in Qt TGA image plugin, some
0153             //   PNG were incorrectly identified as PCX! See:
0154             //   https://bugs.kde.org/show_bug.cgi?id=289819
0155             //
0156             mFormatHint = mMimeType.preferredSuffix().toLocal8Bit().toLower();
0157             mMetaInfoFuture = QtConcurrent::run(&LoadingDocumentImplPrivate::loadMetaInfo, this);
0158             mMetaInfoFutureWatcher.setFuture(mMetaInfoFuture);
0159             break;
0160 
0161         case MimeTypeUtils::KIND_SVG_IMAGE:
0162             q->switchToImpl(new SvgDocumentLoadedImpl(q->document(), mData));
0163             break;
0164 
0165         case MimeTypeUtils::KIND_VIDEO:
0166             break;
0167 
0168         default:
0169             qCWarning(GWENVIEW_LIB_LOG) << "We should not reach this point!";
0170             break;
0171         }
0172     }
0173 
0174     void startImageDataLoading()
0175     {
0176         LOG("");
0177         Q_ASSERT(mMetaInfoLoaded);
0178         Q_ASSERT(mImageDataInvertedZoom != 0);
0179         Q_ASSERT(!mImageDataFuture.isRunning());
0180         mImageDataFuture = QtConcurrent::run(&LoadingDocumentImplPrivate::loadImageData, this);
0181         mImageDataFutureWatcher.setFuture(mImageDataFuture);
0182     }
0183 
0184     bool loadMetaInfo()
0185     {
0186         LOG("mFormatHint" << mFormatHint);
0187         QBuffer buffer;
0188         buffer.setBuffer(&mData);
0189         buffer.open(QIODevice::ReadOnly);
0190 
0191         Exiv2ImageLoader loader;
0192         if (loader.load(mData)) {
0193             mExiv2Image = loader.popImage();
0194         }
0195 
0196         QImageReader reader;
0197 
0198 #ifdef KDCRAW_FOUND
0199         if (!QImageReader::supportedImageFormats().contains(QByteArray("raw"))
0200             && KDcrawIface::KDcraw::rawFilesList().contains(QString::fromLatin1(mFormatHint))) {
0201             QByteArray previewData;
0202 
0203             // if the image is in format supported by dcraw, fetch its embedded preview
0204             mJpegContent = std::make_unique<JpegContent>();
0205 
0206             // use KDcraw for getting the embedded preview
0207             // KDcraw functionality cloned locally (temp. solution)
0208             bool ret = KDcrawIface::KDcraw::loadEmbeddedPreview(previewData, buffer);
0209 
0210             if (!ret) {
0211                 // if the embedded preview loading failed, load half preview instead.
0212                 // That's slower but it works even for images containing
0213                 // small (160x120px) or none embedded preview.
0214                 if (!KDcrawIface::KDcraw::loadHalfPreview(previewData, buffer)) {
0215                     qCWarning(GWENVIEW_LIB_LOG) << "unable to get half preview for " << q->document()->url().fileName();
0216                     return false;
0217                 }
0218             }
0219 
0220             buffer.close();
0221 
0222             // now it's safe to replace mData with the jpeg data
0223             mData = previewData;
0224 
0225             // need to fill mFormat so gwenview can tell the type when trying to save
0226             mFormat = mFormatHint;
0227         } else {
0228 #else
0229         {
0230 #endif
0231             reader.setFormat(mFormatHint);
0232             reader.setDevice(&buffer);
0233             mImageSize = reader.size();
0234 
0235             if (!reader.canRead()) {
0236                 qCWarning(GWENVIEW_LIB_LOG) << "QImageReader::read() using format hint" << mFormatHint << "failed:" << reader.errorString();
0237                 if (buffer.pos() != 0) {
0238                     qCWarning(GWENVIEW_LIB_LOG) << "A bad Qt image decoder moved the buffer to" << buffer.pos() << "in a call to canRead()! Rewinding.";
0239                     buffer.seek(0);
0240                 }
0241                 reader.setFormat(QByteArray());
0242                 // Set buffer again, otherwise QImageReader won't restart from scratch
0243                 reader.setDevice(&buffer);
0244                 if (!reader.canRead()) {
0245                     qCWarning(GWENVIEW_LIB_LOG) << "QImageReader::read() without format hint failed:" << reader.errorString();
0246                     return false;
0247                 }
0248                 qCWarning(GWENVIEW_LIB_LOG) << "Image format is actually" << reader.format() << "not" << mFormatHint;
0249             }
0250 
0251             mFormat = reader.format();
0252 
0253             if (mFormat == "jpg") {
0254                 // if mFormatHint was "jpg", then mFormat is "jpg", but the rest of
0255                 // Gwenview code assumes JPEG images have "jpeg" format.
0256                 mFormat = "jpeg";
0257             }
0258         }
0259 
0260         LOG("mFormat" << mFormat);
0261         GV_RETURN_VALUE_IF_FAIL(!mFormat.isEmpty(), false);
0262 
0263         if (mFormat == "jpeg" && mExiv2Image.get()) {
0264             mJpegContent = std::make_unique<JpegContent>();
0265         }
0266 
0267         if (mJpegContent.get()) {
0268             if (!mJpegContent->loadFromData(mData, mExiv2Image.get()) && !mJpegContent->loadFromData(mData)) {
0269                 qCWarning(GWENVIEW_LIB_LOG) << "Unable to use preview of " << q->document()->url().fileName();
0270                 return false;
0271             }
0272             // Use the size from JpegContent, as its correctly transposed if the
0273             // image has been rotated
0274             mImageSize = mJpegContent->size();
0275 
0276             mCmsProfile = Cms::Profile::loadFromExiv2Image(mExiv2Image.get());
0277         }
0278 
0279         LOG("mImageSize" << mImageSize);
0280 
0281         if (!mCmsProfile) {
0282             mCmsProfile = Cms::Profile::loadFromImageData(mData, mFormat);
0283         }
0284 
0285         if (!mCmsProfile && reader.canRead()) {
0286             const QImage qtimage = reader.read();
0287             if (!qtimage.isNull()) {
0288                 mCmsProfile = Cms::Profile::loadFromICC(qtimage.colorSpace().iccProfile());
0289             }
0290         }
0291 
0292         return true;
0293     }
0294 
0295     void loadImageData()
0296     {
0297         QBuffer buffer;
0298         buffer.setBuffer(&mData);
0299         buffer.open(QIODevice::ReadOnly);
0300         QImageReader reader(&buffer, mFormat);
0301 
0302         LOG("mImageDataInvertedZoom=" << mImageDataInvertedZoom);
0303         if (mImageSize.isValid() && mImageDataInvertedZoom != 1 && reader.supportsOption(QImageIOHandler::ScaledSize)) {
0304             // Do not use mImageSize here: QImageReader needs a non-transposed
0305             // image size
0306             QSize size = reader.size() / mImageDataInvertedZoom;
0307             if (!size.isEmpty()) {
0308                 LOG("Setting scaled size to" << size);
0309                 reader.setScaledSize(size);
0310             } else {
0311                 LOG("Not setting scaled size as it is empty" << size);
0312             }
0313         }
0314 
0315         if (GwenviewConfig::applyExifOrientation()) {
0316             reader.setAutoTransform(true);
0317         }
0318 
0319         bool ok = reader.read(&mImage);
0320         if (!ok) {
0321             LOG("QImageReader::read() failed");
0322             return;
0323         }
0324 
0325         if (reader.supportsAnimation() && reader.nextImageDelay() > 0 // Assume delay == 0 <=> only one frame
0326         ) {
0327             /*
0328              * QImageReader is not really helpful to detect animated gif:
0329              * - QImageReader::imageCount() returns 0
0330              * - QImageReader::nextImageDelay() may return something > 0 if the
0331              *   image consists of only one frame but includes a "Graphic
0332              *   Control Extension" (usually only present if we have an
0333              *   animation) (Bug #185523)
0334              *
0335              * Decoding the next frame is the only reliable way I found to
0336              * detect an animated gif
0337              */
0338             LOG("May be an animated image. delay:" << reader.nextImageDelay());
0339             QImage nextImage;
0340             if (reader.read(&nextImage)) {
0341                 LOG("Really an animated image (more than one frame)");
0342                 mAnimated = true;
0343             } else {
0344                 qCWarning(GWENVIEW_LIB_LOG) << q->document()->url() << "is not really an animated image (only one frame)";
0345             }
0346         }
0347     }
0348 };
0349 
0350 LoadingDocumentImpl::LoadingDocumentImpl(Document *document)
0351     : AbstractDocumentImpl(document)
0352     , d(new LoadingDocumentImplPrivate)
0353 {
0354     d->q = this;
0355     d->mMetaInfoLoaded = false;
0356     d->mAnimated = false;
0357     d->mDownSampledImageLoaded = false;
0358     d->mImageDataInvertedZoom = 0;
0359 
0360     connect(&d->mMetaInfoFutureWatcher, &QFutureWatcherBase::finished, this, &LoadingDocumentImpl::slotMetaInfoLoaded);
0361 
0362     connect(&d->mImageDataFutureWatcher, &QFutureWatcherBase::finished, this, &LoadingDocumentImpl::slotImageLoaded);
0363 }
0364 
0365 LoadingDocumentImpl::~LoadingDocumentImpl()
0366 {
0367     LOG("");
0368     // Disconnect watchers to make sure they do not trigger further work
0369     d->mMetaInfoFutureWatcher.disconnect();
0370     d->mImageDataFutureWatcher.disconnect();
0371 
0372     d->mMetaInfoFutureWatcher.waitForFinished();
0373     d->mImageDataFutureWatcher.waitForFinished();
0374 
0375     if (d->mTransferJob) {
0376         d->mTransferJob->kill();
0377     }
0378     delete d;
0379 }
0380 
0381 void LoadingDocumentImpl::init()
0382 {
0383     QUrl url = document()->url();
0384 
0385     if (UrlUtils::urlIsFastLocalFile(url)) {
0386         // Load file content directly
0387         QFile file(url.toLocalFile());
0388         if (!file.open(QIODevice::ReadOnly)) {
0389             setDocumentErrorString(i18nc("@info", "Could not open file %1", url.toLocalFile()));
0390             Q_EMIT loadingFailed();
0391             switchToImpl(new EmptyDocumentImpl(document()));
0392             return;
0393         }
0394         d->mData = file.read(HEADER_SIZE);
0395         if (d->determineKind()) {
0396             return;
0397         }
0398         d->mData += file.readAll();
0399         d->startLoading();
0400     } else {
0401         // Transfer file via KIO
0402         d->mTransferJob = KIO::get(document()->url(), KIO::NoReload, KIO::HideProgressInfo);
0403         connect(d->mTransferJob.data(), &KIO::TransferJob::data, this, &LoadingDocumentImpl::slotDataReceived);
0404         connect(d->mTransferJob.data(), &KJob::result, this, &LoadingDocumentImpl::slotTransferFinished);
0405         d->mTransferJob->start();
0406     }
0407 }
0408 
0409 void LoadingDocumentImpl::loadImage(int invertedZoom)
0410 {
0411     if (d->mImageDataInvertedZoom == invertedZoom) {
0412         LOG("Already loading an image at invertedZoom=" << invertedZoom);
0413         return;
0414     }
0415     if (d->mImageDataInvertedZoom == 1) {
0416         LOG("Ignoring request: we are loading a full image");
0417         return;
0418     }
0419     d->mImageDataFutureWatcher.waitForFinished();
0420     d->mImageDataInvertedZoom = invertedZoom;
0421 
0422     if (d->mMetaInfoLoaded) {
0423         // Do not test on mMetaInfoFuture.isRunning() here: it might not have
0424         // started if we are downloading the image from a remote url
0425         d->startImageDataLoading();
0426     }
0427 }
0428 
0429 void LoadingDocumentImpl::slotDataReceived(KIO::Job *job, const QByteArray &chunk)
0430 {
0431     d->mData.append(chunk);
0432     if (document()->kind() == MimeTypeUtils::KIND_UNKNOWN && d->mData.length() >= HEADER_SIZE) {
0433         if (d->determineKind()) {
0434             job->kill();
0435             return;
0436         }
0437     }
0438 }
0439 
0440 void LoadingDocumentImpl::slotTransferFinished(KJob *job)
0441 {
0442     if (job->error()) {
0443         setDocumentErrorString(job->errorString());
0444         Q_EMIT loadingFailed();
0445         switchToImpl(new EmptyDocumentImpl(document()));
0446         return;
0447     } else if (document()->kind() == MimeTypeUtils::KIND_UNKNOWN) {
0448         // Transfer finished. If the mime type is still unknown (e.g. for files < HEADER_SIZE)
0449         // determine the kind again.
0450         if (d->determineKind()) {
0451             return;
0452         }
0453     }
0454     d->startLoading();
0455 }
0456 
0457 bool LoadingDocumentImpl::isEditable() const
0458 {
0459     return d->mDownSampledImageLoaded;
0460 }
0461 
0462 Document::LoadingState LoadingDocumentImpl::loadingState() const
0463 {
0464     if (!document()->image().isNull()) {
0465         return Document::Loaded;
0466     } else if (d->mMetaInfoLoaded) {
0467         return Document::MetaInfoLoaded;
0468     } else if (document()->kind() != MimeTypeUtils::KIND_UNKNOWN) {
0469         return Document::KindDetermined;
0470     } else {
0471         return Document::Loading;
0472     }
0473 }
0474 
0475 void LoadingDocumentImpl::slotMetaInfoLoaded()
0476 {
0477     LOG("");
0478     Q_ASSERT(!d->mMetaInfoFuture.isRunning());
0479     if (!d->mMetaInfoFuture.result()) {
0480         setDocumentErrorString(i18nc("@info", "Loading meta information failed."));
0481         Q_EMIT loadingFailed();
0482         switchToImpl(new EmptyDocumentImpl(document()));
0483         return;
0484     }
0485 
0486     setDocumentFormat(d->mFormat);
0487     setDocumentImageSize(d->mImageSize);
0488     setDocumentExiv2Image(std::move(d->mExiv2Image));
0489     setDocumentCmsProfile(d->mCmsProfile);
0490 
0491     d->mMetaInfoLoaded = true;
0492     Q_EMIT metaInfoLoaded();
0493 
0494     // Start image loading if necessary
0495     // We test if mImageDataFuture is not already running because code connected to
0496     // metaInfoLoaded() signal could have called loadImage()
0497     if (!d->mImageDataFuture.isRunning() && d->mImageDataInvertedZoom != 0) {
0498         d->startImageDataLoading();
0499     }
0500 }
0501 
0502 void LoadingDocumentImpl::slotImageLoaded()
0503 {
0504     LOG("");
0505     if (d->mImage.isNull()) {
0506         setDocumentErrorString(i18nc("@info", "Loading image failed."));
0507         Q_EMIT loadingFailed();
0508         switchToImpl(new EmptyDocumentImpl(document()));
0509         return;
0510     }
0511 
0512     if (d->mAnimated) {
0513         if (d->mImage.size() == d->mImageSize) {
0514             // We already decoded the first frame at the right size, let's show
0515             // it
0516             setDocumentImage(d->mImage);
0517         }
0518 
0519         switchToImpl(new AnimatedDocumentLoadedImpl(document(), d->mData));
0520 
0521         return;
0522     }
0523 
0524     if (d->mImageDataInvertedZoom != 1 && d->mImage.size() != d->mImageSize) {
0525         LOG("Loaded a down sampled image");
0526         d->mDownSampledImageLoaded = true;
0527         // We loaded a down sampled image
0528         setDocumentDownSampledImage(d->mImage, d->mImageDataInvertedZoom);
0529         return;
0530     }
0531 
0532     LOG("Loaded a full image");
0533     setDocumentImage(d->mImage);
0534     DocumentLoadedImpl *impl;
0535     if (d->mJpegContent.get()) {
0536         impl = new JpegDocumentLoadedImpl(document(), d->mJpegContent.release());
0537     } else {
0538         impl = new DocumentLoadedImpl(document(), d->mData);
0539     }
0540     switchToImpl(impl);
0541 }
0542 
0543 } // namespace
0544 
0545 #include "moc_loadingdocumentimpl.cpp"