diff --git a/gdk/gdkcolor.c b/gdk/gdkcolor.c
new file mode 100644
index 0000000000..a7dc080346
--- /dev/null
+++ b/gdk/gdkcolor.c
@@ -0,0 +1,301 @@
+/* GDK - The GIMP Drawing Kit
+ *
+ * Copyright (C) 2021 Benjamin Otte
+ *
+ * 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 .
+ */
+
+#include "config.h"
+
+#include "gdkcolorprivate.h"
+#include "gdkcolorstateprivate.h"
+#include "gdkrgbaprivate.h"
+
+/*< private >
+ * GdkColor:
+ * @color_state: the color state to interpret the values in
+ * @values: the 3 coordinates that define the color, followed
+ * by the alpha value
+ *
+ * A `GdkColor` represents a color.
+ *
+ * The color state defines the meaning and range of the values.
+ * E.g., the srgb color state has r, g, b components representing
+ * red, green and blue with a range of [0,1], while the oklch color
+ * state has l, c, h components representing luminosity, chromaticity
+ * and hue, with l ranging from 0 to 1 and c from 0 to about 0.4, while
+ * h is interpreted as angle in degrees.
+ *
+ * value[3] is always the alpha value with a range of [0,1].
+ *
+ * The values are also available under the names red, green, blue
+ * and alpha, or r, g, b and a.
+ */
+
+/*< private >
+ * gdk_color_init:
+ * @self: the `GdkColor` struct to initialize
+ * @color_state: the color state
+ * @values: the values
+ *
+ * Initializes the `GdkColor` with the given color state
+ * and values.
+ *
+ * Note that this takes a reference on @color_state that
+ * must be freed by calling [function@Gdk.Color.finish]
+ * when the `GdkColor` is no longer needed.
+ */
+void
+(gdk_color_init) (GdkColor *self,
+ GdkColorState *color_state,
+ const float values[4])
+{
+ _gdk_color_init (self, color_state, values);
+}
+
+/*< private >
+ * gdk_color_init_copy:
+ * @self: the `GdkColor` struct to initialize
+ * @color: the `GdkColor` to copy
+ *
+ * Initializes the `GdkColor` by copying the contents
+ * of another `GdkColor`.
+ *
+ * Note that this takes a reference on the color state
+ * that must be freed by calling [function@Gdk.Color.finish]
+ * when the `GdkColor` is no longer needed.
+ */
+void
+(gdk_color_init_copy) (GdkColor *self,
+ const GdkColor *color)
+{
+ _gdk_color_init_copy (self, color);
+}
+
+/*< private >
+ * gdk_color_init_from_rgba:
+ * @self: the `GdkColor` struct to initialize
+ * @rgba: the `GdkRGBA` to copy
+ *
+ * Initializes the `GdkColor` by copying the contents
+ * of a `GdkRGBA`.
+ *
+ * Note that `GdkRGBA` colors are always in the sRGB
+ * color state.
+ *
+ * Note that this takes a reference on the color state
+ * that must be freed by calling [function@Gdk.Color.finish]
+ * when the `GdkColor` is no longer needed.
+ */
+void
+(gdk_color_init_from_rgba) (GdkColor *self,
+ const GdkRGBA *rgba)
+{
+ _gdk_color_init_from_rgba (self, rgba);
+}
+
+/*< private >
+ * @self: a `GdkColor`
+ *
+ * Drop the reference on the color state of @self.
+ *
+ * After this, @self is empty and can be initialized again
+ * with [function@Gdk.Color.init] and its variants.
+ */
+void
+(gdk_color_finish) (GdkColor *self)
+{
+ _gdk_color_finish (self);
+}
+
+/*< private >
+ * gdk_color_equal:
+ * @self: a `GdkColor`
+ * @other: another `GdkColor`
+ *
+ * Compares two `GdkColor` structs for equality.
+ *
+ * Returns: `TRUE` if @self and @other are equal
+ */
+gboolean
+(gdk_color_equal) (const GdkColor *self,
+ const GdkColor *other)
+{
+ return _gdk_color_equal (self, other);
+}
+
+/*< private >
+ * gdk_color_is_clear:
+ * @self: a `GdkColor`
+ *
+ * Returns whether @self is fully transparent.
+ *
+ * Returns: `TRUE` if @self is transparent
+ */
+gboolean
+(gdk_color_is_clear) (const GdkColor *self)
+{
+ return _gdk_color_is_clear (self);
+}
+
+/*< private >
+ * gdk_color_is_opaque:
+ * @self: a `GdkColor`
+ *
+ * Returns whether @self is fully opaque.
+ *
+ * Returns: `TRUE` if @self if opaque
+ */
+gboolean
+(gdk_color_is_opaque) (const GdkColor *self)
+{
+ return _gdk_color_is_opaque (self);
+}
+
+/*< private >
+ * gdk_color_convert:
+ * @self: the `GdkColor` to store the result in
+ * @color_state: the target color start
+ * @other: the `GdkColor` to convert
+ *
+ * Converts a given `GdkColor` to another color state.
+ *
+ * After the conversion, @self will represent the same
+ * color as @other in @color_state, to the degree possible.
+ *
+ * Different color states have different gamuts of colors
+ * they can represent, and converting a color to a color
+ * state with a smaller gamut may yield an 'out of gamut'
+ * result.
+ */
+void
+(gdk_color_convert) (GdkColor *self,
+ GdkColorState *color_state,
+ const GdkColor *other)
+{
+ gdk_color_convert (self, color_state, other);
+}
+
+/*< private >
+ * gdk_color_to_float:
+ * @self: a `GdkColor`
+ * @target: the color state to convert to
+ * @values: the location to store the result in
+ *
+ * Converts a given `GdkColor to another color state
+ * and stores the result in a `float[4]`.
+ */
+void
+(gdk_color_to_float) (const GdkColor *self,
+ GdkColorState *target,
+ float values[4])
+{
+ gdk_color_to_float (self, target, values);
+}
+
+/*< private >
+ * gdk_color_from_rgba:
+ * @self: the `GdkColor` to store the result in
+ * @color_state: the target color state
+ * @rgba: the `GdkRGBA` to convert
+ *
+ * Converts a given `GdkRGBA` to the target @color_state.
+ */
+void
+gdk_color_from_rgba (GdkColor *self,
+ GdkColorState *color_state,
+ const GdkRGBA *rgba)
+{
+ GdkColor tmp = {
+ .color_state = GDK_COLOR_STATE_SRGB,
+ .r = rgba->red,
+ .g = rgba->green,
+ .b = rgba->blue,
+ .a = rgba->alpha
+ };
+
+ gdk_color_convert (self, color_state, &tmp);
+ gdk_color_finish (&tmp);
+}
+
+/*< private >
+ * gdk_color_get_depth:
+ * @self: a `GdkColor`
+ *
+ * Returns the preferred depth for the color state of @self.
+ *
+ * Returns: the preferred depth
+ */
+GdkMemoryDepth
+(gdk_color_get_depth) (const GdkColor *self)
+{
+ return gdk_color_state_get_depth (self->color_state);
+}
+
+/*< private >
+ * gdk_color_print:
+ * @self: the `GdkColor` to print
+ * @string: the string to print to
+ *
+ * Appends a representation of @self to @string.
+ *
+ * The representation is inspired by CSS3 colors,
+ * but not 100% identical, and looks like this:
+ *
+ * color(NAME R G B / A)
+ *
+ * where `NAME` identifies color state, and
+ * `R`, `G`, `B` and `A` are the components of the color.
+ *
+ * The alpha may be omitted if it is 1.
+ */
+void
+gdk_color_print (const GdkColor *self,
+ GString *string)
+{
+ if (gdk_color_state_equal (self->color_state, GDK_COLOR_STATE_SRGB))
+ {
+ gdk_rgba_print ((const GdkRGBA *) self->values, string);
+ }
+ else
+ {
+ g_string_append_printf (string, "color(%s %g %g %g",
+ gdk_color_state_get_name (self->color_state),
+ self->r, self->g, self->b);
+ if (self->a < 1)
+ g_string_append_printf (string, " / %g", self->a);
+ g_string_append_c (string, ')');
+ }
+}
+
+/*< private >
+ * gdk_color_print:
+ * @self: the `GdkColor` to print
+ *
+ * Create a string representation of @self.
+ *
+ * See [method@Gdk.Color.print] for details about
+ * the format.
+
+ * Returns: (transfer full): a newly-allocated string
+ */
+char *
+gdk_color_to_string (const GdkColor *self)
+{
+ GString *string = g_string_new ("");
+ gdk_color_print (self, string);
+ return g_string_free (string, FALSE);
+}
+
+
diff --git a/gdk/gdkcolorimpl.h b/gdk/gdkcolorimpl.h
new file mode 100644
index 0000000000..4245e5aeee
--- /dev/null
+++ b/gdk/gdkcolorimpl.h
@@ -0,0 +1,135 @@
+/* GDK - The GIMP Drawing Kit
+ *
+ * Copyright (C) 2021 Benjamin Otte
+ *
+ * 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 "gdkcolorstateprivate.h"
+
+
+#define gdk_color_init(...) _gdk_color_init (__VA_ARGS__)
+static inline void
+_gdk_color_init (GdkColor *self,
+ GdkColorState *color_state,
+ const float values[4])
+{
+ self->color_state = gdk_color_state_ref (color_state);
+ memcpy (self->values, values, sizeof (float) * 4);
+}
+
+#define gdk_color_init_copy(self, color) _gdk_color_init_copy ((self), (color))
+static inline void
+_gdk_color_init_copy (GdkColor *self,
+ const GdkColor *color)
+{
+ _gdk_color_init (self, color->color_state, color->values);
+}
+
+#define gdk_color_init_from_rgb(self, rgba) _gdk_color_init_from_rgba ((self), (rgba))
+static inline void
+_gdk_color_init_from_rgba (GdkColor *self,
+ const GdkRGBA *rgba)
+{
+ _gdk_color_init (self, GDK_COLOR_STATE_SRGB, (const float *) rgba);
+}
+
+#define gdk_color_finish(self) _gdk_color_finish ((self))
+static inline void
+_gdk_color_finish (GdkColor *self)
+{
+ gdk_color_state_unref (self->color_state);
+ self->color_state = NULL;
+}
+
+#define gdk_color_get_color_state(self) _gdk_color_get_color_state ((self))
+static inline GdkColorState *
+_gdk_color_get_color_state (const GdkColor *self)
+{
+ return self->color_state;
+}
+
+#define gdk_color_equal(self, other) _gdk_color_equal ((self), (other))
+static inline gboolean
+_gdk_color_equal (const GdkColor *self,
+ const GdkColor *other)
+{
+ return self->values[0] == other->values[0] &&
+ self->values[1] == other->values[1] &&
+ self->values[2] == other->values[2] &&
+ self->values[3] == other->values[3] &&
+ gdk_color_state_equal (self->color_state, other->color_state);
+}
+
+#define gdk_color_is_clear(self) _gdk_color_is_clear ((self))
+static inline gboolean
+_gdk_color_is_clear (const GdkColor *self)
+{
+ return self->alpha < (255.f / 65535.f);
+}
+
+#define gdk_color_is_opaque(self) _gdk_color_is_opaque ((self))
+static inline gboolean
+_gdk_color_is_opaque (const GdkColor *self)
+{
+ return self->alpha > (65280.f / 65535.f);
+}
+
+#define gdk_color_convert(self, cs, other) _gdk_color_convert ((self), (cs), (other))
+static inline void
+_gdk_color_convert (GdkColor *self,
+ GdkColorState *color_state,
+ const GdkColor *other)
+{
+ if (gdk_color_state_equal (color_state, other->color_state))
+ {
+ gdk_color_init_copy (self, other);
+ return;
+ }
+
+ self->color_state = gdk_color_state_ref (color_state);
+
+ gdk_color_state_convert_color (other->color_state,
+ other->values,
+ self->color_state,
+ self->values);
+}
+
+#define gdk_color_to_float(self, cs, values) _gdk_color_to_float ((self), (cs), (values))
+static inline void
+_gdk_color_to_float (const GdkColor *self,
+ GdkColorState *color_state,
+ float values[4])
+{
+ if (gdk_color_state_equal (self->color_state, color_state))
+ {
+ memcpy (values, self->values, sizeof (float) * 4);
+ return;
+ }
+
+ gdk_color_state_convert_color (self->color_state,
+ self->values,
+ color_state,
+ values);
+}
+
+#define gdk_color_get_depth(self) _gdk_color_get_depth ((self))
+static inline GdkMemoryDepth
+_gdk_color_get_depth (const GdkColor *self)
+{
+ return gdk_color_state_get_depth (self->color_state);
+}
+
diff --git a/gdk/gdkcolorprivate.h b/gdk/gdkcolorprivate.h
new file mode 100644
index 0000000000..cb4185b006
--- /dev/null
+++ b/gdk/gdkcolorprivate.h
@@ -0,0 +1,107 @@
+/* GDK - The GIMP Drawing Kit
+ *
+ * Copyright (C) 2021 Benjamin Otte
+ *
+ * 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
+#include
+#include
+#include
+
+
+typedef struct _GdkColor GdkColor;
+
+/* The interpretation of the first 3 components depends on the color state.
+ * values[3] is always alpha.
+ */
+struct _GdkColor
+{
+ GdkColorState *color_state;
+ union {
+ float values[4];
+ struct {
+ float r;
+ float g;
+ float b;
+ float a;
+ };
+ struct {
+ float red;
+ float green;
+ float blue;
+ float alpha;
+ };
+ };
+};
+
+G_STATIC_ASSERT (G_STRUCT_OFFSET (GdkColor, r) == G_STRUCT_OFFSET (GdkColor, red));
+G_STATIC_ASSERT (G_STRUCT_OFFSET (GdkColor, g) == G_STRUCT_OFFSET (GdkColor, green));
+G_STATIC_ASSERT (G_STRUCT_OFFSET (GdkColor, b) == G_STRUCT_OFFSET (GdkColor, blue));
+G_STATIC_ASSERT (G_STRUCT_OFFSET (GdkColor, a) == G_STRUCT_OFFSET (GdkColor, alpha));
+
+/* The committee notes that since all known implementations but one "get it right"
+ * this may well not be a defect at all.
+ * https://open-std.org/JTC1/SC22/WG14/www/docs/n2396.htm#dr_496
+ */
+#ifndef _MSC_VER
+G_STATIC_ASSERT (G_STRUCT_OFFSET (GdkColor, r) == G_STRUCT_OFFSET (GdkColor, values[0]));
+G_STATIC_ASSERT (G_STRUCT_OFFSET (GdkColor, g) == G_STRUCT_OFFSET (GdkColor, values[1]));
+G_STATIC_ASSERT (G_STRUCT_OFFSET (GdkColor, b) == G_STRUCT_OFFSET (GdkColor, values[2]));
+G_STATIC_ASSERT (G_STRUCT_OFFSET (GdkColor, a) == G_STRUCT_OFFSET (GdkColor, values[3]));
+#endif
+
+#define GDK_COLOR_INIT_SRGB(r,g,b,a) { \
+ .color_state = GDK_COLOR_STATE_SRGB, \
+ .values = { (r), (g), (b), (a) } \
+}
+
+void gdk_color_init (GdkColor *self,
+ GdkColorState *color_state,
+ const float values[4]);
+void gdk_color_init_copy (GdkColor *self,
+ const GdkColor *color);
+void gdk_color_init_from_rgba (GdkColor *self,
+ const GdkRGBA *rgba);
+void gdk_color_finish (GdkColor *self);
+
+gboolean gdk_color_equal (const GdkColor *color1,
+ const GdkColor *color2);
+gboolean gdk_color_is_clear (const GdkColor *self);
+gboolean gdk_color_is_opaque (const GdkColor *self);
+GdkMemoryDepth gdk_color_get_depth (const GdkColor *self);
+
+void gdk_color_convert (GdkColor *self,
+ GdkColorState *color_state,
+ const GdkColor *other);
+
+void gdk_color_to_float (const GdkColor *self,
+ GdkColorState *target,
+ float values[4]);
+
+void gdk_color_from_rgba (GdkColor *self,
+ GdkColorState *color_state,
+ const GdkRGBA *rgba);
+
+void gdk_color_print (const GdkColor *self,
+ GString *string);
+
+char * gdk_color_to_string (const GdkColor *self);
+
+#include "gdkcolorimpl.h"
+
+G_END_DECLS
diff --git a/gdk/meson.build b/gdk/meson.build
index 14b706d76c..78609a9870 100644
--- a/gdk/meson.build
+++ b/gdk/meson.build
@@ -7,6 +7,7 @@ gdk_public_sources = files([
'gdkcairocontext.c',
'gdkcicpparams.c',
'gdkclipboard.c',
+ 'gdkcolor.c',
'gdkcolorstate.c',
'gdkcontentdeserializer.c',
'gdkcontentformats.c',
diff --git a/testsuite/gdk/colorstate-internal.c b/testsuite/gdk/colorstate-internal.c
index b471cdb58f..a95dd87577 100644
--- a/testsuite/gdk/colorstate-internal.c
+++ b/testsuite/gdk/colorstate-internal.c
@@ -1,5 +1,6 @@
#include
#include "gdkcolorstateprivate.h"
+#include "gdkcolorprivate.h"
#include
#include "gdkcolordefs.h"
@@ -134,6 +135,29 @@ test_rec2020_to_srgb (void)
g_assert_cmpfloat_with_epsilon (norm (res), 0, 0.001);
}
+/* Verify that this color is different enough in srgb-linear and srgb
+ * to be detected.
+ */
+static void
+test_color_mislabel (void)
+{
+ GdkColor color;
+ GdkColor color1;
+ GdkColor color2;
+ guint red1, red2;
+
+ gdk_color_init (&color, gdk_color_state_get_srgb_linear (), (float[]) { 0.604, 0, 0, 1 });
+ gdk_color_convert (&color1, gdk_color_state_get_srgb (), &color);
+ gdk_color_init (&color2, gdk_color_state_get_srgb (), (float[]) { 0.604, 0, 0, 1 });
+
+ g_assert_true (!gdk_color_equal (&color1, &color2));
+
+ red1 = round (color1.red * 255.0);
+ red2 = round (color2.red * 255.0);
+
+ g_assert_true (red1 != red2);
+}
+
int
main (int argc, char *argv[])
{
@@ -157,6 +181,7 @@ main (int argc, char *argv[])
g_test_add_func ("/colorstate/matrix/srgb_to_rec2020", test_srgb_to_rec2020);
g_test_add_func ("/colorstate/matrix/rec2020_to_srgb", test_rec2020_to_srgb);
+ g_test_add_func ("/color/mislabel", test_color_mislabel);
return g_test_run ();
}