// SPDX-License-Identifier: GPL-2.0-or-later /* * HSLuv-C: Human-friendly HSL * * Authors: * 2015 Alexei Boronine (original idea, JavaScript implementation) * 2015 Roger Tallada (Obj-C implementation) * 2017 Martin Mitas (C implementation, based on Obj-C implementation) * 2021 Massinissa Derriche (C++ implementation for Inkscape, based on C implementation) * * Copyright (C) 2021 Authors * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #include "hsluv.h" #include #include #include #include <2geom/line.h> #include <2geom/ray.h> namespace Hsluv { /* for RGB */ static const Triplet m[3] = { { 3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366 }, { -0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247 }, { 0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072 } }; /* for XYZ */ static const Triplet m_inv[3] = { { 0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751 }, { 0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500 }, { 0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086 } }; static const double REF_U = 0.19783000664283680764; static const double REF_V = 0.46831999493879100370; // CIE LUV constants static const double KAPPA = 903.29629629629629629630; static const double EPSILON = 0.00885645167903563082; /** * Calculate the bounds of the Luv colors in RGB gamut. * * @param l Lightness. Between 0.0 and 100.0. * @return Bounds of Luv colors in RGB gamut. */ std::array get_bounds(double l) { std::array bounds; double tl = l + 16.0; double sub1 = (tl * tl * tl) / 1560896.0; double sub2 = (sub1 > EPSILON ? sub1 : (l / KAPPA)); int channel; int t; for(channel = 0; channel < 3; channel++) { double m1 = m[channel][0]; double m2 = m[channel][1]; double m3 = m[channel][2]; for (t = 0; t < 2; t++) { double top1 = (284517.0 * m1 - 94839.0 * m3) * sub2; double top2 = (838422.0 * m3 + 769860.0 * m2 + 731718.0 * m1) * l * sub2 - 769860.0 * t * l; double bottom = (632260.0 * m3 - 126452.0 * m2) * sub2 + 126452.0 * t; bounds[channel * 2 + t].setCoefficients(top1, -bottom, top2); } } return bounds; } /** * Calculate the maximum in gamut chromaticity for the given luminance and hue. * * @param l Luminance. * @param h Hue. * @return The maximum chromaticity. */ static double max_chroma_for_lh(double l, double h) { double min_len = std::numeric_limits::max(); auto const ray = Geom::Ray(Geom::Point(0, 0), Geom::rad_from_deg(h)); for (auto const &line : get_bounds(l)) { auto intersections = line.intersect(ray); if (intersections.empty()) { continue; } double len = intersections[0].point().length(); if (len >= 0 && len < min_len) { min_len = len; } } return min_len; } /** * Calculate the dot product of the given arrays. * * @param t1 The first array. * @param t2 The second array. * @return The resulting dot product. */ static double dot_product(const Triplet &t1, const Triplet &t2) { return (t1[0] * t2[0] + t1[1] * t2[1] + t1[2] * t2[2]); } /** * Convenience function used for RGB conversions. * * @param c Value. * @return RGB color component. */ double from_linear(double c) { if (c <= 0.0031308) { return 12.92 * c; } else { return 1.055 * std::pow(c, 1.0 / 2.4) - 0.055; } } /** * Convenience function used for RGB conversions. * * @param c Value. * @return XYZ color component. */ double to_linear(double c) { if (c > 0.04045) { return std::pow((c + 0.055) / 1.055, 2.4); } else { return c / 12.92; } } /** * @overload * @param t RGB color components. * @return XYZ color components. */ static Triplet to_linear(const Triplet &t) { return { to_linear(t[0]), to_linear(t[1]), to_linear(t[2]) }; } /** * Convert a color from the the XYZ colorspace to the RGB colorspace. * * @param in_out[in,out] The XYZ color converted to a RGB color. */ static void xyz2rgb(Triplet &in_out) { Triplet result; for (size_t i : {0, 1, 2}) { result[i] = from_linear(dot_product(m[i], in_out)); } in_out = result; } /** * Convert a color from the the RGB colorspace to the XYZ colorspace. * * @param in_out[in,out] The RGB color converted to a XYZ color. */ static void rgb2xyz(Triplet &in_out) { Triplet rgbl = to_linear(in_out); for (size_t i : {0, 1, 2}) { in_out[i] = dot_product(m_inv[i], rgbl); } } /** * Utility function used to convert from the XYZ colorspace to CIELuv. * https://en.wikipedia.org/wiki/CIELUV * * @param y Y component of the XYZ color. * @return Luminance component of Luv color. */ static double y2l(double y) { if (y <= EPSILON) return y * KAPPA; else return 116.0 * std::cbrt(y) - 16.0; } /** * Utility function used to convert from CIELuv colorspace to XYZ. * * @param l Luminance component of Luv color. * @return Y component of the XYZ color. */ static double l2y(double l) { if (l <= 8.0) { return l / KAPPA; } else { double x = (l + 16.0) / 116.0; return (x * x * x); } } /** * Convert a color from the the XYZ colorspace to the Luv colorspace. * * @param in_out[in,out] The XYZ color converted to a Luv color. */ static void xyz2luv(Triplet &in_out) { double const denominator = in_out[0] + (15.0 * in_out[1]) + (3.0 * in_out[2]); double var_u = 4.0 * in_out[0] / denominator; double var_v = 9.0 * in_out[1] / denominator; double l = y2l(in_out[1]); double u = 13.0 * l * (var_u - REF_U); double v = 13.0 * l * (var_v - REF_V); in_out[0] = l; if (l < 0.00000001) { in_out[1] = 0.0; in_out[2] = 0.0; } else { in_out[1] = u; in_out[2] = v; } } /** * Convert a color from the the Luv colorspace to the XYZ colorspace. * * @param in_out[in,out] The Luv color converted to a XYZ color. */ static void luv2xyz(Triplet &in_out) { if (in_out[0] <= 0.00000001) { /* Black would create a divide-by-zero error. */ in_out[0] = 0.0; in_out[1] = 0.0; in_out[2] = 0.0; return; } double var_u = in_out[1] / (13.0 * in_out[0]) + REF_U; double var_v = in_out[2] / (13.0 * in_out[0]) + REF_V; double y = l2y(in_out[0]); double x = -(9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v); double z = (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v); in_out[0] = x; in_out[1] = y; in_out[2] = z; } /** * Convert a color from the the Luv colorspace to the LCH colorspace. * * @param in_out[in,out] The Luv color converted to a LCH color. */ static void luv2lch(Triplet &in_out) { double l = in_out[0]; auto uv = Geom::Point(in_out[1], in_out[2]); double h; double const c = uv.length(); /* Grays: disambiguate hue */ if (c < 0.00000001) { h = 0; } else { h = Geom::deg_from_rad(Geom::atan2(uv)); if (h < 0.0) { h += 360.0; } } in_out[0] = l; in_out[1] = c; in_out[2] = h; } /** * Convert a color from the the LCH colorspace to the Luv colorspace. * * @param in_out[in,out] The LCH color converted to a Luv color. */ static void lch2luv(Triplet &in_out) { double sinhrad, coshrad; Geom::sincos(Geom::rad_from_deg(in_out[2]), sinhrad, coshrad); double u = coshrad * in_out[1]; double v = sinhrad * in_out[1]; in_out[1] = u; in_out[2] = v; } /** * Convert a color from the the HSLuv colorspace to the LCH colorspace. * * @param in_out[in,out] The HSLuv color converted to a LCH color. */ static void hsluv2lch(Triplet &in_out) { double h = in_out[0]; double s = in_out[1]; double l = in_out[2]; double c; /* White and black: disambiguate chroma */ if(l > 99.9999999 || l < 0.00000001) { c = 0.0; } else { c = max_chroma_for_lh(l, h) / 100.0 * s; } /* Grays: disambiguate hue */ if (s < 0.00000001) { h = 0.0; } in_out[0] = l; in_out[1] = c; in_out[2] = h; } /** * Convert a color from the the LCH colorspace to the HSLuv colorspace. * * @param in_out[in,out] The LCH color converted to a HSLuv color. */ static void lch2hsluv(Triplet &in_out) { double l = in_out[0]; double c = in_out[1]; double h = in_out[2]; double s; /* White and black: disambiguate saturation */ if (l > 99.9999999 || l < 0.00000001) { s = 0.0; } else { s = c / max_chroma_for_lh(l, h) * 100.0; } /* Grays: disambiguate hue */ if (c < 0.00000001) { h = 0.0; } in_out[0] = h; in_out[1] = s; in_out[2] = l; } // Interface functions Triplet luv_to_rgb(double l, double u, double v) { Triplet result{l, u, v}; luv2xyz(result); xyz2rgb(result); for (size_t i : {0, 1, 2}) { result[i] = std::clamp(result[i], 0.0, 1.0); } return result; } Triplet hsluv_to_luv(double h, double s, double l) { Triplet result{h, s, l}; hsluv2lch(result); lch2luv(result); return result; } Triplet luv_to_hsluv(double l, double u, double v) { Triplet result{l, u, v}; luv2lch(result); lch2hsluv(result); return result; } Triplet rgb_to_hsluv(double r, double g, double b) { Triplet result{r, g, b}; rgb2xyz(result); xyz2luv(result); luv2lch(result); lch2hsluv(result); return result; } Triplet hsluv_to_rgb(double h, double s, double l) { Triplet result{h, s, l}; hsluv2lch(result); lch2luv(result); luv2xyz(result); xyz2rgb(result); for (size_t i : {0, 1, 2}) { result[i] = std::clamp(result[i], 0.0, 1.0); } return result; } Triplet hsluv_to_luv(double *hsl) { return hsluv_to_luv(hsl[0], hsl[1], hsl[2]); } double perceptual_lightness(double l) { return l <= 0.885645168 ? l * 0.09032962963 : std::cbrt(l) * 0.249914424 - 0.16; } double rgb_to_perceptual_lightness(Triplet const &rgb) { return perceptual_lightness(rgb_to_hsluv(rgb[0], rgb[1], rgb[2])[2]); } std::pair get_contrasting_color(double l) { double constexpr l_threshold = 0.85; if (l > l_threshold) { // Draw dark over light. auto t = (l - l_threshold) / (1.0 - l_threshold); return {0.0, 0.4 - 0.1 * t}; } else { // Draw light over dark. auto t = (l_threshold - l) / l_threshold; return {1.0, 0.6 + 0.1 * t}; } } } // namespace Hsluv