From e0428071c69202fc3f4d7e95dd1622033d95f2dc Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 12 Jul 2017 15:59:02 +0200 Subject: [PATCH] Added EXIF orientation support for file transfer images' preview --- CMakeLists.txt | 2 + src/components/chat/ChatModel.cpp | 29 +- src/utils/QExifImageHeader.cpp | 1970 +++++++++++++++++++++++++++++ src/utils/QExifImageHeader.h | 348 +++++ 4 files changed, 2345 insertions(+), 4 deletions(-) create mode 100644 src/utils/QExifImageHeader.cpp create mode 100644 src/utils/QExifImageHeader.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b860de9a..c34f2cf20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,7 @@ set(SOURCES src/main.cpp src/utils/LinphoneUtils.cpp src/utils/Utils.cpp + src/utils/QExifImageHeader.cpp ) set(HEADERS @@ -201,6 +202,7 @@ set(HEADERS src/components/url-handlers/UrlHandlers.hpp src/utils/LinphoneUtils.hpp src/utils/Utils.hpp + src/utils/QExifImageHeader.h ) if(APPLE) diff --git a/src/components/chat/ChatModel.cpp b/src/components/chat/ChatModel.cpp index 0225198a6..e2605c173 100644 --- a/src/components/chat/ChatModel.cpp +++ b/src/components/chat/ChatModel.cpp @@ -32,6 +32,7 @@ #include "../../app/paths/Paths.hpp" #include "../../app/providers/ThumbnailProvider.hpp" #include "../../utils/Utils.hpp" +#include "../../utils/QExifImageHeader.h" #include "../core/CoreManager.hpp" #include "ChatModel.hpp" @@ -71,15 +72,35 @@ inline void createThumbnail (const shared_ptr &message) { return; QString thumbnailPath = ::Utils::coreStringToAppString(message->getFileTransferFilepath()); - QImage image(thumbnailPath); if (image.isNull()) return; + int rotation = 0; + QExifImageHeader exifImageHeader; + if (exifImageHeader.loadFromJpeg(thumbnailPath)) { + rotation = (int) exifImageHeader.value(QExifImageHeader::ImageTag::Orientation).toShort(); + } + QImage thumbnail = image.scaled( - THUMBNAIL_IMAGE_FILE_WIDTH, THUMBNAIL_IMAGE_FILE_HEIGHT, - Qt::KeepAspectRatio, Qt::SmoothTransformation - ); + THUMBNAIL_IMAGE_FILE_WIDTH, THUMBNAIL_IMAGE_FILE_HEIGHT, + Qt::KeepAspectRatio, Qt::SmoothTransformation + ); + + if (rotation != 0) { + QTransform transform; + if (rotation == 3 || rotation == 4) { + transform.rotate(180); + } else if (rotation == 5 || rotation == 6) { + transform.rotate(90); + } else if (rotation == 7 || rotation == 8) { + transform.rotate(-90); + } + thumbnail = thumbnail.transformed(transform); + if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7) { + thumbnail = thumbnail.mirrored(true, false); + } + } QString uuid = QUuid::createUuid().toString(); QString fileId = QStringLiteral("%1.jpg").arg(uuid.mid(1, uuid.length() - 2)); diff --git a/src/utils/QExifImageHeader.cpp b/src/utils/QExifImageHeader.cpp new file mode 100644 index 000000000..f885f5e72 --- /dev/null +++ b/src/utils/QExifImageHeader.cpp @@ -0,0 +1,1970 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the Qt scene graph research project. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// This file was copied from Qt Extended 4.5 + +#include "QExifImageHeader.h" + +#include +#include +#include +#include +#include +#include +#include + +/*! + \typedef QExifSRational + + A synonym for \c QPair representing a signed rational number as stored in EXIF + headers. The first integer in the pair is the numerator and the second the denominator. +*/ + +/*! + \typedef QExifURational + + A synonym for \c QPair representing an unsigned rational number as stored in + EXIF headers. The first integer in the pair is the numerator and the second the denominator. +*/ + +struct ExifIfdHeader +{ + quint16 tag; + quint16 type; + quint32 count; + union + { + quint32 offset; + quint8 offsetBytes[ 4 ]; + char offsetAscii[ 4 ]; + quint16 offsetShorts[ 2 ]; + }; +}; + +QDataStream &operator >>( QDataStream &stream, ExifIfdHeader &header ) +{ + stream >> header.tag; + stream >> header.type; + stream >> header.count; + + if( header.type == QExifValue::Byte && header.count <= 4 ) + { + stream.readRawData( header.offsetAscii, 4 ); + } + else if( header.type == QExifValue::Ascii && header.count <= 4 ) + { + stream.readRawData( header.offsetAscii, 4 ); + } + else if( header.type == QExifValue::Short && header.count <= 2 ) + { + stream >> header.offsetShorts[ 0 ]; + stream >> header.offsetShorts[ 1 ]; + } + else + { + stream >> header.offset; + } + + return stream; +} + + +class QExifValuePrivate : public QSharedData +{ +public: + QExifValuePrivate( quint16 t, int c ) + : type( t ), count( c ) + {} + virtual ~QExifValuePrivate(){} + + quint16 type; + int count; +}; + +class QExifByteValuePrivate : public QExifValuePrivate +{ +public: + QExifByteValuePrivate() + : QExifValuePrivate(QExifValue::Byte, 0) + { ref.ref(); } + QExifByteValuePrivate( const QVector< quint8 > &v ) + : QExifValuePrivate( QExifValue::Byte, v.size() ), value( v ) + {} + + QVector< quint8 > value; +}; + +class QExifUndefinedValuePrivate : public QExifValuePrivate +{ +public: + QExifUndefinedValuePrivate( const QByteArray &v ) + : QExifValuePrivate( QExifValue::Undefined, v.size() ), value( v ) + {} + + QByteArray value; +}; + +class QExifAsciiValuePrivate : public QExifValuePrivate +{ +public: + QExifAsciiValuePrivate( const QString &v ) + : QExifValuePrivate( QExifValue::Ascii, v.size() + 1 ), value( v ) + {} + + QString value; +}; + +class QExifShortValuePrivate : public QExifValuePrivate +{ +public: + QExifShortValuePrivate( const QVector< quint16 > &v ) + : QExifValuePrivate( QExifValue::Short, v.size() ), value( v ) + {} + + QVector< quint16 > value; +}; + +class QExifLongValuePrivate : public QExifValuePrivate +{ +public: + QExifLongValuePrivate( const QVector< quint32 > &v ) + : QExifValuePrivate( QExifValue::Long, v.size() ), value( v ) + {} + + QVector< quint32 > value; +}; + +class QExifSignedLongValuePrivate : public QExifValuePrivate +{ +public: + QExifSignedLongValuePrivate( const QVector< qint32 > &v ) + : QExifValuePrivate( QExifValue::SignedLong, v.size() ), value( v ) + {} + + QVector< qint32 > value; +}; + +class QExifRationalValuePrivate : public QExifValuePrivate +{ +public: + QExifRationalValuePrivate( const QVector< QExifURational > &v ) + : QExifValuePrivate( QExifValue::Rational, v.size() ), value( v ) + {} + + QVector< QExifURational > value; +}; + +class QExifSignedRationalValuePrivate : public QExifValuePrivate +{ +public: + QExifSignedRationalValuePrivate( const QVector< QExifSRational > &v ) + : QExifValuePrivate( QExifValue::SignedRational, v.size() ), value( v ) + {} + + QVector< QExifSRational > value; +}; + +Q_GLOBAL_STATIC(QExifByteValuePrivate,qExifValuePrivateSharedNull) + +/*! + \class QExifValue + \inpublicgroup QtBaseModule + \brief The QExifValue class represents data types found in EXIF image headers. + + Tag values in EXIF headers are stored as arrays of a limited number of data types. QExifValue + encapsulates a union of these types and provides conversions to and from appropriate Qt types. + + \section1 String encoding + + Most tags with string values in EXIF headers are ASCII encoded and have the Ascii value type, + but some tags allow other encodings. In this case the value type is Undefined and the encoding + of the text is given by the encoding function(). + + \section1 Date-time values + + Date-time values in EXIF headers are stored in ASCII encoded strings of the form + \c {yyyy:MM:dd HH:mm:ss}. Constructing a QExifValue from a QDateTime will perform this + conversion and likewise an appropriately formed QExifValue can be converted to a QDateTime + using the toDateTime() function. + + \sa QExifImageHeader + + \preliminary +*/ + +/*! + \enum QExifValue::Type + + Enumerates the possible types of EXIF values. + + \value Byte An unsigned 8 bit integer. + \value Ascii A null terminated ascii string. + \value Short An unsigned 16 bit integer. + \value Long An unsigned 32 bit integer. + \value Rational Two unsigned 32 bit integers, representing a the numerator and denominator of an unsigned rational number. + \value Undefined An array of 8 bit integers. + \value SignedLong A signed 32 bit integer. + \value SignedRational Two signed 32 bit integers representing the numerator and denominator of a signed rational number. +*/ + +/*! + \enum QExifValue::TextEncoding + + Enumerates the encodings of text strings in EXIF values of Undefined type. + + \value NoEncoding An ASCII string of Ascii type. + \value AsciiEncoding An ASCII string of Undefined type. + \value JisEncoding A JIS X208-1990 string of Undefined type. + \value UnicodeEncoding A Unicode string of Undefined type. + \value UndefinedEncoding An unspecified string encoding of Undefined type. Assumed to be the local 8-bit encoding. +*/ + +/*! + Constructs a null QExifValue. +*/ +QExifValue::QExifValue() + : d( qExifValuePrivateSharedNull() ) +{ +} + +/*! + Constructs a QExifValue with a \a value of type Byte. +*/ +QExifValue::QExifValue( quint8 value ) + : d( new QExifByteValuePrivate( QVector< quint8 >( 1, value ) ) ) +{ +} + +/*! + Constructs a QExifValue with an array of \a values of type Byte. +*/ +QExifValue::QExifValue( const QVector< quint8 > &values ) + : d( new QExifByteValuePrivate( values ) ) +{ +} + +/*! + Constructs a QExifValue with a \a value of type Ascii or Undefined. + + If the \a encoding is NoEncoding the value will be of type Ascii, otherwise it will be Undefined and the string + encoded using the given \a encoding. +*/ +QExifValue::QExifValue( const QString &value, TextEncoding encoding ) + : d( qExifValuePrivateSharedNull() ) +{ + switch( encoding ) + { + case AsciiEncoding: + d = new QExifUndefinedValuePrivate( QByteArray::fromRawData( "ASCII\0\0\0", 8 ) + value.toUtf8() ); + break; + case JisEncoding: + { + QTextCodec *codec = QTextCodec::codecForName( "JIS X 0208" ); + if( codec ) + d = new QExifUndefinedValuePrivate( QByteArray::fromRawData( "JIS\0\0\0\0\0", 8 ) + codec->fromUnicode( value ) ); + } + break; + case UnicodeEncoding: + { + QTextCodec *codec = QTextCodec::codecForName( "UTF-16" ); + if( codec ) + d = new QExifUndefinedValuePrivate( QByteArray::fromRawData( "UNICODE\0", 8 ) + codec->fromUnicode( value ) ); + } + break; + case UndefinedEncoding: + d = new QExifUndefinedValuePrivate( QByteArray::fromRawData( "\0\0\0\0\0\0\0\\0", 8 ) + value.toLocal8Bit() ); + break; + default: + d = new QExifAsciiValuePrivate( value ); + } +} + +/*! + Constructs a QExifValue with a \a value of type Short. +*/ +QExifValue::QExifValue( quint16 value ) + : d( new QExifShortValuePrivate( QVector< quint16 >( 1, value ) ) ) +{ +} + +/*! + Constructs a QExifValue with an array of \a values of type Short. +*/ +QExifValue::QExifValue( const QVector< quint16 > &values ) + : d( new QExifShortValuePrivate( values ) ) +{ +} + +/*! + Constructs a QExifValue with a \a value of type Long. +*/ +QExifValue::QExifValue( quint32 value ) + : d( new QExifLongValuePrivate( QVector< quint32 >( 1, value ) ) ) +{ +} + +/*! + Constructs a QExifValue with an array of \a values of type Long. +*/ +QExifValue::QExifValue( const QVector< quint32 > &values ) + : d( new QExifLongValuePrivate( values ) ) +{ +} + +/*! + Constructs a QExifValue with a \a value of type Rational. +*/ +QExifValue::QExifValue( const QExifURational &value ) + : d( new QExifRationalValuePrivate( QVector< QExifURational >( 1, value ) ) ) +{ +} + +/*! + Constructs a QExifValue with an array of \a values of type Rational. +*/ +QExifValue::QExifValue( const QVector< QExifURational > &values ) + : d( new QExifRationalValuePrivate( values ) ) +{ +} + +/*! + Constructs a QExifValue with a \a value of type Undefined. +*/ +QExifValue::QExifValue( const QByteArray &value ) + : d( new QExifUndefinedValuePrivate( value ) ) +{ +} + +/*! + Constructs a QExifValue with a \a value of type SignedLong. +*/ +QExifValue::QExifValue( qint32 value ) + : d( new QExifSignedLongValuePrivate( QVector< qint32 >( 1, value ) ) ) +{ +} + +/*! + Constructs a QExifValue with an array of \a values of type SignedLong. +*/ +QExifValue::QExifValue( const QVector< qint32 > &values ) + : d( new QExifSignedLongValuePrivate( values ) ) +{ +} + +/*! + Constructs a QExifValue with a \a value of type SignedRational. +*/ +QExifValue::QExifValue( const QExifSRational &value ) + : d( new QExifSignedRationalValuePrivate( QVector< QExifSRational >( 1, value ) ) ) +{ +} + +/*! + Constructs a QExifValue with an array of \a values of type SignedRational. +*/ +QExifValue::QExifValue( const QVector< QExifSRational > &values ) + : d( new QExifSignedRationalValuePrivate( values ) ) +{ +} + +/*! + Constructs a QExifValue of type Ascii with an ascii string formatted from a date-time \a value. + + Date-times are stored as strings in the format \c {yyyy:MM:dd HH:mm:ss}. +*/ +QExifValue::QExifValue( const QDateTime &value ) + : d( new QExifAsciiValuePrivate( value.toString( QLatin1String( "yyyy:MM:dd HH:mm:ss" ) ) ) ) +{ +} + +/*! + Constructs a copy of the QExifValue \a other. +*/ +QExifValue::QExifValue( const QExifValue &other ) + : d( other.d ) +{ +} + +/*! + Assigns the value of \a other to a QExifValue. +*/ +QExifValue &QExifValue::operator =( const QExifValue &other ) +{ + d = other.d; + + return *this; +} + +/*! + Destroys a QExifValue. +*/ +QExifValue::~QExifValue() +{ +} + +/*! + Compares a QExifValue to \a other. Returns true if they are the same value and false otherwise. +*/ +bool QExifValue::operator ==( const QExifValue &other ) const +{ + return d == other.d; +} + +/*! + Returns true if a QExifValue has a null value and false otherwise. +*/ +bool QExifValue::isNull() const +{ + return d == qExifValuePrivateSharedNull(); +} + +/*! + Returns the type of a QExifValue. +*/ +int QExifValue::type() const +{ + return d->type; +} + +/*! + Returns the number of elements in a QExifValue. For ascii strings this is the length of the string + including the terminating null. +*/ +int QExifValue::count() const +{ + return d->count; +} + +/*! + Returns the encoding of strings stored in Undefined values. +*/ +QExifValue::TextEncoding QExifValue::encoding() const +{ + if( d->type == Undefined && d->count > 8 ) + { + QByteArray value = static_cast< const QExifUndefinedValuePrivate * >( d.constData() )->value; + + if( value.startsWith( QByteArray::fromRawData( "ASCII\0\0\0", 8 ) ) ) + return AsciiEncoding; + else if( value.startsWith( QByteArray::fromRawData( "JIS\0\0\0\0\0", 8 ) ) ) + return JisEncoding; + else if( value.startsWith( QByteArray::fromRawData( "UNICODE\0", 8 ) ) ) + return UnicodeEncoding; + else if( value.startsWith( QByteArray::fromRawData( "\0\0\0\0\0\0\0\0", 8 ) ) ) + return UndefinedEncoding; + } + return NoEncoding; +} + +/*! + Returns the value of a single element QExifValue of type Byte. +*/ +quint8 QExifValue::toByte() const +{ + return d->type == Byte && d->count == 1 + ? static_cast< const QExifByteValuePrivate * >( d.constData() )->value.at( 0 ) + : 0; +} + +/*! + Returns the value of a multiple element QExifValue of type Byte. +*/ +QVector< quint8 > QExifValue::toByteVector() const +{ + return d->type == Byte + ? static_cast< const QExifByteValuePrivate * >( d.constData() )->value + : QVector< quint8 >(); +} + +/*! + Returns the value of a QExifValue of type Ascii. +*/ +QString QExifValue::toString() const +{ + switch( d->type ) + { + case Ascii: + return static_cast< const QExifAsciiValuePrivate * >( d.constData() )->value; + case Undefined: + { + QByteArray string = static_cast< const QExifUndefinedValuePrivate * >( d.constData() )->value.mid( 8 ); + + switch( encoding() ) + { + case AsciiEncoding: + return QString::fromUtf8( string.constData(), string.length() ); + case JisEncoding: + { + QTextCodec *codec = QTextCodec::codecForName( "JIS X 0208" ); + if( codec ) + return codec->toUnicode( string ); + } + break; + case UnicodeEncoding: + { + QTextCodec *codec = QTextCodec::codecForName( "UTF-16" ); + if( codec ) + return codec->toUnicode( string ); + } + case UndefinedEncoding: + return QString::fromLocal8Bit( string.constData(), string.length() ); + default: + break; + } + } + default: + return QString(); + } +} + +/*! + Returns the value of a single element QExifValue of type Byte or Short. +*/ +quint16 QExifValue::toShort() const +{ + if( d->count == 1 ) + { + switch( d->type ) + { + case Byte: + return static_cast< const QExifByteValuePrivate * >( d.constData() )->value.at( 0 ); + case Short: + return static_cast< const QExifShortValuePrivate * >( d.constData() )->value.at( 0 ); + } + } + return 0; +} + +/*! + Returns the value of a single element QExifValue of type Short. +*/ +QVector< quint16 > QExifValue::toShortVector() const +{ + return d->type == Short + ? static_cast< const QExifShortValuePrivate * >( d.constData() )->value + : QVector< quint16 >(); +} + +/*! + Returns the value of a single element QExifValue of type Byte, Short, Long, or SignedLong. +*/ +quint32 QExifValue::toLong() const +{ + if( d->count == 1 ) + { + switch( d->type ) + { + case Byte: + return static_cast< const QExifByteValuePrivate * >( d.constData() )->value.at( 0 ); + case Short: + return static_cast< const QExifShortValuePrivate * >( d.constData() )->value.at( 0 ); + case Long: + return static_cast< const QExifLongValuePrivate * >( d.constData() )->value.at( 0 ); + case SignedLong: + return static_cast< const QExifSignedLongValuePrivate * >( d.constData() )->value.at( 0 ); + } + } + return 0; +} + +/*! + Returns the value of a multiple element QExifValue of type Long. +*/ +QVector< quint32 > QExifValue::toLongVector() const +{ + return d->type == Long + ? static_cast< const QExifLongValuePrivate * >( d.constData() )->value + : QVector< quint32 >(); +} + +/*! + Returns the value of a multiple element QExifValue of type Rational. +*/ +QExifURational QExifValue::toRational() const +{ + return d->type == Rational && d->count == 1 + ? static_cast< const QExifRationalValuePrivate * >( d.constData() )->value.at( 0 ) + : QExifURational(); +} + +/*! + Returns the value of a multiple element QExifValue of type Rational. +*/ +QVector< QExifURational > QExifValue::toRationalVector() const +{ + return d->type == Rational + ? static_cast< const QExifRationalValuePrivate * >( d.constData() )->value + : QVector< QExifURational >(); +} + +/*! + Returns the value of a QExifValue of type Undefined. +*/ +QByteArray QExifValue::toByteArray() const +{ + switch( d->type ) + { + case Ascii: + return static_cast< const QExifAsciiValuePrivate * >( d.constData() )->value.toUtf8(); + case Undefined: + return static_cast< const QExifUndefinedValuePrivate * >( d.constData() )->value; + default: + return QByteArray(); + } +} + +/*! + Returns the value of a single element QExifValue of type Byte, Short, Long, or SignedLong. +*/ +qint32 QExifValue::toSignedLong() const +{ + if( d->count == 1 ) + { + switch( d->type ) + { + case Byte: + return static_cast< const QExifByteValuePrivate * >( d.constData() )->value.at( 0 ); + case Short: + return static_cast< const QExifShortValuePrivate * >( d.constData() )->value.at( 0 ); + case Long: + return static_cast< const QExifLongValuePrivate * >( d.constData() )->value.at( 0 ); + case SignedLong: + return static_cast< const QExifSignedLongValuePrivate * >( d.constData() )->value.at( 0 ); + } + } + return 0; +} + +/*! + Returns the value of a multiple element QExifValue of type SignedLong. +*/ +QVector< qint32 > QExifValue::toSignedLongVector() const +{ + return d->type == SignedLong + ? static_cast< const QExifSignedLongValuePrivate * >( d.constData() )->value + : QVector< qint32 >(); +} + +/*! + Returns the value of a single element QExifValue of type SignedRational. +*/ +QExifSRational QExifValue::toSignedRational() const +{ + return d->type == SignedRational && d->count == 1 + ? static_cast< const QExifSignedRationalValuePrivate * >( d.constData() )->value.at( 0 ) + : QExifSRational(); +} + +/*! + Returns the value of a multiple element QExifValue of type SignedRational. +*/ +QVector< QExifSRational > QExifValue::toSignedRationalVector() const +{ + return d->type == SignedRational + ? static_cast< const QExifSignedRationalValuePrivate * >( d.constData() )->value + : QVector< QExifSRational >(); +} + +/*! + Returns the value of QExifValue storing a date-time. + + Date-times are stored as ascii strings in the format \c {yyyy:MM:dd HH:mm:ss}. +*/ +QDateTime QExifValue::toDateTime() const +{ + return d->type == Ascii && d->count == 20 + ? QDateTime::fromString( static_cast< const QExifAsciiValuePrivate * >( d.constData() )->value, QLatin1String( "yyyy:MM:dd HH:mm:ss" ) ) + : QDateTime(); +} + +class QExifImageHeaderPrivate +{ +public: + QSysInfo::Endian byteOrder; + mutable qint64 size; + QMap imageIfdValues; + QMap exifIfdValues; + QMap gpsIfdValues; + + QSize thumbnailSize; + QByteArray thumbnailData; + QExifValue thumbnailXResolution; + QExifValue thumbnailYResolution; + QExifValue thumbnailResolutionUnit; + QExifValue thumbnailOrientation; +}; + +/*! + \class QExifImageHeader + \inpublicgroup QtBaseModule + \brief The QExifImageHeader class provides functionality for reading and writing EXIF image headers. + + EXIF headers are a collection of properties that describe the image they're embedded in. + Each property is identified by a tag of which there are three kinds. \l {ImageTag}{Image tags} + which mostly describe the format (dimensions, resolution, orientation) but also include some + descriptive information (description, camera make and model, artist). \l {ExifExtendedTag} + {EXIF extended tags} which elaborate on some of the image tags and record the camera settings at + time of capture among other things. Finally there are \l {GpsTag}{GPS tags} which record the + location the image was captured. + + EXIF tags are typically found in JPEG images but may be found in other image formats. To read + headers from a JPEG image QExifImageHeader provides the loadFromJpeg() function, and the + complementary saveToJpeg() function for writing. To allow reading and writing arbitrary + formats QExifImageHeader provides the read() and write() functions which work with just the + EXIF header data itself. + + \preliminary +*/ + +/*! + \enum QExifImageHeader::ImageTag + Enumerates the TIFF image tag IDs defined in the EXIF specification. + + \value ImageWidth + \value ImageLength + \value BitsPerSample + \value Compression + \value PhotometricInterpretation + \value Orientation + \value SamplesPerPixel + \value PlanarConfiguration + \value YCbCrSubSampling + \value XResolution + \value YResolution + \value ResolutionUnit + \value StripOffsets + \value RowsPerStrip + \value StripByteCounts + \value TransferFunction + \value WhitePoint + \value PrimaryChromaciticies + \value YCbCrCoefficients + \value ReferenceBlackWhite + \value DateTime + \value ImageDescription + \value Make + \value Model + \value Software + \value Artist + \value Copyright +*/ + +/*! + \enum QExifImageHeader::ExifExtendedTag + Enumerates the extended EXIF tag IDs defined in the EXIF specification. + + \value ExifVersion + \value FlashPixVersion + \value ColorSpace + \value ComponentsConfiguration + \value CompressedBitsPerPixel + \value PixelXDimension + \value PixelYDimension + \value MakerNote + \value UserComment + \value RelatedSoundFile + \value DateTimeOriginal + \value DateTimeDigitized + \value SubSecTime + \value SubSecTimeOriginal + \value SubSecTimeDigitized + \value ImageUniqueId + \value ExposureTime + \value FNumber + \value ExposureProgram + \value SpectralSensitivity + \value ISOSpeedRatings + \value Oecf + \value ShutterSpeedValue + \value ApertureValue + \value BrightnessValue + \value ExposureBiasValue + \value MaxApertureValue + \value SubjectDistance + \value MeteringMode + \value LightSource + \value Flash + \value FocalLength + \value SubjectArea + \value FlashEnergy + \value SpatialFrequencyResponse + \value FocalPlaneXResolution + \value FocalPlaneYResolution + \value FocalPlaneResolutionUnit + \value SubjectLocation + \value ExposureIndex + \value SensingMethod + \value FileSource + \value SceneType + \value CfaPattern + \value CustomRendered + \value ExposureMode + \value WhiteBalance + \value DigitalZoomRatio + \value FocalLengthIn35mmFilm + \value SceneCaptureType + \value GainControl + \value Contrast + \value Saturation + \value Sharpness + \value DeviceSettingDescription + \value SubjectDistanceRange +*/ + +/*! + \enum QExifImageHeader::GpsTag + Enumerates the GPS tag IDs from the EXIF specification. + + \value GpsVersionId + \value GpsLatitudeRef + \value GpsLatitude + \value GpsLongitudeRef + \value GpsLongitude + \value GpsAltitudeRef + \value GpsAltitude + \value GpsTimeStamp + \value GpsSatellites + \value GpsStatus + \value GpsMeasureMode + \value GpsDop + \value GpsSpeedRef + \value GpsSpeed + \value GpsTrackRef + \value GpsTrack + \value GpsImageDirectionRef + \value GpsImageDirection + \value GpsMapDatum + \value GpsDestLatitudeRef + \value GpsDestLatitude + \value GpsDestLongitudeRef + \value GpsDestLongitude + \value GpsDestBearingRef + \value GpsDestBearing + \value GpsDestDistanceRef + \value GpsDestDistance + \value GpsProcessingMethod + \value GpsAreaInformation + \value GpsDateStamp + \value GpsDifferential +*/ + +/*! + Constructs a new EXIF image data editor. +*/ +QExifImageHeader::QExifImageHeader() + : d( new QExifImageHeaderPrivate ) +{ + d->byteOrder = QSysInfo::ByteOrder; + d->size = -1; +} + +/*! + Constructs a new EXIF image data editor and reads the meta-data from a JPEG image with the given \a fileName. +*/ +QExifImageHeader::QExifImageHeader(const QString &fileName) + : d(new QExifImageHeaderPrivate) +{ + d->byteOrder = QSysInfo::ByteOrder; + d->size = -1; + + loadFromJpeg(fileName); +} + +/*! + Destroys an EXIF image data editor. +*/ +QExifImageHeader::~QExifImageHeader() +{ + clear(); + + delete d; +} + +/*! + Reads meta-data from a JPEG image with the given \a fileName. + + Returns true if the data was successfully parsed and false otherwise. +*/ +bool QExifImageHeader::loadFromJpeg(const QString &fileName) +{ + QFile file(fileName); + + if(file.open(QIODevice::ReadOnly)) + return loadFromJpeg(&file); + else + return false; +} + +/*! + Reads meta-data from an I/O \a device containing a JPEG image. + + Returns true if the data was successfully parsed and false otherwise. +*/ +bool QExifImageHeader::loadFromJpeg(QIODevice *device) +{ + clear(); + + QByteArray exifData = extractExif(device); + + if (!exifData.isEmpty()) { + QBuffer buffer(&exifData); + + return buffer.open(QIODevice::ReadOnly) && read(&buffer); + } + + return false; +} + +/*! + Saves meta-data to a JPEG image with the given \a fileName. + + Returns true if the data was successfully written. +*/ +bool QExifImageHeader::saveToJpeg(const QString &fileName) const +{ + QFile file(fileName); + + if (file.open(QIODevice::ReadWrite)) + return saveToJpeg(&file); + else + return false; +} + +/*! + Save meta-data to the given I/O \a device. + + The device must be non-sequential and already contain a valid JPEG image. + + Returns true if the data was successfully written. +*/ +bool QExifImageHeader::saveToJpeg(QIODevice *device) const +{ + if( device->isSequential() ) + return false; + + QByteArray exif; + + { + QBuffer buffer( &exif ); + + if( !buffer.open( QIODevice::WriteOnly ) ) + return false; + + write( &buffer ); + + buffer.close(); + + exif = QByteArray::fromRawData( "Exif\0\0", 6 ) + exif; + } + + QDataStream stream( device ); + + stream.setByteOrder( QDataStream::BigEndian ); + + if( device->read( 2 ) != "\xFF\xD8" ) // Not a valid JPEG image. + return false; + + quint16 segmentId; + quint16 segmentLength; + + stream >> segmentId; + stream >> segmentLength; + + if( segmentId == 0xFFE0 ) + { + QByteArray jfif = device->read( segmentLength - 2 ); + + if( !jfif.startsWith( "JFIF" ) ) + return false; + + stream >> segmentId; + stream >> segmentLength; + + if( segmentId == 0xFFE1 ) + { + QByteArray oldExif = device->read( segmentLength - 2 ); + + if( !oldExif.startsWith( "Exif" ) ) + return false; + + int dSize = oldExif.size() - exif.size(); + + if( dSize > 0 ) + exif += QByteArray( dSize, '\0' ); + + QByteArray remainder = device->readAll(); + + device->seek( 0 ); + + stream << quint16( 0xFFD8 ); // SOI + stream << quint16( 0xFFE0 ); // APP0 + stream << quint16( jfif.size() + 2 ); + device->write( jfif ); + stream << quint16( 0xFFE1 ); //APP1 + stream << quint16( exif.size() + 2 ); + device->write( exif ); + device->write( remainder ); + } + else + { + QByteArray remainder = device->readAll(); + + device->seek( 0 ); + + stream << quint16( 0xFFD8 ); // SOI + stream << quint16( 0xFFE0 ); // APP0 + stream << quint16( jfif.size() + 2 ); + device->write( jfif ); + stream << quint16( 0xFFE1 ); //APP1 + stream << quint16( exif.size() + 2 ); + device->write( exif ); + stream << quint16( 0xFFE0 ); // APP0 + stream << segmentId; + stream << segmentLength; + device->write( remainder ); + } + } + else if( segmentId == 0xFFE1 ) + { + QByteArray oldExif = device->read( segmentLength - 2 ); + + if( !oldExif.startsWith( "Exif" ) ) + return false; + + int dSize = oldExif.size() - exif.size(); + + if( dSize > 0 ) + exif += QByteArray( dSize, '\0' ); + + QByteArray remainder = device->readAll(); + + device->seek( 0 ); + + stream << quint16( 0xFFD8 ); // SOI + stream << quint16( 0xFFE1 ); //APP1 + stream << quint16( exif.size() + 2 ); + device->write( exif ); + device->write( remainder ); + } + else + { + QByteArray remainder = device->readAll(); + + device->seek( 0 ); + + stream << quint16( 0xFFD8 ); // SOI + stream << quint16( 0xFFE1 ); //APP1 + stream << quint16( exif.size() + 2 ); + device->write( exif ); + stream << segmentId; + stream << segmentLength; + device->write( remainder ); + } + + return true; +} + +/*! + Returns the byte order of EXIF file. +*/ +QSysInfo::Endian QExifImageHeader::byteOrder() const +{ + return d->byteOrder; +} + + +quint32 QExifImageHeader::sizeOf(const QExifValue &value) const +{ + switch (value.type()) { + case QExifValue::Byte: + case QExifValue::Undefined: + return value.count() > 4 + ? 12 + value.count() + : 12; + case QExifValue::Ascii: + return value.count() > 4 + ? 12 + value.count() + : 12; + case QExifValue::Short: + return value.count() > 2 + ? 12 + value.count() * sizeof(quint16) + : 12; + case QExifValue::Long: + case QExifValue::SignedLong: + return value.count() > 1 + ? 12 + value.count() * sizeof(quint32) + : 12; + case QExifValue::Rational: + case QExifValue::SignedRational: + return value.count() > 0 + ? 12 + value.count() * sizeof(quint32) * 2 + : 12; + default: + return 0; + } +} + +template +quint32 QExifImageHeader::calculateSize(const QMap &values) const +{ + quint32 size = sizeof(quint16); + + foreach (const QExifValue &value, values) + size += sizeOf(value); + + return size; +} + +/*! + Returns the size of EXIF data in bytes. +*/ +qint64 QExifImageHeader::size() const +{ + if (d->size == -1) { + d->size + = 2 // Byte Order + + 2 // Marker + + 4 // Image Ifd offset + + 12 // ExifIfdPointer Ifd + + 4 // Thumbnail Ifd offset + + calculateSize(d->imageIfdValues) // Image headers and values. + + calculateSize(d->exifIfdValues); // Exif headers and values. + + if (!d->gpsIfdValues.isEmpty()) { + d->size + += 12 // GpsInfoIfdPointer Ifd + + calculateSize(d->gpsIfdValues); // Gps headers and values. + } + + if (!d->thumbnailData.isEmpty()) { + d->size + += 2 // Thumbnail Ifd count + + 12 // Compression Ifd + + 20 // XResolution Ifd + + 20 // YResolution Ifd + + 12 // ResolutionUnit Ifd + + 12 // JpegInterchangeFormat Ifd + + 12 // JpegInterchangeFormatLength Ifd + + d->thumbnailData.size(); // Thumbnail data size. + } + } + + return d->size; +} + +/*! + Clears all image meta-data. +*/ +void QExifImageHeader::clear() +{ + d->imageIfdValues.clear(); + d->exifIfdValues.clear(); + d->gpsIfdValues.clear(); + d->thumbnailData.clear(); + + d->size = -1; +} + +/*! + Returns a list of all image tags in an EXIF header. +*/ +QList QExifImageHeader::imageTags() const +{ + return d->imageIfdValues.keys(); +} + +/*! + Returns a list of all extended EXIF tags in a header. +*/ +QList QExifImageHeader::extendedTags() const +{ + return d->exifIfdValues.keys(); +} + +/*! + Returns a list of all GPS tags in an EXIF header. +*/ +QList QExifImageHeader::gpsTags() const +{ + return d->gpsIfdValues.keys(); +} + +/*! + Returns true if an EXIf header contains a value for an image \a tag and false otherwise. +*/ +bool QExifImageHeader::contains(ImageTag tag) const +{ + return d->imageIfdValues.contains(tag); +} + +/*! + Returns true if a header contains a a value for an extended EXIF \a tag and false otherwise. +*/ +bool QExifImageHeader::contains(ExifExtendedTag tag) const +{ + return d->exifIfdValues.contains(tag); +} + +/*! + Returns true if an EXIf header contains a value for a GPS \a tag and false otherwise. +*/ +bool QExifImageHeader::contains(GpsTag tag) const +{ + return d->gpsIfdValues.contains(tag); +} + +/*! + Removes the value for an image \a tag. +*/ +void QExifImageHeader::remove(ImageTag tag) +{ + d->imageIfdValues.remove(tag); + + d->size = -1; +} + +/*! + Removes the value for an extended EXIF \a tag. +*/ +void QExifImageHeader::remove(ExifExtendedTag tag) +{ + d->exifIfdValues.remove(tag); + + d->size = -1; +} + +/*! + Removes the value for a GPS \a tag. +*/ +void QExifImageHeader::remove(GpsTag tag) +{ + d->gpsIfdValues.remove(tag); + + d->size = -1; +} + +/*! + Returns the value for an image \a tag. +*/ +QExifValue QExifImageHeader::value(ImageTag tag) const +{ + return d->imageIfdValues.value(tag); +} + +/*! + Returns the value for an extended EXIF \a tag. +*/ +QExifValue QExifImageHeader::value(ExifExtendedTag tag) const +{ + return d->exifIfdValues.value(tag); +} + +/*! + Returns the value for a GPS tag. +*/ +QExifValue QExifImageHeader::value(GpsTag tag) const +{ + return d->gpsIfdValues.value(tag); +} + +/*! + Sets the \a value for an image \a tag. +*/ +void QExifImageHeader::setValue(ImageTag tag, const QExifValue &value) +{ + d->imageIfdValues[tag] = value; + + d->size = -1; +} + +/*! + Sets the \a value for an extended EXIF \a tag. +*/ +void QExifImageHeader::setValue(ExifExtendedTag tag, const QExifValue &value) +{ + d->exifIfdValues[tag] = value; + + d->size = -1; +} + +/*! + Sets the \a value for an GPS \a tag. +*/ +void QExifImageHeader::setValue(GpsTag tag, const QExifValue &value) +{ + d->gpsIfdValues[tag] = value; + + d->size = -1; +} + +/*! + Returns the image thumbnail. +*/ +QImage QExifImageHeader::thumbnail() const +{ + QImage image; + + image.loadFromData(d->thumbnailData, "JPG"); + + if (!d->thumbnailOrientation.isNull()) { + switch (d->thumbnailOrientation.toShort()) { + case 1: + return image; + case 2: + return image.transformed(QTransform().rotate(180, Qt::YAxis)); + case 3: + return image.transformed(QTransform().rotate(180, Qt::ZAxis)); + case 4: + return image.transformed(QTransform().rotate(180, Qt::XAxis)); + case 5: + return image.transformed(QTransform().rotate(180, Qt::YAxis).rotate(90, Qt::ZAxis)); + case 6: + return image.transformed(QTransform().rotate(90, Qt::ZAxis)); + case 7: + return image.transformed(QTransform().rotate(180, Qt::XAxis).rotate(90, Qt::ZAxis)); + case 8: + return image.transformed(QTransform().rotate(270, Qt::ZAxis)); + } + } + + return image; +} + +/*! + Sets the image \a thumbnail. +*/ +void QExifImageHeader::setThumbnail( const QImage &thumbnail ) +{ + if (!thumbnail.isNull()) { + QBuffer buffer; + + if (buffer.open(QIODevice::WriteOnly) && thumbnail.save(&buffer, "JPG")) { + buffer.close(); + + d->thumbnailSize = thumbnail.size(); + d->thumbnailData = buffer.data(); + d->thumbnailOrientation = QExifValue(); + } + } else { + d->thumbnailSize = QSize(); + d->thumbnailData = QByteArray(); + } + + d->size = -1; +} + +QByteArray QExifImageHeader::extractExif( QIODevice *device ) const +{ + QDataStream stream( device ); + + stream.setByteOrder( QDataStream::BigEndian ); + + if( device->read( 2 ) != "\xFF\xD8" ) + return QByteArray(); + + while( device->read( 2 ) != "\xFF\xE1" ) + { + if( device->atEnd() ) + return QByteArray(); + + quint16 length; + + stream >> length; + + device->seek( device->pos() + length - 2 ); + } + + quint16 length; + + stream >> length; + + if( device->read( 4 ) != "Exif" ) + return QByteArray(); + + device->read( 2 ); + + return device->read( length - 8 ); +} + + +QList QExifImageHeader::readIfdHeaders(QDataStream &stream) const +{ + QList headers; + + quint16 count; + + stream >> count; + + for (quint16 i = 0; i < count; i++) { + ExifIfdHeader header; + + stream >> header; + + headers.append(header); + } + + return headers; +} + +QExifValue QExifImageHeader::readIfdValue(QDataStream &stream, int startPos, const ExifIfdHeader &header) const +{ + switch (header.type) { + case QExifValue::Byte: + { + QVector value( header.count ); + + if (header.count > 4) { + stream.device()->seek(startPos + header.offset); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[i]; + } else { + for( quint32 i = 0; i < header.count; i++ ) + value[ i ] = header.offsetBytes[ i ]; + + } + return QExifValue(value); + } + case QExifValue::Undefined: + if (header.count > 4) { + stream.device()->seek(startPos + header.offset); + + return QExifValue(stream.device()->read(header.count)); + } else { + return QExifValue(QByteArray::fromRawData(header.offsetAscii, header.count)); + } + case QExifValue::Ascii: + if (header.count > 4) { + stream.device()->seek(startPos + header.offset); + + QByteArray ascii = stream.device()->read(header.count); + + return QExifValue(QString::fromUtf8(ascii.constData(), ascii.size() - 1)); + } else { + return QExifValue(QString::fromUtf8(header.offsetAscii, header.count - 1)); + } + case QExifValue::Short: + { + QVector value(header.count); + + if (header.count > 2) { + stream.device()->seek(startPos + header.offset); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[i]; + } else { + for (quint32 i = 0; i < header.count; i++) + value[i] = header.offsetShorts[i]; + + } + return QExifValue(value); + } + case QExifValue::Long: + { + QVector value(header.count); + + if (header.count > 1) { + stream.device()->seek(startPos + header.offset); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[i]; + } else if(header.count == 1) { + value[0] = header.offset; + } + return QExifValue(value); + } + case QExifValue::SignedLong: + { + QVector value(header.count); + + if (header.count > 1) { + stream.device()->seek(startPos + header.offset); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[i]; + } else if (header.count == 1) { + value[0] = header.offset; + } + return QExifValue(value); + } + break; + case QExifValue::Rational: + { + QVector value(header.count); + + stream.device()->seek(startPos + header.offset); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[i]; + + return QExifValue(value); + } + case QExifValue::SignedRational: + { + QVector value(header.count); + + stream.device()->seek(startPos + header.offset); + + for(quint32 i = 0; i < header.count; i++) + stream >> value[i]; + + return QExifValue(value); + } + default: + qWarning() << "Invalid Ifd Type" << header.type; + + return QExifValue(); + } +} + +template QMap QExifImageHeader::readIfdValues( + QDataStream &stream, int startPos, const QList &headers) const +{ + QMap values; + + // This needs to be non-const so it works with gcc3 + QList headers_ = headers; + foreach (const ExifIfdHeader &header, headers_) + values[T(header.tag)] = readIfdValue(stream, startPos, header); + + return values; +} + +template +QMap QExifImageHeader::readIfdValues( + QDataStream &stream, int startPos, const QExifValue &pointer) const +{ + if (pointer.type() == QExifValue::Long && pointer.count() == 1) { + stream.device()->seek(startPos + pointer.toLong()); + + QList headers = readIfdHeaders(stream); + + return readIfdValues(stream, startPos, headers); + } else { + return QMap(); + } +} + + +/*! + Reads the contents of an EXIF header from an I/O \a device. + + Returns true if the header was read and false otherwise. + + \sa loadFromJpeg(), write() +*/ +bool QExifImageHeader::read(QIODevice *device) +{ + clear(); + + int startPos = device->pos(); + + QDataStream stream(device); + + QByteArray byteOrder = device->read(2); + + if (byteOrder == "II") { + d->byteOrder = QSysInfo::LittleEndian; + + stream.setByteOrder( QDataStream::LittleEndian ); + } else if (byteOrder == "MM") { + d->byteOrder = QSysInfo::BigEndian; + + stream.setByteOrder( QDataStream::BigEndian ); + } else { + return false; + } + + quint16 id; + quint32 offset; + + stream >> id; + stream >> offset; + + if (id != 0x002A) + return false; + + device->seek(startPos + offset); + + QList headers = readIfdHeaders(stream); + + stream >> offset; + + d->imageIfdValues = readIfdValues(stream, startPos, headers); + + QExifValue exifIfdPointer = d->imageIfdValues.take(ImageTag(ExifIfdPointer)); + QExifValue gpsIfdPointer = d->imageIfdValues.take(ImageTag(GpsInfoIfdPointer)); + + d->exifIfdValues = readIfdValues(stream, startPos, exifIfdPointer); + d->gpsIfdValues = readIfdValues(stream, startPos, gpsIfdPointer); + + d->exifIfdValues.remove(ExifExtendedTag(InteroperabilityIfdPointer)); + + if (offset) { + device->seek(startPos + offset); + + QMap thumbnailIfdValues = readIfdValues( + stream, startPos, readIfdHeaders( stream)); + + QExifValue jpegOffset = thumbnailIfdValues.value(JpegInterchangeFormat); + QExifValue jpegLength = thumbnailIfdValues.value(JpegInterchangeFormatLength); + + if (jpegOffset.type() == QExifValue::Long && jpegOffset.count() == 1 + && jpegLength.type() == QExifValue::Long && jpegLength.count() == 1) + { + device->seek(startPos + jpegOffset.toLong()); + + d->thumbnailData = device->read( jpegLength.toLong() ); + + d->thumbnailXResolution = thumbnailIfdValues.value(XResolution); + d->thumbnailYResolution = thumbnailIfdValues.value(YResolution); + d->thumbnailResolutionUnit = thumbnailIfdValues.value(ResolutionUnit); + d->thumbnailOrientation = thumbnailIfdValues.value(Orientation); + } + } + return true; +} + +quint32 QExifImageHeader::writeExifHeader(QDataStream &stream, quint16 tag, const QExifValue &value, quint32 offset) const +{ + stream << tag; + stream << quint16(value.type()); + stream << quint32(value.count()); + + switch (value.type()) { + case QExifValue::Byte: + if (value.count() <= 4) { + foreach (quint8 byte, value.toByteVector()) + stream << byte; + for (int j = value.count(); j < 4; j++) + stream << quint8(0); + } else { + stream << offset; + + offset += value.count(); + } + break; + case QExifValue::Undefined: + if (value.count() <= 4) { + stream.device()->write(value.toByteArray()); + + if ( value.count() < 4) + stream.writeRawData("\0\0\0\0", 4 - value.count()); + } else { + stream << offset; + + offset += value.count(); + } + break; + case QExifValue::Ascii: + if (value.count() <= 4) { + QByteArray bytes = value.toByteArray(); + + stream.writeRawData(bytes.constData(), value.count()); + if (value.count() < 4) + stream.writeRawData("\0\0\0\0", 4 - value.count()); + } else { + stream << offset; + + offset += value.count(); + } + break; + case QExifValue::Short: + if (value.count() <= 2) { + foreach (quint16 shrt, value.toShortVector()) + stream << shrt; + for (int j = value.count(); j < 2; j++) + stream << quint16(0); + } else { + stream << offset; + + offset += value.count() * sizeof(quint16); + } + break; + case QExifValue::Long: + if(value.count() == 0) { + stream << quint32(0); + } else if(value.count() == 1) { + stream << value.toLong(); + } else { + stream << offset; + + offset += value.count() * sizeof(quint32); + } + break; + case QExifValue::SignedLong: + if (value.count() == 0) { + stream << quint32( 0 ); + } else if (value.count() == 1) { + stream << value.toSignedLong(); + } else { + stream << offset; + + offset += value.count() * sizeof(qint32); + } + break; + case QExifValue::Rational: + if(value.count() == 0) { + stream << quint32(0); + } else { + stream << offset; + + offset += value.count() * sizeof(quint32) * 2; + } + break; + case QExifValue::SignedRational: + if (value.count() == 0) { + stream << quint32(0); + } else { + stream << offset; + + offset += value.count() * sizeof(qint32) * 2; + } + break; + default: + qWarning() << "Invalid Ifd Type" << value.type(); + stream << quint32(0); + } + + return offset; +} + +void QExifImageHeader::writeExifValue(QDataStream &stream, const QExifValue &value) const +{ + switch (value.type()) { + case QExifValue::Byte: + if (value.count() > 4) + foreach (quint8 byte, value.toByteVector()) + stream << byte; + break; + case QExifValue::Undefined: + if (value.count() > 4) + stream.device()->write(value.toByteArray()); + break; + case QExifValue::Ascii: + if (value.count() > 4) { + QByteArray bytes = value.toByteArray(); + + stream.writeRawData(bytes.constData(), bytes.size() + 1); + } + break; + case QExifValue::Short: + if (value.count() > 2) + foreach(quint16 shrt, value.toShortVector()) + stream << shrt; + break; + case QExifValue::Long: + if(value.count() > 1) + foreach (quint32 lng, value.toLongVector()) + stream << lng; + break; + case QExifValue::SignedLong: + if (value.count() > 1) + foreach(qint32 lng, value.toSignedLongVector()) + stream << lng; + break; + case QExifValue::Rational: + if (value.count() > 0) + foreach (QExifURational rational, value.toRationalVector()) + stream << rational; + break; + case QExifValue::SignedRational: + if (value.count() > 0) + foreach (QExifSRational rational, value.toSignedRationalVector()) + stream << rational; + break; + default: + qWarning() << "Invalid Ifd Type" << value.type(); + break; + } +} + +template quint32 QExifImageHeader::writeExifHeaders( + QDataStream &stream, const QMap &values, quint32 offset) const +{ + offset += values.count() * 12; + + for (typename QMap::const_iterator i = values.constBegin(); i != values.constEnd(); i++) + offset = writeExifHeader(stream, i.key(), i.value(), offset); + + return offset; +} + +template void QExifImageHeader::writeExifValues( + QDataStream &stream, const QMap &values) const +{ + for (typename QMap::const_iterator i = values.constBegin(); i != values.constEnd(); i++) + writeExifValue(stream, i.value()); +} + +/*! + Writes an EXIF header to an I/O \a device. + + Returns the total number of bytes written. +*/ +qint64 QExifImageHeader::write(QIODevice *device) const +{ +//#ifndef QT_NO_DEBUG + qint64 startPos = device->pos(); +//#endif + + QDataStream stream( device ); + + if (d->byteOrder == QSysInfo::LittleEndian) { + stream.setByteOrder( QDataStream::LittleEndian ); + + device->write("II", 2); + device->write("\x2A\x00", 2); + device->write("\x08\x00\x00\x00", 4); + } else if (d->byteOrder == QSysInfo::BigEndian) { + stream.setByteOrder(QDataStream::BigEndian); + + device->write("MM", 2); + device->write("\x00\x2A", 2); + device->write("\x00\x00\x00\x08", 4); + } + + quint16 count = d->imageIfdValues.count() + 1; + quint32 offset = 26; + + if (!d->gpsIfdValues.isEmpty()) { + count++; + offset += 12; + } + + stream << count; + + offset = writeExifHeaders(stream, d->imageIfdValues, offset); + + quint32 exifIfdOffset = offset; + + stream << quint16( ExifIfdPointer ); + stream << quint16( QExifValue::Long ); + stream << quint32( 1 ); + stream << exifIfdOffset; + offset += calculateSize(d->exifIfdValues); + + quint32 gpsIfdOffset = offset; + + if (!d->gpsIfdValues.isEmpty()) { + stream << quint16(GpsInfoIfdPointer); + stream << quint16(QExifValue::Long); + stream << quint32(1); + stream << gpsIfdOffset; + + d->imageIfdValues.insert(ImageTag(GpsInfoIfdPointer), QExifValue(offset)); + + offset += calculateSize(d->gpsIfdValues); + } + + if (!d->thumbnailData.isEmpty()) + stream << offset; // Write offset to thumbnail Ifd. + else + stream << quint32(0); + + writeExifValues( stream, d->imageIfdValues ); + + Q_ASSERT(startPos + exifIfdOffset == device->pos()); + + stream << quint16(d->exifIfdValues.count()); + + writeExifHeaders(stream, d->exifIfdValues, exifIfdOffset); + writeExifValues(stream, d->exifIfdValues); + + Q_ASSERT(startPos + gpsIfdOffset == device->pos()); + + if (!d->gpsIfdValues.isEmpty()) { + stream << quint16(d->gpsIfdValues.count()); + + writeExifHeaders(stream, d->gpsIfdValues, gpsIfdOffset); + writeExifValues(stream, d->gpsIfdValues); + } + + Q_ASSERT(startPos + offset == device->pos()); + + if (!d->thumbnailData.isEmpty()) { + offset += 86; + + stream << quint16(7); + + QExifValue xResolution = d->thumbnailXResolution.isNull() + ? QExifValue(QExifURational(72, 1)) + : d->thumbnailXResolution; + + QExifValue yResolution = d->thumbnailYResolution.isNull() + ? QExifValue(QExifURational(72, 1)) + : d->thumbnailYResolution; + + QExifValue resolutionUnit = d->thumbnailResolutionUnit.isNull() + ? QExifValue(quint16(2)) + : d->thumbnailResolutionUnit; + + QExifValue orientation = d->thumbnailOrientation.isNull() + ? QExifValue(quint16(0)) + : d->thumbnailOrientation; + + writeExifHeader(stream, Compression, QExifValue(quint16(6)), offset); + + offset = writeExifHeader(stream, XResolution, xResolution, offset); + offset = writeExifHeader(stream, YResolution, yResolution, offset); + + writeExifHeader(stream, ResolutionUnit, resolutionUnit, offset); + writeExifHeader(stream, Orientation, orientation, offset); + writeExifHeader(stream, JpegInterchangeFormat, QExifValue(offset), offset); + writeExifHeader(stream, JpegInterchangeFormatLength, + QExifValue(quint32(d->thumbnailData.size())), offset); + + writeExifValue(stream, xResolution); + writeExifValue(stream, yResolution); + + Q_ASSERT(startPos + offset == device->pos()); + + device->write(d->thumbnailData); + + offset += d->thumbnailData.size(); + } + + Q_ASSERT(startPos + offset == device->pos()); + + d->size = offset; + + return offset; +} diff --git a/src/utils/QExifImageHeader.h b/src/utils/QExifImageHeader.h new file mode 100644 index 000000000..3f24e929b --- /dev/null +++ b/src/utils/QExifImageHeader.h @@ -0,0 +1,348 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the Qt scene graph research project. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// This file was copied from Qt Extended 4.5 + +#ifndef QEXIFIMAGEHEADER_H +#define QEXIFIMAGEHEADER_H + +#include +#include +#include +#include +#include +#include + +typedef QPair< quint32, quint32 > QExifURational; +typedef QPair< qint32, qint32 > QExifSRational; + +//Q_DECLARE_METATYPE(QExifURational) +//Q_DECLARE_METATYPE(QExifSRational) + +class QExifValuePrivate; + +class QExifValue +{ +public: + enum Type + { + Byte = 1, + Ascii = 2, + Short = 3, + Long = 4, + Rational = 5, + Undefined = 7, + SignedLong = 9, + SignedRational = 10 + }; + + enum TextEncoding + { + NoEncoding, + AsciiEncoding, + JisEncoding, + UnicodeEncoding, + UndefinedEncoding + }; + + QExifValue(); + QExifValue( quint8 value ); + QExifValue( const QVector< quint8 > &value ); + QExifValue( const QString &value, TextEncoding encoding = NoEncoding ); + QExifValue( quint16 value ); + QExifValue( const QVector< quint16 > &value ); + QExifValue( quint32 value ); + QExifValue( const QVector< quint32 > &value ); + QExifValue( const QExifURational &value ); + QExifValue( const QVector< QExifURational > &value ); + QExifValue( const QByteArray &value ); + QExifValue( qint32 value ); + QExifValue( const QVector< qint32 > &value ); + QExifValue( const QExifSRational &value ); + QExifValue( const QVector< QExifSRational > &value ); + QExifValue( const QDateTime &value ); + QExifValue( const QExifValue &other ); + QExifValue &operator =( const QExifValue &other ); + ~QExifValue(); + + bool operator ==( const QExifValue &other ) const; + + bool isNull() const; + + int type() const; + int count() const; + + TextEncoding encoding() const; + + quint8 toByte() const; + QVector< quint8 > toByteVector() const; + QString toString() const; + quint16 toShort() const; + QVector< quint16 > toShortVector() const; + quint32 toLong() const; + QVector< quint32 > toLongVector() const; + QExifURational toRational() const; + QVector< QExifURational > toRationalVector() const; + QByteArray toByteArray() const; + qint32 toSignedLong() const; + QVector< qint32 > toSignedLongVector() const; + QExifSRational toSignedRational() const; + QVector< QExifSRational > toSignedRationalVector() const; + QDateTime toDateTime() const; + +private: + QExplicitlySharedDataPointer< QExifValuePrivate > d; +}; + +struct ExifIfdHeader; + +class QExifImageHeaderPrivate; + +class QExifImageHeader +{ + Q_DISABLE_COPY(QExifImageHeader) +public: + enum ImageTag + { + + ImageWidth = 0x0100, + ImageLength = 0x0101, + BitsPerSample = 0x0102, + Compression = 0x0103, + PhotometricInterpretation = 0x0106, + Orientation = 0x0112, + SamplesPerPixel = 0x0115, + PlanarConfiguration = 0x011C, + YCbCrSubSampling = 0x0212, + XResolution = 0x011A, + YResolution = 0x011B, + ResolutionUnit = 0x0128, + StripOffsets = 0x0111, + RowsPerStrip = 0x0116, + StripByteCounts = 0x0117, + TransferFunction = 0x012D, + WhitePoint = 0x013E, + PrimaryChromaciticies = 0x013F, + YCbCrCoefficients = 0x0211, + ReferenceBlackWhite = 0x0214, + DateTime = 0x0132, + ImageDescription = 0x010E, + Make = 0x010F, + Model = 0x0110, + Software = 0x0131, + Artist = 0x013B, + Copyright = 0x8298 + }; + + enum ExifExtendedTag + { + ExifVersion = 0x9000, + FlashPixVersion = 0xA000, + ColorSpace = 0xA001, + ComponentsConfiguration = 0x9101, + CompressedBitsPerPixel = 0x9102, + PixelXDimension = 0xA002, + PixelYDimension = 0xA003, + MakerNote = 0x927C, + UserComment = 0x9286, + RelatedSoundFile = 0xA004, + DateTimeOriginal = 0x9003, + DateTimeDigitized = 0x9004, + SubSecTime = 0x9290, + SubSecTimeOriginal = 0x9291, + SubSecTimeDigitized = 0x9292, + ImageUniqueId = 0xA420, + + ExposureTime = 0x829A, + FNumber = 0x829D, + ExposureProgram = 0x8822, + SpectralSensitivity = 0x8824, + ISOSpeedRatings = 0x8827, + Oecf = 0x8828, + ShutterSpeedValue = 0x9201, + ApertureValue = 0x9202, + BrightnessValue = 0x9203, + ExposureBiasValue = 0x9204, + MaxApertureValue = 0x9205, + SubjectDistance = 0x9206, + MeteringMode = 0x9207, + LightSource = 0x9208, + Flash = 0x9209, + FocalLength = 0x920A, + SubjectArea = 0x9214, + FlashEnergy = 0xA20B, + SpatialFrequencyResponse = 0xA20C, + FocalPlaneXResolution = 0xA20E, + FocalPlaneYResolution = 0xA20F, + FocalPlaneResolutionUnit = 0xA210, + SubjectLocation = 0xA214, + ExposureIndex = 0xA215, + SensingMethod = 0xA217, + FileSource = 0xA300, + SceneType = 0xA301, + CfaPattern = 0xA302, + CustomRendered = 0xA401, + ExposureMode = 0xA402, + WhiteBalance = 0xA403, + DigitalZoomRatio = 0xA404, + FocalLengthIn35mmFilm = 0xA405, + SceneCaptureType = 0xA406, + GainControl = 0xA407, + Contrast = 0xA408, + Saturation = 0xA409, + Sharpness = 0xA40A, + DeviceSettingDescription = 0xA40B, + SubjectDistanceRange = 0x40C + }; + + enum GpsTag + { + GpsVersionId = 0x0000, + GpsLatitudeRef = 0x0001, + GpsLatitude = 0x0002, + GpsLongitudeRef = 0x0003, + GpsLongitude = 0x0004, + GpsAltitudeRef = 0x0005, + GpsAltitude = 0x0006, + GpsTimeStamp = 0x0007, + GpsSatellites = 0x0008, + GpsStatus = 0x0009, + GpsMeasureMode = 0x000A, + GpsDop = 0x000B, + GpsSpeedRef = 0x000C, + GpsSpeed = 0x000D, + GpsTrackRef = 0x000E, + GpsTrack = 0x000F, + GpsImageDirectionRef = 0x0010, + GpsImageDirection = 0x0011, + GpsMapDatum = 0x0012, + GpsDestLatitudeRef = 0x0013, + GpsDestLatitude = 0x0014, + GpsDestLongitudeRef = 0x0015, + GpsDestLongitude = 0x0016, + GpsDestBearingRef = 0x0017, + GpsDestBearing = 0x0018, + GpsDestDistanceRef = 0x0019, + GpsDestDistance = 0x001A, + GpsProcessingMethod = 0x001B, + GpsAreaInformation = 0x001C, + GpsDateStamp = 0x001D, + GpsDifferential = 0x001E + }; + + QExifImageHeader(); + explicit QExifImageHeader(const QString &fileName); + ~QExifImageHeader(); + + bool loadFromJpeg(const QString &fileName); + bool loadFromJpeg(QIODevice *device); + bool saveToJpeg(const QString &fileName) const; + bool saveToJpeg(QIODevice *device) const; + + bool read(QIODevice *device); + qint64 write(QIODevice *device) const; + + qint64 size() const; + + QSysInfo::Endian byteOrder() const; + + void clear(); + + QList imageTags() const; + QList extendedTags() const; + QList gpsTags() const; + + bool contains(ImageTag tag) const; + bool contains(ExifExtendedTag tag) const; + bool contains(GpsTag tag) const; + + void remove(ImageTag tag); + void remove(ExifExtendedTag tag); + void remove(GpsTag tag); + + QExifValue value(ImageTag tag) const; + QExifValue value(ExifExtendedTag tag) const; + QExifValue value(GpsTag tag) const; + + void setValue(ImageTag tag, const QExifValue &value); + void setValue(ExifExtendedTag tag, const QExifValue &value); + void setValue(GpsTag tag, const QExifValue &value); + + QImage thumbnail() const; + void setThumbnail( const QImage &thumbnail ); + +private: + enum PrivateTag + { + ExifIfdPointer = 0x8769, + GpsInfoIfdPointer = 0x8825, + InteroperabilityIfdPointer = 0xA005, + JpegInterchangeFormat = 0x0201, + JpegInterchangeFormatLength = 0x0202 + }; + + QByteArray extractExif( QIODevice *device ) const; + + QList< ExifIfdHeader > readIfdHeaders( QDataStream &stream ) const; + + QExifValue readIfdValue(QDataStream &stream, int startPos, const ExifIfdHeader &header) const; + template QMap readIfdValues( + QDataStream &stream, int startPos, const QList &headers) const; + template QMap readIfdValues( + QDataStream &stream, int startPos, const QExifValue &pointer) const; + + quint32 writeExifHeader(QDataStream &stream, quint16 tag, const QExifValue &value, quint32 offset) const; + void writeExifValue(QDataStream &stream, const QExifValue &value) const; + + template quint32 writeExifHeaders( + QDataStream &stream, const QMap &values, quint32 offset) const; + template void writeExifValues( + QDataStream &target, const QMap &values) const; + + quint32 sizeOf(const QExifValue &value) const; + + template quint32 calculateSize( + const QMap &values) const; + + QExifImageHeaderPrivate *d; +}; + +#endif