KWin
Loading...
Searching...
No Matches
iccprofile.cpp
Go to the documentation of this file.
1/*
2 SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6#include "iccprofile.h"
7#include "colorlut.h"
8#include "colorlut3d.h"
10#include "colortransformation.h"
11#include "utils/common.h"
12
13#include <lcms2.h>
14#include <span>
15#include <tuple>
16
17namespace KWin
18{
19
20IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, BToATagData &&bToATag, const std::shared_ptr<ColorTransformation> &vcgt)
21 : m_handle(handle)
22 , m_colorimetry(colorimetry)
23 , m_bToATag(std::move(bToATag))
24 , m_vcgt(vcgt)
25{
26}
27
28IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, const std::shared_ptr<ColorTransformation> &inverseEOTF, const std::shared_ptr<ColorTransformation> &vcgt)
29 : m_handle(handle)
30 , m_colorimetry(colorimetry)
31 , m_inverseEOTF(inverseEOTF)
32 , m_vcgt(vcgt)
33{
34}
35
37{
38 cmsCloseProfile(m_handle);
39}
40
42{
43 return m_colorimetry;
44}
45
46std::shared_ptr<ColorTransformation> IccProfile::inverseEOTF() const
47{
48 return m_inverseEOTF;
49}
50
51std::shared_ptr<ColorTransformation> IccProfile::vcgt() const
52{
53 return m_vcgt;
54}
55
57{
58 return m_bToATag ? &m_bToATag.value() : nullptr;
59}
60
61static std::vector<uint8_t> readTagRaw(cmsHPROFILE profile, cmsTagSignature tag)
62{
63 const auto numBytes = cmsReadRawTag(profile, tag, nullptr, 0);
64 std::vector<uint8_t> data(numBytes);
65 cmsReadRawTag(profile, tag, data.data(), numBytes);
66 return data;
67}
68
69template<typename T>
70static T read(std::span<const uint8_t> data, size_t index)
71{
72 // ICC profile data is big-endian
73 T ret;
74 for (size_t i = 0; i < sizeof(T); i++) {
75 *(reinterpret_cast<uint8_t *>(&ret) + i) = data[index + sizeof(T) - i - 1];
76 }
77 return ret;
78}
79
80static float readS15Fixed16(std::span<const uint8_t> data, size_t index)
81{
82 return read<int32_t>(data, index) / 65536.0;
83}
84
85static std::optional<std::tuple<size_t, size_t, size_t>> parseBToACLUTSize(std::span<const uint8_t> data)
86{
87 const uint32_t tagType = read<uint32_t>(data, 0);
88 const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type;
89 if (isLutTag) {
90 const uint8_t size = data[10];
91 return std::make_tuple(size, size, size);
92 } else {
93 const uint32_t clutOffset = read<uint32_t>(data, 24);
94 if (data.size() < clutOffset + 19) {
95 qCWarning(KWIN_CORE, "CLut offset points to invalid position %u", clutOffset);
96 return std::nullopt;
97 }
98 return std::make_tuple(data[clutOffset + 0], data[clutOffset + 1], data[clutOffset + 2]);
99 }
100}
101
102static std::optional<QMatrix4x4> parseMatrix(std::span<const uint8_t> data, bool hasOffset)
103{
104 const size_t matrixSize = hasOffset ? 12 : 9;
105 std::vector<float> floats;
106 floats.reserve(matrixSize);
107 for (size_t i = 0; i < matrixSize; i++) {
108 floats.push_back(readS15Fixed16(data, i * 4));
109 }
110 constexpr double xyzEncodingFactor = 65536.0 / (2 * 65535.0);
111 QMatrix4x4 ret;
112 ret(0, 0) = floats[0] * xyzEncodingFactor;
113 ret(0, 1) = floats[1] * xyzEncodingFactor;
114 ret(0, 2) = floats[2] * xyzEncodingFactor;
115 ret(1, 0) = floats[3] * xyzEncodingFactor;
116 ret(1, 1) = floats[4] * xyzEncodingFactor;
117 ret(1, 2) = floats[5] * xyzEncodingFactor;
118 ret(2, 0) = floats[6] * xyzEncodingFactor;
119 ret(2, 1) = floats[7] * xyzEncodingFactor;
120 ret(2, 2) = floats[8] * xyzEncodingFactor;
121 if (hasOffset) {
122 ret(0, 3) = floats[9] * xyzEncodingFactor;
123 ret(1, 3) = floats[10] * xyzEncodingFactor;
124 ret(2, 3) = floats[11] * xyzEncodingFactor;
125 }
126 return ret;
127}
128
129static std::optional<IccProfile::BToATagData> parseBToATag(cmsHPROFILE profile, cmsTagSignature tag)
130{
131 cmsPipeline *bToAPipeline = static_cast<cmsPipeline *>(cmsReadTag(profile, tag));
132 if (!bToAPipeline) {
133 return std::nullopt;
134 }
135 IccProfile::BToATagData ret;
136 auto data = readTagRaw(profile, tag);
137 const uint32_t tagType = read<uint32_t>(data, 0);
138 switch (tagType) {
139 case cmsSigLut8Type:
140 case cmsSigLut16Type:
141 if (data.size() < 48) {
142 qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size();
143 return std::nullopt;
144 }
145 break;
146 case cmsSigLutBtoAType:
147 if (data.size() < 32) {
148 qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size();
149 return std::nullopt;
150 }
151 break;
152 default:
153 qCWarning(KWIN_CORE).nospace() << "unknown lut type " << (char)data[0] << (char)data[1] << (char)data[2] << (char)data[3];
154 return std::nullopt;
155 }
156 for (auto stage = cmsPipelineGetPtrToFirstStage(bToAPipeline); stage != nullptr; stage = cmsStageNext(stage)) {
157 switch (const cmsStageSignature stageType = cmsStageType(stage)) {
158 case cmsStageSignature::cmsSigCurveSetElemType: {
159 // TODO read the actual functions and apply them in the shader instead
160 // of using LUTs for more accuracy
161 std::vector<std::unique_ptr<ColorPipelineStage>> stages;
162 stages.push_back(std::make_unique<ColorPipelineStage>(cmsStageDup(stage)));
163 auto transformation = std::make_unique<ColorTransformation>(std::move(stages));
164 // the order of operations is fixed, so just sort the LUTs into the appropriate places
165 // depending on the stages that have already been added
166 if (!ret.matrix) {
167 ret.B = std::move(transformation);
168 } else if (!ret.CLut) {
169 ret.M = std::move(transformation);
170 } else if (!ret.A) {
171 ret.A = std::move(transformation);
172 } else {
173 qCWarning(KWIN_CORE, "unexpected amount of curve elements in BToA tag");
174 return std::nullopt;
175 }
176 } break;
177 case cmsStageSignature::cmsSigMatrixElemType: {
178 const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type;
179 const uint32_t matrixOffset = isLutTag ? 12 : read<uint32_t>(data, 16);
180 const uint32_t matrixSize = isLutTag ? 9 : 12;
181 if (data.size() < matrixOffset + matrixSize * 4) {
182 qCWarning(KWIN_CORE, "matrix offset points to invalid position %u", matrixOffset);
183 return std::nullopt;
184 }
185 const auto mat = parseMatrix(std::span(data).subspan(matrixOffset), !isLutTag);
186 if (!mat) {
187 return std::nullopt;
188 }
189 ret.matrix = mat;
190 }; break;
191 case cmsStageSignature::cmsSigCLutElemType: {
192 const auto size = parseBToACLUTSize(data);
193 if (!size) {
194 return std::nullopt;
195 }
196 const auto [x, y, z] = *size;
197 std::vector<std::unique_ptr<ColorPipelineStage>> stages;
198 stages.push_back(std::make_unique<ColorPipelineStage>(cmsStageDup(stage)));
199 ret.CLut = std::make_unique<ColorLUT3D>(std::make_unique<ColorTransformation>(std::move(stages)), x, y, z);
200 } break;
201 default:
202 qCWarning(KWIN_CORE, "unknown stage type %u", stageType);
203 return std::nullopt;
204 }
205 }
206 return ret;
207}
208
209std::unique_ptr<IccProfile> IccProfile::load(const QString &path)
210{
211 if (path.isEmpty()) {
212 return nullptr;
213 }
214 cmsHPROFILE handle = cmsOpenProfileFromFile(path.toUtf8(), "r");
215 if (!handle) {
216 qCWarning(KWIN_CORE) << "Failed to open color profile file:" << path;
217 return nullptr;
218 }
219 if (cmsGetDeviceClass(handle) != cmsSigDisplayClass) {
220 qCWarning(KWIN_CORE) << "Only Display ICC profiles are supported";
221 return nullptr;
222 }
223 if (cmsGetPCS(handle) != cmsColorSpaceSignature::cmsSigXYZData) {
224 qCWarning(KWIN_CORE) << "Only ICC profiles with a XYZ connection space are supported";
225 return nullptr;
226 }
227 if (cmsGetColorSpace(handle) != cmsColorSpaceSignature::cmsSigRgbData) {
228 qCWarning(KWIN_CORE) << "Only ICC profiles with RGB color spaces are supported";
229 return nullptr;
230 }
231
232 std::shared_ptr<ColorTransformation> vcgt;
233 cmsToneCurve **vcgtTag = static_cast<cmsToneCurve **>(cmsReadTag(handle, cmsSigVcgtTag));
234 if (!vcgtTag || !vcgtTag[0]) {
235 qCDebug(KWIN_CORE) << "Profile" << path << "has no VCGT tag";
236 } else {
237 // Need to duplicate the VCGT tone curves as they are owned by the profile.
238 cmsToneCurve *toneCurves[] = {
239 cmsDupToneCurve(vcgtTag[0]),
240 cmsDupToneCurve(vcgtTag[1]),
241 cmsDupToneCurve(vcgtTag[2]),
242 };
243 std::vector<std::unique_ptr<ColorPipelineStage>> stages;
244 stages.push_back(std::make_unique<ColorPipelineStage>(cmsStageAllocToneCurves(nullptr, 3, toneCurves)));
245 vcgt = std::make_shared<ColorTransformation>(std::move(stages));
246 }
247
248 const cmsCIEXYZ *whitepoint = static_cast<cmsCIEXYZ *>(cmsReadTag(handle, cmsSigMediaWhitePointTag));
249 if (!whitepoint) {
250 qCWarning(KWIN_CORE, "profile is missing the wtpt tag");
251 return nullptr;
252 }
253
254 QVector3D red;
255 QVector3D green;
256 QVector3D blue;
257 QVector3D white(whitepoint->X, whitepoint->Y, whitepoint->Z);
258 std::optional<QMatrix4x4> chromaticAdaptationMatrix;
259 if (cmsIsTag(handle, cmsSigChromaticAdaptationTag)) {
260 // the chromatic adaptation tag is a 3x3 matrix that converts from the actual whitepoint to D50
261 const auto data = readTagRaw(handle, cmsSigChromaticAdaptationTag);
262 const auto mat = parseMatrix(std::span(data).subspan(8), false);
263 if (!mat) {
264 qCWarning(KWIN_CORE, "Parsing chromatic adaptation matrix failed");
265 return nullptr;
266 }
267 bool invertable = false;
268 chromaticAdaptationMatrix = mat->inverted(&invertable);
269 if (!invertable) {
270 qCWarning(KWIN_CORE, "Inverting chromatic adaptation matrix failed");
271 return nullptr;
272 }
273 const QVector3D D50(0.9642, 1.0, 0.8249);
274 white = *chromaticAdaptationMatrix * D50;
275 }
276 if (cmsCIExyYTRIPLE *chrmTag = static_cast<cmsCIExyYTRIPLE *>(cmsReadTag(handle, cmsSigChromaticityTag))) {
277 red = Colorimetry::xyToXYZ(QVector2D(chrmTag->Red.x, chrmTag->Red.y)) * chrmTag->Red.Y;
278 green = Colorimetry::xyToXYZ(QVector2D(chrmTag->Green.x, chrmTag->Green.y)) * chrmTag->Green.Y;
279 blue = Colorimetry::xyToXYZ(QVector2D(chrmTag->Blue.x, chrmTag->Blue.y)) * chrmTag->Blue.Y;
280 } else {
281 const cmsCIEXYZ *r = static_cast<cmsCIEXYZ *>(cmsReadTag(handle, cmsSigRedColorantTag));
282 const cmsCIEXYZ *g = static_cast<cmsCIEXYZ *>(cmsReadTag(handle, cmsSigGreenColorantTag));
283 const cmsCIEXYZ *b = static_cast<cmsCIEXYZ *>(cmsReadTag(handle, cmsSigBlueColorantTag));
284 if (!r || !g || !b) {
285 qCWarning(KWIN_CORE, "rXYZ, gXYZ or bXYZ tag is missing");
286 return nullptr;
287 }
288 if (chromaticAdaptationMatrix) {
289 red = *chromaticAdaptationMatrix * QVector3D(r->X, r->Y, r->Z);
290 green = *chromaticAdaptationMatrix * QVector3D(g->X, g->Y, g->Z);
291 blue = *chromaticAdaptationMatrix * QVector3D(b->X, b->Y, b->Z);
292 } else {
293 // if the chromatic adaptation tag isn't available, fall back to using the media whitepoint instead
294 cmsCIEXYZ adaptedR{};
295 cmsCIEXYZ adaptedG{};
296 cmsCIEXYZ adaptedB{};
297 bool success = cmsAdaptToIlluminant(&adaptedR, cmsD50_XYZ(), whitepoint, r);
298 success &= cmsAdaptToIlluminant(&adaptedG, cmsD50_XYZ(), whitepoint, g);
299 success &= cmsAdaptToIlluminant(&adaptedB, cmsD50_XYZ(), whitepoint, b);
300 if (!success) {
301 return nullptr;
302 }
303 red = QVector3D(adaptedR.X, adaptedR.Y, adaptedR.Z);
304 green = QVector3D(adaptedG.X, adaptedG.Y, adaptedG.Z);
305 blue = QVector3D(adaptedB.X, adaptedB.Y, adaptedB.Z);
306 }
307 }
308
309 if (red.y() == 0 || green.y() == 0 || blue.y() == 0 || white.y() == 0) {
310 qCWarning(KWIN_CORE, "Profile has invalid primaries");
311 return nullptr;
312 }
313
314 BToATagData lutData;
315 if (cmsIsTag(handle, cmsSigBToD1Tag) && !cmsIsTag(handle, cmsSigBToA1Tag) && !cmsIsTag(handle, cmsSigBToA0Tag)) {
316 qCWarning(KWIN_CORE, "Profiles with only BToD tags aren't supported yet");
317 return nullptr;
318 }
319 if (cmsIsTag(handle, cmsSigBToA1Tag)) {
320 // lut based profile, with relative colorimetric intent supported
321 auto data = parseBToATag(handle, cmsSigBToA1Tag);
322 if (data) {
323 return std::make_unique<IccProfile>(handle, Colorimetry(red, green, blue, white), std::move(*data), vcgt);
324 } else {
325 qCWarning(KWIN_CORE, "Parsing BToA1 tag failed");
326 return nullptr;
327 }
328 }
329 if (cmsIsTag(handle, cmsSigBToA0Tag)) {
330 // lut based profile, with perceptual intent. The ICC docs say to use this as a fallback
331 auto data = parseBToATag(handle, cmsSigBToA0Tag);
332 if (data) {
333 return std::make_unique<IccProfile>(handle, Colorimetry(red, green, blue, white), std::move(*data), vcgt);
334 } else {
335 qCWarning(KWIN_CORE, "Parsing BToA0 tag failed");
336 return nullptr;
337 }
338 }
339 // matrix based profile. The matrix is already read out for the colorimetry above
340 // All that's missing is the EOTF, which is stored in the rTRC, gTRC and bTRC tags
341 cmsToneCurve *r = static_cast<cmsToneCurve *>(cmsReadTag(handle, cmsSigRedTRCTag));
342 cmsToneCurve *g = static_cast<cmsToneCurve *>(cmsReadTag(handle, cmsSigGreenTRCTag));
343 cmsToneCurve *b = static_cast<cmsToneCurve *>(cmsReadTag(handle, cmsSigBlueTRCTag));
344 if (!r || !g || !b) {
345 qCWarning(KWIN_CORE) << "ICC profile is missing at least one TRC tag";
346 return nullptr;
347 }
348 cmsToneCurve *toneCurves[] = {
349 cmsReverseToneCurveEx(4096, r),
350 cmsReverseToneCurveEx(4096, g),
351 cmsReverseToneCurveEx(4096, b),
352 };
353 std::vector<std::unique_ptr<ColorPipelineStage>> stages;
354 stages.push_back(std::make_unique<ColorPipelineStage>(cmsStageAllocToneCurves(nullptr, 3, toneCurves)));
355 const auto inverseEOTF = std::make_shared<ColorTransformation>(std::move(stages));
356 return std::make_unique<IccProfile>(handle, Colorimetry(red, green, blue, white), inverseEOTF, vcgt);
357}
358
359}
static QVector3D xyToXYZ(QVector2D xy)
const Colorimetry & colorimetry() const
std::shared_ptr< ColorTransformation > vcgt() const
const BToATagData * BtToATag() const
static std::unique_ptr< IccProfile > load(const QString &path)
IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, BToATagData &&bToATag, const std::shared_ptr< ColorTransformation > &vcgt)
std::shared_ptr< ColorTransformation > inverseEOTF() const
struct _cmsPipeline_struct cmsPipeline
void * cmsHPROFILE
Definition iccprofile.h:16