diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index a70f4e733b2..84ee660ff89 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -160,6 +160,7 @@ qt_internal_add_module(Gui painting/qblittable.cpp painting/qblittable_p.h painting/qbrush.cpp painting/qbrush.h painting/qcolor.cpp painting/qcolor.h painting/qcolor_p.h + painting/qcolorclut_p.h painting/qcolormatrix_p.h painting/qcolorspace.cpp painting/qcolorspace.h painting/qcolorspace_p.h painting/qcolortransferfunction_p.h diff --git a/src/gui/painting/qcolorclut_p.h b/src/gui/painting/qcolorclut_p.h new file mode 100644 index 00000000000..81e002d9135 --- /dev/null +++ b/src/gui/painting/qcolorclut_p.h @@ -0,0 +1,80 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QCOLORCLUT_H +#define QCOLORCLUT_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 + +// A 3-dimensional lookup table compatible with ICC lut8, lut16, mAB, and mBA formats. +class QColorCLUT +{ + inline static QColorVector interpolate(const QColorVector &a, const QColorVector &b, float t) + { + return a + (b - a) * t; // faster than std::lerp by assuming no super large or non-number floats + } + inline static void interpolateIn(QColorVector &a, const QColorVector &b, float t) + { + a += (b - a) * t; + } +public: + qsizetype gridPointsX = 0; + qsizetype gridPointsY = 0; + qsizetype gridPointsZ = 0; + QList table; + + bool isEmpty() const { return table.isEmpty(); } + + QColorVector apply(const QColorVector &v) const + { + Q_ASSERT(table.size() == gridPointsX * gridPointsY * gridPointsZ); + const float x = std::clamp(v.x, 0.0f, 1.0f) * (gridPointsX - 1); + const float y = std::clamp(v.y, 0.0f, 1.0f) * (gridPointsY - 1); + const float z = std::clamp(v.z, 0.0f, 1.0f) * (gridPointsZ - 1); + // Variables for trilinear interpolation + const qsizetype lox = static_cast(std::floor(x)); + const qsizetype hix = std::min(lox + 1, gridPointsX - 1); + const qsizetype loy = static_cast(std::floor(y)); + const qsizetype hiy = std::min(loy + 1, gridPointsY - 1); + const qsizetype loz = static_cast(std::floor(z)); + const qsizetype hiz = std::min(loz + 1, gridPointsZ - 1); + const float fracx = x - static_cast(lox); + const float fracy = y - static_cast(loy); + const float fracz = z - static_cast(loz); + QColorVector tmp[4]; + auto index = [&](qsizetype x, qsizetype y, qsizetype z) { return x * gridPointsZ * gridPointsY + y * gridPointsZ + z; }; + // interpolate over z + tmp[0] = interpolate(table[index(lox, loy, loz)], + table[index(lox, loy, hiz)], fracz); + tmp[1] = interpolate(table[index(lox, hiy, loz)], + table[index(lox, hiy, hiz)], fracz); + tmp[2] = interpolate(table[index(hix, loy, loz)], + table[index(hix, loy, hiz)], fracz); + tmp[3] = interpolate(table[index(hix, hiy, loz)], + table[index(hix, hiy, hiz)], fracz); + // interpolate over y + interpolateIn(tmp[0], tmp[1], fracy); + interpolateIn(tmp[2], tmp[3], fracy); + // interpolate over x + interpolateIn(tmp[0], tmp[2], fracx); + return tmp[0]; + } +}; + +QT_END_NAMESPACE + +#endif // QCOLORCLUT_H diff --git a/src/gui/painting/qcolormatrix_p.h b/src/gui/painting/qcolormatrix_p.h index 1ce5df6264b..2d979bc5cba 100644 --- a/src/gui/painting/qcolormatrix_p.h +++ b/src/gui/painting/qcolormatrix_p.h @@ -1,4 +1,4 @@ -// Copyright (C) 2020 The Qt Company Ltd. +// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QCOLORMATRIX_H @@ -31,21 +31,23 @@ public: explicit constexpr QColorVector(const QPointF &chr) // from XY chromaticity : x(chr.x() / chr.y()) , y(1.0f) - , z((1.0 - chr.x() - chr.y()) / chr.y()) + , z((1.0f - chr.x() - chr.y()) / chr.y()) { } - float x = 0.0f; // X, x or red - float y = 0.0f; // Y, y or green - float z = 0.0f; // Z, Y or blue + float x = 0.0f; // X, x, L, or red + float y = 0.0f; // Y, y, a, or green + float z = 0.0f; // Z, Y, b, or blue float _unused = 0.0f; - friend inline bool operator==(const QColorVector &v1, const QColorVector &v2); - friend inline bool operator!=(const QColorVector &v1, const QColorVector &v2); - bool isNull() const + constexpr bool isNull() const noexcept { return !x && !y && !z; } + bool isValid() const noexcept + { + return std::isfinite(x) && std::isfinite(y) && std::isfinite(z); + } - static bool isValidChromaticity(const QPointF &chr) + static constexpr bool isValidChromaticity(const QPointF &chr) { if (chr.x() < qreal(0.0) || chr.x() > qreal(1.0)) return false; @@ -56,26 +58,92 @@ public: return true; } + constexpr QColorVector operator*(float f) const { return QColorVector(x * f, y * f, z * f); } + constexpr QColorVector operator+(const QColorVector &v) const { return QColorVector(x + v.x, y + v.y, z + v.z); } + constexpr QColorVector operator-(const QColorVector &v) const { return QColorVector(x - v.x, y - v.y, z - v.z); } + void operator+=(const QColorVector &v) { x += v.x; y += v.y; z += v.z; } + // Common whitepoints: static constexpr QPointF D50Chromaticity() { return QPointF(0.34567, 0.35850); } static constexpr QPointF D65Chromaticity() { return QPointF(0.31271, 0.32902); } static constexpr QColorVector D50() { return QColorVector(D50Chromaticity()); } static constexpr QColorVector D65() { return QColorVector(D65Chromaticity()); } + + QColorVector xyzToLab() const + { + constexpr QColorVector ref = D50(); + constexpr float eps = 0.008856f; + constexpr float kap = 903.3f; + float xr = x / ref.x; + float yr = y / ref.y; + float zr = z / ref.z; + + float fx, fy, fz; + if (xr > eps) + fx = std::cbrt(xr); + else + fx = (kap * xr + 16.f) / 116.f; + if (yr > eps) + fy = std::cbrt(yr); + else + fy = (kap * yr + 16.f) / 116.f; + if (zr > eps) + fz = std::cbrt(zr); + else + fz = (kap * zr + 16.f) / 116.f; + + const float L = 116.f * fy - 16.f; + const float a = 500.f * (fx - fy); + const float b = 200.f * (fy - fz); + // We output Lab values that has been scaled to 0.0->1.0 values, see also labToXyz. + return QColorVector(L / 100.f, (a + 128.f) / 255.f, (b + 128.f) / 255.f); + } + + QColorVector labToXyz() const + { + constexpr QColorVector ref = D50(); + constexpr float eps = 0.008856f; + constexpr float kap = 903.3f; + // This transform has been guessed from the ICC spec, but it is not stated + // anywhere to be the one to use to map to and from 0.0->1.0 values: + const float L = x * 100.f; + const float a = (y * 255.f) - 128.f; + const float b = (z * 255.f) - 128.f; + // From here is official Lab->XYZ conversion: + float fy = (L + 16.f) / 116.f; + float fx = fy + (a / 500.f); + float fz = fy - (b / 200.f); + + float xr, yr, zr; + if (fx * fx * fx > eps) + xr = fx * fx * fx; + else + xr = (116.f * fx - 16) / kap; + if (L > (kap * eps)) + yr = fy * fy * fy; + else + yr = L / kap; + if (fz * fz * fz > eps) + zr = fz * fz * fz; + else + zr = (116.f * fz - 16) / kap; + + xr = xr * ref.x; + yr = yr * ref.y; + zr = zr * ref.z; + return QColorVector(xr, yr, zr); + } + friend inline bool comparesEqual(const QColorVector &lhs, const QColorVector &rhs); + Q_DECLARE_EQUALITY_COMPARABLE(QColorVector); }; -inline bool operator==(const QColorVector &v1, const QColorVector &v2) +inline bool comparesEqual(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 QTransform because only floats are needed and performance is critical. class QColorMatrix @@ -86,20 +154,20 @@ public: 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 isNull() const + constexpr bool isNull() const { return r.isNull() && g.isNull() && b.isNull(); } + constexpr float determinant() const + { + return 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); + } 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); + return std::isnormal(determinant()); } bool isIdentity() const noexcept { @@ -108,9 +176,7 @@ public: 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); + float det = determinant(); det = 1.0f / det; QColorMatrix inv; inv.r.x = (g.y * b.z - b.y * g.z) * det; @@ -124,20 +190,20 @@ public: inv.b.z = (r.x * g.y - g.x * r.y) * det; return inv; } - QColorMatrix operator*(const QColorMatrix &o) const + friend inline constexpr QColorMatrix operator*(const QColorMatrix &a, const QColorMatrix &o) { 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.x = a.r.x * o.r.x + a.g.x * o.r.y + a.b.x * o.r.z; + comb.g.x = a.r.x * o.g.x + a.g.x * o.g.y + a.b.x * o.g.z; + comb.b.x = a.r.x * o.b.x + a.g.x * o.b.y + a.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.y = a.r.y * o.r.x + a.g.y * o.r.y + a.b.y * o.r.z; + comb.g.y = a.r.y * o.g.x + a.g.y * o.g.y + a.b.y * o.g.z; + comb.b.y = a.r.y * o.b.x + a.g.y * o.b.y + a.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; + comb.r.z = a.r.z * o.r.x + a.g.z * o.r.y + a.b.z * o.r.z; + comb.g.z = a.r.z * o.g.x + a.g.z * o.g.y + a.b.z * o.g.z; + comb.b.z = a.r.z * o.b.x + a.g.z * o.b.y + a.b.z * o.b.z; return comb; } @@ -189,18 +255,15 @@ public: { 0.1351922452f, 0.7118769884f, 0.0000000000f }, { 0.0313525312f, 0.0000856627f, 0.8251883388f } }; } + friend inline bool comparesEqual(const QColorMatrix &lhs, const QColorMatrix &rhs); + Q_DECLARE_EQUALITY_COMPARABLE(QColorMatrix); }; -inline bool operator==(const QColorMatrix &m1, const QColorMatrix &m2) +inline bool comparesEqual(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 index 539a294102e..7244a8fbf02 100644 --- a/src/gui/painting/qcolorspace.cpp +++ b/src/gui/painting/qcolorspace.cpp @@ -5,6 +5,7 @@ #include "qcolorspace_p.h" #include "qcolortransform.h" +#include "qcolorclut_p.h" #include "qcolormatrix_p.h" #include "qcolortransferfunction_p.h" #include "qcolortransform_p.h" @@ -418,7 +419,12 @@ QColorTransform QColorSpacePrivate::transformationToColorSpace(const QColorSpace combined.d = ptr; ptr->colorSpaceIn = this; ptr->colorSpaceOut = out; - ptr->colorMatrix = out->toXyz.inverted() * toXyz; + if (isThreeComponentMatrix()) + ptr->colorMatrix = toXyz; + else + ptr->colorMatrix = QColorMatrix::identity(); + if (out->isThreeComponentMatrix()) + ptr->colorMatrix = out->toXyz.inverted() * ptr->colorMatrix; if (ptr->isIdentity()) return QColorTransform(); return combined; @@ -431,10 +437,30 @@ QColorTransform QColorSpacePrivate::transformationToXYZ() const transform.d = ptr; ptr->colorSpaceIn = this; ptr->colorSpaceOut = this; - ptr->colorMatrix = toXyz; + if (isThreeComponentMatrix()) + ptr->colorMatrix = toXyz; + else + ptr->colorMatrix = QColorMatrix::identity(); return transform; } +bool QColorSpacePrivate::isThreeComponentMatrix() const +{ + return transformModel == QColorSpace::TransformModel::ThreeComponentMatrix; +} + +void QColorSpacePrivate::clearElementListProcessingForEdit() +{ + Q_ASSERT(transformModel == QColorSpace::TransformModel::ElementListProcessing); + Q_ASSERT(primaries == QColorSpace::Primaries::Custom); + Q_ASSERT(transferFunction == QColorSpace::TransferFunction::Custom); + + transformModel = QColorSpace::TransformModel::ThreeComponentMatrix; + isPcsLab = false; + mAB.clear(); + mBA.clear(); +} + /*! \class QColorSpace \brief The QColorSpace class provides a color space abstraction. @@ -512,6 +538,20 @@ QColorTransform QColorSpacePrivate::transformationToXYZ() const \value ProPhotoRgb The ProPhoto RGB transfer function, composed of linear and gamma parts */ +/*! + \enum QColorSpace::TransformModel + \since 6.8 + + Defines the processing model used for color space transforms. + + \value ThreeComponentMatrix The transform consist of a matrix calculated from primaries and set of transfer functions for each color channel. + This is very fast and used by all predefined color spaces. + \value ElementListProcessing The transforms are two lists of processing elements that can do many things, + each list only process either to the connection color space or from it. This is very flexible, but rather + slow, and can only be set by reading ICC profiles (See \l fromIccProfile()). When changing either primaries + or transfer function on a color space on this type it will reset to a ThreeComponentMatrix form. +*/ + /*! \fn QColorSpace::QColorSpace() @@ -689,7 +729,10 @@ void QColorSpace::setTransferFunction(QColorSpace::TransferFunction transferFunc if (d_ptr->transferFunction == transferFunction && d_ptr->gamma == gamma) return; detach(); - d_ptr->description.clear(); + if (d_ptr->transformModel == TransformModel::ElementListProcessing) + d_ptr->clearElementListProcessingForEdit(); + d_ptr->iccProfile = {}; + d_ptr->description = QString(); d_ptr->transferFunction = transferFunction; d_ptr->gamma = gamma; d_ptr->identifyColorSpace(); @@ -710,7 +753,10 @@ void QColorSpace::setTransferFunction(const QList &transferFunctionTab return; } detach(); - d_ptr->description.clear(); + if (d_ptr->transformModel == TransformModel::ElementListProcessing) + d_ptr->clearElementListProcessingForEdit(); + d_ptr->iccProfile = {}; + d_ptr->description = QString(); d_ptr->setTransferFunctionTable(transferFunctionTable); d_ptr->gamma = 0; d_ptr->identifyColorSpace(); @@ -737,7 +783,10 @@ void QColorSpace::setTransferFunctions(const QList &redTransferFunctio return; } detach(); - d_ptr->description.clear(); + if (d_ptr->transformModel == TransformModel::ElementListProcessing) + d_ptr->clearElementListProcessingForEdit(); + d_ptr->iccProfile = {}; + d_ptr->description = QString(); d_ptr->setTransferFunctionTables(redTransferFunctionTable, greenTransferFunctionTable, blueTransferFunctionTable); @@ -753,7 +802,7 @@ void QColorSpace::setTransferFunctions(const QList &redTransferFunctio */ QColorSpace QColorSpace::withTransferFunction(QColorSpace::TransferFunction transferFunction, float gamma) const { - if (!isValid() || transferFunction == QColorSpace::TransferFunction::Custom) + if (!isValid() || transferFunction == TransferFunction::Custom) return *this; if (d_ptr->transferFunction == transferFunction && d_ptr->gamma == gamma) return *this; @@ -813,7 +862,10 @@ void QColorSpace::setPrimaries(QColorSpace::Primaries primariesId) if (d_ptr->primaries == primariesId) return; detach(); - d_ptr->description.clear(); + if (d_ptr->transformModel == TransformModel::ElementListProcessing) + d_ptr->clearElementListProcessingForEdit(); + d_ptr->iccProfile = {}; + d_ptr->description = QString(); d_ptr->primaries = primariesId; d_ptr->identifyColorSpace(); d_ptr->setToXyzMatrix(); @@ -839,13 +891,27 @@ void QColorSpace::setPrimaries(const QPointF &whitePoint, const QPointF &redPoin if (QColorVector(primaries.whitePoint) == d_ptr->whitePoint && toXyz == d_ptr->toXyz) return; detach(); - d_ptr->description.clear(); + if (d_ptr->transformModel == TransformModel::ElementListProcessing) + d_ptr->clearElementListProcessingForEdit(); + d_ptr->iccProfile = {}; + d_ptr->description = QString(); d_ptr->primaries = QColorSpace::Primaries::Custom; d_ptr->toXyz = toXyz; d_ptr->whitePoint = QColorVector(primaries.whitePoint); d_ptr->identifyColorSpace(); } +/*! + \since 6.8 + Returns the transfrom processing model used for this color space. +*/ +QColorSpace::TransformModel QColorSpace::transformModel() const noexcept +{ + if (Q_UNLIKELY(!d_ptr)) + return QColorSpace::TransformModel::ThreeComponentMatrix; + return d_ptr->transformModel; +} + /*! \internal */ @@ -884,7 +950,7 @@ QByteArray QColorSpace::iccProfile() const 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. + RGB or Gray ICC profiles. If the ICC profile is not supported an invalid QColorSpace is returned where you can still read the original ICC profile using iccProfile(). @@ -906,9 +972,23 @@ QColorSpace QColorSpace::fromIccProfile(const QByteArray &iccProfile) */ bool QColorSpace::isValid() const noexcept { - return d_ptr - && d_ptr->toXyz.isValid() - && d_ptr->trc[0].isValid() && d_ptr->trc[1].isValid() && d_ptr->trc[2].isValid(); + if (!d_ptr) + return false; + return d_ptr->isValid(); +} + +/*! + \internal +*/ +bool QColorSpacePrivate::isValid() const noexcept +{ + if (!isThreeComponentMatrix()) + return !mAB.isEmpty(); + if (!toXyz.isValid()) + return false; + if (!trc[0].isValid() || !trc[1].isValid() || !trc[2].isValid()) + return false; + return true; } /*! @@ -925,6 +1005,50 @@ bool QColorSpace::isValid() const noexcept otherwise returns \c false */ +static bool compareElement(const QColorSpacePrivate::TransferElement &element, + const QColorSpacePrivate::TransferElement &other) +{ + return element.trc[0] == other.trc[0] + && element.trc[1] == other.trc[1] + && element.trc[2] == other.trc[2]; +} + +static bool compareElement(const QColorMatrix &element, + const QColorMatrix &other) +{ + return element == other; +} + +static bool compareElement(const QColorVector &element, + const QColorVector &other) +{ + return element == other; +} + +static bool compareElement(const QColorCLUT &element, + const QColorCLUT &other) +{ + if (element.gridPointsX != other.gridPointsX) + return false; + if (element.gridPointsY != other.gridPointsY) + return false; + if (element.gridPointsZ != other.gridPointsZ) + return false; + if (element.table.size() != other.table.size()) + return false; + for (qsizetype i = 0; i < element.table.size(); ++i) { + if (element.table[i] != other.table[i]) + return false; + } + return true; +} + +template +static bool compareElements(const T &element, const QColorSpacePrivate::Element &other) +{ + return compareElement(element, std::get(other)); +} + /*! \internal */ @@ -932,43 +1056,87 @@ bool QColorSpace::equals(const QColorSpace &other) const { if (d_ptr == other.d_ptr) return true; - if (!d_ptr || !other.d_ptr) + if (!d_ptr) + return false; + return d_ptr->equals(other.d_ptr.constData()); +} + +/*! + \internal +*/ +bool QColorSpacePrivate::equals(const QColorSpacePrivate *other) const +{ + if (!other) return false; - if (d_ptr->namedColorSpace && other.d_ptr->namedColorSpace) - return d_ptr->namedColorSpace == other.d_ptr->namedColorSpace; + if (namedColorSpace && other->namedColorSpace) + return namedColorSpace == other->namedColorSpace; const bool valid1 = isValid(); - const bool valid2 = other.isValid(); + const bool valid2 = other->isValid(); if (valid1 != valid2) return false; if (!valid1 && !valid2) { - if (!d_ptr->iccProfile.isEmpty() || !other.d_ptr->iccProfile.isEmpty()) - return d_ptr->iccProfile == other.d_ptr->iccProfile; + if (!iccProfile.isEmpty() || !other->iccProfile.isEmpty()) + return iccProfile == other->iccProfile; + return false; } // At this point one or both color spaces are unknown, and must be compared in detail instead - if (primaries() != QColorSpace::Primaries::Custom && other.primaries() != QColorSpace::Primaries::Custom) { - if (primaries() != other.primaries()) - return false; - } else { - if (d_ptr->toXyz != other.d_ptr->toXyz) - return false; - } + if (transformModel != other->transformModel) + return false; - if (transferFunction() != QColorSpace::TransferFunction::Custom && - other.transferFunction() != QColorSpace::TransferFunction::Custom) { - if (transferFunction() != other.transferFunction()) + if (!isThreeComponentMatrix()) { + if (isPcsLab != other->isPcsLab) return false; - if (transferFunction() == QColorSpace::TransferFunction::Gamma) - return (qAbs(gamma() - other.gamma()) <= (1.0f / 512.0f)); + if (mAB.count() != other->mAB.count()) + return false; + if (mBA.count() != other->mBA.count()) + return false; + + // Compare element types + for (qsizetype i = 0; i < mAB.count(); ++i) { + if (mAB[i].index() != other->mAB[i].index()) + return false; + } + for (qsizetype i = 0; i < mBA.count(); ++i) { + if (mBA[i].index() != other->mBA[i].index()) + return false; + } + + // Compare element contents + for (qsizetype i = 0; i < mAB.count(); ++i) { + if (!std::visit([&](auto &&elm) { return compareElements(elm, other->mAB[i]); }, mAB[i])) + return false; + } + for (qsizetype i = 0; i < mBA.count(); ++i) { + if (!std::visit([&](auto &&elm) { return compareElements(elm, other->mBA[i]); }, mBA[i])) + return false; + } + return true; } - if (d_ptr->trc[0] != other.d_ptr->trc[0] || - d_ptr->trc[1] != other.d_ptr->trc[1] || - d_ptr->trc[2] != other.d_ptr->trc[2]) + if (primaries != QColorSpace::Primaries::Custom && other->primaries != QColorSpace::Primaries::Custom) { + if (primaries != other->primaries) + return false; + } else { + if (toXyz != other->toXyz) + return false; + } + + if (transferFunction != QColorSpace::TransferFunction::Custom && other->transferFunction != QColorSpace::TransferFunction::Custom) { + if (transferFunction != other->transferFunction) + return false; + if (transferFunction == QColorSpace::TransferFunction::Gamma) + return (qAbs(gamma - other->gamma) <= (1.0f / 512.0f)); + return true; + } + + if (trc[0] != other->trc[0] || + trc[1] != other->trc[1] || + trc[2] != other->trc[2]) return false; return true; @@ -985,6 +1153,10 @@ QColorTransform QColorSpace::transformationToColorSpace(const QColorSpace &color if (*this == colorspace) return QColorTransform(); + if (colorspace.transformModel() == TransformModel::ElementListProcessing && colorspace.d_ptr->mBA.isEmpty()) { + qWarning() << "Attempted transform to from-only colorspace"; + return QColorTransform(); + } return d_ptr->transformationToColorSpace(colorspace.d_ptr.get()); } @@ -1024,6 +1196,7 @@ QString QColorSpace::description() const noexcept void QColorSpace::setDescription(const QString &description) { detach(); + d_ptr->iccProfile = {}; d_ptr->userDescription = description; } @@ -1066,6 +1239,28 @@ QDataStream &operator>>(QDataStream &s, QColorSpace &colorSpace) #endif // QT_NO_DATASTREAM #ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug dbg, const QColorSpacePrivate::TransferElement &) +{ + return dbg << ":Transfer"; +} +QDebug operator<<(QDebug dbg, const QColorMatrix &) +{ + return dbg << ":Matrix"; +} +QDebug operator<<(QDebug dbg, const QColorVector &) +{ + return dbg << ":Offset"; +} +QDebug operator<<(QDebug dbg, const QColorCLUT &) +{ + return dbg << ":CLUT"; +} +QDebug operator<<(QDebug dbg, const QList &elements) +{ + for (auto &&element : elements) + std::visit([&](auto &&elm) { dbg << elm; }, element); + return dbg; +} QDebug operator<<(QDebug dbg, const QColorSpace &colorSpace) { QDebugStateSaver saver(dbg); @@ -1074,8 +1269,22 @@ QDebug operator<<(QDebug dbg, const QColorSpace &colorSpace) if (colorSpace.d_ptr) { if (colorSpace.d_ptr->namedColorSpace) dbg << colorSpace.d_ptr->namedColorSpace << ", "; - dbg << colorSpace.primaries() << ", " << colorSpace.transferFunction(); - dbg << ", gamma=" << colorSpace.gamma(); + if (!colorSpace.isValid()) { + dbg << "Invalid"; + if (!colorSpace.d_ptr->iccProfile.isEmpty()) + dbg << " with profile data"; + } else if (colorSpace.d_ptr->isThreeComponentMatrix()) { + dbg << colorSpace.primaries() << ", " << colorSpace.transferFunction(); + dbg << ", gamma=" << colorSpace.gamma(); + } else { + if (colorSpace.d_ptr->isPcsLab) + dbg << "PCSLab, "; + else + dbg << "PCSXYZ, "; + dbg << "A2B" << colorSpace.d_ptr->mAB; + if (!colorSpace.d_ptr->mBA.isEmpty()) + dbg << ", B2A" << colorSpace.d_ptr->mBA; + } } dbg << ')'; return dbg; diff --git a/src/gui/painting/qcolorspace.h b/src/gui/painting/qcolorspace.h index 4fb5c4273fe..127f18caa2a 100644 --- a/src/gui/painting/qcolorspace.h +++ b/src/gui/painting/qcolorspace.h @@ -45,6 +45,11 @@ public: ProPhotoRgb }; Q_ENUM(TransferFunction) + enum class TransformModel : uint8_t { + ThreeComponentMatrix = 0, + ElementListProcessing, + }; + Q_ENUM(TransformModel) QColorSpace() noexcept = default; QColorSpace(NamedColorSpace namedColorSpace); @@ -100,6 +105,7 @@ public: void setPrimaries(const QPointF &whitePoint, const QPointF &redPoint, const QPointF &greenPoint, const QPointF &bluePoint); + TransformModel transformModel() const noexcept; void detach(); bool isValid() const noexcept; diff --git a/src/gui/painting/qcolorspace_p.h b/src/gui/painting/qcolorspace_p.h index 39d901ecdf2..03872ab2c50 100644 --- a/src/gui/painting/qcolorspace_p.h +++ b/src/gui/painting/qcolorspace_p.h @@ -1,4 +1,4 @@ -// Copyright (C) 2018 The Qt Company Ltd. +// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QCOLORSPACE_P_H @@ -16,6 +16,7 @@ // #include "qcolorspace.h" +#include "qcolorclut_p.h" #include "qcolormatrix_p.h" #include "qcolortrc_p.h" #include "qcolortrclut_p.h" @@ -77,6 +78,9 @@ public: return colorSpace.d_ptr.get(); } + bool equals(const QColorSpacePrivate *other) const; + bool isValid() const noexcept; + void initialize(); void setToXyzMatrix(); void setTransferFunction(); @@ -88,21 +92,37 @@ public: QColorTransform transformationToColorSpace(const QColorSpacePrivate *out) const; QColorTransform transformationToXYZ() const; + bool isThreeComponentMatrix() const; + void clearElementListProcessingForEdit(); + static constexpr QColorSpace::NamedColorSpace Unknown = QColorSpace::NamedColorSpace(0); QColorSpace::NamedColorSpace namedColorSpace = Unknown; QColorSpace::Primaries primaries = QColorSpace::Primaries::Custom; QColorSpace::TransferFunction transferFunction = QColorSpace::TransferFunction::Custom; + QColorSpace::TransformModel transformModel = QColorSpace::TransformModel::ThreeComponentMatrix; float gamma = 0.0f; QColorVector whitePoint; + // Three component matrix data: QColorTrc trc[3]; QColorMatrix toXyz; + // Element list processing data: + struct TransferElement { + QColorTrc trc[3]; + }; + using Element = std::variant; + bool isPcsLab = false; + // A = device, B = PCS + QList mAB, mBA; + + // Metadata QString description; QString userDescription; QByteArray iccProfile; + // Cached tables for three component matrix transform: Q_CONSTINIT static QBasicMutex s_lutWriteLock; struct LUT { LUT() = default; diff --git a/src/gui/painting/qcolortransferfunction_p.h b/src/gui/painting/qcolortransferfunction_p.h index 8bbce3ca99c..6afbfd25c20 100644 --- a/src/gui/painting/qcolortransferfunction_p.h +++ b/src/gui/painting/qcolortransferfunction_p.h @@ -53,8 +53,13 @@ public: { if (x < m_d) return m_c * x + m_f; + float t = std::pow(m_a * x + m_b, m_g); + if (std::isfinite(t)) + return t + m_e; + if (t > 0.f) + return 1.f; else - return std::pow(m_a * x + m_b, m_g) + m_e; + return 0.f; } QColorTransferFunction inverted() const @@ -63,7 +68,7 @@ public: d = m_c * m_d + m_f; - if (!qFuzzyIsNull(m_c)) { + if (std::isnormal(m_c)) { c = 1.0f / m_c; f = -m_f / m_c; } else { @@ -71,8 +76,12 @@ public: f = 0.0f; } - if (!qFuzzyIsNull(m_a) && !qFuzzyIsNull(m_g)) { + bool valid_abeg = std::isnormal(m_a) && std::isnormal(m_g); + if (valid_abeg) a = std::pow(1.0f / m_a, m_g); + if (valid_abeg && !std::isfinite(a)) + valid_abeg = false; + if (valid_abeg) { b = -a * m_e; e = -m_b / m_a; g = 1.0f / m_g; diff --git a/src/gui/painting/qcolortransfertable_p.h b/src/gui/painting/qcolortransfertable_p.h index 20c35cac2d2..5b1bd674762 100644 --- a/src/gui/painting/qcolortransfertable_p.h +++ b/src/gui/painting/qcolortransfertable_p.h @@ -29,16 +29,18 @@ QT_BEGIN_NAMESPACE class Q_GUI_EXPORT QColorTransferTable { public: - QColorTransferTable() noexcept - : m_tableSize(0) - { } - QColorTransferTable(uint32_t size, const QList &table) noexcept - : m_tableSize(size), m_table8(table) + enum Type : uint8_t { + TwoWay = 0, + OneWay, + }; + QColorTransferTable() noexcept = default; + QColorTransferTable(uint32_t size, const QList &table, Type type = TwoWay) noexcept + : m_type(type), m_tableSize(size), m_table8(table) { Q_ASSERT(qsizetype(size) <= table.size()); } - QColorTransferTable(uint32_t size, const QList &table) noexcept - : m_tableSize(size), m_table16(table) + QColorTransferTable(uint32_t size, const QList &table, Type type = TwoWay) noexcept + : m_type(type), m_tableSize(size), m_table16(table) { Q_ASSERT(qsizetype(size) <= table.size()); } @@ -69,7 +71,11 @@ public: // At least 2 elements if (m_tableSize < 2) return false; - // The table must describe an injective curve: + return (m_type == OneWay) || checkInvertibility(); + } + bool checkInvertibility() const + { + // The two-way tables must describe an injective curve: if (!m_table8.isEmpty()) { uint8_t val = 0; for (uint i = 0; i < m_tableSize; ++i) { @@ -97,9 +103,9 @@ public: const uint32_t hi = std::min(lo + 1, m_tableSize - 1); const float frac = x - lo; if (!m_table16.isEmpty()) - return (m_table16[lo] * (1.0f - frac) + m_table16[hi] * frac) * (1.0f/65535.0f); + return (m_table16[lo] + (m_table16[hi] - m_table16[lo]) * 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 (m_table8[lo] + (m_table8[hi] - m_table8[lo]) * frac) * (1.0f/255.0f); return x; } @@ -107,6 +113,7 @@ public: float applyInverse(float x, float resultLargerThan = 0.0f) const { Q_ASSERT(resultLargerThan >= 0.0f && resultLargerThan <= 1.0f); + Q_ASSERT(m_type == TwoWay); if (x <= 0.0f) return 0.0f; if (x >= 1.0f) @@ -125,7 +132,6 @@ public: Q_ASSERT(v >= y1 && v <= y2); const float fr = (v - y1) / (y2 - y1); return (i + fr) * (1.0f / (m_tableSize - 1)); - } if (!m_table8.isEmpty()) { const float v = x * 255.0f; @@ -201,7 +207,8 @@ public: friend inline bool operator!=(const QColorTransferTable &t1, const QColorTransferTable &t2); friend inline bool operator==(const QColorTransferTable &t1, const QColorTransferTable &t2); - uint32_t m_tableSize; + Type m_type = TwoWay; + uint32_t m_tableSize = 0; QList m_table8; QList m_table16; }; @@ -210,6 +217,8 @@ inline bool operator!=(const QColorTransferTable &t1, const QColorTransferTable { if (t1.m_tableSize != t2.m_tableSize) return true; + if (t1.m_type != t2.m_type) + return true; if (t1.m_table8.isEmpty() != t2.m_table8.isEmpty()) return true; if (t1.m_table16.isEmpty() != t2.m_table16.isEmpty()) diff --git a/src/gui/painting/qcolortransform.cpp b/src/gui/painting/qcolortransform.cpp index 76d3dfacc86..8d578d7af3a 100644 --- a/src/gui/painting/qcolortransform.cpp +++ b/src/gui/painting/qcolortransform.cpp @@ -1,9 +1,10 @@ -// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qcolortransform.h" #include "qcolortransform_p.h" +#include "qcolorclut_p.h" #include "qcolormatrix_p.h" #include "qcolorspace_p.h" #include "qcolortrc_p.h" @@ -139,11 +140,31 @@ bool QColorTransform::compare(const QColorTransform &other) const return false; if (bool(d->colorSpaceOut) != bool(other.d->colorSpaceOut)) return false; - for (int i = 0; i < 3; ++i) { - if (d->colorSpaceIn && d->colorSpaceIn->trc[i] != other.d->colorSpaceIn->trc[i]) + if (d->colorSpaceIn) { + if (d->colorSpaceIn->transformModel != other.d->colorSpaceIn->transformModel) return false; - if (d->colorSpaceOut && d->colorSpaceOut->trc[i] != other.d->colorSpaceOut->trc[i]) + if (d->colorSpaceIn->isThreeComponentMatrix()) { + for (int i = 0; i < 3; ++i) { + if (d->colorSpaceIn && d->colorSpaceIn->trc[i] != other.d->colorSpaceIn->trc[i]) + return false; + } + } else { + if (!d->colorSpaceIn->equals(other.d->colorSpaceIn.constData())) + return false; + } + } + if (d->colorSpaceOut) { + if (d->colorSpaceOut->transformModel != other.d->colorSpaceOut->transformModel) return false; + if (d->colorSpaceOut->isThreeComponentMatrix()) { + for (int i = 0; i < 3; ++i) { + if (d->colorSpaceOut && d->colorSpaceOut->trc[i] != other.d->colorSpaceOut->trc[i]) + return false; + } + } else { + if (!d->colorSpaceOut->equals(other.d->colorSpaceOut.constData())) + return false; + } } return true; } @@ -159,29 +180,7 @@ QRgb QColorTransform::map(QRgb argb) const return argb; constexpr float f = 1.0f / 255.0f; QColorVector c = { qRed(argb) * f, qGreen(argb) * f, qBlue(argb) * f }; - if (d->colorSpaceIn->lut.generated.loadAcquire()) { - c.x = d->colorSpaceIn->lut[0]->toLinear(c.x); - c.y = d->colorSpaceIn->lut[1]->toLinear(c.y); - c.z = d->colorSpaceIn->lut[2]->toLinear(c.z); - } else { - 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->lut.generated.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); - } - + c = d->map(c); return qRgba(c.x * 255 + 0.5f, c.y * 255 + 0.5f, c.z * 255 + 0.5f, qAlpha(argb)); } @@ -196,29 +195,7 @@ QRgba64 QColorTransform::map(QRgba64 rgba64) const return rgba64; constexpr float f = 1.0f / 65535.0f; QColorVector c = { rgba64.red() * f, rgba64.green() * f, rgba64.blue() * f }; - if (d->colorSpaceIn->lut.generated.loadAcquire()) { - c.x = d->colorSpaceIn->lut[0]->toLinear(c.x); - c.y = d->colorSpaceIn->lut[1]->toLinear(c.y); - c.z = d->colorSpaceIn->lut[2]->toLinear(c.z); - } else { - 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->lut.generated.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); - } - + c = d->map(c); return QRgba64::fromRgba64(c.x * 65535.f + 0.5f, c.y * 65535.f + 0.5f, c.z * 65535.f + 0.5f, rgba64.alpha()); } @@ -232,14 +209,11 @@ QRgbaFloat16 QColorTransform::map(QRgbaFloat16 rgbafp16) const { if (!d) return rgbafp16; - QColorVector c; - c.x = d->colorSpaceIn->trc[0].applyExtended(rgbafp16.r); - c.y = d->colorSpaceIn->trc[1].applyExtended(rgbafp16.g); - c.z = d->colorSpaceIn->trc[2].applyExtended(rgbafp16.b); - c = d->colorMatrix.map(c); - rgbafp16.r = qfloat16(d->colorSpaceOut->trc[0].applyInverseExtended(c.x)); - rgbafp16.g = qfloat16(d->colorSpaceOut->trc[1].applyInverseExtended(c.y)); - rgbafp16.b = qfloat16(d->colorSpaceOut->trc[2].applyInverseExtended(c.z)); + QColorVector c(rgbafp16.r, rgbafp16.g, rgbafp16.b); + c = d->mapExtended(c); + rgbafp16.r = qfloat16(c.x); + rgbafp16.g = qfloat16(c.y); + rgbafp16.b = qfloat16(c.z); return rgbafp16; } @@ -253,14 +227,11 @@ QRgbaFloat32 QColorTransform::map(QRgbaFloat32 rgbafp32) const { if (!d) return rgbafp32; - QColorVector c; - c.x = d->colorSpaceIn->trc[0].applyExtended(rgbafp32.r); - c.y = d->colorSpaceIn->trc[1].applyExtended(rgbafp32.g); - c.z = d->colorSpaceIn->trc[2].applyExtended(rgbafp32.b); - c = d->colorMatrix.map(c); - rgbafp32.r = d->colorSpaceOut->trc[0].applyInverseExtended(c.x); - rgbafp32.g = d->colorSpaceOut->trc[1].applyInverseExtended(c.y); - rgbafp32.b = d->colorSpaceOut->trc[2].applyInverseExtended(c.z); + QColorVector c(rgbafp32.r, rgbafp32.g, rgbafp32.b); + c = d->mapExtended(c); + rgbafp32.r = c.x; + rgbafp32.g = c.y; + rgbafp32.b = c.z; return rgbafp32; } @@ -310,7 +281,12 @@ QColor QColorTransform::map(const QColor &color) const // Optimized sub-routines for fast block based conversion: -template +enum ApplyMatrixForm { + DoNotClamp = 0, + DoClamp = 1 +}; + +template static void applyMatrix(QColorVector *buffer, const qsizetype len, const QColorMatrix &colorMatrix) { #if defined(__SSE2__) @@ -330,7 +306,7 @@ static void applyMatrix(QColorVector *buffer, const qsizetype len, const QColorM cx = _mm_add_ps(cx, cy); cx = _mm_add_ps(cx, cz); // Clamp: - if (DoClamp) { + if (doClamp) { cx = _mm_min_ps(cx, maxV); cx = _mm_max_ps(cx, minV); } @@ -350,19 +326,19 @@ static void applyMatrix(QColorVector *buffer, const qsizetype len, const QColorM cx = vaddq_f32(cx, cy); cx = vaddq_f32(cx, cz); // Clamp: - if (DoClamp) { + if (doClamp) { cx = vminq_f32(cx, maxV); cx = vmaxq_f32(cx, minV); } vst1q_f32(&buffer[j].x, cx); } #else - for (int j = 0; j < len; ++j) { + for (qsizetype j = 0; j < len; ++j) { const QColorVector cv = colorMatrix.map(buffer[j]); - if (DoClamp) { - 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)); + if (doClamp) { + buffer[j].x = std::clamp(cv.x, 0.f, 1.f); + buffer[j].y = std::clamp(cv.y, 0.f, 1.f); + buffer[j].z = std::clamp(cv.z, 0.f, 1.f); } else { buffer[j] = cv; } @@ -1218,9 +1194,343 @@ private: alignas(T) char data[sizeof(T) * Count]; }; -template -void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const +void loadUnpremultipliedLUT(QColorVector *buffer, const QRgb *src, const qsizetype len) { + const float f = 1.0f / 255.f; + for (qsizetype i = 0; i < len; ++i) { + const uint p = src[i]; + buffer[i].x = qRed(p) * f; + buffer[i].y = qGreen(p) * f; + buffer[i].z = qBlue(p) * f; + } +} + +void loadUnpremultipliedLUT(QColorVector *buffer, const QRgba64 *src, const qsizetype len) +{ + const float f = 1.0f / 65535.f; + for (qsizetype i = 0; i < len; ++i) { + buffer[i].x = src[i].red() * f; + buffer[i].y = src[i].green() * f; + buffer[i].z = src[i].blue() * f; + } +} + +void loadUnpremultipliedLUT(QColorVector *buffer, const QRgbaFloat32 *src, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + buffer[i].x = src[i].r; + buffer[i].y = src[i].g; + buffer[i].z = src[i].b; + } +} + +void loadPremultipliedLUT(QColorVector *buffer, const QRgb *src, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const uint p = src[i]; + const float f = 1.0f / qAlpha(p); + buffer[i].x = (qRed(p) * f); + buffer[i].y = (qGreen(p) * f); + buffer[i].z = (qBlue(p) * f); + } +} + +void loadPremultipliedLUT(QColorVector *buffer, const QRgba64 *src, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const float f = 1.0f / src[i].alpha(); + buffer[i].x = (src[i].red() * f); + buffer[i].y = (src[i].green() * f); + buffer[i].z = (src[i].blue() * f); + } +} + +void loadPremultipliedLUT(QColorVector *buffer, const QRgbaFloat32 *src, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const float f = 1.0f / src[i].a; + buffer[i].x = src[i].r * f; + buffer[i].y = src[i].g * f; + buffer[i].z = src[i].b * f; + } +} + +static void storeUnpremultipliedLUT(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const int r = buffer[i].x * 255.f; + const int g = buffer[i].y * 255.f; + const int b = buffer[i].z * 255.f; + dst[i] = (src[i] & 0xff000000) | (r << 16) | (g << 8) | (b << 0); + } +} + +static void storeUnpremultipliedLUT(QRgba64 *dst, const QRgba64 *src, + const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const int r = buffer[i].x * 65535.f; + const int g = buffer[i].y * 65535.f; + const int b = buffer[i].z * 65535.f; + dst[i] = qRgba64(r, g, b, src[i].alpha()); + } +} + +static void storeUnpremultipliedLUT(QRgbaFloat32 *dst, const QRgbaFloat32 *src, + const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const float r = buffer[i].x; + const float g = buffer[i].y; + const float b = buffer[i].z; + dst[i] = QRgbaFloat32{r, g, b, src[i].a}; + } +} + +static void storePremultipliedLUT(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const int a = qAlpha(src[i]); + const int r = buffer[i].x * a; + const int g = buffer[i].y * a; + const int b = buffer[i].z * a; + dst[i] = (src[i] & 0xff000000) | (r << 16) | (g << 8) | (b << 0); + } +} + +static void storePremultipliedLUT(QRgba64 *dst, const QRgba64 *src, + const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const int a = src[i].alpha(); + const int r = buffer[i].x * a; + const int g = buffer[i].y * a; + const int b = buffer[i].z * a; + dst[i] = qRgba64(r, g, b, a); + } +} + +static void storePremultipliedLUT(QRgbaFloat32 *dst, const QRgbaFloat32 *src, + const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const float a = src[i].a; + const float r = buffer[i].x * a; + const float g = buffer[i].y * a; + const float b = buffer[i].z * a; + dst[i] = QRgbaFloat32{r, g, b, a}; + } +} + +static void visitElement(const QColorSpacePrivate::TransferElement &element, QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + buffer[i].x = element.trc[0].apply(buffer[i].x); + buffer[i].y = element.trc[1].apply(buffer[i].y); + buffer[i].z = element.trc[2].apply(buffer[i].z); + } +} + +static void visitElement(const QColorMatrix &element, QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) + buffer[i] = element.map(buffer[i]); +} + +static void visitElement(const QColorVector &offset, QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) + buffer[i] += offset; +} + +static void visitElement(const QColorCLUT &element, QColorVector *buffer, const qsizetype len) +{ + if (element.isEmpty()) + return; + for (qsizetype i = 0; i < len; ++i) + buffer[i] = element.apply(buffer[i]); +} + +/*! + \internal +*/ +QColorVector QColorTransformPrivate::map(QColorVector c) const +{ + if (colorSpaceIn->isThreeComponentMatrix()) { + if (colorSpaceIn->lut.generated.loadAcquire()) { + c.x = colorSpaceIn->lut[0]->toLinear(c.x); + c.y = colorSpaceIn->lut[1]->toLinear(c.y); + c.z = colorSpaceIn->lut[2]->toLinear(c.z); + } else { + c.x = colorSpaceIn->trc[0].apply(c.x); + c.y = colorSpaceIn->trc[1].apply(c.y); + c.z = colorSpaceIn->trc[2].apply(c.z); + } + c = colorMatrix.map(c); + } else { + // Do element based conversion + for (auto &&element : colorSpaceIn->mAB) + std::visit([&c](auto &&elm) { visitElement(elm, &c, 1); }, element); + } + + // Match Profile Connection Spaces (PCS): + if (colorSpaceOut->isPcsLab && !colorSpaceIn->isPcsLab) + c = c.xyzToLab(); + else if (colorSpaceIn->isPcsLab && !colorSpaceOut->isPcsLab) + c = c.labToXyz(); + + if (colorSpaceOut->isThreeComponentMatrix()) { + if (!colorSpaceIn->isThreeComponentMatrix()) + c = colorMatrix.map(c); + c.x = std::clamp(c.x, 0.0f, 1.0f); + c.y = std::clamp(c.y, 0.0f, 1.0f); + c.z = std::clamp(c.z, 0.0f, 1.0f); + if (colorSpaceOut->lut.generated.loadAcquire()) { + c.x = colorSpaceOut->lut[0]->fromLinear(c.x); + c.y = colorSpaceOut->lut[1]->fromLinear(c.y); + c.z = colorSpaceOut->lut[2]->fromLinear(c.z); + } else { + c.x = colorSpaceOut->trc[0].applyInverse(c.x); + c.y = colorSpaceOut->trc[1].applyInverse(c.y); + c.z = colorSpaceOut->trc[2].applyInverse(c.z); + } + } else { + // Do element based conversion + for (auto &&element : colorSpaceOut->mBA) + std::visit([&c](auto &&elm) { visitElement(elm, &c, 1); }, element); + } + return c; +} + +/*! + \internal +*/ +QColorVector QColorTransformPrivate::mapExtended(QColorVector c) const +{ + if (colorSpaceIn->isThreeComponentMatrix()) { + c.x = colorSpaceIn->trc[0].applyExtended(c.x); + c.y = colorSpaceIn->trc[1].applyExtended(c.y); + c.z = colorSpaceIn->trc[2].applyExtended(c.z); + c = colorMatrix.map(c); + } else { + // Do element based conversion + for (auto &&element : colorSpaceIn->mAB) + std::visit([&c](auto &&elm) { visitElement(elm, &c, 1); }, element); + } + + // Match Profile Connection Spaces (PCS): + if (colorSpaceOut->isPcsLab && !colorSpaceIn->isPcsLab) + c = c.xyzToLab(); + else if (colorSpaceIn->isPcsLab && !colorSpaceOut->isPcsLab) + c = c.labToXyz(); + + if (colorSpaceOut->isThreeComponentMatrix()) { + if (!colorSpaceIn->isThreeComponentMatrix()) + c = colorMatrix.map(c); + c.x = colorSpaceOut->trc[0].applyInverseExtended(c.x); + c.y = colorSpaceOut->trc[1].applyInverseExtended(c.y); + c.z = colorSpaceOut->trc[2].applyInverseExtended(c.z); + } else { + // Do element based conversion + for (auto &&element : colorSpaceOut->mBA) + std::visit([&c](auto &&elm) { visitElement(elm, &c, 1); }, element); + } + return c; +} + +template +void QColorTransformPrivate::applyConvertIn(const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const +{ + if (colorSpaceIn->isThreeComponentMatrix()) { + if (flags & InputPremultiplied) + loadPremultiplied(buffer, src, len, this); + else + loadUnpremultiplied(buffer, src, len, this); + + if (!colorSpaceOut->isThreeComponentMatrix()) + applyMatrix(buffer, len, colorMatrix); // colorMatrix should have the first half only. + } else { + if (flags & InputPremultiplied) + loadPremultipliedLUT(buffer, src, len); + else + loadUnpremultipliedLUT(buffer, src, len); + + // Do element based conversion + for (auto &&element : colorSpaceIn->mAB) + std::visit([&buffer, len](auto &&elm) { visitElement(elm, buffer, len); }, element); + } +} + +template +void QColorTransformPrivate::applyConvertOut(T *dst, const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const +{ + if (colorSpaceOut->isThreeComponentMatrix()) { + applyMatrix(buffer, len, colorMatrix); // colorMatrix should have the latter half only. + + if (flags & InputOpaque) + storeOpaque(dst, src, buffer, len, this); + else if (flags & OutputPremultiplied) + storePremultiplied(dst, src, buffer, len, this); + else + storeUnpremultiplied(dst, src, buffer, len, this); + } else { + // Do element based conversion + for (auto &&element : colorSpaceOut->mBA) + std::visit([&buffer, len](auto &&elm) { visitElement(elm, buffer, len); }, element); + + for (qsizetype j = 0; j < len; ++j) { + buffer[j].x = std::clamp(buffer[j].x, 0.f, 1.f); + buffer[j].y = std::clamp(buffer[j].y, 0.f, 1.f); + buffer[j].z = std::clamp(buffer[j].z, 0.f, 1.f); + } + + if (flags & OutputPremultiplied) + storePremultipliedLUT(dst, src, buffer, len); + else + storeUnpremultipliedLUT(dst, src, buffer, len); + } +} + +template +void QColorTransformPrivate::applyElementListTransform(T *dst, const T *src, qsizetype count, TransformFlags flags) const +{ + Q_ASSERT(!colorSpaceIn->isThreeComponentMatrix() || !colorSpaceOut->isThreeComponentMatrix()); + + if (!colorMatrix.isValid()) + return; + + if (colorSpaceIn->isThreeComponentMatrix()) + updateLutsIn(); + if (colorSpaceOut->isThreeComponentMatrix()) + updateLutsOut(); + + QUninitialized buffer; + qsizetype i = 0; + while (i < count) { + const qsizetype len = qMin(count - i, WorkBlockSize); + + applyConvertIn(src + i, buffer, len, flags); + + // Match Profile Connection Spaces (PCS): + if (colorSpaceOut->isPcsLab && !colorSpaceIn->isPcsLab) { + for (qsizetype j = 0; j < len; ++j) + buffer[j] = buffer[j].xyzToLab(); + } else if (colorSpaceIn->isPcsLab && !colorSpaceOut->isPcsLab) { + for (qsizetype j = 0; j < len; ++j) + buffer[j] = buffer[j].labToXyz(); + } + + applyConvertOut(dst + i, src + i, buffer, len, flags); + + i += len; + } +} + +template +void QColorTransformPrivate::applyThreeComponentMatrix(T *dst, const T *src, qsizetype count, TransformFlags flags) const +{ + Q_ASSERT(colorSpaceIn->isThreeComponentMatrix() && colorSpaceOut->isThreeComponentMatrix()); + if (!colorMatrix.isValid()) return; @@ -1228,10 +1538,9 @@ void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, Transf updateLutsOut(); bool doApplyMatrix = !colorMatrix.isIdentity(); - constexpr bool DoClip = !std::is_same_v && !std::is_same_v; + constexpr ApplyMatrixForm doClamp = (std::is_same_v || std::is_same_v) ? DoNotClamp : DoClamp; QUninitialized buffer; - qsizetype i = 0; while (i < count) { const qsizetype len = qMin(count - i, WorkBlockSize); @@ -1241,7 +1550,7 @@ void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, Transf loadUnpremultiplied(buffer, src + i, len, this); if (doApplyMatrix) - applyMatrix(buffer, len, colorMatrix); + applyMatrix(buffer, len, colorMatrix); if (flags & InputOpaque) storeOpaque(dst + i, src + i, buffer, len, this); @@ -1254,6 +1563,15 @@ void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, Transf } } +template +void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const +{ + if (colorSpaceIn->isThreeComponentMatrix() && colorSpaceOut->isThreeComponentMatrix()) + applyThreeComponentMatrix(dst, src, count, flags); + else + applyElementListTransform(dst, src, count, flags); +} + /*! \internal Is to be called on a color-transform to XYZ, returns only luminance values. @@ -1262,6 +1580,28 @@ void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, Transf template void QColorTransformPrivate::applyReturnGray(D *dst, const S *src, qsizetype count, TransformFlags flags) const { + if (!colorSpaceIn->isThreeComponentMatrix()) { + QUninitialized buffer; + + qsizetype i = 0; + while (i < count) { + const qsizetype len = qMin(count - i, WorkBlockSize); + if (flags & InputPremultiplied) + loadPremultipliedLUT(buffer, src + i, len); + else + loadUnpremultipliedLUT(buffer, src + i, len); + + // Do element based conversion + for (auto &&element : colorSpaceIn->mAB) + std::visit([&](auto &&elm) { visitElement(elm, buffer, len); }, element); + + storeGray(dst + i, src + i, buffer, len, this); + + i += len; + } + return; + } + if (!colorMatrix.isValid()) return; @@ -1278,7 +1618,7 @@ void QColorTransformPrivate::applyReturnGray(D *dst, const S *src, qsizetype cou else loadUnpremultiplied(buffer, src + i, len, this); - applyMatrix(buffer, len, colorMatrix); + applyMatrix(buffer, len, colorMatrix); storeGray(dst + i, src + i, buffer, len, this); @@ -1367,14 +1707,29 @@ void QColorTransformPrivate::apply(QRgbaFloat32 *dst, const QRgbaFloat32 *src, q template void QColorTransformPrivate::applyReturnGray(quint8 *dst, const QRgb *src, qsizetype count, TransformFlags flags) const; template void QColorTransformPrivate::applyReturnGray(quint16 *dst, const QRgba64 *src, qsizetype count, TransformFlags flags) const; +bool QColorTransformPrivate::isThreeComponentMatrix() const +{ + if (colorSpaceIn && !colorSpaceIn->isThreeComponentMatrix()) + return false; + if (colorSpaceOut && !colorSpaceOut->isThreeComponentMatrix()) + return false; + return true; +} + /*! \internal */ bool QColorTransformPrivate::isIdentity() const { + if (colorSpaceIn == colorSpaceOut) + return true; if (!colorMatrix.isIdentity()) return false; if (colorSpaceIn && colorSpaceOut) { + if (colorSpaceIn->equals(colorSpaceOut.constData())) + return true; + if (!isThreeComponentMatrix()) + return false; if (colorSpaceIn->transferFunction != colorSpaceOut->transferFunction) return false; if (colorSpaceIn->transferFunction == QColorSpace::TransferFunction::Custom) { @@ -1383,6 +1738,8 @@ bool QColorTransformPrivate::isIdentity() const && colorSpaceIn->trc[2] == colorSpaceOut->trc[2]; } } else { + if (!isThreeComponentMatrix()) + return false; if (colorSpaceIn && colorSpaceIn->transferFunction != QColorSpace::TransferFunction::Linear) return false; if (colorSpaceOut && colorSpaceOut->transferFunction != QColorSpace::TransferFunction::Linear) diff --git a/src/gui/painting/qcolortransform_p.h b/src/gui/painting/qcolortransform_p.h index 1361060b738..1d54aced1b8 100644 --- a/src/gui/painting/qcolortransform_p.h +++ b/src/gui/painting/qcolortransform_p.h @@ -36,6 +36,7 @@ public: void updateLutsIn() const; void updateLutsOut() const; bool isIdentity() const; + bool isThreeComponentMatrix() const; Q_GUI_EXPORT void prepare(); enum TransformFlag { @@ -47,6 +48,9 @@ public: }; Q_DECLARE_FLAGS(TransformFlags, TransformFlag) + QColorVector map(QColorVector color) const; + QColorVector mapExtended(QColorVector color) const; + 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; void apply(QRgbaFloat32 *dst, const QRgbaFloat32 *src, qsizetype count, @@ -55,6 +59,15 @@ public: template void apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const; + template + void applyConvertIn(const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const; + template + void applyConvertOut(T *dst, const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const; + template + void applyElementListTransform(T *dst, const T *src, qsizetype count, TransformFlags flags) const; + template + void applyThreeComponentMatrix(T *dst, const T *src, qsizetype count, TransformFlags flags) const; + template void applyReturnGray(D *dst, const S *src, qsizetype count, TransformFlags flags) const; diff --git a/src/gui/painting/qcolortrc_p.h b/src/gui/painting/qcolortrc_p.h index 6f9030fa3a7..f86d9e86218 100644 --- a/src/gui/painting/qcolortrc_p.h +++ b/src/gui/painting/qcolortrc_p.h @@ -21,8 +21,7 @@ QT_BEGIN_NAMESPACE - -// Defines an ICC TRC (Tone Reproduction Curve) +// Defines a TRC (Tone Reproduction Curve) class Q_GUI_EXPORT QColorTrc { public: @@ -41,9 +40,8 @@ public: bool isIdentity() const { - return m_type == Type::Uninitialized - || (m_type == Type::Function && m_fun.isIdentity()) - || (m_type == Type::Table && m_table.isIdentity()); + return (m_type == Type::Function && m_fun.isIdentity()) + || (m_type == Type::Table && m_table.isIdentity()); } bool isValid() const { diff --git a/src/gui/painting/qicc.cpp b/src/gui/painting/qicc.cpp index d0bfa7eaa3d..48555702617 100644 --- a/src/gui/painting/qicc.cpp +++ b/src/gui/painting/qicc.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2020 The Qt Company Ltd. +// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qicc_p.h" @@ -12,6 +12,8 @@ #include #include +#include "qcolorclut_p.h" +#include "qcolormatrix_p.h" #include "qcolorspace_p.h" #include "qcolortrc_p.h" @@ -60,18 +62,23 @@ constexpr quint32 IccTag(uchar a, uchar b, uchar c, uchar d) enum class ColorSpaceType : quint32 { Rgb = IccTag('R', 'G', 'B', ' '), Gray = IccTag('G', 'R', 'A', 'Y'), + Cmyk = IccTag('C', 'M', 'Y', 'K'), }; enum class ProfileClass : quint32 { Input = IccTag('s', 'c', 'n', 'r'), Display = IccTag('m', 'n', 't', 'r'), - // Not supported: Output = IccTag('p', 'r', 't', 'r'), ColorSpace = IccTag('s', 'p', 'a', 'c'), + // Not supported: + DeviceLink = IccTag('l', 'i', 'n', 'k'), + Abstract = IccTag('a', 'b', 's', 't'), + NamedColor = IccTag('n', 'm', 'c', 'l'), }; enum class Tag : quint32 { acsp = IccTag('a', 'c', 's', 'p'), + Lab_ = IccTag('L', 'a', 'b', ' '), RGB_ = IccTag('R', 'G', 'B', ' '), XYZ_ = IccTag('X', 'Y', 'Z', ' '), rXYZ = IccTag('r', 'X', 'Y', 'Z'), @@ -83,8 +90,18 @@ enum class Tag : quint32 { kTRC = IccTag('k', 'T', 'R', 'C'), A2B0 = IccTag('A', '2', 'B', '0'), A2B1 = IccTag('A', '2', 'B', '1'), + A2B2 = IccTag('A', '2', 'B', '2'), B2A0 = IccTag('B', '2', 'A', '0'), B2A1 = IccTag('B', '2', 'A', '1'), + B2A2 = IccTag('B', '2', 'A', '2'), + B2D0 = IccTag('B', '2', 'D', '0'), + B2D1 = IccTag('B', '2', 'D', '1'), + B2D2 = IccTag('B', '2', 'D', '2'), + B2D3 = IccTag('B', '2', 'D', '3'), + D2B0 = IccTag('D', '2', 'B', '0'), + D2B1 = IccTag('D', '2', 'B', '1'), + D2B2 = IccTag('D', '2', 'B', '2'), + D2B3 = IccTag('D', '2', 'B', '3'), desc = IccTag('d', 'e', 's', 'c'), text = IccTag('t', 'e', 'x', 't'), cprt = IccTag('c', 'p', 'r', 't'), @@ -95,9 +112,11 @@ enum class Tag : quint32 { mft1 = IccTag('m', 'f', 't', '1'), mft2 = IccTag('m', 'f', 't', '2'), mluc = IccTag('m', 'l', 'u', 'c'), + mpet = IccTag('m', 'p', 'e', 't'), mAB_ = IccTag('m', 'A', 'B', ' '), mBA_ = IccTag('m', 'B', 'A', ' '), chad = IccTag('c', 'h', 'a', 'd'), + gamt = IccTag('g', 'a', 'm', 't'), sf32 = IccTag('s', 'f', '3', '2'), // Apple extensions for ICCv2: @@ -163,6 +182,46 @@ struct MlucTagData : GenericTagData { MlucTagRecord records[1]; }; +struct Lut8TagData : GenericTagData { + quint8 inputChannels; + quint8 outputChannels; + quint8 clutGridPoints; + quint8 padding; + qint32_be e1; + qint32_be e2; + qint32_be e3; + qint32_be e4; + qint32_be e5; + qint32_be e6; + qint32_be e7; + qint32_be e8; + qint32_be e9; + // followed by parameter values: quint8[inputChannels * 256]; + // followed by parameter values: quint8[outputChannels * clutGridPoints^inputChannels]; + // followed by parameter values: quint8[outputChannels * 256]; +}; + +struct Lut16TagData : GenericTagData { + quint8 inputChannels; + quint8 outputChannels; + quint8 clutGridPoints; + quint8 padding; + qint32_be e1; + qint32_be e2; + qint32_be e3; + qint32_be e4; + qint32_be e5; + qint32_be e6; + qint32_be e7; + qint32_be e8; + qint32_be e9; + quint16_be inputTableEntries; + quint16_be outputTableEntries; + // followed by parameter values: quint16_be[inputChannels * inputTableEntries]; + // followed by parameter values: quint16_be[outputChannels * clutGridPoints^inputChannels]; + // followed by parameter values: quint16_be[outputChannels * outputTableEntries]; +}; + // For both mAB and mBA struct mABTagData : GenericTagData { quint8 inputChannels; @@ -173,12 +232,36 @@ struct mABTagData : GenericTagData { quint32_be mCurvesOffset; quint32_be clutOffset; quint32_be aCurvesOffset; + // followed by embedded data for the offsets above +}; + +struct mpetTagData : GenericTagData { + quint16_be inputChannels; + quint16_be outputChannels; + quint32_be processingElements; + // element offset table + // element data }; struct Sf32TagData : GenericTagData { quint32_be value[1]; }; +struct MatrixElement { + qint32_be e0; + qint32_be e1; + qint32_be e2; + qint32_be e3; + qint32_be e4; + qint32_be e5; + qint32_be e6; + qint32_be e7; + qint32_be e8; + qint32_be e9; + qint32_be e10; + qint32_be e11; +}; + static int toFixedS1516(float x) { return int(x * 65536.0f + 0.5f); @@ -208,8 +291,8 @@ static bool isValidIccProfile(const ICCProfileHeader &header) if (header.profileClass != uint(ProfileClass::Input) && header.profileClass != uint(ProfileClass::Display) - && (header.profileClass != uint(ProfileClass::Output) - || header.inputColorSpace != uint(ColorSpaceType::Gray))) { + && header.profileClass != uint(ProfileClass::Output) + && header.profileClass != uint(ProfileClass::ColorSpace)) { qCInfo(lcIcc, "Unsupported ICC profile class 0x%x", quint32(header.profileClass)); return false; } @@ -218,8 +301,7 @@ static bool isValidIccProfile(const ICCProfileHeader &header) qCInfo(lcIcc, "Unsupported ICC input color space 0x%x", quint32(header.inputColorSpace)); return false; } - if (header.pcs != 0x58595a20 /* 'XYZ '*/) { - // ### support PCSLAB + if (header.pcs != uint(Tag::XYZ_) && header.pcs != uint(Tag::Lab_)) { qCInfo(lcIcc, "Unsupported ICC profile connection space 0x%x", quint32(header.pcs)); return false; } @@ -278,6 +360,10 @@ static int writeColorTrc(QDataStream &stream, const QColorTrc &trc) stream << ushort(trc.m_table.m_table8[i] * 257U); } } + if (trc.m_table.m_tableSize & 1) { + stream << ushort(0); + return 12 + 2 * trc.m_table.m_tableSize + 2; + } return 12 + 2 * trc.m_table.m_tableSize; } @@ -287,6 +373,10 @@ QByteArray toIccProfile(const QColorSpace &space) return QByteArray(); const QColorSpacePrivate *spaceDPtr = QColorSpacePrivate::get(space); + // This should catch anything not three component matrix based as we can only get that from parsed ICC + if (!spaceDPtr->iccProfile.isEmpty()) + return spaceDPtr->iccProfile; + Q_ASSERT(spaceDPtr->isThreeComponentMatrix()); constexpr int tagCount = 9; constexpr uint profileDataOffset = 128 + 4 + 12 * tagCount; @@ -306,7 +396,7 @@ QByteArray toIccProfile(const QColorSpace &space) 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 << (spaceDPtr->isPcsLab ? uint(Tag::Lab_) : uint(Tag::XYZ_)); stream << uint(0) << uint(0) << uint(0); stream << uint(Tag::acsp); stream << uint(0) << uint(0) << uint(0); @@ -435,23 +525,22 @@ static bool parseXyzData(const QByteArray &data, const TagEntry &tagEntry, QColo return true; } -static bool parseTRC(const QByteArray &data, const TagEntry &tagEntry, QColorTrc &gamma) +static quint32 parseTRC(const QByteArrayView &tagData, QColorTrc &gamma, QColorTransferTable::Type type = QColorTransferTable::TwoWay) { - const GenericTagData trcData = qFromUnaligned(data.constData() - + tagEntry.offset); + const GenericTagData trcData = qFromUnaligned(tagData.constData()); if (trcData.type == quint32(Tag::curv)) { Q_STATIC_ASSERT(sizeof(CurvTagData) == 12); - const CurvTagData curv = qFromUnaligned(data.constData() + tagEntry.offset); + const CurvTagData curv = qFromUnaligned(tagData.constData()); if (curv.valueCount > (1 << 16)) - return false; - if (tagEntry.size - 12 < 2 * curv.valueCount) - return false; - const auto valueOffset = tagEntry.offset + sizeof(CurvTagData); + return 0; + if (tagData.size() < qsizetype(12 + 2 * curv.valueCount)) + return 0; + const auto valueOffset = sizeof(CurvTagData); if (curv.valueCount == 0) { gamma.m_type = QColorTrc::Type::Function; gamma.m_fun = QColorTransferFunction(); // Linear } else if (curv.valueCount == 1) { - const quint16 v = qFromBigEndian(data.constData() + valueOffset); + const quint16 v = qFromBigEndian(tagData.constData() + valueOffset); gamma.m_type = QColorTrc::Type::Function; gamma.m_fun = QColorTransferFunction::fromGamma(v * (1.0f / 256.0f)); } else { @@ -459,12 +548,12 @@ static bool parseTRC(const QByteArray &data, const TagEntry &tagEntry, QColorTrc tabl.resize(curv.valueCount); static_assert(sizeof(GenericTagData) == 2 * sizeof(quint32_be), "GenericTagData has padding. The following code is a subject to UB."); - qFromBigEndian(data.constData() + valueOffset, curv.valueCount, tabl.data()); - QColorTransferTable table = QColorTransferTable(curv.valueCount, std::move(tabl)); + qFromBigEndian(tagData.constData() + valueOffset, curv.valueCount, tabl.data()); + QColorTransferTable table = QColorTransferTable(curv.valueCount, std::move(tabl), type); QColorTransferFunction curve; if (!table.checkValidity()) { qCWarning(lcIcc) << "Invalid curv table"; - return false; + return 0; } else if (!table.asColorTransferFunction(&curve)) { gamma.m_type = QColorTrc::Type::Table; gamma.m_table = table; @@ -474,43 +563,43 @@ static bool parseTRC(const QByteArray &data, const TagEntry &tagEntry, QColorTrc gamma.m_fun = curve; } } - return true; + return 12 + 2 * curv.valueCount; } if (trcData.type == quint32(Tag::para)) { Q_STATIC_ASSERT(sizeof(ParaTagData) == 12); - const ParaTagData para = qFromUnaligned(data.constData() + tagEntry.offset); - const auto parametersOffset = tagEntry.offset + sizeof(ParaTagData); + const ParaTagData para = qFromUnaligned(tagData.constData()); + const auto parametersOffset = sizeof(ParaTagData); quint32 parameters[7]; switch (para.curveType) { case 0: { - if (tagEntry.size < sizeof(ParaTagData) + 1 * 4) - return false; - qFromBigEndian(data.constData() + parametersOffset, 1, parameters); + if (tagData.size() < 12 + 1 * 4) + return 0; + qFromBigEndian(tagData.constData() + parametersOffset, 1, parameters); float g = fromFixedS1516(parameters[0]); gamma.m_type = QColorTrc::Type::Function; gamma.m_fun = QColorTransferFunction::fromGamma(g); - break; + return 12 + 1 * 4; } case 1: { - if (tagEntry.size < sizeof(ParaTagData) + 3 * 4) - return false; - qFromBigEndian(data.constData() + parametersOffset, 3, parameters); + if (tagData.size() < 12 + 3 * 4) + return 0; + qFromBigEndian(tagData.constData() + parametersOffset, 3, parameters); if (parameters[1] == 0) - return false; + return 0; float g = fromFixedS1516(parameters[0]); float a = fromFixedS1516(parameters[1]); float b = fromFixedS1516(parameters[2]); float d = -b / a; gamma.m_type = QColorTrc::Type::Function; gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, 0.0f, 0.0f, g); - break; + return 12 + 3 * 4; } case 2: { - if (tagEntry.size < sizeof(ParaTagData) + 4 * 4) - return false; - qFromBigEndian(data.constData() + parametersOffset, 4, parameters); + if (tagData.size() < 12 + 4 * 4) + return 0; + qFromBigEndian(tagData.constData() + parametersOffset, 4, parameters); if (parameters[1] == 0) - return false; + return 0; float g = fromFixedS1516(parameters[0]); float a = fromFixedS1516(parameters[1]); float b = fromFixedS1516(parameters[2]); @@ -518,12 +607,12 @@ static bool parseTRC(const QByteArray &data, const TagEntry &tagEntry, QColorTrc float d = -b / a; gamma.m_type = QColorTrc::Type::Function; gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, c, c, g); - break; + return 12 + 4 * 4; } case 3: { - if (tagEntry.size < sizeof(ParaTagData) + 5 * 4) - return false; - qFromBigEndian(data.constData() + parametersOffset, 5, parameters); + if (tagData.size() < 12 + 5 * 4) + return 0; + qFromBigEndian(tagData.constData() + parametersOffset, 5, parameters); float g = fromFixedS1516(parameters[0]); float a = fromFixedS1516(parameters[1]); float b = fromFixedS1516(parameters[2]); @@ -531,12 +620,12 @@ static bool parseTRC(const QByteArray &data, const TagEntry &tagEntry, QColorTrc float d = fromFixedS1516(parameters[4]); gamma.m_type = QColorTrc::Type::Function; gamma.m_fun = QColorTransferFunction(a, b, c, d, 0.0f, 0.0f, g); - break; + return 12 + 5 * 4; } case 4: { - if (tagEntry.size < sizeof(ParaTagData) + 7 * 4) - return false; - qFromBigEndian(data.constData() + parametersOffset, 7, parameters); + if (tagData.size() < 12 + 7 * 4) + return 0; + qFromBigEndian(tagData.constData() + parametersOffset, 7, parameters); float g = fromFixedS1516(parameters[0]); float a = fromFixedS1516(parameters[1]); float b = fromFixedS1516(parameters[2]); @@ -546,15 +635,365 @@ static bool parseTRC(const QByteArray &data, const TagEntry &tagEntry, QColorTrc float f = fromFixedS1516(parameters[6]); gamma.m_type = QColorTrc::Type::Function; gamma.m_fun = QColorTransferFunction(a, b, c, d, e, f, g); - break; + return 12 + 7 * 4; } default: qCWarning(lcIcc) << "Unknown para type" << uint(para.curveType); - return false; + return 0; } return true; } - qCWarning(lcIcc) << "Invalid TRC data type"; + qCWarning(lcIcc) << "Invalid TRC data type" << Qt::hex << trcData.type; + return 0; +} + +template +static void parseCLUT(const T *tableData, const float f, QColorCLUT *clut) +{ + for (qsizetype index = 0; index < clut->table.size(); ++index) { + QColorVector v(tableData[index * 3 + 0] * f, + tableData[index * 3 + 1] * f, + tableData[index * 3 + 2] * f); + clut->table[index] = v; + } +} + +// very simple version for small values (<=4) of exp. +static constexpr qsizetype intPow(qsizetype x, qsizetype exp) +{ + return (exp <= 1) ? x : x * intPow(x, exp - 1); +} + +// Parses lut8 and lut16 type elements +template +static bool parseLutData(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *colorSpacePrivate, bool isAb) +{ + if (tagEntry.size < sizeof(T)) { + qCWarning(lcIcc) << "Undersized lut8/lut16 tag"; + return false; + } + using S = std::conditional_t, uint8_t, uint16_t>; + const T lut = qFromUnaligned(data.constData() + tagEntry.offset); + int inputTableEntries, outputTableEntries, precision; + if constexpr (std::is_same_v) { + Q_ASSERT(lut.type == quint32(Tag::mft1)); + if (!colorSpacePrivate->isPcsLab && isAb) { + qCWarning(lcIcc) << "Lut8 can not output XYZ values"; + return false; + } + inputTableEntries = 256; + outputTableEntries = 256; + precision = 1; + } else { + Q_ASSERT(lut.type == quint32(Tag::mft2)); + inputTableEntries = lut.inputTableEntries; + outputTableEntries = lut.outputTableEntries; + precision = 2; + } + + bool inTableIsLinear = true, outTableIsLinear = true; + QColorSpacePrivate::TransferElement inTableElement; + QColorSpacePrivate::TransferElement outTableElement; + QColorCLUT clutElement; + QColorMatrix matrixElement; + + matrixElement.r.x = fromFixedS1516(lut.e1); + matrixElement.g.x = fromFixedS1516(lut.e2); + matrixElement.b.x = fromFixedS1516(lut.e3); + matrixElement.r.y = fromFixedS1516(lut.e4); + matrixElement.g.y = fromFixedS1516(lut.e5); + matrixElement.b.y = fromFixedS1516(lut.e6); + matrixElement.r.z = fromFixedS1516(lut.e7); + matrixElement.g.z = fromFixedS1516(lut.e8); + matrixElement.b.z = fromFixedS1516(lut.e9); + if (!colorSpacePrivate->isPcsLab && !isAb && !matrixElement.isValid()) { + qCWarning(lcIcc) << "Invalid matrix values in lut8/lut16"; + return false; + } + + if (lut.inputChannels != 3) { + qCWarning(lcIcc) << "Unsupported lut8/lut16 input channel count" << lut.inputChannels; + return false; + } + + if (lut.outputChannels != 3) { + qCWarning(lcIcc) << "Unsupported lut8/lut16 output channel count" << lut.outputChannels; + return false; + } + + const qsizetype clutTableSize = intPow(lut.clutGridPoints, lut.inputChannels); + if (tagEntry.size < (sizeof(T) + precision * lut.inputChannels * inputTableEntries + + precision * lut.outputChannels * outputTableEntries + + precision * lut.outputChannels * clutTableSize)) { + qCWarning(lcIcc) << "Undersized lut8/lut16 tag, no room for tables"; + return false; + } + + const uint8_t *tableData = reinterpret_cast(data.constData() + tagEntry.offset + sizeof(T)); + + for (int j = 0; j < lut.inputChannels; ++j) { + QList input(inputTableEntries); + qFromBigEndian(tableData, inputTableEntries, input.data()); + QColorTransferTable table(inputTableEntries, input, QColorTransferTable::OneWay); + if (!table.checkValidity()) { + qCWarning(lcIcc) << "Bad input table in lut8/lut16"; + return false; + } + if (!table.isIdentity()) + inTableIsLinear = false; + inTableElement.trc[j] = std::move(table); + tableData += inputTableEntries * precision; + } + + clutElement.table.resize(clutTableSize); + clutElement.gridPointsX = clutElement.gridPointsY = clutElement.gridPointsZ = lut.clutGridPoints; + if constexpr (std::is_same_v) { + parseCLUT(tableData, 1.f / 255.f, &clutElement); + } else { + float f = 1.0f / 65535.f; + if (colorSpacePrivate->isPcsLab && isAb) // Legacy lut16 conversion to Lab + f = 1.0f / 65280.f; + QList clutTable(clutTableSize * lut.outputChannels); + qFromBigEndian(tableData, clutTable.size(), clutTable.data()); + parseCLUT(clutTable.constData(), f, &clutElement); + } + tableData += clutTableSize * lut.outputChannels * precision; + + for (int j = 0; j < lut.outputChannels; ++j) { + QList output(outputTableEntries); + qFromBigEndian(tableData, outputTableEntries, output.data()); + QColorTransferTable table(outputTableEntries, output, QColorTransferTable::OneWay); + if (!table.checkValidity()) { + qCWarning(lcIcc) << "Bad output table in lut8/lut16"; + return false; + } + if (!table.isIdentity()) + outTableIsLinear = false; + outTableElement.trc[j] = std::move(table); + tableData += outputTableEntries * precision; + } + + if (isAb) { + if (!inTableIsLinear) + colorSpacePrivate->mAB.append(inTableElement); + if (!clutElement.isEmpty()) + colorSpacePrivate->mAB.append(clutElement); + if (!outTableIsLinear) + colorSpacePrivate->mAB.append(outTableElement); + } else { + // The matrix is only to be applied if the input color-space is XYZ + if (!colorSpacePrivate->isPcsLab && !matrixElement.isIdentity()) + colorSpacePrivate->mBA.append(matrixElement); + if (!inTableIsLinear) + colorSpacePrivate->mBA.append(inTableElement); + if (!clutElement.isEmpty()) + colorSpacePrivate->mBA.append(clutElement); + if (!outTableIsLinear) + colorSpacePrivate->mBA.append(outTableElement); + } + return true; +} + +// Parses mAB and mBA type elements +static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *colorSpacePrivate, bool isAb) +{ + if (tagEntry.size < sizeof(mABTagData)) { + qCWarning(lcIcc) << "Undersized mAB/mBA tag"; + return false; + } + if (qsizetype(tagEntry.size) > data.size()) { + qCWarning(lcIcc) << "Truncated mAB/mBA tag"; + return false; + } + const mABTagData mab = qFromUnaligned(data.constData() + tagEntry.offset); + if ((mab.type != quint32(Tag::mAB_) && isAb) || (mab.type != quint32(Tag::mBA_) && !isAb)){ + qCWarning(lcIcc) << "Bad mAB/mBA content type"; + return false; + } + + if (mab.inputChannels != 3) { + qCWarning(lcIcc) << "Unsupported mAB/mBA input channel count" << mab.inputChannels; + return false; + } + + if (mab.outputChannels != 3) { + qCWarning(lcIcc) << "Unsupported mAB/mBA output channel count" << mab.outputChannels; + return false; + } + + // These combinations are legal: B, M + Matrix + B, A + Clut + B, A + Clut + M + Matrix + B + if (!mab.bCurvesOffset) { + qCWarning(lcIcc) << "Illegal mAB/mBA without B table"; + return false; + } + if (((bool)mab.matrixOffset != (bool)mab.mCurvesOffset) || + ((bool)mab.aCurvesOffset != (bool)mab.clutOffset)) { + qCWarning(lcIcc) << "Illegal mAB/mBA element combination"; + return false; + } + + if (mab.aCurvesOffset > (tagEntry.size - 3 * sizeof(GenericTagData)) || + mab.bCurvesOffset > (tagEntry.size - 3 * sizeof(GenericTagData)) || + mab.mCurvesOffset > (tagEntry.size - 3 * sizeof(GenericTagData)) || + mab.matrixOffset > (tagEntry.size - 4 * 12) || + mab.clutOffset > (tagEntry.size - 20)) { + qCWarning(lcIcc) << "Illegal mAB/mBA element offset"; + return false; + } + + QColorSpacePrivate::TransferElement bTableElement; + QColorSpacePrivate::TransferElement aTableElement; + QColorCLUT clutElement; + QColorSpacePrivate::TransferElement mTableElement; + QColorMatrix matrixElement; + QColorVector offsetElement; + + auto parseCurves = [&data, &tagEntry] (uint curvesOffset, QColorTrc *table, int channels) { + for (int i = 0; i < channels; ++i) { + if (qsizetype(tagEntry.offset + curvesOffset + 12) > data.size() || curvesOffset + 12 > tagEntry.size) { + qCWarning(lcIcc) << "Space missing for channel curves in mAB/mBA"; + return false; + } + auto size = parseTRC(QByteArrayView(data).sliced(tagEntry.offset + curvesOffset, tagEntry.size - curvesOffset), table[i], QColorTransferTable::OneWay); + if (!size) + return false; + if (size & 2) size += 2; // possible padding + curvesOffset += size; + } + return true; + }; + + bool bCurvesAreLinear = true, aCurvesAreLinear = true, mCurvesAreLinear = true; + + // B Curves + if (!parseCurves(mab.bCurvesOffset, bTableElement.trc, 3)) { + qCWarning(lcIcc) << "Invalid B curves"; + return false; + } else { + bCurvesAreLinear = bTableElement.trc[0].isIdentity() && bTableElement.trc[1].isIdentity() && bTableElement.trc[2].isIdentity(); + } + + // A Curves + if (mab.aCurvesOffset) { + if (!parseCurves(mab.aCurvesOffset, aTableElement.trc, 3)) { + qCWarning(lcIcc) << "Invalid A curves"; + return false; + } else { + aCurvesAreLinear = aTableElement.trc[0].isIdentity() && aTableElement.trc[1].isIdentity() && aTableElement.trc[2].isIdentity(); + } + } + + // M Curves + if (mab.mCurvesOffset) { + if (!parseCurves(mab.mCurvesOffset, mTableElement.trc, 3)) { + qCWarning(lcIcc) << "Invalid M curves"; + return false; + } else { + mCurvesAreLinear = mTableElement.trc[0].isIdentity() && mTableElement.trc[1].isIdentity() && mTableElement.trc[2].isIdentity(); + } + } + + // Matrix + if (mab.matrixOffset) { + const MatrixElement matrix = qFromUnaligned(data.constData() + tagEntry.offset + mab.matrixOffset); + matrixElement.r.x = fromFixedS1516(matrix.e0); + matrixElement.g.x = fromFixedS1516(matrix.e1); + matrixElement.b.x = fromFixedS1516(matrix.e2); + matrixElement.r.y = fromFixedS1516(matrix.e3); + matrixElement.g.y = fromFixedS1516(matrix.e4); + matrixElement.b.y = fromFixedS1516(matrix.e5); + matrixElement.r.z = fromFixedS1516(matrix.e6); + matrixElement.g.z = fromFixedS1516(matrix.e7); + matrixElement.b.z = fromFixedS1516(matrix.e8); + offsetElement.x = fromFixedS1516(matrix.e9); + offsetElement.y = fromFixedS1516(matrix.e10); + offsetElement.z = fromFixedS1516(matrix.e11); + if (!matrixElement.isValid() || !offsetElement.isValid()) { + qCWarning(lcIcc) << "Invalid matrix values in mAB/mBA element"; + return false; + } + } + + // CLUT + if (mab.clutOffset) { + clutElement.gridPointsX = data[tagEntry.offset + mab.clutOffset]; + clutElement.gridPointsY = data[tagEntry.offset + mab.clutOffset + 1]; + clutElement.gridPointsZ = data[tagEntry.offset + mab.clutOffset + 2]; + const uchar precision = data[tagEntry.offset + mab.clutOffset + 16]; + if (precision > 2 || precision < 1) { + qCWarning(lcIcc) << "Invalid mAB/mBA element CLUT precision"; + return false; + } + if (clutElement.gridPointsX < 2 || clutElement.gridPointsY < 2 || clutElement.gridPointsZ < 2) { + qCWarning(lcIcc) << "Empty CLUT"; + return false; + } + const qsizetype clutTableSize = clutElement.gridPointsX * clutElement.gridPointsY * clutElement.gridPointsZ; + if ((mab.clutOffset + 20 + clutTableSize * mab.outputChannels * precision) > tagEntry.size) { + qCWarning(lcIcc) << "CLUT oversized for tag"; + return false; + } + + clutElement.table.resize(clutTableSize); + if (precision == 2) { + QList clutTable(clutTableSize * mab.outputChannels); + qFromBigEndian(data.constData() + tagEntry.offset + mab.clutOffset + 20, clutTable.size(), clutTable.data()); + parseCLUT(clutTable.constData(), (1.f/65535.f), &clutElement); + } else { + parseCLUT(data.constData() + tagEntry.offset + mab.clutOffset + 20, (1.f/255.f), &clutElement); + } + } + + if (isAb) { + if (mab.aCurvesOffset) { + if (!aCurvesAreLinear) + colorSpacePrivate->mAB.append(std::move(aTableElement)); + if (!clutElement.isEmpty()) + colorSpacePrivate->mAB.append(std::move(clutElement)); + } + if (mab.mCurvesOffset) { + if (!mCurvesAreLinear) + colorSpacePrivate->mAB.append(std::move(mTableElement)); + if (!matrixElement.isIdentity()) + colorSpacePrivate->mAB.append(std::move(matrixElement)); + if (!offsetElement.isNull()) + colorSpacePrivate->mAB.append(std::move(offsetElement)); + } + if (!bCurvesAreLinear) + colorSpacePrivate->mAB.append(std::move(bTableElement)); + } else { + if (!bCurvesAreLinear) + colorSpacePrivate->mBA.append(std::move(bTableElement)); + if (mab.mCurvesOffset) { + if (!matrixElement.isIdentity()) + colorSpacePrivate->mBA.append(std::move(matrixElement)); + if (!offsetElement.isNull()) + colorSpacePrivate->mBA.append(std::move(offsetElement)); + if (!mCurvesAreLinear) + colorSpacePrivate->mBA.append(std::move(mTableElement)); + } + if (mab.aCurvesOffset) { + if (!clutElement.isEmpty()) + colorSpacePrivate->mBA.append(std::move(clutElement)); + if (!aCurvesAreLinear) + colorSpacePrivate->mBA.append(std::move(aTableElement)); + } + } + + return true; +} + +static bool parseA2B(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *privat, bool isAb) +{ + const GenericTagData a2bData = qFromUnaligned(data.constData() + tagEntry.offset); + if (a2bData.type == quint32(Tag::mft1)) + return parseLutData(data, tagEntry, privat, isAb); + else if (a2bData.type == quint32(Tag::mft2)) + return parseLutData(data, tagEntry, privat, isAb); + else if (a2bData.type == quint32(Tag::mAB_) || a2bData.type == quint32(Tag::mBA_)) + return parseMabData(data, tagEntry, privat, isAb); + + qCWarning(lcIcc) << "fromIccProfile: Unknown A2B/B2A data type"; return false; } @@ -616,6 +1055,10 @@ static bool parseRgbMatrix(const QByteArray &data, const QHash &t return false; if (!parseXyzData(data, tagIndex[Tag::wtpt], colorspaceDPtr->whitePoint)) return false; + if (!colorspaceDPtr->toXyz.isValid() || !colorspaceDPtr->whitePoint.isValid() || colorspaceDPtr->whitePoint.isNull()) { + qCWarning(lcIcc) << "Invalid XYZ values in RGB matrix"; + return false; + } colorspaceDPtr->primaries = QColorSpace::Primaries::Custom; if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromSRgb()) { @@ -642,7 +1085,7 @@ static bool parseGrayMatrix(const QByteArray &data, const QHash & QColorVector whitePoint; if (!parseXyzData(data, tagIndex[Tag::wtpt], whitePoint)) return false; - if (!qFuzzyCompare(whitePoint.y, 1.0f) || (1.0f + whitePoint.z + whitePoint.x) == 0.0f) { + if (!whitePoint.isValid() || !qFuzzyCompare(whitePoint.y, 1.0f) || (1.0f + whitePoint.z + whitePoint.x) == 0.0f) { qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - gray white-point not normalized"; return false; } @@ -660,6 +1103,10 @@ static bool parseGrayMatrix(const QByteArray &data, const QHash & return false; } colorspaceDPtr->toXyz = primaries.toXyzMatrix(); + if (!colorspaceDPtr->toXyz.isValid()) { + qCWarning(lcIcc, "fromIccProfile: Invalid ICC profile - invalid white-point(%f, %f)", x, y); + return false; + } } return true; } @@ -686,15 +1133,15 @@ static bool parseTRCs(const QByteArray &data, const QHash &tagInd QColorTrc rCurve; QColorTrc gCurve; QColorTrc bCurve; - if (!parseTRC(data, rTrc, rCurve)) { + if (!parseTRC(QByteArrayView(data).sliced(rTrc.offset, rTrc.size), rCurve, QColorTransferTable::TwoWay)) { qCWarning(lcIcc) << "fromIccProfile: Invalid rTRC"; return false; } - if (!parseTRC(data, gTrc, gCurve)) { + if (!parseTRC(QByteArrayView(data).sliced(gTrc.offset, gTrc.size), gCurve, QColorTransferTable::TwoWay)) { qCWarning(lcIcc) << "fromIccProfile: Invalid gTRC"; return false; } - if (!parseTRC(data, bTrc, bCurve)) { + if (!parseTRC(QByteArrayView(data).sliced(bTrc.offset, bTrc.size), bCurve, QColorTransferTable::TwoWay)) { qCWarning(lcIcc) << "fromIccProfile: Invalid bTRC"; return false; } @@ -704,20 +1151,15 @@ static bool parseTRCs(const QByteArray &data, const QHash &tagInd colorspaceDPtr->trc[0] = QColorTransferFunction(); colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Linear; colorspaceDPtr->gamma = 1.0f; - } else if (rCurve.m_type == QColorTrc::Type::Function) { - 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; - } + } else if (rCurve.m_type == QColorTrc::Type::Function && 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_type == QColorTrc::Type::Function && 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; @@ -789,40 +1231,70 @@ bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace) tagIndex.insert(Tag(quint32(tagTable.signature)), { tagTable.offset, tagTable.size }); } - // Check the profile is three-component matrix based (what we currently support): + bool threeComponentMatrix = true; + if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) { + // Check the profile is three-component matrix based: 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)) { - qCInfo(lcIcc) << "fromIccProfile: Unsupported ICC profile - not three component matrix based"; - return false; + threeComponentMatrix = false; + // Check if the profile is valid n-LUT based: + if (!tagIndex.contains(Tag::A2B0)) { + qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - neither valid three component nor LUT"; + return false; + } } - } else { - Q_ASSERT(header.inputColorSpace == uint(ColorSpaceType::Gray)); + } else if (header.inputColorSpace == uint(ColorSpaceType::Gray)) { if (!tagIndex.contains(Tag::kTRC) || !tagIndex.contains(Tag::wtpt)) { qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - not valid gray scale based"; return false; } + } else { + Q_UNREACHABLE(); } colorSpace->detach(); QColorSpacePrivate *colorspaceDPtr = QColorSpacePrivate::get(*colorSpace); - if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) { - if (!parseRgbMatrix(data, tagIndex, colorspaceDPtr)) + if (threeComponentMatrix) { + colorspaceDPtr->isPcsLab = false; + colorspaceDPtr->transformModel = QColorSpace::TransformModel::ThreeComponentMatrix; + + if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) { + if (!parseRgbMatrix(data, tagIndex, colorspaceDPtr)) + return false; + } else if (header.inputColorSpace == uint(ColorSpaceType::Gray)) { + if (!parseGrayMatrix(data, tagIndex, colorspaceDPtr)) + return false; + } else { + Q_UNREACHABLE(); + } + + // Reset the matrix to our canonical values: + if (colorspaceDPtr->primaries != QColorSpace::Primaries::Custom) + colorspaceDPtr->setToXyzMatrix(); + + if (!parseTRCs(data, tagIndex, colorspaceDPtr, header.inputColorSpace == uint(ColorSpaceType::Gray))) return false; } else { - if (!parseGrayMatrix(data, tagIndex, colorspaceDPtr)) + colorspaceDPtr->isPcsLab = (header.pcs == uint(Tag::Lab_)); + colorspaceDPtr->transformModel = QColorSpace::TransformModel::ElementListProcessing; + + // Only parse the default perceptual transform for now + if (!parseA2B(data, tagIndex[Tag::A2B0], colorspaceDPtr, true)) return false; + if (tagIndex.contains(Tag::B2A0)) { + if (!parseA2B(data, tagIndex[Tag::B2A0], colorspaceDPtr, false)) + return false; + } + + if (tagIndex.contains(Tag::wtpt)) { + if (!parseXyzData(data, tagIndex[Tag::wtpt], colorspaceDPtr->whitePoint)) + return false; + } } - // Reset the matrix to our canonical values: - if (colorspaceDPtr->primaries != QColorSpace::Primaries::Custom) - colorspaceDPtr->setToXyzMatrix(); - - if (!parseTRCs(data, tagIndex, colorspaceDPtr, header.inputColorSpace == uint(ColorSpaceType::Gray))) - return false; - if (tagIndex.contains(Tag::desc)) { if (!parseDesc(data, tagIndex[Tag::desc], colorspaceDPtr->description)) qCWarning(lcIcc) << "fromIccProfile: Failed to parse description"; diff --git a/tests/auto/gui/painting/qcolorspace/resources/VideoHD.icc b/tests/auto/gui/painting/qcolorspace/resources/VideoHD.icc new file mode 100644 index 00000000000..b96eb68136e Binary files /dev/null and b/tests/auto/gui/painting/qcolorspace/resources/VideoHD.icc differ diff --git a/tests/auto/gui/painting/qcolorspace/resources/sRGB_ICC_v4_Appearance.icc b/tests/auto/gui/painting/qcolorspace/resources/sRGB_ICC_v4_Appearance.icc new file mode 100644 index 00000000000..30da9509078 Binary files /dev/null and b/tests/auto/gui/painting/qcolorspace/resources/sRGB_ICC_v4_Appearance.icc differ diff --git a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp index d27babb14ab..7ee248bbad6 100644 --- a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp +++ b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp @@ -42,6 +42,8 @@ private slots: void imageConversionOverLargerGamut(); void imageConversionOverLargerGamut2_data(); void imageConversionOverLargerGamut2(); + void imageConversionOverNonThreeComponentMatrix_data(); + void imageConversionOverNonThreeComponentMatrix(); void loadImage(); @@ -187,6 +189,12 @@ void tst_QColorSpace::fromIccProfile_data() // My monitor's profile: QTest::newRow("HP ZR30w (ICCv4)") << prefix + "HP_ZR30w.icc" << QColorSpace::NamedColorSpace(0) << QColorSpace::TransferFunction::Gamma << QString("HP Z30i"); + // A profile to HD TV + QTest::newRow("VideoHD") << prefix + "VideoHD.icc" << QColorSpace::NamedColorSpace(0) + << QColorSpace::TransferFunction::Custom << QString("HDTV (Rec. 709)"); + // sRGB on PCSLab format + QTest::newRow("sRGB ICCv4 Appearance") << prefix + "sRGB_ICC_v4_Appearance.icc" << QColorSpace::NamedColorSpace(0) + << QColorSpace::TransferFunction::Custom << QString("sRGB_ICC_v4_Appearance.icc"); } void tst_QColorSpace::fromIccProfile() @@ -207,6 +215,11 @@ void tst_QColorSpace::fromIccProfile() QCOMPARE(fileColorSpace.transferFunction(), transferFunction); QCOMPARE(fileColorSpace.description(), description); + + QByteArray iccProfile2 = fileColorSpace.iccProfile(); + QCOMPARE(iccProfile, iccProfile2); + QColorSpace fileColorSpace2 = QColorSpace::fromIccProfile(iccProfile2); + QCOMPARE(fileColorSpace2, fileColorSpace); } void tst_QColorSpace::imageConversion_data() @@ -517,6 +530,63 @@ void tst_QColorSpace::imageConversionOverLargerGamut2() QVERIFY(resultImage.pixelColor(0, 255).greenF() > 1.0f); } +void tst_QColorSpace::imageConversionOverNonThreeComponentMatrix_data() +{ + QTest::addColumn("fromColorSpace"); + QTest::addColumn("toColorSpace"); + + QString prefix = QFINDTESTDATA("resources/"); + QFile file1(prefix + "VideoHD.icc"); + QFile file2(prefix + "sRGB_ICC_v4_Appearance.icc"); + file1.open(QFile::ReadOnly); + file2.open(QFile::ReadOnly); + QByteArray iccProfile1 = file1.readAll(); + QByteArray iccProfile2 = file2.readAll(); + QColorSpace hdtvColorSpace = QColorSpace::fromIccProfile(iccProfile1); + QColorSpace srgbPcsColorSpace = QColorSpace::fromIccProfile(iccProfile2); + + QTest::newRow("sRGB PCSLab -> sRGB") << srgbPcsColorSpace << QColorSpace(QColorSpace::SRgb); + QTest::newRow("sRGB -> sRGB PCSLab") << QColorSpace(QColorSpace::SRgb) << srgbPcsColorSpace; + QTest::newRow("HDTV -> sRGB") << hdtvColorSpace << QColorSpace(QColorSpace::SRgb); + QTest::newRow("sRGB -> HDTV") << QColorSpace(QColorSpace::SRgb) << hdtvColorSpace; + QTest::newRow("sRGB PCSLab -> HDTV") << srgbPcsColorSpace << hdtvColorSpace; + QTest::newRow("HDTV -> sRGB PCSLab") << hdtvColorSpace << srgbPcsColorSpace; +} + +void tst_QColorSpace::imageConversionOverNonThreeComponentMatrix() +{ + QFETCH(QColorSpace, fromColorSpace); + QFETCH(QColorSpace, toColorSpace); + QVERIFY(fromColorSpace.isValid()); + QVERIFY(toColorSpace.isValid()); + + QVERIFY(!fromColorSpace.transformationToColorSpace(toColorSpace).isIdentity()); + + QImage testImage(256, 256, QImage::Format_RGBX64); + testImage.setColorSpace(fromColorSpace); + for (int y = 0; y < 256; ++y) + for (int x = 0; x < 256; ++x) + testImage.setPixel(x, y, qRgb(x, y, 0)); + + QImage resultImage = testImage.convertedToColorSpace(toColorSpace); + for (int y = 0; y < 256; ++y) { + int lastRed = 0; + for (int x = 0; x < 256; ++x) { + QRgb p = resultImage.pixel(x, y); + QVERIFY(qRed(p) >= lastRed); + lastRed = qRed(p); + } + } + for (int x = 0; x < 256; ++x) { + int lastGreen = 0; + for (int y = 0; y < 256; ++y) { + QRgb p = resultImage.pixel(x, y); + QVERIFY(qGreen(p) >= lastGreen); + lastGreen = qGreen(p); + } + } +} + void tst_QColorSpace::loadImage() { QString prefix = QFINDTESTDATA("resources/"); @@ -694,10 +764,28 @@ void tst_QColorSpace::changePrimaries() cs.setPrimaries(QColorSpace::Primaries::DciP3D65); QVERIFY(cs.isValid()); QCOMPARE(cs, QColorSpace(QColorSpace::DisplayP3)); + QCOMPARE(cs.transformModel(), QColorSpace::TransformModel::ThreeComponentMatrix); cs.setTransferFunction(QColorSpace::TransferFunction::Linear); cs.setPrimaries(QPointF(0.3127, 0.3290), QPointF(0.640, 0.330), QPointF(0.3000, 0.6000), QPointF(0.150, 0.060)); QCOMPARE(cs, QColorSpace(QColorSpace::SRgbLinear)); + + + QFile iccFile(QFINDTESTDATA("resources/") + "VideoHD.icc"); + iccFile.open(QFile::ReadOnly); + QByteArray iccData = iccFile.readAll(); + QColorSpace hdtvColorSpace = QColorSpace::fromIccProfile(iccData); + QVERIFY(hdtvColorSpace.isValid()); + QCOMPARE(hdtvColorSpace.transformModel(), QColorSpace::TransformModel::ElementListProcessing); + QCOMPARE(hdtvColorSpace.primaries(), QColorSpace::Primaries::Custom); + QCOMPARE(hdtvColorSpace.transferFunction(), QColorSpace::TransferFunction::Custom); + // Unsets both primaries and transferfunction because they were inseparable in element list processing + hdtvColorSpace.setPrimaries(QColorSpace::Primaries::SRgb); + QVERIFY(!hdtvColorSpace.isValid()); + hdtvColorSpace.setTransferFunction(QColorSpace::TransferFunction::SRgb); + QVERIFY(hdtvColorSpace.isValid()); + QCOMPARE(hdtvColorSpace.transformModel(), QColorSpace::TransformModel::ThreeComponentMatrix); + QCOMPARE(hdtvColorSpace, QColorSpace(QColorSpace::SRgb)); } void tst_QColorSpace::transferFunctionTable() diff --git a/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp b/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp index 85edee41492..765c3324126 100644 --- a/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp +++ b/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp @@ -18,5 +18,18 @@ extern "C" int LLVMFuzzerTestOneInput(const char *data, size_t size) { static char *argv[] = {arg1, arg2, arg3, nullptr}; static QGuiApplication qga(argc, argv); QColorSpace cs = QColorSpace::fromIccProfile(QByteArray::fromRawData(data, size)); + if (cs.isValid()) { + cs.description(); + QColorTransform trans1 = cs.transformationToColorSpace(QColorSpace::SRgb); + trans1.isIdentity(); + QColorSpace cs2 = cs; + cs2.setDescription("Hello"); + bool b = (cs == cs2); + QRgb color = 0xfaf8fa00; + color = trans1.map(color); + QColorTransform trans2 = QColorSpace(QColorSpace::SRgb).transformationToColorSpace(cs); + bool a = (trans1 == trans2); + color = trans2.map(color); + } return 0; }