diff --git a/app/kipiinterface.cpp b/app/kipiinterface.cpp --- a/app/kipiinterface.cpp +++ b/app/kipiinterface.cpp @@ -51,7 +51,7 @@ #include "kipiimagecollectionselector.h" #include "kipiuploadwidget.h" #include -#include +#include #include #include #include @@ -129,7 +129,7 @@ { if (!_url.isLocalFile()) return QString(); - JpegContent content; + Exiv2ImageLoader content; bool ok = content.load(_url.toLocalFile()); if (!ok) return QString(); diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -131,7 +131,6 @@ imageutils.cpp invisiblebuttongroup.cpp iodevicejpegsourcemanager.cpp - jpegcontent.cpp kindproxymodel.cpp semanticinfo/sorteddirmodel.cpp memoryutils.cpp diff --git a/lib/cms/cmsprofile.h b/lib/cms/cmsprofile.h --- a/lib/cms/cmsprofile.h +++ b/lib/cms/cmsprofile.h @@ -32,19 +32,14 @@ class QByteArray; class QString; -namespace Exiv2 -{ - class Image; -} - typedef void* cmsHPROFILE; namespace Gwenview { +class Exiv2ImageLoader; namespace Cms { - struct ProfilePrivate; /** * Wrapper for lcms color profile @@ -65,7 +60,7 @@ cmsHPROFILE handle() const; static Profile::Ptr loadFromImageData(const QByteArray& data, const QByteArray& format); - static Profile::Ptr loadFromExiv2Image(const Exiv2::Image* image); + static Profile::Ptr loadFromExiv2Image(const Exiv2ImageLoader& exiv); static Profile::Ptr getMonitorProfile(); static Profile::Ptr getSRgbProfile(); diff --git a/lib/cms/cmsprofile.cpp b/lib/cms/cmsprofile.cpp --- a/lib/cms/cmsprofile.cpp +++ b/lib/cms/cmsprofile.cpp @@ -43,7 +43,7 @@ #include // Exiv2 -#include +#include // X11 #ifdef HAVE_X11 @@ -157,28 +157,18 @@ return ptr; } -Profile::Ptr Profile::loadFromExiv2Image(const Exiv2::Image* image) +Profile::Ptr Profile::loadFromExiv2Image(const Exiv2ImageLoader& exiv) { Profile::Ptr ptr; cmsHPROFILE hProfile = nullptr; - const Exiv2::ExifData& exifData = image->exifData(); - Exiv2::ExifKey key("Exif.Image.InterColorProfile"); - Exiv2::ExifData::const_iterator it = exifData.findKey(key); - if (it == exifData.end()) { - LOG("No profile found"); - return ptr; - } - int size = it->size(); - LOG("size:" << size); - - QByteArray data; - data.resize(size); - it->copy(reinterpret_cast(data.data()), Exiv2::invalidByteOrder); - hProfile = cmsOpenProfileFromMem(data, size); + QByteArray profData = exiv.interColorProfile(); + if(!profData.isEmpty()) { + hProfile = cmsOpenProfileFromMem(profData.data(), profData.size()); - if (hProfile) { - ptr = new Profile(hProfile); + if (hProfile) { + ptr = new Profile(hProfile); + } } return ptr; } diff --git a/lib/document/abstractdocumentimpl.h b/lib/document/abstractdocumentimpl.h --- a/lib/document/abstractdocumentimpl.h +++ b/lib/document/abstractdocumentimpl.h @@ -113,7 +113,7 @@ void setDocumentImageSize(const QSize& size); void setDocumentKind(MimeTypeUtils::Kind); void setDocumentFormat(const QByteArray& format); - void setDocumentExiv2Image(std::unique_ptr); + void setDocumentExiv2Image(const Exiv2ImageLoader* exivContent); void setDocumentDownSampledImage(const QImage&, int invertedZoom); void setDocumentCmsProfile(const Cms::Profile::Ptr &profile); void setDocumentErrorString(const QString&); diff --git a/lib/document/abstractdocumentimpl.cpp b/lib/document/abstractdocumentimpl.cpp --- a/lib/document/abstractdocumentimpl.cpp +++ b/lib/document/abstractdocumentimpl.cpp @@ -78,9 +78,9 @@ d->mDocument->setKind(kind); } -void AbstractDocumentImpl::setDocumentExiv2Image(std::unique_ptr image) +void AbstractDocumentImpl::setDocumentExiv2Image(const Exiv2ImageLoader* exivContent) { - d->mDocument->setExiv2Image(std::move(image)); + d->mDocument->setExiv2Image(exivContent); } void AbstractDocumentImpl::setDocumentDownSampledImage(const QImage& image, int invertedZoom) diff --git a/lib/document/document.h b/lib/document/document.h --- a/lib/document/document.h +++ b/lib/document/document.h @@ -240,7 +240,7 @@ void setKind(MimeTypeUtils::Kind); void setFormat(const QByteArray&); void setSize(const QSize&); - void setExiv2Image(std::unique_ptr); + void setExiv2Image(const Exiv2ImageLoader* exivContent); void setDownSampledImage(const QImage&, int invertedZoom); void switchToImpl(AbstractDocumentImpl* impl); void setErrorString(const QString&); diff --git a/lib/document/document.cpp b/lib/document/document.cpp --- a/lib/document/document.cpp +++ b/lib/document/document.cpp @@ -176,7 +176,6 @@ 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); @@ -391,10 +390,9 @@ return d->mImpl->editor(); } -void Document::setExiv2Image(std::unique_ptr image) +void Document::setExiv2Image(const Exiv2ImageLoader* exivContent) { - d->mExiv2Image = std::move(image); - d->mImageMetaInfoModel.setExiv2Image(d->mExiv2Image.get()); + d->mImageMetaInfoModel.setExiv2Image(exivContent); emit metaInfoUpdated(); } diff --git a/lib/document/document_p.h b/lib/document/document_p.h --- a/lib/document/document_p.h +++ b/lib/document/document_p.h @@ -62,7 +62,6 @@ QSize mSize; QImage mImage; QMap mDownSampledImageMap; - std::unique_ptr mExiv2Image; MimeTypeUtils::Kind mKind; QByteArray mFormat; ImageMetaInfoModel mImageMetaInfoModel; diff --git a/lib/document/jpegdocumentloadedimpl.h b/lib/document/jpegdocumentloadedimpl.h --- a/lib/document/jpegdocumentloadedimpl.h +++ b/lib/document/jpegdocumentloadedimpl.h @@ -33,16 +33,15 @@ namespace Gwenview { -class JpegContent; +class Exiv2ImageLoader; struct JpegDocumentLoadedImplPrivate; class JpegDocumentLoadedImpl : public DocumentLoadedImpl { Q_OBJECT public: - JpegDocumentLoadedImpl(Document*, JpegContent*); + JpegDocumentLoadedImpl(Document*, std::unique_ptr&&); ~JpegDocumentLoadedImpl() override; - QByteArray rawData() const override; protected: bool saveInternal(QIODevice* device, const QByteArray& format) override; diff --git a/lib/document/jpegdocumentloadedimpl.cpp b/lib/document/jpegdocumentloadedimpl.cpp --- a/lib/document/jpegdocumentloadedimpl.cpp +++ b/lib/document/jpegdocumentloadedimpl.cpp @@ -28,51 +28,43 @@ // KDE // Local -#include "jpegcontent.h" +#include "exiv2imageloader.h" namespace Gwenview { struct JpegDocumentLoadedImplPrivate { - JpegContent* mJpegContent; + std::unique_ptr mExivContent; }; -JpegDocumentLoadedImpl::JpegDocumentLoadedImpl(Document* doc, JpegContent* jpegContent) +JpegDocumentLoadedImpl::JpegDocumentLoadedImpl(Document* doc, std::unique_ptr&& exivContent) : DocumentLoadedImpl(doc, QByteArray() /* rawData */) , d(new JpegDocumentLoadedImplPrivate) { - Q_ASSERT(jpegContent); - d->mJpegContent = jpegContent; + Q_ASSERT(exivContent); + d->mExivContent = std::move(exivContent); } JpegDocumentLoadedImpl::~JpegDocumentLoadedImpl() { - delete d->mJpegContent; delete d; } bool JpegDocumentLoadedImpl::saveInternal(QIODevice* device, const QByteArray& format) { - if (format == "jpeg") { - if (!d->mJpegContent->thumbnail().isNull()) { - QImage thumbnail = document()->image().scaled(128, 128, Qt::KeepAspectRatio); - d->mJpegContent->setThumbnail(thumbnail); - } - - bool ok = d->mJpegContent->save(device); - if (!ok) { - setDocumentErrorString(d->mJpegContent->errorString()); - } - return ok; + bool ok = d->mExivContent->save(device); + if (ok) { + setDocumentFormat(format); } else { - return DocumentLoadedImpl::saveInternal(device, format); + setDocumentErrorString(d->mExivContent->errorMessage()); } + return ok; } void JpegDocumentLoadedImpl::setImage(const QImage& image) { - d->mJpegContent->setImage(image); + d->mExivContent->setImage(image); DocumentLoadedImpl::setImage(image); } @@ -81,15 +73,10 @@ DocumentLoadedImpl::applyTransformation(orientation); // Apply Exif transformation first to normalize image - d->mJpegContent->transform(d->mJpegContent->orientation()); - d->mJpegContent->resetOrientation(); + d->mExivContent->transform(d->mExivContent->orientation()); + d->mExivContent->resetOrientation(); - d->mJpegContent->transform(orientation); -} - -QByteArray JpegDocumentLoadedImpl::rawData() const -{ - return d->mJpegContent->rawData(); + d->mExivContent->transform(orientation); } } // namespace diff --git a/lib/document/loadingdocumentimpl.cpp b/lib/document/loadingdocumentimpl.cpp --- a/lib/document/loadingdocumentimpl.cpp +++ b/lib/document/loadingdocumentimpl.cpp @@ -59,7 +59,6 @@ #include "exiv2imageloader.h" #include "gvdebug.h" #include "imageutils.h" -#include "jpegcontent.h" #include "jpegdocumentloadedimpl.h" #include "orientation.h" #include "svgdocumentloadedimpl.h" @@ -101,8 +100,7 @@ QByteArray mData; QByteArray mFormat; QSize mImageSize; - std::unique_ptr mExiv2Image; - std::unique_ptr mJpegContent; + std::unique_ptr mExivContent; QImage mImage; Cms::Profile::Ptr mCmsProfile; @@ -194,20 +192,18 @@ buffer.setBuffer(&mData); buffer.open(QIODevice::ReadOnly); - Exiv2ImageLoader loader; - if (loader.load(mData)) { - mExiv2Image = loader.popImage(); + std::unique_ptr exivContent(new Exiv2ImageLoader()); + if (exivContent->load(q->document()->url().path())) { + mExivContent = std::move(exivContent); + } else { + qWarning() << "unable load exif data, error was: " << exivContent->errorMessage(); } #ifdef KDCRAW_FOUND if (KDcrawIface::KDcraw::rawFilesList().contains(QString::fromLatin1(mFormatHint))) { QByteArray previewData; - // if the image is in format supported by dcraw, fetch its embedded preview - mJpegContent.reset(new JpegContent()); - // use KDcraw for getting the embedded preview - // KDcraw functionality cloned locally (temp. solution) bool ret = KDcrawIface::KDcraw::loadEmbeddedPreview(previewData, buffer); if (!ret) { @@ -262,22 +258,12 @@ LOG("mFormat" << mFormat); GV_RETURN_VALUE_IF_FAIL(!mFormat.isEmpty(), false); - if (mFormat == "jpeg" && mExiv2Image.get()) { - mJpegContent.reset(new JpegContent()); - } - - if (mJpegContent.get()) { - if (!mJpegContent->loadFromData(mData, mExiv2Image.get()) && - !mJpegContent->loadFromData(mData)) { - qWarning() << "Unable to use preview of " << q->document()->url().fileName(); - return false; - } - // Use the size from JpegContent, as its correctly transposed if the + if (mExivContent != nullptr) { + // Use the size from exif, as its correctly transposed if the // image has been rotated - mImageSize = mJpegContent->size(); - - mCmsProfile = Cms::Profile::loadFromExiv2Image(mExiv2Image.get()); + mImageSize = mExivContent->size(); + mCmsProfile = Cms::Profile::loadFromExiv2Image(*mExivContent.get()); } LOG("mImageSize" << mImageSize); @@ -492,7 +478,7 @@ setDocumentFormat(d->mFormat); setDocumentImageSize(d->mImageSize); - setDocumentExiv2Image(std::move(d->mExiv2Image)); + setDocumentExiv2Image(d->mExivContent.get()); setDocumentCmsProfile(d->mCmsProfile); d->mMetaInfoLoaded = true; @@ -543,10 +529,10 @@ LOG("Loaded a full image"); setDocumentImage(d->mImage); DocumentLoadedImpl* impl; - if (d->mJpegContent.get()) { + if (d->mExivContent != nullptr) { impl = new JpegDocumentLoadedImpl( document(), - d->mJpegContent.release()); + std::move(d->mExivContent)); } else { impl = new DocumentLoadedImpl( document(), diff --git a/lib/exiv2imageloader.h b/lib/exiv2imageloader.h --- a/lib/exiv2imageloader.h +++ b/lib/exiv2imageloader.h @@ -37,9 +37,13 @@ } // Local +#include "orientation.h" class QByteArray; +class QIODevice; class QString; +class QImage; +class QSize; namespace Gwenview { @@ -57,12 +61,44 @@ ~Exiv2ImageLoader(); bool load(const QString&); - bool load(const QByteArray&); + + /** + * @warning: Exiv2 takes ownership of the bytearray passed in, so the caller must keep a reference to it + * to avoid a use-after-free! + */ + bool loadFromData(const QByteArray&); + + bool save(const QString& file); + bool save(QIODevice*); + QString errorMessage() const; std::unique_ptr popImage(); + Orientation orientation() const; + void resetOrientation(); + + int dotsPerMeterX() const; + int dotsPerMeterY() const; + + QSize size() const; + + QString comment() const; + void setComment(const QString&); + + void transform(Orientation); + + QImage thumbnail() const; + void setThumbnail(const QImage&); + void setImage(const QImage& image); + + QByteArray interColorProfile() const; + const Exiv2::Image* handle() const; + private: Exiv2ImageLoaderPrivate* const d; + + + int dotsPerMeter(const QString& keyName) const; }; } // namespace diff --git a/lib/exiv2imageloader.cpp b/lib/exiv2imageloader.cpp --- a/lib/exiv2imageloader.cpp +++ b/lib/exiv2imageloader.cpp @@ -21,29 +21,37 @@ // Self #include "exiv2imageloader.h" +// System +#include + // Qt +#include #include +#include #include #include // KDE +#include // Exiv2 #include // Local +#include "gwenviewconfig.h" +#include "imageutils.h" namespace Gwenview { struct Exiv2ImageLoaderPrivate { - std::unique_ptr mImage; + std::unique_ptr mExivHandle; QString mErrorMessage; }; Exiv2ImageLoader::Exiv2ImageLoader() -: d(new Exiv2ImageLoaderPrivate) +: d(new Exiv2ImageLoaderPrivate()) { } @@ -56,27 +64,97 @@ { QByteArray filePathByteArray = QFile::encodeName(filePath); try { - d->mImage.reset(Exiv2::ImageFactory::open(filePathByteArray.constData()).release()); - d->mImage->readMetadata(); + d->mExivHandle.reset(Exiv2::ImageFactory::open(filePathByteArray.constData()).release()); + d->mExivHandle->readMetadata(); } catch (const Exiv2::Error& error) { d->mErrorMessage = QString::fromUtf8(error.what()); return false; } + d->mErrorMessage = ""; return true; } -bool Exiv2ImageLoader::load(const QByteArray& data) +// NOTE: Exiv2 takes ownership of data, so the caller must keep a reference to it to avoid use-after-free! +bool Exiv2ImageLoader::loadFromData(const QByteArray& data) { try { - d->mImage.reset(Exiv2::ImageFactory::open((unsigned char*)data.constData(), data.size()).release()); - d->mImage->readMetadata(); + // cast needed here, otherwise Exiv2::ImageFactory::open(const std::string&, bool) will be called + d->mExivHandle.reset(Exiv2::ImageFactory::open(reinterpret_cast(data.constData()), data.size()).release()); + d->mExivHandle->readMetadata(); } catch (const Exiv2::Error& error) { d->mErrorMessage = QString::fromUtf8(error.what()); return false; } + d->mErrorMessage = ""; return true; } +bool Exiv2ImageLoader::save(const QString& path) +{ + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) { + d->mErrorMessage = i18nc("@info", "Could not open file for writing."); + return false; + } + + return save(&file); +} + +bool Exiv2ImageLoader::save(QIODevice* device) +{ + try { + QByteArray rawData; + { + // read in the old image content + Exiv2::BasicIo& io = d->mExivHandle->io(); + if (io.open() != 0) { + d->mErrorMessage = "Failed to open Exiv2::BasicIo during Exiv2ImageLoader::save()."; + return false; + } + + Exiv2::IoCloser closer(io); + rawData.resize(io.size()); + io.read((unsigned char*)rawData.data(), io.size()); + } + + // read its metadata + std::unique_ptr image; + image.reset(Exiv2::ImageFactory::open((unsigned char*)rawData.data(), rawData.size()).release()); + + // copy all the potentially modified metadata to the new image + image->setExifData(d->mExivHandle->exifData()); + image->setIptcData(d->mExivHandle->iptcData()); + image->setXmpData(d->mExivHandle->xmpData()); + image->setComment(d->mExivHandle->comment()); + + // this does NOT write to rawData, it allocates its own intermediate buffer instead! + image->writeMetadata(); + + // read the new data from the intermediate buffer + Exiv2::BasicIo& io = image->io(); + if (io.open() != 0) { + d->mErrorMessage = "Failed to open Exiv2::BasicIo during Exiv2ImageLoader::save()."; + return false; + } + + // write buffer to IODevice + QDataStream stream(device); + stream.writeRawData(reinterpret_cast(io.mmap()), io.size()); + + return true; + } catch(const std::bad_alloc&) { + d->mErrorMessage = "Out of memory during Exiv2ImageLoader::save()."; + } catch (const Exiv2::Error& error) { + d->mErrorMessage = QString::fromUtf8(error.what()); + } + return false; +} + +const Exiv2::Image* Exiv2ImageLoader::handle() const +{ + return d->mExivHandle.get(); +} + QString Exiv2ImageLoader::errorMessage() const { return d->mErrorMessage; @@ -84,7 +162,207 @@ std::unique_ptr Exiv2ImageLoader::popImage() { - return std::move(d->mImage); + return std::move(d->mExivHandle); +} + +Orientation Exiv2ImageLoader::orientation() const +{ + Exiv2::ExifKey key("Exif.Image.Orientation"); + Exiv2::ExifData::iterator it = d->mExivHandle->exifData().findKey(key); + + // We do the same checks as in libexiv2's src/crwimage.cpp: + // http://dev.exiv2.org/projects/exiv2/repository/entry/trunk/src/crwimage.cpp?rev=2681#L1336 + if (it == d->mExivHandle->exifData().end() || it->count() == 0 || it->typeId() != Exiv2::unsignedShort) { + return NOT_AVAILABLE; + } + return Orientation(it->toLong()); +} + +int Exiv2ImageLoader::dotsPerMeterX() const +{ + return dotsPerMeter(QStringLiteral("XResolution")); +} + +int Exiv2ImageLoader::dotsPerMeterY() const +{ + return dotsPerMeter(QStringLiteral("YResolution")); +} + +int Exiv2ImageLoader::dotsPerMeter(const QString& keyName) const +{ + Exiv2::ExifKey keyResUnit("Exif.Image.ResolutionUnit"); + Exiv2::ExifData::iterator it = d->mExivHandle->exifData().findKey(keyResUnit); + if (it == d->mExivHandle->exifData().end()) { + return 0; + } + int res = it->toLong(); + QString keyVal = QStringLiteral("Exif.Image.") + keyName; + Exiv2::ExifKey keyResolution(keyVal.toLocal8Bit().data()); + it = d->mExivHandle->exifData().findKey(keyResolution); + if (it == d->mExivHandle->exifData().end()) { + return 0; + } + // The unit for measuring XResolution and YResolution. The same unit is used for both XResolution and YResolution. + // If the image resolution in unknown, 2 (inches) is designated. + // Default = 2 + // 2 = inches + // 3 = centimeters + // Other = reserved + constexpr float INCHESPERMETER = (100. / 2.54); + switch (res) { + case 3: // dots per cm + return int(it->toLong() * 100); + default: // dots per inch + return int(it->toLong() * INCHESPERMETER); + } + + return 0; +} + +void Exiv2ImageLoader::transform(Orientation orientation) +{ + if (orientation != NOT_AVAILABLE) { + Exiv2::ExifKey key("Exif.Image.Orientation"); + Exiv2::ExifData::iterator it = d->mExivHandle->exifData().findKey(key); + if (it != d->mExivHandle->exifData().end()) { + *it = uint16_t(orientation); + } + } +} + +void Exiv2ImageLoader::resetOrientation() +{ + transform(NORMAL); +} + +QSize Exiv2ImageLoader::size() const +{ + QSize size(d->mExivHandle->pixelWidth(), d->mExivHandle->pixelHeight()); + + if (GwenviewConfig::applyExifOrientation()) { + // Adjust the size according to the orientation + switch (orientation()) { + case TRANSPOSE: + case ROT_90: + case TRANSVERSE: + case ROT_270: + size.transpose(); + break; + default: + break; + } + } + + return size; +} + +QString Exiv2ImageLoader::comment() const +{ + return QString::fromUtf8(d->mExivHandle->comment().c_str()); +} + +void Exiv2ImageLoader::setComment(const QString& comment) +{ + d->mExivHandle->setComment(comment.toUtf8().toStdString()); +} + +QImage Exiv2ImageLoader::thumbnail() const +{ + QImage image; + if (!d->mExivHandle->exifData().empty()) { +#if (EXIV2_TEST_VERSION(0,17,91)) + Exiv2::ExifThumbC thumb(d->mExivHandle->exifData()); + Exiv2::DataBuf thumbnail = thumb.copy(); +#else + Exiv2::DataBuf thumbnail = d->mExivHandle->exifData().copyThumbnail(); +#endif + image.loadFromData(thumbnail.pData_, thumbnail.size_); + + Exiv2::ExifData::iterator it = d->mExivHandle->exifData().findKey(Exiv2::ExifKey("Exif.Canon.ThumbnailImageValidArea")); + // ensure ThumbnailImageValidArea actually specifies a rectangle, i.e. there must be 4 coordinates + if (it != d->mExivHandle->exifData().end() && it->count() == 4) { + QRect validArea(QPoint(it->toLong(0), it->toLong(2)), QPoint(it->toLong(1), it->toLong(3))); + image = image.copy(validArea); + } else { + // Unfortunately, Sony does not provide an exif tag that specifies the valid area of the + // embedded thumbnail. Need to derive it from the size of the preview image instead. + it = d->mExivHandle->exifData().findKey(Exiv2::ExifKey("Exif.Sony1.PreviewImageSize")); + if (it != d->mExivHandle->exifData().end() && it->count() == 2) { + const long prevHeight = it->toLong(0); + const long prevWidth = it->toLong(1); + + const double scale = prevWidth / image.width(); + + // the embedded thumb only needs to be cropped vertically + const long validThumbAreaHeight = ceil(prevHeight / scale); + const long totalHeightOfBlackArea = image.height() - validThumbAreaHeight; + // black bars on top and bottom should be equal in height + const long offsetFromTop = totalHeightOfBlackArea / 2; + + const QRect validArea(QPoint(0, offsetFromTop), QSize(image.width(), validThumbAreaHeight)); + image = image.copy(validArea); + } + } + + Orientation o = orientation(); + if (GwenviewConfig::applyExifOrientation() && o != NORMAL && o != NOT_AVAILABLE) { + image = image.transformed(ImageUtils::transformMatrix(o)); + } + } + return image; +} + +void Exiv2ImageLoader::setThumbnail(const QImage& thumbnail) +{ + if (d->mExivHandle->exifData().empty()) { + return; + } + + QByteArray array; + QBuffer buffer(&array); + buffer.open(QIODevice::WriteOnly); + QImageWriter writer(&buffer, "JPEG"); + if (!writer.write(thumbnail)) { + qCritical() << "Could not write thumbnail\n"; + return; + } + +#if (EXIV2_TEST_VERSION(0,17,91)) + Exiv2::ExifThumb thumb(d->mExivHandle->exifData()); + thumb.setJpegThumbnail((unsigned char*)array.data(), array.size()); +#else + d->mExivHandle->exifData().setJpegThumbnail((unsigned char*)array.data(), array.size()); +#endif +} + + +void Exiv2ImageLoader::setImage(const QImage& image) +{ + d->mExivHandle->exifData()["Exif.Photo.PixelXDimension"] = image.width(); + d->mExivHandle->exifData()["Exif.Photo.PixelYDimension"] = image.height(); + + QImage thumbnail = image.scaled(128, 128, Qt::KeepAspectRatio); + setThumbnail(thumbnail); + + resetOrientation(); +} + +QByteArray Exiv2ImageLoader::interColorProfile() const +{ + QByteArray data; + + const Exiv2::ExifData& exifData = d->mExivHandle->exifData(); + static const Exiv2::ExifKey key("Exif.Image.InterColorProfile"); + Exiv2::ExifData::const_iterator it = exifData.findKey(key); + if (it == exifData.end()) { + qWarning() << "No profile found"; + return data; + } + + data.resize(it->size()); + it->copy(reinterpret_cast(data.data()), Exiv2::invalidByteOrder); + + return data; } } // namespace diff --git a/lib/imagemetainfomodel.h b/lib/imagemetainfomodel.h --- a/lib/imagemetainfomodel.h +++ b/lib/imagemetainfomodel.h @@ -32,13 +32,9 @@ class QUrl; -namespace Exiv2 -{ -class Image; -} - namespace Gwenview { +class Exiv2ImageLoader; struct ImageMetaInfoModelPrivate; class GWENVIEWLIB_EXPORT ImageMetaInfoModel : public QAbstractItemModel @@ -50,7 +46,7 @@ void setUrl(const QUrl&); void setImageSize(const QSize&); - void setExiv2Image(const Exiv2::Image*); + void setExiv2Image(const Exiv2ImageLoader*); QString keyForIndex(const QModelIndex&) const; void getInfoForKey(const QString& key, QString* label, QString* value) const; diff --git a/lib/imagemetainfomodel.cpp b/lib/imagemetainfomodel.cpp --- a/lib/imagemetainfomodel.cpp +++ b/lib/imagemetainfomodel.cpp @@ -41,6 +41,8 @@ #include "urlutils.h" #endif +#include "exiv2imageloader.h" + namespace Gwenview { @@ -419,7 +421,7 @@ d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.ImageSize"), imageSize); } -void ImageMetaInfoModel::setExiv2Image(const Exiv2::Image* image) +void ImageMetaInfoModel::setExiv2Image(const Exiv2ImageLoader* exivContent) { MetaInfoGroup* exifGroup = d->mMetaInfoGroupVector[ExifGroup]; MetaInfoGroup* iptcGroup = d->mMetaInfoGroupVector[IptcGroup]; @@ -431,10 +433,11 @@ d->clearGroup(iptcGroup, iptcIndex); d->clearGroup(xmpGroup, xmpIndex); - if (!image) { + if (!exivContent) { return; } + const Exiv2::Image* image = exivContent->handle(); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Comment"), QString::fromUtf8(image->comment().c_str())); if (image->checkMode(Exiv2::mdExif) & Exiv2::amRead) { diff --git a/lib/jpegcontent.h b/lib/jpegcontent.h deleted file mode 100644 --- a/lib/jpegcontent.h +++ /dev/null @@ -1,92 +0,0 @@ -// vim: set tabstop=4 shiftwidth=4 expandtab -/* -Gwenview: an image viewer -Copyright 2007 Aurélien Gâteau - -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. - -*/ -#ifndef JPEGCONTENT_H -#define JPEGCONTENT_H - -// Local -#include -#include -#include -class QImage; -class QSize; -class QString; -class QIODevice; - -namespace Exiv2 -{ -class Image; -} - -namespace Gwenview -{ - -class GWENVIEWLIB_EXPORT JpegContent -{ -public: - JpegContent(); - ~JpegContent(); - - Orientation orientation() const; - void resetOrientation(); - - int dotsPerMeterX() const; - int dotsPerMeterY() const; - - QSize size() const; - - QString comment() const; - void setComment(const QString&); - - void transform(Orientation); - - QImage thumbnail() const; - void setThumbnail(const QImage&); - - // Recreate raw data to represent image - // Note: thumbnail must be updated separately - void setImage(const QImage& image); - - bool load(const QString& file); - bool loadFromData(const QByteArray& rawData); - /** - * Use this version of loadFromData if you already have an Exiv2::Image* - */ - bool loadFromData(const QByteArray& rawData, Exiv2::Image*); - bool save(const QString& file); - bool save(QIODevice*); - - QByteArray rawData() const; - - QString errorString() const; - -private: - struct Private; - Private *d; - - JpegContent(const JpegContent&); - void operator=(const JpegContent&); - void applyPendingTransformation(); - int dotsPerMeter(const QString& keyName) const; -}; - -} // namespace - -#endif /* JPEGCONTENT_H */ diff --git a/lib/jpegcontent.cpp b/lib/jpegcontent.cpp deleted file mode 100644 --- a/lib/jpegcontent.cpp +++ /dev/null @@ -1,714 +0,0 @@ -// vim: set tabstop=4 shiftwidth=4 expandtab: -/* -Gwenview: an image viewer -Copyright 2007 Aurélien Gâteau - -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 "jpegcontent.h" - -// System -#include -#include -#include -#include -extern "C" { -#include -#include "transupp.h" -} - -// Qt -#include -#include -#include -#include -#include -#include - -// KDE -#include - -// Exiv2 -#include - -// Local -#include "jpegerrormanager.h" -#include "iodevicejpegsourcemanager.h" -#include "exiv2imageloader.h" -#include "gwenviewconfig.h" -#include "imageutils.h" - -namespace Gwenview -{ - -const int INMEM_DST_DELTA = 4096; - -//----------------------------------------------- -// -// In-memory data destination manager for libjpeg -// -//----------------------------------------------- -struct inmem_dest_mgr : public jpeg_destination_mgr -{ - QByteArray* mOutput; - - void dump() - { - qDebug() << "dest_mgr:\n"; - qDebug() << "- next_output_byte: " << next_output_byte; - qDebug() << "- free_in_buffer: " << free_in_buffer; - qDebug() << "- output size: " << mOutput->size(); - } -}; - -void inmem_init_destination(j_compress_ptr cinfo) -{ - inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); - if (dest->mOutput->size() == 0) { - dest->mOutput->resize(INMEM_DST_DELTA); - } - dest->free_in_buffer = dest->mOutput->size(); - dest->next_output_byte = (JOCTET*)(dest->mOutput->data()); -} - -boolean inmem_empty_output_buffer(j_compress_ptr cinfo) -{ - inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); - dest->mOutput->resize(dest->mOutput->size() + INMEM_DST_DELTA); - dest->next_output_byte = (JOCTET*)(dest->mOutput->data() + dest->mOutput->size() - INMEM_DST_DELTA); - dest->free_in_buffer = INMEM_DST_DELTA; - - return true; -} - -void inmem_term_destination(j_compress_ptr cinfo) -{ - inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); - int finalSize = dest->next_output_byte - (JOCTET*)(dest->mOutput->data()); - Q_ASSERT(finalSize >= 0); - dest->mOutput->resize(finalSize); -} - -//--------------------- -// -// JpegContent::Private -// -//--------------------- -struct JpegContent::Private -{ - // JpegContent usually stores the image pixels as compressed JPEG data in - // mRawData. However if the image is set with setImage() because the user - // performed a lossy image manipulation, mRawData is cleared and the image - // pixels are kept in mImage until updateRawDataFromImage() is called. - QImage mImage; - - // Store the input file, keep it open readOnly. This allows the file to be memory mapped - // (i.e. mRawData may point to mFile.map()) rather than completely read on load. Postpone - // QFile::readAll() as long as possible (currently in save()). - QFile mFile; - QByteArray mRawData; - - QSize mSize; - QString mComment; - bool mPendingTransformation; - QTransform mTransformMatrix; - Exiv2::ExifData mExifData; - QString mErrorString; - - Private() - { - mPendingTransformation = false; - } - - void setupInmemDestination(j_compress_ptr cinfo, QByteArray* outputData) - { - Q_ASSERT(!cinfo->dest); - inmem_dest_mgr* dest = (inmem_dest_mgr*) - (*cinfo->mem->alloc_small)((j_common_ptr) cinfo, JPOOL_PERMANENT, - sizeof(inmem_dest_mgr)); - cinfo->dest = (struct jpeg_destination_mgr*)(dest); - - dest->init_destination = inmem_init_destination; - dest->empty_output_buffer = inmem_empty_output_buffer; - dest->term_destination = inmem_term_destination; - - dest->mOutput = outputData; - } - bool readSize() - { - struct jpeg_decompress_struct srcinfo; - - // Init JPEG structs - JPEGErrorManager errorManager; - - // Initialize the JPEG decompression object - srcinfo.err = &errorManager; - jpeg_create_decompress(&srcinfo); - if (setjmp(errorManager.jmp_buffer)) { - qCritical() << "libjpeg fatal error\n"; - return false; - } - - // Specify data source for decompression - QBuffer buffer(&mRawData); - buffer.open(QIODevice::ReadOnly); - IODeviceJpegSourceManager::setup(&srcinfo, &buffer); - - // Read the header - jcopy_markers_setup(&srcinfo, JCOPYOPT_ALL); - int result = jpeg_read_header(&srcinfo, true); - if (result != JPEG_HEADER_OK) { - qCritical() << "Could not read jpeg header\n"; - jpeg_destroy_decompress(&srcinfo); - return false; - } - mSize = QSize(srcinfo.image_width, srcinfo.image_height); - - jpeg_destroy_decompress(&srcinfo); - return true; - } - - bool updateRawDataFromImage() - { - QBuffer buffer; - QImageWriter writer(&buffer, "jpeg"); - writer.setQuality(GwenviewConfig::jPEGQuality()); - if (!writer.write(mImage)) { - mErrorString = writer.errorString(); - return false; - } - mRawData = buffer.data(); - mImage = QImage(); - return true; - } -}; - -//------------ -// -// JpegContent -// -//------------ -JpegContent::JpegContent() -{ - d = new JpegContent::Private(); -} - -JpegContent::~JpegContent() -{ - delete d; -} - -bool JpegContent::load(const QString& path) -{ - if (d->mFile.isOpen()) { - d->mFile.unmap(reinterpret_cast(d->mRawData.data())); - d->mFile.close(); - d->mRawData.clear(); - } - - d->mFile.setFileName(path); - if (!d->mFile.open(QIODevice::ReadOnly)) { - qCritical() << "Could not open '" << path << "' for reading\n"; - return false; - } - - QByteArray rawData; - uchar* mappedFile = d->mFile.map(0, d->mFile.size(), QFileDevice::MapPrivateOption); - if (mappedFile == nullptr) { - // process' mapping limit exceeded, file is sealed or filesystem doesn't support it, etc. - qDebug() << "Could not mmap '" << path << "', falling back to QFile::readAll()\n"; - - rawData = d->mFile.readAll(); - // all read in, no need to keep it open - d->mFile.close(); - } else { - rawData = QByteArray::fromRawData(reinterpret_cast(mappedFile), d->mFile.size()); - } - - return loadFromData(rawData); -} - -bool JpegContent::loadFromData(const QByteArray& data) -{ - std::unique_ptr image; - Exiv2ImageLoader loader; - if (!loader.load(data)) { - qCritical() << "Could not load image with Exiv2, reported error:" << loader.errorMessage(); - } - image.reset(loader.popImage().release()); - - return loadFromData(data, image.get()); -} - -bool JpegContent::loadFromData(const QByteArray& data, Exiv2::Image* exiv2Image) -{ - d->mPendingTransformation = false; - d->mTransformMatrix.reset(); - - d->mRawData = data; - if (d->mRawData.size() == 0) { - qCritical() << "No data\n"; - return false; - } - - if (!d->readSize()) return false; - - d->mExifData = exiv2Image->exifData(); - d->mComment = QString::fromUtf8(exiv2Image->comment().c_str()); - - if (!GwenviewConfig::applyExifOrientation()) { - return true; - } - - // Adjust the size according to the orientation - switch (orientation()) { - case TRANSPOSE: - case ROT_90: - case TRANSVERSE: - case ROT_270: - d->mSize.transpose(); - break; - default: - break; - } - - return true; -} - -QByteArray JpegContent::rawData() const -{ - return d->mRawData; -} - -Orientation JpegContent::orientation() const -{ - Exiv2::ExifKey key("Exif.Image.Orientation"); - Exiv2::ExifData::iterator it = d->mExifData.findKey(key); - - // We do the same checks as in libexiv2's src/crwimage.cpp: - // http://dev.exiv2.org/projects/exiv2/repository/entry/trunk/src/crwimage.cpp?rev=2681#L1336 - if (it == d->mExifData.end() || it->count() == 0 || it->typeId() != Exiv2::unsignedShort) { - return NOT_AVAILABLE; - } - return Orientation(it->toLong()); -} - -int JpegContent::dotsPerMeterX() const -{ - return dotsPerMeter(QStringLiteral("XResolution")); -} - -int JpegContent::dotsPerMeterY() const -{ - return dotsPerMeter(QStringLiteral("YResolution")); -} - -int JpegContent::dotsPerMeter(const QString& keyName) const -{ - Exiv2::ExifKey keyResUnit("Exif.Image.ResolutionUnit"); - Exiv2::ExifData::iterator it = d->mExifData.findKey(keyResUnit); - if (it == d->mExifData.end()) { - return 0; - } - int res = it->toLong(); - QString keyVal = QLatin1String("Exif.Image.") + keyName; - Exiv2::ExifKey keyResolution(keyVal.toLocal8Bit().data()); - it = d->mExifData.findKey(keyResolution); - if (it == d->mExifData.end()) { - return 0; - } - // The unit for measuring XResolution and YResolution. The same unit is used for both XResolution and YResolution. - // If the image resolution in unknown, 2 (inches) is designated. - // Default = 2 - // 2 = inches - // 3 = centimeters - // Other = reserved - const float INCHESPERMETER = (100. / 2.54); - switch (res) { - case 3: // dots per cm - return int(it->toLong() * 100); - default: // dots per inch - return int(it->toLong() * INCHESPERMETER); - } - - return 0; -} - -void JpegContent::resetOrientation() -{ - Exiv2::ExifKey key("Exif.Image.Orientation"); - Exiv2::ExifData::iterator it = d->mExifData.findKey(key); - if (it == d->mExifData.end()) { - return; - } - - *it = uint16_t(NORMAL); -} - -QSize JpegContent::size() const -{ - return d->mSize; -} - -QString JpegContent::comment() const -{ - return d->mComment; -} - -void JpegContent::setComment(const QString& comment) -{ - d->mComment = comment; -} - -static QTransform createRotMatrix(int angle) -{ - QTransform matrix; - matrix.rotate(angle); - return matrix; -} - -static QTransform createScaleMatrix(int dx, int dy) -{ - QTransform matrix; - matrix.scale(dx, dy); - return matrix; -} - -struct OrientationInfo -{ - OrientationInfo() - : orientation(NOT_AVAILABLE) - , jxform(JXFORM_NONE) - {} - - OrientationInfo(Orientation o, const QTransform &m, JXFORM_CODE j) - : orientation(o), matrix(m), jxform(j) - {} - - Orientation orientation; - QTransform matrix; - JXFORM_CODE jxform; -}; -typedef QList OrientationInfoList; - -static const OrientationInfoList& orientationInfoList() -{ - static OrientationInfoList list; - if (list.size() == 0) { - QTransform rot90 = createRotMatrix(90); - QTransform hflip = createScaleMatrix(-1, 1); - QTransform vflip = createScaleMatrix(1, -1); - - list - << OrientationInfo() - << OrientationInfo(NORMAL, QTransform(), JXFORM_NONE) - << OrientationInfo(HFLIP, hflip, JXFORM_FLIP_H) - << OrientationInfo(ROT_180, createRotMatrix(180), JXFORM_ROT_180) - << OrientationInfo(VFLIP, vflip, JXFORM_FLIP_V) - << OrientationInfo(TRANSPOSE, hflip * rot90, JXFORM_TRANSPOSE) - << OrientationInfo(ROT_90, rot90, JXFORM_ROT_90) - << OrientationInfo(TRANSVERSE, vflip * rot90, JXFORM_TRANSVERSE) - << OrientationInfo(ROT_270, createRotMatrix(270), JXFORM_ROT_270) - ; - } - return list; -} - -void JpegContent::transform(Orientation orientation) -{ - if (orientation != NOT_AVAILABLE && orientation != NORMAL) { - d->mPendingTransformation = true; - OrientationInfoList::ConstIterator it(orientationInfoList().begin()), end(orientationInfoList().end()); - for (; it != end; ++it) { - if ((*it).orientation == orientation) { - d->mTransformMatrix = (*it).matrix * d->mTransformMatrix; - break; - } - } - if (it == end) { - qWarning() << "Could not find matrix for orientation\n"; - } - } -} - -#if 0 -static void dumpMatrix(const QTransform& matrix) -{ - qDebug() << "matrix | " << matrix.m11() << ", " << matrix.m12() << " |\n"; - qDebug() << " | " << matrix.m21() << ", " << matrix.m22() << " |\n"; - qDebug() << " ( " << matrix.dx() << ", " << matrix.dy() << " )\n"; -} -#endif - -static bool matricesAreSame(const QTransform& m1, const QTransform& m2, double tolerance) -{ - return fabs(m1.m11() - m2.m11()) < tolerance - && fabs(m1.m12() - m2.m12()) < tolerance - && fabs(m1.m21() - m2.m21()) < tolerance - && fabs(m1.m22() - m2.m22()) < tolerance - && fabs(m1.dx() - m2.dx()) < tolerance - && fabs(m1.dy() - m2.dy()) < tolerance; -} - -static JXFORM_CODE findJxform(const QTransform& matrix) -{ - OrientationInfoList::ConstIterator it(orientationInfoList().begin()), end(orientationInfoList().end()); - for (; it != end; ++it) { - if (matricesAreSame((*it).matrix, matrix, 0.001)) { - return (*it).jxform; - } - } - qWarning() << "findJxform: failed\n"; - return JXFORM_NONE; -} - -void JpegContent::applyPendingTransformation() -{ - if (d->mRawData.size() == 0) { - qCritical() << "No data loaded\n"; - return; - } - - // The following code is inspired by jpegtran.c from the libjpeg - - // Init JPEG structs - struct jpeg_decompress_struct srcinfo; - struct jpeg_compress_struct dstinfo; - jvirt_barray_ptr * src_coef_arrays; - jvirt_barray_ptr * dst_coef_arrays; - - // Initialize the JPEG decompression object - JPEGErrorManager srcErrorManager; - srcinfo.err = &srcErrorManager; - jpeg_create_decompress(&srcinfo); - if (setjmp(srcErrorManager.jmp_buffer)) { - qCritical() << "libjpeg error in src\n"; - return; - } - - // Initialize the JPEG compression object - JPEGErrorManager dstErrorManager; - dstinfo.err = &dstErrorManager; - jpeg_create_compress(&dstinfo); - if (setjmp(dstErrorManager.jmp_buffer)) { - qCritical() << "libjpeg error in dst\n"; - return; - } - - // Specify data source for decompression - QBuffer buffer(&d->mRawData); - buffer.open(QIODevice::ReadOnly); - IODeviceJpegSourceManager::setup(&srcinfo, &buffer); - - // Enable saving of extra markers that we want to copy - jcopy_markers_setup(&srcinfo, JCOPYOPT_ALL); - - (void) jpeg_read_header(&srcinfo, true); - - // Init transformation - jpeg_transform_info transformoption; - memset(&transformoption, 0, sizeof(jpeg_transform_info)); - transformoption.transform = findJxform(d->mTransformMatrix); - jtransform_request_workspace(&srcinfo, &transformoption); - - /* Read source file as DCT coefficients */ - src_coef_arrays = jpeg_read_coefficients(&srcinfo); - - /* Initialize destination compression parameters from source values */ - jpeg_copy_critical_parameters(&srcinfo, &dstinfo); - - /* Adjust destination parameters if required by transform options; - * also find out which set of coefficient arrays will hold the output. - */ - dst_coef_arrays = jtransform_adjust_parameters(&srcinfo, &dstinfo, - src_coef_arrays, - &transformoption); - - /* Specify data destination for compression */ - QByteArray output; - output.resize(d->mRawData.size()); - d->setupInmemDestination(&dstinfo, &output); - - /* Start compressor (note no image data is actually written here) */ - jpeg_write_coefficients(&dstinfo, dst_coef_arrays); - - /* Copy to the output file any extra markers that we want to preserve */ - jcopy_markers_execute(&srcinfo, &dstinfo, JCOPYOPT_ALL); - - /* Execute image transformation, if any */ - jtransform_execute_transformation(&srcinfo, &dstinfo, - src_coef_arrays, - &transformoption); - - /* Finish compression and release memory */ - jpeg_finish_compress(&dstinfo); - jpeg_destroy_compress(&dstinfo); - (void) jpeg_finish_decompress(&srcinfo); - jpeg_destroy_decompress(&srcinfo); - - // Set rawData to our new JPEG - d->mRawData = output; -} - -QImage JpegContent::thumbnail() const -{ - QImage image; - if (!d->mExifData.empty()) { -#if(EXIV2_TEST_VERSION(0,17,91)) - Exiv2::ExifThumbC thumb(d->mExifData); - Exiv2::DataBuf thumbnail = thumb.copy(); -#else - Exiv2::DataBuf thumbnail = d->mExifData.copyThumbnail(); -#endif - image.loadFromData(thumbnail.pData_, thumbnail.size_); - - Exiv2::ExifData::iterator it = d->mExifData.findKey(Exiv2::ExifKey("Exif.Canon.ThumbnailImageValidArea")); - // ensure ThumbnailImageValidArea actually specifies a rectangle, i.e. there must be 4 coordinates - if (it != d->mExifData.end() && it->count() == 4) { - QRect validArea(QPoint(it->toLong(0), it->toLong(2)), QPoint(it->toLong(1), it->toLong(3))); - image = image.copy(validArea); - } else { - // Unfortunately, Sony does not provide an exif tag that specifies the valid area of the - // embedded thumbnail. Need to derive it from the size of the preview image instead. - it = d->mExifData.findKey(Exiv2::ExifKey("Exif.Sony1.PreviewImageSize")); - if (it != d->mExifData.end() && it->count() == 2) { - const long prevHeight = it->toLong(0); - const long prevWidth = it->toLong(1); - - const double scale = prevWidth / image.width(); - - // the embedded thumb only needs to be cropped vertically - const long validThumbAreaHeight = ceil(prevHeight / scale); - const long totalHeightOfBlackArea = image.height() - validThumbAreaHeight; - // black bars on top and bottom should be equal in height - const long offsetFromTop = totalHeightOfBlackArea / 2; - - const QRect validArea(QPoint(0, offsetFromTop), QSize(image.width(), validThumbAreaHeight)); - image = image.copy(validArea); - } - } - - Orientation o = orientation(); - if (GwenviewConfig::applyExifOrientation() && o != NORMAL && o != NOT_AVAILABLE) { - image = image.transformed(ImageUtils::transformMatrix(o)); - } - } - return image; -} - -void JpegContent::setThumbnail(const QImage& thumbnail) -{ - if (d->mExifData.empty()) { - return; - } - - QByteArray array; - QBuffer buffer(&array); - buffer.open(QIODevice::WriteOnly); - QImageWriter writer(&buffer, "JPEG"); - if (!writer.write(thumbnail)) { - qCritical() << "Could not write thumbnail\n"; - return; - } - -#if (EXIV2_TEST_VERSION(0,17,91)) - Exiv2::ExifThumb thumb(d->mExifData); - thumb.setJpegThumbnail((unsigned char*)array.data(), array.size()); -#else - d->mExifData.setJpegThumbnail((unsigned char*)array.data(), array.size()); -#endif -} - -bool JpegContent::save(const QString& path) -{ - // we need to take ownership of the input file's data - // if the input file is still open, data is still only mem-mapped - if (d->mFile.isOpen()) { - // backup the mmap() pointer - auto* mappedFile = reinterpret_cast(d->mRawData.data()); - // read the file to memory - d->mRawData = d->mFile.readAll(); - d->mFile.unmap(mappedFile); - d->mFile.close(); - } - - QFile file(path); - if (!file.open(QIODevice::WriteOnly)) { - d->mErrorString = i18nc("@info", "Could not open file for writing."); - return false; - } - - return save(&file); -} - -bool JpegContent::save(QIODevice* device) -{ - if (!d->mImage.isNull()) { - if (!d->updateRawDataFromImage()) { - return false; - } - } - - if (d->mRawData.size() == 0) { - d->mErrorString = i18nc("@info", "No data to store."); - return false; - } - - if (d->mPendingTransformation) { - applyPendingTransformation(); - d->mPendingTransformation = false; - } - - std::unique_ptr image; - image.reset(Exiv2::ImageFactory::open((unsigned char*)d->mRawData.data(), d->mRawData.size()).release()); - - // Store Exif info - image->setExifData(d->mExifData); - image->setComment(d->mComment.toUtf8().toStdString()); - image->writeMetadata(); - - // Update mRawData - Exiv2::BasicIo& io = image->io(); - d->mRawData.resize(io.size()); - io.read((unsigned char*)d->mRawData.data(), io.size()); - - QDataStream stream(device); - stream.writeRawData(d->mRawData.data(), d->mRawData.size()); - - // Make sure we are up to date - loadFromData(d->mRawData); - return true; -} - -QString JpegContent::errorString() const -{ - return d->mErrorString; -} - -void JpegContent::setImage(const QImage& image) -{ - d->mRawData.clear(); - d->mImage = image; - d->mSize = image.size(); - d->mExifData["Exif.Photo.PixelXDimension"] = image.width(); - d->mExifData["Exif.Photo.PixelYDimension"] = image.height(); - resetOrientation(); - - d->mPendingTransformation = false; - d->mTransformMatrix = QTransform(); -} - -} // namespace diff --git a/lib/thumbnailprovider/thumbnailgenerator.cpp b/lib/thumbnailprovider/thumbnailgenerator.cpp --- a/lib/thumbnailprovider/thumbnailgenerator.cpp +++ b/lib/thumbnailprovider/thumbnailgenerator.cpp @@ -22,7 +22,6 @@ #include "thumbnailgenerator.h" // Local -#include "jpegcontent.h" #include "gwenviewconfig.h" #include "exiv2imageloader.h" @@ -58,26 +57,38 @@ { mImage = QImage(); mNeedCaching = true; - QImage originalImage; - QSize originalSize; + + Exiv2ImageLoader content; + bool exivContentAvailable = content.load(pixPath); + int previewRatio = 1; + + // If there's jpeg content (from jpg or raw files), try to load an embedded thumbnail, if available. + if (exivContentAvailable) { + QImage thumbnail = content.thumbnail(); + + // If the user does not care about the generated thumbnails (by deleting them on exit), use ANY + // embedded thumnail, even if it's too small. + if (!thumbnail.isNull() && + (GwenviewConfig::lowResourceUsageMode() || qMax(thumbnail.width(), thumbnail.height()) >= pixelSize) + ) { + mImage = std::move(thumbnail); + mOriginalWidth = content.size().width(); + mOriginalHeight = content.size().height(); + return true; + } + } QByteArray formatHint = pixPath.section(QLatin1Char('.'), -1).toLocal8Bit().toLower(); QImageReader reader(pixPath); - JpegContent content; - QByteArray format; +#ifdef KDCRAW_FOUND QByteArray data; QBuffer buffer; - int previewRatio = 1; - -#ifdef KDCRAW_FOUND // raw images deserve special treatment if (KDcrawIface::KDcraw::rawFilesList().contains(QString::fromLatin1(formatHint))) { // use KDCraw to extract the preview bool ret = KDcrawIface::KDcraw::loadEmbeddedPreview(data, pixPath); - // We need QImage. Loading JpegContent from QImage - exif lost - // Loading QImage from JpegContent - unimplemented, would go with loadFromData if (!ret) { // if the embedded preview loading failed, load half preview instead. That's slower... if (!KDcrawIface::KDcraw::loadHalfPreview(data, pixPath)) { @@ -87,12 +98,6 @@ previewRatio = 2; } - // And we need JpegContent too because of EXIF (orientation!). - if (!content.loadFromData(data)) { - qWarning() << "unable to load preview for " << pixPath.toUtf8().constData(); - return false; - } - buffer.setBuffer(&data); buffer.open(QIODevice::ReadOnly); reader.setDevice(&buffer); @@ -106,30 +111,10 @@ // Set filename again, otherwise QImageReader won't restart from scratch reader.setFileName(pixPath); } - - if (reader.format() == "jpeg" && GwenviewConfig::applyExifOrientation()) { - content.load(pixPath); - } - } - - // If there's jpeg content (from jpg or raw files), try to load an embedded thumbnail, if available. - if (!content.rawData().isEmpty()) { - QImage thumbnail = content.thumbnail(); - - // If the user does not care about the generated thumbnails (by deleting them on exit), use ANY - // embedded thumnail, even if it's too small. - if (!thumbnail.isNull() && - (GwenviewConfig::lowResourceUsageMode() || qMax(thumbnail.width(), thumbnail.height()) >= pixelSize) - ) { - mImage = std::move(thumbnail); - mOriginalWidth = content.size().width(); - mOriginalHeight = content.size().height(); - return true; - } } // Generate thumbnail from full image - originalSize = reader.size(); + QSize originalSize = reader.size(); if (originalSize.isValid() && reader.supportsOption(QImageIOHandler::ScaledSize) && qMax(originalSize.width(), originalSize.height()) >= pixelSize) { @@ -146,7 +131,8 @@ } // format() is empty after QImageReader::read() is called - format = reader.format(); + QByteArray format = reader.format(); + QImage originalImage; if (!reader.read(&originalImage)) { return false; } diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt --- a/tests/auto/CMakeLists.txt +++ b/tests/auto/CMakeLists.txt @@ -32,7 +32,7 @@ gv_add_unit_test(documenttest testutils.cpp) endif() gv_add_unit_test(transformimageoperationtest) -gv_add_unit_test(jpegcontenttest) +gv_add_unit_test(exiv2contenttest) gv_add_unit_test(thumbnailprovidertest testutils.cpp) if (NOT GWENVIEW_SEMANTICINFO_BACKEND_NONE) gv_add_unit_test(semanticinfobackendtest) diff --git a/tests/auto/jpegcontenttest.h b/tests/auto/exiv2contenttest.h rename from tests/auto/jpegcontenttest.h rename to tests/auto/exiv2contenttest.h --- a/tests/auto/jpegcontenttest.h +++ b/tests/auto/exiv2contenttest.h @@ -23,7 +23,7 @@ // Qt #include -class JpegContentTest : public QObject +class Exiv2ImageLoaderTest : public QObject { Q_OBJECT @@ -37,7 +37,6 @@ void testSetComment(); void testMultipleRotations(); void testLoadTruncated(); - void testRawData(); void testSetImage(); }; diff --git a/tests/auto/jpegcontenttest.cpp b/tests/auto/exiv2contenttest.cpp rename from tests/auto/jpegcontenttest.cpp rename to tests/auto/exiv2contenttest.cpp --- a/tests/auto/jpegcontenttest.cpp +++ b/tests/auto/exiv2contenttest.cpp @@ -17,7 +17,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "jpegcontenttest.h" +#include "exiv2contenttest.h" #include // Qt @@ -32,7 +32,7 @@ // Local #include "../lib/orientation.h" -#include "../lib/jpegcontent.h" +#include "../lib/exiv2imageloader.h" #include "testutils.h" using namespace std; @@ -47,9 +47,9 @@ const int ORIENT6_HEIGHT = 256; // has been applied const QString ORIENT6_COMMENT = "a comment"; -QTEST_MAIN(JpegContentTest) +QTEST_MAIN(Exiv2ImageLoaderTest) -void JpegContentTest::initTestCase() +void Exiv2ImageLoaderTest::initTestCase() { bool result; QFile in(pathForTestFile(ORIENT6_FILE)); @@ -72,7 +72,7 @@ delete []data; } -void JpegContentTest::cleanupTestCase() +void Exiv2ImageLoaderTest::cleanupTestCase() { QDir::current().remove(CUT_FILE); } @@ -118,14 +118,15 @@ } #endif -void JpegContentTest::testResetOrientation() +void Exiv2ImageLoaderTest::testResetOrientation() { - Gwenview::JpegContent content; + Gwenview::Exiv2ImageLoader content; bool result; // Test resetOrientation without transform result = content.load(pathForTestFile(ORIENT6_FILE)); QVERIFY(result); + QCOMPARE(content.orientation(), Gwenview::ROT_90); content.resetOrientation(); @@ -148,21 +149,21 @@ result = content.load(TMP_FILE); QVERIFY(result); - QCOMPARE(content.orientation(), Gwenview::NORMAL); + QCOMPARE(content.orientation(), Gwenview::ROT_90); } /** - * This function tests JpegContent::transform() by applying a ROT_90 + * This function tests Exiv2ImageLoader::transform() by applying a ROT_90 * transformation, saving, reloading and applying a ROT_270 to undo the ROT_90. * Saving and reloading are necessary because lossless transformation only - * happens in JpegContent::save() + * happens in Exiv2ImageLoader::save() */ -void JpegContentTest::testTransform() +void Exiv2ImageLoaderTest::testTransform() { bool result; QImage finalImage, expectedImage; - Gwenview::JpegContent content; + Gwenview::Exiv2ImageLoader content; result = content.load(pathForTestFile(ORIENT6_FILE)); QVERIFY(result); @@ -185,10 +186,10 @@ QCOMPARE(finalImage , expectedImage); } -void JpegContentTest::testSetComment() +void Exiv2ImageLoaderTest::testSetComment() { QString comment = "test comment"; - Gwenview::JpegContent content; + Gwenview::Exiv2ImageLoader content; bool result; result = content.load(pathForTestFile(ORIENT6_FILE)); QVERIFY(result); @@ -203,9 +204,9 @@ QCOMPARE(content.comment() , comment); } -void JpegContentTest::testReadInfo() +void Exiv2ImageLoaderTest::testReadInfo() { - Gwenview::JpegContent content; + Gwenview::Exiv2ImageLoader content; bool result = content.load(pathForTestFile(ORIENT6_FILE)); QVERIFY(result); QCOMPARE(int(content.orientation()), 6); @@ -213,9 +214,9 @@ QCOMPARE(content.size() , QSize(ORIENT6_WIDTH, ORIENT6_HEIGHT)); } -void JpegContentTest::testThumbnail() +void Exiv2ImageLoaderTest::testThumbnail() { - Gwenview::JpegContent content; + Gwenview::Exiv2ImageLoader content; bool result = content.load(pathForTestFile(ORIENT6_FILE)); QVERIFY(result); QImage thumbnail = content.thumbnail(); @@ -223,10 +224,10 @@ QVERIFY(result); } -void JpegContentTest::testMultipleRotations() +void Exiv2ImageLoaderTest::testMultipleRotations() { // Test that rotating a file a lot of times does not cause findJxform() to fail - Gwenview::JpegContent content; + Gwenview::Exiv2ImageLoader content; bool result = content.load(pathForTestFile(ORIENT6_FILE)); QVERIFY(result); result = content.load(pathForTestFile(ORIENT6_FILE)); @@ -242,7 +243,7 @@ result = content.load(TMP_FILE); QVERIFY(result); - QCOMPARE(content.size() , QSize(ORIENT6_HEIGHT, ORIENT6_WIDTH)); + QCOMPARE(content.size() , QSize(ORIENT6_WIDTH, ORIENT6_HEIGHT)); // Check the other meta info are still here // QStringList ignoredKeys; @@ -250,10 +251,10 @@ // compareMetaInfo(pathForTestFile(ORIENT6_FILE), pathForTestFile(ORIENT1_VFLIP_FILE), ignoredKeys); } -void JpegContentTest::testLoadTruncated() +void Exiv2ImageLoaderTest::testLoadTruncated() { // Test that loading and manipulating a truncated file does not crash - Gwenview::JpegContent content; + Gwenview::Exiv2ImageLoader content; bool result = content.load(CUT_FILE); QVERIFY(result); QCOMPARE(int(content.orientation()), 6); @@ -264,24 +265,9 @@ qWarning() << "#" ; } -void JpegContentTest::testRawData() -{ - Gwenview::JpegContent content; - bool result = content.load(pathForTestFile(ORIENT6_FILE)); - QVERIFY(result); - - QByteArray fileData; - QFile file(pathForTestFile(ORIENT6_FILE)); - result = file.open(QIODevice::ReadOnly); - QVERIFY(result); - fileData = file.readAll(); - - QCOMPARE(content.rawData(), fileData); -} - -void JpegContentTest::testSetImage() +void Exiv2ImageLoaderTest::testSetImage() { - Gwenview::JpegContent content; + Gwenview::Exiv2ImageLoader content; bool result = content.load(pathForTestFile(ORIENT6_FILE)); QVERIFY(result); diff --git a/tests/auto/imagemetainfomodeltest.cpp b/tests/auto/imagemetainfomodeltest.cpp --- a/tests/auto/imagemetainfomodeltest.cpp +++ b/tests/auto/imagemetainfomodeltest.cpp @@ -51,13 +51,9 @@ data = file.readAll(); } - std::unique_ptr image; - { - Exiv2ImageLoader loader; - QVERIFY(loader.load(data)); - image = loader.popImage(); - } + Exiv2ImageLoader loader; + QVERIFY(loader.loadFromData(data)); ImageMetaInfoModel model; - model.setExiv2Image(image.get()); + model.setExiv2Image(&loader); }