File indexing completed on 2024-05-12 04:44:28

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // Own header
0005 #include "absolutecolor.h"
0006 
0007 #include "helpermath.h"
0008 #include "helperposixmath.h"
0009 #include <cmath>
0010 #include <lcms2.h>
0011 #include <optional>
0012 #include <qgenericmatrix.h>
0013 #include <qglobal.h>
0014 #include <qmath.h>
0015 
0016 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
0017 #include <qhashfunctions.h>
0018 #include <type_traits>
0019 #endif
0020 
0021 namespace PerceptualColor
0022 {
0023 
0024 // Doxygen doesn’t handle correctly the Q_GLOBAL_STATIC_WITH_ARGS macro, so
0025 // we instruct Doxygen with the @cond command to ignore  this part of the code.
0026 /// @cond
0027 
0028 // clang-format off
0029 
0030 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
0031 Q_GLOBAL_STATIC_WITH_ARGS(
0032     const SquareMatrix3,
0033     m1,
0034     (std::array<double, 9>{{
0035         +0.8189330101, +0.3618667424, -0.1288597137,
0036         +0.0329845436, +0.9293118715, +0.0361456387,
0037         +0.0482003018, +0.2643662691, +0.6338517070}}.data()))
0038 
0039 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
0040 Q_GLOBAL_STATIC_WITH_ARGS(
0041     const SquareMatrix3,
0042     m2,
0043     (std::array<double, 9>{{
0044         +0.2104542553, +0.7936177850, -0.0040720468,
0045         +1.9779984951, -2.4285922050, +0.4505937099,
0046         +0.0259040371, +0.7827717662, -0.8086757660}}.data()))
0047 
0048 // https://fujiwaratko.sakura.ne.jp/infosci/colorspace/bradford_e.html
0049 Q_GLOBAL_STATIC_WITH_ARGS(
0050     const SquareMatrix3,
0051     xyzD65ToXyzD50,
0052     (std::array<double, 9>{{
0053         +1.047886, +0.022919, -0.050216,
0054         +0.029582, +0.990484, -0.017079,
0055         -0.009252, +0.015073, +0.751678}}.data()))
0056 
0057 // clang-format on
0058 
0059 Q_GLOBAL_STATIC_WITH_ARGS( //
0060     const SquareMatrix3,
0061     m1inverse,
0062     (inverseMatrix(*m1).value_or(SquareMatrix3())))
0063 
0064 Q_GLOBAL_STATIC_WITH_ARGS( //
0065     const SquareMatrix3,
0066     m2inverse,
0067     (inverseMatrix(*m2).value_or(SquareMatrix3())))
0068 
0069 Q_GLOBAL_STATIC_WITH_ARGS( //
0070     const SquareMatrix3,
0071     xyzD50ToXyzD65,
0072     (inverseMatrix(*xyzD65ToXyzD50).value_or(SquareMatrix3())))
0073 
0074 /// @endcond
0075 
0076 /** @brief List of all available conversions from this color model.
0077  *
0078  * @param model The color model from which to convert.
0079  *
0080  * @returns List of all available conversions from this color model. */
0081 QList<AbsoluteColor::Conversion> AbsoluteColor::conversionsFrom(const ColorModel model)
0082 {
0083     QList<AbsoluteColor::Conversion> result;
0084     for (const auto &item : conversionList) {
0085         if (item.from == model) {
0086             result.append(item);
0087         }
0088     }
0089     return result;
0090 }
0091 
0092 /** @brief Adds some @ref GenericColor to an existing hash table.
0093  *
0094  * @param values A hash table with color values.
0095  * @param model The color model from which to perform conversions.
0096  *
0097  * @pre <em>values</em> contains the key <em>model</em>.
0098  *
0099  * @post For all available direct conversions from <em>model</em>, it is
0100  * checked whether a value for the destination color model is already
0101  * available in <em>values</em>. If not, this value is calculated and added
0102  * to <em>values</em>, and this function is called recursively again for this
0103  * destination color model. */
0104 void AbsoluteColor::addDirectConversionsRecursivly(QHash<ColorModel, GenericColor> *values, ColorModel model)
0105 {
0106     const auto availableConversions = conversionsFrom(model);
0107     const auto currentValue = values->value(model);
0108     for (const auto &conversion : availableConversions) {
0109         if (!values->contains(conversion.to)) {
0110             values->insert(conversion.to, conversion.conversionFunction(currentValue));
0111             addDirectConversionsRecursivly(values, conversion.to);
0112         }
0113     }
0114 }
0115 
0116 /** @brief Calculate conversions to all color models.
0117  *
0118  * @param model The original color model
0119  * @param value The original color value
0120  *
0121  * @returns A list containing the original value and containing conversions
0122  * to all other @ref ColorModel. */
0123 QHash<ColorModel, GenericColor> AbsoluteColor::allConversions(const ColorModel model, const GenericColor &value)
0124 {
0125     QHash<ColorModel, GenericColor> result;
0126     result.insert(model, value);
0127     addDirectConversionsRecursivly(&result, model);
0128     return result;
0129 }
0130 
0131 /** @internal
0132  *
0133  * @brief Conversion from <a href="https://bottosson.github.io/posts/oklab/">
0134  * Oklab color space</a> to
0135  * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space">
0136  * CIE 1931 XYZ color space</a>.
0137  *
0138  * @param value The value to be converted
0139  *
0140  * @note <a href="https://bottosson.github.io/posts/oklab/">
0141  * Oklab</a> does not specify which
0142  * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer">
0143  * observer</a> the D65 whitepoint should use. But it states that
0144  * <em>“Oklab uses a D65 whitepoint, since this is what sRGB and other
0145  * common color spaces use.”</em>. As
0146  * <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a>
0147  * uses the <em>CIE 1931 2° Standard Observer</em>, this
0148  * might be a good choice.
0149  *
0150  * @returns the same color in
0151  * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space">
0152  * CIE 1931 XYZ color space</a>. The XYZ value has
0153  * <a href="https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab">
0154  * “a D65 whitepoint and white as Y=1”</a>. */
0155 GenericColor AbsoluteColor::fromOklabToXyzD65(const GenericColor &value)
0156 {
0157     // The following algorithm is as described in
0158     // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
0159     //
0160     // Oklab: “The inverse operation, going from Oklab to XYZ is done with
0161     // the following steps:”
0162 
0163     auto lms = (*m2inverse) * value.toTrio(); // NOTE Entries might be negative.
0164     // LMS (long, medium, short) is the response of the three types of
0165     // cones of the human eye.
0166 
0167     lms(/*row*/ 0, /*column*/ 0) = std::pow(lms(/*row*/ 0, /*column*/ 0), 3);
0168     lms(/*row*/ 1, /*column*/ 0) = std::pow(lms(/*row*/ 1, /*column*/ 0), 3);
0169     lms(/*row*/ 2, /*column*/ 0) = std::pow(lms(/*row*/ 2, /*column*/ 0), 3);
0170 
0171     return GenericColor((*m1inverse) * lms);
0172 }
0173 
0174 /** @internal
0175  *
0176  * @brief Conversion from
0177  * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space">
0178  * CIE 1931 XYZ color space</a> to
0179  * <a href="https://bottosson.github.io/posts/oklab/">
0180  * Oklab color space</a>.
0181  *
0182  * @param value The value to be converted
0183  *
0184  * @pre The XYZ value has
0185  * <a href="https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab">
0186  * “a D65 whitepoint and white as Y=1”</a>.
0187  *
0188  * @note <a href="https://bottosson.github.io/posts/oklab/">
0189  * Oklab</a> does not specify which
0190  * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer">
0191  * observer</a> the D65 whitepoint should use. But it states that
0192  * <em>“Oklab uses a D65 whitepoint, since this is what sRGB and other
0193  * common color spaces use.”</em>. As
0194  * <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a>
0195  * uses the <em>CIE 1931 2° Standard Observer</em>, this
0196  * might be a good choice.
0197  *
0198  * @returns the same color in
0199  * <a href="https://bottosson.github.io/posts/oklab/">
0200  * Oklab color space</a>. */
0201 GenericColor AbsoluteColor::fromXyzD65ToOklab(const GenericColor &value)
0202 {
0203     // The following algorithm is as described in
0204     // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
0205     //
0206     // Oklab: “First the XYZ coordinates are converted to an approximate
0207     // cone responses:”
0208     auto lms = (*m1) * value.toTrio(); // NOTE Entries might be negative.
0209     // LMS (long, medium, short) is the response of the three types of
0210     // cones of the human eye.
0211 
0212     // Oklab: “A non-linearity is applied:”
0213     // NOTE The original paper of Björn Ottosson, available at
0214     // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
0215     // proposes to calculate this: “x raised to the power of ⅓”. However,
0216     // x might be negative. The original paper does not explicitly explain
0217     // what the expected behaviour is, as “x raised to the power of ⅓”
0218     // is not universally defined for negative x values. Also,
0219     // std::pow(x, 1.0/3) would return “nan” for negative x. The
0220     // original paper does not provide a reference implementation for
0221     // the conversion between XYZ and Oklab. But it provides a reference
0222     // implementation for a direct (shortcut) conversion between sRGB
0223     // and Oklab, and this reference implementation uses std::cbrtf()
0224     // instead of std::pow(x, 1.0/3). And std::cbrtf() seems to allow
0225     // a negative radicand. This makes round-trip conversations possible,
0226     // because it gives unique results for each x value. Therefore, here
0227     // we do the same, but using std::cbrt() instead of std::cbrtf() to
0228     // allow double precision instead of float precision.
0229     lms(/*row*/ 0, /*column*/ 0) = std::cbrt(lms(/*row*/ 0, /*column*/ 0));
0230     lms(/*row*/ 1, /*column*/ 0) = std::cbrt(lms(/*row*/ 1, /*column*/ 0));
0231     lms(/*row*/ 2, /*column*/ 0) = std::cbrt(lms(/*row*/ 2, /*column*/ 0));
0232 
0233     // Oklab: “Finally, this is transformed into the Lab-coordinates:”
0234     return GenericColor((*m2) * lms);
0235 }
0236 
0237 /** @internal
0238  *
0239  * @brief Color conversion.
0240  *
0241  * @param value Color to be converted.
0242  *
0243  * @returns the converted color */
0244 GenericColor AbsoluteColor::fromXyzD65ToXyzD50(const GenericColor &value)
0245 {
0246     return GenericColor((*xyzD65ToXyzD50) * value.toTrio());
0247 }
0248 
0249 /** @internal
0250  *
0251  * @brief Color conversion.
0252  *
0253  * @param value Color to be converted.
0254  *
0255  * @returns the converted color */
0256 GenericColor AbsoluteColor::fromXyzD50ToXyzD65(const GenericColor &value)
0257 {
0258     return GenericColor((*xyzD50ToXyzD65) * value.toTrio());
0259 }
0260 
0261 /** @internal
0262  *
0263  * @brief Color conversion.
0264  *
0265  * @param value Color to be converted.
0266  *
0267  * @returns the converted color */
0268 GenericColor AbsoluteColor::fromXyzD50ToCielabD50(const GenericColor &value)
0269 {
0270     const cmsCIEXYZ cmsXyzD50 = value.reinterpretAsXyzToCmsciexyz();
0271     cmsCIELab result;
0272     cmsXYZ2Lab(cmsD50_XYZ(), // white point (for both, XYZ and also Cielab)
0273                &result, // output
0274                &cmsXyzD50); // input
0275     return GenericColor(result);
0276 }
0277 
0278 /** @internal
0279  *
0280  * @brief Color conversion.
0281  *
0282  * @param value Color to be converted.
0283  *
0284  * @returns the converted color */
0285 GenericColor AbsoluteColor::fromCielabD50ToXyzD50(const GenericColor &value)
0286 {
0287     const auto temp = value.reinterpretAsLabToCmscielab();
0288     cmsCIEXYZ xyzD50;
0289     cmsLab2XYZ(cmsD50_XYZ(), // white point (for both, XYZ and also Lab)
0290                &xyzD50, // output
0291                &temp); // input
0292     return GenericColor(xyzD50);
0293 }
0294 
0295 /** @internal
0296  *
0297  * @brief Color conversion.
0298  *
0299  * @param value Color to be converted.
0300  *
0301  * @returns the converted color
0302  *
0303  * This is a generic function converting between polar coordinates
0304  * (format: ignored, radius, angleDegree, ignored) and Cartesian coordinates
0305  * (format: ignored, x, y, ignored). */
0306 GenericColor AbsoluteColor::fromCartesianToPolar(const GenericColor &value)
0307 {
0308     GenericColor result = value;
0309     const auto &x = value.second;
0310     const auto &y = value.third;
0311     const auto radius = sqrt(pow(x, 2) + pow(y, 2));
0312     result.second = radius;
0313     if (radius == 0) {
0314         result.third = 0;
0315         return result;
0316     }
0317     if (y >= 0) {
0318         result.third = qRadiansToDegrees(acos(x / radius));
0319     } else {
0320         result.third = qRadiansToDegrees(2 * pi - acos(x / radius));
0321     }
0322     return result;
0323 }
0324 
0325 /** @internal
0326  *
0327  * @brief Color conversion.
0328  *
0329  * @param value Color to be converted.
0330  *
0331  * @returns the converted color
0332  *
0333  * This is a generic function converting between polar coordinates
0334  * (format: ignored, radius, angleDegree, ignored) and Cartesian coordinates
0335  * (format: ignored, x, y, ignored). */
0336 GenericColor AbsoluteColor::fromPolarToCartesian(const GenericColor &value)
0337 {
0338     const auto &radius = value.second;
0339     const auto &angleDegree = value.third;
0340     return GenericColor(value.first, //
0341                         radius * cos(qDegreesToRadians(angleDegree)),
0342                         radius * sin(qDegreesToRadians(angleDegree)),
0343                         value.fourth);
0344 }
0345 
0346 /** @brief Convert a color from one color model to another.
0347  *
0348  * @param from The color model from which the conversion is made.
0349  * @param value The value being converted.
0350  * @param to The color model to which the conversion is made.
0351  *
0352  * @returns The value converted into the new color model.
0353  *
0354  * @note This function is <em>not</em> speed-optimized. */
0355 std::optional<GenericColor> AbsoluteColor::convert(const ColorModel from, const GenericColor &value, const ColorModel to)
0356 {
0357     const auto temp = allConversions(from, value);
0358     if (temp.contains(to)) {
0359         return temp.value(to);
0360     }
0361     return std::nullopt;
0362 }
0363 
0364 } // namespace PerceptualColor