How to retrieve original DPI attributes from an image file?
-
I can edit image file attributes in Photoshop or for example in FastStone Image Viewer and set any DPI value.
But when I load the image file to QImage.
QImage img; img.load(<path to image file>);
How to get this data?
Functions like QImage::devicePixelRatioX/Y() is not help.
-
Hi,
What changed did you make ?
-
Hi
This patches is not pretend on a complete solution, but...
I made some investigation regarding the problem described above, and found that for some image formats Qt really using values from the image metadata/Exif but for the some of that not.
This is why I add new essences (QImage::get/setOriginalDPIX/Y ()) because it's confusing, and I do not want to break some compatibilities (if it exists).
So.. here are the patches:qjpeghandler.cpp
--- qtbase/src/plugins/imageformats/jpeg/qjpeghandler.cpp 2019-12-07 06:27:08.000000000 +0200 +++ qtbase/src/plugins/imageformats/jpeg/qjpeghandler.cpp 2020-02-16 09:11:31.533186500 +0200 @@ -46,20 +46,21 @@ #include <qvariant.h> #include <qvector.h> #include <qbuffer.h> #include <qmath.h> #include <private/qicc_p.h> #include <private/qsimd_p.h> #include <private/qimage_p.h> // for qt_getImageText #include <stdio.h> // jpeglib needs this to be pre-included #include <setjmp.h> +#include <optional> #ifdef FAR #undef FAR #endif // including jpeglib.h seems to be a little messy extern "C" { // jpeglib.h->jmorecfg.h tries to typedef int boolean; but this conflicts with // some Windows headers that may or may not have been included #ifdef HAVE_BOOLEAN @@ -721,39 +722,40 @@ { public: enum State { Ready, ReadHeader, ReadingEnd, Error }; QJpegHandlerPrivate(QJpegHandler *qq) - : quality(75), transformation(QImageIOHandler::TransformationNone), iod_src(0), + : quality(75), transformation(QImageIOHandler::TransformationNone), DPIX(96.), DPIY(96.), iod_src(0), rgb888ToRgb32ConverterPtr(qt_convert_rgb888_to_rgb32), state(Ready), optimize(false), progressive(false), q(qq) {} ~QJpegHandlerPrivate() { if(iod_src) { jpeg_destroy_decompress(&info); delete iod_src; iod_src = 0; } } bool readJpegHeader(QIODevice*); bool read(QImage *image); int quality; QImageIOHandler::Transformations transformation; + qreal DPIX, DPIY; QVariant size; QImage::Format format; QSize scaledSize; QRect scaledClipRect; QRect clipRect; QString description; QStringList readTexts; QByteArray iccProfile; struct jpeg_decompress_struct info; @@ -772,112 +774,183 @@ 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; } +const quint16 exif_tag_Orientattion = 0x0112; +const quint16 exif_tag_XResolution = 0x011A; +const quint16 exif_tag_YResolution = 0x011B; + +struct pair_quint32 +{ + quint32 nominator, denominator; +}; + +union exif_values { + quint16 uint16; + pair_quint32 pair_uint32; +}; + +/* + Validate Exif tag +*/ +static bool validateTag(quint16 req_tag, quint32 req_components, exif_values value) +{ + switch (req_tag) { + case exif_tag_Orientattion: { + if (req_components != 1) + return false; + if (value.uint16 < 1 || value.uint16 > 8) // check for valid range + return false; + + return true; + } break; + case exif_tag_XResolution: + case exif_tag_YResolution: { + if (req_components != 1) + return false; + + return true; + } break; + } + + return false; +} + /* * 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) +static std::optional<exif_values> getExifTag(quint16 req_tag, QByteArray &exifData) { // Current EXIF version (2.3) says there can be at most 5 IFDs, // byte we allow for 10 so we're able to deal with future extensions. const int maxIfdCount = 10; QDataStream stream(&exifData, QIODevice::ReadOnly); if (!readExifHeader(stream)) - return -1; + return std::nullopt; quint16 val; quint32 offset; - const qint64 headerStart = 6; // the EXIF header has a constant size + const qint64 headerStart = 6; // the EXIF header has a constant size Q_ASSERT(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 + return std::nullopt; // unknown byte order // confirm byte order stream >> val; if (val != 0x2a) - return -1; + return std::nullopt; stream >> offset; // read IFD for (int n = 0; n < maxIfdCount; ++n) { quint16 numEntries; const qint64 bytesToSkip = offset - (stream.device()->pos() - headerStart); if (bytesToSkip < 0 || (offset + headerStart >= exifData.size())) { // disallow going backwards, though it's permitted in the spec - return -1; + return std::nullopt; } else if (bytesToSkip != 0) { // seek to the IFD if (!stream.device()->seek(offset + headerStart)) - return -1; + return std::nullopt; } stream >> numEntries; + enum { exif_record_skip = sizeof(quint16) * 3 + sizeof(quint32) }; + for (; numEntries > 0 && stream.status() == QDataStream::Ok; --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 tag + stream >> tag; + if (req_tag != tag) { + stream.skipRawData(exif_record_skip); + } else { + quint16 type; + // read type + stream >> type; + + // currently support( type 3(quint16) ,5(quint32, quint32)) + switch (type) { + // quint16 + case 3: { + quint32 components; + exif_values value; + quint16 dummy; + + stream >> components >> value.uint16 >> dummy; + + if (!validateTag(req_tag, components, value)) + return std::nullopt; + + return std::make_optional<exif_values>(value); + } break; + // (quint32, quint32) + case 5: { + quint32 components; + quint32 offset; + + stream >> components >> offset; + + exif_values value; + //+6 = Exif\0\0 + stream.skipRawData(offset - stream.device()->pos() + 6); + stream >> value.pair_uint32.nominator >> value.pair_uint32.denominator; + + if (!validateTag(req_tag, components, value)) + return std::nullopt; + + return std::make_optional<exif_values>(value); + } break; + default: { + stream.skipRawData(exif_record_skip - sizeof(quint16)); + } break; + } // switch + } // else + } // for // read offset to next IFD stream >> offset; if (stream.status() != QDataStream::Ok) - return -1; + return std::nullopt; if (offset == 0) // this is the last IFD - return 0; // No Exif orientation was found + return std::nullopt; // No Exif orientation was found } // too many IFDs - return -1; + return std::nullopt; } 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 @@ -951,24 +1024,53 @@ } else if (marker->marker == JPEG_APP0 + 1) { exifData.append((const char*)marker->data, marker->data_length); } else if (marker->marker == JPEG_APP0 + 2) { if (marker->data_length > 128 + 4 + 14 && strcmp((const char *)marker->data, "ICC_PROFILE") == 0) { iccProfile.append((const char*)marker->data + 14, marker->data_length - 14); } } } if (!exifData.isEmpty()) { - // Exif data present - int exifOrientation = getExifOrientation(exifData); - if (exifOrientation > 0) - transformation = exif2Qt(exifOrientation); + /* + * 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 + */ + std::optional<exif_values> exifOrientation = + getExifTag(exif_tag_Orientattion, exifData); + if (exifOrientation) + transformation = exif2Qt(exifOrientation->uint16); + + std::optional<exif_values> exifXResolution = + getExifTag(exif_tag_XResolution, exifData); + std::optional<exif_values> exifYResolution = + getExifTag(exif_tag_YResolution, exifData); + + if (exifXResolution && exifYResolution) { + DPIX = exifXResolution->pair_uint32.nominator + / (qreal)exifXResolution->pair_uint32.denominator; + DPIY = exifYResolution->pair_uint32.nominator + / (qreal)exifYResolution->pair_uint32.denominator; + } + } + else + { + DPIX = info.X_density; + DPIY = info.Y_density; } state = ReadHeader; return true; } else { return false; } } @@ -985,20 +1087,23 @@ if(state == ReadHeader) { bool success = read_jpeg_image(image, scaledSize, scaledClipRect, clipRect, quality, rgb888ToRgb32ConverterPtr, &info, &err); if (success) { for (int i = 0; i < readTexts.size()-1; i+=2) image->setText(readTexts.at(i), readTexts.at(i+1)); if (!iccProfile.isEmpty()) image->setColorSpace(QColorSpace::fromIccProfile(iccProfile)); + image->setOriginalDPIX(DPIX); + image->setOriginalDPIY(DPIY); + state = ReadingEnd; return true; } state = Error; } return false; }
qbmphandler.cpp
--- qtbase/src/gui/image/qbmphandler.cpp 2019-12-07 06:27:08.000000000 +0200 +++ qtbase/src/gui/image/qbmphandler.cpp 2020-02-12 19:47:59.316533900 +0200 @@ -740,20 +740,23 @@ // Intel byte order s.setByteOrder(QDataStream::LittleEndian); // read image const bool readSuccess = m_format == BmpFormat ? read_dib_body(s, infoHeader, fileHeader.bfOffBits, startpos, *image) : read_dib_body(s, infoHeader, -1, startpos - BMP_FILEHDR_SIZE, *image); if (!readSuccess) return false; + image->setOriginalDPIX(infoHeader.biXPelsPerMeter * 0.0254); + image->setOriginalDPIY(infoHeader.biYPelsPerMeter * 0.0254); + state = Ready; return true; } bool QBmpHandler::write(const QImage &img) { QImage image; switch (img.format()) { case QImage::Format_Mono: case QImage::Format_Indexed8:
qpnghandler.cpp
--- qtbase/src/gui/image/qpnghandler.cpp 2019-12-07 06:27:08.000000000 +0200 +++ qtbase/src/gui/image/qpnghandler.cpp 2020-02-12 21:33:13.707239900 +0200 @@ -713,20 +713,22 @@ amp.row_pointers = new png_bytep[height]; for (uint y = 0; y < height; y++) amp.row_pointers[y] = data + y * bpl; png_read_image(png_ptr, amp.row_pointers); amp.deallocate(); outImage->setDotsPerMeterX(png_get_x_pixels_per_meter(png_ptr,info_ptr)); outImage->setDotsPerMeterY(png_get_y_pixels_per_meter(png_ptr,info_ptr)); + outImage->setOriginalDPIX(png_get_x_pixels_per_meter(png_ptr, info_ptr) * 0.0254); + outImage->setOriginalDPIY(png_get_y_pixels_per_meter(png_ptr, info_ptr) * 0.0254); if (unit_type == PNG_OFFSET_PIXEL) outImage->setOffset(QPoint(offset_x, offset_y)); // sanity check palette entries if (color_type == PNG_COLOR_TYPE_PALETTE && outImage->format() == QImage::Format_Indexed8) { int color_table_size = outImage->colorCount(); for (int y=0; y<(int)height; ++y) { uchar *p = FAST_SCAN_LINE(data, bpl, y); uchar *end = p + width;
qtiffhandler.cpp
--- qtimageformats/src/plugins/imageformats/tiff/qtiffhandler.cpp 2019-11-19 14:47:50.000000000 +0200 +++ qtimageformats/src/plugins/imageformats/tiff/qtiffhandler.cpp 2020-02-12 23:08:13.882787800 +0200 @@ -470,24 +470,28 @@ if (!TIFFGetField(tiff, TIFFTAG_RESOLUTIONUNIT, &resUnit)) resUnit = RESUNIT_INCH; if (TIFFGetField(tiff, TIFFTAG_XRESOLUTION, &resX) && TIFFGetField(tiff, TIFFTAG_YRESOLUTION, &resY)) { switch(resUnit) { case RESUNIT_CENTIMETER: image->setDotsPerMeterX(qRound(resX * 100)); image->setDotsPerMeterY(qRound(resY * 100)); + image->setOriginalDPIX((resX * 100) * 0.254); + image->setOriginalDPIY((resY * 100) * 0.254); break; case RESUNIT_INCH: image->setDotsPerMeterX(qRound(resX * (100 / 2.54))); image->setDotsPerMeterY(qRound(resY * (100 / 2.54))); + image->setOriginalDPIX(resX); + image->setOriginalDPIY(resY); break; default: // do nothing as defaults have already // been set within the QImage class break; } } uint32 count; void *profile;
qimage_p.h
--- qtbase/src/gui/image/qimage_p.h 2019-12-07 06:27:08.000000000 +0200 +++ qtbase/src/gui/image/qimage_p.h 2020-02-12 15:44:52.016150000 +0200 @@ -78,20 +78,22 @@ qreal devicePixelRatio; QVector<QRgb> colortable; uchar *data; QImage::Format format; qsizetype bytes_per_line; int ser_no; // serial number int detach_no; qreal dpmx; // dots per meter X (or 0) qreal dpmy; // dots per meter Y (or 0) + qreal odpix; + qreal odpiy; QPoint offset; // offset in pixels uint own_data : 1; uint ro_data : 1; uint has_alpha_clut : 1; uint is_cached : 1; uint is_locked : 1; QImageCleanupFunction cleanupFunction; void* cleanupInfo;
qimage.h
--- qtbase/src/gui/image/qimage.h 2019-12-07 06:27:08.000000000 +0200 +++ qtbase/src/gui/image/qimage.h 2020-02-12 23:08:13.886784700 +0200 @@ -326,20 +326,22 @@ QPaintEngine *paintEngine() const override; // Auxiliary data int dotsPerMeterX() const; int dotsPerMeterY() const; void setDotsPerMeterX(int); void setDotsPerMeterY(int); QPoint offset() const; void setOffset(const QPoint&); + qreal originalDPIX() const; + qreal originalDPIY() const; QStringList textKeys() const; QString text(const QString &key = QString()) const; void setText(const QString &key, const QString &value); QPixelFormat pixelFormat() const noexcept; static QPixelFormat toPixelFormat(QImage::Format format) noexcept; static QImage::Format toImageFormat(QPixelFormat format) noexcept; // Platform specific conversion functions @@ -365,20 +367,29 @@ virtual int metric(PaintDeviceMetric metric) const override; QImage mirrored_helper(bool horizontal, bool vertical) const; QImage rgbSwapped_helper() const; void mirrored_inplace(bool horizontal, bool vertical); void rgbSwapped_inplace(); QImage convertToFormat_helper(Format format, Qt::ImageConversionFlags flags) const; bool convertToFormat_inplace(Format format, Qt::ImageConversionFlags flags); QImage smoothScaled(int w, int h) const; private: + friend class QJpegHandlerPrivate; + friend class QBmpHandler; + friend class QPngHandlerPrivate; + friend class QTiffHandler; + + void setOriginalDPIX(qreal); + void setOriginalDPIY(qreal); + +private: friend class QWSOnScreenSurface; QImageData *d; friend class QRasterPlatformPixmap; friend class QBlittablePlatformPixmap; friend class QPixmapCacheEntry; public: typedef QImageData * DataPtr; inline DataPtr &data_ptr() { return d; }
qimage.cpp
--- qtbase/src/gui/image/qimage.cpp 2019-12-07 06:27:08.000000000 +0200 +++ qtbase/src/gui/image/qimage.cpp 2020-02-12 15:49:10.283828100 +0200 @@ -98,20 +98,22 @@ return 1 + serial.fetchAndAddRelaxed(1); } QImageData::QImageData() : ref(0), width(0), height(0), depth(0), nbytes(0), devicePixelRatio(1.0), data(0), format(QImage::Format_ARGB32), bytes_per_line(0), ser_no(next_qimage_serial_number()), detach_no(0), dpmx(qt_defaultDpiX() * 100 / qreal(2.54)), dpmy(qt_defaultDpiY() * 100 / qreal(2.54)), + odpix(-1), + odpiy(-1), offset(0, 0), own_data(true), ro_data(false), has_alpha_clut(false), is_cached(false), is_locked(false), cleanupFunction(0), cleanupInfo(0), paintEngine(0) { } /*! \fn QImageData * QImageData::create(const QSize &size, QImage::Format format) \internal @@ -3944,20 +3946,42 @@ void QImage::setDotsPerMeterY(int y) { if (!d || !y) return; detach(); if (d) d->dpmy = y; } +qreal QImage::originalDPIX() const +{ + return d->odpix; +} + +qreal QImage::originalDPIY() const +{ + return d->odpiy; +} + +void QImage::setOriginalDPIX(qreal x) +{ + if (d) + d->odpix = x; +} + +void QImage::setOriginalDPIY(qreal y) +{ + if (d) + d->odpiy = y; +} + /*! \fn QPoint QImage::offset() const Returns the number of pixels by which the image is intended to be offset by when positioning relative to other images. \sa setOffset(), {QImage#Image Information}{Image Information} */ QPoint QImage::offset() const {