From 434aa248ad5710c7f65283fc3beb7e8adb8b1ad7 Mon Sep 17 00:00:00 2001 From: Eirik Aavitsland <eirik.aavitsland@qt.io> Date: Tue, 26 Feb 2019 16:23:08 +0100 Subject: [PATCH] Heic handler: fix orientation and other image properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mac heic handler lacked support for any meta-data i/o. Most notably, the image orientation proprty was ignored, so images read in could be wrongly oriented. Fixes: QTBUG-73415 Change-Id: I779f91dc28c7441b124aab4557e1abcd3e69fde9 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> --- .../imageformats/macheif/qmacheifhandler.cpp | 30 +-- .../imageformats/macheif/qmacheifhandler.h | 7 +- .../imageformats/shared/qiiofhelpers.cpp | 194 +++++++++++++++--- .../imageformats/shared/qiiofhelpers_p.h | 24 +++ tests/auto/heif/tst_qheif.cpp | 80 +++++++- tests/shared/images/heif.qrc | 1 + tests/shared/images/heif/newlogoCCW.heic | Bin 0 -> 4847 bytes 7 files changed, 287 insertions(+), 49 deletions(-) create mode 100644 tests/shared/images/heif/newlogoCCW.heic diff --git a/src/plugins/imageformats/macheif/qmacheifhandler.cpp b/src/plugins/imageformats/macheif/qmacheifhandler.cpp index 7c63e52a..385d5e2a 100644 --- a/src/plugins/imageformats/macheif/qmacheifhandler.cpp +++ b/src/plugins/imageformats/macheif/qmacheifhandler.cpp @@ -43,22 +43,8 @@ QT_BEGIN_NAMESPACE -class QMacHeifHandlerPrivate -{ - Q_DECLARE_PUBLIC(QMacHeifHandler) - Q_DISABLE_COPY(QMacHeifHandlerPrivate) -public: - QMacHeifHandlerPrivate(QMacHeifHandler *q_ptr) - : writeQuality(-1), q_ptr(q_ptr) - {} - - int writeQuality; - QMacHeifHandler *q_ptr; -}; - - QMacHeifHandler::QMacHeifHandler() - : d_ptr(new QMacHeifHandlerPrivate(this)) + : d(new QIIOFHelper(this)) { } @@ -90,28 +76,30 @@ bool QMacHeifHandler::canRead() const bool QMacHeifHandler::read(QImage *image) { - return QIIOFHelpers::readImage(this, image); + return d->readImage(image); } bool QMacHeifHandler::write(const QImage &image) { - return QIIOFHelpers::writeImage(this, image, QStringLiteral("public.heic")); + return d->writeImage(image, QStringLiteral("public.heic")); } QVariant QMacHeifHandler::option(ImageOption option) const { - return QVariant(); + return d->imageProperty(option); } void QMacHeifHandler::setOption(ImageOption option, const QVariant &value) { - Q_UNUSED(option) - Q_UNUSED(value) + d->setOption(option, value); } bool QMacHeifHandler::supportsOption(ImageOption option) const { - return false; + return option == Quality + || option == Size + || option == ImageTransformation + || option == TransformedByDefault; } QT_END_NAMESPACE diff --git a/src/plugins/imageformats/macheif/qmacheifhandler.h b/src/plugins/imageformats/macheif/qmacheifhandler.h index 6e94a596..cf63de8b 100644 --- a/src/plugins/imageformats/macheif/qmacheifhandler.h +++ b/src/plugins/imageformats/macheif/qmacheifhandler.h @@ -49,13 +49,13 @@ class QImage; class QByteArray; class QIODevice; class QVariant; -class QMacHeifHandlerPrivate; +class QIIOFHelper; class QMacHeifHandler : public QImageIOHandler { public: QMacHeifHandler(); - ~QMacHeifHandler(); + ~QMacHeifHandler() override; bool canRead() const override; bool read(QImage *image) override; @@ -67,8 +67,7 @@ public: static bool canRead(QIODevice *iod); private: - Q_DECLARE_PRIVATE(QMacHeifHandler) - QScopedPointer<QMacHeifHandlerPrivate> d_ptr; + QScopedPointer<QIIOFHelper> d; }; QT_END_NAMESPACE diff --git a/src/plugins/imageformats/shared/qiiofhelpers.cpp b/src/plugins/imageformats/shared/qiiofhelpers.cpp index 2b16787d..f8172fe8 100644 --- a/src/plugins/imageformats/shared/qiiofhelpers.cpp +++ b/src/plugins/imageformats/shared/qiiofhelpers.cpp @@ -38,15 +38,12 @@ ****************************************************************************/ #include <QGuiApplication> -#include <qpa/qplatformnativeinterface.h> #include <QBuffer> #include <QImageIOHandler> #include <QImage> -#include <private/qcore_mac_p.h> #include "qiiofhelpers_p.h" -#include <ImageIO/ImageIO.h> QT_BEGIN_NAMESPACE @@ -57,14 +54,14 @@ static size_t cbGetBytes(void *info, void *buffer, size_t count) QIODevice *dev = static_cast<QIODevice *>(info); if (!dev || !buffer) return 0; - qint64 res = dev->read(static_cast<char *>(buffer), count); - return qMax(qint64(0), res); + qint64 res = dev->read(static_cast<char *>(buffer), qint64(count)); + return size_t(qMax(qint64(0), res)); } static off_t cbSkipForward(void *info, off_t count) { QIODevice *dev = static_cast<QIODevice *>(info); - if (!dev || !count) + if (!dev || count <= 0) return 0; qint64 res = 0; if (!dev->isSequential()) { @@ -72,7 +69,7 @@ static off_t cbSkipForward(void *info, off_t count) dev->seek(prevPos + count); res = dev->pos() - prevPos; } else { - char *buf = new char[count]; + char *buf = new char[quint64(count)]; res = dev->read(buf, count); delete[] buf; } @@ -89,8 +86,8 @@ static size_t cbPutBytes(void *info, const void *buffer, size_t count) QIODevice *dev = static_cast<QIODevice *>(info); if (!dev || !buffer) return 0; - qint64 res = dev->write(static_cast<const char *>(buffer), count); - return qMax(qint64(0), res); + qint64 res = dev->write(static_cast<const char *>(buffer), qint64(count)); + return size_t(qMax(qint64(0), res)); } @@ -117,23 +114,50 @@ QImageIOPlugin::Capabilities QIIOFHelpers::systemCapabilities(const QString &uti } bool QIIOFHelpers::readImage(QImageIOHandler *q_ptr, QImage *out) +{ + QIIOFHelper h(q_ptr); + return h.readImage(out); +} + +bool QIIOFHelpers::writeImage(QImageIOHandler *q_ptr, const QImage &in, const QString &uti) +{ + QIIOFHelper h(q_ptr); + return h.writeImage(in, uti); +} + +QIIOFHelper::QIIOFHelper(QImageIOHandler *q) + : q_ptr(q) +{ +} + +bool QIIOFHelper::initRead() { static const CGDataProviderSequentialCallbacks cgCallbacks = { 0, &cbGetBytes, &cbSkipForward, &cbRewind, nullptr }; - if (!q_ptr || !q_ptr->device() || !out) + if (cgImageSource) + return true; + if (!q_ptr || !q_ptr->device()) return false; - QCFType<CGDataProviderRef> cgDataProvider; if (QBuffer *b = qobject_cast<QBuffer *>(q_ptr->device())) { // do direct access to avoid data copy const void *rawData = b->data().constData() + b->pos(); - cgDataProvider = CGDataProviderCreateWithData(nullptr, rawData, b->data().size() - b->pos(), nullptr); + cgDataProvider = CGDataProviderCreateWithData(nullptr, rawData, size_t(b->data().size() - b->pos()), nullptr); } else { cgDataProvider = CGDataProviderCreateSequential(q_ptr->device(), &cgCallbacks); } - QCFType<CGImageSourceRef> cgImageSource = CGImageSourceCreateWithDataProvider(cgDataProvider, nullptr); - if (!cgImageSource) + cgImageSource = CGImageSourceCreateWithDataProvider(cgDataProvider, nullptr); + + if (cgImageSource) + cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgImageSource, 0, nullptr); + + return (cgImageSource); +} + +bool QIIOFHelper::readImage(QImage *out) +{ + if (!out || !initRead()) return false; QCFType<CGImageRef> cgImage = CGImageSourceCreateImageAtIndex(cgImageSource, 0, nullptr); @@ -141,11 +165,117 @@ bool QIIOFHelpers::readImage(QImageIOHandler *q_ptr, QImage *out) return false; *out = qt_mac_toQImage(cgImage); - return !out->isNull(); + if (out->isNull()) + return false; + + int dpi = 0; + if (getIntProperty(kCGImagePropertyDPIWidth, &dpi)) + out->setDotsPerMeterX(qRound(dpi / 0.0254f)); + if (getIntProperty(kCGImagePropertyDPIHeight, &dpi)) + out->setDotsPerMeterY(qRound(dpi / 0.0254f)); + + return true; } +bool QIIOFHelper::getIntProperty(CFStringRef property, int *value) +{ + if (!cfImageDict) + return false; -bool QIIOFHelpers::writeImage(QImageIOHandler *q_ptr, const QImage &in, const QString &uti) + CFNumberRef cfNumber = static_cast<CFNumberRef>(CFDictionaryGetValue(cfImageDict, property)); + if (cfNumber) { + int intVal; + if (CFNumberGetValue(cfNumber, kCFNumberIntType, &intVal)) { + if (value) + *value = intVal; + return true; + } + } + return false; +} + +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; + } + return QImageIOHandler::TransformationNone; +} + +static int qt2Exif(QImageIOHandler::Transformations transformation) +{ + switch (transformation) { + case QImageIOHandler::TransformationNone: + return 1; + case QImageIOHandler::TransformationMirror: + return 2; + case QImageIOHandler::TransformationRotate180: + return 3; + case QImageIOHandler::TransformationFlip: + return 4; + case QImageIOHandler::TransformationFlipAndRotate90: + return 5; + case QImageIOHandler::TransformationRotate90: + return 6; + case QImageIOHandler::TransformationMirrorAndRotate90: + return 7; + case QImageIOHandler::TransformationRotate270: + return 8; + } + qWarning("Invalid Qt image transformation"); + return 1; +} + +QVariant QIIOFHelper::imageProperty(QImageIOHandler::ImageOption option) +{ + if (!initRead()) + return QVariant(); + + switch (option) { + case QImageIOHandler::Size: { + QSize sz; + if (getIntProperty(kCGImagePropertyPixelWidth, &sz.rwidth()) + && getIntProperty(kCGImagePropertyPixelHeight, &sz.rheight())) { + return sz; + } + break; + } + case QImageIOHandler::ImageTransformation: { + int orient; + if (getIntProperty(kCGImagePropertyOrientation, &orient)) + return int(exif2Qt(orient)); + break; + } + default: + break; + } + + return QVariant(); +} + +void QIIOFHelper::setOption(QImageIOHandler::ImageOption option, const QVariant &value) +{ + if (writeOptions.size() < option + 1) + writeOptions.resize(option + 1); + writeOptions[option] = value; +} + +bool QIIOFHelper::writeImage(const QImage &in, const QString &uti) { static const CGDataConsumerCallbacks cgCallbacks = { &cbPutBytes, nullptr }; @@ -159,17 +289,35 @@ bool QIIOFHelpers::writeImage(QImageIOHandler *q_ptr, const QImage &in, const QS if (!cgImageDest || !cgImage) return false; - QCFType<CFNumberRef> cfVal; - QCFType<CFDictionaryRef> cfProps; + QCFType<CFNumberRef> cfQuality = nullptr; + QCFType<CFNumberRef> cfOrientation = nullptr; + const void *dictKeys[2]; + const void *dictVals[2]; + int dictSize = 0; + if (q_ptr->supportsOption(QImageIOHandler::Quality)) { bool ok = false; - int writeQuality = q_ptr->option(QImageIOHandler::Quality).toInt(&ok); + int writeQuality = writeOptions.value(QImageIOHandler::Quality).toInt(&ok); // If quality is unset, default to 75% - float quality = (ok && writeQuality >= 0 ? (qMin(writeQuality, 100)) : 75) / 100.0; - cfVal = CFNumberCreate(nullptr, kCFNumberFloatType, &quality); - cfProps = CFDictionaryCreate(nullptr, (const void **)&kCGImageDestinationLossyCompressionQuality, (const void **)&cfVal, 1, - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + float quality = (ok && writeQuality >= 0 ? (qMin(writeQuality, 100)) : 75) / 100.0f; + cfQuality = CFNumberCreate(nullptr, kCFNumberFloatType, &quality); + dictKeys[dictSize] = static_cast<const void *>(kCGImageDestinationLossyCompressionQuality); + dictVals[dictSize] = static_cast<const void *>(cfQuality); + dictSize++; + } + if (q_ptr->supportsOption(QImageIOHandler::ImageTransformation)) { + int orient = qt2Exif(static_cast<QImageIOHandler::Transformation>(writeOptions.value(QImageIOHandler::ImageTransformation).toInt())); + cfOrientation = CFNumberCreate(nullptr, kCFNumberIntType, &orient); + dictKeys[dictSize] = static_cast<const void *>(kCGImagePropertyOrientation); + dictVals[dictSize] = static_cast<const void *>(cfOrientation); + dictSize++; } + + QCFType<CFDictionaryRef> cfProps = nullptr; + if (dictSize) + cfProps = CFDictionaryCreate(nullptr, dictKeys, dictVals, dictSize, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CGImageDestinationAddImage(cgImageDest, cgImage, cfProps); return CGImageDestinationFinalize(cgImageDest); } diff --git a/src/plugins/imageformats/shared/qiiofhelpers_p.h b/src/plugins/imageformats/shared/qiiofhelpers_p.h index da517318..1b1771b9 100644 --- a/src/plugins/imageformats/shared/qiiofhelpers_p.h +++ b/src/plugins/imageformats/shared/qiiofhelpers_p.h @@ -52,6 +52,9 @@ // #include <QImageIOPlugin> +#include <private/qcore_mac_p.h> +#include <ImageIO/ImageIO.h> +#include <QVector> QT_BEGIN_NAMESPACE @@ -67,6 +70,27 @@ public: static bool writeImage(QImageIOHandler *q_ptr, const QImage &in, const QString &uti); }; +class QIIOFHelper +{ +public: + QIIOFHelper(QImageIOHandler *q); + + bool readImage(QImage *out); + bool writeImage(const QImage &in, const QString &uti); + QVariant imageProperty(QImageIOHandler::ImageOption option); + void setOption(QImageIOHandler::ImageOption option, const QVariant &value); + +protected: + bool initRead(); + bool getIntProperty(CFStringRef property, int *value); + + QImageIOHandler *q_ptr = nullptr; + QVector<QVariant> writeOptions; + QCFType<CGDataProviderRef> cgDataProvider = nullptr; + QCFType<CGImageSourceRef> cgImageSource = nullptr; + QCFType<CFDictionaryRef> cfImageDict = nullptr; +}; + QT_END_NAMESPACE #endif diff --git a/tests/auto/heif/tst_qheif.cpp b/tests/auto/heif/tst_qheif.cpp index faf22fa1..26ddf731 100644 --- a/tests/auto/heif/tst_qheif.cpp +++ b/tests/auto/heif/tst_qheif.cpp @@ -37,6 +37,9 @@ private slots: void initTestCase(); void readImage_data(); void readImage(); + void readProperties_data(); + void readProperties(); + void writeImage(); }; void tst_qheif::initTestCase() @@ -49,8 +52,10 @@ void tst_qheif::readImage_data() { QTest::addColumn<QString>("fileName"); QTest::addColumn<QSize>("size"); + QTest::addColumn<int>("transform"); - QTest::newRow("col") << QString("col320x480.heic") << QSize(320, 480); + QTest::newRow("col") << QString("col320x480.heic") << QSize(320, 480) << int(QImageIOHandler::TransformationNone); + QTest::newRow("rot") << QString("newlogoCCW.heic") << QSize(110, 78) << int(QImageIOHandler::TransformationRotate90); } void tst_qheif::readImage() @@ -66,5 +71,78 @@ void tst_qheif::readImage() QCOMPARE(image.size(), size); } +void tst_qheif::readProperties_data() +{ + readImage_data(); +} + +void tst_qheif::readProperties() +{ + QFETCH(QString, fileName); + QFETCH(QSize, size); + QFETCH(int, transform); + + QSize rawSize = (transform & QImageIOHandler::TransformationRotate90) ? size.transposed() : size; + + QString path = QStringLiteral(":/heif/") + fileName; + QImageReader reader(path); + QCOMPARE(reader.size(), rawSize); + QCOMPARE(int(reader.transformation()), transform); + + QImage image = reader.read(); + QCOMPARE(image.size(), size); + + QCOMPARE(reader.size(), rawSize); + QCOMPARE(int(reader.transformation()), transform); +} + +void tst_qheif::writeImage() +{ + QImage img(20, 10, QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::green); + + QBuffer buf1, buf2; + QImage rimg1; + + { + buf1.open(QIODevice::WriteOnly); + QImageWriter writer(&buf1, "heic"); + QVERIFY(writer.write(img)); + buf1.close(); + QVERIFY(buf1.size() > 0); + + buf1.open(QIODevice::ReadOnly); + QImageReader reader(&buf1); + QVERIFY(reader.read(&rimg1)); + buf1.close(); + QVERIFY(rimg1.size() == img.size()); + } + + { + buf2.open(QIODevice::WriteOnly); + QImageWriter writer(&buf2, "heic"); + writer.setQuality(20); + QVERIFY(writer.write(img)); + buf2.close(); + QVERIFY(buf2.size() > 0); + QVERIFY(buf2.size() < buf1.size()); + } + + { + buf2.open(QIODevice::WriteOnly); + QImageWriter writer(&buf2, "heic"); + writer.setTransformation(QImageIOHandler::TransformationRotate270); + QVERIFY(writer.write(img)); + buf2.close(); + + QImage rimg2; + buf2.open(QIODevice::ReadOnly); + QImageReader reader(&buf2); + QVERIFY(reader.read(&rimg2)); + buf2.close(); + QVERIFY(rimg2.size() == img.size().transposed()); + } +} + QTEST_MAIN(tst_qheif) #include "tst_qheif.moc" diff --git a/tests/shared/images/heif.qrc b/tests/shared/images/heif.qrc index 2a41c36b..8232b6a5 100644 --- a/tests/shared/images/heif.qrc +++ b/tests/shared/images/heif.qrc @@ -1,5 +1,6 @@ <RCC> <qresource prefix="/"> <file>heif/col320x480.heic</file> + <file>heif/newlogoCCW.heic</file> </qresource> </RCC> diff --git a/tests/shared/images/heif/newlogoCCW.heic b/tests/shared/images/heif/newlogoCCW.heic new file mode 100644 index 0000000000000000000000000000000000000000..1604947e8b7998c14c42954f560608674da43be8 GIT binary patch literal 4847 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 E0dyOg?*IS* literal 0 HcmV?d00001 -- GitLab