Optionally apply orientation on QImage read

Make it possible to read images with EXIF orientation automatically
applied. This was originally implemented without opt-out in Qt 5.4, but
reverted. Here it is implemented as opt-in for JPEG, and opt-out for TIFF
to keep behavioral consistency.

The EXIF support for JPEG was written by Rainer Keller.

[ChangeLog][QtGui][Image plugins] An option has been added to
QImageReader to enable automatic application of EXIF orientation.
This behavior was default in Qt 5.4.1, but reverted in Qt 5.4.2.

Task-number: QTBUG-37946
Task-number: QTBUG-43563
Task-number: QTBUG-45552
Task-number: QTBUG-45865
Change-Id: Iaafd2519b63ede66ecc1f8aa4c7118081312b8f5
Reviewed-by: Gunnar Sletta <gunnar@sletta.org>
This commit is contained in:
Allan Sandfeld Jensen 2015-05-04 13:32:32 +02:00
parent 12dd4ff7e7
commit ba323b04cd
19 changed files with 388 additions and 4 deletions

View File

@ -5048,4 +5048,18 @@ QImage::Format QImage::toImageFormat(QPixelFormat format) Q_DECL_NOTHROW
return Format_Invalid;
}
Q_GUI_EXPORT void qt_imageTransform(QImage &src, QImageIOHandler::Transformations orient)
{
if (orient == QImageIOHandler::TransformationNone)
return;
if (orient == QImageIOHandler::TransformationRotate270) {
src = rotated270(src);
} else {
src = qMove(src).mirrored(orient & QImageIOHandler::TransformationMirror,
orient & QImageIOHandler::TransformationFlip);
if (orient & QImageIOHandler::TransformationRotate90)
src = rotated90(src);
}
}
QT_END_NAMESPACE

View File

