Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "qtiffhandler_p.h"
++
++ #include <qcolorspace.h>
++ #include <qdebug.h>
++ #include <qfloat16.h>
++ #include <qimage.h>
++ #include <qloggingcategory.h>
++ #include <qvariant.h>
++ #include <qvarlengtharray.h>
++ #include <qbuffer.h>
++ #include <qfiledevice.h>
++ #include <qimagereader.h>
++
++ extern "C" {
++ #include "tiffio.h"
++ }
++
++ #include <memory>
++ #include <QtCore/q20utility.h>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_STATIC_LOGGING_CATEGORY(lcTiff, "qt.imageformats.tiff")
++
++ namespace {
++ struct TiffCloseDeleter {
++ void operator()(TIFF *p) const noexcept
++ { TIFFClose(p); } // unique_ptr only calls us for p != nullptr
++ };
++ }
++
++ using TiffUniquePtr = std::unique_ptr<TIFF, TiffCloseDeleter>;
++
++ tsize_t qtiffReadProc(thandle_t fd, tdata_t buf, tsize_t size)
++ {
++ QIODevice *device = static_cast<QIODevice *>(fd);
++ return device->isReadable() ? device->read(static_cast<char *>(buf), size) : -1;
++ }
++
++ tsize_t qtiffWriteProc(thandle_t fd, tdata_t buf, tsize_t size)
++ {
++ return static_cast<QIODevice *>(fd)->write(static_cast<char *>(buf), size);
++ }
++
++ toff_t qtiffSeekProc(thandle_t fd, toff_t off, int whence)
++ {
++ QIODevice *device = static_cast<QIODevice *>(fd);
++ switch (whence) {
++ case SEEK_SET:
++ device->seek(off);
++ break;
++ case SEEK_CUR:
++ device->seek(device->pos() + off);
++ break;
++ case SEEK_END:
++ device->seek(device->size() + off);
++ break;
++ }
++
++ return device->pos();
++ }
++
++ int qtiffCloseProc(thandle_t /*fd*/)
++ {
++ return 0;
++ }
++
++ toff_t qtiffSizeProc(thandle_t fd)
++ {
++ return static_cast<QIODevice *>(fd)->size();
++ }
++
++ int qtiffMapProc(thandle_t fd, void **base, toff_t *size)
++ {
++ QIODevice *device = static_cast<QIODevice *>(fd);
++
++ QFileDevice *file = qobject_cast<QFileDevice *>(device);
++ if (file) {
++ *base = file->map(0, file->size());
++ if (*base != nullptr) {
++ *size = file->size();
++ return 1;
++ }
++ } else {
++ QBuffer *buf = qobject_cast<QBuffer *>(device);
++ if (buf) {
++ *base = const_cast<char *>(buf->data().constData());
++ *size = buf->size();
++ return 1;
++ }
++ }
++ return 0;
++ }
++
++ void qtiffUnmapProc(thandle_t fd, void *base, toff_t /*size*/)
++ {
++ QFileDevice *file = qobject_cast<QFileDevice *>(static_cast<QIODevice *>(fd));
++ if (file && base)
++ file->unmap(static_cast<uchar *>(base));
++ }
++
++
++ class QTiffHandlerPrivate
++ {
++ public:
++ QTiffHandlerPrivate();
++ ~QTiffHandlerPrivate();
++
++ static bool canRead(QIODevice *device);
++ bool openForRead(QIODevice *device);
++ bool readHeaders(QIODevice *device);
++ bool readNextImage(QImage *image); // implementation of one read()
++ void close();
++ TiffUniquePtr openInternal(const char *mode, QIODevice *device);
++ #if TIFFLIB_VERSION >= 20221213
++ static int tiffErrorHandler(TIFF *tif, void *user_data, const char *,
++ const char *fmt, va_list ap);
++ static int tiffWarningHandler(TIFF *tif, void *user_data, const char *,
++ const char *fmt, va_list ap);
++ #endif
++ static void convert32BitOrder(void *buffer, int width);
++ void rgb48fixup(QImage *image);
++ static void rgb96fixup(QImage *image);
++ void rgbFixup(QImage *image);
++
++ TIFF *tiff = nullptr;
++ QTiffHandler::Compression compression = QTiffHandler::Compression::None;
++ QImageIOHandler::Transformations transformation = QImageIOHandler::TransformationNone;
++ QImage::Format format = QImage::Format_Invalid;
++ QSize size;
++ uint16_t photometric = {}; // no good default, so just value-init
++ bool grayscale = false;
++ bool floatingPoint = false;
++ bool separatePlanes = false;
++ bool headersRead = false;
++ int currentDirectory = 0;
++ int directoryCount = 0;
++ };
++
++ static QImageIOHandler::Transformations exif2Qt(int exifOrientation)
++ {
++ switch (exifOrientation) {
++ case 1: // normal
++ return QImageIOHandler::TransformationNone;
++ case 2: // mirror horizontal
++ return QImageIOHandler::TransformationMirror;
++ case 3: // rotate 180
++ return QImageIOHandler::TransformationRotate180;
++ case 4: // mirror vertical
++ return QImageIOHandler::TransformationFlip;
++ case 5: // mirror horizontal and rotate 270 CW
++ return QImageIOHandler::TransformationFlipAndRotate90;
++ case 6: // rotate 90 CW
++ return QImageIOHandler::TransformationRotate90;
++ case 7: // mirror horizontal and rotate 90 CW
++ return QImageIOHandler::TransformationMirrorAndRotate90;
++ case 8: // rotate 270 CW
++ return QImageIOHandler::TransformationRotate270;
++ }
++ qCWarning(lcTiff, "Invalid EXIF orientation");
++ 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;
++ }
++ qCWarning(lcTiff, "Invalid Qt image transformation");
++ return 1;
++ }
++
++ QTiffHandlerPrivate::QTiffHandlerPrivate()
++ = default;
++
++ QTiffHandlerPrivate::~QTiffHandlerPrivate()
++ {
++ close();
++ }
++
++ void QTiffHandlerPrivate::close()
++ {
++ if (tiff)
++ TIFFClose(tiff);
++ tiff = 0;
++ headersRead = false;
++ }
++
++ TiffUniquePtr QTiffHandlerPrivate::openInternal(const char *mode, QIODevice *device)
++ {
++ // TIFFLIB_VERSION 20221213 -> 4.5.0
++ #if TIFFLIB_VERSION >= 20221213
++ TIFFOpenOptions *opts = TIFFOpenOptionsAlloc();
++ TIFFOpenOptionsSetErrorHandlerExtR(opts, &tiffErrorHandler, this);
++ TIFFOpenOptionsSetWarningHandlerExtR(opts, &tiffWarningHandler, this);
++
++ #if TIFFLIB_AT_LEAST(4, 7, 0)
++ quint64 maxAlloc = quint64(QImageReader::allocationLimit()) << 20;
++ if (maxAlloc) {
++ maxAlloc = qMin(maxAlloc, quint64(std::numeric_limits<tmsize_t>::max()));
++ TIFFOpenOptionsSetMaxCumulatedMemAlloc(opts, tmsize_t(maxAlloc));
++ }
++ #endif
++
++ auto handle = TIFFClientOpenExt("foo",
++ mode,
++ device,
++ qtiffReadProc,
++ qtiffWriteProc,
++ qtiffSeekProc,
++ qtiffCloseProc,
++ qtiffSizeProc,
++ qtiffMapProc,
++ qtiffUnmapProc,
++ opts);
++ TIFFOpenOptionsFree(opts);
++ #else
++ auto handle = TIFFClientOpen("foo",
++ mode,
++ device,
++ qtiffReadProc,
++ qtiffWriteProc,
++ qtiffSeekProc,
++ qtiffCloseProc,
++ qtiffSizeProc,
++ qtiffMapProc,
++ qtiffUnmapProc);
++ #endif
++ return TiffUniquePtr{handle};
++ }
++
++
++ #if TIFFLIB_VERSION >= 20221213
++ int QTiffHandlerPrivate::tiffErrorHandler(TIFF *tif, void *user_data, const char *,
++ const char *fmt, va_list ap)
++ {
++ const auto priv = static_cast<QTiffHandlerPrivate *>(user_data);
++ if (!priv || priv->tiff != tif)
++ return 0;
++ qCCritical(lcTiff) << QString::vasprintf(fmt, ap);
++ return 1;
++ }
++
++ int QTiffHandlerPrivate::tiffWarningHandler(TIFF *tif, void *user_data, const char *,
++ const char *fmt, va_list ap)
++ {
++ const auto priv = static_cast<QTiffHandlerPrivate *>(user_data);
++ if (!priv || priv->tiff != tif)
++ return 0;
++ qCWarning(lcTiff) << QString::vasprintf(fmt, ap);
++ return 1;
++ }
++ #endif
++
++ bool QTiffHandlerPrivate::canRead(QIODevice *device)
++ {
++ if (!device) {
++ qCWarning(lcTiff, "QTiffHandler::canRead() called with no device");
++ return false;
++ }
++
++ // current implementation uses TIFFClientOpen which needs to be
++ // able to seek, so sequential devices are not supported
++ char h[4];
++ if (device->peek(h, 4) != 4)
++ return false;
++ if ((h[0] == 0x49 && h[1] == 0x49) && (h[2] == 0x2a || h[2] == 0x2b) && h[3] == 0)
++ return true; // Little endian, classic or bigtiff
++ if ((h[0] == 0x4d && h[1] == 0x4d) && h[2] == 0 && (h[3] == 0x2a || h[3] == 0x2b))
++ return true; // Big endian, classic or bigtiff
++ return false;
++ }
++
++ bool QTiffHandlerPrivate::openForRead(QIODevice *device)
++ {
++ if (tiff)
++ return true;
++
++ if (!canRead(device))
++ return false;
++
++ tiff = openInternal("rh", device).release();
++ return tiff != nullptr;
++ }
++
++ bool QTiffHandlerPrivate::readHeaders(QIODevice *device)
++ {
++ if (headersRead)
++ return true;
++
++ if (!openForRead(device))
++ return false;
++
++ if (!TIFFSetDirectory(tiff, currentDirectory)) {
++ close();
++ return false;
++ }
++
++ uint32_t width;
++ uint32_t height;
++ if (!TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &width)
++ || !TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &height)
++ || !TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric)) {
++ close();
++ return false;
++ }
++ size = QSize(width, height);
++
++ uint16_t orientationTag;
++ if (TIFFGetField(tiff, TIFFTAG_ORIENTATION, &orientationTag))
++ transformation = exif2Qt(orientationTag);
++
++ // BitsPerSample defaults to 1 according to the TIFF spec.
++ uint16_t bitPerSample;
++ if (!TIFFGetField(tiff, TIFFTAG_BITSPERSAMPLE, &bitPerSample))
++ bitPerSample = 1;
++ uint16_t samplesPerPixel; // they may be e.g. grayscale with 2 samples per pixel
++ if (!TIFFGetField(tiff, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel))
++ samplesPerPixel = 1;
++ uint16_t sampleFormat;
++ if (!TIFFGetField(tiff, TIFFTAG_SAMPLEFORMAT, &sampleFormat))
++ sampleFormat = SAMPLEFORMAT_VOID;
++ floatingPoint = (sampleFormat == SAMPLEFORMAT_IEEEFP);
++
++ uint16_t planarConfig;
++ if (!TIFFGetField(tiff, TIFFTAG_PLANARCONFIG, &planarConfig))
++ planarConfig = PLANARCONFIG_CONTIG;
++ separatePlanes = planarConfig == PLANARCONFIG_SEPARATE && samplesPerPixel > 1;
++
++ grayscale = photometric == PHOTOMETRIC_MINISBLACK || photometric == PHOTOMETRIC_MINISWHITE;
++
++ if (grayscale && bitPerSample == 1 && samplesPerPixel == 1)
++ format = QImage::Format_Mono;
++ else if (photometric == PHOTOMETRIC_MINISBLACK && bitPerSample == 8 && samplesPerPixel == 1)
++ format = QImage::Format_Grayscale8;
++ else if (photometric == PHOTOMETRIC_MINISBLACK && bitPerSample == 16 && samplesPerPixel == 1 && !floatingPoint)
++ format = QImage::Format_Grayscale16;
++ else if ((grayscale || photometric == PHOTOMETRIC_PALETTE) && bitPerSample == 8 && samplesPerPixel == 1)
++ format = QImage::Format_Indexed8;
++ else if (samplesPerPixel < 4) {
++ bool regular = (samplesPerPixel != 2) && (photometric == PHOTOMETRIC_RGB || photometric == PHOTOMETRIC_MINISBLACK);
++ if (bitPerSample == 16 && regular)
++ format = floatingPoint ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
++ else if (bitPerSample == 32 && floatingPoint && regular)
++ format = QImage::Format_RGBX32FPx4;
++ else
++ format = QImage::Format_RGB32;
++ } else {
++ uint16_t count;
++ uint16_t *extrasamples;
++ // If there is any definition of the alpha-channel, libtiff will return premultiplied
++ // data to us. If there is none, libtiff will not touch it and we assume it to be
++ // non-premultiplied, matching behavior of tested image editors, and how older Qt
++ // versions used to save it.
++ bool premultiplied = true;
++ bool gotField = TIFFGetField(tiff, TIFFTAG_EXTRASAMPLES, &count, &extrasamples);
++ if (!gotField || !count || extrasamples[0] == EXTRASAMPLE_UNSPECIFIED)
++ premultiplied = false;
++
++ if (bitPerSample == 16 && photometric == PHOTOMETRIC_RGB) {
++ // We read 64-bit raw, so unassoc remains unpremultiplied.
++ if (gotField && count && extrasamples[0] == EXTRASAMPLE_UNASSALPHA)
++ premultiplied = false;
++ if (premultiplied)
++ format = floatingPoint ? QImage::Format_RGBA16FPx4_Premultiplied : QImage::Format_RGBA64_Premultiplied;
++ else
++ format = floatingPoint ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
++ } else if (bitPerSample == 32 && floatingPoint && photometric == PHOTOMETRIC_RGB) {
++ if (gotField && count && extrasamples[0] == EXTRASAMPLE_UNASSALPHA)
++ premultiplied = false;
++ if (premultiplied)
++ format = QImage::Format_RGBA32FPx4_Premultiplied;
++ else
++ format = QImage::Format_RGBA32FPx4;
++ } else if (samplesPerPixel == 4 && bitPerSample == 8 && photometric == PHOTOMETRIC_SEPARATED) {
++ uint16_t inkSet;
++ const bool gotInkSetField = TIFFGetField(tiff, TIFFTAG_INKSET, &inkSet);
++ if (!gotInkSetField || inkSet == INKSET_CMYK) {
++ format = QImage::Format_CMYK8888;
++ } else {
++ close();
++ return false;
++ }
++ } else {
++ if (premultiplied)
++ format = QImage::Format_ARGB32_Premultiplied;
++ else
++ format = QImage::Format_ARGB32;
++ }
++ }
++
++ headersRead = true;
++ return true;
++ }
++
++ QTiffHandler::QTiffHandler()
++ : QImageIOHandler()
++ , d(new QTiffHandlerPrivate)
++ {
++ }
++
++ bool QTiffHandler::canRead() const
++ {
++ if (d->tiff)
++ return true;
++ if (QTiffHandlerPrivate::canRead(device())) {
++ setFormat("tiff");
++ return true;
++ }
++ return false;
++ }
++
++ bool QTiffHandler::canRead(QIODevice *device)
++ {
++ return QTiffHandlerPrivate::canRead(device);
++ }
++
++ bool QTiffHandler::read(QImage *image)
++ {
++ // Open file and read headers if it hasn't already been done.
++ if (!d->readHeaders(device()))
++ return false;
++
++ if (!d->readNextImage(image)) {
++ d->close();
++ return false;
++ }
++
++ return true;
++ }
++
++ bool QTiffHandlerPrivate::readNextImage(QImage *image)
++ {
++ if (!QImageIOHandler::allocateImage(size, format, image))
++ return false;
++
++ // Check for corrupt images early, before libtiff sinks time into parsing:
++ if (TIFFIsTiled(tiff) && TIFFTileSize64(tiff) > uint64_t(image->sizeInBytes()))
++ return false;
++
++ const quint32 width = size.width();
++ const quint32 height = size.height();
++
++ // Setup color tables
++ if (format == QImage::Format_Mono || format == QImage::Format_Indexed8) {
++ if (format == QImage::Format_Mono) {
++ QList<QRgb> colortable(2);
++ if (photometric == PHOTOMETRIC_MINISBLACK) {
++ colortable[0] = 0xff000000;
++ colortable[1] = 0xffffffff;
++ } else {
++ colortable[0] = 0xffffffff;
++ colortable[1] = 0xff000000;
++ }
++ image->setColorTable(colortable);
++ } else if (format == QImage::Format_Indexed8) {
++ const uint16_t tableSize = 256;
++ QList<QRgb> qtColorTable(tableSize);
++ if (grayscale) {
++ for (int i = 0; i<tableSize; ++i) {
++ const int c = (photometric == PHOTOMETRIC_MINISBLACK) ? i : (255 - i);
++ qtColorTable[i] = qRgb(c, c, c);
++ }
++ } else {
++ // create the color table
++ uint16_t *redTable = 0;
++ uint16_t *greenTable = 0;
++ uint16_t *blueTable = 0;
++ if (!TIFFGetField(tiff, TIFFTAG_COLORMAP, &redTable, &greenTable, &blueTable))
++ return false;
++ if (!redTable || !greenTable || !blueTable)
++ return false;
++
++ for (int i = 0; i<tableSize ;++i) {
++ // emulate libtiff behavior for 16->8 bit color map conversion: just ignore the lower 8 bits
++ const int red = redTable[i] >> 8;
++ const int green = greenTable[i] >> 8;
++ const int blue = blueTable[i] >> 8;
++ qtColorTable[i] = qRgb(red, green, blue);
++ }
++ }
++ image->setColorTable(qtColorTable);
++ // free redTable, greenTable and greenTable done by libtiff
++ }
++ }
++ bool format8bit = (format == QImage::Format_Mono || format == QImage::Format_Indexed8 || format == QImage::Format_Grayscale8);
++ bool format16bit = (format == QImage::Format_Grayscale16);
++ bool formatCmyk32bit = (format == QImage::Format_CMYK8888);
++ bool format64bit = (format == QImage::Format_RGBX64 || format == QImage::Format_RGBA64 || format == QImage::Format_RGBA64_Premultiplied);
++ bool format64fp = (format == QImage::Format_RGBX16FPx4 || format == QImage::Format_RGBA16FPx4 || format == QImage::Format_RGBA16FPx4_Premultiplied);
++ bool format128fp = (format == QImage::Format_RGBX32FPx4 || format == QImage::Format_RGBA32FPx4 || format == QImage::Format_RGBA32FPx4_Premultiplied);
++
++ // Formats we read directly, instead of over RGBA32:
++ if (format8bit || format16bit || formatCmyk32bit || format64bit || format64fp || format128fp) {
++ if (separatePlanes)
++ return false; // the direct-read path assumes interleaved samples
++ image->fill(0); // scanlines or tiles may fill partially, so ensure initialized
++
++ int bytesPerPixel = image->depth() / 8;
++ if (format == QImage::Format_RGBX64 || format == QImage::Format_RGBX16FPx4)
++ bytesPerPixel = photometric == PHOTOMETRIC_RGB ? 6 : 2;
++ else if (format == QImage::Format_RGBX32FPx4)
++ bytesPerPixel = photometric == PHOTOMETRIC_RGB ? 12 : 4;
++
++ if (TIFFIsTiled(tiff)) {
++ quint32 tileWidth, tileLength;
++ if (!TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tileWidth)
++ || !TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tileLength)
++ || !tileWidth || !tileLength || tileWidth % 16 || tileLength % 16)
++ {
++ return false;
++ }
++ quint32 byteWidth = (format == QImage::Format_Mono) ? (width + 7)/8 : (width * bytesPerPixel);
++ quint32 byteTileWidth = (format == QImage::Format_Mono) ? tileWidth/8 : (tileWidth * bytesPerPixel);
++ tmsize_t byteTileSize = TIFFTileSize(tiff);
++ if (byteTileSize > image->sizeInBytes() || byteTileSize / tileLength < byteTileWidth)
++ return false;
++ uchar *buf = (uchar *)_TIFFmalloc(byteTileSize);
++ if (!buf)
++ return false;
++ for (quint32 y = 0; y < height; y += tileLength) {
++ for (quint32 x = 0; x < width; x += tileWidth) {
++ if (TIFFReadTile(tiff, buf, x, y, 0, 0) < 0) {
++ _TIFFfree(buf);
++ return false;
++ }
++ quint32 linesToCopy = qMin(tileLength, height - y);
++ quint32 byteOffset = (format == QImage::Format_Mono) ? x/8 : (x * bytesPerPixel);
++ quint32 widthToCopy = qMin(byteTileWidth, byteWidth - byteOffset);
++ for (quint32 i = 0; i < linesToCopy; i++) {
++ ::memcpy(image->scanLine(y + i) + byteOffset, buf + (i * byteTileWidth), widthToCopy);
++ }
++ }
++ }
++ _TIFFfree(buf);
++ } else {
++ if (image->bytesPerLine() < TIFFScanlineSize(tiff))
++ return false;
++ for (uint32_t y=0; y<height; ++y) {
++ if (TIFFReadScanline(tiff, image->scanLine(y), y, 0) < 0)
++ return false;
++ }
++ }
++ if (format == QImage::Format_RGBX64 || format == QImage::Format_RGBX16FPx4) {
++ if (photometric == PHOTOMETRIC_RGB)
++ rgb48fixup(image);
++ else
++ rgbFixup(image);
++ } else if (format == QImage::Format_RGBX32FPx4) {
++ if (photometric == PHOTOMETRIC_RGB)
++ rgb96fixup(image);
++ else
++ rgbFixup(image);
++ }
++ } else {
++ const int stopOnError = 1;
++ if (TIFFReadRGBAImageOriented(tiff, width, height, reinterpret_cast<uint32_t *>(image->bits()),
++ qt2Exif(transformation), stopOnError))
++ {
++ for (uint32_t y=0; y<height; ++y)
++ convert32BitOrder(image->scanLine(y), width);
++ } else {
++ return false;
++ }
++ }
++
++
++ float resX = 0;
++ float resY = 0;
++ uint16_t resUnit;
++ 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));
++ break;
++ case RESUNIT_INCH:
++ image->setDotsPerMeterX(qRound(resX * (100 / 2.54)));
++ image->setDotsPerMeterY(qRound(resY * (100 / 2.54)));
++ break;
++ default:
++ // do nothing as defaults have already
++ // been set within the QImage class
++ break;
++ }
++ }
++
++ uint32_t count;
++ void *profile;
++ if (TIFFGetField(tiff, TIFFTAG_ICCPROFILE, &count, &profile)) {
++ QByteArray iccProfile(reinterpret_cast<const char *>(profile), count);
++ image->setColorSpace(QColorSpace::fromIccProfile(iccProfile));
++ }
++ // We do not handle colorimetric metadat not on ICC profile form, it seems to be a lot
++ // less common, and would need additional API in QColorSpace.
++
++ return true;
++ }
++
++ static bool checkGrayscale(const QList<QRgb> &colorTable)
++ {
++ if (colorTable.size() != 256)
++ return false;
++
++ const bool increasing = (colorTable.at(0) == 0xff000000);
++ for (int i = 0; i < 256; ++i) {
++ if ((increasing && colorTable.at(i) != qRgb(i, i, i))
++ || (!increasing && colorTable.at(i) != qRgb(255 - i, 255 - i, 255 - i)))
++ return false;
++ }
++ return true;
++ }
++
++ static QList<QRgb> effectiveColorTable(const QImage &image)
++ {
++ QList<QRgb> colors;
++ switch (image.format()) {
++ case QImage::Format_Indexed8:
++ colors = image.colorTable();
++ break;
++ case QImage::Format_Alpha8:
++ colors.resize(256);
++ for (int i = 0; i < 256; ++i)
++ colors[i] = qRgba(0, 0, 0, i);
++ break;
++ case QImage::Format_Grayscale8:
++ case QImage::Format_Grayscale16:
++ colors.resize(256);
++ for (int i = 0; i < 256; ++i)
++ colors[i] = qRgb(i, i, i);
++ break;
++ default:
++ Q_UNREACHABLE();
++ }
++ return colors;
++ }
++
++ static quint32 defaultStripSize(const TiffUniquePtr &p)
++ {
++ auto tiff = p.get();
++ // Aim for 4MB strips
++ qint64 scanSize = qMax(qint64(1), qint64(TIFFScanlineSize(tiff)));
++ qint64 numRows = (4 * 1024 * 1024) / scanSize;
++ quint32 reqSize = static_cast<quint32>(qBound(qint64(1), numRows, qint64(UINT_MAX)));
++ return TIFFDefaultStripSize(tiff, reqSize);
++ }
++
++ bool QTiffHandler::write(const QImage &image)
++ {
++ if (!device()->isWritable())
++ return false;
++
++ const auto tiff = d->openInternal("wB", device());
++ if (!tiff)
++ return false;
++
++ // image.scanLine() returns const uchar*, but TIFFWriteScanline wants non-const void*, adapt:
++ const auto writeScanline = [&](const void *line, int y) {
++ return TIFFWriteScanline(tiff.get(), const_cast<void*>(line), y);
++ };
++ // this one is just for DRYing:
++ const auto setField = [&] (uint32_t tag, auto&&...args) {
++ return TIFFSetField(tiff.get(), tag, std::forward<decltype(args)>(args)...);
++ };
++
++ const int width = image.width();
++ const int height = image.height();
++
++ if (!setField(TIFFTAG_IMAGEWIDTH, width)
++ || !setField(TIFFTAG_IMAGELENGTH, height)
++ || !setField(TIFFTAG_COMPRESSION, toLibTiffCompression(d->compression))
++ || !setField(TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG))
++ {
++ return false;
++ }
++
++ // set the resolution
++ bool resolutionSet = false;
++ const int dotPerMeterX = image.dotsPerMeterX();
++ const int dotPerMeterY = image.dotsPerMeterY();
++ if ((dotPerMeterX % 100) == 0
++ && (dotPerMeterY % 100) == 0) {
++ resolutionSet = setField(TIFFTAG_RESOLUTIONUNIT, RESUNIT_CENTIMETER)
++ && setField(TIFFTAG_XRESOLUTION, dotPerMeterX/100.0)
++ && setField(TIFFTAG_YRESOLUTION, dotPerMeterY/100.0);
++ } else {
++ resolutionSet = setField(TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH)
++ && setField(TIFFTAG_XRESOLUTION, static_cast<float>(image.logicalDpiX()))
++ && setField(TIFFTAG_YRESOLUTION, static_cast<float>(image.logicalDpiY()));
++ }
++ if (!resolutionSet)
++ return false;
++
++ // set the orienataion
++ if (!setField(TIFFTAG_ORIENTATION, qt2Exif(d->transformation)))
++ return false;
++
++ // set color space
++ const QByteArray iccProfile = image.colorSpace().iccProfile();
++ if (!iccProfile.isEmpty()) {
++ const auto size = static_cast<uint32_t>(iccProfile.size());
++ if (!q20::cmp_equal(size, iccProfile.size()) // narrowed
++ || !setField(TIFFTAG_ICCPROFILE, size, iccProfile.data()))
++ {
++ return false;
++ }
++ }
++
++ // configure image depth
++ const QImage::Format format = image.format();
++ if (format == QImage::Format_Mono || format == QImage::Format_MonoLSB) {
++ uint16_t photometric = PHOTOMETRIC_MINISBLACK;
++ if (image.colorTable().value(0) == 0xffffffff)
++ photometric = PHOTOMETRIC_MINISWHITE;
++ if (!setField(TIFFTAG_PHOTOMETRIC, photometric)
++ || !setField(TIFFTAG_BITSPERSAMPLE, 1)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++
++ // try to do the conversion in chunks no greater than 16 MB
++ const int chunks = int(image.sizeInBytes() / (1024 * 1024 * 16)) + 1;
++ const int chunkHeight = qMax(height / chunks, 1);
++
++ int y = 0;
++ while (y < height) {
++ QImage chunk = image.copy(0, y, width, qMin(chunkHeight, height - y)).convertToFormat(QImage::Format_Mono);
++
++ int chunkStart = y;
++ int chunkEnd = y + chunk.height();
++ while (y < chunkEnd) {
++ if (writeScanline(chunk.scanLine(y - chunkStart), y) != 1)
++ return false;
++ ++y;
++ }
++ }
++ } else if (format == QImage::Format_Indexed8
++ || format == QImage::Format_Grayscale8
++ || format == QImage::Format_Grayscale16
++ || format == QImage::Format_Alpha8) {
++ QList<QRgb> colorTable = effectiveColorTable(image);
++ bool isGrayscale = checkGrayscale(colorTable);
++ if (isGrayscale) {
++ uint16_t photometric = PHOTOMETRIC_MINISBLACK;
++ if (colorTable.at(0) == 0xffffffff)
++ photometric = PHOTOMETRIC_MINISWHITE;
++ if (!setField(TIFFTAG_PHOTOMETRIC, photometric)
++ || !setField(TIFFTAG_BITSPERSAMPLE, image.depth())
++ || !setField(TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++ } else {
++ if (!setField(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_PALETTE)
++ || !setField(TIFFTAG_BITSPERSAMPLE, 8)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++ // write the color table; libtiff reads 256 entries
++ constexpr qsizetype colorMapSize = 256;
++ const int tableSize = int(qMin(colorTable.size(), colorMapSize));
++ QVarLengthArray<uint16_t> redTable(colorMapSize, 0);
++ QVarLengthArray<uint16_t> greenTable(colorMapSize, 0);
++ QVarLengthArray<uint16_t> blueTable(colorMapSize, 0);
++
++ // set the color table
++ for (int i = 0; i<tableSize; ++i) {
++ const QRgb color = colorTable.at(i);
++ redTable[i] = qRed(color) * 257;
++ greenTable[i] = qGreen(color) * 257;
++ blueTable[i] = qBlue(color) * 257;
++ }
++
++ if (!setField(TIFFTAG_COLORMAP, redTable.data(), greenTable.data(), blueTable.data()))
++ return false;
++ }
++
++ //// write the data
++ for (int y = 0; y < height; ++y) {
++ if (writeScanline(image.scanLine(y), y) != 1)
++ return false;
++ }
++ } else if (format == QImage::Format_RGBX64 || format == QImage::Format_RGBX16FPx4) {
++ if (!setField(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB)
++ || !setField(TIFFTAG_SAMPLESPERPIXEL, 3)
++ || !setField(TIFFTAG_BITSPERSAMPLE, 16)
++ || !setField(TIFFTAG_SAMPLEFORMAT,
++ format == QImage::Format_RGBX64
++ ? SAMPLEFORMAT_UINT
++ : SAMPLEFORMAT_IEEEFP)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++ std::unique_ptr<quint16[]> rgb48line(new quint16[width * 3]);
++ for (int y = 0; y < height; ++y) {
++ const quint16 *srcLine = reinterpret_cast<const quint16 *>(image.constScanLine(y));
++ for (int x = 0; x < width; ++x) {
++ rgb48line[x * 3 + 0] = srcLine[x * 4 + 0];
++ rgb48line[x * 3 + 1] = srcLine[x * 4 + 1];
++ rgb48line[x * 3 + 2] = srcLine[x * 4 + 2];
++ }
++
++ if (writeScanline(rgb48line.get(), y) != 1)
++ return false;
++ }
++ } else if (format == QImage::Format_RGBA64
++ || format == QImage::Format_RGBA64_Premultiplied) {
++ const bool premultiplied = image.format() != QImage::Format_RGBA64;
++ const uint16_t extrasamples = premultiplied ? EXTRASAMPLE_ASSOCALPHA : EXTRASAMPLE_UNASSALPHA;
++ if (!setField(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB)
++ || !setField(TIFFTAG_SAMPLESPERPIXEL, 4)
++ || !setField(TIFFTAG_BITSPERSAMPLE, 16)
++ || !setField(TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT)
++ || !setField(TIFFTAG_EXTRASAMPLES, 1, &extrasamples)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++ for (int y = 0; y < height; ++y) {
++ if (writeScanline(image.scanLine(y), y) != 1)
++ return false;
++ }
++ } else if (format == QImage::Format_RGBX32FPx4) {
++ if (!setField(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB)
++ || !setField(TIFFTAG_SAMPLESPERPIXEL, 3)
++ || !setField(TIFFTAG_BITSPERSAMPLE, 32)
++ || !setField(TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++ std::unique_ptr<float[]> line(new float[width * 3]);
++ for (int y = 0; y < height; ++y) {
++ const float *srcLine = reinterpret_cast<const float *>(image.constScanLine(y));
++ for (int x = 0; x < width; ++x) {
++ line[x * 3 + 0] = srcLine[x * 4 + 0];
++ line[x * 3 + 1] = srcLine[x * 4 + 1];
++ line[x * 3 + 2] = srcLine[x * 4 + 2];
++ }
++
++ if (writeScanline(line.get(), y) != 1)
++ return false;
++ }
++ } else if (format == QImage::Format_RGBA16FPx4 || format == QImage::Format_RGBA32FPx4
++ || format == QImage::Format_RGBA16FPx4_Premultiplied
++ || format == QImage::Format_RGBA32FPx4_Premultiplied) {
++ const bool premultiplied = image.format() != QImage::Format_RGBA16FPx4 && image.format() != QImage::Format_RGBA32FPx4;
++ const uint16_t extrasamples = premultiplied ? EXTRASAMPLE_ASSOCALPHA : EXTRASAMPLE_UNASSALPHA;
++ if (!setField(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB)
++ || !setField(TIFFTAG_SAMPLESPERPIXEL, 4)
++ || !setField(TIFFTAG_BITSPERSAMPLE, image.depth() == 64 ? 16 : 32)
++ || !setField(TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP)
++ || !setField(TIFFTAG_EXTRASAMPLES, 1, &extrasamples)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++ for (int y = 0; y < height; ++y) {
++ if (writeScanline(image.scanLine(y), y) != 1)
++ return false;
++ }
++ } else if (format == QImage::Format_CMYK8888) {
++ if (!setField(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_SEPARATED)
++ || !setField(TIFFTAG_SAMPLESPERPIXEL, 4)
++ || !setField(TIFFTAG_BITSPERSAMPLE, 8)
++ || !setField(TIFFTAG_INKSET, INKSET_CMYK)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++
++ for (int y = 0; y < image.height(); ++y) {
++ if (writeScanline(image.scanLine(y), y) != 1)
++ return false;
++ }
++ } else if (!image.hasAlphaChannel()) {
++ if (!setField(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB)
++ || !setField(TIFFTAG_SAMPLESPERPIXEL, 3)
++ || !setField(TIFFTAG_BITSPERSAMPLE, 8)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++ // try to do the RGB888 conversion in chunks no greater than 16 MB
++ const int chunks = int(image.sizeInBytes() / (1024 * 1024 * 16)) + 1;
++ const int chunkHeight = qMax(height / chunks, 1);
++
++ int y = 0;
++ while (y < height) {
++ const QImage chunk = image.copy(0, y, width, qMin(chunkHeight, height - y)).convertToFormat(QImage::Format_RGB888);
++
++ int chunkStart = y;
++ int chunkEnd = y + chunk.height();
++ while (y < chunkEnd) {
++ if (writeScanline(chunk.scanLine(y - chunkStart), y) != 1)
++ return false;
++ ++y;
++ }
++ }
++ } else {
++ const bool premultiplied = image.format() != QImage::Format_ARGB32
++ && image.format() != QImage::Format_RGBA8888;
++ const uint16_t extrasamples = premultiplied ? EXTRASAMPLE_ASSOCALPHA : EXTRASAMPLE_UNASSALPHA;
++ if (!setField(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB)
++ || !setField(TIFFTAG_SAMPLESPERPIXEL, 4)
++ || !setField(TIFFTAG_BITSPERSAMPLE, 8)
++ || !setField(TIFFTAG_EXTRASAMPLES, 1, &extrasamples)
++ || !setField(TIFFTAG_ROWSPERSTRIP, defaultStripSize(tiff)))
++ {
++ return false;
++ }
++ // try to do the RGBA8888 conversion in chunks no greater than 16 MB
++ const int chunks = int(image.sizeInBytes() / (1024 * 1024 * 16)) + 1;
++ const int chunkHeight = qMax(height / chunks, 1);
++
++ const QImage::Format format = premultiplied ? QImage::Format_RGBA8888_Premultiplied
++ : QImage::Format_RGBA8888;
++ int y = 0;
++ while (y < height) {
++ const QImage chunk = image.copy(0, y, width, qMin(chunkHeight, height - y)).convertToFormat(format);
++
++ int chunkStart = y;
++ int chunkEnd = y + chunk.height();
++ while (y < chunkEnd) {
++ if (writeScanline(chunk.scanLine(y - chunkStart), y) != 1)
++ return false;
++ ++y;
++ }
++ }
++ }
++
++ return true;
++ }
++
++ QVariant QTiffHandler::option(ImageOption option) const
++ {
++ if (option == Size && canRead()) {
++ if (d->readHeaders(device()))
++ return d->size;
++ } else if (option == CompressionRatio) {
++ return int(d->compression);
++ } else if (option == ImageFormat) {
++ if (d->readHeaders(device()))
++ return d->format;
++ } else if (option == ImageTransformation) {
++ if (d->readHeaders(device()))
++ return int(d->transformation);
++ }
++ return QVariant();
++ }
++
++ void QTiffHandler::setOption(ImageOption option, const QVariant &value)
++ {
++ if (option == CompressionRatio && value.metaType().id() == QMetaType::Int)
++ d->compression = static_cast<Compression>(qBound(0, value.toInt(), 5));
++ if (option == ImageTransformation) {
++ int transformation = value.toInt();
++ if (transformation > 0 && transformation < 8)
++ d->transformation = QImageIOHandler::Transformations(transformation);
++ }
++ }
++
++ bool QTiffHandler::supportsOption(ImageOption option) const
++ {
++ return option == CompressionRatio
++ || option == Size
++ || option == ImageFormat
++ || option == ImageTransformation;
++ }
++
++ bool QTiffHandler::jumpToNextImage()
++ {
++ if (!ensureHaveDirectoryCount())
++ return false;
++ if (d->currentDirectory >= d->directoryCount - 1)
++ return false;
++
++ d->headersRead = false;
++ ++d->currentDirectory;
++ return true;
++ }
++
++ bool QTiffHandler::jumpToImage(int imageNumber)
++ {
++ if (!ensureHaveDirectoryCount())
++ return false;
++ if (imageNumber < 0 || imageNumber >= d->directoryCount)
++ return false;
++
++ if (d->currentDirectory != imageNumber) {
++ d->headersRead = false;
++ d->currentDirectory = imageNumber;
++ }
++ return true;
++ }
++
++ int QTiffHandler::imageCount() const
++ {
++ if (!ensureHaveDirectoryCount())
++ return 1;
++
++ return d->directoryCount;
++ }
++
++ int QTiffHandler::currentImageNumber() const
++ {
++ return d->currentDirectory;
++ }
++
++ void QTiffHandlerPrivate::convert32BitOrder(void *buffer, int width)
++ {
++ uint32_t *target = reinterpret_cast<uint32_t *>(buffer);
++ for (int32_t x=0; x<width; ++x) {
++ uint32_t p = target[x];
++ // convert between ARGB and ABGR
++ target[x] = (p & 0xff000000)
++ | ((p & 0x00ff0000) >> 16)
++ | (p & 0x0000ff00)
++ | ((p & 0x000000ff) << 16);
++ }
++ }
++
++ void QTiffHandlerPrivate::rgb48fixup(QImage *image)
++ {
++ Q_ASSERT(image->depth() == 64);
++ const int h = image->height();
++ const int w = image->width();
++ uchar *scanline = image->bits();
++ const qsizetype bpl = image->bytesPerLine();
++ quint16 mask = 0xffff;
++ const qfloat16 fp_mask = qfloat16(1.0f);
++ if (floatingPoint)
++ memcpy(&mask, &fp_mask, 2);
++ for (int y = 0; y < h; ++y) {
++ quint16 *dst = reinterpret_cast<uint16_t *>(scanline);
++ for (int x = w - 1; x >= 0; --x) {
++ dst[x * 4 + 3] = mask;
++ dst[x * 4 + 2] = dst[x * 3 + 2];
++ dst[x * 4 + 1] = dst[x * 3 + 1];
++ dst[x * 4 + 0] = dst[x * 3 + 0];
++ }
++ scanline += bpl;
++ }
++ }
++
++ void QTiffHandlerPrivate::rgb96fixup(QImage *image)
++ {
++ Q_ASSERT(image->depth() == 128);
++ const int h = image->height();
++ const int w = image->width();
++ uchar *scanline = image->bits();
++ const qsizetype bpl = image->bytesPerLine();
++ for (int y = 0; y < h; ++y) {
++ float *dst = reinterpret_cast<float *>(scanline);
++ for (int x = w - 1; x >= 0; --x) {
++ dst[x * 4 + 3] = 1.0f;
++ dst[x * 4 + 2] = dst[x * 3 + 2];
++ dst[x * 4 + 1] = dst[x * 3 + 1];
++ dst[x * 4 + 0] = dst[x * 3 + 0];
++ }
++ scanline += bpl;
++ }
++ }
++
++ void QTiffHandlerPrivate::rgbFixup(QImage *image)
++ {
++ if (image->depth() == 64) {
++ const int h = image->height();
++ const int w = image->width();
++ uchar *scanline = image->bits();
++ const qsizetype bpl = image->bytesPerLine();
++ quint16 mask = 0xffff;
++ const qfloat16 fp_mask = qfloat16(1.0f);
++ if (floatingPoint)
++ memcpy(&mask, &fp_mask, 2);
++ for (int y = 0; y < h; ++y) {
++ quint16 *dst = reinterpret_cast<quint16 *>(scanline);
++ for (int x = w - 1; x >= 0; --x) {
++ dst[x * 4 + 3] = mask;
++ dst[x * 4 + 2] = dst[x];
++ dst[x * 4 + 1] = dst[x];
++ dst[x * 4 + 0] = dst[x];
++ }
++ scanline += bpl;
++ }
++ } else {
++ Q_ASSERT(floatingPoint); // depth 128 is only 32-bit float (RGBX32FPx4)
++ const int h = image->height();
++ const int w = image->width();
++ uchar *scanline = image->bits();
++ const qsizetype bpl = image->bytesPerLine();
++ for (int y = 0; y < h; ++y) {
++ float *dst = reinterpret_cast<float *>(scanline);
++ for (int x = w - 1; x >= 0; --x) {
++ dst[x * 4 + 3] = 1.0f;
++ dst[x * 4 + 2] = dst[x];
++ dst[x * 4 + 1] = dst[x];
++ dst[x * 4 + 0] = dst[x];
++ }
++ scanline += bpl;
++ }
++ }
++ }
++
++ bool QTiffHandler::ensureHaveDirectoryCount() const
++ {
++ if (d->directoryCount > 0)
++ return true;
++
++ const auto tiff = d->openInternal("rh", device());
++
++ if (!tiff) {
++ device()->reset();
++ return false;
++ }
++
++ while (TIFFReadDirectory(tiff.get()))
++ ++d->directoryCount;
++ device()->reset();
++ return true;
++ }
++
++ int QTiffHandler::toLibTiffCompression(Compression compression) const
++ {
++ switch (compression) {
++ case Compression::None:
++ return COMPRESSION_NONE;
++ case Compression::Lzw:
++ return COMPRESSION_LZW;
++ case Compression::HuffmanRLE:
++ return COMPRESSION_CCITTRLE;
++ case Compression::Group3:
++ return COMPRESSION_CCITTFAX3;
++ case Compression::Group4:
++ return COMPRESSION_CCITTFAX4;
++ case Compression::Jpeg:
++ return COMPRESSION_JPEG;
++ }
++ qCWarning(lcTiff, "Invalid compression value (%d)", int(compression));
++ return COMPRESSION_NONE;
++ }
++
++ QT_END_NAMESPACE