732 lines
22 KiB
C++

// vim: set tabstop=4 shiftwidth=4 expandtab:
/*
Gwenview: an image viewer
Copyright 2007 Aurélien Gâteau <agateau@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "jpegcontent.h"
// System
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
extern "C" {
#include <jpeglib.h> // Must be included before transupp.h
#include "transupp.h"
}
// Qt
#include <QBuffer>
#include <QFile>
#include <QImage>
#include <QImageWriter>
#include <QTransform>
// KF
#include <KLocalizedString>
// Exiv2
#include <exiv2/exiv2.hpp>
// Local
#include "exiv2imageloader.h"
#include "gwenview_lib_debug.h"
#include "gwenviewconfig.h"
#include "imageutils.h"
#include "iodevicejpegsourcemanager.h"
#include "jpegerrormanager.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()
{
qCDebug(GWENVIEW_LIB_LOG) << "dest_mgr:\n";
qCDebug(GWENVIEW_LIB_LOG) << "- next_output_byte: " << next_output_byte;
qCDebug(GWENVIEW_LIB_LOG) << "- free_in_buffer: " << free_in_buffer;
qCDebug(GWENVIEW_LIB_LOG) << "- output size: " << mOutput->size();
}
};
void inmem_init_destination(j_compress_ptr cinfo)
{
auto 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)
{
auto 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)
{
auto 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);
auto 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)) {
qCCritical(GWENVIEW_LIB_LOG) << "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) {
qCCritical(GWENVIEW_LIB_LOG) << "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<unsigned char *>(d->mRawData.data()));
d->mFile.close();
d->mRawData.clear();
}
d->mFile.setFileName(path);
if (!d->mFile.open(QIODevice::ReadOnly)) {
qCCritical(GWENVIEW_LIB_LOG) << "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.
qCDebug(GWENVIEW_LIB_LOG) << "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<char *>(mappedFile), d->mFile.size());
}
return loadFromData(rawData);
}
bool JpegContent::loadFromData(const QByteArray &data)
{
std::unique_ptr<Exiv2::Image> image;
Exiv2ImageLoader loader;
if (!loader.load(data)) {
qCCritical(GWENVIEW_LIB_LOG) << "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) {
qCCritical(GWENVIEW_LIB_LOG) << "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");
auto it = d->mExifData.findKey(key);
// We do the same checks as in libexiv2's src/crwimage.cpp:
// https://github.com/Exiv2/exiv2/blob/0d397b95c7b4a10819c0ea0f36fa20943e6a4ea5/src/crwimage.cpp#L1336
if (it == d->mExifData.end() || it->count() == 0 || it->typeId() != Exiv2::unsignedShort) {
return NOT_AVAILABLE;
}
#if EXIV2_TEST_VERSION(0, 28, 0)
return Orientation(it->toUint32());
#else
return Orientation(it->toLong());
#endif
}
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");
auto it = d->mExifData.findKey(keyResUnit);
if (it == d->mExifData.end()) {
return 0;
}
#if EXIV2_TEST_VERSION(0, 28, 0)
int res = it->toUint32();
#else
int res = it->toLong();
#endif
QString keyVal = QStringLiteral("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
#if EXIV2_TEST_VERSION(0, 28, 0)
return int(it->toUint32() * 100);
#else
return int(it->toLong() * 100);
#endif
default: // dots per inch
#if EXIV2_TEST_VERSION(0, 28, 0)
return int(it->toUint32() * INCHESPERMETER);
#else
return int(it->toLong() * INCHESPERMETER);
#endif
}
return 0;
}
void JpegContent::resetOrientation()
{
Exiv2::ExifKey key("Exif.Image.Orientation");
auto 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;
};
using OrientationInfoList = QList<OrientationInfo>;
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) {
qCWarning(GWENVIEW_LIB_LOG) << "Could not find matrix for orientation\n";
}
}
}
#if 0
static void dumpMatrix(const QTransform& matrix)
{
qCDebug(GWENVIEW_LIB_LOG) << "matrix | " << matrix.m11() << ", " << matrix.m12() << " |\n";
qCDebug(GWENVIEW_LIB_LOG) << " | " << matrix.m21() << ", " << matrix.m22() << " |\n";
qCDebug(GWENVIEW_LIB_LOG) << " ( " << 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;
}
}
qCWarning(GWENVIEW_LIB_LOG) << "findJxform: failed\n";
return JXFORM_NONE;
}
void JpegContent::applyPendingTransformation()
{
if (d->mRawData.size() == 0) {
qCCritical(GWENVIEW_LIB_LOG) << "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)) {
qCCritical(GWENVIEW_LIB_LOG) << "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)) {
qCCritical(GWENVIEW_LIB_LOG) << "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
#if (EXIV2_TEST_VERSION(0, 28, 0))
image.loadFromData(thumbnail.data(), thumbnail.size());
#else
image.loadFromData(thumbnail.pData_, thumbnail.size_);
#endif
auto 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) {
#if EXIV2_TEST_VERSION(0, 28, 0)
QRect validArea(QPoint(it->toUint32(0), it->toUint32(2)), QPoint(it->toUint32(1), it->toUint32(3)));
#else
QRect validArea(QPoint(it->toLong(0), it->toLong(2)), QPoint(it->toLong(1), it->toLong(3)));
#endif
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) {
#if EXIV2_TEST_VERSION(0, 28, 0)
const long prevHeight = it->toUint32(0);
const long prevWidth = it->toUint32(1);
#else
const long prevHeight = it->toLong(0);
const long prevWidth = it->toLong(1);
#endif
if (image.width() > 0 && prevWidth > 0) {
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)) {
qCCritical(GWENVIEW_LIB_LOG) << "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<unsigned char *>(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<Exiv2::Image> 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