File indexing completed on 2024-05-19 05:31:35

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 }