Rotate images according to Exif orientation
Task-number: QTBUG-37946 Change-Id: I181f88acdff0ef76aa02e968dc6afca1ed495f38 Reviewed-by: aavit <eirik.aavitsland@digia.com>
@ -726,7 +726,7 @@ public:
|
||||
};
|
||||
|
||||
QJpegHandlerPrivate(QJpegHandler *qq)
|
||||
: quality(75), iod_src(0), state(Ready), q(qq)
|
||||
: quality(75), exifOrientation(1), iod_src(0), state(Ready), q(qq)
|
||||
{}
|
||||
|
||||
~QJpegHandlerPrivate()
|
||||
@ -741,8 +741,10 @@ public:
|
||||
|
||||
bool readJpegHeader(QIODevice*);
|
||||
bool read(QImage *image);
|
||||
void applyExifOrientation(QImage *image);
|
||||
|
||||
int quality;
|
||||
int exifOrientation;
|
||||
QVariant size;
|
||||
QImage::Format format;
|
||||
QSize scaledSize;
|
||||
@ -760,6 +762,97 @@ public:
|
||||
QJpegHandler *q;
|
||||
};
|
||||
|
||||
static bool readExifHeader(QDataStream &stream)
|
||||
{
|
||||
char prefix[6];
|
||||
if (stream.readRawData(prefix, sizeof(prefix)) != sizeof(prefix))
|
||||
return false;
|
||||
if (prefix[0] != 'E' || prefix[1] != 'x' || prefix[2] != 'i' || prefix[3] != 'f' || prefix[4] != 0 || prefix[5] != 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
// 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;
|
||||
// we have already used 8 bytes of TIFF header
|
||||
offset -= 8;
|
||||
|
||||
// read IFD
|
||||
while (!stream.atEnd()) {
|
||||
quint16 numEntries;
|
||||
|
||||
// skip offset bytes to get the next IFD
|
||||
if (stream.skipRawData(offset) != (qint32)offset)
|
||||
return -1;
|
||||
|
||||
stream >> numEntries;
|
||||
|
||||
for (;numEntries > 0; --numEntries) {
|
||||
quint16 tag;
|
||||
quint16 type;
|
||||
quint32 components;
|
||||
quint32 value;
|
||||
|
||||
stream >> tag >> type >> components >> value;
|
||||
|
||||
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;
|
||||
}
|
||||
/*!
|
||||
\internal
|
||||
*/
|
||||
@ -779,6 +872,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);
|
||||
|
||||
@ -790,6 +884,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;
|
||||
@ -807,9 +903,18 @@ 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.size()) {
|
||||
// Exif data present
|
||||
int orientation = getExifOrientation(exifData);
|
||||
if (orientation > 0)
|
||||
exifOrientation = orientation;
|
||||
}
|
||||
|
||||
state = ReadHeader;
|
||||
return true;
|
||||
}
|
||||
@ -823,6 +928,48 @@ bool QJpegHandlerPrivate::readJpegHeader(QIODevice *device)
|
||||
return true;
|
||||
}
|
||||
|
||||
void QJpegHandlerPrivate::applyExifOrientation(QImage *image)
|
||||
{
|
||||
// This is not an optimized implementation, but easiest to maintain
|
||||
QTransform transform;
|
||||
|
||||
switch (exifOrientation) {
|
||||
case 1: // normal
|
||||
break;
|
||||
case 2: // mirror horizontal
|
||||
*image = image->mirrored(true, false);
|
||||
break;
|
||||
case 3: // rotate 180
|
||||
transform.rotate(180);
|
||||
*image = image->transformed(transform);
|
||||
break;
|
||||
case 4: // mirror vertical
|
||||
*image = image->mirrored(false, true);
|
||||
break;
|
||||
case 5: // mirror horizontal and rotate 270 CCW
|
||||
*image = image->mirrored(true, false);
|
||||
transform.rotate(270);
|
||||
*image = image->transformed(transform);
|
||||
break;
|
||||
case 6: // rotate 90 CW
|
||||
transform.rotate(90);
|
||||
*image = image->transformed(transform);
|
||||
break;
|
||||
case 7: // mirror horizontal and rotate 90 CW
|
||||
*image = image->mirrored(true, false);
|
||||
transform.rotate(90);
|
||||
*image = image->transformed(transform);
|
||||
break;
|
||||
case 8: // rotate 270 CW
|
||||
transform.rotate(-90);
|
||||
*image = image->transformed(transform);
|
||||
break;
|
||||
default:
|
||||
qWarning("This should never happen");
|
||||
}
|
||||
exifOrientation = 1;
|
||||
}
|
||||
|
||||
bool QJpegHandlerPrivate::read(QImage *image)
|
||||
{
|
||||
if(state == Ready)
|
||||
@ -834,6 +981,7 @@ bool QJpegHandlerPrivate::read(QImage *image)
|
||||
if (success) {
|
||||
for (int i = 0; i < readTexts.size()-1; i+=2)
|
||||
image->setText(readTexts.at(i), readTexts.at(i+1));
|
||||
applyExifOrientation(image);
|
||||
|
||||
state = Ready;
|
||||
return true;
|
||||
|
After Width: | Height: | Size: 910 B |
After Width: | Height: | Size: 910 B |
After Width: | Height: | Size: 988 B |
After Width: | Height: | Size: 995 B |
After Width: | Height: | Size: 912 B |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 987 B |
After Width: | Height: | Size: 991 B |
@ -173,6 +173,8 @@ private slots:
|
||||
void invertPixelsRGB_data();
|
||||
void invertPixelsRGB();
|
||||
|
||||
void exifOrientation();
|
||||
|
||||
void cleanupFunctions();
|
||||
|
||||
private:
|
||||
@ -2624,6 +2626,22 @@ void tst_QImage::invertPixelsRGB()
|
||||
QCOMPARE(qBlue(pixel) >> 4, (255 - 96) >> 4);
|
||||
}
|
||||
|
||||
void tst_QImage::exifOrientation()
|
||||
{
|
||||
for (unsigned int i = 1; i <= 8; ++i) {
|
||||
QImage img;
|
||||
QRgb px;
|
||||
|
||||
QVERIFY(img.load(m_prefix + QString::fromLatin1("jpeg_exif_orientation_value_%1.jpg").arg(i)));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static void cleanupFunction(void* info)
|
||||
{
|
||||
bool *called = static_cast<bool*>(info);
|
||||
|