File indexing completed on 2024-11-10 04:56:38
0001 /* 0002 SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 #include "iccprofile.h" 0007 #include "colorlut.h" 0008 #include "colorlut3d.h" 0009 #include "colorpipelinestage.h" 0010 #include "colortransformation.h" 0011 #include "utils/common.h" 0012 0013 #include <lcms2.h> 0014 #include <span> 0015 #include <tuple> 0016 0017 namespace KWin 0018 { 0019 0020 IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, BToATagData &&bToATag, const std::shared_ptr<ColorTransformation> &vcgt) 0021 : m_handle(handle) 0022 , m_colorimetry(colorimetry) 0023 , m_bToATag(std::move(bToATag)) 0024 , m_vcgt(vcgt) 0025 { 0026 } 0027 0028 IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, const std::shared_ptr<ColorTransformation> &inverseEOTF, const std::shared_ptr<ColorTransformation> &vcgt) 0029 : m_handle(handle) 0030 , m_colorimetry(colorimetry) 0031 , m_inverseEOTF(inverseEOTF) 0032 , m_vcgt(vcgt) 0033 { 0034 } 0035 0036 IccProfile::~IccProfile() 0037 { 0038 cmsCloseProfile(m_handle); 0039 } 0040 0041 const Colorimetry &IccProfile::colorimetry() const 0042 { 0043 return m_colorimetry; 0044 } 0045 0046 std::shared_ptr<ColorTransformation> IccProfile::inverseEOTF() const 0047 { 0048 return m_inverseEOTF; 0049 } 0050 0051 std::shared_ptr<ColorTransformation> IccProfile::vcgt() const 0052 { 0053 return m_vcgt; 0054 } 0055 0056 const IccProfile::BToATagData *IccProfile::BtToATag() const 0057 { 0058 return m_bToATag ? &m_bToATag.value() : nullptr; 0059 } 0060 0061 static std::vector<uint8_t> readTagRaw(cmsHPROFILE profile, cmsTagSignature tag) 0062 { 0063 const auto numBytes = cmsReadRawTag(profile, tag, nullptr, 0); 0064 std::vector<uint8_t> data(numBytes); 0065 cmsReadRawTag(profile, tag, data.data(), numBytes); 0066 return data; 0067 } 0068 0069 template<typename T> 0070 static T read(std::span<const uint8_t> data, size_t index) 0071 { 0072 // ICC profile data is big-endian 0073 T ret; 0074 for (size_t i = 0; i < sizeof(T); i++) { 0075 *(reinterpret_cast<uint8_t *>(&ret) + i) = data[index + sizeof(T) - i - 1]; 0076 } 0077 return ret; 0078 } 0079 0080 static float readS15Fixed16(std::span<const uint8_t> data, size_t index) 0081 { 0082 return read<int32_t>(data, index) / 65536.0; 0083 } 0084 0085 static std::optional<std::tuple<size_t, size_t, size_t>> parseBToACLUTSize(std::span<const uint8_t> data) 0086 { 0087 const uint32_t tagType = read<uint32_t>(data, 0); 0088 const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type; 0089 if (isLutTag) { 0090 const uint8_t size = data[10]; 0091 return std::make_tuple(size, size, size); 0092 } else { 0093 const uint32_t clutOffset = read<uint32_t>(data, 24); 0094 if (data.size() < clutOffset + 19) { 0095 qCWarning(KWIN_CORE, "CLut offset points to invalid position %u", clutOffset); 0096 return std::nullopt; 0097 } 0098 return std::make_tuple(data[clutOffset + 0], data[clutOffset + 1], data[clutOffset + 2]); 0099 } 0100 } 0101 0102 static std::optional<QMatrix4x4> parseMatrix(std::span<const uint8_t> data, bool hasOffset) 0103 { 0104 const size_t matrixSize = hasOffset ? 12 : 9; 0105 std::vector<float> floats; 0106 floats.reserve(matrixSize); 0107 for (size_t i = 0; i < matrixSize; i++) { 0108 floats.push_back(readS15Fixed16(data, i * 4)); 0109 } 0110 constexpr double xyzEncodingFactor = 65536.0 / (2 * 65535.0); 0111 QMatrix4x4 ret; 0112 ret(0, 0) = floats[0] * xyzEncodingFactor; 0113 ret(0, 1) = floats[1] * xyzEncodingFactor; 0114 ret(0, 2) = floats[2] * xyzEncodingFactor; 0115 ret(1, 0) = floats[3] * xyzEncodingFactor; 0116 ret(1, 1) = floats[4] * xyzEncodingFactor; 0117 ret(1, 2) = floats[5] * xyzEncodingFactor; 0118 ret(2, 0) = floats[6] * xyzEncodingFactor; 0119 ret(2, 1) = floats[7] * xyzEncodingFactor; 0120 ret(2, 2) = floats[8] * xyzEncodingFactor; 0121 if (hasOffset) { 0122 ret(0, 3) = floats[9] * xyzEncodingFactor; 0123 ret(1, 3) = floats[10] * xyzEncodingFactor; 0124 ret(2, 3) = floats[11] * xyzEncodingFactor; 0125 } 0126 return ret; 0127 } 0128 0129 static std::optional<IccProfile::BToATagData> parseBToATag(cmsHPROFILE profile, cmsTagSignature tag) 0130 { 0131 cmsPipeline *bToAPipeline = static_cast<cmsPipeline *>(cmsReadTag(profile, tag)); 0132 if (!bToAPipeline) { 0133 return std::nullopt; 0134 } 0135 IccProfile::BToATagData ret; 0136 auto data = readTagRaw(profile, tag); 0137 const uint32_t tagType = read<uint32_t>(data, 0); 0138 switch (tagType) { 0139 case cmsSigLut8Type: 0140 case cmsSigLut16Type: 0141 if (data.size() < 48) { 0142 qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size(); 0143 return std::nullopt; 0144 } 0145 break; 0146 case cmsSigLutBtoAType: 0147 if (data.size() < 32) { 0148 qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size(); 0149 return std::nullopt; 0150 } 0151 break; 0152 default: 0153 qCWarning(KWIN_CORE).nospace() << "unknown lut type " << (char)data[0] << (char)data[1] << (char)data[2] << (char)data[3]; 0154 return std::nullopt; 0155 } 0156 for (auto stage = cmsPipelineGetPtrToFirstStage(bToAPipeline); stage != nullptr; stage = cmsStageNext(stage)) { 0157 switch (const cmsStageSignature stageType = cmsStageType(stage)) { 0158 case cmsStageSignature::cmsSigCurveSetElemType: { 0159 // TODO read the actual functions and apply them in the shader instead 0160 // of using LUTs for more accuracy 0161 std::vector<std::unique_ptr<ColorPipelineStage>> stages; 0162 stages.push_back(std::make_unique<ColorPipelineStage>(cmsStageDup(stage))); 0163 auto transformation = std::make_unique<ColorTransformation>(std::move(stages)); 0164 // the order of operations is fixed, so just sort the LUTs into the appropriate places 0165 // depending on the stages that have already been added 0166 if (!ret.matrix) { 0167 ret.B = std::move(transformation); 0168 } else if (!ret.CLut) { 0169 ret.M = std::move(transformation); 0170 } else if (!ret.A) { 0171 ret.A = std::move(transformation); 0172 } else { 0173 qCWarning(KWIN_CORE, "unexpected amount of curve elements in BToA tag"); 0174 return std::nullopt; 0175 } 0176 } break; 0177 case cmsStageSignature::cmsSigMatrixElemType: { 0178 const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type; 0179 const uint32_t matrixOffset = isLutTag ? 12 : read<uint32_t>(data, 16); 0180 const uint32_t matrixSize = isLutTag ? 9 : 12; 0181 if (data.size() < matrixOffset + matrixSize * 4) { 0182 qCWarning(KWIN_CORE, "matrix offset points to invalid position %u", matrixOffset); 0183 return std::nullopt; 0184 } 0185 const auto mat = parseMatrix(std::span(data).subspan(matrixOffset), !isLutTag); 0186 if (!mat) { 0187 return std::nullopt; 0188 } 0189 ret.matrix = mat; 0190 }; break; 0191 case cmsStageSignature::cmsSigCLutElemType: { 0192 const auto size = parseBToACLUTSize(data); 0193 if (!size) { 0194 return std::nullopt; 0195 } 0196 const auto [x, y, z] = *size; 0197 std::vector<std::unique_ptr<ColorPipelineStage>> stages; 0198 stages.push_back(std::make_unique<ColorPipelineStage>(cmsStageDup(stage))); 0199 ret.CLut = std::make_unique<ColorLUT3D>(std::make_unique<ColorTransformation>(std::move(stages)), x, y, z); 0200 } break; 0201 default: 0202 qCWarning(KWIN_CORE, "unknown stage type %u", stageType); 0203 return std::nullopt; 0204 } 0205 } 0206 return ret; 0207 } 0208 0209 std::unique_ptr<IccProfile> IccProfile::load(const QString &path) 0210 { 0211 if (path.isEmpty()) { 0212 return nullptr; 0213 } 0214 cmsHPROFILE handle = cmsOpenProfileFromFile(path.toUtf8(), "r"); 0215 if (!handle) { 0216 qCWarning(KWIN_CORE) << "Failed to open color profile file:" << path; 0217 return nullptr; 0218 } 0219 if (cmsGetDeviceClass(handle) != cmsSigDisplayClass) { 0220 qCWarning(KWIN_CORE) << "Only Display ICC profiles are supported"; 0221 return nullptr; 0222 } 0223 if (cmsGetPCS(handle) != cmsColorSpaceSignature::cmsSigXYZData) { 0224 qCWarning(KWIN_CORE) << "Only ICC profiles with a XYZ connection space are supported"; 0225 return nullptr; 0226 } 0227 if (cmsGetColorSpace(handle) != cmsColorSpaceSignature::cmsSigRgbData) { 0228 qCWarning(KWIN_CORE) << "Only ICC profiles with RGB color spaces are supported"; 0229 return nullptr; 0230 } 0231 0232 std::shared_ptr<ColorTransformation> vcgt; 0233 cmsToneCurve **vcgtTag = static_cast<cmsToneCurve **>(cmsReadTag(handle, cmsSigVcgtTag)); 0234 if (!vcgtTag || !vcgtTag[0]) { 0235 qCDebug(KWIN_CORE) << "Profile" << path << "has no VCGT tag"; 0236 } else { 0237 // Need to duplicate the VCGT tone curves as they are owned by the profile. 0238 cmsToneCurve *toneCurves[] = { 0239 cmsDupToneCurve(vcgtTag[0]), 0240 cmsDupToneCurve(vcgtTag[1]), 0241 cmsDupToneCurve(vcgtTag[2]), 0242 }; 0243 std::vector<std::unique_ptr<ColorPipelineStage>> stages; 0244 stages.push_back(std::make_unique<ColorPipelineStage>(cmsStageAllocToneCurves(nullptr, 3, toneCurves))); 0245 vcgt = std::make_shared<ColorTransformation>(std::move(stages)); 0246 } 0247 0248 const cmsCIEXYZ *whitepoint = static_cast<cmsCIEXYZ *>(cmsReadTag(handle, cmsSigMediaWhitePointTag)); 0249 if (!whitepoint) { 0250 qCWarning(KWIN_CORE, "profile is missing the wtpt tag"); 0251 return nullptr; 0252 } 0253 0254 QVector3D red; 0255 QVector3D green; 0256 QVector3D blue; 0257 QVector3D white(whitepoint->X, whitepoint->Y, whitepoint->Z); 0258 std::optional<QMatrix4x4> chromaticAdaptationMatrix; 0259 if (cmsIsTag(handle, cmsSigChromaticAdaptationTag)) { 0260 // the chromatic adaptation tag is a 3x3 matrix that converts from the actual whitepoint to D50 0261 const auto data = readTagRaw(handle, cmsSigChromaticAdaptationTag); 0262 const auto mat = parseMatrix(std::span(data).subspan(8), false); 0263 if (!mat) { 0264 qCWarning(KWIN_CORE, "Parsing chromatic adaptation matrix failed"); 0265 return nullptr; 0266 } 0267 bool invertable = false; 0268 chromaticAdaptationMatrix = mat->inverted(&invertable); 0269 if (!invertable) { 0270 qCWarning(KWIN_CORE, "Inverting chromatic adaptation matrix failed"); 0271 return nullptr; 0272 } 0273 const QVector3D D50(0.9642, 1.0, 0.8249); 0274 white = *chromaticAdaptationMatrix * D50; 0275 } 0276 if (cmsCIExyYTRIPLE *chrmTag = static_cast<cmsCIExyYTRIPLE *>(cmsReadTag(handle, cmsSigChromaticityTag))) { 0277 red = Colorimetry::xyToXYZ(QVector2D(chrmTag->Red.x, chrmTag->Red.y)) * chrmTag->Red.Y; 0278 green = Colorimetry::xyToXYZ(QVector2D(chrmTag->Green.x, chrmTag->Green.y)) * chrmTag->Green.Y; 0279 blue = Colorimetry::xyToXYZ(QVector2D(chrmTag->Blue.x, chrmTag->Blue.y)) * chrmTag->Blue.Y; 0280 } else { 0281 const cmsCIEXYZ *r = static_cast<cmsCIEXYZ *>(cmsReadTag(handle, cmsSigRedColorantTag)); 0282 const cmsCIEXYZ *g = static_cast<cmsCIEXYZ *>(cmsReadTag(handle, cmsSigGreenColorantTag)); 0283 const cmsCIEXYZ *b = static_cast<cmsCIEXYZ *>(cmsReadTag(handle, cmsSigBlueColorantTag)); 0284 if (!r || !g || !b) { 0285 qCWarning(KWIN_CORE, "rXYZ, gXYZ or bXYZ tag is missing"); 0286 return nullptr; 0287 } 0288 if (chromaticAdaptationMatrix) { 0289 red = *chromaticAdaptationMatrix * QVector3D(r->X, r->Y, r->Z); 0290 green = *chromaticAdaptationMatrix * QVector3D(g->X, g->Y, g->Z); 0291 blue = *chromaticAdaptationMatrix * QVector3D(b->X, b->Y, b->Z); 0292 } else { 0293 // if the chromatic adaptation tag isn't available, fall back to using the media whitepoint instead 0294 cmsCIEXYZ adaptedR{}; 0295 cmsCIEXYZ adaptedG{}; 0296 cmsCIEXYZ adaptedB{}; 0297 bool success = cmsAdaptToIlluminant(&adaptedR, cmsD50_XYZ(), whitepoint, r); 0298 success &= cmsAdaptToIlluminant(&adaptedG, cmsD50_XYZ(), whitepoint, g); 0299 success &= cmsAdaptToIlluminant(&adaptedB, cmsD50_XYZ(), whitepoint, b); 0300 if (!success) { 0301 return nullptr; 0302 } 0303 red = QVector3D(adaptedR.X, adaptedR.Y, adaptedR.Z); 0304 green = QVector3D(adaptedG.X, adaptedG.Y, adaptedG.Z); 0305 blue = QVector3D(adaptedB.X, adaptedB.Y, adaptedB.Z); 0306 } 0307 } 0308 0309 if (red.y() == 0 || green.y() == 0 || blue.y() == 0 || white.y() == 0) { 0310 qCWarning(KWIN_CORE, "Profile has invalid primaries"); 0311 return nullptr; 0312 } 0313 0314 BToATagData lutData; 0315 if (cmsIsTag(handle, cmsSigBToD1Tag) && !cmsIsTag(handle, cmsSigBToA1Tag) && !cmsIsTag(handle, cmsSigBToA0Tag)) { 0316 qCWarning(KWIN_CORE, "Profiles with only BToD tags aren't supported yet"); 0317 return nullptr; 0318 } 0319 if (cmsIsTag(handle, cmsSigBToA1Tag)) { 0320 // lut based profile, with relative colorimetric intent supported 0321 auto data = parseBToATag(handle, cmsSigBToA1Tag); 0322 if (data) { 0323 return std::make_unique<IccProfile>(handle, Colorimetry(red, green, blue, white), std::move(*data), vcgt); 0324 } else { 0325 qCWarning(KWIN_CORE, "Parsing BToA1 tag failed"); 0326 return nullptr; 0327 } 0328 } 0329 if (cmsIsTag(handle, cmsSigBToA0Tag)) { 0330 // lut based profile, with perceptual intent. The ICC docs say to use this as a fallback 0331 auto data = parseBToATag(handle, cmsSigBToA0Tag); 0332 if (data) { 0333 return std::make_unique<IccProfile>(handle, Colorimetry(red, green, blue, white), std::move(*data), vcgt); 0334 } else { 0335 qCWarning(KWIN_CORE, "Parsing BToA0 tag failed"); 0336 return nullptr; 0337 } 0338 } 0339 // matrix based profile. The matrix is already read out for the colorimetry above 0340 // All that's missing is the EOTF, which is stored in the rTRC, gTRC and bTRC tags 0341 cmsToneCurve *r = static_cast<cmsToneCurve *>(cmsReadTag(handle, cmsSigRedTRCTag)); 0342 cmsToneCurve *g = static_cast<cmsToneCurve *>(cmsReadTag(handle, cmsSigGreenTRCTag)); 0343 cmsToneCurve *b = static_cast<cmsToneCurve *>(cmsReadTag(handle, cmsSigBlueTRCTag)); 0344 if (!r || !g || !b) { 0345 qCWarning(KWIN_CORE) << "ICC profile is missing at least one TRC tag"; 0346 return nullptr; 0347 } 0348 cmsToneCurve *toneCurves[] = { 0349 cmsReverseToneCurveEx(4096, r), 0350 cmsReverseToneCurveEx(4096, g), 0351 cmsReverseToneCurveEx(4096, b), 0352 }; 0353 std::vector<std::unique_ptr<ColorPipelineStage>> stages; 0354 stages.push_back(std::make_unique<ColorPipelineStage>(cmsStageAllocToneCurves(nullptr, 3, toneCurves))); 0355 const auto inverseEOTF = std::make_shared<ColorTransformation>(std::move(stages)); 0356 return std::make_unique<IccProfile>(handle, Colorimetry(red, green, blue, white), inverseEOTF, vcgt); 0357 } 0358 0359 }