@ -159,6 +159,43 @@
\value ProgressiveScanWrite. A handler which supports
this option is expected to write the image as a progressive scan image.
\value ImageTransformation. A handler which supports this option can read
the transformation metadata of an image. A handler that supports this option
should not apply the transformation itself.
\value TransformedByDefault. A handler that reports support for this feature
will have image transformation metadata applied by default on read.
*/
/*! \enum QImageIOHandler::Transformation
\since 5.5
This enum describes the different transformations or orientations
supported by some image formats, usually through EXIF.
\value TransformationNone No transformation should be applied.
\value TransformationMirror Mirror the image horizontally.
\value TransformationFlip Mirror the image vertically.
\value TransformationRotate180 Rotate the image 180 degrees.
This is the same as mirroring it both horizontally and vertically.
\value TransformationRotate90 Rotate the image 90 degrees.
\value TransformationMirrorAndRotate90 Mirror the image horizontally
and then rotate it 90 degrees.
\value TransformationFlipAndRotate90 Mirror the image vertically
and then rotate it 90 degrees.
\value TransformationRotate270 Rotate the image 270 degrees.
This is the same as mirroring it both horizontally, vertically and
then rotating it 90 degrees.
\sa QImageReader::transformation(), QImageReader::setAutoTransform(), QImageWriter::setTransformation()
*/
/*!

View File

@ -86,8 +86,25 @@ public:
ImageFormat,
SupportedSubTypes,
OptimizedWrite,
ProgressiveScanWrite
ProgressiveScanWrite,
ImageTransformation
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
, TransformedByDefault
#endif
};
enum Transformation {
TransformationNone = 0,
TransformationMirror = 1,
TransformationFlip = 2,
TransformationRotate180 = TransformationMirror | TransformationFlip,
TransformationRotate90 = 4,
TransformationMirrorAndRotate90 = TransformationMirror | TransformationRotate90,
TransformationFlipAndRotate90 = TransformationFlip | TransformationRotate90,
TransformationRotate270 = TransformationRotate180 | TransformationRotate90
};
Q_DECLARE_FLAGS(Transformations, Transformation)
virtual QVariant option(ImageOption option) const;
virtual void setOption(ImageOption option, const QVariant &value);
virtual bool supportsOption(ImageOption option) const;

View File

@ -533,6 +533,11 @@ public:
int quality;
QMap<QString, QString> text;
void getText();
enum {
UsePluginDefault,
ApplyTransform,
DoNotApplyTransform
} autoTransform;
// error
QImageReader::ImageReaderError imageReaderError;
@ -552,6 +557,7 @@ QImageReaderPrivate::QImageReaderPrivate(QImageReader *qq)
handler = 0;
quality = -1;
imageReaderError = QImageReader::UnknownError;
autoTransform = UsePluginDefault;
q = qq;
}
@ -1143,6 +1149,59 @@ QList<QByteArray> QImageReader::supportedSubTypes() const
return QList<QByteArray>();
}
/*!
\since 5.5
Returns the transformation metadata of the image, including image orientation. If the format
does not support transformation metadata \c QImageIOHandler::Transformation_None is returned.
\sa setAutoTransform(), autoTransform()
*/
QImageIOHandler::Transformations QImageReader::transformation() const
{
int option = QImageIOHandler::TransformationNone;
if (d->initHandler() && d->handler->supportsOption(QImageIOHandler::ImageTransformation))
option = d->handler->option(QImageIOHandler::ImageTransformation).toInt();
return QImageIOHandler::Transformations(option);
}
/*!
\since 5.5
Sets if images returned by read() should have transformation metadata automatically applied.
\sa autoTransform(), transform(), read()
*/
void QImageReader::setAutoTransform(bool enabled)
{
d->autoTransform = enabled ? QImageReaderPrivate::ApplyTransform
: QImageReaderPrivate::DoNotApplyTransform;
}
/*!
\since 5.5
Returns \c true if the image handler will apply transformation metadata on read().
\sa setAutoTransform(), transformation(), read()
*/
bool QImageReader::autoTransform() const
{
switch (d->autoTransform) {
case QImageReaderPrivate::ApplyTransform:
return true;
case QImageReaderPrivate::DoNotApplyTransform:
return false;
case QImageReaderPrivate::UsePluginDefault:
if (d->initHandler())
return d->handler->supportsOption(QImageIOHandler::TransformedByDefault);
// no break
default:
break;
}
return false;
}
/*!
Returns \c true if an image can be read for the device (i.e., the
image format is supported, and the device seems to contain valid
@ -1185,6 +1244,8 @@ QImage QImageReader::read()
return read(&image) ? image : QImage();
}
extern void qt_imageTransform(QImage &src, QImageIOHandler::Transformations orient);
/*!
\overload
@ -1294,6 +1355,8 @@ bool QImageReader::read(QImage *image)
if (!disable2xImageLoading && QFileInfo(fileName()).baseName().endsWith(QLatin1String("@2x"))) {
image->setDevicePixelRatio(2.0);
}
if (autoTransform())
qt_imageTransform(*image, transformation());
return true;
}

View File

@ -105,6 +105,11 @@ public:
bool supportsAnimation() const;
QImageIOHandler::Transformations transformation() const;
void setAutoTransform(bool enabled);
bool autoTransform() const;
QByteArray subType() const;
QList<QByteArray> supportedSubTypes() const;

View File

@ -91,6 +91,7 @@
#include <qbytearray.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qimage.h>
#include <qimageiohandler.h>
#include <qjsonarray.h>
#include <qset.h>
@ -254,6 +255,7 @@ public:
QByteArray subType;
bool optimizedWrite;
bool progressiveScanWrite;
QImageIOHandler::Transformations transformation;
// error
QImageWriter::ImageWriterError imageWriterError;
@ -277,6 +279,7 @@ QImageWriterPrivate::QImageWriterPrivate(QImageWriter *qq)
progressiveScanWrite = false;
imageWriterError = QImageWriter::UnknownError;
errorString = QImageWriter::tr("Unknown error");
transformation = QImageIOHandler::TransformationNone;
q = qq;
}
@ -615,6 +618,33 @@ bool QImageWriter::progressiveScanWrite() const
return d->progressiveScanWrite;
}
/*!
\since 5.5
Sets the image transformations metadata including orientation.
If transformation metadata is not supported by the image format,
the transform is applied before writing.
\sa transformation(), write()
*/
void QImageWriter::setTransformation(QImageIOHandler::Transformations transform)
{
d->transformation = transform;
}
/*!
\since 5.5
Returns the transformation and orientation the image has been set to written with.
\sa setTransformation()
*/
QImageIOHandler::Transformations QImageWriter::transformation() const
{
return d->transformation;
}
/*!
\obsolete
@ -694,6 +724,8 @@ bool QImageWriter::canWrite() const
return d->canWriteHelper();
}
extern void qt_imageTransform(QImage &src, QImageIOHandler::Transformations orient);
/*!
Writes the image \a image to the assigned device or file
name. Returns \c true on success; otherwise returns \c false. If the
@ -708,6 +740,7 @@ bool QImageWriter::write(const QImage &image)
if (!canWrite())
return false;
QImage img = image;
if (d->handler->supportsOption(QImageIOHandler::Quality))
d->handler->setOption(QImageIOHandler::Quality, d->quality);
if (d->handler->supportsOption(QImageIOHandler::CompressionRatio))
@ -722,8 +755,12 @@ bool QImageWriter::write(const QImage &image)
d->handler->setOption(QImageIOHandler::OptimizedWrite, d->optimizedWrite);
if (d->handler->supportsOption(QImageIOHandler::ProgressiveScanWrite))
d->handler->setOption(QImageIOHandler::ProgressiveScanWrite, d->progressiveScanWrite);
if (d->handler->supportsOption(QImageIOHandler::ImageTransformation))
d->handler->setOption(QImageIOHandler::ImageTransformation, int(d->transformation));
else
qt_imageTransform(img, d->transformation);
if (!d->handler->write(image))
if (!d->handler->write(img))
return false;
if (QFile *file = qobject_cast<QFile *>(d->device))
file->flush();

View File

@ -89,6 +89,9 @@ public:
void setProgressiveScanWrite(bool progressive);
bool progressiveScanWrite() const;
QImageIOHandler::Transformations transformation() const;
void setTransformation(QImageIOHandler::Transformations orientation);
// Obsolete as of 4.1
void setDescription(const QString &description);
QString description() const;

View File

@ -714,7 +714,7 @@ public:
};
QJpegHandlerPrivate(QJpegHandler *qq)
: quality(75), iod_src(0),
: quality(75), transformation(QImageIOHandler::TransformationNone), iod_src(0),
rgb888ToRgb32ConverterPtr(qt_convert_rgb888_to_rgb32), state(Ready), optimize(false), progressive(false), q(qq)
{}
@ -732,6 +732,7 @@ public:
bool read(QImage *image);
int quality;
QImageIOHandler::Transformations transformation;
QVariant size;
QImage::Format format;
QSize scaledSize;
@ -754,6 +755,122 @@ public:
QJpegHandler *q;
};
static bool readExifHeader(QDataStream &stream)
{
char prefix[6];
if (stream.readRawData(prefix, sizeof(prefix)) != sizeof(prefix))
return false;
static const char exifMagic[6] = {'E', 'x', 'i', 'f', 0, 0};
return memcmp(prefix, exifMagic, 6) == 0;
}
/*
* Returns -1 on error
* Returns 0 if no Exif orientation was found
* Returns 1 orientation is horizontal (normal)
* Returns 2 mirror horizontal
* Returns 3 rotate 180
* Returns 4 mirror vertical
* Returns 5 mirror horizontal and rotate 270 CCW
* Returns 6 rotate 90 CW
* Returns 7 mirror horizontal and rotate 90 CW
* Returns 8 rotate 270 CW
*/
static int getExifOrientation(QByteArray &exifData)
{
QDataStream stream(&exifData, QIODevice::ReadOnly);
if (!readExifHeader(stream))
return -1;
quint16 val;
quint32 offset;
const qint64 headerStart = stream.device()->pos();
// read byte order marker
stream >> val;
if (val == 0x4949) // 'II' == Intel
stream.setByteOrder(QDataStream::LittleEndian);
else if (val == 0x4d4d) // 'MM' == Motorola
stream.setByteOrder(QDataStream::BigEndian);
else
return -1; // unknown byte order
// read size
stream >> val;
if (val != 0x2a)
return -1;
stream >> offset;
// read IFD
while (!stream.atEnd()) {
quint16 numEntries;
// skip offset bytes to get the next IFD
const qint64 bytesToSkip = offset - (stream.device()->pos() - headerStart);
if (stream.skipRawData(bytesToSkip) != bytesToSkip)
return -1;
stream >> numEntries;
for (; numEntries > 0; --numEntries) {
quint16 tag;
quint16 type;
quint32 components;
quint16 value;
quint16 dummy;
stream >> tag >> type >> components >> value >> dummy;
if (tag == 0x0112) { // Tag Exif.Image.Orientation
if (components != 1)
return -1;
if (type != 3) // we are expecting it to be an unsigned short
return -1;
if (value < 1 || value > 8) // check for valid range
return -1;
// It is possible to include the orientation multiple times.
// Right now the first value is returned.
return value;
}
}
// read offset to next IFD
stream >> offset;
if (offset == 0) // this is the last IFD
break;
}
// No Exif orientation was found
return 0;
}
static QImageIOHandler::Transformations exif2Qt(int exifOrientation)
{
switch (exifOrientation) {
case 1: // normal
return QImageIOHandler::TransformationNone;
case 2: // mirror horizontal
return QImageIOHandler::TransformationMirror;
case 3: // rotate 180
return QImageIOHandler::TransformationRotate180;
case 4: // mirror vertical
return QImageIOHandler::TransformationFlip;
case 5: // mirror horizontal and rotate 270 CW
return QImageIOHandler::TransformationFlipAndRotate90;
case 6: // rotate 90 CW
return QImageIOHandler::TransformationRotate90;
case 7: // mirror horizontal and rotate 90 CW
return QImageIOHandler::TransformationMirrorAndRotate90;
case 8: // rotate 270 CW
return QImageIOHandler::TransformationRotate270;
}
qWarning("Invalid EXIF orientation");
return QImageIOHandler::TransformationNone;
}
/*!
\internal
*/
@ -773,6 +890,7 @@ bool QJpegHandlerPrivate::readJpegHeader(QIODevice *device)
if (!setjmp(err.setjmp_buffer)) {
jpeg_save_markers(&info, JPEG_COM, 0xFFFF);
jpeg_save_markers(&info, JPEG_APP0 + 1, 0xFFFF); // Exif uses APP1 marker
(void) jpeg_read_header(&info, TRUE);
@ -784,6 +902,8 @@ bool QJpegHandlerPrivate::readJpegHeader(QIODevice *device)
format = QImage::Format_Invalid;
read_jpeg_format(format, &info);
QByteArray exifData;
for (jpeg_saved_marker_ptr marker = info.marker_list; marker != NULL; marker = marker->next) {
if (marker->marker == JPEG_COM) {
QString key, value;
@ -801,9 +921,20 @@ bool QJpegHandlerPrivate::readJpegHeader(QIODevice *device)
description += key + QLatin1String(": ") + value.simplified();
readTexts.append(key);
readTexts.append(value);
} else if (marker->marker == JPEG_APP0 + 1) {
exifData.append((const char*)marker->data, marker->data_length);
}
}
if (!exifData.isEmpty()) {
// Exif data present
int exifOrientation = getExifOrientation(exifData);
if (exifOrientation == -1)
return false;
if (exifOrientation > 0)
transformation = exif2Qt(exifOrientation);
}
state = ReadHeader;
return true;
}
@ -905,8 +1036,16 @@ bool QJpegHandler::read(QImage *image)
return d->read(image);
}
extern void qt_imageTransform(QImage &src, QImageIOHandler::Transformations orient);
bool QJpegHandler::write(const QImage &image)
{
if (d->transformation != QImageIOHandler::TransformationNone) {
// We don't support writing EXIF headers so apply the transform to the data.
QImage img = image;
qt_imageTransform(img, d->transformation);
return write_jpeg_image(img, device(), d->quality, d->description, d->optimize, d->progressive);
}
return write_jpeg_image(image, device(), d->quality, d->description, d->optimize, d->progressive);
}
@ -920,7 +1059,8 @@ bool QJpegHandler::supportsOption(ImageOption option) const
|| option == Size
|| option == ImageFormat
|| option == OptimizedWrite
|| option == ProgressiveScanWrite;
|| option == ProgressiveScanWrite
|| option == ImageTransformation;
}
QVariant QJpegHandler::option(ImageOption option) const
@ -947,6 +1087,9 @@ QVariant QJpegHandler::option(ImageOption option) const
return d->optimize;
case ProgressiveScanWrite:
return d->progressive;
case ImageTransformation:
d->readJpegHeader(device());
return int(d->transformation);
default:
break;
}
@ -978,6 +1121,11 @@ void QJpegHandler::setOption(ImageOption option, const QVariant &value)
case ProgressiveScanWrite:
d->progressive = value.toBool();
break;
case ImageTransformation: {
int transformation = value.toInt();
if (transformation > 0 && transformation < 8)
d->transformation = QImageIOHandler::Transformations(transformation);
}
default:
break;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

