summaryrefslogtreecommitdiffstats
path: root/src/gui/painting
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2024-02-16 13:52:08 +0100
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2024-03-07 23:42:41 +0100
commit8a6d9ac7bcd3ac3dfba1c9efdb9487ce48b28c9c (patch)
tree34e2f7e0ac76b19b5e8e81594295a4502ef14d1b /src/gui/painting
parent5010eda5345bdbfc12e134d6fb3ae5b7370e2185 (diff)
Add A2B tables, and PCSLab support to QIcc
This finishes off ICC color space support in Qt (short of the optional multi process elements), and includes necessary parts for potential CMYK support (PCSLab and A2B support) [ChangeLog][QColorSpace] ICC profiles that are not three-component matrix based are now supported. Task-number: QTBUG-108175 Change-Id: Ie29b17a366d9ba0ea8135e1752530d6bb19e7520 Reviewed-by: Giuseppe D'Angelo <giuseppe.dangelo@kdab.com>
Diffstat (limited to 'src/gui/painting')
-rw-r--r--src/gui/painting/qcolorclut_p.h80
-rw-r--r--src/gui/painting/qcolormatrix_p.h149
-rw-r--r--src/gui/painting/qcolorspace.cpp271
-rw-r--r--src/gui/painting/qcolorspace.h6
-rw-r--r--src/gui/painting/qcolorspace_p.h22
-rw-r--r--src/gui/painting/qcolortransferfunction_p.h15
-rw-r--r--src/gui/painting/qcolortransfertable_p.h33
-rw-r--r--src/gui/painting/qcolortransform.cpp515
-rw-r--r--src/gui/painting/qcolortransform_p.h13
-rw-r--r--src/gui/painting/qcolortrc_p.h8
-rw-r--r--src/gui/painting/qicc.cpp628
11 files changed, 1488 insertions, 252 deletions
diff --git a/src/gui/painting/qcolorclut_p.h b/src/gui/painting/qcolorclut_p.h
new file mode 100644
index 0000000000..81e002d913
--- /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 <QtCore/qlist.h>
+#include <QtGui/private/qcolormatrix_p.h>
+
+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<QColorVector> 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<qsizetype>(std::floor(x));
+ const qsizetype hix = std::min(lox + 1, gridPointsX - 1);
+ const qsizetype loy = static_cast<qsizetype>(std::floor(y));
+ const qsizetype hiy = std::min(loy + 1, gridPointsY - 1);
+ const qsizetype loz = static_cast<qsizetype>(std::floor(z));
+ const qsizetype hiz = std::min(loz + 1, gridPointsZ - 1);
+ const float fracx = x - static_cast<float>(lox);
+ const float fracy = y - static_cast<float>(loy);
+ const float fracz = z - static_cast<float>(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 1ce5df6264..2d979bc5cb 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 539a294102..7244a8fbf0 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.
@@ -513,6 +539,20 @@ QColorTransform QColorSpacePrivate::transformationToXYZ() const
*/
/*!
+ \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()
Creates a new colorspace object that represents an undefined and invalid colorspace.
@@ -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<uint16_t> &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<uint16_t> &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<uint16_t> &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,7 +891,10 @@ 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);
@@ -847,6 +902,17 @@ void QColorSpace::setPrimaries(const QPointF &whitePoint, const QPointF &redPoin
}
/*!
+ \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
*/
void QColorSpace::detach()
@@ -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<typename T>
+static bool compareElements(const T &element, const QColorSpacePrivate::Element &other)
+{
+ return compareElement(element, std::get<T>(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())
+ if (transformModel != other->transformModel)
+ return false;
+
+ if (!isThreeComponentMatrix()) {
+ if (isPcsLab != other->isPcsLab)
+ return false;
+ 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 (primaries != QColorSpace::Primaries::Custom && other->primaries != QColorSpace::Primaries::Custom) {
+ if (primaries != other->primaries)
return false;
} else {
- if (d_ptr->toXyz != other.d_ptr->toXyz)
+ if (toXyz != other->toXyz)
return false;
}
- if (transferFunction() != QColorSpace::TransferFunction::Custom &&
- other.transferFunction() != QColorSpace::TransferFunction::Custom) {
- if (transferFunction() != other.transferFunction())
+ 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));
+ if (transferFunction == QColorSpace::TransferFunction::Gamma)
+ return (qAbs(gamma - other->gamma) <= (1.0f / 512.0f));
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 (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<QColorSpacePrivate::Element> &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 4fb5c4273f..127f18caa2 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 39d901ecdf..03872ab2c5 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<TransferElement, QColorMatrix, QColorVector, QColorCLUT>;
+ bool isPcsLab = false;
+ // A = device, B = PCS
+ QList<Element> 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 8bbce3ca99..6afbfd25c2 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 20c35cac2d..5b1bd67476 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<uint8_t> &table) noexcept
- : m_tableSize(size), m_table8(table)
+ enum Type : uint8_t {
+ TwoWay = 0,
+ OneWay,
+ };
+ QColorTransferTable() noexcept = default;
+ QColorTransferTable(uint32_t size, const QList<uint8_t> &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<uint16_t> &table) noexcept
- : m_tableSize(size), m_table16(table)
+ QColorTransferTable(uint32_t size, const QList<uint16_t> &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<uint8_t> m_table8;
QList<uint16_t> 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 76d3dfacc8..8d578d7af3 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<bool DoClamp = true>
+enum ApplyMatrixForm {
+ DoNotClamp = 0,
+ DoClamp = 1
+};
+
+template<ApplyMatrixForm doClamp = DoClamp>
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];
};
+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<typename T>
-void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const
+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<DoClamp>(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<typename T>
+void QColorTransformPrivate::applyConvertOut(T *dst, const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const
{
+ if (colorSpaceOut->isThreeComponentMatrix()) {
+ applyMatrix<DoClamp>(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<typename T>
+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<QColorVector, WorkBlockSize> 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<typename T>
+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<T, QRgbaFloat16> && !std::is_same_v<T, QRgbaFloat32>;
+ constexpr ApplyMatrixForm doClamp = (std::is_same_v<T, QRgbaFloat16> || std::is_same_v<T, QRgbaFloat32>) ? DoNotClamp : DoClamp;
QUninitialized<QColorVector, WorkBlockSize> 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<DoClip>(buffer, len, colorMatrix);
+ applyMatrix<doClamp>(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<typename T>
+void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const
+{
+ if (colorSpaceIn->isThreeComponentMatrix() && colorSpaceOut->isThreeComponentMatrix())
+ applyThreeComponentMatrix<T>(dst, src, count, flags);
+ else
+ applyElementListTransform<T>(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<typename D, typename S>
void QColorTransformPrivate::applyReturnGray(D *dst, const S *src, qsizetype count, TransformFlags flags) const
{
+ if (!colorSpaceIn->isThreeComponentMatrix()) {
+ QUninitialized<QColorVector, WorkBlockSize> 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<DoClamp>(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, QRgb>(quint8 *dst, const QRgb *src, qsizetype count, TransformFlags flags) const;
template void QColorTransformPrivate::applyReturnGray<quint16, QRgba64>(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 1361060b73..1d54aced1b 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<typename T>
void apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const;
+ template<typename T>
+ void applyConvertIn(const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const;
+ template<typename T>
+ void applyConvertOut(T *dst, const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const;
+ template<typename T>
+ void applyElementListTransform(T *dst, const T *src, qsizetype count, TransformFlags flags) const;
+ template<typename T>
+ void applyThreeComponentMatrix(T *dst, const T *src, qsizetype count, TransformFlags flags) const;
+
template<typename D, typename S>
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 6f9030fa3a..f86d9e8621 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 d0bfa7eaa3..4855570261 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 <qloggingcategory.h>
#include <qstring.h>
+#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<GenericTagData>(data.constData()
- + tagEntry.offset);
+ const GenericTagData trcData = qFromUnaligned<GenericTagData>(tagData.constData());
if (trcData.type == quint32(Tag::curv)) {
Q_STATIC_ASSERT(sizeof(CurvTagData) == 12);
- const CurvTagData curv = qFromUnaligned<CurvTagData>(data.constData() + tagEntry.offset);
+ const CurvTagData curv = qFromUnaligned<CurvTagData>(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<quint16>(data.constData() + valueOffset);
+ const quint16 v = qFromBigEndian<quint16>(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<quint16>(data.constData() + valueOffset, curv.valueCount, tabl.data());
- QColorTransferTable table = QColorTransferTable(curv.valueCount, std::move(tabl));
+ qFromBigEndian<quint16>(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<ParaTagData>(data.constData() + tagEntry.offset);
- const auto parametersOffset = tagEntry.offset + sizeof(ParaTagData);
+ const ParaTagData para = qFromUnaligned<ParaTagData>(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<quint32>(data.constData() + parametersOffset, 1, parameters);
+ if (tagData.size() < 12 + 1 * 4)
+ return 0;
+ qFromBigEndian<quint32>(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<quint32>(data.constData() + parametersOffset, 3, parameters);
+ if (tagData.size() < 12 + 3 * 4)
+ return 0;
+ qFromBigEndian<quint32>(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<quint32>(data.constData() + parametersOffset, 4, parameters);
+ if (tagData.size() < 12 + 4 * 4)
+ return 0;
+ qFromBigEndian<quint32>(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<quint32>(data.constData() + parametersOffset, 5, parameters);
+ if (tagData.size() < 12 + 5 * 4)
+ return 0;
+ qFromBigEndian<quint32>(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<quint32>(data.constData() + parametersOffset, 7, parameters);
+ if (tagData.size() < 12 + 7 * 4)
+ return 0;
+ qFromBigEndian<quint32>(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 0;
+ }
+ return true;
+ }
+ qCWarning(lcIcc) << "Invalid TRC data type" << Qt::hex << trcData.type;
+ return 0;
+}
+
+template<typename T>
+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<typename T>
+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<std::is_same_v<T, Lut8TagData>, uint8_t, uint16_t>;
+ const T lut = qFromUnaligned<T>(data.constData() + tagEntry.offset);
+ int inputTableEntries, outputTableEntries, precision;
+ if constexpr (std::is_same_v<T, Lut8TagData>) {
+ 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<const uint8_t *>(data.constData() + tagEntry.offset + sizeof(T));
+
+ for (int j = 0; j < lut.inputChannels; ++j) {
+ QList<S> input(inputTableEntries);
+ qFromBigEndian<S>(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<T, Lut8TagData>) {
+ 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<S> clutTable(clutTableSize * lut.outputChannels);
+ qFromBigEndian<S>(tableData, clutTable.size(), clutTable.data());
+ parseCLUT(clutTable.constData(), f, &clutElement);
+ }
+ tableData += clutTableSize * lut.outputChannels * precision;
+
+ for (int j = 0; j < lut.outputChannels; ++j) {
+ QList<S> output(outputTableEntries);
+ qFromBigEndian<S>(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<mABTagData>(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<MatrixElement>(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<uint16_t> clutTable(clutTableSize * mab.outputChannels);
+ qFromBigEndian<uint16_t>(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);
+ }
}
- qCWarning(lcIcc) << "Invalid TRC data type";
+
+ 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<GenericTagData>(data.constData() + tagEntry.offset);
+ if (a2bData.type == quint32(Tag::mft1))
+ return parseLutData<Lut8TagData>(data, tagEntry, privat, isAb);
+ else if (a2bData.type == quint32(Tag::mft2))
+ return parseLutData<Lut16TagData>(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<Tag, TagEntry> &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<Tag, TagEntry> &
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<Tag, TagEntry> &
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<Tag, TagEntry> &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<Tag, TagEntry> &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,39 +1231,69 @@ 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))
- return false;
- }
+ colorspaceDPtr->isPcsLab = (header.pcs == uint(Tag::Lab_));
+ colorspaceDPtr->transformModel = QColorSpace::TransformModel::ElementListProcessing;
- // Reset the matrix to our canonical values:
- if (colorspaceDPtr->primaries != QColorSpace::Primaries::Custom)
- colorspaceDPtr->setToXyzMatrix();
+ // 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 (!parseTRCs(data, tagIndex, colorspaceDPtr, header.inputColorSpace == uint(ColorSpaceType::Gray)))
- return false;
+ if (tagIndex.contains(Tag::wtpt)) {
+ if (!parseXyzData(data, tagIndex[Tag::wtpt], colorspaceDPtr->whitePoint))
+ return false;
+ }
+ }
if (tagIndex.contains(Tag::desc)) {
if (!parseDesc(data, tagIndex[Tag::desc], colorspaceDPtr->description))