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"