diff --git a/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpace.h b/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpace.h index 4df5803e5b..eb8ccea047 100644 --- a/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpace.h +++ b/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpace.h @@ -1,59 +1,105 @@ #ifndef LCMSRGBP2020PQCOLORSPACE_H #define LCMSRGBP2020PQCOLORSPACE_H #include #include #include #include #include "KoColorConversionTransformationFactory.h" #include template struct ColorSpaceFromFactory { }; template<> struct ColorSpaceFromFactory { typedef RgbU8ColorSpace type; }; template<> struct ColorSpaceFromFactory { typedef RgbU16ColorSpace type; }; template<> struct ColorSpaceFromFactory { typedef RgbF16ColorSpace type; }; template<> struct ColorSpaceFromFactory { typedef RgbF32ColorSpace type; }; +/** + * Define a singly linked list of supported bit depth traits + */ +template struct NextTrait { using type = void; }; +template<> struct NextTrait { using type = KoBgrU16Traits; }; +template<> struct NextTrait { using type = KoRgbF16Traits; }; +template<> struct NextTrait { using type = KoRgbF32Traits; }; + +/** + * Recursively add bit-depths conversions to the color space. We add only + * **outgoing** conversions for every RGB color space. That is, every color + * space has exactly three outgoing edges for color conversion. + */ +template +void addInternalConversion(QList &list, CurrentTraits*) +{ + // general case: add a converter and recurse for the next traits + list << new LcmsScaleRGBP2020PQTransformationFactory(); + + using NextTraits = typename NextTrait::type; + addInternalConversion(list, static_cast(0)); +} + +template +void addInternalConversion(QList &list, typename ParentColorSpace::ColorSpaceTraits*) +{ + // exception: skip adding an edge to the same bit depth + + using CurrentTraits = typename ParentColorSpace::ColorSpaceTraits; + using NextTraits = typename NextTrait::type; + addInternalConversion(list, static_cast(0)); +} + +template +void addInternalConversion(QList &, void*) +{ + // stop recursion +} template class LcmsRGBP2020PQColorSpaceFactoryWrapper : public BaseColorSpaceFactory { typedef typename ColorSpaceFromFactory::type RelatedColorSpaceType; KoColorSpace *createColorSpace(const KoColorProfile *p) const override { return new RelatedColorSpaceType(this->name(), p->clone()); } QList colorConversionLinks() const override { QList list; - list << new LcmsFromRGBP2020PQTransformationFactory(); - list << new LcmsToRGBP2020PQTransformationFactory(); + // we skip direct conversions to RGB U8, because it cannot fit linear color space + list << new LcmsFromRGBP2020PQTransformationFactory(); + list << new LcmsFromRGBP2020PQTransformationFactory(); + list << new LcmsFromRGBP2020PQTransformationFactory(); + list << new LcmsToRGBP2020PQTransformationFactory(); + list << new LcmsToRGBP2020PQTransformationFactory(); + list << new LcmsToRGBP2020PQTransformationFactory(); + + // internally, we can convert to RGB U8 if needed + addInternalConversion(list, static_cast(0)); return list; } }; #endif // LCMSRGBP2020PQCOLORSPACE_H diff --git a/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpaceTransformation.h b/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpaceTransformation.h index f382b581f1..a96517e5eb 100644 --- a/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpaceTransformation.h +++ b/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpaceTransformation.h @@ -1,199 +1,248 @@ #ifndef LCMSRGBP2020PQCOLORSPACETRANSFORMATION_H #define LCMSRGBP2020PQCOLORSPACETRANSFORMATION_H #include "KoAlwaysInline.h" #include "KoColorModelStandardIds.h" #include "KoColorSpaceMaths.h" #include "KoColorModelStandardIdsUtils.h" #include "KoColorConversionTransformationFactory.h" #include #include #include #include namespace { ALWAYS_INLINE float applySmpte2048Curve(float x) { const float m1 = 2610.0 / 4096.0 / 4.0; const float m2 = 2523.0 / 4096.0 * 128.0; const float a1 = 3424.0 / 4096.0; const float c2 = 2413.0 / 4096.0 * 32.0; const float c3 = 2392.0 / 4096.0 * 32.0; const float a4 = 1.0; const float x_p = powf(0.008 * std::max(0.0f, x), m1); const float res = powf((a1 + c2 * x_p) / (a4 + c3 * x_p), m2); return res; } ALWAYS_INLINE float removeSmpte2048Curve(float x) { const float m1_r = 4096.0 * 4.0 / 2610.0; const float m2_r = 4096.0 / 2523.0 / 128.0; const float a1 = 3424.0 / 4096.0; const float c2 = 2413.0 / 4096.0 * 32.0; const float c3 = 2392.0 / 4096.0 * 32.0; const float x_p = powf(x, m2_r); const float res = powf(qMax(0.0f, x_p - a1) / (c2 - c3 * x_p), m1_r); return res * 125.0f; } template struct DstTraitsForSource { typedef KoRgbF32Traits result; }; template <> struct DstTraitsForSource { typedef KoRgbF16Traits result; }; template <> struct DstTraitsForSource { typedef KoRgbF16Traits result; }; template struct RemoveSmpte2048Policy { static ALWAYS_INLINE dst_channel_type process(src_channel_type value) { return KoColorSpaceMaths::scaleToA( removeSmpte2048Curve( KoColorSpaceMaths::scaleToA( value))); } }; template struct ApplySmpte2048Policy { static ALWAYS_INLINE dst_channel_type process(src_channel_type value) { return KoColorSpaceMaths::scaleToA( applySmpte2048Curve( KoColorSpaceMaths::scaleToA( value))); } }; +template +struct NoopPolicy { + static ALWAYS_INLINE dst_channel_type process(src_channel_type value) { + return KoColorSpaceMaths::scaleToA(value); + } +}; + } template class Policy> struct ApplyRgbShaper : public KoColorConversionTransformation { ApplyRgbShaper(const KoColorSpace* srcCs, const KoColorSpace* dstCs, Intent renderingIntent, ConversionFlags conversionFlags) : KoColorConversionTransformation(srcCs, dstCs, renderingIntent, conversionFlags) { } void transform(const quint8 *src, quint8 *dst, qint32 nPixels) const override { KIS_ASSERT(src != dst); const typename SrcCSTraits::Pixel *srcPixel = reinterpret_cast(src); typename DstCSTraits::Pixel *dstPixel = reinterpret_cast(dst); typedef typename SrcCSTraits::channels_type src_channel_type; typedef typename DstCSTraits::channels_type dst_channel_type; typedef Policy ConcretePolicy; for (int i = 0; i < nPixels; i++) { dstPixel->red = ConcretePolicy::process(srcPixel->red); dstPixel->green = ConcretePolicy::process(srcPixel->green); dstPixel->blue = ConcretePolicy::process(srcPixel->blue); dstPixel->alpha = KoColorSpaceMaths::scaleToA( srcPixel->alpha); srcPixel++; dstPixel++; } } }; -template +template::result> class LcmsFromRGBP2020PQTransformationFactory : public KoColorConversionTransformationFactory { public: LcmsFromRGBP2020PQTransformationFactory() : KoColorConversionTransformationFactory(RGBAColorModelID.id(), colorDepthIdForChannelType().id(), "High Dynamic Range UHDTV Wide Color Gamut Display (Rec. 2020) - SMPTE ST 2084 PQ EOTF", RGBAColorModelID.id(), - colorDepthIdForChannelType::result::channels_type>().id(), + colorDepthIdForChannelType().id(), "Rec2020-elle-V4-g10.icc") { } bool conserveColorInformation() const override { return true; } bool conserveDynamicRange() const override { - return true; + return + dstColorDepthId() == Float16BitsColorDepthID.id() || + dstColorDepthId() == Float32BitsColorDepthID.id() || + dstColorDepthId() == Float64BitsColorDepthID.id(); } KoColorConversionTransformation* createColorTransformation(const KoColorSpace* srcColorSpace, const KoColorSpace* dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const override { return new ApplyRgbShaper< typename ParentColorSpace::ColorSpaceTraits, - typename DstTraitsForSource::result, + DstColorSpaceTraits, RemoveSmpte2048Policy>(srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); } }; -template +template::result> class LcmsToRGBP2020PQTransformationFactory : public KoColorConversionTransformationFactory { public: LcmsToRGBP2020PQTransformationFactory() : KoColorConversionTransformationFactory(RGBAColorModelID.id(), - colorDepthIdForChannelType::result::channels_type>().id(), + colorDepthIdForChannelType().id(), "Rec2020-elle-V4-g10.icc", RGBAColorModelID.id(), colorDepthIdForChannelType().id(), "High Dynamic Range UHDTV Wide Color Gamut Display (Rec. 2020) - SMPTE ST 2084 PQ EOTF") { } bool conserveColorInformation() const override { return true; } bool conserveDynamicRange() const override { return true; } KoColorConversionTransformation* createColorTransformation(const KoColorSpace* srcColorSpace, const KoColorSpace* dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const override { return new ApplyRgbShaper< - typename DstTraitsForSource::result, + DstColorSpaceTraits, typename ParentColorSpace::ColorSpaceTraits, ApplySmpte2048Policy>(srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); } }; +template +class LcmsScaleRGBP2020PQTransformationFactory : public KoColorConversionTransformationFactory +{ +public: + LcmsScaleRGBP2020PQTransformationFactory() + : KoColorConversionTransformationFactory(RGBAColorModelID.id(), + colorDepthIdForChannelType().id(), + "High Dynamic Range UHDTV Wide Color Gamut Display (Rec. 2020) - SMPTE ST 2084 PQ EOTF", + RGBAColorModelID.id(), + colorDepthIdForChannelType().id(), + "High Dynamic Range UHDTV Wide Color Gamut Display (Rec. 2020) - SMPTE ST 2084 PQ EOTF") + { + KIS_SAFE_ASSERT_RECOVER_NOOP(srcColorDepthId() != dstColorDepthId()); + } + + bool conserveColorInformation() const override { + return true; + } + + bool conserveDynamicRange() const override { + return true; + } + + KoColorConversionTransformation* createColorTransformation(const KoColorSpace* srcColorSpace, + const KoColorSpace* dstColorSpace, + KoColorConversionTransformation::Intent renderingIntent, + KoColorConversionTransformation::ConversionFlags conversionFlags) const override + { + return new ApplyRgbShaper< + typename ParentColorSpace::ColorSpaceTraits, + DstColorSpaceTraits, + NoopPolicy>(srcColorSpace, + dstColorSpace, + renderingIntent, + conversionFlags); + } +}; + #endif // LCMSRGBP2020PQCOLORSPACETRANSFORMATION_H diff --git a/plugins/color/lcms2engine/tests/CMakeLists.txt b/plugins/color/lcms2engine/tests/CMakeLists.txt index f98aa8c461..4f1230e01c 100644 --- a/plugins/color/lcms2engine/tests/CMakeLists.txt +++ b/plugins/color/lcms2engine/tests/CMakeLists.txt @@ -1,31 +1,32 @@ add_definitions(-DFILES_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data/") set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) include_directories( ../colorspaces/cmyk_u16 ../colorspaces/cmyk_u8 ../colorspaces/gray_u16 ../colorspaces/gray_u8 ../colorspaces/lab_u16 ../colorspaces/rgb_u16 ../colorspaces/rgb_u8 ../colorspaces/xyz_u16 ../colorprofiles .. ) if(OPENEXR_FOUND) include_directories(SYSTEM ${OPENEXR_INCLUDE_DIR}) endif() include_directories( ${LCMS2_INCLUDE_DIR} ) if(MSVC OR (WIN32 AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel")) # avoid "cannot open file 'LIBC.lib'" error set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /NODEFAULTLIB:LIBC.LIB") endif() ecm_add_tests( TestKoLcmsColorProfile.cpp TestKoColorSpaceRegistry.cpp + TestLcmsRGBP2020PQColorSpace.cpp NAME_PREFIX "plugins-lcmsengine-" LINK_LIBRARIES kritawidgets kritapigment KF5::I18n Qt5::Test ${LCMS2_LIBRARIES}) diff --git a/plugins/color/lcms2engine/tests/TestLcmsRGBP2020PQColorSpace.cpp b/plugins/color/lcms2engine/tests/TestLcmsRGBP2020PQColorSpace.cpp new file mode 100644 index 0000000000..68b0f4ce79 --- /dev/null +++ b/plugins/color/lcms2engine/tests/TestLcmsRGBP2020PQColorSpace.cpp @@ -0,0 +1,167 @@ +#include "TestLcmsRGBP2020PQColorSpace.h" + +#include +#include "sdk/tests/kistest.h" + +#include "kis_debug.h" + +#include "KoColorProfile.h" +#include "KoColorSpaceRegistry.h" +#include "KoColor.h" +#include "KoColorModelStandardIds.h" + +inline QString truncated(QString value) { + value.truncate(24); + return value; +} + +enum SourceType { + SDR, + HDR, + HDR_PQ +}; + +void testRoundTrip(const KoColorSpace *srcCS, const KoColorSpace *dstCS, SourceType sourceIsPQ) +{ + qDebug() << "Testing:" << srcCS->id() << truncated(srcCS->profile()->name()) + << "->" + << dstCS->id() << truncated(dstCS->profile()->name()); + + KoColor srcColor(srcCS); + KoColor dstColor(dstCS); + + QVector refChannels; + + if (sourceIsPQ == HDR) { + refChannels << 2.8; // R + refChannels << 1.8; // G + refChannels << 0.8; // B + refChannels << 0.9; // A + } else if (sourceIsPQ == HDR_PQ) { + refChannels << 0.9; // R (PQ) + refChannels << 0.7; // G (PQ) + refChannels << 0.1; // B (PQ) + refChannels << 0.9; // A + } else if (sourceIsPQ == SDR) { + refChannels << 0.15; // R + refChannels << 0.17; // G + refChannels << 0.19; // B + refChannels << 0.90; // A + } + + srcCS->fromNormalisedChannelsValue(srcColor.data(), refChannels); + + srcCS->convertPixelsTo(srcColor.data(), dstColor.data(), dstCS, 1, + KoColorConversionTransformation::internalRenderingIntent(), + KoColorConversionTransformation::internalConversionFlags()); + + dstCS->convertPixelsTo(dstColor.data(), srcColor.data(), srcCS, 1, + KoColorConversionTransformation::internalRenderingIntent(), + KoColorConversionTransformation::internalConversionFlags()); + + QVector result(4); + srcCS->normalisedChannelsValue(srcColor.data(), result); + + QList channels = srcCS->channels(); + + // 5% tolerance for CMYK, 4% for 8-bit, and 1% for everything else + const float tolerance = + dstCS->colorModelId() == CMYKAColorModelID ? 0.05 : + (dstCS->colorDepthId() == Integer8BitsColorDepthID || + srcCS->colorDepthId() == Integer8BitsColorDepthID) ? 0.04 : + 0.01; + + bool roundTripIsCorrect = true; + for (int i = 0; i < 4; i++) { + roundTripIsCorrect &= qAbs(refChannels[i] - result[i]) < tolerance; + } + + if (!roundTripIsCorrect) { + for (int i = 0; i < 4; i++) { + qDebug() << channels[i]->name() << "ref" << refChannels[i] << "result" << result[i]; + } + } + + QVERIFY(roundTripIsCorrect); +} + +void testRoundTrip(const KoID &linearColorDepth, const KoID &pqColorDepth, SourceType sourceIsPQ) +{ + const KoColorProfile *p2020PQProfile = KoColorSpaceRegistry::instance()->p2020PQProfile(); + const KoColorProfile *p2020G10Profile = KoColorSpaceRegistry::instance()->p2020G10Profile(); + + const KoColorSpace *srcCS = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), linearColorDepth.id(), p2020G10Profile); + const KoColorSpace *dstCS = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), pqColorDepth.id(), p2020PQProfile);; + + if (sourceIsPQ == HDR_PQ) { + std::swap(srcCS, dstCS); + } + + testRoundTrip(srcCS, dstCS, sourceIsPQ); +} + +void TestLcmsRGBP2020PQColorSpace::test() +{ + const KoColorProfile *p2020PQProfile = KoColorSpaceRegistry::instance()->p2020PQProfile(); + const KoColorProfile *p2020G10Profile = KoColorSpaceRegistry::instance()->p2020G10Profile(); + const KoColorProfile *p709G10Profile = KoColorSpaceRegistry::instance()->p709G10Profile(); + + QVERIFY(p2020PQProfile); + QVERIFY(p2020G10Profile); + QVERIFY(p709G10Profile); + + QVector linearModes; + linearModes << Float16BitsColorDepthID; + linearModes << Float32BitsColorDepthID; + + QVector pqModes; + pqModes << Integer8BitsColorDepthID; + pqModes << Integer16BitsColorDepthID; + pqModes << Float16BitsColorDepthID; + pqModes << Float32BitsColorDepthID; + + Q_FOREACH(const KoID &src, linearModes) { + Q_FOREACH(const KoID &dst, pqModes) { + testRoundTrip(src, dst, HDR); + } + } + + Q_FOREACH(const KoID &src, linearModes) { + Q_FOREACH(const KoID &dst, pqModes) { + testRoundTrip(src, dst, HDR_PQ); + } + } +} + +void TestLcmsRGBP2020PQColorSpace::testInternalConversions() +{ + const KoColorProfile *p2020PQProfile = KoColorSpaceRegistry::instance()->p2020PQProfile(); + + QVector pqModes; + pqModes << Integer16BitsColorDepthID; + pqModes << Float16BitsColorDepthID; + pqModes << Float32BitsColorDepthID; + + Q_FOREACH(const KoID &src, pqModes) { + Q_FOREACH(const KoID &dst, pqModes) { + if (src == dst) continue; + + const KoColorSpace *srcCS = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), src.id(), p2020PQProfile); + const KoColorSpace *dstCS = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), dst.id(), p2020PQProfile); + + testRoundTrip(srcCS, dstCS, HDR_PQ); + } + } +} + +void TestLcmsRGBP2020PQColorSpace::testConvertToCmyk() +{ + const KoColorProfile *p2020PQProfile = KoColorSpaceRegistry::instance()->p2020PQProfile(); + + const KoColorSpace *srcCS = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Integer16BitsColorDepthID.id(), p2020PQProfile); + const KoColorSpace *dstCS = KoColorSpaceRegistry::instance()->colorSpace(CMYKAColorModelID.id(), Integer8BitsColorDepthID.id(), 0); + + testRoundTrip(srcCS, dstCS, SDR); +} + +KISTEST_MAIN(TestLcmsRGBP2020PQColorSpace) diff --git a/plugins/color/lcms2engine/tests/TestLcmsRGBP2020PQColorSpace.h b/plugins/color/lcms2engine/tests/TestLcmsRGBP2020PQColorSpace.h new file mode 100644 index 0000000000..9857c2fdd6 --- /dev/null +++ b/plugins/color/lcms2engine/tests/TestLcmsRGBP2020PQColorSpace.h @@ -0,0 +1,14 @@ +#ifndef TESTLCMSRGBP2020PQCOLORSPACE_H +#define TESTLCMSRGBP2020PQCOLORSPACE_H +#include + +class TestLcmsRGBP2020PQColorSpace : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void test(); + void testInternalConversions(); + void testConvertToCmyk(); +}; + +#endif // TESTLCMSRGBP2020PQCOLORSPACE_H