nodeparser: Add support for cicp color states

Allow defining cicp color states with an @-rule:

    @cicp "jpeg" {
      primaries: 1;
      transfer: 13;
      matrix: 6;
      range: full;
    }

And allow using them in color() like this:

    color("jpeg" 50% 0.5 1 / 75%)

Note that custom color states use a string, unlike default color
states which use an ident.

Test included.
This commit is contained in:
Matthias Clasen 2024-08-04 18:16:44 -04:00
parent 842949fcf3
commit 16431da3f2
11 changed files with 273 additions and 5 deletions

View File

@ -5,7 +5,8 @@ GSK render nodes can be serialized and deserialized using APIs such as `gsk_rend
The format is a text format that follows the [CSS syntax rules](https://drafts.csswg.org/css-syntax-3/). In particular, this means that every array of bytes will produce a render node when parsed, as there is a defined error recovery method. For more details on error handling, please refer to the documentation of the parsing APIs.
The grammar of a node text representation using [the CSS value definition syntax](https://drafts.csswg.org/css-values-3/#value-defs) looks like this:
**document**: `<node>\*`
**document**: `<@-rule>\*<node>\*`
**@-rule**: @cicp "name" { <property>* }
**node**: container [ "name" ] { <document> } | `<node-type> [ "name" ] { <property>* }` | "name"
**property**: `<property-name>: <node> | <value> ;`
@ -25,6 +26,45 @@ Nodes can be given a name by adding a string after the `<node-type>` in their de
Just like nodes, textures can be referenced by name. When defining a named texture, the name has to be placed in front of the URL.
# Color states
Color states are represented either by an ident (for builtin ones) or a string
(for custom ones):
**color-state**: `<ident> | <string>`
Custom color states can be defined at the beginning of the document, with an `@cicp` rule.
The format for `@cicp` rules is
@cicp "name" {
...
}
The following properties can be set for custom color states:
| property | syntax | default | printed |
| --------- | ---------------- | -------- | ----------- |
| primaries | `<integer>` | 2 | always |
| transfer | `<integer>` | 2 | always |
| matrix | `<integer>` | 2 | always |
| range | `narrow | full` | full | non-default |
Note that the primaries, transfer and matrix properties always need
to be specified, since GTK does not allow creating color state objects
with these being set to 2 (== unspecified).
# Colors
Colors can be specified with a variation of the modern CSS color syntax:
color(<color-state> <number> <number> <number> ["/" <number>])
The traditional syntax for sRGB colors still works as well:
rgba(<number>, <number>, <number>, <number)
rgb(<number, <number>, <number>)
# Nodes
### container

View File

@ -33,6 +33,7 @@
#include "gskprivate.h"
#include "gdk/gdkcolorstateprivate.h"
#include "gdk/gdkcolorprivate.h"
#include "gdk/gdkrgbaprivate.h"
#include "gdk/gdktextureprivate.h"
#include "gdk/gdkmemoryformatprivate.h"
@ -65,6 +66,7 @@ struct _Context
{
GHashTable *named_nodes;
GHashTable *named_textures;
GHashTable *named_color_states;
PangoFontMap *fontmap;
};
@ -89,6 +91,7 @@ context_finish (Context *context)
{
g_clear_pointer (&context->named_nodes, g_hash_table_unref);
g_clear_pointer (&context->named_textures, g_hash_table_unref);
g_clear_pointer (&context->named_color_states, g_hash_table_unref);
g_clear_object (&context->fontmap);
}
@ -1468,6 +1471,100 @@ typedef struct
float values[4];
} Color;
static gboolean
parse_cicp_range (GtkCssParser *parser,
Context *context,
gpointer out)
{
if (!parse_enum (parser, GDK_TYPE_CICP_RANGE, out))
return FALSE;
return TRUE;
}
static gboolean
parse_unsigned (GtkCssParser *parser,
Context *context,
gpointer out)
{
const GtkCssToken *token;
token = gtk_css_parser_get_token (parser);
if (gtk_css_token_is (token, GTK_CSS_TOKEN_SIGNLESS_INTEGER))
{
gtk_css_parser_consume_token (parser);
*((guint *)out) = (guint) token->number.number;
return TRUE;
}
gtk_css_parser_error_value (parser, "Not an allowed value here");
return FALSE;
}
static gboolean
parse_color_state_rule (GtkCssParser *parser,
Context *context)
{
char *name = NULL;
GdkColorState *cs = NULL;
GdkCicp cicp = { 2, 2, 2, GDK_CICP_RANGE_FULL };
const Declaration declarations[] = {
{ "primaries", parse_unsigned, NULL, &cicp.color_primaries },
{ "transfer", parse_unsigned, NULL, &cicp.transfer_function },
{ "matrix", parse_unsigned, NULL, &cicp.matrix_coefficients },
{ "range", parse_cicp_range, NULL, &cicp.range },
};
const char *default_names[] = { "srgb", "srgb-linear", "rec2100-pq", "rec2100-linear", NULL};
GError *error = NULL;
GtkCssLocation start;
GtkCssLocation end;
/* We only return FALSE when we see the wrong @ keyword, since the caller
* throws an error in this case.
*/
if (!gtk_css_parser_try_at_keyword (parser, "cicp"))
return FALSE;
name = gtk_css_parser_consume_string (parser);
if (name == NULL)
return TRUE;
if (g_strv_contains (default_names, name) ||
(context->named_color_states &&
g_hash_table_contains (context->named_color_states, name)))
{
gtk_css_parser_error_value (parser, "A color state named \"%s\" already exists", name);
g_free (name);
return TRUE;
}
start = *gtk_css_parser_get_block_location (parser);
end = *gtk_css_parser_get_end_location (parser);
gtk_css_parser_end_block_prelude (parser);
parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
cs = gdk_color_state_new_for_cicp (&cicp, &error);
if (!cs)
{
gtk_css_parser_error (parser,
GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
&start, &end,
"Not a valid cicp tuple: %s", error->message);
g_error_free (error);
return TRUE;
}
if (context->named_color_states == NULL)
context->named_color_states = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, (GDestroyNotify) gdk_color_state_unref);
g_hash_table_insert (context->named_color_states, name, cs);
return TRUE;
}
static gboolean
parse_color_state (GtkCssParser *parser,
Context *context,
@ -1483,13 +1580,29 @@ parse_color_state (GtkCssParser *parser,
cs = gdk_color_state_get_rec2100_pq ();
else if (gtk_css_parser_try_ident (parser, "rec2100-linear"))
cs = gdk_color_state_get_rec2100_linear ();
else if (gtk_css_token_is (gtk_css_parser_get_token (parser), GTK_CSS_TOKEN_STRING))
{
char *name = gtk_css_parser_consume_string (parser);
if (context->named_color_states)
cs = g_hash_table_lookup (context->named_color_states, name);
if (!cs)
{
gtk_css_parser_error_value (parser, "No color state named \"%s\"", name);
g_free (name);
return FALSE;
}
g_free (name);
}
else
{
gtk_css_parser_error_syntax (parser, "Expected a valid color state");
return FALSE;
}
*(GdkColorState **) color_state = cs;
*(GdkColorState **) color_state = gdk_color_state_ref (cs);
return TRUE;
}
@ -3045,6 +3158,16 @@ gsk_render_node_deserialize_from_bytes (GBytes *bytes,
&error_func_pair, NULL);
context_init (&context);
while (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_AT_KEYWORD))
{
gtk_css_parser_start_semicolon_block (parser, GTK_CSS_TOKEN_OPEN_CURLY);
if (!parse_color_state_rule (parser, &context))
{
gtk_css_parser_error_syntax (parser, "Unknown @ rule");
}
gtk_css_parser_end_block (parser);
}
root = parse_container_node (parser, &context);
if (root && gsk_container_node_get_n_children (root) == 1)
@ -3072,6 +3195,8 @@ typedef struct
gsize named_node_counter;
GHashTable *named_textures;
gsize named_texture_counter;
GHashTable *named_color_states;
gsize named_color_state_counter;
GHashTable *fonts;
} Printer;
@ -3087,6 +3212,22 @@ printer_init_check_texture (Printer *printer,
g_hash_table_insert (printer->named_textures, texture, g_strdup (""));
}
static void
printer_init_check_color_state (Printer *printer,
GdkColorState *cs)
{
gpointer name;
if (GDK_IS_DEFAULT_COLOR_STATE (cs))
return;
if (!g_hash_table_lookup_extended (printer->named_color_states, cs, NULL, &name))
{
name = g_strdup_printf ("cicp%zu", ++printer->named_color_state_counter);
g_hash_table_insert (printer->named_color_states, cs, name);
}
}
typedef struct {
hb_face_t *face;
hb_subset_input_t *input;
@ -3162,8 +3303,11 @@ printer_init_duplicates_for_node (Printer *printer,
printer_init_collect_font_info (printer, node);
break;
case GSK_CAIRO_NODE:
case GSK_COLOR_NODE:
printer_init_check_color_state (printer, gsk_color_node_get_color2 (node)->color_state);
break;
case GSK_CAIRO_NODE:
case GSK_LINEAR_GRADIENT_NODE:
case GSK_REPEATING_LINEAR_GRADIENT_NODE:
case GSK_RADIAL_GRADIENT_NODE:
@ -3288,6 +3432,8 @@ printer_init (Printer *self,
self->named_node_counter = 0;
self->named_textures = g_hash_table_new_full (NULL, NULL, NULL, g_free);
self->named_texture_counter = 0;
self->named_color_states = g_hash_table_new_full (NULL, NULL, NULL, g_free);
self->named_color_state_counter = 0;
self->fonts = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, font_info_free);
printer_init_duplicates_for_node (self, node);
@ -3300,6 +3446,7 @@ printer_clear (Printer *self)
g_string_free (self->str, TRUE);
g_hash_table_unref (self->named_nodes);
g_hash_table_unref (self->named_textures);
g_hash_table_unref (self->named_color_states);
g_hash_table_unref (self->fonts);
}
@ -3459,6 +3606,15 @@ append_float_param (Printer *p,
g_string_append (p->str, ";\n");
}
static void
append_unsigned_param (Printer *p,
const char *param_name,
guint value)
{
_indent (p);
g_string_append_printf (p->str, "%s: %u;\n", param_name, value);
}
static void
append_rgba_param (Printer *p,
const char *param_name,
@ -3481,8 +3637,19 @@ print_color (Printer *p,
}
else
{
g_string_append_printf (p->str, "color(%s ",
gdk_color_state_get_name (color->color_state));
if (GDK_IS_DEFAULT_COLOR_STATE (color->color_state))
{
g_string_append_printf (p->str, "color(%s ",
gdk_color_state_get_name (color->color_state));
}
else
{
const char *name = g_hash_table_lookup (p->named_color_states,
color->color_state);
g_assert (name != NULL);
g_string_append_printf (p->str, "color(\"%s\" ", name);
}
string_append_double (p->str, color->r);
g_string_append_c (p->str, ' ');
string_append_double (p->str, color->g);
@ -4715,6 +4882,24 @@ G_GNUC_END_IGNORE_DEPRECATIONS
}
}
static void
serialize_color_state (Printer *p,
GdkColorState *color_state,
const char *name)
{
const GdkCicp *cicp = gdk_color_state_get_cicp (color_state);
g_string_append_printf (p->str, "@cicp \"%s\" {\n", name);
p->indentation_level ++;
append_unsigned_param (p, "primaries", cicp->color_primaries);
append_unsigned_param (p, "transfer", cicp->transfer_function);
append_unsigned_param (p, "matrix", cicp->matrix_coefficients);
if (cicp->range != GDK_CICP_RANGE_FULL)
append_enum_param (p, "range", GDK_TYPE_CICP_RANGE, cicp->range);
p->indentation_level --;
g_string_append (p->str, "}\n");
}
/**
* gsk_render_node_serialize:
* @node: a `GskRenderNode`
@ -4736,9 +4921,16 @@ gsk_render_node_serialize (GskRenderNode *node)
{
Printer p;
GBytes *res;
GHashTableIter iter;
GdkColorState *cs;
const char *name;
printer_init (&p, node);
g_hash_table_iter_init (&iter, p.named_color_states);
while (g_hash_table_iter_next (&iter, (gpointer *)&cs, (gpointer *)&name))
serialize_color_state (&p, cs, name);
if (gsk_render_node_get_node_type (node) == GSK_CONTAINER_NODE)
{
guint i;

View File

@ -314,12 +314,15 @@ foreach renderer : renderers
endforeach
node_parser_tests = [
'at-rule.node',
'blend.node',
'blend-unknown-mode.errors',
'blend-unknown-mode.node',
'blend-unknown-mode.ref.node',
'border.node',
'color.node',
'color2.node',
'color3.node',
'conic-gradient.node',
'conic-gradient.ref.node',
'crash1.errors',

View File

@ -0,0 +1,3 @@
<data>:1:1-5: error: GTK_CSS_PARSER_ERROR_SYNTAX
<data>:4:1-8: error: GTK_CSS_PARSER_ERROR_SYNTAX
<data>:6:1-12: error: GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE

View File

@ -0,0 +1,7 @@
@foo {
}
@import "foo";
@cicp "foo" {
}

View File

@ -0,0 +1,11 @@
@cicp "cs1" {
primaries: 1;
transfer: 1;
matrix: 0;
range: full;
}
color {
bounds: 100 100 200 300;
color: color("cs1" 0.4 50% 1 / 20%);
}

View File

@ -0,0 +1,9 @@
@cicp "cicp1" {
primaries: 1;
transfer: 1;
matrix: 0;
}
color {
bounds: 100 100 200 300;
color: color("cicp1" 0.4 0.5 1 / 0.2);
}

View File

@ -0,0 +1 @@
<data>:1:1-12: error: GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE

View File

@ -0,0 +1,2 @@
@cicp "foo" {
}

View File