| /* | |
| * Copyright 2025 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/core/SkBitmap.h" | |
| #include "include/core/SkCanvas.h" | |
| #include "include/core/SkColorFilter.h" | |
| #include "include/core/SkData.h" | |
| #include "include/core/SkImage.h" | |
| #include "include/core/SkScalar.h" | |
| #include "include/private/SkHdrMetadata.h" | |
| #include "src/codec/SkHdrAgtmPriv.h" | |
| #include "tests/Test.h" | |
| #include <array> | |
| DEF_TEST(HdrMetadata_ParseSerialize_ContentLightLevelInformation, r) { | |
| uint8_t data[] = { | |
| 0x03, 0xE8, | |
| 0x00, 0xFA, | |
| }; | |
| // Data taken from: | |
| // https://www.w3.org/TR/png-3/#example-13 | |
| // https://www.w3.org/TR/png-3/#example-14 | |
| uint8_t dataPng[] = { | |
| 0x00, 0x98, 0x96, 0x80, | |
| 0x00, 0x26, 0x25, 0xA0, | |
| }; | |
| skhdr::ContentLightLevelInformation clliExpected = { | |
| 1000.f, 250.f, | |
| }; | |
| auto skData = SkData::MakeWithoutCopy(data, sizeof(data)); | |
| auto skDataPng = SkData::MakeWithoutCopy(dataPng, sizeof(dataPng)); | |
| skhdr::ContentLightLevelInformation clli; | |
| REPORTER_ASSERT(r, clli.parse(skData.get())); | |
| REPORTER_ASSERT(r, clli == clliExpected); | |
| REPORTER_ASSERT(r, skData->equals(clli.serialize().get())); | |
| skhdr::ContentLightLevelInformation clliPng; | |
| REPORTER_ASSERT(r, clliPng.parsePngChunk(skDataPng.get())); | |
| REPORTER_ASSERT(r, clliPng == clliExpected); | |
| REPORTER_ASSERT(r, skDataPng->equals(clli.serializePngChunk().get())); | |
| } | |
| DEF_TEST(HdrMetadata_ParseSerialize_MasteringDisplayColorVolume, r) { | |
| // Data taken from: | |
| // https://www.w3.org/TR/png-3/#example-5 | |
| // https://www.w3.org/TR/png-3/#example-6 | |
| // https://www.w3.org/TR/png-3/#example-7 | |
| // https://www.w3.org/TR/png-3/#example-8 | |
| uint8_t data[] = { | |
| 0x8A, 0x48, 0x39, 0x08, // Red | |
| 0x21, 0x34, 0x9B, 0xAA, // Green | |
| 0x19, 0x96, 0x08, 0xFC, // Blue | |
| 0x3D, 0x13, 0x40, 0x42, // White | |
| 0x02, 0x62, 0x5A, 0x00, // Maximum luminance | |
| 0x00, 0x00, 0x00, 0x05, // Minimum luminance | |
| }; | |
| skhdr::MasteringDisplayColorVolume mdcvExpected = { | |
| {0.708f, 0.292f, 0.17f, 0.797f, 0.131f, 0.046f, 0.3127f, 0.329f}, | |
| 4000.f, 0.0005f, | |
| }; | |
| auto skData = SkData::MakeWithoutCopy(data, sizeof(data)); | |
| skhdr::MasteringDisplayColorVolume mdcv; | |
| REPORTER_ASSERT(r, mdcv.parse(skData.get())); | |
| REPORTER_ASSERT(r, mdcv == mdcvExpected); | |
| REPORTER_ASSERT(r, skData->equals(mdcv.serialize().get())); | |
| } | |
| DEF_TEST(HdrMetadata_Agtm_Cubic, r) { | |
| skhdr::AdaptiveGlobalToneMap::GainCurve cubic = { | |
| .fControlPoints = { | |
| { .fX = 0.10720647f, .fY = 0.37384606f }, | |
| { .fX = 0.76246667f, .fY = 0.93143060f }, | |
| { .fX = 1.39535723f, .fY = 0.f }, | |
| { .fX = 2.17572099f, .fY = 1.23009354f }, | |
| { .fX = 2.47834070f, .fY = 1.25542898f }, | |
| { .fX = 3.14288223f, .fY = 2.22460677f }, | |
| { .fX = 3.35428070f, .fY = 2.69226748f }, | |
| { .fX = 4.24864607f, .fY = 3.45838813f }, | |
| { .fX = 4.59087493f, .fY = 4.44597502f }, | |
| { .fX = 4.80373641f, .fY = 5.19196203f } | |
| } | |
| }; | |
| skhdr::AgtmHelpers::PopulateSlopeFromPCHIP(cubic); | |
| static constexpr std::array<float, 10> mExpected = { | |
| 2.03242568f, 0.f, 0.f, 0.14042951f, 0.14250506f, | |
| 1.82245618f, 1.35855757f, 1.43703564f, 3.18918733f, 3.74186390f, | |
| }; | |
| for (size_t i = 0; i < 10; ++i) { | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic.fControlPoints[i].fM, mExpected[i], 0.0001f)); | |
| } | |
| static constexpr std::array<float, 11> yExpected = { | |
| 0.37384606f, 0.86280187f, 0.63630745f, 0.05871820f, 1.05625216f, | |
| 1.26009455f, 1.95243885f, 2.85680727f, 3.19521825f, 4.14318213f, | |
| 5.13419092f | |
| }; | |
| for (size_t i = 0; i < 11; ++i) { | |
| const float x = i / 2.f; | |
| const float y = skhdr::AgtmHelpers::EvaluateGainCurve(cubic, x); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(y, yExpected[i], 0.0001f)); | |
| } | |
| } | |
| DEF_TEST(HdrMetadata_Agtm_PCHIP_EdgeCases, r) { | |
| struct SlopeTest { | |
| const char* name; | |
| skhdr::AdaptiveGlobalToneMap::GainCurve curve; | |
| std::vector<std::pair<int, float>> expected; | |
| }; | |
| const SlopeTest tests[] = { | |
| { | |
| .name = "SinglePoint", | |
| .curve = { .fControlPoints = { | |
| {.fX = 1.f, .fY = 1.f}} | |
| }, | |
| .expected = { {0, 0.f} } | |
| }, | |
| { | |
| .name = "Left endpoint, three-point, sign change s0 vs m", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 1.f, .fY = 1.f}, | |
| {.fX = 2.f, .fY = 5.f}} | |
| }, | |
| .expected = { {0, 0.f} } | |
| }, | |
| { | |
| .name = "Left endpoint, three-point, sign change s1 vs m, |m| > 3|s0|", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 1.f, .fY = 1.f}, | |
| {.fX = 2.f, .fY = -9.f}} | |
| }, | |
| .expected = { {0, 3.f} } | |
| }, | |
| { | |
| .name = "Interior point, different signs", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 1.f, .fY = 1.f}, | |
| {.fX = 2.f, .fY = 0.f}} | |
| }, | |
| .expected = { {1, 0.f} } | |
| }, | |
| { | |
| .name = "Interior point, duplicate point left", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 1.f, .fY = 2.f}} | |
| }, | |
| .expected = { {1, 2.f} } | |
| }, | |
| { | |
| .name = "Interior point, duplicate point right", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 1.f, .fY = 2.f}, | |
| {.fX = 1.f, .fY = 2.f}} | |
| }, | |
| .expected = { {1, 2.f} } | |
| }, | |
| { | |
| .name = "Interior point, flat on both sides", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 2.f}, | |
| {.fX = 1.f, .fY = 2.f}, | |
| {.fX = 2.f, .fY = 2.f}} | |
| }, | |
| .expected = { {1, 0.f} } | |
| }, | |
| { | |
| .name = "N=2, two-point difference", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 2.f, .fY = 4.f}} | |
| }, | |
| .expected = { {0, 2.f}, {1, 2.f} } | |
| }, | |
| { | |
| .name = "h[i]=0 case", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 0.f, .fY = 0.f}, | |
| {.fX = 1.f, .fY = 1.f}} | |
| }, | |
| .expected = { {0, 0.f}, {1, 1.f} } | |
| }, | |
| { | |
| .name = "Right endpoint, three-point, sign change s0 vs m", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = 4.f}, | |
| {.fX = 1.f, .fY = 0.f}, | |
| {.fX = 2.f, .fY = -1.f}} | |
| }, | |
| .expected = { {2, 0.f} } | |
| }, | |
| { | |
| .name = "Right endpoint, three-point, sign change s1 vs m, |m| > 3|s0|", | |
| .curve = { .fControlPoints = { | |
| {.fX = 0.f, .fY = -4.f}, | |
| {.fX = 1.f, .fY = 0.f}, | |
| {.fX = 2.f, .fY = -1.f}} | |
| }, | |
| .expected = { {2, -3.f} } | |
| }, | |
| }; | |
| for (const auto& t : tests) { | |
| skiatest::ReporterContext ctx(r, t.name); | |
| skhdr::AdaptiveGlobalToneMap::GainCurve curve = t.curve; | |
| skhdr::AgtmHelpers::PopulateSlopeFromPCHIP(curve); | |
| for (const auto& [index, expectedM] : t.expected) { | |
| REPORTER_ASSERT(r, curve.fControlPoints[index].fM == expectedM); | |
| } | |
| } | |
| } | |
| DEF_TEST(HdrMetadata_Agtm_Mix, r) { | |
| auto test = [&r](const std::string& name, skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction mix, | |
| SkColor4f input, SkColor4f expected) { | |
| skiatest::ReporterContext ctx(r, name); | |
| SkColor4f actual = skhdr::AgtmHelpers::EvaluateComponentMixingFunction(mix, input); | |
| REPORTER_ASSERT(r, actual.fR == expected.fR); | |
| REPORTER_ASSERT(r, actual.fG == expected.fG); | |
| REPORTER_ASSERT(r, actual.fB == expected.fB); | |
| REPORTER_ASSERT(r, actual.fA == expected.fA); | |
| REPORTER_ASSERT(r, actual.fA == input.fA); | |
| }; | |
| test("Red only", | |
| skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction({.fRed=1.f}), | |
| SkColor4f({0.5f, 0.75f, 0.25f, 1.f}), | |
| SkColor4f({0.5f, 0.5f, 0.5f, 1.f})); | |
| test("Green only", | |
| skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction({.fGreen=1.f}), | |
| SkColor4f({0.75f, 0.5f, 0.25f, 1.f}), | |
| SkColor4f({0.5f, 0.5f, 0.5f, 1.f})); | |
| test("Blue only", | |
| skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction({.fBlue=1.f}), | |
| SkColor4f({0.75f, 0.25f, 0.5f, 1.f}), | |
| SkColor4f({0.5f, 0.5f, 0.5f, 1.f})); | |
| test("Max only", | |
| skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction({.fMax=1.f}), | |
| SkColor4f({0.75f, 0.5f, 0.25f, 1.f}), | |
| SkColor4f({0.75f, 0.75f, 0.75f, 1.f})); | |
| test("Min only", | |
| skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction({.fMin=1.f}), | |
| SkColor4f({0.75f, 0.5f, 0.25f, 1.f}), | |
| SkColor4f({0.25f, 0.25f, 0.25f, 1.f})); | |
| test("Component only", | |
| skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction({.fComponent=1.f}), | |
| SkColor4f({0.75f, 0.5f, 0.25f, 1.f}), | |
| SkColor4f({0.75f, 0.5f, 0.25f, 1.f})); | |
| test("CIE Y (luminance)", | |
| skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction( | |
| {.fRed=0.2627f, .fGreen=0.6780f, .fBlue=0.0593f}), | |
| SkColor4f({0.75f, 0.5f, 0.25f, 0.125f}), | |
| SkColor4f({0.55085f, 0.55085f, 0.55085f, 0.125f})); | |
| test("max-component", | |
| skhdr::AdaptiveGlobalToneMap::ComponentMixingFunction({.fMax=0.75f, .fComponent=0.25f}), | |
| SkColor4f({0.75f, 0.5f, 0.25f, 0.125f}), | |
| SkColor4f({0.75f, 0.6875f, 0.6250f, 0.125f})); | |
| } | |
| DEF_TEST(HdrMetadata_Agtm_RWTMO, r) { | |
| skhdr::AdaptiveGlobalToneMap agtm = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 1.f, | |
| }} | |
| }; | |
| auto& hatm = agtm.fHeadroomAdaptiveToneMap.value(); | |
| skhdr::AgtmHelpers::PopulateUsingRwtmo(hatm); | |
| REPORTER_ASSERT(r, memcmp(&hatm.fGainApplicationSpacePrimaries, &SkNamedPrimaries::kRec2020, | |
| sizeof(SkColorSpacePrimaries)) == 0); | |
| REPORTER_ASSERT(r, hatm.fAlternateImages.size() == 2); | |
| REPORTER_ASSERT(r, hatm.fAlternateImages[0].fHdrHeadroom == 0.f); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(hatm.fAlternateImages[1].fHdrHeadroom, | |
| 0.6151137835929048f)); | |
| static constexpr std::array<std::array<const float, 8>, 2> xExpected = {{ | |
| {1.00000f, 1.06461f, 1.15531f, 1.27209f, 1.41494f, 1.58388f, 1.77890f, 2.00000f}, | |
| {1.00000f, 1.10504f, 1.22269f, 1.35294f, 1.49580f, 1.65126f, 1.81933f, 2.00000f}, | |
| }}; | |
| static constexpr std::array<std::array<const float, 8>, 2> yExpected = {{ | |
| {-0.35356f, -0.37367f, -0.42913f, -0.51246f, -0.61663f, -0.73563f, -0.86465f, -1.00000f}, | |
| { 0.00000f, -0.01253f, -0.04583f, -0.09477f, -0.15559f, -0.22550f, -0.30244f, -0.38489f}, | |
| }}; | |
| static constexpr std::array<std::array<const float, 8>, 2> mExpected = {{ | |
| {0.00000f, -0.50266f, -0.68079f, -0.73059f, -0.72159f, -0.68535f, -0.63784f, -0.58742f}, | |
| {0.00000f, -0.21470f, -0.33759f, -0.40581f, -0.44088f, -0.45573f, -0.45828f, -0.45351f}, | |
| }}; | |
| for (size_t a = 0; a < 2; ++a) { | |
| const auto& cubic = hatm.fAlternateImages[a].fColorGainFunction.fGainCurve; | |
| REPORTER_ASSERT(r, cubic.fControlPoints.size() == 8u); | |
| for (size_t c = 0; c < 8; ++c) { | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(xExpected[a][c], cubic.fControlPoints[c].fX)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(yExpected[a][c], cubic.fControlPoints[c].fY)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(mExpected[a][c], cubic.fControlPoints[c].fM)); | |
| } | |
| } | |
| } | |
| DEF_TEST(HdrMetadata_Agtm_Weighting, r) { | |
| skhdr::AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap hatm; | |
| auto test = [&r, &hatm](const std::string& name, | |
| float targetedHdrHeadroom, | |
| const skhdr::AgtmHelpers::Weighting& wExpected) { | |
| skiatest::ReporterContext ctx(r, name); | |
| skhdr::AgtmHelpers::Weighting w = skhdr::AgtmHelpers::ComputeWeighting( | |
| hatm, targetedHdrHeadroom); | |
| REPORTER_ASSERT(r, w.fWeight[0] == wExpected.fWeight[0]); | |
| REPORTER_ASSERT(r, w.fWeight[1] == wExpected.fWeight[1]); | |
| REPORTER_ASSERT(r, w.fAlternateImageIndex[0] == wExpected.fAlternateImageIndex[0]); | |
| REPORTER_ASSERT(r, w.fAlternateImageIndex[1] == wExpected.fAlternateImageIndex[1]); | |
| }; | |
| // Tests with a single baseline representation. | |
| hatm.fBaselineHdrHeadroom = 1.f; | |
| test("base-1, target-0", 0.f, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| test("base-1, target-1", 1.f, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| test("base-2, target-2", 2.f, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| // Tests with a baseline and an alternate representation. | |
| hatm.fBaselineHdrHeadroom = 1.f; | |
| hatm.fAlternateImages = { | |
| { .fHdrHeadroom = 0.f }, | |
| }; | |
| test("base-1-alt0, target-0", 0.f, | |
| {{0, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {1.f, 0.f}}); | |
| test("base-1-alt0, target-0.25", 0.25f, | |
| {{0, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.75f, 0.f}}); | |
| test("base-1-alt0, target-1", 1.f, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| test("base-1-alt0, target-1.25", 1.25f, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| // Two alternate representations. | |
| hatm.fBaselineHdrHeadroom = 1.f; | |
| hatm.fAlternateImages = { | |
| { .fHdrHeadroom = 0.f }, | |
| { .fHdrHeadroom = 2.f }, | |
| }; | |
| test("base-1-alt0-alt2, target-0", 0.f, | |
| {{0, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {1.f, 0.f}}); | |
| test("base-1-alt0-alt2, target-0.25", 0.25f, | |
| {{0, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.75f, 0.f}}); | |
| test("base-1-alt0-alt2, target-1", 1.f, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| test("base-1-alt0-alt2, target-1.25", 1.25f, | |
| {{1, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.25f, 0.f}}); | |
| test("base-1-alt0-alt2, target-2", 2.f, | |
| {{1, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {1.f, 0.f}}); | |
| test("base-1-alt0-alt2, target-3", 3.f, | |
| {{1, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {1.f, 0.f}}); | |
| // Two alternate representations again, now mix-able. | |
| hatm.fBaselineHdrHeadroom = 2.f; | |
| hatm.fAlternateImages = { | |
| { .fHdrHeadroom = 0.f }, | |
| { .fHdrHeadroom = 1.f }, | |
| }; | |
| test("base-2-alt0-alt1, target-0.25", 0.25f, | |
| {{0, 1}, | |
| {0.75f, 0.25f}}); | |
| test("base-2-alt0-alt1, target-neg", -1.f, | |
| {{0, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {1.f, 0.f}}); | |
| test("base-2-alt0-alt1, target-inf", SK_FloatInfinity, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| test("base-2-alt0-alt1, target-neg-inf", SK_FloatNegativeInfinity, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| test("base-2-alt0-alt1, target-nan", SK_FloatNaN, | |
| {{skhdr::AgtmHelpers::Weighting::kInvalidIndex, skhdr::AgtmHelpers::Weighting::kInvalidIndex}, | |
| {0.f, 0.f}}); | |
| } | |
| static void assert_agtms_equal(skiatest::Reporter* r, | |
| const skhdr::AdaptiveGlobalToneMap& agtmIn, | |
| const skhdr::AdaptiveGlobalToneMap& agtmOut) { | |
| // Allow error for headrooms, x, and y to twice the their encoding step. | |
| constexpr float kHeadroomError = 0.0002f; | |
| constexpr float kXError = 0.002f; | |
| constexpr float kYError = 0.0002f; | |
| // The slope is non-uniformly sampled. It has higher precision than the Y values near 0, but | |
| // lower precision at higher values. | |
| constexpr float kMError = 0.0004f; | |
| REPORTER_ASSERT(r, agtmIn.fHdrReferenceWhite == agtmOut.fHdrReferenceWhite); | |
| REPORTER_ASSERT(r, agtmIn.fHeadroomAdaptiveToneMap.has_value() == | |
| agtmOut.fHeadroomAdaptiveToneMap.has_value()); | |
| if (!agtmIn.fHeadroomAdaptiveToneMap.has_value()) { | |
| return; | |
| } | |
| const auto& hatmIn = agtmIn.fHeadroomAdaptiveToneMap.value(); | |
| const auto& hatmOut = agtmOut.fHeadroomAdaptiveToneMap.value(); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual( | |
| hatmIn.fBaselineHdrHeadroom, hatmOut.fBaselineHdrHeadroom, kHeadroomError)); | |
| REPORTER_ASSERT(r, hatmIn.fGainApplicationSpacePrimaries == | |
| hatmOut.fGainApplicationSpacePrimaries); | |
| REPORTER_ASSERT(r, hatmIn.fAlternateImages.size() == hatmOut.fAlternateImages.size()); | |
| if (hatmIn.fAlternateImages.size() != hatmOut.fAlternateImages.size()) { | |
| return; | |
| } | |
| for (size_t a = 0; a < hatmIn.fAlternateImages.size(); ++a) { | |
| const auto& altrIn = hatmIn.fAlternateImages[a]; | |
| const auto& altrOut = hatmOut.fAlternateImages[a]; | |
| skiatest::ReporterContext ctxA( | |
| r, SkStringPrintf("AlternateImage:a=%d", static_cast<int>(a))); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual( | |
| altrIn.fHdrHeadroom, altrOut.fHdrHeadroom, kHeadroomError)); | |
| auto& mixIn = altrIn.fColorGainFunction.fComponentMixing; | |
| auto& mixOut = altrOut.fColorGainFunction.fComponentMixing; | |
| REPORTER_ASSERT(r, mixIn.fRed == mixOut.fRed); | |
| REPORTER_ASSERT(r, mixIn.fGreen == mixOut.fGreen); | |
| REPORTER_ASSERT(r, mixIn.fBlue == mixOut.fBlue); | |
| REPORTER_ASSERT(r, mixIn.fMax == mixOut.fMax); | |
| REPORTER_ASSERT(r, mixIn.fMin == mixOut.fMin); | |
| REPORTER_ASSERT(r, mixIn.fComponent == mixOut.fComponent); | |
| auto& curveIn = altrIn.fColorGainFunction.fGainCurve; | |
| auto& curveOut = altrOut.fColorGainFunction.fGainCurve; | |
| REPORTER_ASSERT(r, curveIn.fControlPoints.size() == curveOut.fControlPoints.size()); | |
| if (curveIn.fControlPoints.size() != curveOut.fControlPoints.size()) { | |
| return; | |
| } | |
| for (uint8_t c = 0; c < curveIn.fControlPoints.size(); ++c) { | |
| skiatest::ReporterContext ctxC(r, SkStringPrintf("ControlPoint:c=%u", c)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual( | |
| curveIn.fControlPoints[c].fX, curveOut.fControlPoints[c].fX, kXError)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual( | |
| curveIn.fControlPoints[c].fY, curveOut.fControlPoints[c].fY, kYError)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual( | |
| curveIn.fControlPoints[c].fM, curveOut.fControlPoints[c].fM, kMError)); | |
| } | |
| } | |
| } | |
| // Test round-trip serialization of AGTM metadata. | |
| DEF_TEST(HdrMetadata_Agtm_RoundTripSerialize, r) { | |
| struct Test { | |
| // The name of the test. | |
| const char* name; | |
| // The AGTM data that we will serialize and deserialized. | |
| skhdr::AdaptiveGlobalToneMap agtm; | |
| // Different binary encodings, which should all produce `agtm`, and some of them may be | |
| // bit-exact the same as `agtm.serialize()`. | |
| struct Encoding { | |
| // The name of the serialization, if it has some unique properties. | |
| const char* name = nullptr; | |
| // If true, then `data` should equal `agtm.serialize()`. | |
| const bool is_default_encoding = true; | |
| // The serialized data. | |
| std::vector<uint8_t> data; | |
| }; | |
| std::vector<Encoding> encodings; | |
| }; | |
| auto get_rwtmo = [](float hdr_reference_white, float headroom) { | |
| skhdr::AdaptiveGlobalToneMap agtm = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = headroom | |
| }} | |
| }; | |
| skhdr::AgtmHelpers::PopulateUsingRwtmo(agtm.fHeadroomAdaptiveToneMap.value()); | |
| return agtm; | |
| }; | |
| Test tests[] = { | |
| { | |
| .name = "NoAdaptiveToneMap-DefaultWhite", | |
| .agtm = {}, | |
| .encodings = { | |
| { | |
| .data = { 0x00, 0x00, }, | |
| }, | |
| // The application_version syntax element is 7. Because minimum_application_version | |
| // is 0, we should still parse this. See Clause C.2.1. | |
| { | |
| .name = "application_version=7", | |
| .is_default_encoding = false, | |
| .data = { 0xe0, 0x00, }, | |
| }, | |
| }, | |
| }, | |
| { | |
| .name = "NoAdaptiveToneMap-123-White", | |
| .agtm = { .fHdrReferenceWhite = 123.f, }, | |
| .encodings = {{ | |
| .data = { 0x00, 0x80, 0x02, 0x67, }, | |
| }}, | |
| }, | |
| { | |
| .name = "NoAdaptiveToneMap-MinWhite", | |
| .agtm = { .fHdrReferenceWhite = 0.2f, }, | |
| .encodings = { | |
| { | |
| .data = { 0x00, 0x80, 0x00, 0x01, }, | |
| }, | |
| // The hdr_reference_white syntax element is 0 but clamped to 1 by Clause C.3.3. | |
| { | |
| .name = "hdr_reference_white=0", | |
| .is_default_encoding = false, | |
| .data = { 0x00, 0x80, 0x00, 0x00, }, | |
| }, | |
| }, | |
| }, | |
| { | |
| .name = "NoAdaptiveToneMap-MaxWhite", | |
| .agtm = { .fHdrReferenceWhite = 10000.f, }, | |
| .encodings = { | |
| { | |
| .data = { 0x00, 0x80, 0xc3, 0x50, }, | |
| }, | |
| // The hdr_reference_white syntax element is 65535 but clamped to 50000 by Clause | |
| // C.3.3. | |
| { | |
| .name = "hdr_reference_white=65535", | |
| .is_default_encoding = false, | |
| .data = { 0x00, 0x80, 0xff, 0xff, }, | |
| }, | |
| }, | |
| }, | |
| { | |
| .name = "RWTMO-min-headroom", | |
| .agtm = get_rwtmo(203.f, 0.f), | |
| .encodings = { | |
| { | |
| .data = { 0x00, 0x40, 0x00, 0x00, 0x80, }, | |
| }, | |
| }, | |
| }, | |
| { | |
| .name = "RWTMO-mid-headroom", | |
| .agtm = get_rwtmo(203.f, 3.f), | |
| .encodings = { | |
| { | |
| .data = { 0x00, 0x40, 0x75, 0x30, 0x80 }, | |
| } | |
| }, | |
| }, | |
| { | |
| .name = "RWTMO-max-headroom", | |
| .agtm = get_rwtmo(203.f, 6.f), | |
| .encodings = { | |
| { | |
| .data = { 0x00, 0x40, 0xea, 0x60, 0x80 }, | |
| }, | |
| // The baseline_hdr_headroom syntax element is 65535 but clamped to 60000 by Clause | |
| // C.3.4. | |
| { | |
| .name = "baseline_hdr_headroom=65535", | |
| .is_default_encoding = false, | |
| .data = { 0x00, 0x40, 0xff, 0xff, 0x80 }, | |
| } | |
| }, | |
| }, | |
| { | |
| .name = "ClampInRec601", | |
| .agtm = { | |
| .fHdrReferenceWhite = 100.f, | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 2.f, | |
| .fGainApplicationSpacePrimaries = SkNamedPrimaries::kRec601, | |
| }}, | |
| }, | |
| .encodings = { | |
| { | |
| .data = { | |
| 0x00, 0xc0, 0x01, 0xf4, 0x4e, 0x20, 0x0c, 0x7b, 0x0c, 0x42, 0x68, 0x3c, | |
| 0x8c, 0x74, 0x36, 0x1e, 0x46, 0x0d, 0xac, 0x3d, 0x13, 0x40, 0x42, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| .name = "OneAlternates", | |
| .agtm = { | |
| .fHdrReferenceWhite = 400.f, | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 4.f, | |
| .fGainApplicationSpacePrimaries = SkNamedPrimaries::kSMPTE_EG_432_1, | |
| .fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 0.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = {.fMax = 1.f}, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| {1.f, 0.f, 0.f}, | |
| {16.f, -4.f, 0.f}, | |
| } | |
| } | |
| } | |
| } | |
| }, | |
| }} | |
| }, | |
| .encodings = { | |
| { | |
| .data = { | |
| 0x00, 0xc0, 0x07, 0xd0, 0x9c, 0x40, 0x14, 0x00, 0x00, 0x00, 0x08, 0x03, | |
| 0xe8, 0x3e, 0x80, 0x00, 0x00, 0x9c, 0x40, 0x46, 0x50, 0x46, 0x50, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| .name = "OneAlternates-MaxValues", | |
| .agtm = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 6.f, | |
| .fGainApplicationSpacePrimaries = SkNamedPrimaries::kRec2020, | |
| .fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 0.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = {.fMax = 1.f}, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| {64.f, -6.f, 0.f}, | |
| } | |
| } | |
| } | |
| } | |
| }, | |
| }} | |
| }, | |
| .encodings = { | |
| { | |
| .data = { | |
| 0x00, 0x40, 0xea, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0xea, | |
| 0x60, 0x46, 0x50, | |
| }, | |
| }, | |
| // The gain_curve_control_points_x and gain_curve_control_points_y syntax elements | |
| // are 65535 but are clamped to 64000 and 60000 by Clause C.3.7. | |
| { | |
| .name = "gain_curve_control_points_x/y=65535", | |
| .is_default_encoding = false, | |
| .data = { | |
| 0x00, 0x40, 0xea, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, | |
| 0xff, 0x46, 0x50, | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| .name = "FourAlternates", | |
| .agtm = { | |
| .fHdrReferenceWhite = 400.f, | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 2.f, | |
| .fGainApplicationSpacePrimaries = SkNamedPrimaries::kSMPTE_EG_432_1, | |
| .fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 0.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = { .fMax = 0.75f, .fMin = 0.25f }, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| { .fX = 0.f, .fY = -1.f, .fM = 0.f }, | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| .fHdrHeadroom = 1.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = { .fMax = 1.f, }, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| { .fX = 0.f, .fY = -1.f, .fM = 0.f }, | |
| { .fX = 1.f, .fY = -0.5f, .fM = 0.1f }, | |
| { .fX = 2.f, .fY = -0.4f, .fM = 0.2f }, | |
| { .fX = 3.f, .fY = -0.3f, .fM = 0.3f }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| .fHdrHeadroom = 3.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = { .fComponent = 1.f, }, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| { .fX = 0.f, .fY = 1.f, .fM = 0.f }, | |
| { .fX = 1.f, .fY = 0.5f, .fM = 0.1f }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| .fHdrHeadroom = 4.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = { | |
| .fRed = 0.3f, | |
| .fGreen = 0.6f, | |
| .fBlue = 0.1f, | |
| }, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| { .fX = 0.f, .fY = 1.f, .fM = 0.f }, | |
| { .fX = 1.f, .fY = 0.5f, .fM = 0.1f }, | |
| { .fX = 2.f, .fY = 0.4f, .fM = 0.5f }, | |
| { .fX = 2.f, .fY = 0.4f, .fM = 0.7f }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }}, | |
| }, | |
| .encodings = { | |
| { | |
| .data = { | |
| 0x00, 0xc0, 0x07, 0xd0, 0x4e, 0x20, 0x44, 0x00, 0x00, 0xc6, 0x92, 0x7c, | |
| 0x30, 0xd4, 0x00, 0x00, 0x00, 0x27, 0x10, 0x46, 0x50, 0x27, 0x10, 0x00, | |
| 0x18, 0x00, 0x00, 0x03, 0xe8, 0x07, 0xd0, 0x0b, 0xb8, 0x27, 0x10, 0x13, | |
| 0x88, 0x0f, 0xa0, 0x0b, 0xb8, 0x46, 0x50, 0x4a, 0xc6, 0x4f, 0x26, 0x53, | |
| 0x5c, 0x75, 0x30, 0x40, 0x08, 0x00, 0x00, 0x03, 0xe8, 0x27, 0x10, 0x13, | |
| 0x88, 0x46, 0x50, 0x4a, 0xc6, 0x9c, 0x40, 0xf8, 0x3a, 0x98, 0x75, 0x30, | |
| 0x13, 0x88, 0x18, 0x00, 0x00, 0x03, 0xe8, 0x07, 0xd0, 0x07, 0xd0, 0x27, | |
| 0x10, 0x13, 0x88, 0x0f, 0xa0, 0x0f, 0xa0, 0x46, 0x50, 0x4a, 0xc6, 0x5b, | |
| 0x11, 0x61, 0xa6, | |
| }, | |
| }, | |
| // The num_alternate_images syntax element is 7 but clamped to 4 by Clause C.3.4. | |
| // The difference from the above is the 0x74 element, which was previously 0x44. | |
| { | |
| .name = "num_alternate_images=7", | |
| .is_default_encoding = false, | |
| .data = { | |
| 0x00, 0xc0, 0x07, 0xd0, 0x4e, 0x20, 0x74, 0x00, 0x00, 0xc6, 0x92, 0x7c, | |
| 0x30, 0xd4, 0x00, 0x00, 0x00, 0x27, 0x10, 0x46, 0x50, 0x27, 0x10, 0x00, | |
| 0x18, 0x00, 0x00, 0x03, 0xe8, 0x07, 0xd0, 0x0b, 0xb8, 0x27, 0x10, 0x13, | |
| 0x88, 0x0f, 0xa0, 0x0b, 0xb8, 0x46, 0x50, 0x4a, 0xc6, 0x4f, 0x26, 0x53, | |
| 0x5c, 0x75, 0x30, 0x40, 0x08, 0x00, 0x00, 0x03, 0xe8, 0x27, 0x10, 0x13, | |
| 0x88, 0x46, 0x50, 0x4a, 0xc6, 0x9c, 0x40, 0xf8, 0x3a, 0x98, 0x75, 0x30, | |
| 0x13, 0x88, 0x18, 0x00, 0x00, 0x03, 0xe8, 0x07, 0xd0, 0x07, 0xd0, 0x27, | |
| 0x10, 0x13, 0x88, 0x0f, 0xa0, 0x0f, 0xa0, 0x46, 0x50, 0x4a, 0xc6, 0x5b, | |
| 0x11, 0x61, 0xa6, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| // This is an inverse tone map, computed using the script used to generate the figures | |
| // used in SMPTE ST 2094-50. This also tests using 32 control points (the maximum | |
| // allowable). | |
| .name = "InverseToneMap", | |
| .agtm = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 0.f, | |
| .fGainApplicationSpacePrimaries = SkNamedPrimaries::kSMPTE_EG_432_1, | |
| .fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 2.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = { | |
| .fMax = 1.f, | |
| }, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| { .fX = 0.5f, .fY = 0.9998566f, .fM = 0.0015297f }, | |
| { .fX = 0.5208116f, .fY = 1.0027409f, .fM = 0.2734709f }, | |
| { .fX = 0.5413111f, .fY = 1.0109166f, .fM = 0.5211183f }, | |
| { .fX = 0.5614984f, .fY = 1.0237336f, .fM = 0.7456896f }, | |
| { .fX = 0.5813735f, .fY = 1.0406077f, .fM = 0.9495246f }, | |
| { .fX = 0.6009365f, .fY = 1.0610152f, .fM = 1.1343419f }, | |
| { .fX = 0.6201873f, .fY = 1.0844893f, .fM = 1.3021958f }, | |
| { .fX = 0.6391259f, .fY = 1.1106143f, .fM = 1.4547685f }, | |
| { .fX = 0.6577523f, .fY = 1.1390220f, .fM = 1.5937591f }, | |
| { .fX = 0.6760665f, .fY = 1.1693869f, .fM = 1.7207136f }, | |
| { .fX = 0.6940686f, .fY = 1.2014223f, .fM = 1.8370443f }, | |
| { .fX = 0.7117585f, .fY = 1.2348758f, .fM = 1.9440458f }, | |
| { .fX = 0.7291363f, .fY = 1.2695263f, .fM = 2.0429054f }, | |
| { .fX = 0.7462018f, .fY = 1.3051801f, .fM = 2.1347281f }, | |
| { .fX = 0.7629552f, .fY = 1.3416678f, .fM = 2.2204760f }, | |
| { .fX = 0.7793964f, .fY = 1.3788421f, .fM = 2.3010690f }, | |
| { .fX = 0.7955254f, .fY = 1.4165743f, .fM = 2.3773159f }, | |
| { .fX = 0.8113423f, .fY = 1.4547529f, .fM = 2.4499659f }, | |
| { .fX = 0.8268470f, .fY = 1.4932810f, .fM = 2.5197041f }, | |
| { .fX = 0.8420395f, .fY = 1.5320748f, .fM = 2.5871586f }, | |
| { .fX = 0.8569198f, .fY = 1.5710618f, .fM = 2.6529097f }, | |
| { .fX = 0.8714880f, .fY = 1.6101797f, .fM = 2.7174979f }, | |
| { .fX = 0.8857440f, .fY = 1.6493748f, .fM = 2.7814233f }, | |
| { .fX = 0.8996878f, .fY = 1.6886011f, .fM = 2.8451656f }, | |
| { .fX = 0.9133194f, .fY = 1.7278194f, .fM = 2.9091748f }, | |
| { .fX = 0.9266389f, .fY = 1.7669963f, .fM = 2.9738880f }, | |
| { .fX = 0.9396462f, .fY = 1.8061036f, .fM = 3.0397306f }, | |
| { .fX = 0.9523413f, .fY = 1.8451175f, .fM = 3.1071218f }, | |
| { .fX = 0.9647242f, .fY = 1.8840181f, .fM = 3.1764804f }, | |
| { .fX = 0.9767950f, .fY = 1.9227892f, .fM = 3.2482303f }, | |
| { .fX = 0.9885535f, .fY = 1.9614173f, .fM = 3.3228070f }, | |
| { .fX = 1.0000000f, .fY = 1.9998918f, .fM = 3.4006599f }, | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }} | |
| }, | |
| .encodings = { | |
| { | |
| .data = { | |
| 0x00, 0x40, 0x00, 0x00, 0x14, 0x4e, 0x20, 0x00, 0xf8, 0x01, 0xf4, 0x02, | |
| 0x09, 0x02, 0x1d, 0x02, 0x31, 0x02, 0x45, 0x02, 0x59, 0x02, 0x6c, 0x02, | |
| 0x7f, 0x02, 0x92, 0x02, 0xa4, 0x02, 0xb6, 0x02, 0xc8, 0x02, 0xd9, 0x02, | |
| 0xea, 0x02, 0xfb, 0x03, 0x0b, 0x03, 0x1c, 0x03, 0x2b, 0x03, 0x3b, 0x03, | |
| 0x4a, 0x03, 0x59, 0x03, 0x67, 0x03, 0x76, 0x03, 0x84, 0x03, 0x91, 0x03, | |
| 0x9f, 0x03, 0xac, 0x03, 0xb8, 0x03, 0xc5, 0x03, 0xd1, 0x03, 0xdd, 0x03, | |
| 0xe8, 0x27, 0x0f, 0x27, 0x2b, 0x27, 0x7d, 0x27, 0xfd, 0x28, 0xa6, 0x29, | |
| 0x72, 0x2a, 0x5d, 0x2b, 0x62, 0x2c, 0x7e, 0x2d, 0xae, 0x2e, 0xee, 0x30, | |
| 0x3d, 0x31, 0x97, 0x32, 0xfc, 0x34, 0x69, 0x35, 0xdc, 0x37, 0x56, 0x38, | |
| 0xd4, 0x3a, 0x55, 0x3b, 0xd9, 0x3d, 0x5f, 0x3e, 0xe6, 0x40, 0x6e, 0x41, | |
| 0xf6, 0x43, 0x7e, 0x45, 0x06, 0x46, 0x8d, 0x48, 0x13, 0x49, 0x98, 0x4b, | |
| 0x1c, 0x4c, 0x9e, 0x4e, 0x1f, 0x46, 0x62, 0x52, 0x43, 0x5b, 0xd1, 0x62, | |
| 0xfe, 0x68, 0x4f, 0x6c, 0x48, 0x6f, 0x50, 0x71, 0xab, 0x73, 0x8b, 0x75, | |
| 0x0f, 0x76, 0x50, 0x77, 0x5c, 0x78, 0x40, 0x79, 0x04, 0x79, 0xaf, 0x7a, | |
| 0x46, 0x7a, 0xcd, 0x7b, 0x47, 0x7b, 0xb7, 0x7c, 0x1d, 0x7c, 0x7d, 0x7c, | |
| 0xd7, 0x7d, 0x2d, 0x7d, 0x7f, 0x7d, 0xce, 0x7e, 0x1b, 0x7e, 0x66, 0x7e, | |
| 0xb0, 0x7e, 0xf9, 0x7f, 0x42, 0x7f, 0x8a, 0x7f, 0xd3, | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| .name = "Mix-Normalization", | |
| .agtm = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 1.f, | |
| .fGainApplicationSpacePrimaries = SkNamedPrimaries::kRec2020, | |
| .fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 0.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = {.fMax = 0.75f, .fComponent = 0.25f}, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| {1.f, -1.f, -0.5f}, | |
| } | |
| } | |
| } | |
| } | |
| }, | |
| }} | |
| }, | |
| .encodings = { | |
| { | |
| .is_default_encoding = false, | |
| .data = { | |
| 0x00, 0x40, 0x27, 0x10, 0x18, 0x00, 0x00, 0xc5, 0x00, 0x03, 0x00, 0x01, | |
| 0x00, 0x03, 0xe8, 0x27, 0x10, 0x31, 0x8f, | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| for (const auto& test : tests) { | |
| skiatest::ReporterContext ctx(r, test.name); | |
| // Serialize the `agtm` member, and verify the bits come out as expected. | |
| auto serialized = test.agtm.serialize(); | |
| REPORTER_ASSERT(r, serialized != nullptr); | |
| for (const auto& encoding : test.encodings) { | |
| skiatest::ReporterContext ctxSubSubTest(r, encoding.name ? encoding.name : "default"); | |
| // Parse `encoding.data`, and verify that parsing it matches `agtm`. | |
| skhdr::AdaptiveGlobalToneMap agtmParsed; | |
| auto encoding_data = SkData::MakeWithoutCopy( | |
| encoding.data.data(), encoding.data.size()); | |
| REPORTER_ASSERT(r, agtmParsed.parse(encoding_data.get())); | |
| assert_agtms_equal(r, test.agtm, agtmParsed); | |
| // If this is idempotent, then `encoding.data` should bit-equal `serialized`. | |
| REPORTER_ASSERT(r, encoding.is_default_encoding == | |
| SkData::Equals(serialized.get(), encoding_data.get())); | |
| } | |
| } | |
| } | |
| // Test the logic to apply the AGTM tone mapping. | |
| DEF_TEST(HdrMetadata_Agtm_Apply_and_Shader, r) { | |
| // This will tone map several input colors to different targeted HDR headrooms using this | |
| // RWTMO metadata. | |
| skhdr::AdaptiveGlobalToneMap agtm = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 2.f, | |
| }} | |
| }; | |
| auto& hatm = agtm.fHeadroomAdaptiveToneMap.value(); | |
| skhdr::AgtmHelpers::PopulateUsingRwtmo(hatm); | |
| // We will use the following input pixel values in gain application color space. These include | |
| // monochrome and non-monochrome values, as well as values that are less than white (less than | |
| // 1) and brighter than white (greater than 1). | |
| constexpr size_t kNumTestColors = 6; | |
| SkColor4f inputTestColors[kNumTestColors] = { | |
| {1.00f, 1.00f, 1.00f, 1.f}, | |
| {1.00f, 0.50f, 0.25f, 1.f}, | |
| {4.00f, 4.00f, 4.00f, 1.f}, | |
| {1.00f, 2.00f, 4.00f, 1.f}, | |
| {0.50f, 0.50f, 0.50f, 1.f}, | |
| {2.00f, 2.00f, 2.00f, 1.f}, | |
| }; | |
| // We will test applying the gain for the following targetd HDR headroom values. | |
| constexpr size_t kNumTests = 5; | |
| const std::array<float, kNumTests> testTargetedHdrHeadrooms = { | |
| 0.f, | |
| 1.f, | |
| hatm.fAlternateImages[1].fHdrHeadroom, | |
| std::log2(3.f), | |
| 2.f, | |
| }; | |
| // These are the expected output pixel values for each of the targted HDR headrooms. | |
| SkColor4f expectedTestColors[kNumTests][kNumTestColors] = { | |
| { | |
| {0.565302f, 0.565302f, 0.565302f, 1.f}, | |
| {0.565302f, 0.282651f, 0.141326f, 1.f}, | |
| {1.000000f, 1.000000f, 1.000000f, 1.f}, | |
| {0.250000f, 0.500000f, 1.000000f, 1.f}, | |
| {0.282651f, 0.282651f, 0.282651f, 1.f}, | |
| {0.815278f, 0.815278f, 0.815278f, 1.f}, | |
| }, | |
| { | |
| {0.898755f, 0.898755f, 0.898755f, 1.f}, | |
| {0.898755f, 0.449377f, 0.224689f, 1.f}, | |
| {2.000000f, 2.000000f, 2.000000f, 1.f}, | |
| {0.500000f, 1.000000f, 2.000000f, 1.f}, | |
| {0.449377f, 0.449377f, 0.449377f, 1.f}, | |
| {1.471569f, 1.471569f, 1.471569f, 1.f}, | |
| }, | |
| { | |
| {1.000000f, 1.000000f, 1.000000f, 1.f}, | |
| {1.000000f, 0.500000f, 0.250000f, 1.f}, | |
| {2.346040f, 2.346040f, 2.346040f, 1.f}, | |
| {0.586510f, 1.173020f, 2.346040f, 1.f}, | |
| {0.500000f, 0.500000f, 0.500000f, 1.f}, | |
| {1.685886f, 1.685886f, 1.685886f, 1.f}, | |
| }, | |
| { | |
| {1.000000f, 1.000000f, 1.000000f, 1.f}, | |
| {1.000000f, 0.500000f, 0.250000f, 1.f}, | |
| {3.000000f, 3.000000f, 3.000000f, 1.f}, | |
| {0.750000f, 1.500000f, 3.000000f, 1.f}, | |
| {0.500000f, 0.500000f, 0.500000f, 1.f}, | |
| {1.823991f, 1.823991f, 1.823991f, 1.f}, | |
| }, | |
| { | |
| {1.00f, 1.00f, 1.00f, 1.f}, | |
| {1.00f, 0.50f, 0.25f, 1.f}, | |
| {4.00f, 4.00f, 4.00f, 1.f}, | |
| {1.00f, 2.00f, 4.00f, 1.f}, | |
| {0.50f, 0.50f, 0.50f, 1.f}, | |
| {2.00f, 2.00f, 2.00f, 1.f}, | |
| }, | |
| }; | |
| // All of the math is done with at least half-precision. Given the range of values we are in | |
| // (not far from 1), we should maintain at least ten bit precision. | |
| constexpr float kEpsilon = 1.f/1024.f; | |
| // Test the AgtmHelpers::ApplyGain function. | |
| for (size_t t = 0; t < kNumTests; ++t) { | |
| const auto targetedHdrHeadroom = testTargetedHdrHeadrooms[t]; | |
| skiatest::ReporterContext ctx( | |
| r, | |
| SkStringPrintf("AgtmHelpers::ApplyGain, targetedHdrHeadroom:%f", targetedHdrHeadroom)); | |
| // Copy the inputTextColors to outputTestColors (because ApplyGain works in-place). | |
| SkColor4f outputTestColors[kNumTestColors]; | |
| for (size_t i = 0; i < kNumTestColors; ++i) { | |
| outputTestColors[i] = inputTestColors[i]; | |
| } | |
| // Apply the tone mapping gain in-place on outputTestColors. | |
| skhdr::AgtmHelpers::ApplyGain(hatm, | |
| SkSpan<SkColor4f>(outputTestColors, kNumTestColors), | |
| targetedHdrHeadroom); | |
| // Verify the result matches expectations. | |
| for (size_t i = 0; i < kNumTestColors; ++i) { | |
| const auto& output = outputTestColors[i]; | |
| const auto& expected = expectedTestColors[t][i]; | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(output.fR, expected.fR, kEpsilon)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(output.fG, expected.fG, kEpsilon)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(output.fB, expected.fB, kEpsilon)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(output.fA, expected.fA, kEpsilon)); | |
| } | |
| } | |
| // Test using an SkColorFilter from skhdr::Metadata. | |
| for (size_t t = 0; t < kNumTests; ++t) { | |
| const auto targetedHdrHeadroom = testTargetedHdrHeadrooms[t]; | |
| skiatest::ReporterContext ctx( | |
| r, SkStringPrintf("skhdr::Metadata::makeToneMapColorFilter, targetedHdrHeadroom:%f", | |
| targetedHdrHeadroom)); | |
| // The input and output images will be kNumTestColors-by-1. | |
| const auto info = SkImageInfo::Make( | |
| kNumTestColors, 1, | |
| kRGBA_F32_SkColorType, kPremul_SkAlphaType, | |
| skhdr::AgtmHelpers::GetGainApplicationSpace(hatm)); | |
| // Create an SkImage that references the inputTestColors array directly. | |
| const auto inputImage = SkImages::RasterFromData( | |
| info, | |
| SkData::MakeWithoutCopy(inputTestColors, sizeof(inputTestColors)), | |
| info.minRowBytes()); | |
| constexpr size_t kNumInputImages = 3; | |
| std::array<const char*, kNumInputImages> inputImageNames = { | |
| "linear", | |
| "pq", | |
| "pq-100", | |
| }; | |
| std::array<sk_sp<SkImage>, kNumInputImages> inputImages; | |
| inputImages[0] = inputImage; | |
| inputImages[1] = inputImage->makeColorSpace( | |
| nullptr, SkColorSpace::MakeRGB(SkNamedTransferFn::kPQ, SkNamedGamut::kRec2020), {}); | |
| { | |
| skcms_TransferFunction pq100; | |
| skcms_TransferFunction_makePQ(&pq100, 100); | |
| inputImages[2] = inputImages[1]->reinterpretColorSpace( | |
| SkColorSpace::MakeRGB(pq100, SkNamedGamut::kRec2020)); | |
| } | |
| for (size_t s = 0; s < kNumInputImages; ++s) { | |
| skiatest::ReporterContext subCtx( | |
| r, SkStringPrintf("inputImage:%s", inputImageNames[s])); | |
| // Create an output SkBitmap to draw into. | |
| SkBitmap bm; | |
| bm.allocPixels(info); | |
| // Call drawImage, using the color filter created by Agtm::makeColorFilter. | |
| { | |
| skhdr::Metadata metadata; | |
| metadata.setAdaptiveGlobalToneMap(agtm); | |
| auto colorFilter = metadata.makeToneMapColorFilter( | |
| targetedHdrHeadroom, inputImages[s]->colorSpace()); | |
| SkPaint paint; | |
| SkASSERT(colorFilter); | |
| paint.setColorFilter(colorFilter); | |
| auto canvas = SkCanvas::MakeRasterDirect(bm.info(), bm.getPixels(), bm.rowBytes()); | |
| canvas->drawImage(inputImages[s].get(), 0, 0, SkSamplingOptions(), &paint); | |
| } | |
| // Verify that the pixels written into the SkBitmap match the expected values. | |
| for (size_t i = 0; i < kNumTestColors; ++i) { | |
| // There is more error in the PQ transfer function. | |
| constexpr float kLooserEpsilon = 1.f/100.f; | |
| const auto& output = *reinterpret_cast<const SkColor4f*>(bm.getAddr(i, 0)); | |
| const auto& expected = expectedTestColors[t][i]; | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(output.fR, expected.fR, kLooserEpsilon)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(output.fG, expected.fG, kLooserEpsilon)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(output.fB, expected.fB, kLooserEpsilon)); | |
| REPORTER_ASSERT(r, SkScalarNearlyEqual(output.fA, expected.fA, kLooserEpsilon)); | |
| } | |
| } | |
| } | |
| } | |
| DEF_TEST(HdrMetadata_ShaderParams, r) { | |
| sk_sp<SkColorSpace> cs_srgb = SkColorSpace::MakeSRGB(); | |
| sk_sp<SkColorSpace> cs_pq = SkColorSpace::MakeRGB( | |
| SkNamedTransferFn::kPQ, SkNamedGamut::kRec2020); | |
| sk_sp<SkColorSpace> cs_pq100; | |
| { | |
| skcms_TransferFunction pq100; | |
| skcms_TransferFunction_makePQ(&pq100, 100); | |
| cs_pq100 = SkColorSpace::MakeRGB(pq100, SkNamedGamut::kRec2020); | |
| } | |
| // Start with CLLI and MDCV metadata. | |
| skhdr::Metadata metadata; | |
| { | |
| skhdr::ContentLightLevelInformation clli; | |
| clli.fMaxCLL = 406.f; | |
| metadata.setContentLightLevelInformation(clli); | |
| skhdr::MasteringDisplayColorVolume mdcv; | |
| mdcv.fMaximumDisplayMasteringLuminance = 812.f; | |
| metadata.setMasteringDisplayColorVolume(mdcv); | |
| } | |
| skhdr::AdaptiveGlobalToneMap toneMapAgtm; | |
| float scaleFactor = 1.f; | |
| // Because this has no AGTM metadata, SDR inputs get no tone mapping shader. | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::PopulateToneMapAgtmParams( | |
| metadata, cs_srgb.get(), &toneMapAgtm, &scaleFactor)); | |
| // This will have headroom log2(406/203)=1 for PQ. | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::PopulateToneMapAgtmParams( | |
| metadata, cs_pq.get(), &toneMapAgtm, &scaleFactor)); | |
| REPORTER_ASSERT(r, scaleFactor == 1.f); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap.has_value()); | |
| REPORTER_ASSERT(r, | |
| toneMapAgtm.fHeadroomAdaptiveToneMap->fBaselineHdrHeadroom == 1.f); | |
| // This will have headroom log2(406/100) for PQ with 100 nit white. | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::PopulateToneMapAgtmParams( | |
| metadata, cs_pq100.get(), &toneMapAgtm, &scaleFactor)); | |
| REPORTER_ASSERT(r, scaleFactor == 1.f); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap.has_value()); | |
| REPORTER_ASSERT(r, | |
| toneMapAgtm.fHeadroomAdaptiveToneMap->fBaselineHdrHeadroom == std::log2(4.06f)); | |
| // Invalidate the CLLI metadata. | |
| { | |
| skhdr::ContentLightLevelInformation clli; | |
| clli.fMaxCLL = 0.f; | |
| metadata.setContentLightLevelInformation(clli); | |
| } | |
| // This will now have headroom log2(812/203)=2 for PQ. | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::PopulateToneMapAgtmParams( | |
| metadata, cs_pq.get(), &toneMapAgtm, &scaleFactor)); | |
| REPORTER_ASSERT(r, scaleFactor == 1.f); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap.has_value()); | |
| REPORTER_ASSERT(r, | |
| toneMapAgtm.fHeadroomAdaptiveToneMap->fBaselineHdrHeadroom == 2.f); | |
| // Set AGTM metadata with just white level set. | |
| { | |
| skhdr::AdaptiveGlobalToneMap agtm; | |
| agtm.fHdrReferenceWhite = 100.f; | |
| metadata.setAdaptiveGlobalToneMap(agtm); | |
| } | |
| // PQ input at 203 nits will be scaled now. | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::PopulateToneMapAgtmParams( | |
| metadata, cs_pq.get(), &toneMapAgtm, &scaleFactor)); | |
| REPORTER_ASSERT(r, scaleFactor == 203.f/100.f); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap.has_value()); | |
| REPORTER_ASSERT(r, | |
| toneMapAgtm.fHeadroomAdaptiveToneMap->fBaselineHdrHeadroom == std::log2(812.f / 100.f)); | |
| // PQ input at 100 nits will not be scaled now. | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::PopulateToneMapAgtmParams( | |
| metadata, cs_pq100.get(), &toneMapAgtm, &scaleFactor)); | |
| REPORTER_ASSERT(r, scaleFactor == 1.f); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap.has_value()); | |
| REPORTER_ASSERT(r, | |
| toneMapAgtm.fHeadroomAdaptiveToneMap->fBaselineHdrHeadroom == std::log2(812.f / 100.f)); | |
| // Infinite peak luminance should clamp baseline headroom to 6. | |
| { | |
| skhdr::ContentLightLevelInformation clli; | |
| clli.fMaxCLL = SK_FloatInfinity; | |
| metadata.setContentLightLevelInformation(clli); | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::PopulateToneMapAgtmParams( | |
| metadata, cs_pq100.get(), &toneMapAgtm, &scaleFactor)); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap.has_value()); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap->fBaselineHdrHeadroom == 6.f); | |
| } | |
| // NaN reference white should result in 0 baseline headroom. | |
| { | |
| skhdr::AdaptiveGlobalToneMap agtm; | |
| agtm.fHdrReferenceWhite = SK_FloatNaN; | |
| metadata.setAdaptiveGlobalToneMap(agtm); | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::PopulateToneMapAgtmParams( | |
| metadata, cs_pq100.get(), &toneMapAgtm, &scaleFactor)); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap.has_value()); | |
| REPORTER_ASSERT(r, toneMapAgtm.fHeadroomAdaptiveToneMap->fBaselineHdrHeadroom == 0.f); | |
| } | |
| } | |
| DEF_TEST(HdrMetadata_Agtm_Invalid, r) { | |
| const skhdr::AdaptiveGlobalToneMap agtm_baseline_0 = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 0.0f, | |
| .fGainApplicationSpacePrimaries = SkNamedPrimaries::kRec2020, | |
| }}, | |
| }; | |
| const skhdr::AdaptiveGlobalToneMap agtm_baseline_2 = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 2.0f, | |
| .fGainApplicationSpacePrimaries = SkNamedPrimaries::kRec2020, | |
| }}, | |
| }; | |
| { | |
| skiatest::ReporterContext ctx(r, "HdrReferenceWhite too low"); | |
| skhdr::AdaptiveGlobalToneMap agtm = { .fHdrReferenceWhite = -1.f }; | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::Validate(agtm)); | |
| } | |
| { | |
| skiatest::ReporterContext ctx(r, "HdrReferenceWhite too high"); | |
| skhdr::AdaptiveGlobalToneMap agtm = { .fHdrReferenceWhite = 10000.1f }; | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::Validate(agtm)); | |
| } | |
| { | |
| skiatest::ReporterContext ctx(r, "BaselineHdrHeadroom too low"); | |
| skhdr::AdaptiveGlobalToneMap agtm = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = -0.01f, | |
| }}, | |
| }; | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::Validate(agtm)); | |
| } | |
| { | |
| skiatest::ReporterContext ctx(r, "BaselineHdrHeadroom too high"); | |
| skhdr::AdaptiveGlobalToneMap agtm = { | |
| .fHeadroomAdaptiveToneMap = {{ | |
| .fBaselineHdrHeadroom = 6.01f, | |
| }}, | |
| }; | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::Validate(agtm)); | |
| } | |
| { | |
| skiatest::ReporterContext ctx(r, "Equal headrooms"); | |
| skhdr::AdaptiveGlobalToneMap agtm = agtm_baseline_0; | |
| agtm.fHeadroomAdaptiveToneMap->fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 0.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = {.fMax = 1.f}, | |
| .fGainCurve = { .fControlPoints = { {0.f, 0.f, 0.f}, } } | |
| } | |
| } | |
| }; | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::Validate(agtm)); | |
| } | |
| { | |
| skiatest::ReporterContext ctx(r, "Non-monotone headrooms"); | |
| skhdr::AdaptiveGlobalToneMap agtm = agtm_baseline_0; | |
| agtm.fHeadroomAdaptiveToneMap->fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 1.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = {.fMax = 1.f}, | |
| .fGainCurve = { .fControlPoints = { {0.f, 0.f, 0.f}, } } | |
| } | |
| }, | |
| { | |
| .fHdrHeadroom = 2.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = {.fMax = 1.f}, | |
| .fGainCurve = { .fControlPoints = { {0.f, 0.f, 0.f}, } } | |
| } | |
| }, | |
| }; | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::Validate(agtm)); | |
| std::swap(agtm.fHeadroomAdaptiveToneMap->fAlternateImages[0], | |
| agtm.fHeadroomAdaptiveToneMap->fAlternateImages[1]); | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::Validate(agtm)); | |
| } | |
| { | |
| skiatest::ReporterContext ctx(r, "Non-monotone X"); | |
| skhdr::AdaptiveGlobalToneMap agtm = agtm_baseline_2; | |
| agtm.fHeadroomAdaptiveToneMap->fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 0.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = {.fMax = 1.f}, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| {4.f, -2.f, 0.f}, | |
| {0.f, 0.f, 0.f}, | |
| } | |
| } | |
| } | |
| }, | |
| }; | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::Validate(agtm)); | |
| } | |
| { | |
| skiatest::ReporterContext ctx(r, "Discontinuous Y"); | |
| skhdr::AdaptiveGlobalToneMap agtm = agtm_baseline_2; | |
| agtm.fHeadroomAdaptiveToneMap->fAlternateImages = { | |
| { | |
| .fHdrHeadroom = 0.f, | |
| .fColorGainFunction = { | |
| .fComponentMixing = {.fMax = 1.f}, | |
| .fGainCurve = { | |
| .fControlPoints = { | |
| {0.f, 0.f, 0.f}, | |
| {2.f, -1.f, 0.f}, | |
| {2.f, -1.f, 0.f}, | |
| {4.f, -2.f, 0.f}, | |
| } | |
| } | |
| } | |
| }, | |
| }; | |
| REPORTER_ASSERT(r, skhdr::AgtmHelpers::Validate(agtm)); | |
| auto& alt = agtm.fHeadroomAdaptiveToneMap->fAlternateImages[0]; | |
| alt.fColorGainFunction.fGainCurve.fControlPoints[2].fY = -2.f; | |
| REPORTER_ASSERT(r, !skhdr::AgtmHelpers::Validate(agtm)); | |
| } | |
| } | |