File indexing completed on 2024-04-28 04:18:51

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 #include "jpegcontent.h"
0022 
0023 // System
0024 #include <cmath>
0025 #include <cstdio>
0026 #include <cstdlib>
0027 #include <cstring>
0028 
0029 extern "C" {
0030 #include <jpeglib.h> // Must be included before transupp.h
0031 
0032 #include "transupp.h"
0033 }
0034 
0035 // Qt
0036 #include <QBuffer>
0037 #include <QFile>
0038 #include <QImage>
0039 #include <QImageWriter>
0040 #include <QTransform>
0041 
0042 // KF
0043 #include <KLocalizedString>
0044 
0045 // Exiv2
0046 #include <exiv2/exiv2.hpp>
0047 
0048 // Local
0049 #include "exiv2imageloader.h"
0050 #include "gwenview_lib_debug.h"
0051 #include "gwenviewconfig.h"
0052 #include "imageutils.h"
0053 #include "iodevicejpegsourcemanager.h"
0054 #include "jpegerrormanager.h"
0055 
0056 namespace Gwenview
0057 {
0058 const int INMEM_DST_DELTA = 4096;
0059 
0060 //-----------------------------------------------
0061 //
0062 // In-memory data destination manager for libjpeg
0063 //
0064 //-----------------------------------------------
0065 struct inmem_dest_mgr : public jpeg_destination_mgr {
0066     QByteArray *mOutput;
0067 
0068     void dump()
0069     {
0070         qCDebug(GWENVIEW_LIB_LOG) << "dest_mgr:\n";
0071         qCDebug(GWENVIEW_LIB_LOG) << "- next_output_byte: " << next_output_byte;
0072         qCDebug(GWENVIEW_LIB_LOG) << "- free_in_buffer: " << free_in_buffer;
0073         qCDebug(GWENVIEW_LIB_LOG) << "- output size: " << mOutput->size();
0074     }
0075 };
0076 
0077 void inmem_init_destination(j_compress_ptr cinfo)
0078 {
0079     auto dest = (inmem_dest_mgr *)(cinfo->dest);
0080     if (dest->mOutput->size() == 0) {
0081         dest->mOutput->resize(INMEM_DST_DELTA);
0082     }
0083     dest->free_in_buffer = dest->mOutput->size();
0084     dest->next_output_byte = (JOCTET *)(dest->mOutput->data());
0085 }
0086 
0087 boolean inmem_empty_output_buffer(j_compress_ptr cinfo)
0088 {
0089     auto dest = (inmem_dest_mgr *)(cinfo->dest);
0090     dest->mOutput->resize(dest->mOutput->size() + INMEM_DST_DELTA);
0091     dest->next_output_byte = (JOCTET *)(dest->mOutput->data() + dest->mOutput->size() - INMEM_DST_DELTA);
0092     dest->free_in_buffer = INMEM_DST_DELTA;
0093 
0094     return true;
0095 }
0096 
0097 void inmem_term_destination(j_compress_ptr cinfo)
0098 {
0099     auto dest = (inmem_dest_mgr *)(cinfo->dest);
0100     int finalSize = dest->next_output_byte - (JOCTET *)(dest->mOutput->data());
0101     Q_ASSERT(finalSize >= 0);
0102     dest->mOutput->resize(finalSize);
0103 }
0104 
0105 //---------------------
0106 //
0107 // JpegContent::Private
0108 //
0109 //---------------------
0110 struct JpegContent::Private {
0111     // JpegContent usually stores the image pixels as compressed JPEG data in
0112     // mRawData. However if the image is set with setImage() because the user
0113     // performed a lossy image manipulation, mRawData is cleared and the image
0114     // pixels are kept in mImage until updateRawDataFromImage() is called.
0115     QImage mImage;
0116 
0117     // Store the input file, keep it open readOnly. This allows the file to be memory mapped
0118     // (i.e. mRawData may point to mFile.map()) rather than completely read on load. Postpone
0119     // QFile::readAll() as long as possible (currently in save()).
0120     QFile mFile;
0121     QByteArray mRawData;
0122 
0123     QSize mSize;
0124     QString mComment;
0125     bool mPendingTransformation;
0126     QTransform mTransformMatrix;
0127     Exiv2::ExifData mExifData;
0128     QString mErrorString;
0129 
0130     Private()
0131     {
0132         mPendingTransformation = false;
0133     }
0134 
0135     void setupInmemDestination(j_compress_ptr cinfo, QByteArray *outputData)
0136     {
0137         Q_ASSERT(!cinfo->dest);
0138         auto dest = (inmem_dest_mgr *)(*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_PERMANENT, sizeof(inmem_dest_mgr));
0139         cinfo->dest = (struct jpeg_destination_mgr *)(dest);
0140 
0141         dest->init_destination = inmem_init_destination;
0142         dest->empty_output_buffer = inmem_empty_output_buffer;
0143         dest->term_destination = inmem_term_destination;
0144 
0145         dest->mOutput = outputData;
0146     }
0147     bool readSize()
0148     {
0149         struct jpeg_decompress_struct srcinfo;
0150 
0151         // Init JPEG structs
0152         JPEGErrorManager errorManager;
0153 
0154         // Initialize the JPEG decompression object
0155         srcinfo.err = &errorManager;
0156         jpeg_create_decompress(&srcinfo);
0157         if (setjmp(errorManager.jmp_buffer)) {
0158             qCCritical(GWENVIEW_LIB_LOG) << "libjpeg fatal error\n";
0159             return false;
0160         }
0161 
0162         // Specify data source for decompression
0163         QBuffer buffer(&mRawData);
0164         buffer.open(QIODevice::ReadOnly);
0165         IODeviceJpegSourceManager::setup(&srcinfo, &buffer);
0166 
0167         // Read the header
0168         jcopy_markers_setup(&srcinfo, JCOPYOPT_ALL);
0169         int result = jpeg_read_header(&srcinfo, true);
0170         if (result != JPEG_HEADER_OK) {
0171             qCCritical(GWENVIEW_LIB_LOG) << "Could not read jpeg header\n";
0172             jpeg_destroy_decompress(&srcinfo);
0173             return false;
0174         }
0175         mSize = QSize(srcinfo.image_width, srcinfo.image_height);
0176 
0177         jpeg_destroy_decompress(&srcinfo);
0178         return true;
0179     }
0180 
0181     bool updateRawDataFromImage()
0182     {
0183         QBuffer buffer;
0184         QImageWriter writer(&buffer, "jpeg");
0185         writer.setQuality(GwenviewConfig::jPEGQuality());
0186         if (!writer.write(mImage)) {
0187             mErrorString = writer.errorString();
0188             return false;
0189         }
0190         mRawData = buffer.data();
0191         mImage = QImage();
0192         return true;
0193     }
0194 };
0195 
0196 //------------
0197 //
0198 // JpegContent
0199 //
0200 //------------
0201 JpegContent::JpegContent()
0202 {
0203     d = new JpegContent::Private();
0204 }
0205 
0206 JpegContent::~JpegContent()
0207 {
0208     delete d;
0209 }
0210 
0211 bool JpegContent::load(const QString &path)
0212 {
0213     if (d->mFile.isOpen()) {
0214         d->mFile.unmap(reinterpret_cast<unsigned char *>(d->mRawData.data()));
0215         d->mFile.close();
0216         d->mRawData.clear();
0217     }
0218 
0219     d->mFile.setFileName(path);
0220     if (!d->mFile.open(QIODevice::ReadOnly)) {
0221         qCCritical(GWENVIEW_LIB_LOG) << "Could not open '" << path << "' for reading\n";
0222         return false;
0223     }
0224 
0225     QByteArray rawData;
0226     uchar *mappedFile = d->mFile.map(0, d->mFile.size(), QFileDevice::MapPrivateOption);
0227     if (mappedFile == nullptr) {
0228         // process' mapping limit exceeded, file is sealed or filesystem doesn't support it, etc.
0229         qCDebug(GWENVIEW_LIB_LOG) << "Could not mmap '" << path << "', falling back to QFile::readAll()\n";
0230 
0231         rawData = d->mFile.readAll();
0232         // all read in, no need to keep it open
0233         d->mFile.close();
0234     } else {
0235         rawData = QByteArray::fromRawData(reinterpret_cast<char *>(mappedFile), d->mFile.size());
0236     }
0237 
0238     return loadFromData(rawData);
0239 }
0240 
0241 bool JpegContent::loadFromData(const QByteArray &data)
0242 {
0243     std::unique_ptr<Exiv2::Image> image;
0244     Exiv2ImageLoader loader;
0245     if (!loader.load(data)) {
0246         qCCritical(GWENVIEW_LIB_LOG) << "Could not load image with Exiv2, reported error:" << loader.errorMessage();
0247     }
0248     image.reset(loader.popImage().release());
0249 
0250     return loadFromData(data, image.get());
0251 }
0252 
0253 bool JpegContent::loadFromData(const QByteArray &data, Exiv2::Image *exiv2Image)
0254 {
0255     d->mPendingTransformation = false;
0256     d->mTransformMatrix.reset();
0257 
0258     d->mRawData = data;
0259     if (d->mRawData.size() == 0) {
0260         qCCritical(GWENVIEW_LIB_LOG) << "No data\n";
0261         return false;
0262     }
0263 
0264     if (!d->readSize())
0265         return false;
0266 
0267     d->mExifData = exiv2Image->exifData();
0268     d->mComment = QString::fromUtf8(exiv2Image->comment().c_str());
0269 
0270     if (!GwenviewConfig::applyExifOrientation()) {
0271         return true;
0272     }
0273 
0274     // Adjust the size according to the orientation
0275     switch (orientation()) {
0276     case TRANSPOSE:
0277     case ROT_90:
0278     case TRANSVERSE:
0279     case ROT_270:
0280         d->mSize.transpose();
0281         break;
0282     default:
0283         break;
0284     }
0285 
0286     return true;
0287 }
0288 
0289 QByteArray JpegContent::rawData() const
0290 {
0291     return d->mRawData;
0292 }
0293 
0294 Orientation JpegContent::orientation() const
0295 {
0296     Exiv2::ExifKey key("Exif.Image.Orientation");
0297     auto it = d->mExifData.findKey(key);
0298 
0299     // We do the same checks as in libexiv2's src/crwimage.cpp:
0300     // https://github.com/Exiv2/exiv2/blob/0d397b95c7b4a10819c0ea0f36fa20943e6a4ea5/src/crwimage.cpp#L1336
0301     if (it == d->mExifData.end() || it->count() == 0 || it->typeId() != Exiv2::unsignedShort) {
0302         return NOT_AVAILABLE;
0303     }
0304 #if EXIV2_TEST_VERSION(0, 28, 0)
0305     return Orientation(it->toUint32());
0306 #else
0307     return Orientation(it->toLong());
0308 #endif
0309 }
0310 
0311 int JpegContent::dotsPerMeterX() const
0312 {
0313     return dotsPerMeter(QStringLiteral("XResolution"));
0314 }
0315 
0316 int JpegContent::dotsPerMeterY() const
0317 {
0318     return dotsPerMeter(QStringLiteral("YResolution"));
0319 }
0320 
0321 int JpegContent::dotsPerMeter(const QString &keyName) const
0322 {
0323     Exiv2::ExifKey keyResUnit("Exif.Image.ResolutionUnit");
0324     auto it = d->mExifData.findKey(keyResUnit);
0325     if (it == d->mExifData.end()) {
0326         return 0;
0327     }
0328 #if EXIV2_TEST_VERSION(0, 28, 0)
0329     int res = it->toUint32();
0330 #else
0331     int res = it->toLong();
0332 #endif
0333     QString keyVal = QStringLiteral("Exif.Image.") + keyName;
0334     Exiv2::ExifKey keyResolution(keyVal.toLocal8Bit().data());
0335     it = d->mExifData.findKey(keyResolution);
0336     if (it == d->mExifData.end()) {
0337         return 0;
0338     }
0339     // The unit for measuring XResolution and YResolution. The same unit is used for both XResolution and YResolution.
0340     //     If the image resolution in unknown, 2 (inches) is designated.
0341     //         Default = 2
0342     //         2 = inches
0343     //         3 = centimeters
0344     //         Other = reserved
0345     const float INCHESPERMETER = (100. / 2.54);
0346     switch (res) {
0347     case 3: // dots per cm
0348 #if EXIV2_TEST_VERSION(0, 28, 0)
0349         return int(it->toUint32() * 100);
0350 #else
0351         return int(it->toLong() * 100);
0352 #endif
0353     default: // dots per inch
0354 #if EXIV2_TEST_VERSION(0, 28, 0)
0355         return int(it->toUint32() * INCHESPERMETER);
0356 #else
0357         return int(it->toLong() * INCHESPERMETER);
0358 #endif
0359     }
0360 
0361     return 0;
0362 }
0363 
0364 void JpegContent::resetOrientation()
0365 {
0366     Exiv2::ExifKey key("Exif.Image.Orientation");
0367     auto it = d->mExifData.findKey(key);
0368     if (it == d->mExifData.end()) {
0369         return;
0370     }
0371 
0372     *it = uint16_t(NORMAL);
0373 }
0374 
0375 QSize JpegContent::size() const
0376 {
0377     return d->mSize;
0378 }
0379 
0380 QString JpegContent::comment() const
0381 {
0382     return d->mComment;
0383 }
0384 
0385 void JpegContent::setComment(const QString &comment)
0386 {
0387     d->mComment = comment;
0388 }
0389 
0390 static QTransform createRotMatrix(int angle)
0391 {
0392     QTransform matrix;
0393     matrix.rotate(angle);
0394     return matrix;
0395 }
0396 
0397 static QTransform createScaleMatrix(int dx, int dy)
0398 {
0399     QTransform matrix;
0400     matrix.scale(dx, dy);
0401     return matrix;
0402 }
0403 
0404 struct OrientationInfo {
0405     OrientationInfo()
0406         : orientation(NOT_AVAILABLE)
0407         , jxform(JXFORM_NONE)
0408     {
0409     }
0410 
0411     OrientationInfo(Orientation o, const QTransform &m, JXFORM_CODE j)
0412         : orientation(o)
0413         , matrix(m)
0414         , jxform(j)
0415     {
0416     }
0417 
0418     Orientation orientation;
0419     QTransform matrix;
0420     JXFORM_CODE jxform;
0421 };
0422 using OrientationInfoList = QList<OrientationInfo>;
0423 
0424 static const OrientationInfoList &orientationInfoList()
0425 {
0426     static OrientationInfoList list;
0427     if (list.size() == 0) {
0428         QTransform rot90 = createRotMatrix(90);
0429         QTransform hflip = createScaleMatrix(-1, 1);
0430         QTransform vflip = createScaleMatrix(1, -1);
0431 
0432         list << OrientationInfo() << OrientationInfo(NORMAL, QTransform(), JXFORM_NONE) << OrientationInfo(HFLIP, hflip, JXFORM_FLIP_H)
0433              << OrientationInfo(ROT_180, createRotMatrix(180), JXFORM_ROT_180) << OrientationInfo(VFLIP, vflip, JXFORM_FLIP_V)
0434              << OrientationInfo(TRANSPOSE, hflip * rot90, JXFORM_TRANSPOSE) << OrientationInfo(ROT_90, rot90, JXFORM_ROT_90)
0435              << OrientationInfo(TRANSVERSE, vflip * rot90, JXFORM_TRANSVERSE) << OrientationInfo(ROT_270, createRotMatrix(270), JXFORM_ROT_270);
0436     }
0437     return list;
0438 }
0439 
0440 void JpegContent::transform(Orientation orientation)
0441 {
0442     if (orientation != NOT_AVAILABLE && orientation != NORMAL) {
0443         d->mPendingTransformation = true;
0444         OrientationInfoList::ConstIterator it(orientationInfoList().begin()), end(orientationInfoList().end());
0445         for (; it != end; ++it) {
0446             if ((*it).orientation == orientation) {
0447                 d->mTransformMatrix = (*it).matrix * d->mTransformMatrix;
0448                 break;
0449             }
0450         }
0451         if (it == end) {
0452             qCWarning(GWENVIEW_LIB_LOG) << "Could not find matrix for orientation\n";
0453         }
0454     }
0455 }
0456 
0457 #if 0
0458 static void dumpMatrix(const QTransform& matrix)
0459 {
0460     qCDebug(GWENVIEW_LIB_LOG) << "matrix | " << matrix.m11() << ", " << matrix.m12() << " |\n";
0461     qCDebug(GWENVIEW_LIB_LOG) << "       | " << matrix.m21() << ", " << matrix.m22() << " |\n";
0462     qCDebug(GWENVIEW_LIB_LOG) << "       ( " << matrix.dx()  << ", " << matrix.dy()  << " )\n";
0463 }
0464 #endif
0465 
0466 static bool matricesAreSame(const QTransform &m1, const QTransform &m2, double tolerance)
0467 {
0468     return fabs(m1.m11() - m2.m11()) < tolerance && fabs(m1.m12() - m2.m12()) < tolerance && fabs(m1.m21() - m2.m21()) < tolerance
0469         && fabs(m1.m22() - m2.m22()) < tolerance && fabs(m1.dx() - m2.dx()) < tolerance && fabs(m1.dy() - m2.dy()) < tolerance;
0470 }
0471 
0472 static JXFORM_CODE findJxform(const QTransform &matrix)
0473 {
0474     OrientationInfoList::ConstIterator it(orientationInfoList().begin()), end(orientationInfoList().end());
0475     for (; it != end; ++it) {
0476         if (matricesAreSame((*it).matrix, matrix, 0.001)) {
0477             return (*it).jxform;
0478         }
0479     }
0480     qCWarning(GWENVIEW_LIB_LOG) << "findJxform: failed\n";
0481     return JXFORM_NONE;
0482 }
0483 
0484 void JpegContent::applyPendingTransformation()
0485 {
0486     if (d->mRawData.size() == 0) {
0487         qCCritical(GWENVIEW_LIB_LOG) << "No data loaded\n";
0488         return;
0489     }
0490 
0491     // The following code is inspired by jpegtran.c from the libjpeg
0492 
0493     // Init JPEG structs
0494     struct jpeg_decompress_struct srcinfo;
0495     struct jpeg_compress_struct dstinfo;
0496     jvirt_barray_ptr *src_coef_arrays;
0497     jvirt_barray_ptr *dst_coef_arrays;
0498 
0499     // Initialize the JPEG decompression object
0500     JPEGErrorManager srcErrorManager;
0501     srcinfo.err = &srcErrorManager;
0502     jpeg_create_decompress(&srcinfo);
0503     if (setjmp(srcErrorManager.jmp_buffer)) {
0504         qCCritical(GWENVIEW_LIB_LOG) << "libjpeg error in src\n";
0505         return;
0506     }
0507 
0508     // Initialize the JPEG compression object
0509     JPEGErrorManager dstErrorManager;
0510     dstinfo.err = &dstErrorManager;
0511     jpeg_create_compress(&dstinfo);
0512     if (setjmp(dstErrorManager.jmp_buffer)) {
0513         qCCritical(GWENVIEW_LIB_LOG) << "libjpeg error in dst\n";
0514         return;
0515     }
0516 
0517     // Specify data source for decompression
0518     QBuffer buffer(&d->mRawData);
0519     buffer.open(QIODevice::ReadOnly);
0520     IODeviceJpegSourceManager::setup(&srcinfo, &buffer);
0521 
0522     // Enable saving of extra markers that we want to copy
0523     jcopy_markers_setup(&srcinfo, JCOPYOPT_ALL);
0524 
0525     (void)jpeg_read_header(&srcinfo, true);
0526 
0527     // Init transformation
0528     jpeg_transform_info transformoption;
0529     memset(&transformoption, 0, sizeof(jpeg_transform_info));
0530     transformoption.transform = findJxform(d->mTransformMatrix);
0531     jtransform_request_workspace(&srcinfo, &transformoption);
0532 
0533     /* Read source file as DCT coefficients */
0534     src_coef_arrays = jpeg_read_coefficients(&srcinfo);
0535 
0536     /* Initialize destination compression parameters from source values */
0537     jpeg_copy_critical_parameters(&srcinfo, &dstinfo);
0538 
0539     /* Adjust destination parameters if required by transform options;
0540      * also find out which set of coefficient arrays will hold the output.
0541      */
0542     dst_coef_arrays = jtransform_adjust_parameters(&srcinfo, &dstinfo, src_coef_arrays, &transformoption);
0543 
0544     /* Specify data destination for compression */
0545     QByteArray output;
0546     output.resize(d->mRawData.size());
0547     d->setupInmemDestination(&dstinfo, &output);
0548 
0549     /* Start compressor (note no image data is actually written here) */
0550     jpeg_write_coefficients(&dstinfo, dst_coef_arrays);
0551 
0552     /* Copy to the output file any extra markers that we want to preserve */
0553     jcopy_markers_execute(&srcinfo, &dstinfo, JCOPYOPT_ALL);
0554 
0555     /* Execute image transformation, if any */
0556     jtransform_execute_transformation(&srcinfo, &dstinfo, src_coef_arrays, &transformoption);
0557 
0558     /* Finish compression and release memory */
0559     jpeg_finish_compress(&dstinfo);
0560     jpeg_destroy_compress(&dstinfo);
0561     (void)jpeg_finish_decompress(&srcinfo);
0562     jpeg_destroy_decompress(&srcinfo);
0563 
0564     // Set rawData to our new JPEG
0565     d->mRawData = output;
0566 }
0567 
0568 QImage JpegContent::thumbnail() const
0569 {
0570     QImage image;
0571     if (!d->mExifData.empty()) {
0572 #if (EXIV2_TEST_VERSION(0, 17, 91))
0573         Exiv2::ExifThumbC thumb(d->mExifData);
0574         Exiv2::DataBuf thumbnail = thumb.copy();
0575 #else
0576         Exiv2::DataBuf thumbnail = d->mExifData.copyThumbnail();
0577 #endif
0578 #if (EXIV2_TEST_VERSION(0, 28, 0))
0579         image.loadFromData(thumbnail.data(), thumbnail.size());
0580 #else
0581         image.loadFromData(thumbnail.pData_, thumbnail.size_);
0582 #endif
0583 
0584         auto it = d->mExifData.findKey(Exiv2::ExifKey("Exif.Canon.ThumbnailImageValidArea"));
0585         // ensure ThumbnailImageValidArea actually specifies a rectangle, i.e. there must be 4 coordinates
0586         if (it != d->mExifData.end() && it->count() == 4) {
0587 #if EXIV2_TEST_VERSION(0, 28, 0)
0588             QRect validArea(QPoint(it->toUint32(0), it->toUint32(2)), QPoint(it->toUint32(1), it->toUint32(3)));
0589 #else
0590             QRect validArea(QPoint(it->toLong(0), it->toLong(2)), QPoint(it->toLong(1), it->toLong(3)));
0591 #endif
0592             image = image.copy(validArea);
0593         } else {
0594             // Unfortunately, Sony does not provide an exif tag that specifies the valid area of the
0595             // embedded thumbnail. Need to derive it from the size of the preview image instead.
0596             it = d->mExifData.findKey(Exiv2::ExifKey("Exif.Sony1.PreviewImageSize"));
0597             if (it != d->mExifData.end() && it->count() == 2) {
0598 #if EXIV2_TEST_VERSION(0, 28, 0)
0599                 const long prevHeight = it->toUint32(0);
0600                 const long prevWidth = it->toUint32(1);
0601 #else
0602                 const long prevHeight = it->toLong(0);
0603                 const long prevWidth = it->toLong(1);
0604 #endif
0605 
0606                 if (image.width() > 0 && prevWidth > 0) {
0607                     const double scale = prevWidth / image.width();
0608 
0609                     // the embedded thumb only needs to be cropped vertically
0610                     const long validThumbAreaHeight = ceil(prevHeight / scale);
0611                     const long totalHeightOfBlackArea = image.height() - validThumbAreaHeight;
0612                     // black bars on top and bottom should be equal in height
0613                     const long offsetFromTop = totalHeightOfBlackArea / 2;
0614 
0615                     const QRect validArea(QPoint(0, offsetFromTop), QSize(image.width(), validThumbAreaHeight));
0616                     image = image.copy(validArea);
0617                 }
0618             }
0619         }
0620 
0621         Orientation o = orientation();
0622         if (GwenviewConfig::applyExifOrientation() && o != NORMAL && o != NOT_AVAILABLE) {
0623             image = image.transformed(ImageUtils::transformMatrix(o));
0624         }
0625     }
0626     return image;
0627 }
0628 
0629 void JpegContent::setThumbnail(const QImage &thumbnail)
0630 {
0631     if (d->mExifData.empty()) {
0632         return;
0633     }
0634 
0635     QByteArray array;
0636     QBuffer buffer(&array);
0637     buffer.open(QIODevice::WriteOnly);
0638     QImageWriter writer(&buffer, "JPEG");
0639     if (!writer.write(thumbnail)) {
0640         qCCritical(GWENVIEW_LIB_LOG) << "Could not write thumbnail\n";
0641         return;
0642     }
0643 
0644 #if (EXIV2_TEST_VERSION(0, 17, 91))
0645     Exiv2::ExifThumb thumb(d->mExifData);
0646     thumb.setJpegThumbnail((unsigned char *)array.data(), array.size());
0647 #else
0648     d->mExifData.setJpegThumbnail((unsigned char *)array.data(), array.size());
0649 #endif
0650 }
0651 
0652 bool JpegContent::save(const QString &path)
0653 {
0654     // we need to take ownership of the input file's data
0655     // if the input file is still open, data is still only mem-mapped
0656     if (d->mFile.isOpen()) {
0657         // backup the mmap() pointer
0658         auto mappedFile = reinterpret_cast<unsigned char *>(d->mRawData.data());
0659         // read the file to memory
0660         d->mRawData = d->mFile.readAll();
0661         d->mFile.unmap(mappedFile);
0662         d->mFile.close();
0663     }
0664 
0665     QFile file(path);
0666     if (!file.open(QIODevice::WriteOnly)) {
0667         d->mErrorString = i18nc("@info", "Could not open file for writing.");
0668         return false;
0669     }
0670 
0671     return save(&file);
0672 }
0673 
0674 bool JpegContent::save(QIODevice *device)
0675 {
0676     if (!d->mImage.isNull()) {
0677         if (!d->updateRawDataFromImage()) {
0678             return false;
0679         }
0680     }
0681 
0682     if (d->mRawData.size() == 0) {
0683         d->mErrorString = i18nc("@info", "No data to store.");
0684         return false;
0685     }
0686 
0687     if (d->mPendingTransformation) {
0688         applyPendingTransformation();
0689         d->mPendingTransformation = false;
0690     }
0691 
0692     std::unique_ptr<Exiv2::Image> image;
0693     image.reset(Exiv2::ImageFactory::open((unsigned char *)d->mRawData.data(), d->mRawData.size()).release());
0694 
0695     // Store Exif info
0696     image->setExifData(d->mExifData);
0697     image->setComment(d->mComment.toUtf8().toStdString());
0698     image->writeMetadata();
0699 
0700     // Update mRawData
0701     Exiv2::BasicIo &io = image->io();
0702     d->mRawData.resize(io.size());
0703     io.read((unsigned char *)d->mRawData.data(), io.size());
0704 
0705     QDataStream stream(device);
0706     stream.writeRawData(d->mRawData.data(), d->mRawData.size());
0707 
0708     // Make sure we are up to date
0709     loadFromData(d->mRawData);
0710     return true;
0711 }
0712 
0713 QString JpegContent::errorString() const
0714 {
0715     return d->mErrorString;
0716 }
0717 
0718 void JpegContent::setImage(const QImage &image)
0719 {
0720     d->mRawData.clear();
0721     d->mImage = image;
0722     d->mSize = image.size();
0723     d->mExifData["Exif.Photo.PixelXDimension"] = image.width();
0724     d->mExifData["Exif.Photo.PixelYDimension"] = image.height();
0725     resetOrientation();
0726 
0727     d->mPendingTransformation = false;
0728     d->mTransformMatrix = QTransform();
0729 }
0730 
0731 } // namespace