TestICCProfile.cpp 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. /*
  2. * Copyright (c) 2023, Nico Weber <thakis@chromium.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/Endian.h>
  7. #include <LibCore/MappedFile.h>
  8. #include <LibGfx/ICC/BinaryWriter.h>
  9. #include <LibGfx/ICC/Profile.h>
  10. #include <LibGfx/ICC/WellKnownProfiles.h>
  11. #include <LibGfx/ImageFormats/JPEGLoader.h>
  12. #include <LibGfx/ImageFormats/PNGLoader.h>
  13. #include <LibGfx/ImageFormats/WebPLoader.h>
  14. #include <LibTest/TestCase.h>
  15. #ifdef AK_OS_SERENITY
  16. # define TEST_INPUT(x) ("/usr/Tests/LibGfx/test-inputs/" x)
  17. #else
  18. # define TEST_INPUT(x) ("test-inputs/" x)
  19. #endif
  20. TEST_CASE(png)
  21. {
  22. auto file = MUST(Core::MappedFile::map(TEST_INPUT("icc-v2.png"sv)));
  23. auto png = MUST(Gfx::PNGImageDecoderPlugin::create(file->bytes()));
  24. EXPECT(png->initialize());
  25. auto icc_bytes = MUST(png->icc_data());
  26. EXPECT(icc_bytes.has_value());
  27. auto icc_profile = MUST(Gfx::ICC::Profile::try_load_from_externally_owned_memory(icc_bytes.value()));
  28. EXPECT(icc_profile->is_v2());
  29. }
  30. TEST_CASE(jpg)
  31. {
  32. auto file = MUST(Core::MappedFile::map(TEST_INPUT("icc-v4.jpg"sv)));
  33. auto jpg = MUST(Gfx::JPEGImageDecoderPlugin::create(file->bytes()));
  34. EXPECT(jpg->initialize());
  35. auto icc_bytes = MUST(jpg->icc_data());
  36. EXPECT(icc_bytes.has_value());
  37. auto icc_profile = MUST(Gfx::ICC::Profile::try_load_from_externally_owned_memory(icc_bytes.value()));
  38. EXPECT(icc_profile->is_v4());
  39. }
  40. TEST_CASE(webp_extended_lossless)
  41. {
  42. auto file = MUST(Core::MappedFile::map(TEST_INPUT("extended-lossless.webp"sv)));
  43. auto webp = MUST(Gfx::WebPImageDecoderPlugin::create(file->bytes()));
  44. EXPECT(webp->initialize());
  45. auto icc_bytes = MUST(webp->icc_data());
  46. EXPECT(icc_bytes.has_value());
  47. auto icc_profile = MUST(Gfx::ICC::Profile::try_load_from_externally_owned_memory(icc_bytes.value()));
  48. EXPECT(icc_profile->is_v2());
  49. }
  50. TEST_CASE(webp_extended_lossy)
  51. {
  52. auto file = MUST(Core::MappedFile::map(TEST_INPUT("extended-lossy.webp"sv)));
  53. auto webp = MUST(Gfx::WebPImageDecoderPlugin::create(file->bytes()));
  54. EXPECT(webp->initialize());
  55. auto icc_bytes = MUST(webp->icc_data());
  56. EXPECT(icc_bytes.has_value());
  57. auto icc_profile = MUST(Gfx::ICC::Profile::try_load_from_externally_owned_memory(icc_bytes.value()));
  58. EXPECT(icc_profile->is_v2());
  59. }
  60. TEST_CASE(serialize_icc)
  61. {
  62. auto file = MUST(Core::MappedFile::map(TEST_INPUT("p3-v4.icc"sv)));
  63. auto icc_profile = MUST(Gfx::ICC::Profile::try_load_from_externally_owned_memory(file->bytes()));
  64. EXPECT(icc_profile->is_v4());
  65. auto serialized_bytes = MUST(Gfx::ICC::encode(*icc_profile));
  66. EXPECT_EQ(serialized_bytes, file->bytes());
  67. }
  68. TEST_CASE(built_in_sRGB)
  69. {
  70. auto sRGB = MUST(Gfx::ICC::sRGB());
  71. auto serialized_bytes = MUST(Gfx::ICC::encode(sRGB));
  72. // We currently exactly match the curve in GIMP's built-in sRGB profile. It's a type 3 'para' curve with 5 parameters.
  73. u32 para[] = { 0x70617261, 0x00000000, 0x00030000, 0x00026666, 0x0000F2A7, 0x00000D59, 0x000013D0, 0x00000A5B };
  74. for (u32& i : para)
  75. i = AK::convert_between_host_and_big_endian(i);
  76. EXPECT(memmem(serialized_bytes.data(), serialized_bytes.size(), para, sizeof(para)) != nullptr);
  77. // We currently exactly match the chromatic adaptation matrix in GIMP's (and other's) built-in sRGB profile.
  78. u32 sf32[] = { 0x73663332, 0x00000000, 0x00010C42, 0x000005DE, 0xFFFFF325, 0x00000793, 0x0000FD90, 0xFFFFFBA1, 0xFFFFFDA2, 0x000003DC, 0x0000C06E };
  79. for (u32& i : sf32)
  80. i = AK::convert_between_host_and_big_endian(i);
  81. EXPECT(memmem(serialized_bytes.data(), serialized_bytes.size(), sf32, sizeof(sf32)) != nullptr);
  82. }
  83. TEST_CASE(to_pcs)
  84. {
  85. auto sRGB = MUST(Gfx::ICC::sRGB());
  86. EXPECT(sRGB->data_color_space() == Gfx::ICC::ColorSpace::RGB);
  87. EXPECT(sRGB->connection_space() == Gfx::ICC::ColorSpace::PCSXYZ);
  88. auto sRGB_curve_pointer = MUST(Gfx::ICC::sRGB_curve());
  89. VERIFY(sRGB_curve_pointer->type() == Gfx::ICC::ParametricCurveTagData::Type);
  90. auto const& sRGB_curve = static_cast<Gfx::ICC::ParametricCurveTagData const&>(*sRGB_curve_pointer);
  91. EXPECT_EQ(sRGB_curve.evaluate(0.f), 0.f);
  92. EXPECT_EQ(sRGB_curve.evaluate(1.f), 1.f);
  93. auto xyz_from_sRGB = [&sRGB](u8 r, u8 g, u8 b) {
  94. u8 rgb[3] = { r, g, b };
  95. return MUST(sRGB->to_pcs(rgb));
  96. };
  97. auto vec3_from_xyz = [](Gfx::ICC::XYZ const& xyz) {
  98. return FloatVector3 { xyz.X, xyz.Y, xyz.Z };
  99. };
  100. #define EXPECT_APPROXIMATE_VECTOR3(v1, v2) \
  101. EXPECT_APPROXIMATE((v1)[0], (v2)[0]); \
  102. EXPECT_APPROXIMATE((v1)[1], (v2)[1]); \
  103. EXPECT_APPROXIMATE((v1)[2], (v2)[2]);
  104. // At 0 and 255, the gamma curve is (exactly) 0 and 1, so these just test the matrix part.
  105. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(0, 0, 0), FloatVector3(0, 0, 0));
  106. auto r_xyz = vec3_from_xyz(sRGB->red_matrix_column());
  107. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(255, 0, 0), r_xyz);
  108. auto g_xyz = vec3_from_xyz(sRGB->green_matrix_column());
  109. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(0, 255, 0), g_xyz);
  110. auto b_xyz = vec3_from_xyz(sRGB->blue_matrix_column());
  111. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(0, 0, 255), b_xyz);
  112. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(255, 255, 0), r_xyz + g_xyz);
  113. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(255, 0, 255), r_xyz + b_xyz);
  114. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(0, 255, 255), g_xyz + b_xyz);
  115. // FIXME: This should also be equal to sRGB->pcs_illuminant() and to the profiles mediaWhitePointTag,
  116. // but at the moment it's off by a bit too much. See also FIXME in WellKnownProfiles.cpp.
  117. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(255, 255, 255), r_xyz + g_xyz + b_xyz);
  118. // These test the curve part.
  119. float f64 = sRGB_curve.evaluate(64 / 255.f);
  120. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(64, 64, 64), (r_xyz + g_xyz + b_xyz) * f64);
  121. float f128 = sRGB_curve.evaluate(128 / 255.f);
  122. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(128, 128, 128), (r_xyz + g_xyz + b_xyz) * f128);
  123. // Test for curve and matrix combined.
  124. float f192 = sRGB_curve.evaluate(192 / 255.f);
  125. EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(64, 128, 192), r_xyz * f64 + g_xyz * f128 + b_xyz * f192);
  126. }
  127. TEST_CASE(from_pcs)
  128. {
  129. auto sRGB = MUST(Gfx::ICC::sRGB());
  130. auto sRGB_from_xyz = [&sRGB](FloatVector3 const& XYZ) {
  131. u8 rgb[3];
  132. MUST(sRGB->from_pcs(XYZ, rgb));
  133. return Color(rgb[0], rgb[1], rgb[2]);
  134. };
  135. auto vec3_from_xyz = [](Gfx::ICC::XYZ const& xyz) {
  136. return FloatVector3 { xyz.X, xyz.Y, xyz.Z };
  137. };
  138. // At 0 and 255, the gamma curve is (exactly) 0 and 1, so these just test the matrix part.
  139. EXPECT_EQ(sRGB_from_xyz(FloatVector3 { 0, 0, 0 }), Color(0, 0, 0));
  140. auto r_xyz = vec3_from_xyz(sRGB->red_matrix_column());
  141. EXPECT_EQ(sRGB_from_xyz(r_xyz), Color(255, 0, 0));
  142. auto g_xyz = vec3_from_xyz(sRGB->green_matrix_column());
  143. EXPECT_EQ(sRGB_from_xyz(g_xyz), Color(0, 255, 0));
  144. auto b_xyz = vec3_from_xyz(sRGB->blue_matrix_column());
  145. EXPECT_EQ(sRGB_from_xyz(b_xyz), Color(0, 0, 255));
  146. EXPECT_EQ(sRGB_from_xyz(r_xyz + g_xyz), Color(255, 255, 0));
  147. EXPECT_EQ(sRGB_from_xyz(r_xyz + b_xyz), Color(255, 0, 255));
  148. EXPECT_EQ(sRGB_from_xyz(g_xyz + b_xyz), Color(0, 255, 255));
  149. EXPECT_EQ(sRGB_from_xyz(r_xyz + g_xyz + b_xyz), Color(255, 255, 255));
  150. // FIXME: Implement and test the inverse curve transform.
  151. }
  152. TEST_CASE(to_lab)
  153. {
  154. auto sRGB = MUST(Gfx::ICC::sRGB());
  155. auto lab_from_sRGB = [&sRGB](u8 r, u8 g, u8 b) {
  156. u8 rgb[3] = { r, g, b };
  157. return MUST(sRGB->to_lab(rgb));
  158. };
  159. // The `expected` numbers are from https://colorjs.io/notebook/ for this snippet of code:
  160. // new Color("srgb", [0, 0, 0]).lab.toString();
  161. //
  162. // new Color("srgb", [1, 0, 0]).lab.toString();
  163. // new Color("srgb", [0, 1, 0]).lab.toString();
  164. // new Color("srgb", [0, 0, 1]).lab.toString();
  165. //
  166. // new Color("srgb", [1, 1, 0]).lab.toString();
  167. // new Color("srgb", [1, 0, 1]).lab.toString();
  168. // new Color("srgb", [0, 1, 1]).lab.toString();
  169. //
  170. // new Color("srgb", [1, 1, 1]).lab.toString();
  171. Gfx::CIELAB expected[] = {
  172. { 0, 0, 0 },
  173. { 54.29054294696968, 80.80492033462421, 69.89098825896275 },
  174. { 87.81853633115202, -79.27108223854806, 80.99459785152247 },
  175. { 29.56829715344471, 68.28740665215547, -112.02971798617645 },
  176. { 97.60701009682253, -15.749846639252663, 93.39361164266089 },
  177. { 60.16894098715946, 93.53959546199253, -60.50080231921204 },
  178. { 90.66601315791455, -50.65651077286893, -14.961666625736525 },
  179. { 100.00000139649632, -0.000007807961277528364, 0.000006766250648659877 },
  180. };
  181. // We're off by more than the default EXPECT_APPROXIMATE() error, so use EXPECT_APPROXIMATE_WITH_ERROR().
  182. // The difference is not too bad: ranges for L*, a*, b* are [0, 100], [-125, 125], [-125, 125],
  183. // so this is an error of considerably less than 0.1 for u8 channels.
  184. #define EXPECT_APPROXIMATE_LAB(l1, l2) \
  185. EXPECT_APPROXIMATE_WITH_ERROR((l1).L, (l2).L, 0.01); \
  186. EXPECT_APPROXIMATE_WITH_ERROR((l1).a, (l2).a, 0.03); \
  187. EXPECT_APPROXIMATE_WITH_ERROR((l1).b, (l2).b, 0.02);
  188. EXPECT_APPROXIMATE_LAB(lab_from_sRGB(0, 0, 0), expected[0]);
  189. EXPECT_APPROXIMATE_LAB(lab_from_sRGB(255, 0, 0), expected[1]);
  190. EXPECT_APPROXIMATE_LAB(lab_from_sRGB(0, 255, 0), expected[2]);
  191. EXPECT_APPROXIMATE_LAB(lab_from_sRGB(0, 0, 255), expected[3]);
  192. EXPECT_APPROXIMATE_LAB(lab_from_sRGB(255, 255, 0), expected[4]);
  193. EXPECT_APPROXIMATE_LAB(lab_from_sRGB(255, 0, 255), expected[5]);
  194. EXPECT_APPROXIMATE_LAB(lab_from_sRGB(0, 255, 255), expected[6]);
  195. EXPECT_APPROXIMATE_LAB(lab_from_sRGB(255, 255, 255), expected[7]);
  196. }