From 90a8de656fe689b6aa856e70e2d22de6630ea855 Mon Sep 17 00:00:00 2001 From: Allan Sandfeld Jensen Date: Tue, 26 Jun 2018 17:12:02 +0200 Subject: [PATCH] Long live QColorSpace and friends Adds QColorSpace and QColorTransform classes, and parsing of a common subset of ICC profiles found in images, and also parses the ICC profiles in PNG and JPEGs. For backwards compatibility no automatic color handling is done by this patch. [ChangeLog][QtGui] A QColorSpace class has been added, and color spaces are now parsed from PNG and JPEG images. No automatic color space conversion is done however, and applications must request it. Change-Id: Ic09935f84640a716467fa3a9ed1e73c02daf3675 Reviewed-by: Eirik Aavitsland --- .../widgets/imageviewer/imageviewer.cpp | 2 + src/gui/image/qimage.cpp | 134 +++- src/gui/image/qimage.h | 10 +- src/gui/image/qimage_conversions.cpp | 4 +- src/gui/image/qimage_p.h | 4 + src/gui/image/qpnghandler.cpp | 72 +- src/gui/kernel/qguiapplication.cpp | 30 +- src/gui/kernel/qguiapplication_p.h | 11 +- src/gui/painting/painting.pri | 16 +- src/gui/painting/qcolormatrix_p.h | 214 ++++++ src/gui/painting/qcolorspace.cpp | 633 ++++++++++++++++ src/gui/painting/qcolorspace.h | 136 ++++ src/gui/painting/qcolorspace_p.h | 96 +++ src/gui/painting/qcolortransferfunction_p.h | 207 ++++++ src/gui/painting/qcolortransfertable_p.h | 245 +++++++ src/gui/painting/qcolortransform.cpp | 679 ++++++++++++++++++ src/gui/painting/qcolortransform.h | 93 +++ src/gui/painting/qcolortransform_p.h | 89 +++ src/gui/painting/qcolortrc_p.h | 129 ++++ .../{qcolorprofile.cpp => qcolortrclut.cpp} | 49 +- .../{qcolorprofile_p.h => qcolortrclut_p.h} | 72 +- src/gui/painting/qdrawhelper.cpp | 22 +- src/gui/painting/qicc.cpp | 669 +++++++++++++++++ src/gui/painting/qicc_p.h | 70 ++ src/gui/painting/qpainter_p.h | 2 + .../imageformats/jpeg/qjpeghandler.cpp | 14 +- tests/auto/gui/painting/painting.pro | 1 + tests/auto/gui/painting/qcolor/tst_qcolor.cpp | 5 +- .../gui/painting/qcolorspace/qcolorspace.pro | 9 + .../qcolorspace/resources/ProPhoto.jpg | Bin 0 -> 30900 bytes .../qcolorspace/resources/sRGB2014.icc | Bin 0 -> 3024 bytes .../painting/qcolorspace/tst_qcolorspace.cpp | 238 ++++++ 32 files changed, 3863 insertions(+), 92 deletions(-) create mode 100644 src/gui/painting/qcolormatrix_p.h create mode 100644 src/gui/painting/qcolorspace.cpp create mode 100644 src/gui/painting/qcolorspace.h create mode 100644 src/gui/painting/qcolorspace_p.h create mode 100644 src/gui/painting/qcolortransferfunction_p.h create mode 100644 src/gui/painting/qcolortransfertable_p.h create mode 100644 src/gui/painting/qcolortransform.cpp create mode 100644 src/gui/painting/qcolortransform.h create mode 100644 src/gui/painting/qcolortransform_p.h create mode 100644 src/gui/painting/qcolortrc_p.h rename src/gui/painting/{qcolorprofile.cpp => qcolortrclut.cpp} (69%) rename src/gui/painting/{qcolorprofile_p.h => qcolortrclut_p.h} (87%) create mode 100644 src/gui/painting/qicc.cpp create mode 100644 src/gui/painting/qicc_p.h create mode 100644 tests/auto/gui/painting/qcolorspace/qcolorspace.pro create mode 100644 tests/auto/gui/painting/qcolorspace/resources/ProPhoto.jpg create mode 100644 tests/auto/gui/painting/qcolorspace/resources/sRGB2014.icc create mode 100644 tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp diff --git a/examples/widgets/widgets/imageviewer/imageviewer.cpp b/examples/widgets/widgets/imageviewer/imageviewer.cpp index fed551dade7..2fc8ff63dec 100644 --- a/examples/widgets/widgets/imageviewer/imageviewer.cpp +++ b/examples/widgets/widgets/imageviewer/imageviewer.cpp @@ -106,6 +106,8 @@ bool ImageViewer::loadFile(const QString &fileName) void ImageViewer::setImage(const QImage &newImage) { image = newImage; + if (image.colorSpace().isValid()) + image.convertToColorSpace(QColorSpace::SRgb); imageLabel->setPixmap(QPixmap::fromImage(image)); //! [4] scaleFactor = 1.0; diff --git a/src/gui/image/qimage.cpp b/src/gui/image/qimage.cpp index 16de045cf0c..204729b551a 100644 --- a/src/gui/image/qimage.cpp +++ b/src/gui/image/qimage.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtGui module of the Qt Toolkit. @@ -38,8 +38,10 @@ ****************************************************************************/ #include "qimage.h" -#include "qdatastream.h" + #include "qbuffer.h" +#include "qdatastream.h" +#include "qcolortransform.h" #include "qmap.h" #include "qmatrix.h" #include "qtransform.h" @@ -54,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -1098,6 +1101,7 @@ static void copyMetadata(QImageData *dst, const QImageData *src) dst->dpmy = src->dpmy; dst->devicePixelRatio = src->devicePixelRatio; dst->text = src->text; + dst->colorSpace = src->colorSpace; } /*! @@ -4920,6 +4924,132 @@ QTransform QImage::trueMatrix(const QTransform &matrix, int w, int h) return matrix * QTransform().translate(-delta.x(), -delta.y()); } +/*! + \since 5.14 + + Sets the image color space to \a colorSpace without performing any conversions on image data. + + \sa colorSpace() +*/ +void QImage::setColorSpace(const QColorSpace &colorSpace) +{ + if (!d) + return; + if (d->colorSpace == colorSpace) + return; + if (!isDetached()) // Detach only if shared, not for read-only data. + detach(); + d->colorSpace = colorSpace; +} + +/*! + \since 5.14 + + Converts the image to \a colorSpace. + + If the image has no valid color space, the method does nothing. + + \sa convertedToColorSpace(), setColorSpace() +*/ +void QImage::convertToColorSpace(const QColorSpace &colorSpace) +{ + if (!d) + return; + if (!d->colorSpace.isValid()) + return; + if (!colorSpace.isValid()) { + qWarning() << "QImage::convertToColorSpace: Output colorspace is not valid"; + return; + } + detach(); + applyColorTransform(d->colorSpace.transformationToColorSpace(colorSpace)); + d->colorSpace = colorSpace; +} + +/*! + \since 5.14 + + Returns the image converted to \a colorSpace. + + If the image has no valid color space, a null QImage is returned. + + \sa convertToColorSpace() +*/ +QImage QImage::convertedToColorSpace(const QColorSpace &colorSpace) const +{ + if (!d || !d->colorSpace.isValid() || !colorSpace.isValid()) + return QImage(); + QImage image = copy(); + image.convertToColorSpace(colorSpace); + return image; +} + +/*! + \since 5.14 + + Returns the color space of the image if a color space is defined. +*/ +QColorSpace QImage::colorSpace() const +{ + if (!d) + return QColorSpace::Undefined; + return d->colorSpace; +} + +/*! + \since 5.14 + + Applies the color transformation \a transform to all pixels in the image. +*/ +void QImage::applyColorTransform(const QColorTransform &transform) +{ + QImage::Format oldFormat = format(); + if (depth() > 32) { + if (format() != QImage::Format_RGBX64 && format() != QImage::Format_RGBA64 + && format() != QImage::Format_RGBA64_Premultiplied) + *this = std::move(*this).convertToFormat(QImage::Format_RGBA64); + } else if (format() != QImage::Format_ARGB32 && format() != QImage::Format_RGB32 + && format() != QImage::Format_ARGB32_Premultiplied) { + if (hasAlphaChannel()) + *this = std::move(*this).convertToFormat(QImage::Format_ARGB32); + else + *this = std::move(*this).convertToFormat(QImage::Format_RGB32); + } + + QColorTransformPrivate::TransformFlags flags = QColorTransformPrivate::Unpremultiplied; + switch (format()) { + case Format_ARGB32_Premultiplied: + case Format_RGBA64_Premultiplied: + flags = QColorTransformPrivate::Premultiplied; + break; + case Format_RGB32: + case Format_RGBX64: + flags = QColorTransformPrivate::InputOpaque; + break; + case Format_ARGB32: + case Format_RGBA64: + break; + default: + Q_UNREACHABLE(); + } + + if (depth() > 32) { + for (int i = 0; i < height(); ++i) { + QRgba64 *scanline = reinterpret_cast(scanLine(i)); + transform.d_func()->apply(scanline, scanline, width(), flags); + } + } else { + for (int i = 0; i < height(); ++i) { + QRgb *scanline = reinterpret_cast(scanLine(i)); + transform.d_func()->apply(scanline, scanline, width(), flags); + } + } + + if (oldFormat != format()) + *this = std::move(*this).convertToFormat(oldFormat); +} + + bool QImageData::convertInPlace(QImage::Format newFormat, Qt::ImageConversionFlags flags) { if (format == newFormat) diff --git a/src/gui/image/qimage.h b/src/gui/image/qimage.h index 45f571807c3..b09e69c839e 100644 --- a/src/gui/image/qimage.h +++ b/src/gui/image/qimage.h @@ -61,9 +61,11 @@ Q_FORWARD_DECLARE_MUTABLE_CG_TYPE(CGImage); QT_BEGIN_NAMESPACE +class QColorSpace; +class QColorTransform; class QIODevice; -class QStringList; class QMatrix; +class QStringList; class QTransform; class QVariant; template class QList; @@ -296,6 +298,12 @@ public: #endif void invertPixels(InvertMode = InvertRgb); + QColorSpace colorSpace() const; + QImage convertedToColorSpace(const QColorSpace &) const; + void convertToColorSpace(const QColorSpace &); + void setColorSpace(const QColorSpace &); + + void applyColorTransform(const QColorTransform &transform); bool load(QIODevice *device, const char* format); bool load(const QString &fileName, const char *format = nullptr); diff --git a/src/gui/image/qimage_conversions.cpp b/src/gui/image/qimage_conversions.cpp index 82ffb8af8bd..837ac88470d 100644 --- a/src/gui/image/qimage_conversions.cpp +++ b/src/gui/image/qimage_conversions.cpp @@ -39,7 +39,7 @@ #include #include -#include +#include #include #include #include @@ -100,7 +100,7 @@ const uchar *qt_get_bitflip_array() void qGamma_correct_back_to_linear_cs(QImage *image) { - const QColorProfile *cp = QGuiApplicationPrivate::instance()->colorProfileForA32Text(); + const QColorTrcLut *cp = QGuiApplicationPrivate::instance()->colorProfileForA32Text(); if (!cp) return; // gamma correct the pixels back to linear color space... diff --git a/src/gui/image/qimage_p.h b/src/gui/image/qimage_p.h index de12a313e83..a599fc17c95 100644 --- a/src/gui/image/qimage_p.h +++ b/src/gui/image/qimage_p.h @@ -51,7 +51,9 @@ // We mean it. // +#include #include +#include #include #include @@ -106,6 +108,8 @@ struct Q_GUI_EXPORT QImageData { // internal image data QPaintEngine *paintEngine; + QColorSpace colorSpace; + struct ImageSizeParameters { qsizetype bytesPerLine; qsizetype totalSize; diff --git a/src/gui/image/qpnghandler.cpp b/src/gui/image/qpnghandler.cpp index 3655c39326c..066261620b1 100644 --- a/src/gui/image/qpnghandler.cpp +++ b/src/gui/image/qpnghandler.cpp @@ -42,6 +42,7 @@ #ifndef QT_NO_IMAGEFORMAT_PNG #include +#include #include #include #include @@ -50,6 +51,10 @@ #include // for qt_getImageText +#include +#include +#include + #include #include @@ -96,9 +101,16 @@ public: ReadingEnd, Error }; + // Defines the order of how the various ways of setting colorspace overrides eachother: + enum ColorSpaceState { + Undefined = 0, + GammaChrm = 1, // gAMA+cHRM chunks + Srgb = 2, // sRGB chunk + Icc = 3 // iCCP chunk + }; QPngHandlerPrivate(QPngHandler *qq) - : gamma(0.0), fileGamma(0.0), quality(50), compression(50), png_ptr(0), info_ptr(0), end_info(0), state(Ready), q(qq) + : gamma(0.0), fileGamma(0.0), quality(50), compression(50), colorSpaceState(Undefined), png_ptr(0), info_ptr(0), end_info(0), state(Ready), q(qq) { } float gamma; @@ -108,6 +120,8 @@ public: QString description; QSize scaledSize; QStringList readTexts; + QColorSpace colorSpace; + ColorSpaceState colorSpaceState; png_struct *png_ptr; png_info *info_ptr; @@ -226,11 +240,8 @@ void qpiw_flush_fn(png_structp /* png_ptr */) } static -void setup_qt(QImage& image, png_structp png_ptr, png_infop info_ptr, QSize scaledSize, bool *doScaledRead, float screen_gamma=0.0, float file_gamma=0.0) +void setup_qt(QImage& image, png_structp png_ptr, png_infop info_ptr, QSize scaledSize, bool *doScaledRead) { - if (screen_gamma != 0.0 && file_gamma != 0.0) - png_set_gamma(png_ptr, 1.0f / screen_gamma, file_gamma); - png_uint_32 width; png_uint_32 height; int bit_depth; @@ -585,10 +596,45 @@ bool QPngHandlerPrivate::readPngHeader() readPngTexts(info_ptr); +#ifdef PNG_iCCP_SUPPORTED + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP)) { + png_charp name = nullptr; + int compressionType = 0; +#if (PNG_LIBPNG_VER < 10500) + png_charp profileData = nullptr; +#else + png_bytep profileData = nullptr; +#endif + png_uint_32 profLen; + png_get_iCCP(png_ptr, info_ptr, &name, &compressionType, &profileData, &profLen); + if (!QIcc::fromIccProfile(QByteArray::fromRawData((const char *)profileData, profLen), &colorSpace)) { + qWarning() << "QPngHandler: Failed to parse ICC profile"; + } else { + colorSpaceState = Icc; + } + } +#endif + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) { + int rendering_intent = -1; + png_get_sRGB(png_ptr, info_ptr, &rendering_intent); + // We don't actually care about the rendering_intent, just that it is valid + if (rendering_intent >= 0 && rendering_intent <= 3 && colorSpaceState <= Srgb) { + colorSpace = QColorSpace::SRgb; + colorSpaceState = Srgb; + } + } if (png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA)) { double file_gamma = 0.0; png_get_gAMA(png_ptr, info_ptr, &file_gamma); fileGamma = file_gamma; + if (fileGamma > 0.0f && colorSpaceState <= GammaChrm) { + QColorSpacePrivate *csPrivate = colorSpace.d_func(); + csPrivate->gamut = QColorSpace::Gamut::SRgb; + csPrivate->transferFunction = QColorSpace::TransferFunction::Gamma; + csPrivate->gamma = fileGamma; + csPrivate->initialize(); + colorSpaceState = GammaChrm; + } } state = ReadHeader; @@ -613,8 +659,19 @@ bool QPngHandlerPrivate::readPngImage(QImage *outImage) return false; } + if (gamma != 0.0 && fileGamma != 0.0) { + // This configuration forces gamma correction and + // thus changes the output colorspace + png_set_gamma(png_ptr, 1.0f / gamma, fileGamma); + QColorSpacePrivate *csPrivate = colorSpace.d_func(); + csPrivate->transferFunction = QColorSpace::TransferFunction::Gamma; + csPrivate->gamma = gamma; + csPrivate->initialize(); + colorSpaceState = GammaChrm; + } + bool doScaledRead = false; - setup_qt(*outImage, png_ptr, info_ptr, scaledSize, &doScaledRead, gamma, fileGamma); + setup_qt(*outImage, png_ptr, info_ptr, scaledSize, &doScaledRead); if (outImage->isNull()) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); @@ -683,6 +740,9 @@ bool QPngHandlerPrivate::readPngImage(QImage *outImage) if (scaledSize.isValid() && outImage->size() != scaledSize) *outImage = outImage->scaled(scaledSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + if (colorSpaceState > Undefined && colorSpace.isValid()) + outImage->setColorSpace(colorSpace); + return true; } diff --git a/src/gui/kernel/qguiapplication.cpp b/src/gui/kernel/qguiapplication.cpp index 4a0febc615a..6753bf64bc4 100644 --- a/src/gui/kernel/qguiapplication.cpp +++ b/src/gui/kernel/qguiapplication.cpp @@ -68,7 +68,7 @@ #include #include #include "qsessionmanager.h" -#include +#include #include #include @@ -1643,8 +1643,6 @@ QGuiApplicationPrivate::~QGuiApplicationPrivate() platform_theme = 0; delete platform_integration; platform_integration = 0; - delete m_a8ColorProfile.load(); - delete m_a32ColorProfile.load(); window_list.clear(); screen_list.clear(); @@ -4019,32 +4017,26 @@ void QGuiApplicationPrivate::notifyDragStarted(const QDrag *drag) } #endif -const QColorProfile *QGuiApplicationPrivate::colorProfileForA8Text() +const QColorTrcLut *QGuiApplicationPrivate::colorProfileForA8Text() { #ifdef Q_OS_WIN - QColorProfile *result = m_a8ColorProfile.load(); - if (!result){ - QColorProfile *cs = QColorProfile::fromGamma(2.31); // This is a hard-coded thing for Windows text rendering - if (!m_a8ColorProfile.testAndSetRelease(0, cs)) - delete cs; - result = m_a8ColorProfile.load(); + if (!m_a8ColorProfile){ + QColorTrcLut *cs = QColorTrcLut::fromGamma(2.31); // This is a hard-coded thing for Windows text rendering + m_a8ColorProfile.reset(cs); } - return result; + return m_a8ColorProfile.get(); #else return colorProfileForA32Text(); #endif } -const QColorProfile *QGuiApplicationPrivate::colorProfileForA32Text() +const QColorTrcLut *QGuiApplicationPrivate::colorProfileForA32Text() { - QColorProfile *result = m_a32ColorProfile.load(); - if (!result){ - QColorProfile *cs = QColorProfile::fromGamma(fontSmoothingGamma); - if (!m_a32ColorProfile.testAndSetRelease(0, cs)) - delete cs; - result = m_a32ColorProfile.load(); + if (!m_a32ColorProfile) { + QColorTrcLut *cs = QColorTrcLut::fromGamma(fontSmoothingGamma); + m_a32ColorProfile.reset(cs); } - return result; + return m_a32ColorProfile.get(); } void QGuiApplicationPrivate::_q_updateFocusObject(QObject *object) diff --git a/src/gui/kernel/qguiapplication_p.h b/src/gui/kernel/qguiapplication_p.h index 63646dcd501..61d9661286e 100644 --- a/src/gui/kernel/qguiapplication_p.h +++ b/src/gui/kernel/qguiapplication_p.h @@ -55,6 +55,7 @@ #include #include +#include #include #include @@ -66,7 +67,7 @@ QT_BEGIN_NAMESPACE -class QColorProfile; +class QColorTrcLut; class QPlatformIntegration; class QPlatformTheme; class QPlatformDragQtResponse; @@ -299,8 +300,8 @@ public: static QInputDeviceManager *inputDeviceManager(); - const QColorProfile *colorProfileForA8Text(); - const QColorProfile *colorProfileForA32Text(); + const QColorTrcLut *colorProfileForA8Text(); + const QColorTrcLut *colorProfileForA32Text(); // hook reimplemented in QApplication to apply the QStyle function on the QIcon virtual QPixmap applyQIconStyleHelper(QIcon::Mode, const QPixmap &basePixmap) const { return basePixmap; } @@ -327,8 +328,8 @@ private: static QGuiApplicationPrivate *self; static QTouchDevice *m_fakeTouchDevice; static int m_fakeMouseSourcePointId; - QAtomicPointer m_a8ColorProfile; - QAtomicPointer m_a32ColorProfile; + QSharedPointer m_a8ColorProfile; + QSharedPointer m_a32ColorProfile; bool ownGlobalShareContext; diff --git a/src/gui/painting/painting.pri b/src/gui/painting/painting.pri index c3585a4647d..0e2dfed9ab8 100644 --- a/src/gui/painting/painting.pri +++ b/src/gui/painting/painting.pri @@ -8,7 +8,15 @@ HEADERS += \ painting/qbrush.h \ painting/qcolor.h \ painting/qcolor_p.h \ - painting/qcolorprofile_p.h \ + painting/qcolormatrix_p.h \ + painting/qcolorspace.h \ + painting/qcolorspace_p.h \ + painting/qcolortransferfunction_p.h \ + painting/qcolortransfertable_p.h \ + painting/qcolortransform.h \ + painting/qcolortransform_p.h \ + painting/qcolortrc_p.h \ + painting/qcolortrclut_p.h \ painting/qcosmeticstroker_p.h \ painting/qdatabuffer_p.h \ painting/qdrawhelper_p.h \ @@ -17,6 +25,7 @@ HEADERS += \ painting/qemulationpaintengine_p.h \ painting/qfixed_p.h \ painting/qgrayraster_p.h \ + painting/qicc_p.h \ painting/qmatrix.h \ painting/qmemrotate_p.h \ painting/qoutlinemapper_p.h \ @@ -64,12 +73,15 @@ SOURCES += \ painting/qblittable.cpp \ painting/qbrush.cpp \ painting/qcolor.cpp \ - painting/qcolorprofile.cpp \ + painting/qcolorspace.cpp \ + painting/qcolortransform.cpp \ + painting/qcolortrclut.cpp \ painting/qcompositionfunctions.cpp \ painting/qcosmeticstroker.cpp \ painting/qdrawhelper.cpp \ painting/qemulationpaintengine.cpp \ painting/qgrayraster.c \ + painting/qicc.cpp \ painting/qimagescale.cpp \ painting/qmatrix.cpp \ painting/qmemrotate.cpp \ diff --git a/src/gui/painting/qcolormatrix_p.h b/src/gui/painting/qcolormatrix_p.h new file mode 100644 index 00000000000..3d1dca6222c --- /dev/null +++ b/src/gui/painting/qcolormatrix_p.h @@ -0,0 +1,214 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOLORMATRIX_H +#define QCOLORMATRIX_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +// An abstract 3 value color +class QColorVector +{ +public: + QColorVector() = default; + constexpr QColorVector(float x, float y, float z) : x(x), y(y), z(z), _unused(0.0f) { } + float x; // X, x or red + float y; // Y, y or green + float z; // Z, Y or blue + float _unused; + + friend inline bool operator==(const QColorVector &v1, const QColorVector &v2); + friend inline bool operator!=(const QColorVector &v1, const QColorVector &v2); + + static constexpr QColorVector null() { return QColorVector(0.0f, 0.0f, 0.0f); } + // Common whitepoints on normalized XYZ form: + static constexpr QColorVector D50() { return QColorVector(0.96421f, 1.0f, 0.82519f); } + static constexpr QColorVector D65() { return QColorVector(0.95043f, 1.0f, 1.08890f); } +}; + +inline bool operator==(const QColorVector &v1, const QColorVector &v2) +{ + return (std::abs(v1.x - v2.x) < (1.0f / 2048.0f)) + && (std::abs(v1.y - v2.y) < (1.0f / 2048.0f)) + && (std::abs(v1.z - v2.z) < (1.0f / 2048.0f)); +} + +inline bool operator!=(const QColorVector &v1, const QColorVector &v2) +{ + return !(v1 == v2); +} + + +// A matrix mapping 3 value colors. +// Not using QMatrix because only floats are needed and performance is critical. +class QColorMatrix +{ +public: + // We are storing the matrix transposed as that is more convenient: + QColorVector r; + QColorVector g; + QColorVector b; + + friend inline bool operator==(const QColorMatrix &m1, const QColorMatrix &m2); + friend inline bool operator!=(const QColorMatrix &m1, const QColorMatrix &m2); + + bool isValid() const + { + // A color matrix must be invertible + float det = r.x * (b.z * g.y - g.z * b.y) - + r.y * (b.z * g.x - g.z * b.x) + + r.z * (b.y * g.x - g.y * b.x); + return !qFuzzyIsNull(det); + } + + QColorMatrix inverted() const + { + float det = r.x * (b.z * g.y - g.z * b.y) - + r.y * (b.z * g.x - g.z * b.x) + + r.z * (b.y * g.x - g.y * b.x); + det = 1.0f / det; + QColorMatrix inv; + inv.r.x = (g.y * b.z - b.y * g.z) * det; + inv.r.y = (b.y * r.z - r.y * b.z) * det; + inv.r.z = (r.y * g.z - g.y * r.z) * det; + inv.g.x = (b.x * g.z - g.x * b.z) * det; + inv.g.y = (r.x * b.z - b.x * r.z) * det; + inv.g.z = (g.x * r.z - r.x * g.z) * det; + inv.b.x = (g.x * b.y - b.x * g.y) * det; + inv.b.y = (b.x * r.y - r.x * b.y) * det; + inv.b.z = (r.x * g.y - g.x * r.y) * det; + return inv; + } + QColorMatrix operator*(const QColorMatrix &o) const + { + QColorMatrix comb; + comb.r.x = r.x * o.r.x + g.x * o.r.y + b.x * o.r.z; + comb.g.x = r.x * o.g.x + g.x * o.g.y + b.x * o.g.z; + comb.b.x = r.x * o.b.x + g.x * o.b.y + b.x * o.b.z; + + comb.r.y = r.y * o.r.x + g.y * o.r.y + b.y * o.r.z; + comb.g.y = r.y * o.g.x + g.y * o.g.y + b.y * o.g.z; + comb.b.y = r.y * o.b.x + g.y * o.b.y + b.y * o.b.z; + + comb.r.z = r.z * o.r.x + g.z * o.r.y + b.z * o.r.z; + comb.g.z = r.z * o.g.x + g.z * o.g.y + b.z * o.g.z; + comb.b.z = r.z * o.b.x + g.z * o.b.y + b.z * o.b.z; + return comb; + + } + QColorVector map(const QColorVector &c) const + { + return QColorVector { c.x * r.x + c.y * g.x + c.z * b.x, + c.x * r.y + c.y * g.y + c.z * b.y, + c.x * r.z + c.y * g.z + c.z * b.z }; + } + QColorMatrix transposed() const + { + return QColorMatrix { { r.x, g.x, b.x }, + { r.y, g.y, b.y }, + { r.z, g.z, b.z } }; + } + + static QColorMatrix null() + { + return { QColorVector::null(), QColorVector::null(), QColorVector::null() }; + } + static QColorMatrix identity() + { + return { { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } }; + } + static QColorMatrix toXyzFromSRgb() + { + return QColorMatrix { { 0.4360217452f, 0.2224751115f, 0.0139281144f }, + { 0.3851087987f, 0.7169067264f, 0.0971015394f }, + { 0.1430812478f, 0.0606181994f, 0.7141585946f } }; + } + static QColorMatrix toXyzFromAdobeRgb() + { + return QColorMatrix { { 0.6097189188f, 0.3111021519f, 0.0194766335f }, + { 0.2052682191f, 0.6256770492f, 0.0608891509f }, + { 0.1492247432f, 0.0632209629f, 0.7448224425f } }; + } + static QColorMatrix toXyzFromDciP3D65() + { + return QColorMatrix { { 0.5150973201f, 0.2411795557f, -0.0010491034f }, + { 0.2919696569f, 0.6922441125f, 0.0418830328f }, + { 0.1571449190f, 0.0665764511f, 0.7843542695f } }; + } + static QColorMatrix toXyzFromProPhotoRgb() + { + return QColorMatrix { { 0.7976672649f, 0.2880374491f, 0.0000000000f }, + { 0.1351922452f, 0.7118769884f, 0.0000000000f }, + { 0.0313525312f, 0.0000856627f, 0.8251883388f } }; + } + static QColorMatrix toXyzFromBt2020() + { + return QColorMatrix { { 0.6506130099f, 0.2695676684f, -0.0018652577f }, + { 0.1865101457f, 0.6840794086f, 0.0172256753f }, + { 0.1270887405f, 0.0463530831f, 0.8098278046f } }; + } +}; + +inline bool operator==(const QColorMatrix &m1, const QColorMatrix &m2) +{ + return (m1.r == m2.r) && (m1.g == m2.g) && (m1.b == m2.b); +} + +inline bool operator!=(const QColorMatrix &m1, const QColorMatrix &m2) +{ + return !(m1 == m2); +} + +QT_END_NAMESPACE + +#endif // QCOLORMATRIX_P_H diff --git a/src/gui/painting/qcolorspace.cpp b/src/gui/painting/qcolorspace.cpp new file mode 100644 index 00000000000..24785f7b617 --- /dev/null +++ b/src/gui/painting/qcolorspace.cpp @@ -0,0 +1,633 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qcolorspace.h" +#include "qcolorspace_p.h" + +#include "qcolortransform.h" +#include "qcolormatrix_p.h" +#include "qcolortransferfunction_p.h" +#include "qcolortransform_p.h" +#include "qicc_p.h" + +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +QColorSpacePrivate::QColorSpacePrivate() + : id(QColorSpace::Unknown) + , gamut(QColorSpace::Gamut::Custom) + , transferFunction(QColorSpace::TransferFunction::Custom) + , gamma(0.0f) + , whitePoint(QColorVector::null()) + , toXyz(QColorMatrix::null()) +{ +} + +QColorSpacePrivate::QColorSpacePrivate(QColorSpace::ColorSpaceId colorSpaceId) + : id(colorSpaceId) +{ + switch (colorSpaceId) { + case QColorSpace::Undefined: + gamut = QColorSpace::Gamut::Custom; + transferFunction = QColorSpace::TransferFunction::Custom; + gamma = 0.0f; + description = QStringLiteral("Undefined"); + break; + case QColorSpace::SRgb: + gamut = QColorSpace::Gamut::SRgb; + transferFunction = QColorSpace::TransferFunction::SRgb; + gamma = 2.31f; // ? + description = QStringLiteral("sRGB"); + break; + case QColorSpace::SRgbLinear: + gamut = QColorSpace::Gamut::SRgb; + transferFunction = QColorSpace::TransferFunction::Linear; + gamma = 1.0f; + description = QStringLiteral("Linear sRGB"); + break; + case QColorSpace::AdobeRgb: + gamut = QColorSpace::Gamut::AdobeRgb; + transferFunction = QColorSpace::TransferFunction::Gamma; + gamma = 2.19921875f; // Not quite 2.2, see https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf + description = QStringLiteral("Adobe RGB"); + break; + case QColorSpace::DisplayP3: + gamut = QColorSpace::Gamut::DciP3D65; + transferFunction = QColorSpace::TransferFunction::SRgb; + gamma = 2.31f; // ? + description = QStringLiteral("Display P3"); + break; + case QColorSpace::ProPhotoRgb: + gamut = QColorSpace::Gamut::ProPhotoRgb; + transferFunction = QColorSpace::TransferFunction::ProPhotoRgb; + gamma = 1.8f; + description = QStringLiteral("ProPhoto RGB"); + break; + case QColorSpace::Bt2020: + gamut = QColorSpace::Gamut::Bt2020; + transferFunction = QColorSpace::TransferFunction::Bt2020; + gamma = 2.1f; // ? + description = QStringLiteral("BT.2020"); + break; + case QColorSpace::Unknown: + qWarning("Can not create an unknown color space"); + Q_FALLTHROUGH(); + default: + Q_UNREACHABLE(); + } + initialize(); +} + +QColorSpacePrivate::QColorSpacePrivate(QColorSpace::Gamut gamut, QColorSpace::TransferFunction fun, float gamma) + : gamut(gamut) + , transferFunction(fun) + , gamma(gamma) +{ + if (!identifyColorSpace()) + id = QColorSpace::Unknown; + initialize(); +} + +bool QColorSpacePrivate::identifyColorSpace() +{ + switch (gamut) { + case QColorSpace::Gamut::SRgb: + if (transferFunction == QColorSpace::TransferFunction::SRgb) { + id = QColorSpace::SRgb; + description = QStringLiteral("sRGB"); + return true; + } + if (transferFunction == QColorSpace::TransferFunction::Linear) { + id = QColorSpace::SRgbLinear; + description = QStringLiteral("Linear sRGB"); + return true; + } + break; + case QColorSpace::Gamut::AdobeRgb: + if (transferFunction == QColorSpace::TransferFunction::Gamma) { + if (qAbs(gamma - 2.19921875f) < (1/1024.0f)) { + id = QColorSpace::AdobeRgb; + description = QStringLiteral("Adobe RGB"); + return true; + } + } + break; + case QColorSpace::Gamut::DciP3D65: + if (transferFunction == QColorSpace::TransferFunction::SRgb) { + id = QColorSpace::DisplayP3; + description = QStringLiteral("Display P3"); + return true; + } + break; + case QColorSpace::Gamut::ProPhotoRgb: + if (transferFunction == QColorSpace::TransferFunction::ProPhotoRgb) { + id = QColorSpace::ProPhotoRgb; + description = QStringLiteral("ProPhoto RGB"); + return true; + } + if (transferFunction == QColorSpace::TransferFunction::Gamma) { + // ProPhoto RGB's curve is effectively gamma 1.8 for 8bit precision. + if (qAbs(gamma - 1.8f) < (1/1024.0f)) { + id = QColorSpace::ProPhotoRgb; + description = QStringLiteral("ProPhoto RGB"); + return true; + } + } + break; + case QColorSpace::Gamut::Bt2020: + if (transferFunction == QColorSpace::TransferFunction::Bt2020) { + id = QColorSpace::Bt2020; + description = QStringLiteral("BT.2020"); + return true; + } + break; + default: + break; + } + return false; +} + +void QColorSpacePrivate::initialize() +{ + setToXyzMatrix(); + setTransferFunction(); +} + +void QColorSpacePrivate::setToXyzMatrix() +{ + switch (gamut) { + case QColorSpace::Gamut::SRgb: + toXyz = QColorMatrix::toXyzFromSRgb(); + whitePoint = QColorVector::D65(); + return; + case QColorSpace::Gamut::AdobeRgb: + toXyz = QColorMatrix::toXyzFromAdobeRgb(); + whitePoint = QColorVector::D65(); + return; + case QColorSpace::Gamut::DciP3D65: + toXyz = QColorMatrix::toXyzFromDciP3D65(); + whitePoint = QColorVector::D65(); + return; + case QColorSpace::Gamut::ProPhotoRgb: + toXyz = QColorMatrix::toXyzFromProPhotoRgb(); + whitePoint = QColorVector::D50(); + return; + case QColorSpace::Gamut::Bt2020: + toXyz = QColorMatrix::toXyzFromBt2020(); + whitePoint = QColorVector::D65(); + return; + case QColorSpace::Gamut::Custom: + toXyz = QColorMatrix::null(); + whitePoint = QColorVector::D50(); + return; + } + Q_UNREACHABLE(); +} + +void QColorSpacePrivate::setTransferFunction() +{ + switch (transferFunction) { + case QColorSpace::TransferFunction::Linear: + trc[0].m_type = QColorTrc::Type::Function; + trc[0].m_fun = QColorTransferFunction(); + break; + case QColorSpace::TransferFunction::Gamma: + trc[0].m_type = QColorTrc::Type::Function; + trc[0].m_fun = QColorTransferFunction::fromGamma(gamma); + break; + case QColorSpace::TransferFunction::SRgb: + trc[0].m_type = QColorTrc::Type::Function; + trc[0].m_fun = QColorTransferFunction::fromSRgb(); + break; + case QColorSpace::TransferFunction::ProPhotoRgb: + trc[0].m_type = QColorTrc::Type::Function; + trc[0].m_fun = QColorTransferFunction::fromProPhotoRgb(); + break; + case QColorSpace::TransferFunction::Bt2020: + trc[0].m_type = QColorTrc::Type::Function; + trc[0].m_fun = QColorTransferFunction::fromBt2020(); + break; + case QColorSpace::TransferFunction::Custom: + break; + default: + Q_UNREACHABLE(); + break; + } + trc[1] = trc[0]; + trc[2] = trc[0]; +} + +QColorTransform QColorSpacePrivate::transformationToColorSpace(const QColorSpacePrivate *out) const +{ + Q_ASSERT(out); + QColorTransform combined; + combined.d_ptr.reset(new QColorTransformPrivate); + combined.d_ptr->colorSpaceIn = this; + combined.d_ptr->colorSpaceOut = out; + combined.d_ptr->colorMatrix = out->toXyz.inverted() * toXyz; + return combined; +} + +/*! + \class QColorSpace + \brief The QColorSpace class provides a color space abstraction. + \since 5.14 + + \ingroup painting + \ingroup appearance + \inmodule QtGui + + Color values can be interpreted in different ways, and based on the interpretation + can live in different spaces. We call this \e {color spaces}. + + QColorSpace provides access to creating several predefined color spaces and + can generate QColorTransforms for converting colors from one color space to + another. + + QColorSpace can also represent color spaces defined by ICC profiles or embedded + in images, that do not otherwise fit the predefined color spaces. + + A color space can generally speaking be conceived as a combination of a transfer + function and a gamut. The gamut defines which colors the color space can represent. + A color space that can represent a wider range of colors is also known as a + wide-gamut color space. The gamut is defined by three primary colors that represent + exactly how red, green, and blue look in this particular color space, and a white + color that represents where and how bright pure white is. + + The transfer function or gamma curve determines how each component in the + color space is encoded. These are used because human perception does not operate + linearly, and the transfer functions try to ensure that colors will seem evenly + spaced to human eyes. +*/ + + +/*! + \enum QColorSpace::ColorSpaceId + + Predefined color spaces. + + \value Undefined An empty, invalid or unsupported color space. + \value Unknown A valid color space that doesn't match any of the predefined color spaces. + \value SRgb The sRGB color space, which Qt operates in by default. It is a close approximation + of how most classic monitors operate, and a mode most software and hardware support. + \l{http://www.color.org/chardata/rgb/srgb.xalter}{ICC registration of sRGB}. + \value SRgbLinear The sRGB color space with linear gamma. Useful for gamma-corrected blending. + \value AdobeRgb The Adobe RGB color space is a classic wide-gamut color space, using a gamma of 2.2. + \l{http://www.color.org/chardata/rgb/adobergb.xalter}{ICC registration of Adobe RGB (1998)} + \value DisplayP3 A color-space using the primaries of DCI-P3, but with the whitepoint and transfer + function of sRGB. Common in modern wide-gamut screens. + \l{http://www.color.org/chardata/rgb/DCIP3.xalter}{ICC registration of DCI-P3} + \value ProPhotoRgb The Pro Photo RGB color space, also known as ROMM RGB is a very wide gamut color space. + \l{http://www.color.org/chardata/rgb/rommrgb.xalter}{ICC registration of ROMM RGB} + \value Bt2020 BT.2020 also known as Rec.2020 is the color space of HDR TVs. + \l{http://www.color.org/chardata/rgb/BT2020.xalter}{ICC registration of BT.2020} +*/ + +/*! + \enum QColorSpace::Gamut + + Predefined gamuts, or sets of primary colors. + + \value Custom The gamut is undefined or does not match any predefined sets. + \value SRgb The sRGB gamut + \value AdobeRgb The Adobe RGB gamut + \value DciP3D65 The DCI-P3 gamut with the D65 whitepoint + \value ProPhotoRgb The ProPhoto RGB gamut with the D50 whitepoint + \value Bt2020 The BT.2020 gamut +*/ + +/*! + \enum QColorSpace::TransferFunction + + Predefined transfer functions or gamma curves. + + \value Custom The custom or null transfer function + \value Linear The linear transfer functions + \value Gamma A transfer function that is a real gamma curve based on the value of gamma() + \value SRgb The sRGB transfer function, composed of linear and gamma parts + \value ProPhotoRgb The ProPhoto RGB transfer function, composed of linear and gamma parts + \value Bt2020 The BT.2020 transfer function, composed of linear and gamma parts +*/ + +/*! + Creates a new colorspace object that represents \a colorSpaceId. + */ +QColorSpace::QColorSpace(QColorSpace::ColorSpaceId colorSpaceId) +{ + static QExplicitlySharedDataPointer predefinedColorspacePrivates[QColorSpace::Bt2020]; + if (colorSpaceId <= QColorSpace::Unknown) { + if (!predefinedColorspacePrivates[0]) + predefinedColorspacePrivates[0] = new QColorSpacePrivate(QColorSpace::Undefined); + d_ptr = predefinedColorspacePrivates[0]; // unknown and undefined both returns the static undefined colorspace. + } else { + if (!predefinedColorspacePrivates[colorSpaceId - 1]) + predefinedColorspacePrivates[colorSpaceId - 1] = new QColorSpacePrivate(colorSpaceId); + d_ptr = predefinedColorspacePrivates[colorSpaceId - 1]; + } + + Q_ASSERT(colorSpaceId == QColorSpace::Undefined || isValid()); +} + +/*! + Creates a custom color space with the gamut \a gamut, using the transfer function \a fun and + optionally \a gamma. + */ +QColorSpace::QColorSpace(QColorSpace::Gamut gamut, QColorSpace::TransferFunction fun, float gamma) + : d_ptr(new QColorSpacePrivate(gamut, fun, gamma)) +{ +} + +/*! + Creates a custom color space with the gamut \a gamut, using a gamma transfer function of + \a gamma. + */ +QColorSpace::QColorSpace(QColorSpace::Gamut gamut, float gamma) + : d_ptr(new QColorSpacePrivate(gamut, TransferFunction::Gamma, gamma)) +{ +} + +QColorSpace::~QColorSpace() +{ +} + +QColorSpace::QColorSpace(QColorSpace &&colorSpace) + : d_ptr(std::move(colorSpace.d_ptr)) +{ +} + +QColorSpace::QColorSpace(const QColorSpace &colorSpace) + : d_ptr(colorSpace.d_ptr) +{ +} + +QColorSpace &QColorSpace::operator=(QColorSpace &&colorSpace) +{ + d_ptr = std::move(colorSpace.d_ptr); + return *this; +} + +QColorSpace &QColorSpace::operator=(const QColorSpace &colorSpace) +{ + d_ptr = colorSpace.d_ptr; + return *this; +} + +/*! + Returns the id of the predefined color space this object + represents or \c Unknown if it doesn't match any of them. +*/ +QColorSpace::ColorSpaceId QColorSpace::colorSpaceId() const noexcept +{ + return d_ptr->id; +} + +/*! + Returns the predefined gamut of the color space + or \c Gamut::Custom if it doesn't match any of them. +*/ +QColorSpace::Gamut QColorSpace::gamut() const noexcept +{ + return d_ptr->gamut; +} + +/*! + Returns the predefined transfer function of the color space + or \c TransferFunction::Custom if it doesn't match any of them. + + \sa gamma() +*/ +QColorSpace::TransferFunction QColorSpace::transferFunction() const noexcept +{ + return d_ptr->transferFunction; +} + +/*! + Returns the gamma value of color spaces with \c TransferFunction::Gamma, + an approximate gamma value for other predefined color spaces, or + 0.0 if no approximate gamma is known. + + \sa transferFunction() +*/ +float QColorSpace::gamma() const noexcept +{ + return d_ptr->gamma; +} + +/*! + Returns an ICC profile representing the color space. + + If the color space was generated from an ICC profile, that profile + is returned, otherwise one is generated. + + \note Even invalid color spaces may return the ICC profile if they + were generated from one, to allow applications to implement wider + support themselves. + + \sa fromIccProfile() +*/ +QByteArray QColorSpace::iccProfile() const +{ + if (!d_ptr->iccProfile.isEmpty()) + return d_ptr->iccProfile; + if (!isValid()) + return QByteArray(); + return QIcc::toIccProfile(*this); +} + +/*! + Creates a QColorSpace from ICC profile \a iccProfile. + + \note Not all ICC profiles are supported. QColorSpace only supports + RGB-XYZ ICC profiles that are three-component matrix-based. + + If the ICC profile is not supported an invalid QColorSpace is returned + where you can still read the original ICC profile using iccProfile(). + + \sa iccProfile() +*/ +QColorSpace QColorSpace::fromIccProfile(const QByteArray &iccProfile) +{ + QColorSpace colorSpace; + if (QIcc::fromIccProfile(iccProfile, &colorSpace)) + return colorSpace; + colorSpace.d_ptr->id = QColorSpace::Undefined; + colorSpace.d_ptr->iccProfile = iccProfile; + return colorSpace; +} + +/*! + Returns \c true if the color space is valid. +*/ +bool QColorSpace::isValid() const noexcept +{ + return d_ptr->id != QColorSpace::Undefined && d_ptr->toXyz.isValid() + && d_ptr->trc[0].isValid() && d_ptr->trc[1].isValid() && d_ptr->trc[2].isValid(); +} + +/*! + \relates QColorSpace + Returns \c true if colorspace \a colorSpace1 is equal to colorspace \a colorSpace2; + otherwise returns \c false +*/ +bool operator==(const QColorSpace &colorSpace1, const QColorSpace &colorSpace2) +{ + if (colorSpace1.d_ptr == colorSpace2.d_ptr) + return true; + + if (colorSpace1.colorSpaceId() == QColorSpace::Undefined && colorSpace2.colorSpaceId() == QColorSpace::Undefined) + return colorSpace1.d_ptr->iccProfile == colorSpace2.d_ptr->iccProfile; + + if (colorSpace1.colorSpaceId() != QColorSpace::Unknown && colorSpace2.colorSpaceId() != QColorSpace::Unknown) + return colorSpace1.colorSpaceId() == colorSpace2.colorSpaceId(); + + if (colorSpace1.gamut() != QColorSpace::Gamut::Custom && colorSpace2.gamut() != QColorSpace::Gamut::Custom) { + if (colorSpace1.gamut() != colorSpace2.gamut()) + return false; + } else { + if (colorSpace1.d_ptr->toXyz != colorSpace2.d_ptr->toXyz) + return false; + } + + if (colorSpace1.transferFunction() != QColorSpace::TransferFunction::Custom && + colorSpace2.transferFunction() != QColorSpace::TransferFunction::Custom) { + if (colorSpace1.transferFunction() != colorSpace2.transferFunction()) + return false; + if (colorSpace1.transferFunction() == QColorSpace::TransferFunction::Gamma) + return colorSpace1.gamma() == colorSpace2.gamma(); + return true; + } + + if (colorSpace1.d_ptr->trc[0] != colorSpace2.d_ptr->trc[0] || + colorSpace1.d_ptr->trc[1] != colorSpace2.d_ptr->trc[1] || + colorSpace1.d_ptr->trc[2] != colorSpace2.d_ptr->trc[2]) + return false; + + return true; +} + +/*! + \fn bool operator!=(const QColorSpace &colorSpace1, const QColorSpace &colorSpace2) + \relates QColorSpace + + Returns \c true if colorspace \a colorspace1 is not equal to colorspace \a colorspace2; + otherwise returns \c false +*/ + +/*! + Generates and returns a color space transformation from this color space to + \a colorspace. +*/ +QColorTransform QColorSpace::transformationToColorSpace(const QColorSpace &colorspace) const +{ + if (!isValid() || !colorspace.isValid()) + return QColorTransform(); + + return d_ptr->transformationToColorSpace(colorspace.d_ptr.constData()); +} + +/*! + \internal +*/ +QColorSpacePrivate *QColorSpace::d_func() +{ + d_ptr.detach(); + return d_ptr.data(); +} + +/*! + \fn const QColorSpacePrivate* QColorSpacePrivate::d_func() const + \internal +*/ + +/***************************************************************************** + QColorSpace stream functions + *****************************************************************************/ +#if !defined(QT_NO_DATASTREAM) +/*! + \fn QDataStream &operator<<(QDataStream &stream, const QColorSpace &colorSpace) + \relates QColorSpace + + Writes the given \a colorSpace to the given \a stream as an ICC profile. + + \sa QColorSpace::iccProfile(), {Serializing Qt Data Types} +*/ + +QDataStream &operator<<(QDataStream &s, const QColorSpace &image) +{ + s << image.iccProfile(); + return s; +} + +/*! + \fn QDataStream &operator>>(QDataStream &stream, QColorSpace &colorSpace) + \relates QColorSpace + + Reads a color space from the given \a stream and stores it in the given + \a colorSpace. + + \sa QColorSpace::fromIccProfile(), {Serializing Qt Data Types} +*/ + +QDataStream &operator>>(QDataStream &s, QColorSpace &colorSpace) +{ + QByteArray iccProfile; + s >> iccProfile; + colorSpace = QColorSpace::fromIccProfile(iccProfile); + return s; +} +#endif // QT_NO_DATASTREAM + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug dbg, const QColorSpace &colorSpace) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QColorSpace("; + dbg << colorSpace.colorSpaceId() << ", " << colorSpace.gamut() << ", " << colorSpace.transferFunction(); + dbg << ", gamma=" << colorSpace.gamma(); + dbg << ')'; + return dbg; +} +#endif + +QT_END_NAMESPACE diff --git a/src/gui/painting/qcolorspace.h b/src/gui/painting/qcolorspace.h new file mode 100644 index 00000000000..923546ec6fd --- /dev/null +++ b/src/gui/painting/qcolorspace.h @@ -0,0 +1,136 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOLORSPACE_H +#define QCOLORSPACE_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QColorSpacePrivate; + +class Q_GUI_EXPORT QColorSpace +{ + Q_GADGET +public: + enum ColorSpaceId { + Undefined = 0, + Unknown = 1, + SRgb, + SRgbLinear, + AdobeRgb, + DisplayP3, + ProPhotoRgb, + Bt2020, + }; + Q_ENUM(ColorSpaceId) + enum class Gamut { + Custom = 0, + SRgb, + AdobeRgb, + DciP3D65, + ProPhotoRgb, + Bt2020, + }; + Q_ENUM(Gamut) + enum class TransferFunction { + Custom = 0, + Linear, + Gamma, + SRgb, + ProPhotoRgb, + Bt2020, + }; + Q_ENUM(TransferFunction) + + QColorSpace(ColorSpaceId colorSpaceId = Undefined); + QColorSpace(Gamut gamut, TransferFunction fun, float gamma = 0.0f); + QColorSpace(Gamut gamut, float gamma); + ~QColorSpace(); + + QColorSpace(QColorSpace &&colorSpace); + QColorSpace(const QColorSpace &colorSpace); + QColorSpace &operator=(QColorSpace &&colorSpace); + QColorSpace &operator=(const QColorSpace &colorSpace); + + ColorSpaceId colorSpaceId() const noexcept; + Gamut gamut() const noexcept; + TransferFunction transferFunction() const noexcept; + float gamma() const noexcept; + + bool isValid() const noexcept; + + friend Q_GUI_EXPORT bool operator==(const QColorSpace &colorSpace1, const QColorSpace &colorSpace2); + friend inline bool operator!=(const QColorSpace &colorSpace1, const QColorSpace &colorSpace2); + + static QColorSpace fromIccProfile(const QByteArray &iccProfile); + QByteArray iccProfile() const; + + QColorTransform transformationToColorSpace(const QColorSpace &colorspace) const; + + QColorSpacePrivate *d_func(); + inline const QColorSpacePrivate *d_func() const { return d_ptr.constData(); } + +private: + friend class QColorSpacePrivate; + QExplicitlySharedDataPointer d_ptr; +}; + +bool Q_GUI_EXPORT operator==(const QColorSpace &colorSpace1, const QColorSpace &colorSpace2); +inline bool operator!=(const QColorSpace &colorSpace1, const QColorSpace &colorSpace2) +{ + return !(colorSpace1 == colorSpace2); +} + +// QColorSpace stream functions +#if !defined(QT_NO_DATASTREAM) +Q_GUI_EXPORT QDataStream &operator<<(QDataStream &, const QColorSpace &); +Q_GUI_EXPORT QDataStream &operator>>(QDataStream &, QColorSpace &); +#endif + +#ifndef QT_NO_DEBUG_STREAM +Q_GUI_EXPORT QDebug operator<<(QDebug, const QColorSpace &); +#endif + +QT_END_NAMESPACE + +#endif // QCOLORSPACE_P_H diff --git a/src/gui/painting/qcolorspace_p.h b/src/gui/painting/qcolorspace_p.h new file mode 100644 index 00000000000..91107a9a898 --- /dev/null +++ b/src/gui/painting/qcolorspace_p.h @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOLORSPACE_P_H +#define QCOLORSPACE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qcolorspace.h" +#include "qcolormatrix_p.h" +#include "qcolortrc_p.h" +#include "qcolortrclut_p.h" + +#include + +QT_BEGIN_NAMESPACE + +class QColorSpacePrivate : public QSharedData +{ +public: + QColorSpacePrivate(); + QColorSpacePrivate(QColorSpace::ColorSpaceId colorSpaceId); + QColorSpacePrivate(QColorSpace::Gamut gamut, QColorSpace::TransferFunction fun, float gamma); + QColorSpacePrivate(const QColorSpacePrivate &other) = default; + QColorSpacePrivate &operator=(const QColorSpacePrivate &other) = default; + + void initialize(); + void setToXyzMatrix(); + void setTransferFunction(); + bool identifyColorSpace(); + QColorTransform transformationToColorSpace(const QColorSpacePrivate *out) const; + + QColorSpace::ColorSpaceId id; + QColorSpace::Gamut gamut; + QColorSpace::TransferFunction transferFunction; + float gamma; + QColorVector whitePoint; + + QColorTrc trc[3]; + QColorMatrix toXyz; + + QString description; + QByteArray iccProfile; + + mutable QSharedPointer lut[3]; + mutable QAtomicInt lutsGenerated; +}; + +QT_END_NAMESPACE + +#endif // QCOLORSPACE_P_H diff --git a/src/gui/painting/qcolortransferfunction_p.h b/src/gui/painting/qcolortransferfunction_p.h new file mode 100644 index 00000000000..fd7cfa2b2bb --- /dev/null +++ b/src/gui/painting/qcolortransferfunction_p.h @@ -0,0 +1,207 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOLORTRANSFERFUNCTION_P_H +#define QCOLORTRANSFERFUNCTION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +#include + +QT_BEGIN_NAMESPACE + +// Defines a ICC parametric curve type 4 +class Q_GUI_EXPORT QColorTransferFunction +{ +public: + QColorTransferFunction() noexcept + : m_a(1.0f), m_b(0.0f), m_c(1.0f), m_d(0.0f), m_e(0.0f), m_f(0.0f), m_g(1.0f), m_flags(0) + { } + QColorTransferFunction(float a, float b, float c, float d, float e, float f, float g) noexcept + : m_a(a), m_b(b), m_c(c), m_d(d), m_e(e), m_f(f), m_g(g), m_flags(0) + { } + + bool isGamma() const + { + updateHints(); + return m_flags & quint32(Hints::IsGamma); + } + bool isLinear() const + { + updateHints(); + return m_flags & quint32(Hints::IsLinear); + } + bool isSRgb() const + { + updateHints(); + return m_flags & quint32(Hints::IsSRgb); + } + + float apply(float x) const + { + if (x < m_d) + return m_c * x + m_f; + else + return std::pow(m_a * x + m_b, m_g) + m_e; + } + + QColorTransferFunction inverted() const + { + float a, b, c, d, e, f, g; + + d = m_c * m_d + m_f; + + if (!qFuzzyIsNull(m_c)) { + c = 1.0f / m_c; + f = -m_f / m_c; + } else { + c = 0.0f; + f = 0.0f; + } + + if (!qFuzzyIsNull(m_a) && !qFuzzyIsNull(m_g)) { + a = std::pow(1.0f / m_a, m_g); + b = -a * m_e; + e = -m_b / m_a; + g = 1.0f / m_g; + } else { + a = 0.0f; + b = 0.0f; + e = 1.0f; + g = 1.0f; + } + + return QColorTransferFunction(a, b, c, d, e, f, g); + } + + // A few predefined curves: + static QColorTransferFunction fromGamma(float gamma) + { + return QColorTransferFunction(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, gamma); + } + static QColorTransferFunction fromSRgb() + { + return QColorTransferFunction(1.0f / 1.055f, 0.055f / 1.055f, 1.0f / 12.92f, 0.04045f, 0.0f, 0.0f, 2.4f); + } + static QColorTransferFunction fromBt2020() + { + return QColorTransferFunction(1.0f / 1.0993f, 0.0993f / 1.0993f, 1.0f / 4.5f, 0.08145f, 0.0f, 0.0f, 2.2f); + } + static QColorTransferFunction fromProPhotoRgb() + { + return QColorTransferFunction(1.0f, 0.0f, 1.0f / 16.0f, 16.0f / 512.0f, 0.0f, 0.0f, 1.8f); + } + bool matches(const QColorTransferFunction &o) const + { + return paramCompare(m_a, o.m_a) && paramCompare(m_b, o.m_b) + && paramCompare(m_c, o.m_c) && paramCompare(m_d, o.m_d) + && paramCompare(m_e, o.m_e) && paramCompare(m_f, o.m_f) + && paramCompare(m_g, o.m_g); + } + friend inline bool operator==(const QColorTransferFunction &f1, const QColorTransferFunction &f2); + friend inline bool operator!=(const QColorTransferFunction &f1, const QColorTransferFunction &f2); + + float m_a; + float m_b; + float m_c; + float m_d; + float m_e; + float m_f; + float m_g; + +private: + static inline bool paramCompare(float p1, float p2) + { + // Much fuzzier than fuzzy compare. + // It tries match parameters that has been passed through a 8.8 + // fixed point form. + return (qAbs(p1 - p2) <= (1.0f / 512.0f)); + } + + void updateHints() const + { + if (m_flags & quint32(Hints::Calculated)) + return; + // We do not consider the case with m_d = 1.0f linear or simple, + // since it wouldn't be linear for applyExtended(). + bool simple = paramCompare(m_a, 1.0f) && paramCompare(m_b, 0.0f) + && paramCompare(m_d, 0.0f) + && paramCompare(m_e, 0.0f); + if (simple) { + m_flags |= quint32(Hints::IsGamma); + if (qFuzzyCompare(m_g, 1.0f)) + m_flags |= quint32(Hints::IsLinear); + } else { + if (*this == fromSRgb()) + m_flags |= quint32(Hints::IsSRgb); + } + m_flags |= quint32(Hints::Calculated); + } + enum class Hints : quint32 { + Calculated = 1, + IsGamma = 2, + IsLinear = 4, + IsSRgb = 8 + }; + mutable quint32 m_flags; +}; + +inline bool operator==(const QColorTransferFunction &f1, const QColorTransferFunction &f2) +{ + return f1.matches(f2); +} +inline bool operator!=(const QColorTransferFunction &f1, const QColorTransferFunction &f2) +{ + return !f1.matches(f2); +} + +QT_END_NAMESPACE + +#endif // QCOLORTRANSFERFUNCTION_P_H diff --git a/src/gui/painting/qcolortransfertable_p.h b/src/gui/painting/qcolortransfertable_p.h new file mode 100644 index 00000000000..c8b2f7bd926 --- /dev/null +++ b/src/gui/painting/qcolortransfertable_p.h @@ -0,0 +1,245 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOLORTRANSFERTABLE_P_H +#define QCOLORTRANSFERTABLE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include "qcolortransferfunction_p.h" + +#include +#include + +QT_BEGIN_NAMESPACE + +// Defines either an ICC TRC 'curve' or a lut8/lut16 A or B table +class Q_GUI_EXPORT QColorTransferTable +{ +public: + QColorTransferTable() noexcept + : m_tableSize(0) + { } + QColorTransferTable(uint32_t size, const QVector &table) noexcept + : m_tableSize(size) + , m_table8(table) + { } + QColorTransferTable(uint32_t size, const QVector &table) noexcept + : m_tableSize(size) + , m_table16(table) + { } + + bool isValid() const + { + if (m_tableSize < 2) + return false; + +#if !defined(QT_NO_DEBUG) + // The table must describe an injective curve: + if (!m_table8.isEmpty()) { + uint8_t val = 0; + for (uint i = 0; i < m_tableSize; ++i) { + Q_ASSERT(m_table8[i] >= val); + val = m_table8[i]; + } + } + if (!m_table16.isEmpty()) { + uint16_t val = 0; + for (uint i = 0; i < m_tableSize; ++i) { + Q_ASSERT(m_table16[i] >= val); + val = m_table16[i]; + } + } +#endif + return !m_table8.isEmpty() || !m_table16.isEmpty(); + } + + float apply(float x) const + { + x = std::min(std::max(x, 0.0f), 1.0f); + x *= m_tableSize - 1; + uint32_t lo = (int)std::floor(x); + uint32_t hi = std::min(lo + 1, m_tableSize); + float frac = x - lo; + if (!m_table16.isEmpty()) + return (m_table16[lo] * (1.0f - frac) + m_table16[hi] * frac) * (1.0f/65535.0f); + if (!m_table8.isEmpty()) + return (m_table8[lo] * (1.0f - frac) + m_table8[hi] * frac) * (1.0f/255.0f); + return x; + } + + // Apply inverse, optimized by giving a previous result a value < x. + float applyInverse(float x, float resultLargerThan = 0.0f) const + { + Q_ASSERT(resultLargerThan >= 0.0f && resultLargerThan <= 1.0f); + if (x <= 0.0f) + return 0.0f; + if (x >= 1.0f) + return 1.0f; + if (!m_table16.isEmpty()) { + float v = x * 65535.0f; + uint i = std::floor(resultLargerThan * (m_tableSize - 1)) + 1; + for ( ; i < m_tableSize; ++i) { + if (m_table16[i] > v) + break; + } + if (i >= m_tableSize - 1) + return 1.0f; + float y1 = m_table16[i - 1]; + float y2 = m_table16[i]; + Q_ASSERT(x >= y1 && x < y2); + float fr = (v - y1) / (y2 - y1); + return (i + fr) * (1.0f / (m_tableSize - 1)); + + } + if (!m_table8.isEmpty()) { + float v = x * 255.0f; + uint i = std::floor(resultLargerThan * (m_tableSize - 1)) + 1; + for ( ; i < m_tableSize; ++i) { + if (m_table8[i] > v) + break; + } + if (i >= m_tableSize - 1) + return 1.0f; + float y1 = m_table8[i - 1]; + float y2 = m_table8[i]; + Q_ASSERT(x >= y1 && x < y2); + float fr = (v - y1) / (y2 - y1); + return (i + fr) * (1.0f / (m_tableSize - 1)); + } + return x; + } + + bool asColorTransferFunction(QColorTransferFunction *transferFn) + { + Q_ASSERT(isValid()); + Q_ASSERT(transferFn); + if (!m_table8.isEmpty() && (m_table8[0] != 0 || m_table8[m_tableSize - 1] != 255)) + return false; + if (!m_table16.isEmpty() && (m_table16[0] != 0 || m_table16[m_tableSize - 1] != 65535)) + return false; + if (m_tableSize == 2) { + *transferFn = QColorTransferFunction(); // Linear + return true; + } + // The following heuristics are based on those from Skia: + if (m_tableSize == 26 && !m_table16.isEmpty()) { + // code.facebook.com/posts/411525055626587/under-the-hood-improving-facebook-photos + if (m_table16[6] != 3062) + return false; + if (m_table16[12] != 12824) + return false; + if (m_table16[18] != 31237) + return false; + *transferFn = QColorTransferFunction::fromSRgb(); + return true; + } + if (m_tableSize == 1024 && !m_table16.isEmpty()) { + // HP and Canon sRGB gamma tables: + if (m_table16[257] != 3366) + return false; + if (m_table16[513] != 14116) + return false; + if (m_table16[768] != 34318) + return false; + *transferFn = QColorTransferFunction::fromSRgb(); + return true; + } + if (m_tableSize == 4096 && !m_table16.isEmpty()) { + // Nikon, Epson, and lcms2 sRGB gamma tables: + if (m_table16[515] != 960) + return false; + if (m_table16[1025] != 3342) + return false; + if (m_table16[2051] != 14079) + return false; + *transferFn = QColorTransferFunction::fromSRgb(); + return true; + } + return false; + } + friend inline bool operator!=(const QColorTransferTable &t1, const QColorTransferTable &t2); + friend inline bool operator==(const QColorTransferTable &t1, const QColorTransferTable &t2); + + uint32_t m_tableSize; + QVector m_table8; + QVector m_table16; +}; + +inline bool operator!=(const QColorTransferTable &t1, const QColorTransferTable &t2) +{ + if (t1.m_tableSize != t2.m_tableSize) + return true; + if (t1.m_table8.isEmpty() != t2.m_table8.isEmpty()) + return true; + if (t1.m_table16.isEmpty() != t2.m_table16.isEmpty()) + return true; + if (!t1.m_table8.isEmpty()) { + for (quint32 i = 0; i < t1.m_tableSize; ++i) { + if (t1.m_table8[i] != t2.m_table8[i]) + return true; + } + } + if (!t1.m_table16.isEmpty()) { + for (quint32 i = 0; i < t1.m_tableSize; ++i) { + if (t1.m_table16[i] != t2.m_table16[i]) + return true; + } + } + return false; +} + +inline bool operator==(const QColorTransferTable &t1, const QColorTransferTable &t2) +{ + return !(t1 != t2); +} + +QT_END_NAMESPACE + +#endif // QCOLORTRANSFERTABLE_P_H diff --git a/src/gui/painting/qcolortransform.cpp b/src/gui/painting/qcolortransform.cpp new file mode 100644 index 00000000000..b677c4b36ba --- /dev/null +++ b/src/gui/painting/qcolortransform.cpp @@ -0,0 +1,679 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include "qcolortransform.h" +#include "qcolortransform_p.h" + +#include "qcolormatrix_p.h" +#include "qcolorspace_p.h" +#include "qcolortrc_p.h" +#include "qcolortrclut_p.h" + +#include +#include +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +QColorTrcLut *lutFromTrc(const QColorTrc &trc) +{ + if (trc.m_type == QColorTrc::Type::Table) + return QColorTrcLut::fromTransferTable(trc.m_table); + if (trc.m_type == QColorTrc::Type::Function) + return QColorTrcLut::fromTransferFunction(trc.m_fun); + qWarning() << "TRC uninitialized"; + return nullptr; +} + +void QColorTransformPrivate::updateLutsIn() const +{ + if (colorSpaceIn->lutsGenerated.loadAcquire()) + return; + for (int i = 0; i < 3; ++i) { + if (!colorSpaceIn->trc[i].isValid()) + return; + } + + if (colorSpaceIn->trc[0] == colorSpaceIn->trc[1] && colorSpaceIn->trc[0] == colorSpaceIn->trc[2]) { + colorSpaceIn->lut[0].reset(lutFromTrc(colorSpaceIn->trc[0])); + colorSpaceIn->lut[1] = colorSpaceIn->lut[0]; + colorSpaceIn->lut[2] = colorSpaceIn->lut[0]; + } else { + for (int i = 0; i < 3; ++i) + colorSpaceIn->lut[i].reset(lutFromTrc(colorSpaceIn->trc[i])); + } + + colorSpaceIn->lutsGenerated.storeRelease(1); +} + +void QColorTransformPrivate::updateLutsOut() const +{ + if (colorSpaceOut->lutsGenerated.loadAcquire()) + return; + for (int i = 0; i < 3; ++i) { + if (!colorSpaceOut->trc[i].isValid()) + return; + } + + if (colorSpaceOut->trc[0] == colorSpaceOut->trc[1] && colorSpaceOut->trc[0] == colorSpaceOut->trc[2]) { + colorSpaceOut->lut[0].reset(lutFromTrc(colorSpaceOut->trc[0])); + colorSpaceOut->lut[1] = colorSpaceOut->lut[0]; + colorSpaceOut->lut[2] = colorSpaceOut->lut[0]; + } else { + for (int i = 0; i < 3; ++i) + colorSpaceOut->lut[i].reset(lutFromTrc(colorSpaceOut->trc[i])); + } + + colorSpaceOut->lutsGenerated.storeRelease(1); +} + +/*! + \class QColorTransform + \brief The QColorTransform class is a transformation between color spaces. + \since 5.14 + + \ingroup painting + \ingroup appearance + \inmodule QtGui + + QColorTransform is an instantiation of a transformation between color spaces. + It can be applied on color and pixels to convert them from one color space to + another. + + Setting up a QColorTransform takes some preprocessing, so keeping around + QColorTransforms that you need often is recommended, instead of generating + them on the fly. +*/ + + +QColorTransform::~QColorTransform() noexcept +{ +} + +/*! + Applies the color transformation on the QRgb value \a argb. + + The input should be opaque or unpremultiplied. +*/ +QRgb QColorTransform::map(const QRgb &argb) const +{ + if (!d_ptr) + return argb; + Q_D(const QColorTransform); + constexpr float f = 1.0f / 255.0f; + QColorVector c = { qRed(argb) * f, qGreen(argb) * f, qBlue(argb) * f }; + c.x = d->colorSpaceIn->trc[0].apply(c.x); + c.y = d->colorSpaceIn->trc[1].apply(c.y); + c.z = d->colorSpaceIn->trc[2].apply(c.z); + c = d->colorMatrix.map(c); + c.x = std::max(0.0f, std::min(1.0f, c.x)); + c.y = std::max(0.0f, std::min(1.0f, c.y)); + c.z = std::max(0.0f, std::min(1.0f, c.z)); + if (d->colorSpaceOut->lutsGenerated.loadAcquire()) { + c.x = d->colorSpaceOut->lut[0]->fromLinear(c.x); + c.y = d->colorSpaceOut->lut[1]->fromLinear(c.y); + c.z = d->colorSpaceOut->lut[2]->fromLinear(c.z); + } else { + c.x = d->colorSpaceOut->trc[0].applyInverse(c.x); + c.y = d->colorSpaceOut->trc[1].applyInverse(c.y); + c.z = d->colorSpaceOut->trc[2].applyInverse(c.z); + } + + return qRgba(c.x * 255 + 0.5f, c.y * 255 + 0.5f, c.z * 255 + 0.5f, qAlpha(argb)); +} + +/*! + Applies the color transformation on the QRgba64 value \a rgba64. + + The input should be opaque or unpremultiplied. +*/ +QRgba64 QColorTransform::map(const QRgba64 &rgba64) const +{ + if (!d_ptr) + return rgba64; + Q_D(const QColorTransform); + constexpr float f = 1.0f / 65535.0f; + QColorVector c = { rgba64.red() * f, rgba64.green() * f, rgba64.blue() * f }; + c.x = d->colorSpaceIn->trc[0].apply(c.x); + c.y = d->colorSpaceIn->trc[1].apply(c.y); + c.z = d->colorSpaceIn->trc[2].apply(c.z); + c = d->colorMatrix.map(c); + c.x = std::max(0.0f, std::min(1.0f, c.x)); + c.y = std::max(0.0f, std::min(1.0f, c.y)); + c.z = std::max(0.0f, std::min(1.0f, c.z)); + if (d->colorSpaceOut->lutsGenerated.loadAcquire()) { + c.x = d->colorSpaceOut->lut[0]->fromLinear(c.x); + c.y = d->colorSpaceOut->lut[1]->fromLinear(c.y); + c.z = d->colorSpaceOut->lut[2]->fromLinear(c.z); + } else { + c.x = d->colorSpaceOut->trc[0].applyInverse(c.x); + c.y = d->colorSpaceOut->trc[1].applyInverse(c.y); + c.z = d->colorSpaceOut->trc[2].applyInverse(c.z); + } + + return QRgba64::fromRgba64(c.x * 65535, c.y * 65535, c.z * 65535, rgba64.alpha()); +} + +/*! + Applies the color transformation on the QColor value \a color. + +*/ +QColor QColorTransform::map(const QColor &color) const +{ + if (!d_ptr) + return color; + Q_D(const QColorTransform); + QColorVector c = { (float)color.redF(), (float)color.greenF(), (float)color.blueF() }; + c.x = d->colorSpaceIn->trc[0].apply(c.x); + c.y = d->colorSpaceIn->trc[1].apply(c.y); + c.z = d->colorSpaceIn->trc[2].apply(c.z); + c = d->colorMatrix.map(c); + if (d_ptr->colorSpaceOut->lutsGenerated.loadAcquire()) { + c.x = d->colorSpaceOut->lut[0]->fromLinear(c.x); + c.y = d->colorSpaceOut->lut[1]->fromLinear(c.y); + c.z = d->colorSpaceOut->lut[2]->fromLinear(c.z); + } else { + c.x = d->colorSpaceOut->trc[0].applyInverse(c.x); + c.y = d->colorSpaceOut->trc[1].applyInverse(c.y); + c.z = d->colorSpaceOut->trc[2].applyInverse(c.z); + } + QColor out; + out.setRgbF(c.x, c.y, c.z, color.alphaF()); + return out; +} + +// Optimized sub-routines for fast block based conversion: + +static void applyMatrix(QColorVector *buffer, const qsizetype len, const QColorMatrix &colorMatrix) +{ +#if defined(__SSE2__) + const __m128 minV = _mm_set1_ps(0.0f); + const __m128 maxV = _mm_set1_ps(1.0f); + const __m128 xMat = _mm_loadu_ps(&colorMatrix.r.x); + const __m128 yMat = _mm_loadu_ps(&colorMatrix.g.x); + const __m128 zMat = _mm_loadu_ps(&colorMatrix.b.x); + for (qsizetype j = 0; j < len; ++j) { + __m128 c = _mm_loadu_ps(&buffer[j].x); + __m128 cx = _mm_shuffle_ps(c, c, _MM_SHUFFLE(0, 0, 0, 0)); + __m128 cy = _mm_shuffle_ps(c, c, _MM_SHUFFLE(1, 1, 1, 1)); + __m128 cz = _mm_shuffle_ps(c, c, _MM_SHUFFLE(2, 2, 2, 2)); + cx = _mm_mul_ps(cx, xMat); + cy = _mm_mul_ps(cy, yMat); + cz = _mm_mul_ps(cz, zMat); + cx = _mm_add_ps(cx, cy); + cx = _mm_add_ps(cx, cz); + // Clamp: + cx = _mm_min_ps(cx, maxV); + cx = _mm_max_ps(cx, minV); + _mm_storeu_ps(&buffer[j].x, cx); + } +#else + for (int j = 0; j < len; ++j) { + const QColorVector cv = colorMatrix.map(buffer[j]); + buffer[j].x = std::max(0.0f, std::min(1.0f, cv.x)); + buffer[j].y = std::max(0.0f, std::min(1.0f, cv.y)); + buffer[j].z = std::max(0.0f, std::min(1.0f, cv.z)); + } +#endif +} + +template +static void loadPremultiplied(QColorVector *buffer, const T *src, const qsizetype len, const QColorTransformPrivate *d_ptr); +template +static void loadUnpremultiplied(QColorVector *buffer, const T *src, const qsizetype len, const QColorTransformPrivate *d_ptr); + +#if defined(__SSE2__) +// Load to [0-alpha] in 4x32 SIMD +template +static inline void loadP(const T &p, __m128i &v); + +template<> +inline void loadP(const QRgb &p, __m128i &v) +{ + v = _mm_cvtsi32_si128(p); +#if defined(__SSE4_1__) + v = _mm_cvtepu8_epi32(v); +#else + v = _mm_unpacklo_epi8(v, _mm_setzero_si128()); + v = _mm_unpacklo_epi16(v, _mm_setzero_si128()); +#endif +} + +template<> +inline void loadP(const QRgba64 &p, __m128i &v) +{ + v = _mm_loadl_epi64((const __m128i *)&p); +#if defined(__SSE4_1__) + v = _mm_cvtepu16_epi32(v); +#else + v = _mm_unpacklo_epi16(v, _mm_setzero_si128()); +#endif + // Shuffle to ARGB as the template below expects it + v = _mm_shuffle_epi32(v, _MM_SHUFFLE(3, 0, 1, 2)); +} + +template +static void loadPremultiplied(QColorVector *buffer, const T *src, const qsizetype len, const QColorTransformPrivate *d_ptr) +{ + const __m128 v4080 = _mm_set1_ps(4080.f); + const __m128 iFF00 = _mm_set1_ps(1.0f / (255 * 256)); + for (qsizetype i = 0; i < len; ++i) { + __m128i v; + loadP(src[i], v); + __m128 vf = _mm_cvtepi32_ps(v); + // Approximate 1/a: + __m128 va = _mm_shuffle_ps(vf, vf, _MM_SHUFFLE(3, 3, 3, 3)); + __m128 via = _mm_rcp_ps(va); + via = _mm_sub_ps(_mm_add_ps(via, via), _mm_mul_ps(via, _mm_mul_ps(via, va))); + // v * (1/a) + vf = _mm_mul_ps(vf, via); + + // Handle zero alpha + __m128 vAlphaMask = _mm_cmpeq_ps(va, _mm_set1_ps(0.0f)); + vf = _mm_andnot_ps(vAlphaMask, vf); + + // LUT + v = _mm_cvtps_epi32(_mm_mul_ps(vf, v4080)); + const int ridx = _mm_extract_epi16(v, 4); + const int gidx = _mm_extract_epi16(v, 2); + const int bidx = _mm_extract_epi16(v, 0); + v = _mm_insert_epi16(v, d_ptr->colorSpaceIn->lut[0]->m_toLinear[ridx], 0); + v = _mm_insert_epi16(v, d_ptr->colorSpaceIn->lut[1]->m_toLinear[gidx], 2); + v = _mm_insert_epi16(v, d_ptr->colorSpaceIn->lut[2]->m_toLinear[bidx], 4); + vf = _mm_mul_ps(_mm_cvtepi32_ps(v), iFF00); + + _mm_storeu_ps(&buffer[i].x, vf); + } +} + +// Load to [0-4080] in 4x32 SIMD +template +static inline void loadPU(const T &p, __m128i &v); + +template<> +inline void loadPU(const QRgb &p, __m128i &v) +{ + v = _mm_cvtsi32_si128(p); +#if defined(__SSE4_1__) + v = _mm_cvtepu8_epi32(v); +#else + v = _mm_unpacklo_epi8(v, _mm_setzero_si128()); + v = _mm_unpacklo_epi16(v, _mm_setzero_si128()); +#endif + v = _mm_slli_epi32(v, 4); +} + +template<> +inline void loadPU(const QRgba64 &p, __m128i &v) +{ + v = _mm_loadl_epi64((const __m128i *)&p); + v = _mm_sub_epi16(v, _mm_srli_epi16(v, 8)); +#if defined(__SSE4_1__) + v = _mm_cvtepu16_epi32(v); +#else + v = _mm_unpacklo_epi16(v, _mm_setzero_si128()); +#endif + v = _mm_srli_epi32(v, 4); + // Shuffle to ARGB as the template below expects it + v = _mm_shuffle_epi32(v, _MM_SHUFFLE(3, 0, 1, 2)); +} + +template +void loadUnpremultiplied(QColorVector *buffer, const T *src, const qsizetype len, const QColorTransformPrivate *d_ptr) +{ + const __m128 iFF00 = _mm_set1_ps(1.0f / (255 * 256)); + for (qsizetype i = 0; i < len; ++i) { + __m128i v; + loadPU(src[i], v); + const int ridx = _mm_extract_epi16(v, 4); + const int gidx = _mm_extract_epi16(v, 2); + const int bidx = _mm_extract_epi16(v, 0); + v = _mm_insert_epi16(v, d_ptr->colorSpaceIn->lut[0]->m_toLinear[ridx], 0); + v = _mm_insert_epi16(v, d_ptr->colorSpaceIn->lut[1]->m_toLinear[gidx], 2); + v = _mm_insert_epi16(v, d_ptr->colorSpaceIn->lut[2]->m_toLinear[bidx], 4); + __m128 vf = _mm_mul_ps(_mm_cvtepi32_ps(v), iFF00); + _mm_storeu_ps(&buffer[i].x, vf); + } +} + +#else +template<> +void loadPremultiplied(QColorVector *buffer, const QRgb *src, const qsizetype len, const QColorTransformPrivate *d_ptr) +{ + for (qsizetype i = 0; i < len; ++i) { + const uint p = src[i]; + const int a = qAlpha(p); + if (a) { + const float ia = 4080.0f / a; + const int ridx = int(qRed(p) * ia + 0.5f); + const int gidx = int(qGreen(p) * ia + 0.5f); + const int bidx = int(qBlue(p) * ia + 0.5f); + buffer[i].x = d_ptr->colorSpaceIn->lut[0]->m_toLinear[ridx] * (1.0f / (255 * 256)); + buffer[i].y = d_ptr->colorSpaceIn->lut[1]->m_toLinear[gidx] * (1.0f / (255 * 256)); + buffer[i].z = d_ptr->colorSpaceIn->lut[2]->m_toLinear[bidx] * (1.0f / (255 * 256)); + } else { + buffer[i].x = buffer[i].y = buffer[i].z = 0.0f; + } + } +} + +template<> +void loadPremultiplied(QColorVector *buffer, const QRgba64 *src, const qsizetype len, const QColorTransformPrivate *d_ptr) +{ + for (qsizetype i = 0; i < len; ++i) { + const QRgba64 &p = src[i]; + const int a = p.alpha(); + if (a) { + const float ia = 4080.0f / a; + const int ridx = int(p.red() * ia + 0.5f); + const int gidx = int(p.green() * ia + 0.5f); + const int bidx = int(p.blue() * ia + 0.5f); + buffer[i].x = d_ptr->colorSpaceIn->lut[0]->m_toLinear[ridx] * (1.0f / (255 * 256)); + buffer[i].y = d_ptr->colorSpaceIn->lut[1]->m_toLinear[gidx] * (1.0f / (255 * 256)); + buffer[i].z = d_ptr->colorSpaceIn->lut[2]->m_toLinear[bidx] * (1.0f / (255 * 256)); + } else { + buffer[i].x = buffer[i].y = buffer[i].z = 0.0f; + } + } +} + +template<> +void loadUnpremultiplied(QColorVector *buffer, const QRgb *src, const qsizetype len, const QColorTransformPrivate *d_ptr) +{ + for (qsizetype i = 0; i < len; ++i) { + const uint p = src[i]; + buffer[i].x = d_ptr->colorSpaceIn->lut[0]->u8ToLinearF32(qRed(p)); + buffer[i].y = d_ptr->colorSpaceIn->lut[1]->u8ToLinearF32(qGreen(p)); + buffer[i].z = d_ptr->colorSpaceIn->lut[2]->u8ToLinearF32(qBlue(p)); + } +} + +template<> +void loadUnpremultiplied(QColorVector *buffer, const QRgba64 *src, const qsizetype len, const QColorTransformPrivate *d_ptr) +{ + for (qsizetype i = 0; i < len; ++i) { + const QRgba64 &p = src[i]; + buffer[i].x = d_ptr->colorSpaceIn->lut[0]->u16ToLinearF32(p.red()); + buffer[i].y = d_ptr->colorSpaceIn->lut[1]->u16ToLinearF32(p.green()); + buffer[i].z = d_ptr->colorSpaceIn->lut[2]->u16ToLinearF32(p.blue()); + } +} +#endif + +static void storePremultiplied(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) +{ +#if defined(__SSE2__) + const __m128 v4080 = _mm_set1_ps(4080.f); + const __m128 iFF00 = _mm_set1_ps(1.0f / (255 * 256)); + for (qsizetype i = 0; i < len; ++i) { + const int a = qAlpha(src[i]); + __m128 vf = _mm_loadu_ps(&buffer[i].x); + __m128i v = _mm_cvtps_epi32(_mm_mul_ps(vf, v4080)); + __m128 va = _mm_set1_ps(a); + va = _mm_mul_ps(va, iFF00); + const int ridx = _mm_extract_epi16(v, 0); + const int gidx = _mm_extract_epi16(v, 2); + const int bidx = _mm_extract_epi16(v, 4); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[0]->m_fromLinear[ridx], 4); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[1]->m_fromLinear[gidx], 2); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[2]->m_fromLinear[bidx], 0); + vf = _mm_cvtepi32_ps(v); + vf = _mm_mul_ps(vf, va); + v = _mm_cvtps_epi32(vf); + v = _mm_packs_epi32(v, v); + v = _mm_insert_epi16(v, a, 3); + v = _mm_packus_epi16(v, v); + dst[i] = _mm_cvtsi128_si32(v); + } +#else + for (qsizetype i = 0; i < len; ++i) { + const int a = qAlpha(src[i]); + const float fa = a / (255.0f * 256.0f); + const float r = d_ptr->colorSpaceOut->lut[0]->m_fromLinear[int(buffer[i].x * 4080.0f + 0.5f)]; + const float g = d_ptr->colorSpaceOut->lut[1]->m_fromLinear[int(buffer[i].y * 4080.0f + 0.5f)]; + const float b = d_ptr->colorSpaceOut->lut[2]->m_fromLinear[int(buffer[i].z * 4080.0f + 0.5f)]; + dst[i] = qRgba(r * fa + 0.5f, g * fa + 0.5f, b * fa + 0.5f, a); + } +#endif +} + +static void storeUnpremultiplied(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) +{ +#if defined(__SSE2__) + const __m128 v4080 = _mm_set1_ps(4080.f); + for (qsizetype i = 0; i < len; ++i) { + const int a = qAlpha(src[i]); + __m128 vf = _mm_loadu_ps(&buffer[i].x); + __m128i v = _mm_cvtps_epi32(_mm_mul_ps(vf, v4080)); + const int ridx = _mm_extract_epi16(v, 0); + const int gidx = _mm_extract_epi16(v, 2); + const int bidx = _mm_extract_epi16(v, 4); + v = _mm_setzero_si128(); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[0]->m_fromLinear[ridx], 2); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[1]->m_fromLinear[gidx], 1); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[2]->m_fromLinear[bidx], 0); + v = _mm_add_epi16(v, _mm_set1_epi16(0x80)); + v = _mm_srli_epi16(v, 8); + v = _mm_insert_epi16(v, a, 3); + v = _mm_packus_epi16(v, v); + dst[i] = _mm_cvtsi128_si32(v); + } +#else + for (qsizetype i = 0; i < len; ++i) { + const int r = d_ptr->colorSpaceOut->lut[0]->u8FromLinearF32(buffer[i].x); + const int g = d_ptr->colorSpaceOut->lut[1]->u8FromLinearF32(buffer[i].y); + const int b = d_ptr->colorSpaceOut->lut[2]->u8FromLinearF32(buffer[i].z); + dst[i] = (src[i] & 0xff000000) | (r << 16) | (g << 8) | (b << 0); + } +#endif +} + +static void storeOpaque(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) +{ + Q_UNUSED(src); +#if defined(__SSE2__) + const __m128 v4080 = _mm_set1_ps(4080.f); + for (qsizetype i = 0; i < len; ++i) { + __m128 vf = _mm_loadu_ps(&buffer[i].x); + __m128i v = _mm_cvtps_epi32(_mm_mul_ps(vf, v4080)); + const int ridx = _mm_extract_epi16(v, 0); + const int gidx = _mm_extract_epi16(v, 2); + const int bidx = _mm_extract_epi16(v, 4); + v = _mm_setzero_si128(); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[0]->m_fromLinear[ridx], 2); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[1]->m_fromLinear[gidx], 1); + v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[2]->m_fromLinear[bidx], 0); + v = _mm_add_epi16(v, _mm_set1_epi16(0x80)); + v = _mm_srli_epi16(v, 8); + v = _mm_insert_epi16(v, 255, 3); + v = _mm_packus_epi16(v, v); + dst[i] = _mm_cvtsi128_si32(v); + } +#else + for (qsizetype i = 0; i < len; ++i) { + const int r = d_ptr->colorSpaceOut->lut[0]->u8FromLinearF32(buffer[i].x); + const int g = d_ptr->colorSpaceOut->lut[1]->u8FromLinearF32(buffer[i].y); + const int b = d_ptr->colorSpaceOut->lut[2]->u8FromLinearF32(buffer[i].z); + dst[i] = 0xff000000 | (r << 16) | (g << 8) | (b << 0); + } +#endif +} + +static void storePremultiplied(QRgba64 *dst, const QRgba64 *src, const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) +{ + for (qsizetype i = 0; i < len; ++i) { + const int a = src[i].alpha(); + const float fa = a / (255.0f * 256.0f); + const float r = d_ptr->colorSpaceOut->lut[0]->m_fromLinear[int(buffer[i].x * 4080.0f + 0.5f)]; + const float g = d_ptr->colorSpaceOut->lut[1]->m_fromLinear[int(buffer[i].y * 4080.0f + 0.5f)]; + const float b = d_ptr->colorSpaceOut->lut[2]->m_fromLinear[int(buffer[i].z * 4080.0f + 0.5f)]; + dst[i] = qRgba64(r * fa + 0.5f, g * fa + 0.5f, b * fa + 0.5f, a); + } +} + +static void storeUnpremultiplied(QRgba64 *dst, const QRgba64 *src, const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) +{ + for (qsizetype i = 0; i < len; ++i) { + const int r = d_ptr->colorSpaceOut->lut[0]->u16FromLinearF32(buffer[i].x); + const int g = d_ptr->colorSpaceOut->lut[1]->u16FromLinearF32(buffer[i].y); + const int b = d_ptr->colorSpaceOut->lut[2]->u16FromLinearF32(buffer[i].z); + dst[i] = qRgba64(r, g, b, src[i].alpha()); + } +} + +static void storeOpaque(QRgba64 *dst, const QRgba64 *src, const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) +{ + Q_UNUSED(src); + for (qsizetype i = 0; i < len; ++i) { + const int r = d_ptr->colorSpaceOut->lut[0]->u16FromLinearF32(buffer[i].x); + const int g = d_ptr->colorSpaceOut->lut[1]->u16FromLinearF32(buffer[i].y); + const int b = d_ptr->colorSpaceOut->lut[2]->u16FromLinearF32(buffer[i].z); + dst[i] = qRgba64(r, g, b, 0xFFFF); + } +} + +static constexpr qsizetype WorkBlockSize = 256; + +template +void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const +{ + if (!colorMatrix.isValid()) + return; + + updateLutsIn(); + updateLutsOut(); + + bool doApplyMatrix = (colorMatrix != QColorMatrix::identity()); + + QColorVector buffer[WorkBlockSize]; + qsizetype i = 0; + while (i < count) { + const qsizetype len = qMin(count - i, WorkBlockSize); + if (flags & InputPremultiplied) + loadPremultiplied(buffer, src + i, len, this); + else + loadUnpremultiplied(buffer, src + i, len, this); + + if (doApplyMatrix) + applyMatrix(buffer, len, colorMatrix); + + if (flags & InputOpaque) + storeOpaque(dst + i, src + i, buffer, len, this); + else if (flags & OutputPremultiplied) + storePremultiplied(dst + i, src + i, buffer, len, this); + else + storeUnpremultiplied(dst + i, src + i, buffer, len, this); + + i += len; + } +} + +/*! + \internal + \enum QColorTransformPrivate::TransformFlag + + Defines how the transform is to be applied. + + \value Unpremultiplied The input and output should both be unpremultiplied. + \value InputOpaque The input is guaranteed to be opaque. + \value InputPremultiplied The input is premultiplied. + \value OutputPremultiplied The output should be premultiplied. + \value Premultiplied Both input and output should both be premultiplied. +*/ + +/*! + \internal + Prepares a color transformation for fast application. You do not need to + call this explicitly as it will be called implicitly on the first transforms, but + if you want predictable performance on the first transforms, you can perform it + in advance. + + \sa QColorTransform::map(), apply() +*/ +void QColorTransformPrivate::prepare() +{ + updateLutsIn(); + updateLutsOut(); +} + +/*! + \internal + Applies the color transformation on \a count QRgb pixels starting from + \a src and stores the result in \a dst. + + Thread-safe if prepare() has been called first. + + Assumes unpremultiplied data by default. Set \a flags to change defaults. + + \sa prepare() +*/ +void QColorTransformPrivate::apply(QRgb *dst, const QRgb *src, qsizetype count, TransformFlags flags) const +{ + apply(dst, src, count, flags); +} + +/*! + \internal + Applies the color transformation on \a count QRgba64 pixels starting from + \a src and stores the result in \a dst. + + Thread-safe if prepare() has been called first. + + Assumes unpremultiplied data by default. Set \a flags to change defaults. + + \sa prepare() +*/ +void QColorTransformPrivate::apply(QRgba64 *dst, const QRgba64 *src, qsizetype count, TransformFlags flags) const +{ + apply(dst, src, count, flags); +} + + +QT_END_NAMESPACE diff --git a/src/gui/painting/qcolortransform.h b/src/gui/painting/qcolortransform.h new file mode 100644 index 00000000000..9274387b97b --- /dev/null +++ b/src/gui/painting/qcolortransform.h @@ -0,0 +1,93 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOLORTRANSFORM_H +#define QCOLORTRANSFORM_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QColor; +class QRgba64; +class QColorSpacePrivate; +class QColorTransformPrivate; + +class Q_GUI_EXPORT QColorTransform +{ +public: + QColorTransform() noexcept : d_ptr(nullptr) { } + ~QColorTransform() noexcept; + QColorTransform(const QColorTransform &colorTransform) noexcept + : d_ptr(colorTransform.d_ptr) + { } + QColorTransform(QColorTransform &&colorTransform) noexcept + : d_ptr(std::move(colorTransform.d_ptr)) + { } + QColorTransform &operator=(const QColorTransform &other) noexcept + { + d_ptr = other.d_ptr; + return *this; + } + QColorTransform &operator=(QColorTransform &&other) noexcept + { + d_ptr = std::move(other.d_ptr); + return *this; + } + + bool isNull() const { return d_ptr.isNull(); } + + QRgb map(const QRgb &argb) const; + QRgba64 map(const QRgba64 &rgba64) const; + QColor map(const QColor &color) const; + +private: + friend class QColorSpace; + friend class QColorSpacePrivate; + friend class QImage; + + Q_DECLARE_PRIVATE(QColorTransform) + QSharedPointer d_ptr; +}; + +QT_END_NAMESPACE + +#endif // QCOLORTRANSFORM_H diff --git a/src/gui/painting/qcolortransform_p.h b/src/gui/painting/qcolortransform_p.h new file mode 100644 index 00000000000..74a1e7fe0a8 --- /dev/null +++ b/src/gui/painting/qcolortransform_p.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOLORTRANSFORM_P_H +#define QCOLORTRANSFORM_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qcolormatrix_p.h" +#include "qcolorspace_p.h" + +QT_BEGIN_NAMESPACE + +class QColorTransformPrivate +{ +public: + QColorMatrix colorMatrix; + QExplicitlySharedDataPointer colorSpaceIn; + QExplicitlySharedDataPointer colorSpaceOut; + + void updateLutsIn() const; + void updateLutsOut() const; + bool simpleGammaCorrection() const; + + void prepare(); + enum TransformFlag { + Unpremultiplied = 0, + InputOpaque = 1, + InputPremultiplied = 2, + OutputPremultiplied = 4, + Premultiplied = (InputPremultiplied | OutputPremultiplied) + }; + Q_DECLARE_FLAGS(TransformFlags, TransformFlag) + + void apply(QRgb *dst, const QRgb *src, qsizetype count, TransformFlags flags = Unpremultiplied) const; + void apply(QRgba64 *dst, const QRgba64 *src, qsizetype count, TransformFlags flags = Unpremultiplied) const; + + template + void apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const; +}; + +QT_END_NAMESPACE + +#endif // QCOLORTRANSFORM_P_H diff --git a/src/gui/painting/qcolortrc_p.h b/src/gui/painting/qcolortrc_p.h new file mode 100644 index 00000000000..3a649f37569 --- /dev/null +++ b/src/gui/painting/qcolortrc_p.h @@ -0,0 +1,129 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOLORTRC_P_H +#define QCOLORTRC_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include "qcolortransferfunction_p.h" +#include "qcolortransfertable_p.h" + +QT_BEGIN_NAMESPACE + + +// Defines an ICC TRC (Tone Reproduction Curve) +class Q_GUI_EXPORT QColorTrc +{ +public: + QColorTrc() noexcept : m_type(Type::Uninitialized) + { } + QColorTrc(const QColorTransferFunction &fun) : m_type(Type::Function), m_fun(fun) + { } + QColorTrc(const QColorTransferTable &table) : m_type(Type::Table), m_table(table) + { } + + enum class Type { + Uninitialized, + Function, + Table + }; + + bool isLinear() const + { + return m_type == Type::Uninitialized || (m_type == Type::Function && m_fun.isLinear()); + } + bool isValid() const + { + return m_type != Type::Uninitialized; + } + float apply(float x) const + { + if (m_type == Type::Table) + return m_table.apply(x); + if (m_type == Type::Function) + return m_fun.apply(x); + return x; + } + + float applyInverse(float x) const + { + if (m_type == Type::Table) + return m_table.applyInverse(x); + if (m_type == Type::Function) + return m_fun.inverted().apply(x); + return x; + } + + friend inline bool operator!=(const QColorTrc &o1, const QColorTrc &o2); + friend inline bool operator==(const QColorTrc &o1, const QColorTrc &o2); + + Type m_type; + QColorTransferFunction m_fun; + QColorTransferTable m_table; +}; + +inline bool operator!=(const QColorTrc &o1, const QColorTrc &o2) +{ + if (o1.m_type != o2.m_type) + return true; + if (o1.m_type == QColorTrc::Type::Function) + return o1.m_fun != o2.m_fun; + if (o1.m_type == QColorTrc::Type::Table) + return o1.m_table != o2.m_table; + return false; +} +inline bool operator==(const QColorTrc &o1, const QColorTrc &o2) +{ + return !(o1 != o2); +} + +QT_END_NAMESPACE + +#endif // QCOLORTRC diff --git a/src/gui/painting/qcolorprofile.cpp b/src/gui/painting/qcolortrclut.cpp similarity index 69% rename from src/gui/painting/qcolorprofile.cpp rename to src/gui/painting/qcolortrclut.cpp index 3b7b0a248b2..268d7252b4c 100644 --- a/src/gui/painting/qcolorprofile.cpp +++ b/src/gui/painting/qcolortrclut.cpp @@ -37,14 +37,16 @@ ** ****************************************************************************/ -#include "qcolorprofile_p.h" +#include "qcolortrclut_p.h" +#include "qcolortransferfunction_p.h" +#include "qcolortransfertable_p.h" #include QT_BEGIN_NAMESPACE -QColorProfile *QColorProfile::fromGamma(qreal gamma) +QColorTrcLut *QColorTrcLut::fromGamma(qreal gamma) { - QColorProfile *cp = new QColorProfile; + QColorTrcLut *cp = new QColorTrcLut; for (int i = 0; i <= (255 * 16); ++i) { cp->m_toLinear[i] = ushort(qRound(qPow(i / qreal(255 * 16), gamma) * (255 * 256))); @@ -54,31 +56,28 @@ QColorProfile *QColorProfile::fromGamma(qreal gamma) return cp; } -static qreal srgbToLinear(qreal v) +QColorTrcLut *QColorTrcLut::fromTransferFunction(const QColorTransferFunction &fun) { - const qreal a = 0.055; - if (v <= qreal(0.04045)) - return v / qreal(12.92); - else - return qPow((v + a) / (qreal(1) + a), qreal(2.4)); -} - -static qreal linearToSrgb(qreal v) -{ - const qreal a = 0.055; - if (v <= qreal(0.0031308)) - return v * qreal(12.92); - else - return (qreal(1) + a) * qPow(v, qreal(1.0 / 2.4)) - a; -} - -QColorProfile *QColorProfile::fromSRgb() -{ - QColorProfile *cp = new QColorProfile; + QColorTrcLut *cp = new QColorTrcLut; + QColorTransferFunction inv = fun.inverted(); for (int i = 0; i <= (255 * 16); ++i) { - cp->m_toLinear[i] = ushort(qRound(srgbToLinear(i / qreal(255 * 16)) * (255 * 256))); - cp->m_fromLinear[i] = ushort(qRound(linearToSrgb(i / qreal(255 * 16)) * (255 * 256))); + cp->m_toLinear[i] = ushort(qRound(fun.apply(i / qreal(255 * 16)) * (255 * 256))); + cp->m_fromLinear[i] = ushort(qRound(inv.apply(i / qreal(255 * 16)) * (255 * 256))); + } + + return cp; +} + +QColorTrcLut *QColorTrcLut::fromTransferTable(const QColorTransferTable &table) +{ + QColorTrcLut *cp = new QColorTrcLut; + + float minInverse = 0.0f; + for (int i = 0; i <= (255 * 16); ++i) { + cp->m_toLinear[i] = ushort(qBound(0, qRound(table.apply(i / qreal(255 * 16)) * (255 * 256)), 65280)); + minInverse = table.applyInverse(i / qreal(255 * 16), minInverse); + cp->m_fromLinear[i] = ushort(qBound(0, qRound(minInverse * (255 * 256)), 65280)); } return cp; diff --git a/src/gui/painting/qcolorprofile_p.h b/src/gui/painting/qcolortrclut_p.h similarity index 87% rename from src/gui/painting/qcolorprofile_p.h rename to src/gui/painting/qcolortrclut_p.h index 425e9abace0..76a6a60803d 100644 --- a/src/gui/painting/qcolorprofile_p.h +++ b/src/gui/painting/qcolortrclut_p.h @@ -37,8 +37,8 @@ ** ****************************************************************************/ -#ifndef QCOLORPROFILE_P_H -#define QCOLORPROFILE_P_H +#ifndef QCOLORTRCLUT_P_H +#define QCOLORTRCLUT_P_H // // W A R N I N G @@ -52,21 +52,29 @@ // #include +#include #include #include +#include + #if defined(__SSE2__) #include #elif defined(__ARM_NEON__) || defined(__ARM_NEON) #include #endif + QT_BEGIN_NAMESPACE -class Q_GUI_EXPORT QColorProfile +class QColorTransferFunction; +class QColorTransferTable; + +class Q_GUI_EXPORT QColorTrcLut : public QEnableSharedFromThis { public: - static QColorProfile *fromGamma(qreal gamma); - static QColorProfile *fromSRgb(); + static QColorTrcLut *fromGamma(qreal gamma); + static QColorTrcLut *fromTransferFunction(const QColorTransferFunction &transfn); + static QColorTrcLut *fromTransferTable(const QColorTransferTable &transTable); // The following methods all convert opaque or unpremultiplied colors: @@ -121,6 +129,25 @@ public: return convertWithTable(rgb64, m_toLinear); } + float u8ToLinearF32(int c) const + { + ushort v = m_toLinear[c << 4]; + return v * (1.0f / (255*256)); + } + + float u16ToLinearF32(int c) const + { + c -= (c >> 8); + ushort v = m_toLinear[c >> 4]; + return v * (1.0f / (255*256)); + } + + float toLinear(float f) const + { + ushort v = m_toLinear[(int)(f * (255 * 16) + 0.5f)]; + return v * (1.0f / (255*256)); + } + QRgb fromLinear64(QRgba64 rgb64) const { #if defined(__SSE2__) @@ -176,8 +203,31 @@ public: return convertWithTable(rgb64, m_fromLinear); } + int u8FromLinearF32(float f) const + { + ushort v = m_fromLinear[(int)(f * (255 * 16) + 0.5f)]; + return (v + 0x80) >> 8; + } + int u16FromLinearF32(float f) const + { + ushort v = m_fromLinear[(int)(f * (255 * 16) + 0.5f)]; + return v + (v >> 8); + } + float fromLinear(float f) const + { + ushort v = m_fromLinear[(int)(f * (255 * 16) + 0.5f)]; + return v * (1.0f / (255*256)); + } + + // We translate to 0-65280 (255*256) instead to 0-65535 to make simple + // shifting an accurate conversion. + // We translate from 0-4080 (255*16) for the same speed up, and to keep + // the tables small enough to fit in most inner caches. + ushort m_toLinear[(255 * 16) + 1]; // [0-4080] -> [0-65280] + ushort m_fromLinear[(255 * 16) + 1]; // [0-4080] -> [0-65280] + private: - QColorProfile() { } + QColorTrcLut() { } Q_ALWAYS_INLINE static QRgb convertWithTable(QRgb rgb32, const ushort *table) { @@ -230,16 +280,8 @@ private: return QRgba64::fromRgba64(r, g, b, rgb64.alpha()); #endif } - - // We translate to 0-65280 (255*256) instead to 0-65535 to make simple - // shifting an accurate conversion. - // We translate from 0-4080 (255*16) for the same speed up, and to keep - // the tables small enough to fit in most inner caches. - ushort m_toLinear[(255 * 16) + 1]; // [0-4080] -> [0-65280] - ushort m_fromLinear[(255 * 16) + 1]; // [0-4080] -> [0-65280] - }; QT_END_NAMESPACE -#endif // QCOLORPROFILE_P_H +#endif // QCOLORTRCLUT_P_H diff --git a/src/gui/painting/qdrawhelper.cpp b/src/gui/painting/qdrawhelper.cpp index 2dd18f6dfc1..1ed51d26a2b 100644 --- a/src/gui/painting/qdrawhelper.cpp +++ b/src/gui/painting/qdrawhelper.cpp @@ -43,7 +43,7 @@ #include #include #include -#include +#include #include #include #include @@ -5523,7 +5523,7 @@ inline static void qt_bitmapblit_quint16(QRasterBuffer *rasterBuffer, map, mapWidth, mapHeight, mapStride); } -static inline void alphamapblend_generic(int coverage, QRgba64 *dest, int x, const QRgba64 &srcLinear, const QRgba64 &src, const QColorProfile *colorProfile) +static inline void alphamapblend_generic(int coverage, QRgba64 *dest, int x, const QRgba64 &srcLinear, const QRgba64 &src, const QColorTrcLut *colorProfile) { if (coverage == 0) { // nothing @@ -5558,7 +5558,7 @@ static void qt_alphamapblit_generic(QRasterBuffer *rasterBuffer, if (color.isTransparent()) return; - const QColorProfile *colorProfile = nullptr; + const QColorTrcLut *colorProfile = nullptr; if (useGammaCorrection) colorProfile = QGuiApplicationPrivate::instance()->colorProfileForA8Text(); @@ -5684,7 +5684,7 @@ void qt_alphamapblit_quint16(QRasterBuffer *rasterBuffer, } } -static inline void rgbBlendPixel(quint32 *dst, int coverage, QRgba64 slinear, const QColorProfile *colorProfile) +static inline void rgbBlendPixel(quint32 *dst, int coverage, QRgba64 slinear, const QColorTrcLut *colorProfile) { // Do a gammacorrected RGB alphablend... const QRgba64 dlinear = colorProfile ? colorProfile->toLinear64(*dst) : QRgba64::fromArgb32(*dst); @@ -5694,7 +5694,7 @@ static inline void rgbBlendPixel(quint32 *dst, int coverage, QRgba64 slinear, co *dst = colorProfile ? colorProfile->fromLinear64(blend) : toArgb32(blend); } -static inline void grayBlendPixel(quint32 *dst, int coverage, QRgba64 srcLinear, const QColorProfile *colorProfile) +static inline void grayBlendPixel(quint32 *dst, int coverage, QRgba64 srcLinear, const QColorTrcLut *colorProfile) { // Do a gammacorrected gray alphablend... const QRgba64 dstLinear = colorProfile ? colorProfile->toLinear64(*dst) : QRgba64::fromArgb32(*dst); @@ -5704,7 +5704,7 @@ static inline void grayBlendPixel(quint32 *dst, int coverage, QRgba64 srcLinear, *dst = colorProfile ? colorProfile->fromLinear64(blend) : toArgb32(blend); } -static inline void alphamapblend_argb32(quint32 *dst, int coverage, QRgba64 srcLinear, quint32 src, const QColorProfile *colorProfile) +static inline void alphamapblend_argb32(quint32 *dst, int coverage, QRgba64 srcLinear, quint32 src, const QColorTrcLut *colorProfile) { if (coverage == 0) { // nothing @@ -5734,7 +5734,7 @@ static void qt_alphamapblit_argb32(QRasterBuffer *rasterBuffer, if (color.isTransparent()) return; - const QColorProfile *colorProfile = nullptr; + const QColorTrcLut *colorProfile = nullptr; if (useGammaCorrection) colorProfile = QGuiApplicationPrivate::instance()->colorProfileForA8Text(); @@ -5830,7 +5830,7 @@ static inline QRgb rgbBlend(QRgb d, QRgb s, uint rgbAlpha) #endif } -static inline void alphargbblend_generic(uint coverage, QRgba64 *dest, int x, const QRgba64 &srcLinear, const QRgba64 &src, const QColorProfile *colorProfile) +static inline void alphargbblend_generic(uint coverage, QRgba64 *dest, int x, const QRgba64 &srcLinear, const QRgba64 &src, const QColorTrcLut *colorProfile) { if (coverage == 0xff000000) { // nothing @@ -5852,7 +5852,7 @@ static inline void alphargbblend_generic(uint coverage, QRgba64 *dest, int x, co } } -static inline void alphargbblend_argb32(quint32 *dst, uint coverage, const QRgba64 &srcLinear, quint32 src, const QColorProfile *colorProfile) +static inline void alphargbblend_argb32(quint32 *dst, uint coverage, const QRgba64 &srcLinear, quint32 src, const QColorTrcLut *colorProfile) { if (coverage == 0xff000000) { // nothing @@ -5877,7 +5877,7 @@ static void qt_alphargbblit_generic(QRasterBuffer *rasterBuffer, if (color.isTransparent()) return; - const QColorProfile *colorProfile = nullptr; + const QColorTrcLut *colorProfile = nullptr; if (useGammaCorrection) colorProfile = QGuiApplicationPrivate::instance()->colorProfileForA32Text(); @@ -5954,7 +5954,7 @@ static void qt_alphargbblit_argb32(QRasterBuffer *rasterBuffer, const quint32 c = color.toArgb32(); - const QColorProfile *colorProfile = nullptr; + const QColorTrcLut *colorProfile = nullptr; if (useGammaCorrection) colorProfile = QGuiApplicationPrivate::instance()->colorProfileForA32Text(); diff --git a/src/gui/painting/qicc.cpp b/src/gui/painting/qicc.cpp new file mode 100644 index 00000000000..d88b0057827 --- /dev/null +++ b/src/gui/painting/qicc.cpp @@ -0,0 +1,669 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qicc_p.h" + +#include +#include +#include +#include +#include + +#include "qcolorspace_p.h" +#include "qcolortrc_p.h" + +QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcIcc, "qt.gui.icc") + +struct ICCProfileHeader +{ + quint32_be profileSize; + + quint32_be preferredCmmType; + + quint32_be profileVersion; + quint32_be profileClass; + quint32_be inputColorSpace; + quint32_be pcs; + quint32_be datetime[3]; + quint32_be signature; + quint32_be platformSignature; + quint32_be flags; + quint32_be deviceManufacturer; + quint32_be deviceModel; + quint32_be deviceAttributes[2]; + + quint32_be renderingIntent; + qint32_be illuminantXyz[3]; + + quint32_be creatorSignature; + quint32_be profileId[4]; + + quint32_be reserved[7]; + +// Technically after the header, but easier to include here: + quint32_be tagCount; +}; + +constexpr quint32 IccTag(uchar a, uchar b, uchar c, uchar d) +{ + return (a << 24) | (b << 16) | (c << 8) | d; +} + +enum class ProfileClass : quint32 { + Input = IccTag('s', 'c', 'r', 'n'), + Display = IccTag('m', 'n', 't', 'r'), + // Not supported: + Output = IccTag('p', 'r', 't', 'r'), + ColorSpace = IccTag('s', 'p', 'a', 'c'), +}; + +enum class Tag : quint32 { + acsp = IccTag('a', 'c', 's', 'p'), + RGB_ = IccTag('R', 'G', 'B', ' '), + XYZ_ = IccTag('X', 'Y', 'Z', ' '), + rXYZ = IccTag('r', 'X', 'Y', 'Z'), + gXYZ = IccTag('g', 'X', 'Y', 'Z'), + bXYZ = IccTag('b', 'X', 'Y', 'Z'), + rTRC = IccTag('r', 'T', 'R', 'C'), + gTRC = IccTag('g', 'T', 'R', 'C'), + bTRC = IccTag('b', 'T', 'R', 'C'), + A2B0 = IccTag('A', '2', 'B', '0'), + A2B1 = IccTag('A', '2', 'B', '1'), + B2A0 = IccTag('B', '2', 'A', '0'), + B2A1 = IccTag('B', '2', 'A', '1'), + desc = IccTag('d', 'e', 's', 'c'), + text = IccTag('t', 'e', 'x', 't'), + cprt = IccTag('c', 'p', 'r', 't'), + curv = IccTag('c', 'u', 'r', 'v'), + para = IccTag('p', 'a', 'r', 'a'), + wtpt = IccTag('w', 't', 'p', 't'), + bkpt = IccTag('b', 'k', 'p', 't'), + mft1 = IccTag('m', 'f', 't', '1'), + mft2 = IccTag('m', 'f', 't', '2'), + mAB_ = IccTag('m', 'A', 'B', ' '), + mBA_ = IccTag('m', 'B', 'A', ' '), + chad = IccTag('c', 'h', 'a', 'd'), + sf32 = IccTag('s', 'f', '3', '2'), + + // Apple extensions for ICCv2: + aarg = IccTag('a', 'a', 'r', 'g'), + aagg = IccTag('a', 'a', 'g', 'g'), + aabg = IccTag('a', 'a', 'b', 'g'), +}; + +inline uint qHash(const Tag &key, uint seed = 0) +{ + return qHash(quint32(key), seed); +} + +namespace QIcc { + +struct TagTableEntry +{ + quint32_be signature; + quint32_be offset; + quint32_be size; +}; + +struct GenericTagData { + quint32_be type; + quint32_be null; +}; + +struct XYZTagData : GenericTagData { + qint32_be fixedX; + qint32_be fixedY; + qint32_be fixedZ; +}; + +struct CurvTagData : GenericTagData { + quint32_be valueCount; + quint16_be value[1]; +}; + +struct ParaTagData : GenericTagData { + quint16_be curveType; + quint16_be null2; + quint32_be parameter[1]; +}; + +// For both mAB and mBA +struct mABTagData : GenericTagData { + quint8 inputChannels; + quint8 outputChannels; + quint8 padding[2]; + quint32_be bCurvesOffset; + quint32_be matrixOffset; + quint32_be mCurvesOffset; + quint32_be clutOffset; + quint32_be aCurvesOffset; +}; + +struct Sf32TagData : GenericTagData { + quint32_be value[1]; +}; + +static int toFixedS1516(float x) +{ + return int(x * 65536.0f + 0.5f); +} + +static float fromFixedS1516(int x) +{ + return x * (1.0f / 65536.0f); +} + +QColorVector fromXyzData(const XYZTagData *xyz) +{ + const float x = fromFixedS1516(xyz->fixedX); + const float y = fromFixedS1516(xyz->fixedY); + const float z = fromFixedS1516(xyz->fixedZ); + qCDebug(lcIcc) << "XYZ_ " << x << y << z; + + return QColorVector(x, y, z); +} + +static bool isValidIccProfile(const ICCProfileHeader &header) +{ + if (header.signature != uint(Tag::acsp)) { + qCWarning(lcIcc, "Failed ICC signature test"); + return false; + } + if (header.profileSize < (sizeof(ICCProfileHeader) + header.tagCount * sizeof(TagTableEntry))) { + qCWarning(lcIcc, "Failed basic size sanity"); + return false; + } + + if (header.profileClass != uint(ProfileClass::Input) + && header.profileClass != uint(ProfileClass::Display)) { + qCWarning(lcIcc, "Unsupported ICC profile class %x", quint32(header.profileClass)); + return false; + } + if (header.inputColorSpace != 0x52474220 /* 'RGB '*/) { + qCWarning(lcIcc, "Unsupported ICC input color space %x", quint32(header.inputColorSpace)); + return false; + } + if (header.pcs != 0x58595a20 /* 'XYZ '*/) { + // ### support PCSLAB + qCWarning(lcIcc, "Unsupported ICC profile connection space %x", quint32(header.pcs)); + return false; + } + + QColorVector illuminant; + illuminant.x = fromFixedS1516(header.illuminantXyz[0]); + illuminant.y = fromFixedS1516(header.illuminantXyz[1]); + illuminant.z = fromFixedS1516(header.illuminantXyz[2]); + if (illuminant != QColorVector::D50()) { + qCWarning(lcIcc, "Invalid ICC illuminant"); + return false; + } + + return true; +} + +static int writeColorTrc(QDataStream &stream, const QColorTrc &trc) +{ + if (trc.isLinear()) { + stream << uint(Tag::curv) << uint(0); + stream << uint(0); + return 12; + } + + if (trc.m_type == QColorTrc::Type::Function) { + const QColorTransferFunction &fun = trc.m_fun; + stream << uint(Tag::para) << uint(0); + if (fun.isGamma()) { + stream << ushort(0) << ushort(0); + stream << toFixedS1516(fun.m_g); + return 12 + 4; + } + bool type3 = qFuzzyIsNull(fun.m_e) && qFuzzyIsNull(fun.m_f); + stream << ushort(type3 ? 3 : 4) << ushort(0); + stream << toFixedS1516(fun.m_g); + stream << toFixedS1516(fun.m_a); + stream << toFixedS1516(fun.m_b); + stream << toFixedS1516(fun.m_c); + stream << toFixedS1516(fun.m_d); + if (type3) + return 12 + 5 * 4; + stream << toFixedS1516(fun.m_e); + stream << toFixedS1516(fun.m_f); + return 12 + 7 * 4; + } + + Q_ASSERT(trc.m_type == QColorTrc::Type::Table); + stream << uint(Tag::curv) << uint(0); + stream << uint(trc.m_table.m_tableSize); + if (!trc.m_table.m_table16.isEmpty()) { + for (uint i = 0; i < trc.m_table.m_tableSize; ++i) { + stream << ushort(trc.m_table.m_table16[i]); + } + } else { + for (uint i = 0; i < trc.m_table.m_tableSize; ++i) { + stream << ushort(trc.m_table.m_table8[i] * 257U); + } + } + return 12 + 2 * trc.m_table.m_tableSize; +} + +QByteArray toIccProfile(const QColorSpace &space) +{ + if (!space.isValid()) + return QByteArray(); + + const QColorSpacePrivate *spaceDPtr = space.d_func(); + + constexpr int tagCount = 9; + constexpr uint profileDataOffset = 128 + 4 + 12 * tagCount; + constexpr uint variableTagTableOffsets = 128 + 4 + 12 * 5; + uint currentOffset = 0; + uint rTrcOffset, gTrcOffset, bTrcOffset; + uint rTrcSize, gTrcSize, bTrcSize; + uint descOffset, descSize; + + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + QDataStream stream(&buffer); + + // Profile header: + stream << uint(0); // Size, we will update this later + stream << uint(0); + stream << uint(0x02400000); // Version 2.4 (note we use 'para' from version 4) + stream << uint(ProfileClass::Display); + stream << uint(Tag::RGB_); + stream << uint(Tag::XYZ_); + stream << uint(0) << uint(0) << uint(0); + stream << uint(Tag::acsp); + stream << uint(0) << uint(0) << uint(0); + stream << uint(0) << uint(0) << uint(0); + stream << uint(1); // Rendering intent + stream << uint(0x0000f6d6); // D50 X + stream << uint(0x00010000); // D50 Y + stream << uint(0x0000d32d); // D50 Z + stream << IccTag('Q','t', QT_VERSION_MAJOR, QT_VERSION_MINOR); + stream << uint(0) << uint(0) << uint(0) << uint(0); + stream << uint(0) << uint(0) << uint(0) << uint(0) << uint(0) << uint(0) << uint(0); + + // Tag table: + stream << uint(tagCount); + stream << uint(Tag::rXYZ) << uint(profileDataOffset + 00) << uint(20); + stream << uint(Tag::gXYZ) << uint(profileDataOffset + 20) << uint(20); + stream << uint(Tag::bXYZ) << uint(profileDataOffset + 40) << uint(20); + stream << uint(Tag::wtpt) << uint(profileDataOffset + 60) << uint(20); + stream << uint(Tag::cprt) << uint(profileDataOffset + 80) << uint(12); + // From here the offset and size will be updated later: + stream << uint(Tag::rTRC) << uint(0) << uint(0); + stream << uint(Tag::gTRC) << uint(0) << uint(0); + stream << uint(Tag::bTRC) << uint(0) << uint(0); + stream << uint(Tag::desc) << uint(0) << uint(0); + // TODO: consider adding 'chad' tag (required in ICC >=4 when we have non-D50 whitepoint) + currentOffset = profileDataOffset; + + // Tag data: + stream << uint(Tag::XYZ_) << uint(0); + stream << toFixedS1516(spaceDPtr->toXyz.r.x); + stream << toFixedS1516(spaceDPtr->toXyz.r.y); + stream << toFixedS1516(spaceDPtr->toXyz.r.z); + stream << uint(Tag::XYZ_) << uint(0); + stream << toFixedS1516(spaceDPtr->toXyz.g.x); + stream << toFixedS1516(spaceDPtr->toXyz.g.y); + stream << toFixedS1516(spaceDPtr->toXyz.g.z); + stream << uint(Tag::XYZ_) << uint(0); + stream << toFixedS1516(spaceDPtr->toXyz.b.x); + stream << toFixedS1516(spaceDPtr->toXyz.b.y); + stream << toFixedS1516(spaceDPtr->toXyz.b.z); + stream << uint(Tag::XYZ_) << uint(0); + stream << toFixedS1516(spaceDPtr->whitePoint.x); + stream << toFixedS1516(spaceDPtr->whitePoint.y); + stream << toFixedS1516(spaceDPtr->whitePoint.z); + stream << uint(Tag::text) << uint(0); + stream << uint(IccTag('N', '/', 'A', '\0')); + currentOffset += 92; + + // From now on the data is variable sized: + rTrcOffset = currentOffset; + rTrcSize = writeColorTrc(stream, spaceDPtr->trc[0]); + currentOffset += rTrcSize; + if (spaceDPtr->trc[0] == spaceDPtr->trc[1]) { + gTrcOffset = rTrcOffset; + gTrcSize = rTrcSize; + } else { + gTrcOffset = currentOffset; + gTrcSize = writeColorTrc(stream, spaceDPtr->trc[1]); + currentOffset += gTrcSize; + } + if (spaceDPtr->trc[0] == spaceDPtr->trc[2]) { + bTrcOffset = rTrcOffset; + bTrcSize = rTrcSize; + } else { + bTrcOffset = currentOffset; + bTrcSize = writeColorTrc(stream, spaceDPtr->trc[2]); + currentOffset += bTrcSize; + } + + descOffset = currentOffset; + QByteArray description = spaceDPtr->description.toUtf8(); + stream << uint(Tag::desc) << uint(0); + stream << uint(description.size() + 1); + stream.writeRawData(description.constData(), description.size() + 1); + stream << uint(0) << uint(0); + stream << ushort(0) << uchar(0); + QByteArray macdesc(67, '\0'); + stream.writeRawData(macdesc.constData(), 67); + descSize = 90 + description.size() + 1; + currentOffset += descSize; + + buffer.close(); + QByteArray iccProfile = buffer.buffer(); + // Now write final size + *(quint32_be *)iccProfile.data() = iccProfile.size(); + // And the final indices and sizes of variable size tags: + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = rTrcOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = rTrcSize; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 4) = gTrcOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 8) = gTrcSize; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 2 * 12 + 4) = bTrcOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 2 * 12 + 8) = bTrcSize; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 3 * 12 + 4) = descOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 3 * 12 + 8) = descSize; + +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) + const ICCProfileHeader *iccHeader = (const ICCProfileHeader *)iccProfile.constData(); + Q_ASSERT(qsizetype(iccHeader->profileSize) == qsizetype(iccProfile.size())); + Q_ASSERT(isValidIccProfile(*iccHeader)); +#endif + + return iccProfile; +} + +bool parseTRC(const GenericTagData *trcData, QColorTrc &gamma) +{ + if (trcData->type == quint32(Tag::curv)) { + const CurvTagData *curv = reinterpret_cast(trcData); + qCDebug(lcIcc) << "curv" << uint(curv->valueCount); + if (curv->valueCount == 0) { + gamma.m_type = QColorTrc::Type::Function; + gamma.m_fun = QColorTransferFunction(); // Linear + } else if (curv->valueCount == 1) { + float g = curv->value[0] * (1.0f / 256.0f); + qCDebug(lcIcc) << g; + gamma.m_type = QColorTrc::Type::Function; + gamma.m_fun = QColorTransferFunction::fromGamma(g); + } else { + QVector tabl; + tabl.resize(curv->valueCount); + for (uint i = 0; i < curv->valueCount; ++i) + tabl[i] = curv->value[i]; + QColorTransferTable table = QColorTransferTable(curv->valueCount, std::move(tabl)); + QColorTransferFunction curve; + if (!table.asColorTransferFunction(&curve)) { + gamma.m_type = QColorTrc::Type::Table; + gamma.m_table = table; + } else { + qCDebug(lcIcc) << "Detected curv table as function"; + gamma.m_type = QColorTrc::Type::Function; + gamma.m_fun = curve; + } + } + return true; + } + if (trcData->type == quint32(Tag::para)) { + const ParaTagData *para = reinterpret_cast(trcData); + qCDebug(lcIcc) << "para" << uint(para->curveType); + switch (para->curveType) { + case 0: { + float g = fromFixedS1516(para->parameter[0]); + qCDebug(lcIcc) << g; + gamma.m_type = QColorTrc::Type::Function; + gamma.m_fun = QColorTransferFunction::fromGamma(g); + break; + } + case 1: { + float g = fromFixedS1516(para->parameter[0]); + float a = fromFixedS1516(para->parameter[1]); + float b = fromFixedS1516(para->parameter[2]); + float d = -b / a; + qCDebug(lcIcc) << g << a << b; + gamma.m_type = QColorTrc::Type::Function; + gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, 0.0f, 0.0f, g); + break; + } + case 2: { + float g = fromFixedS1516(para->parameter[0]); + float a = fromFixedS1516(para->parameter[1]); + float b = fromFixedS1516(para->parameter[2]); + float c = fromFixedS1516(para->parameter[3]); + float d = -b / a; + qCDebug(lcIcc) << g << a << b << c; + gamma.m_type = QColorTrc::Type::Function; + gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, c, c, g); + break; + } + case 3: { + float g = fromFixedS1516(para->parameter[0]); + float a = fromFixedS1516(para->parameter[1]); + float b = fromFixedS1516(para->parameter[2]); + float c = fromFixedS1516(para->parameter[3]); + float d = fromFixedS1516(para->parameter[4]); + qCDebug(lcIcc) << g << a << b << c << d; + gamma.m_type = QColorTrc::Type::Function; + gamma.m_fun = QColorTransferFunction(a, b, c, d, 0.0f, 0.0f, g); + break; + } + case 4: { + float g = fromFixedS1516(para->parameter[0]); + float a = fromFixedS1516(para->parameter[1]); + float b = fromFixedS1516(para->parameter[2]); + float c = fromFixedS1516(para->parameter[3]); + float d = fromFixedS1516(para->parameter[4]); + float e = fromFixedS1516(para->parameter[5]); + float f = fromFixedS1516(para->parameter[6]); + qCDebug(lcIcc) << g << a << b << c << d << e << f; + gamma.m_type = QColorTrc::Type::Function; + gamma.m_fun = QColorTransferFunction(a, b, c, d, e, f, g); + break; + } + default: + qCWarning(lcIcc) << "Unknown para type" << uint(para->curveType); + return false; + } + return true; + } + qCWarning(lcIcc) << "Invalid TRC data type"; + return false; +} + +bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace) +{ + if (data.size() < qsizetype(sizeof(ICCProfileHeader))) { + qCWarning(lcIcc) << "fromIccProfile: failed size sanity 1"; + return false; + } + const ICCProfileHeader *header = (const ICCProfileHeader *)data.constData(); + if (!isValidIccProfile(*header)) { + qCWarning(lcIcc) << "fromIccProfile: failed general sanity check"; + return false; + } + if (qsizetype(header->profileSize) > data.size()) { + qCWarning(lcIcc) << "fromIccProfile: failed size sanity 2"; + return false; + } + + // Read tag index + const TagTableEntry *tagTable = (const TagTableEntry *)(data.constData() + sizeof(ICCProfileHeader)); + const qsizetype offsetToData = sizeof(ICCProfileHeader) + header->tagCount * sizeof(TagTableEntry); + + QHash tagIndex; + for (uint i = 0; i < header->tagCount; ++i) { + // Sanity check tag sizes and offsets: + if (qsizetype(tagTable[i].offset) < offsetToData) { + qCWarning(lcIcc) << "fromIccProfile: failed tag offset sanity 1"; + return false; + } + // Checked separately from (+ size) to handle overflow. + if (tagTable[i].offset > header->profileSize) { + qCWarning(lcIcc) << "fromIccProfile: failed tag offset sanity 2"; + return false; + } + if ((tagTable[i].offset + tagTable[i].size) > header->profileSize) { + qCWarning(lcIcc) << "fromIccProfile: failed tag offset + size sanity"; + return false; + } +// printf("'%4s' %d %d\n", (const char *)&tagTable[i].signature, +// quint32(tagTable[i].offset), +// quint32(tagTable[i].size)); + tagIndex.insert(Tag(quint32(tagTable[i].signature)), tagTable[i].offset); + } + // Check the profile is three-component matrix based (what we currently support): + if (!tagIndex.contains(Tag::rXYZ) || !tagIndex.contains(Tag::gXYZ) || !tagIndex.contains(Tag::bXYZ) || + !tagIndex.contains(Tag::rTRC) || !tagIndex.contains(Tag::gTRC) || !tagIndex.contains(Tag::bTRC) || + !tagIndex.contains(Tag::wtpt)) { + qCWarning(lcIcc) << "fromIccProfile: Unsupported ICC profile - not three component matrix based"; + return false; + } + + // Parse XYZ tags + const XYZTagData *rXyz = (const XYZTagData *)(data.constData() + tagIndex[Tag::rXYZ]); + const XYZTagData *gXyz = (const XYZTagData *)(data.constData() + tagIndex[Tag::gXYZ]); + const XYZTagData *bXyz = (const XYZTagData *)(data.constData() + tagIndex[Tag::bXYZ]); + const XYZTagData *wXyz = (const XYZTagData *)(data.constData() + tagIndex[Tag::wtpt]); + if (rXyz->type != quint32(Tag::XYZ_) || gXyz->type != quint32(Tag::XYZ_) || + wXyz->type != quint32(Tag::XYZ_) || wXyz->type != quint32(Tag::XYZ_)) { + qCWarning(lcIcc) << "fromIccProfile: Bad XYZ data type"; + return false; + } + QColorSpacePrivate *colorspaceDPtr = colorSpace->d_func(); + + colorspaceDPtr->toXyz.r = fromXyzData(rXyz); + colorspaceDPtr->toXyz.g = fromXyzData(gXyz); + colorspaceDPtr->toXyz.b = fromXyzData(bXyz); + QColorVector whitePoint = fromXyzData(wXyz); + colorspaceDPtr->whitePoint = whitePoint; + + colorspaceDPtr->gamut = QColorSpace::Gamut::Custom; + if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromSRgb()) { + qCDebug(lcIcc) << "fromIccProfile: sRGB gamut detected"; + colorspaceDPtr->gamut = QColorSpace::Gamut::SRgb; + } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromAdobeRgb()) { + qCDebug(lcIcc) << "fromIccProfile: Adobe RGB gamut detected"; + colorspaceDPtr->gamut = QColorSpace::Gamut::AdobeRgb; + } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromDciP3D65()) { + qCDebug(lcIcc) << "fromIccProfile: DCI-P3 D65 gamut detected"; + colorspaceDPtr->gamut = QColorSpace::Gamut::DciP3D65; + } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromBt2020()) { + qCDebug(lcIcc) << "fromIccProfile: BT.2020 gamut detected"; + colorspaceDPtr->gamut = QColorSpace::Gamut::Bt2020; + } + if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromProPhotoRgb()) { + qCDebug(lcIcc) << "fromIccProfile: ProPhoto RGB gamut detected"; + colorspaceDPtr->gamut = QColorSpace::Gamut::ProPhotoRgb; + } + // Reset the matrix to our canonical values: + if (colorspaceDPtr->gamut != QColorSpace::Gamut::Custom) + colorspaceDPtr->setToXyzMatrix(); + + // Parse TRC tags + const GenericTagData *rTrc; + const GenericTagData *gTrc; + const GenericTagData *bTrc; + if (tagIndex.contains(Tag::aarg) && tagIndex.contains(Tag::aagg) && tagIndex.contains(Tag::aabg)) { + // Apple extension for parametric version of TRCs in ICCv2: + rTrc = (const GenericTagData *)(data.constData() + tagIndex[Tag::aarg]); + gTrc = (const GenericTagData *)(data.constData() + tagIndex[Tag::aagg]); + bTrc = (const GenericTagData *)(data.constData() + tagIndex[Tag::aabg]); + } else { + rTrc = (const GenericTagData *)(data.constData() + tagIndex[Tag::rTRC]); + gTrc = (const GenericTagData *)(data.constData() + tagIndex[Tag::gTRC]); + bTrc = (const GenericTagData *)(data.constData() + tagIndex[Tag::bTRC]); + } + + QColorTrc rCurve; + QColorTrc gCurve; + QColorTrc bCurve; + if (!parseTRC(rTrc, rCurve)) + return false; + if (!parseTRC(gTrc, gCurve)) + return false; + if (!parseTRC(bTrc, bCurve)) + return false; + if (rCurve == gCurve && gCurve == bCurve && rCurve.m_type == QColorTrc::Type::Function) { + if (rCurve.m_fun.isLinear()) { + qCDebug(lcIcc) << "fromIccProfile: Linear gamma detected"; + colorspaceDPtr->trc[0] = QColorTransferFunction(); + colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Linear; + colorspaceDPtr->gamma = 1.0f; + } else if (rCurve.m_fun.isGamma()) { + qCDebug(lcIcc) << "fromIccProfile: Simple gamma detected"; + colorspaceDPtr->trc[0] = QColorTransferFunction::fromGamma(rCurve.m_fun.m_g); + colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Gamma; + colorspaceDPtr->gamma = rCurve.m_fun.m_g; + } else if (rCurve.m_fun.isSRgb()) { + qCDebug(lcIcc) << "fromIccProfile: sRGB gamma detected"; + colorspaceDPtr->trc[0] = QColorTransferFunction::fromSRgb(); + colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::SRgb; + } else { + colorspaceDPtr->trc[0] = rCurve; + colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom; + } + + colorspaceDPtr->trc[1] = colorspaceDPtr->trc[0]; + colorspaceDPtr->trc[2] = colorspaceDPtr->trc[0]; + } else { + colorspaceDPtr->trc[0] = rCurve; + colorspaceDPtr->trc[1] = gCurve; + colorspaceDPtr->trc[2] = bCurve; + colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom; + } + + // FIXME: try to parse the description.. + + if (!colorspaceDPtr->identifyColorSpace()) + colorspaceDPtr->id = QColorSpace::Unknown; + else + qCDebug(lcIcc) << "fromIccProfile: Named colorspace detected: " << colorSpace->colorSpaceId(); + + colorspaceDPtr->iccProfile = data; + + return true; +} + +} // namespace QIcc + +QT_END_NAMESPACE diff --git a/src/gui/painting/qicc_p.h b/src/gui/painting/qicc_p.h new file mode 100644 index 00000000000..c3220391f44 --- /dev/null +++ b/src/gui/painting/qicc_p.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QICC_P_H +#define QICC_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +class QColorSpace; + +namespace QIcc { + +Q_GUI_EXPORT bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace); +Q_GUI_EXPORT QByteArray toIccProfile(const QColorSpace &space); + +} + +QT_END_NAMESPACE + +#endif // QICC_P_H diff --git a/src/gui/painting/qpainter_p.h b/src/gui/painting/qpainter_p.h index 930180e9faf..9857a590701 100644 --- a/src/gui/painting/qpainter_p.h +++ b/src/gui/painting/qpainter_p.h @@ -54,6 +54,8 @@ #include #include #include "QtGui/qbrush.h" +#include "QtGui/qcolorspace.h" +#include "QtGui/qcolortransform.h" #include "QtGui/qfont.h" #include "QtGui/qpen.h" #include "QtGui/qregion.h" diff --git a/src/plugins/imageformats/jpeg/qjpeghandler.cpp b/src/plugins/imageformats/jpeg/qjpeghandler.cpp index 54fe857908a..9d5ccc8a3d8 100644 --- a/src/plugins/imageformats/jpeg/qjpeghandler.cpp +++ b/src/plugins/imageformats/jpeg/qjpeghandler.cpp @@ -40,10 +40,14 @@ #include "qjpeghandler_p.h" #include +#include +#include +#include #include #include #include #include +#include #include #include // for qt_getImageText @@ -725,6 +729,7 @@ public: QRect clipRect; QString description; QStringList readTexts; + QByteArray iccProfile; struct jpeg_decompress_struct info; struct my_jpeg_source_mgr * iod_src; @@ -887,6 +892,7 @@ bool QJpegHandlerPrivate::readJpegHeader(QIODevice *device) if (!setjmp(err.setjmp_buffer)) { jpeg_save_markers(&info, JPEG_COM, 0xFFFF); jpeg_save_markers(&info, JPEG_APP0 + 1, 0xFFFF); // Exif uses APP1 marker + jpeg_save_markers(&info, JPEG_APP0 + 2, 0xFFFF); // ICC uses APP2 marker (void) jpeg_read_header(&info, TRUE); @@ -919,6 +925,10 @@ bool QJpegHandlerPrivate::readJpegHeader(QIODevice *device) readTexts.append(value); } 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); + } } } @@ -954,6 +964,9 @@ bool QJpegHandlerPrivate::read(QImage *image) 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)); + state = ReadingEnd; return true; } @@ -962,7 +975,6 @@ bool QJpegHandlerPrivate::read(QImage *image) } return false; - } Q_GUI_EXPORT void QT_FASTCALL qt_convert_rgb888_to_rgb32_neon(quint32 *dst, const uchar *src, int len); diff --git a/tests/auto/gui/painting/painting.pro b/tests/auto/gui/painting/painting.pro index 831fffab30f..26e84c1b158 100644 --- a/tests/auto/gui/painting/painting.pro +++ b/tests/auto/gui/painting/painting.pro @@ -3,6 +3,7 @@ SUBDIRS=\ qpainterpath \ qpainterpathstroker \ qcolor \ + qcolorspace \ qbrush \ qregion \ qpagelayout \ diff --git a/tests/auto/gui/painting/qcolor/tst_qcolor.cpp b/tests/auto/gui/painting/qcolor/tst_qcolor.cpp index 72bad03a6ae..46a2d73ecea 100644 --- a/tests/auto/gui/painting/qcolor/tst_qcolor.cpp +++ b/tests/auto/gui/painting/qcolor/tst_qcolor.cpp @@ -33,7 +33,7 @@ #include #include -#include +#include #include #include @@ -1632,14 +1632,13 @@ void tst_QColor::qcolorprofile_data() QTest::newRow("gamma=1.7") << qreal(1.7) << 2; QTest::newRow("gamma=2.0") << qreal(2.0) << 8; QTest::newRow("gamma=2.31") << qreal(2.31) << 33; - QTest::newRow("SRgb") << qreal(0.0) << 7; } void tst_QColor::qcolorprofile() { QFETCH(qreal, gammaC); QFETCH(int, tolerance); - QColorProfile *cp = (gammaC == 0) ? QColorProfile::fromSRgb(): QColorProfile::fromGamma(gammaC); + QColorTrcLut *cp = QColorTrcLut::fromGamma(gammaC); // Test we are accurate for most values after converting through gamma-correction. int error = 0; diff --git a/tests/auto/gui/painting/qcolorspace/qcolorspace.pro b/tests/auto/gui/painting/qcolorspace/qcolorspace.pro new file mode 100644 index 00000000000..14bd699bf7a --- /dev/null +++ b/tests/auto/gui/painting/qcolorspace/qcolorspace.pro @@ -0,0 +1,9 @@ +CONFIG += testcase +QT += testlib gui-private core-private + +TARGET = tst_qcolorspace +SOURCES += tst_qcolorspace.cpp + +RESOURCES += $$files(resources/*) + +TESTDATA += resources/* diff --git a/tests/auto/gui/painting/qcolorspace/resources/ProPhoto.jpg b/tests/auto/gui/painting/qcolorspace/resources/ProPhoto.jpg new file mode 100644 index 0000000000000000000000000000000000000000..481d35ca8ec02d94537ab16c585ccee339d8d249 GIT binary patch literal 30900 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 literal 0 HcmV?d00001 diff --git a/tests/auto/gui/painting/qcolorspace/resources/sRGB2014.icc b/tests/auto/gui/painting/qcolorspace/resources/sRGB2014.icc new file mode 100644 index 0000000000000000000000000000000000000000..49afbfef10f22a1832590b68369d2f248ea553b9 GIT binary patch literal 3024 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 literal 0 HcmV?d00001 diff --git a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp new file mode 100644 index 00000000000..9bd4b75443d --- /dev/null +++ b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp @@ -0,0 +1,238 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include + +#include +#include +#include + +Q_DECLARE_METATYPE(QColorSpace::ColorSpaceId) +Q_DECLARE_METATYPE(QColorSpace::Gamut) +Q_DECLARE_METATYPE(QColorSpace::TransferFunction) + +class tst_QColorSpace : public QObject +{ + Q_OBJECT + +public: + tst_QColorSpace(); + +private slots: + void namedColorSpaces_data(); + void namedColorSpaces(); + + void toIccProfile_data(); + void toIccProfile(); + + void fromIccProfile(); + + void imageConversion_data(); + void imageConversion(); + + void loadImage(); +}; + +tst_QColorSpace::tst_QColorSpace() +{ } + + +void tst_QColorSpace::namedColorSpaces_data() +{ + QTest::addColumn("colorSpaceId"); + QTest::addColumn("gamutId"); + QTest::addColumn("transferFunctionId"); + + QTest::newRow("sRGB") << QColorSpace::SRgb + << QColorSpace::Gamut::SRgb + << QColorSpace::TransferFunction::SRgb; + QTest::newRow("sRGB Linear") << QColorSpace::SRgbLinear + << QColorSpace::Gamut::SRgb + << QColorSpace::TransferFunction::Linear; + QTest::newRow("Adobe RGB") << QColorSpace::AdobeRgb + << QColorSpace::Gamut::AdobeRgb + << QColorSpace::TransferFunction::Gamma; + QTest::newRow("Display-P3") << QColorSpace::DisplayP3 + << QColorSpace::Gamut::DciP3D65 + << QColorSpace::TransferFunction::SRgb; + QTest::newRow("ProPhoto RGB") << QColorSpace::ProPhotoRgb + << QColorSpace::Gamut::ProPhotoRgb + << QColorSpace::TransferFunction::ProPhotoRgb; + QTest::newRow("BT.2020") << QColorSpace::Bt2020 + << QColorSpace::Gamut::Bt2020 + << QColorSpace::TransferFunction::Bt2020; +} + +void tst_QColorSpace::namedColorSpaces() +{ + QFETCH(QColorSpace::ColorSpaceId, colorSpaceId); + QFETCH(QColorSpace::Gamut, gamutId); + QFETCH(QColorSpace::TransferFunction, transferFunctionId); + + QColorSpace colorSpace = colorSpaceId; + + QVERIFY(colorSpace.isValid()); + + QCOMPARE(colorSpace.colorSpaceId(), colorSpaceId); + QCOMPARE(colorSpace.gamut(), gamutId); + QCOMPARE(colorSpace.transferFunction(), transferFunctionId); +} + + +void tst_QColorSpace::toIccProfile_data() +{ + namedColorSpaces_data(); +} + +void tst_QColorSpace::toIccProfile() +{ + QFETCH(QColorSpace::ColorSpaceId, colorSpaceId); + QFETCH(QColorSpace::Gamut, gamutId); + QFETCH(QColorSpace::TransferFunction, transferFunctionId); + + Q_UNUSED(gamutId); + Q_UNUSED(transferFunctionId); + + QColorSpace colorSpace = colorSpaceId; + QByteArray iccProfile = colorSpace.iccProfile(); + QVERIFY(!iccProfile.isEmpty()); + + QColorSpace colorSpace2 = QColorSpace::fromIccProfile(iccProfile); + QVERIFY(colorSpace2.isValid()); + + QCOMPARE(colorSpace2, colorSpace); + + QByteArray iccProfile2 = colorSpace2.iccProfile(); + QVERIFY(!iccProfile2.isEmpty()); + + QCOMPARE(iccProfile2, iccProfile); +} + +void tst_QColorSpace::fromIccProfile() +{ + // Read the official sRGB ICCv2 profile: + QString prefix = QFINDTESTDATA("resources/"); + QFile file(prefix + "sRGB2014.icc"); + file.open(QIODevice::ReadOnly); + QByteArray iccProfile = file.readAll(); + QColorSpace stdSRgb = QColorSpace::fromIccProfile(iccProfile); + QVERIFY(stdSRgb.isValid()); + + QCOMPARE(stdSRgb.gamut(), QColorSpace::Gamut::SRgb); + QCOMPARE(stdSRgb.transferFunction(), QColorSpace::TransferFunction::SRgb); + QCOMPARE(stdSRgb.colorSpaceId(), QColorSpace::SRgb); + + QCOMPARE(stdSRgb, QColorSpace(QColorSpace::SRgb)); +} + +void tst_QColorSpace::imageConversion_data() +{ + QTest::addColumn("fromColorSpace"); + QTest::addColumn("toColorSpace"); + QTest::addColumn("tolerance"); + + QTest::newRow("sRGB -> Display-P3") << QColorSpace::SRgb << QColorSpace::DisplayP3 << 0; + QTest::newRow("sRGB -> Adobe RGB") << QColorSpace::SRgb << QColorSpace::AdobeRgb << 2; + QTest::newRow("Display-P3 -> sRGB") << QColorSpace::DisplayP3 << QColorSpace::SRgb << 0; + QTest::newRow("Adobe RGB -> sRGB") << QColorSpace::AdobeRgb << QColorSpace::SRgb << 2; + QTest::newRow("Display-P3 -> Adobe RGB") << QColorSpace::DisplayP3 << QColorSpace::AdobeRgb << 2; + QTest::newRow("Display-P3 -> BT.2020") << QColorSpace::DisplayP3 << QColorSpace::Bt2020 << 4; + QTest::newRow("sRGB -> sRGB Linear") << QColorSpace::SRgb << QColorSpace::SRgbLinear << 0; +} + +void tst_QColorSpace::imageConversion() +{ + QFETCH(QColorSpace::ColorSpaceId, fromColorSpace); + QFETCH(QColorSpace::ColorSpaceId, toColorSpace); + QFETCH(int, tolerance); + + QImage testImage(256, 1, QImage::Format_RGB32); + + for (int i = 0; i < 256; ++i) + testImage.setPixel(i, 0, qRgb(i, i, i)); + + testImage.setColorSpace(fromColorSpace); + QCOMPARE(testImage.colorSpace(), QColorSpace(fromColorSpace)); + + testImage.convertToColorSpace(toColorSpace); + QCOMPARE(testImage.colorSpace(), QColorSpace(toColorSpace)); + + int lastRed = 0; + int lastGreen = 0; + int lastBlue = 0; + for (int i = 0; i < 256; ++i) { + QRgb p = testImage.pixel(i, 0); + QVERIFY(qRed(p) >= lastRed); + QVERIFY(qGreen(p) >= lastGreen); + QVERIFY(qBlue(p) >= lastBlue); + lastRed = qRed(p); + lastGreen = qGreen(p); + lastBlue = qBlue(p); + } + + lastRed = 0; + lastGreen = 0; + lastBlue = 0; + testImage.convertToColorSpace(fromColorSpace); + QCOMPARE(testImage.colorSpace(), QColorSpace(fromColorSpace)); + for (int i = 0; i < 256; ++i) { + QRgb p = testImage.pixel(i, 0); + QVERIFY(qAbs(qRed(p) - qGreen(p)) <= tolerance); + QVERIFY(qAbs(qRed(p) - qBlue(p)) <= tolerance); + QVERIFY((lastRed - qRed(p)) <= (tolerance / 2)); + QVERIFY((lastGreen - qGreen(p)) <= (tolerance / 2)); + QVERIFY((lastBlue - qBlue(p)) <= (tolerance / 2)); + lastRed = qRed(p); + lastGreen = qGreen(p); + lastBlue = qBlue(p); + } +} + + +void tst_QColorSpace::loadImage() +{ + QString prefix = QFINDTESTDATA("resources/"); + QImageReader reader(prefix + "ProPhoto.jpg"); + QImage image = reader.read(); + + QVERIFY(!image.isNull()); + QVERIFY(image.colorSpace().isValid()); + QCOMPARE(image.colorSpace().colorSpaceId(), QColorSpace::ProPhotoRgb); + QVERIFY(!image.colorSpace().iccProfile().isEmpty()); + + QColorSpace defaultProPhotoRgb = QColorSpace::ProPhotoRgb; + QVERIFY(!defaultProPhotoRgb.iccProfile().isEmpty()); + + // Test the iccProfile getter returns the ICC profile from the image + // which since we didn't write it, isn't identical to our defaults. + QVERIFY(defaultProPhotoRgb.iccProfile() != image.colorSpace().iccProfile()); +} + +QTEST_MAIN(tst_QColorSpace) +#include "tst_qcolorspace.moc"