View File

@ -181,6 +181,11 @@ private slots:
void invertPixelsRGB_data();
void invertPixelsRGB();
void exifOrientation_data();
void exifOrientation();
void exif_QTBUG45865();
void cleanupFunctions();
void devicePixelRatio();
@ -2810,6 +2815,61 @@ void tst_QImage::invertPixelsRGB()
QCOMPARE(qBlue(pixel) >> 4, (255 - 96) >> 4);
}
void tst_QImage::exifOrientation_data()
{
QTest::addColumn<QString>("fileName");
QTest::addColumn<int>("orientation");
QTest::newRow("Orientation 1, Intel format") << m_prefix + "jpeg_exif_orientation_value_1.jpg" << (int)QImageIOHandler::TransformationNone;
QTest::newRow("Orientation 2, Intel format") << m_prefix + "jpeg_exif_orientation_value_2.jpg" << (int)QImageIOHandler::TransformationMirror;
QTest::newRow("Orientation 3, Intel format") << m_prefix + "jpeg_exif_orientation_value_3.jpg" << (int)QImageIOHandler::TransformationRotate180;
QTest::newRow("Orientation 4, Intel format") << m_prefix + "jpeg_exif_orientation_value_4.jpg" << (int)QImageIOHandler::TransformationFlip;
QTest::newRow("Orientation 5, Intel format") << m_prefix + "jpeg_exif_orientation_value_5.jpg" << (int)QImageIOHandler::TransformationFlipAndRotate90;
QTest::newRow("Orientation 6, Intel format") << m_prefix + "jpeg_exif_orientation_value_6.jpg" << (int)QImageIOHandler::TransformationRotate90;
QTest::newRow("Orientation 6, Motorola format") << m_prefix + "jpeg_exif_orientation_value_6_motorola.jpg" << (int)QImageIOHandler::TransformationRotate90;
QTest::newRow("Orientation 7, Intel format") << m_prefix + "jpeg_exif_orientation_value_7.jpg" << (int)QImageIOHandler::TransformationMirrorAndRotate90;
QTest::newRow("Orientation 8, Intel format") << m_prefix + "jpeg_exif_orientation_value_8.jpg" << (int)QImageIOHandler::TransformationRotate270;
}
QT_BEGIN_NAMESPACE
extern void qt_imageTransform(QImage &src, QImageIOHandler::Transformations orient);
QT_END_NAMESPACE
QT_USE_NAMESPACE
void tst_QImage::exifOrientation()
{
QFETCH(QString, fileName);
QFETCH(int, orientation);
QImageReader imageReader(fileName);
imageReader.setAutoTransform(true);
QCOMPARE(imageReader.transformation(), orientation);
QImage img = imageReader.read();
QRgb px;
QVERIFY(!img.isNull());
px = img.pixel(0, 0);
QVERIFY(qRed(px) > 250 && qGreen(px) < 5 && qBlue(px) < 5);
px = img.pixel(img.width() - 1, 0);
QVERIFY(qRed(px) < 5 && qGreen(px) < 5 && qBlue(px) > 250);
QImageReader imageReader2(fileName);
QCOMPARE(imageReader2.autoTransform(), false);
QCOMPARE(imageReader2.transformation(), orientation);
QImage img2 = imageReader2.read();
qt_imageTransform(img2, imageReader2.transformation());
QCOMPARE(img, img2);
}
void tst_QImage::exif_QTBUG45865()
{
QFile file(m_prefix + "jpeg_exif_QTBUG-45865.jpg");
QVERIFY(file.open(QIODevice::ReadOnly));
QByteArray byteArray = file.readAll();
QImage image = QImage::fromData(byteArray);
QCOMPARE(image.size(), QSize(5, 8));
}
static void cleanupFunction(void* info)
{
bool *called = static_cast<bool*>(info);