diff --git a/gdk/meson.build b/gdk/meson.build index 62e80ea6c4..1e00cd6764 100644 --- a/gdk/meson.build +++ b/gdk/meson.build @@ -128,8 +128,8 @@ install_headers(gdk_deprecated_headers, subdir: 'gtk-4.0/gdk/deprecated') gdk_sources = gdk_public_sources + gdk_deprecated_sources gdk_private_h_sources = files([ - 'gdkeventsprivate.h', 'gdkdevicetoolprivate.h', + 'gdkeventsprivate.h', 'gdkhslaprivate.h', 'gdkmonitorprivate.h', 'gdkseatdefaultprivate.h', diff --git a/gtk/gtkcolorutils.c b/gtk/gtkcolorutils.c index 9b15eea997..27a29d6bc2 100644 --- a/gtk/gtkcolorutils.c +++ b/gtk/gtkcolorutils.c @@ -32,7 +32,9 @@ #include #include -#include "gtkcolorutils.h" +#include "gtkcolorutilsprivate.h" +#include "gdkhslaprivate.h" +#include /* Converts from RGB to HSV */ static void @@ -226,3 +228,135 @@ gtk_rgb_to_hsv (float r, float g, float b, if (v) *v = b; } + +void +gtk_rgb_to_hwb (float red, float green, float blue, + float *hue, float *white, float *black) +{ + GdkRGBA rgba = (GdkRGBA) { red, green, blue, 1 }; + GdkHSLA hsla; + + _gdk_hsla_init_from_rgba (&hsla, &rgba); + + *hue = hsla.hue; + *white = MIN (MIN (red, green), blue); + *black = (1 - MAX (MAX (red, green), blue)); +} + +void +gtk_hwb_to_rgb (float hue, float white, float black, + float *red, float *green, float *blue) +{ + GdkHSLA hsla; + GdkRGBA rgba; + + if (white + black >= 1) + { + float gray = white / (white + black); + + *red = gray; + *green = gray; + *blue = gray; + + return; + } + + hsla.hue = hue; + hsla.saturation = 1.0; + hsla.lightness = 0.5; + + _gdk_rgba_init_from_hsla (&rgba, &hsla); + + *red = rgba.red * (1 - white - black) + white; + *green = rgba.green * (1 - white - black) + white; + *blue = rgba.blue * (1 - white - black) + white; +} + +#define DEG_TO_RAD(x) ((x) * G_PI / 180) +#define RAD_TO_DEG(x) ((x) * 180 / G_PI) + +void +gtk_oklab_to_oklch (float L, float a, float b, + float *L2, float *C, float *H) +{ + *L2 = L; + *C = sqrtf (a * a + b * b); + *H = atan2 (b, a); +} + +void +gtk_oklch_to_oklab (float L, float C, float H, + float *L2, float *a, float *b) +{ + *L2 = L; + + if (H == 0) + { + *a = *b = 0; + } + else + { + *a = C * cosf (DEG_TO_RAD (H)); + *b = C * sinf (DEG_TO_RAD (H)); + } +} + +static float +apply_gamma (float v) +{ + if (v > 0.0031308) + return 1.055 * pow (v, 1/2.4) - 0.055; + else + return 12.92 * v; +} + +static float +unapply_gamma (float v) +{ + if (v >= 0.04045) + return pow (((v + 0.055)/(1 + 0.055)), 2.4); + else + return v / 12.92; +} + +void +gtk_oklab_to_rgb (float L, float a, float b, + float *red, float *green, float *blue) +{ + float l = L + 0.3963377774f * a + 0.2158037573f * b; + float m = L - 0.1055613458f * a - 0.0638541728f * b; + float s = L - 0.0894841775f * a - 1.2914855480f * b; + + l = powf (l, 3); + m = powf (m, 3); + s = powf (s, 3); + + float linear_red = +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s; + float linear_green = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s; + float linear_blue = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s; + + *red = apply_gamma (linear_red); + *green = apply_gamma (linear_green); + *blue = apply_gamma (linear_blue); +} + +void +gtk_rgb_to_oklab (float red, float green, float blue, + float *L, float *a, float *b) +{ + float linear_red = unapply_gamma (red); + float linear_green = unapply_gamma (green); + float linear_blue = unapply_gamma (blue); + + float l = 0.4122214708f * linear_red + 0.5363325363f * linear_green + 0.0514459929f * linear_blue; + float m = 0.2119034982f * linear_red + 0.6806995451f * linear_green + 0.1073969566f * linear_blue; + float s = 0.0883024619f * linear_red + 0.2817188376f * linear_green + 0.6299787005f * linear_blue; + + l = cbrtf (l); + m = cbrtf (m); + s = cbrtf (s); + + *L = 0.2104542553f*l + 0.7936177850f*m - 0.0040720468f*s; + *a = 1.9779984951f*l - 2.4285922050f*m + 0.4505937099f*s; + *b = 0.0259040371f*l + 0.7827717662f*m - 0.8086757660f*s; +} diff --git a/gtk/gtkcolorutilsprivate.h b/gtk/gtkcolorutilsprivate.h new file mode 100644 index 0000000000..c9b3a00298 --- /dev/null +++ b/gtk/gtkcolorutilsprivate.h @@ -0,0 +1,40 @@ +/* GTK - The GIMP Toolkit + * Copyright (C) 2024 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include "gtkcolorutils.h" + +G_BEGIN_DECLS + +void gtk_rgb_to_hwb (float red, float green, float blue, + float *hue, float *white, float *black); +void gtk_hwb_to_rgb (float hue, float white, float black, + float *red, float *green, float *blue); + +void gtk_oklab_to_oklch (float L, float a, float b, + float *L2, float *C, float *H); +void gtk_oklch_to_oklab (float L, float C, float H, + float *L2, float *a, float *b); + +void gtk_oklab_to_rgb (float L, float a, float b, + float *red, float *green, float *blue); +void gtk_rgb_to_oklab (float red, float green, float blue, + float *L, float *a, float *b); + +G_END_DECLS + diff --git a/gtk/gtkcsscolorvalue.c b/gtk/gtkcsscolorvalue.c index 31226eb68f..f843c8c362 100644 --- a/gtk/gtkcsscolorvalue.c +++ b/gtk/gtkcsscolorvalue.c @@ -28,6 +28,7 @@ #include "gdk/gdkhslaprivate.h" #include "gdk/gdkrgbaprivate.h" +#include "gtkcolorutilsprivate.h" typedef enum { COLOR_TYPE_LITERAL, @@ -694,7 +695,10 @@ gtk_css_color_value_can_parse (GtkCssParser *parser) || gtk_css_parser_has_function (parser, "hsl") || gtk_css_parser_has_function (parser, "hsla") || gtk_css_parser_has_function (parser, "rgb") - || gtk_css_parser_has_function (parser, "rgba"); + || gtk_css_parser_has_function (parser, "rgba") + || gtk_css_parser_has_function (parser, "hwb") + || gtk_css_parser_has_function (parser, "oklab") + || gtk_css_parser_has_function (parser, "oklch"); } typedef struct @@ -800,6 +804,25 @@ parse_hsl_channel_value (GtkCssParser *parser, return TRUE; } +static gboolean +parse_hwb_channel_value (GtkCssParser *parser, + float *value) +{ + GtkCssNumberParseFlags flags = GTK_CSS_PARSE_PERCENT | GTK_CSS_PARSE_NUMBER; + GtkCssValue *val; + + val = gtk_css_number_value_parse (parser, flags); + if (val == NULL) + return FALSE; + + *value = gtk_css_number_value_get_canonical (val, 100); + *value = CLAMP (*value, 0.0, 100.0); + + gtk_css_value_unref (val); + + return TRUE; +} + static gboolean parse_hue_value (GtkCssParser *parser, float *value) @@ -817,6 +840,62 @@ parse_hue_value (GtkCssParser *parser, return TRUE; } +static gboolean +parse_ok_L_value (GtkCssParser *parser, + float *value) +{ + GtkCssNumberParseFlags flags = GTK_CSS_PARSE_PERCENT | GTK_CSS_PARSE_NUMBER; + GtkCssValue *val; + + val = gtk_css_number_value_parse (parser, flags); + if (val == NULL) + return FALSE; + + *value = gtk_css_number_value_get_canonical (val, 1); + *value = CLAMP (*value, 0.0, 1.0); + + gtk_css_value_unref (val); + + return TRUE; +} + +static gboolean +parse_ok_C_value (GtkCssParser *parser, + float *value) +{ + GtkCssNumberParseFlags flags = GTK_CSS_PARSE_PERCENT | GTK_CSS_PARSE_NUMBER; + GtkCssValue *val; + + val = gtk_css_number_value_parse (parser, flags); + if (val == NULL) + return FALSE; + + *value = gtk_css_number_value_get_canonical (val, 1); + *value = MAX (*value, 0.0); + + gtk_css_value_unref (val); + + return TRUE; +} + +static gboolean +parse_ok_ab_value (GtkCssParser *parser, + float *value) +{ + GtkCssNumberParseFlags flags = GTK_CSS_PARSE_PERCENT | GTK_CSS_PARSE_NUMBER; + GtkCssValue *val; + + val = gtk_css_number_value_parse (parser, flags); + if (val == NULL) + return FALSE; + + *value = gtk_css_number_value_get_canonical (val, 0.4); + + gtk_css_value_unref (val); + + return TRUE; +} + static guint parse_rgba_color_channel (GtkCssParser *parser, guint arg, @@ -890,8 +969,129 @@ parse_hsla_color_channel (GtkCssParser *parser, } } +typedef struct { + float hue, white, black, alpha; +} HwbData; + +static guint +parse_hwb_color_channel (GtkCssParser *parser, + guint arg, + ColorSyntax syntax, + gpointer data) +{ + HwbData *hwb = data; + + switch (arg) + { + case 0: + if (!parse_hue_value (parser, &hwb->hue)) + return 0; + return 1; + + case 1: + if (!parse_hwb_channel_value (parser, &hwb->white)) + return 0; + return 1; + + case 2: + if (!parse_hwb_channel_value (parser, &hwb->black)) + return 0; + return 1; + + case 3: + if (!parse_alpha_value (parser, &hwb->alpha, syntax)) + return 0; + return 1; + + default: + g_assert_not_reached (); + return 0; + } +} + +typedef struct { + float L, a, b, alpha; +} LabData; + +static guint +parse_oklab_color_channel (GtkCssParser *parser, + guint arg, + ColorSyntax syntax, + gpointer data) +{ + LabData *oklab = data; + + switch (arg) + { + case 0: + if (!parse_ok_L_value (parser, &oklab->L)) + return 0; + return 1; + + case 1: + if (!parse_ok_ab_value (parser, &oklab->a)) + return 0; + return 1; + + case 2: + if (!parse_ok_ab_value (parser, &oklab->b)) + return 0; + return 1; + + case 3: + if (!parse_alpha_value (parser, &oklab->alpha, syntax)) + return 0; + return 1; + + default: + g_assert_not_reached (); + return 0; + } +} + +typedef struct { + float L, C, H, alpha; +} LchData; + +static guint +parse_oklch_color_channel (GtkCssParser *parser, + guint arg, + ColorSyntax syntax, + gpointer data) +{ + LchData *oklch = data; + + switch (arg) + { + case 0: + if (!parse_ok_L_value (parser, &oklch->L)) + return 0; + return 1; + + case 1: + if (!parse_ok_C_value (parser, &oklch->C)) + return 0; + return 1; + + case 2: + if (!parse_hue_value (parser, &oklch->H)) + return 0; + return 1; + + case 3: + if (!parse_alpha_value (parser, &oklch->alpha, syntax)) + return 0; + return 1; + + default: + g_assert_not_reached (); + return 0; + } +} + static gboolean parse_color_function (GtkCssParser *self, + ColorSyntax syntax, gboolean allow_alpha, gboolean require_alpha, guint (* parse_func) (GtkCssParser *, guint, ColorSyntax, gpointer), @@ -903,7 +1103,6 @@ parse_color_function (GtkCssParser *self, guint arg; guint min_args = 3; guint max_args = 4; - ColorSyntax syntax = COLOR_SYNTAX_DETECTING; token = gtk_css_parser_get_token (self); g_return_val_if_fail (gtk_css_token_is (token, GTK_CSS_TOKEN_FUNCTION), FALSE); @@ -1029,7 +1228,7 @@ gtk_css_color_value_parse (GtkCssParser *parser) has_alpha = gtk_css_parser_has_function (parser, "rgba"); - if (!parse_color_function (parser, has_alpha, has_alpha, parse_rgba_color_channel, &data)) + if (!parse_color_function (parser, COLOR_SYNTAX_DETECTING, has_alpha, has_alpha, parse_rgba_color_channel, &data)) return NULL; return gtk_css_color_value_new_literal (&rgba); @@ -1040,11 +1239,65 @@ gtk_css_color_value_parse (GtkCssParser *parser) hsla.alpha = 1.0; - if (!parse_color_function (parser, TRUE, FALSE, parse_hsla_color_channel, &hsla)) + if (!parse_color_function (parser, COLOR_SYNTAX_DETECTING, TRUE, FALSE, parse_hsla_color_channel, &hsla)) return NULL; _gdk_rgba_init_from_hsla (&rgba, &hsla); + return gtk_css_color_value_new_literal (&rgba); + } + else if (gtk_css_parser_has_function (parser, "hwb")) + { + HwbData hwb = { 0, }; + float red, green, blue; + + hwb.alpha = 1.0; + + if (!parse_color_function (parser, COLOR_SYNTAX_MODERN, TRUE, FALSE, parse_hwb_color_channel, &hwb)) + return NULL; + + hwb.white /= 100.0; + hwb.black /= 100.0; + + gtk_hwb_to_rgb (hwb.hue, hwb.white, hwb.black, &red, &green, &blue); + + rgba.red = red; + rgba.green = green; + rgba.blue = blue; + rgba.alpha = hwb.alpha; + + return gtk_css_color_value_new_literal (&rgba); + } + else if (gtk_css_parser_has_function (parser, "oklab")) + { + LabData oklab = { 0, }; + + oklab.alpha = 1.0; + + if (!parse_color_function (parser, COLOR_SYNTAX_MODERN, TRUE, FALSE, parse_oklab_color_channel, &oklab)) + return NULL; + + gtk_oklab_to_rgb (oklab.L, oklab.a, oklab.b, &rgba.red, &rgba.green, &rgba.blue); + rgba.alpha = oklab.alpha; + + return gtk_css_color_value_new_literal (&rgba); + } + else if (gtk_css_parser_has_function (parser, "oklch")) + { + LchData oklch = { 0, }; + float L, a, b; + float red, green, blue; + + oklch.alpha = 1.0; + + if (!parse_color_function (parser, COLOR_SYNTAX_MODERN, TRUE, FALSE, parse_oklch_color_channel, &oklch)) + return NULL; + + gtk_oklch_to_oklab (oklch.L, oklch.C, oklch.H, &L, &a, &b); + gtk_oklab_to_rgb (L, a, b, &red, &green, &blue); + + rgba.alpha = oklch.alpha; + return gtk_css_color_value_new_literal (&rgba); } else if (gtk_css_parser_has_function (parser, "lighter")) diff --git a/testsuite/gtk/colorutils.c b/testsuite/gtk/colorutils.c index 8f0acfb5e6..73ed626296 100644 --- a/testsuite/gtk/colorutils.c +++ b/testsuite/gtk/colorutils.c @@ -14,39 +14,36 @@ * License along with this library. If not, see . */ #include -#include - -struct { - float r, g, b; - float h, s, v; -} tests[] = { - { 0, 0, 0, 0, 0, 0 }, - { 1, 1, 1, 0, 0, 1 }, - { 1, 0, 0, 0, 1, 1 }, - { 1, 1, 0, 1.0 / 6.0, 1, 1 }, - { 0, 1, 0, 2.0 / 6.0, 1, 1 }, - { 0, 1, 1, 3.0 / 6.0, 1, 1 }, - { 0, 0, 1, 4.0 / 6.0, 1, 1 }, - { 1, 0, 1, 5.0 / 6.0, 1, 1 }, -}; - -/* Close enough for float precision to match, even with some - * rounding errors */ -#define EPSILON 1e-6 +#include static void -test_roundtrips (void) +test_roundtrips_rgb_hsv (void) { + struct { + float r, g, b; + float h, s, v; + } tests[] = { + { 0, 0, 0, 0, 0, 0 }, + { 1, 1, 1, 0, 0, 1 }, + { 1, 0, 0, 0, 1, 1 }, + { 1, 1, 0, 1.0 / 6.0, 1, 1 }, + { 0, 1, 0, 2.0 / 6.0, 1, 1 }, + { 0, 1, 1, 3.0 / 6.0, 1, 1 }, + { 0, 0, 1, 4.0 / 6.0, 1, 1 }, + { 1, 0, 1, 5.0 / 6.0, 1, 1 }, + }; + const float EPSILON = 1e-6; + for (unsigned int i = 0; i < G_N_ELEMENTS (tests); i++) { float r, g, b; float h, s, v; - g_print ("color %u\n", i); gtk_hsv_to_rgb (tests[i].h, tests[i].s, tests[i].v, &r, &g, &b); g_assert_cmpfloat_with_epsilon (r, tests[i].r, EPSILON); g_assert_cmpfloat_with_epsilon (g, tests[i].g, EPSILON); g_assert_cmpfloat_with_epsilon (b, tests[i].b, EPSILON); + gtk_rgb_to_hsv (tests[i].r, tests[i].g, tests[i].b, &h, &s, &v); g_assert_cmpfloat_with_epsilon (h, tests[i].h, EPSILON); g_assert_cmpfloat_with_epsilon (s, tests[i].s, EPSILON); @@ -54,13 +51,87 @@ test_roundtrips (void) } } +static void +test_roundtrips_rgb_hwb (void) +{ + struct { + float r, g, b; + float hue, white, black; + } tests[] = { + { 0, 0, 0, 0, 0, 1 }, + { 1, 1, 1, 0, 1, 0 }, + { 1, 0, 0, 0, 0, 0 }, + { 1, 1, 0, 60, 0, 0 }, + { 0, 1, 0, 120, 0, 0 }, + { 0, 1, 1, 180, 0, 0 }, + { 0, 0, 1, 240, 0, 0 }, + { 1, 0, 1, 300, 0, 0 }, + { 0.5, 0.5, 0.5, 0, 0.5, 0.5 }, + }; + const float EPSILON = 1e-6; + + for (unsigned int i = 0; i < G_N_ELEMENTS (tests); i++) + { + float r, g, b; + float hue, white, black; + + gtk_hwb_to_rgb (tests[i].hue, tests[i].white, tests[i].black, &r, &g, &b); + g_assert_cmpfloat_with_epsilon (r, tests[i].r, EPSILON); + g_assert_cmpfloat_with_epsilon (g, tests[i].g, EPSILON); + g_assert_cmpfloat_with_epsilon (b, tests[i].b, EPSILON); + + gtk_rgb_to_hwb (tests[i].r, tests[i].g, tests[i].b, &hue, &white, &black); + g_assert_cmpfloat_with_epsilon (hue, tests[i].hue, EPSILON); + g_assert_cmpfloat_with_epsilon (white, tests[i].white, EPSILON); + g_assert_cmpfloat_with_epsilon (black, tests[i].black, EPSILON); + } +} + +static void +test_roundtrips_rgb_oklab (void) +{ + struct { + float red, green, blue; + float L, a, b; + } tests[] = { + { 0, 0, 0, 0, 0, 0 }, + { 1, 1, 1, 1, 0, 0 }, + { 1, 0, 0, 0.62796, 0.22486, 0.12585 }, + { 1, 1, 0, 0.96798, -0.07137, 0.19857 }, + { 0, 1, 0, 0.86644, -0.23389, 0.17950 }, + { 0, 1, 1, 0.90540, -0.14944, -0.03940 }, + { 0, 0, 1, 0.45201, -0.03246, -0.31153 }, + { 1, 0, 1, 0.70167, 0.27457, -0.16916 }, + { 0.5, 0.5, 0.5, 0.598181, 0.00000, 0.00000 }, + }; + const float EPSILON = 1e-3; + + for (unsigned int i = 0; i < G_N_ELEMENTS (tests); i++) + { + float red, green, blue; + float L, a, b; + + gtk_oklab_to_rgb (tests[i].L, tests[i].a, tests[i].b, &red, &green, &blue); + g_assert_cmpfloat_with_epsilon (red, tests[i].red, EPSILON); + g_assert_cmpfloat_with_epsilon (green, tests[i].green, EPSILON); + g_assert_cmpfloat_with_epsilon (blue, tests[i].blue, EPSILON); + + gtk_rgb_to_oklab (tests[i].red, tests[i].green, tests[i].blue, &L, &a, &b); + g_assert_cmpfloat_with_epsilon (L, tests[i].L, EPSILON); + g_assert_cmpfloat_with_epsilon (a, tests[i].a, EPSILON); + g_assert_cmpfloat_with_epsilon (b, tests[i].b, EPSILON); + } +} + int main (int argc, char *argv[]) { gtk_test_init (&argc, &argv); - g_test_add_func ("/color/roundtrips", test_roundtrips); + g_test_add_func ("/color/roundtrips/rgb-hsv", test_roundtrips_rgb_hsv); + g_test_add_func ("/color/roundtrips/rgb-hwb", test_roundtrips_rgb_hwb); + g_test_add_func ("/color/roundtrips/rgb-oklab", test_roundtrips_rgb_oklab); return g_test_run(); }