/* SPDX-FileCopyrightText: 2023 Xaver Hugl SPDX-License-Identifier: GPL-2.0-or-later */ #include "iccprofile.h" #include "colorlut3d.h" #include "colorpipelinestage.h" #include "colortransformation.h" #include "utils/common.h" #include #include #include namespace KWin { IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, BToATagData &&bToATag, const std::shared_ptr &vcgt, std::optional minBrightness, std::optional maxBrightness) : m_handle(handle) , m_colorimetry(colorimetry) , m_bToATag(std::move(bToATag)) , m_vcgt(vcgt) , m_minBrightness(minBrightness) , m_maxBrightness(maxBrightness) { } IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, const std::shared_ptr &inverseEOTF, const std::shared_ptr &vcgt, std::optional minBrightness, std::optional maxBrightness) : m_handle(handle) , m_colorimetry(colorimetry) , m_inverseEOTF(inverseEOTF) , m_vcgt(vcgt) , m_minBrightness(minBrightness) , m_maxBrightness(maxBrightness) { } IccProfile::~IccProfile() { cmsCloseProfile(m_handle); } std::optional IccProfile::minBrightness() const { return m_minBrightness; } std::optional IccProfile::maxBrightness() const { return m_maxBrightness; } const Colorimetry &IccProfile::colorimetry() const { return m_colorimetry; } std::shared_ptr IccProfile::inverseEOTF() const { return m_inverseEOTF; } std::shared_ptr IccProfile::vcgt() const { return m_vcgt; } const IccProfile::BToATagData *IccProfile::BtToATag() const { return m_bToATag ? &m_bToATag.value() : nullptr; } static std::vector readTagRaw(cmsHPROFILE profile, cmsTagSignature tag) { const auto numBytes = cmsReadRawTag(profile, tag, nullptr, 0); std::vector data(numBytes); cmsReadRawTag(profile, tag, data.data(), numBytes); return data; } template static T read(std::span data, size_t index) { // ICC profile data is big-endian T ret; for (size_t i = 0; i < sizeof(T); i++) { *(reinterpret_cast(&ret) + i) = data[index + sizeof(T) - i - 1]; } return ret; } static float readS15Fixed16(std::span data, size_t index) { return read(data, index) / 65536.0; } static std::optional> parseBToACLUTSize(std::span data) { const uint32_t tagType = read(data, 0); const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type; if (isLutTag) { const uint8_t size = data[10]; return std::make_tuple(size, size, size); } else { const uint32_t clutOffset = read(data, 24); if (data.size() < clutOffset + 19) { qCWarning(KWIN_CORE, "CLut offset points to invalid position %u", clutOffset); return std::nullopt; } return std::make_tuple(data[clutOffset + 0], data[clutOffset + 1], data[clutOffset + 2]); } } static std::optional parseMatrix(std::span data, bool hasOffset) { const size_t matrixSize = hasOffset ? 12 : 9; std::vector floats; floats.reserve(matrixSize); for (size_t i = 0; i < matrixSize; i++) { floats.push_back(readS15Fixed16(data, i * 4)); } constexpr double xyzEncodingFactor = 65536.0 / (2 * 65535.0); QMatrix4x4 ret; ret(0, 0) = floats[0] * xyzEncodingFactor; ret(0, 1) = floats[1] * xyzEncodingFactor; ret(0, 2) = floats[2] * xyzEncodingFactor; ret(1, 0) = floats[3] * xyzEncodingFactor; ret(1, 1) = floats[4] * xyzEncodingFactor; ret(1, 2) = floats[5] * xyzEncodingFactor; ret(2, 0) = floats[6] * xyzEncodingFactor; ret(2, 1) = floats[7] * xyzEncodingFactor; ret(2, 2) = floats[8] * xyzEncodingFactor; if (hasOffset) { ret(0, 3) = floats[9] * xyzEncodingFactor; ret(1, 3) = floats[10] * xyzEncodingFactor; ret(2, 3) = floats[11] * xyzEncodingFactor; } return ret; } static std::optional parseBToATag(cmsHPROFILE profile, cmsTagSignature tag) { cmsPipeline *bToAPipeline = static_cast(cmsReadTag(profile, tag)); if (!bToAPipeline) { return std::nullopt; } IccProfile::BToATagData ret; auto data = readTagRaw(profile, tag); const uint32_t tagType = read(data, 0); switch (tagType) { case cmsSigLut8Type: case cmsSigLut16Type: if (data.size() < 48) { qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size(); return std::nullopt; } break; case cmsSigLutBtoAType: if (data.size() < 32) { qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size(); return std::nullopt; } break; default: qCWarning(KWIN_CORE).nospace() << "unknown lut type " << (char)data[0] << (char)data[1] << (char)data[2] << (char)data[3]; return std::nullopt; } for (auto stage = cmsPipelineGetPtrToFirstStage(bToAPipeline); stage != nullptr; stage = cmsStageNext(stage)) { switch (const cmsStageSignature stageType = cmsStageType(stage)) { case cmsStageSignature::cmsSigCurveSetElemType: { // TODO read the actual functions and apply them in the shader instead // of using LUTs for more accuracy std::vector> stages; stages.push_back(std::make_unique(cmsStageDup(stage))); auto transformation = std::make_unique(std::move(stages)); // the order of operations is fixed, so just sort the LUTs into the appropriate places // depending on the stages that have already been added if (!ret.matrix) { ret.B = std::move(transformation); } else if (!ret.CLut) { ret.M = std::move(transformation); } else if (!ret.A) { ret.A = std::move(transformation); } else { qCWarning(KWIN_CORE, "unexpected amount of curve elements in BToA tag"); return std::nullopt; } } break; case cmsStageSignature::cmsSigMatrixElemType: { const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type; const uint32_t matrixOffset = isLutTag ? 12 : read(data, 16); const uint32_t matrixSize = isLutTag ? 9 : 12; if (data.size() < matrixOffset + matrixSize * 4) { qCWarning(KWIN_CORE, "matrix offset points to invalid position %u", matrixOffset); return std::nullopt; } const auto mat = parseMatrix(std::span(data).subspan(matrixOffset), !isLutTag); if (!mat) { return std::nullopt; } ret.matrix = mat; }; break; case cmsStageSignature::cmsSigCLutElemType: { const auto size = parseBToACLUTSize(data); if (!size) { return std::nullopt; } const auto [x, y, z] = *size; std::vector> stages; stages.push_back(std::make_unique(cmsStageDup(stage))); ret.CLut = std::make_unique(std::make_unique(std::move(stages)), x, y, z); } break; default: qCWarning(KWIN_CORE, "unknown stage type %u", stageType); return std::nullopt; } } return ret; } std::unique_ptr IccProfile::load(const QString &path) { if (path.isEmpty()) { return nullptr; } cmsHPROFILE handle = cmsOpenProfileFromFile(path.toUtf8(), "r"); if (!handle) { qCWarning(KWIN_CORE) << "Failed to open color profile file:" << path; return nullptr; } if (cmsGetDeviceClass(handle) != cmsSigDisplayClass) { qCWarning(KWIN_CORE) << "Only Display ICC profiles are supported"; return nullptr; } if (cmsGetPCS(handle) != cmsColorSpaceSignature::cmsSigXYZData) { qCWarning(KWIN_CORE) << "Only ICC profiles with a XYZ connection space are supported"; return nullptr; } if (cmsGetColorSpace(handle) != cmsColorSpaceSignature::cmsSigRgbData) { qCWarning(KWIN_CORE) << "Only ICC profiles with RGB color spaces are supported"; return nullptr; } std::shared_ptr vcgt; cmsToneCurve **vcgtTag = static_cast(cmsReadTag(handle, cmsSigVcgtTag)); if (!vcgtTag || !vcgtTag[0]) { qCDebug(KWIN_CORE) << "Profile" << path << "has no VCGT tag"; } else { // Need to duplicate the VCGT tone curves as they are owned by the profile. cmsToneCurve *toneCurves[] = { cmsDupToneCurve(vcgtTag[0]), cmsDupToneCurve(vcgtTag[1]), cmsDupToneCurve(vcgtTag[2]), }; std::vector> stages; stages.push_back(std::make_unique(cmsStageAllocToneCurves(nullptr, 3, toneCurves))); vcgt = std::make_shared(std::move(stages)); } const cmsCIEXYZ *whitepoint = static_cast(cmsReadTag(handle, cmsSigMediaWhitePointTag)); if (!whitepoint) { qCWarning(KWIN_CORE, "profile is missing the wtpt tag"); return nullptr; } if (whitepoint->Y == 0) { qCWarning(KWIN_CORE, "profile has a zero luminance whitepoint"); return nullptr; } XYZ red; XYZ green; XYZ blue; XYZ white = XYZ{whitepoint->X, whitepoint->Y, whitepoint->Z}; std::optional chromaticAdaptationMatrix; if (cmsIsTag(handle, cmsSigChromaticAdaptationTag)) { // the chromatic adaptation tag is a 3x3 matrix that converts from the actual whitepoint to D50 const auto data = readTagRaw(handle, cmsSigChromaticAdaptationTag); const auto mat = parseMatrix(std::span(data).subspan(8), false); if (!mat) { qCWarning(KWIN_CORE, "Parsing chromatic adaptation matrix failed"); return nullptr; } bool invertable = false; chromaticAdaptationMatrix = mat->inverted(&invertable); if (!invertable) { qCWarning(KWIN_CORE, "Inverting chromatic adaptation matrix failed"); return nullptr; } const QVector3D D50(0.9642, 1.0, 0.8249); white = XYZ::fromVector(*chromaticAdaptationMatrix * D50); } if (cmsCIExyYTRIPLE *chrmTag = static_cast(cmsReadTag(handle, cmsSigChromaticityTag))) { red = xyY{chrmTag->Red.x, chrmTag->Red.y, chrmTag->Red.Y}.toXYZ(); green = xyY{chrmTag->Green.x, chrmTag->Green.y, chrmTag->Green.Y}.toXYZ(); blue = xyY{chrmTag->Blue.x, chrmTag->Blue.y, chrmTag->Blue.Y}.toXYZ(); } else { const cmsCIEXYZ *r = static_cast(cmsReadTag(handle, cmsSigRedColorantTag)); const cmsCIEXYZ *g = static_cast(cmsReadTag(handle, cmsSigGreenColorantTag)); const cmsCIEXYZ *b = static_cast(cmsReadTag(handle, cmsSigBlueColorantTag)); if (!r || !g || !b) { qCWarning(KWIN_CORE, "rXYZ, gXYZ or bXYZ tag is missing"); return nullptr; } if (chromaticAdaptationMatrix) { red = XYZ::fromVector(*chromaticAdaptationMatrix * QVector3D(r->X, r->Y, r->Z)); green = XYZ::fromVector(*chromaticAdaptationMatrix * QVector3D(g->X, g->Y, g->Z)); blue = XYZ::fromVector(*chromaticAdaptationMatrix * QVector3D(b->X, b->Y, b->Z)); } else { // if the chromatic adaptation tag isn't available, fall back to using the media whitepoint instead cmsCIEXYZ adaptedR{}; cmsCIEXYZ adaptedG{}; cmsCIEXYZ adaptedB{}; bool success = cmsAdaptToIlluminant(&adaptedR, cmsD50_XYZ(), whitepoint, r); success &= cmsAdaptToIlluminant(&adaptedG, cmsD50_XYZ(), whitepoint, g); success &= cmsAdaptToIlluminant(&adaptedB, cmsD50_XYZ(), whitepoint, b); if (!success) { return nullptr; } red = XYZ(adaptedR.X, adaptedR.Y, adaptedR.Z); green = XYZ(adaptedG.X, adaptedG.Y, adaptedG.Z); blue = XYZ(adaptedB.X, adaptedB.Y, adaptedB.Z); } } if (red.Y == 0 || green.Y == 0 || blue.Y == 0 || white.Y == 0) { qCWarning(KWIN_CORE, "Profile has invalid primaries"); return nullptr; } std::optional minBrightness; std::optional maxBrightness; if (cmsCIEXYZ *luminance = static_cast(cmsReadTag(handle, cmsSigLuminanceTag))) { // for some reason, lcms exposes the luminance as a XYZ triple... // only Y is non-zero, and it's the brightness in nits maxBrightness = luminance->Y; cmsCIEXYZ blackPoint; if (cmsDetectDestinationBlackPoint(&blackPoint, handle, INTENT_RELATIVE_COLORIMETRIC, 0)) { minBrightness = blackPoint.Y * luminance->Y; } } BToATagData lutData; if (cmsIsTag(handle, cmsSigBToD1Tag) && !cmsIsTag(handle, cmsSigBToA1Tag) && !cmsIsTag(handle, cmsSigBToA0Tag)) { qCWarning(KWIN_CORE, "Profiles with only BToD tags aren't supported yet"); return nullptr; } if (cmsIsTag(handle, cmsSigBToA1Tag)) { // lut based profile, with relative colorimetric intent supported auto data = parseBToATag(handle, cmsSigBToA1Tag); if (data) { return std::make_unique(handle, Colorimetry(red, green, blue, white), std::move(*data), vcgt, minBrightness, maxBrightness); } else { qCWarning(KWIN_CORE, "Parsing BToA1 tag failed"); return nullptr; } } if (cmsIsTag(handle, cmsSigBToA0Tag)) { // lut based profile, with perceptual intent. The ICC docs say to use this as a fallback auto data = parseBToATag(handle, cmsSigBToA0Tag); if (data) { return std::make_unique(handle, Colorimetry(red, green, blue, white), std::move(*data), vcgt, minBrightness, maxBrightness); } else { qCWarning(KWIN_CORE, "Parsing BToA0 tag failed"); return nullptr; } } // matrix based profile. The matrix is already read out for the colorimetry above // All that's missing is the EOTF, which is stored in the rTRC, gTRC and bTRC tags cmsToneCurve *r = static_cast(cmsReadTag(handle, cmsSigRedTRCTag)); cmsToneCurve *g = static_cast(cmsReadTag(handle, cmsSigGreenTRCTag)); cmsToneCurve *b = static_cast(cmsReadTag(handle, cmsSigBlueTRCTag)); if (!r || !g || !b) { qCWarning(KWIN_CORE) << "ICC profile is missing at least one TRC tag"; return nullptr; } cmsToneCurve *toneCurves[] = { cmsReverseToneCurveEx(4096, r), cmsReverseToneCurveEx(4096, g), cmsReverseToneCurveEx(4096, b), }; std::vector> stages; stages.push_back(std::make_unique(cmsStageAllocToneCurves(nullptr, 3, toneCurves))); const auto inverseEOTF = std::make_shared(std::move(stages)); return std::make_unique(handle, Colorimetry(red, green, blue, white), inverseEOTF, vcgt, minBrightness, maxBrightness); } }