Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Impressum gskrendernodeparser.c

  Sprache: C
 

/*
 * Copyright © 2019 Benjamin Otte
 *                  Timm Bäder
 *
 * 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.1 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 <http://www.gnu.org/licenses/>.
 *
 * Authors: Benjamin Otte <otte@gnome.org>
 *          Timm Bäder <mail@baedert.org>
 */


#include "config.h"

#include "gskrendernodeparserprivate.h"

#include "gskarithmeticnodeprivate.h"
#include "gskblendnodeprivate.h"
#include "gskblurnode.h"
#include "gskbordernodeprivate.h"
#include "gskcaironodeprivate.h"
#include "gskclipnodeprivate.h"
#include "gskcolormatrixnodeprivate.h"
#include "gskcolornodeprivate.h"
#include "gskconicgradientnodeprivate.h"
#include "gskcomponenttransfernodeprivate.h"
#include "gskcomponenttransferprivate.h"
#include "gskcompositenode.h"
#include "gskcontainernodeprivate.h"
#include "gskcopynode.h"
#include "gskcrossfadenode.h"
#include "gskdebugnode.h"
#include "gskdisplacementnodeprivate.h"
#include "gskenumtypes.h"
#include "gskfillnode.h"
#include "gskglshadernode.h"
#include "gskgradientprivate.h"
#include "gskinsetshadownodeprivate.h"
#include "gskisolationnode.h"
#include "gsklineargradientnodeprivate.h"
#include "gskmasknode.h"
#include "gskopacitynode.h"
#include "gskoutsetshadownodeprivate.h"
#include "gskpastenodeprivate.h"
#include "gskpath.h"
#include "gskpathbuilder.h"
#include "gskprivate.h"
#include "gskradialgradientnodeprivate.h"
#include "gskrectsnapprivate.h"
#include "gskrendernodeprivate.h"
#include "gskrepeatnodeprivate.h"
#include "gskroundedclipnodeprivate.h"
#include "gskroundedrectprivate.h"
#include "gskshadownodeprivate.h"
#include "gskstroke.h"
#include "gskstrokenode.h"
#include "gsksubsurfacenode.h"
#include "gsktextnodeprivate.h"
#include "gskturbulencenodeprivate.h"
#include "gsktexturenodeprivate.h"
#include "gsktexturescalenodeprivate.h"
#include "gsktransformnode.h"
#include "gsktransformprivate.h"

#include "gdk/gdkcolorstateprivate.h"
#include "gdk/gdkcolorprivate.h"
#include "gdk/gdkdmabufprivate.h"
#include "gdk/gdkdmabuffourccprivate.h"
#include "gdk/gdkdmabuftextureprivate.h"
#include "gdk/gdkdmabuftexturebuilderprivate.h"
#include "gdk/gdkmemoryformatprivate.h"
#include "gdk/gdkmemorytextureprivate.h"
#include "gdk/gdkrgbaprivate.h"
#include "gdk/gdktextureprivate.h"
#include <gtk/css/gtkcss.h>
#include "gtk/css/gtkcssdataurlprivate.h"
#include "gtk/css/gtkcssparserprivate.h"
#include "gtk/css/gtkcssserializerprivate.h"
#include "gtk/gtkpopcountprivate.h"

#ifdef GDK_WINDOWING_WIN32
#include "gdk/win32/gdkd3d12texturebuilder.h"
#include "gdk/win32/gdkd3d12textureprivate.h"
#include "gdk/win32/gdkd3d12utilsprivate.h"
#endif

#ifdef CAIRO_HAS_SCRIPT_SURFACE
#include <cairo-script.h>
#endif
#ifdef HAVE_CAIRO_SCRIPT_INTERPRETER
#include <cairo-script-interpreter.h>
#endif

#include <cairo-gobject.h>
#include <pango/pangocairo.h>

#ifdef HAVE_PANGOFT
#include <pango/pangofc-fontmap.h>
#endif

#ifdef HAVE_PANGOWIN32
#undef STRICT
#include <pango/pangowin32.h>
#endif

#include <hb-subset.h>

#include <glib/gstdio.h>

typedef struct _Context Context;

struct _Context
{
  GHashTable *named_nodes;
  GHashTable *named_textures;
  GHashTable *named_color_states;
  PangoFontMap *fontmap;
};

typedef struct _Declaration Declaration;

struct _Declaration
{
  const char *name;
  gboolean (* parse_func) (GtkCssParser *parser, Context *context, gpointer result);
  void (* clear_func) (gpointer data);
  gpointer result;
};

static void
context_init (Context *context)
{
  memset (context, 0sizeof (Context));
}

static void
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);
}

static guint
parse_declarations (GtkCssParser      *parser,
                    Context           *context,
                    const Declaration *declarations,
                    guint              n_declarations)
{
  guint parsed = 0;
  guint i;

  g_assert (n_declarations < 8 * sizeof (guint));

  while (!gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF))
    {
      gtk_css_parser_start_semicolon_block (parser, GTK_CSS_TOKEN_OPEN_CURLY);

      for (i = 0; i < n_declarations; i++)
        {
          if (gtk_css_parser_try_ident (parser, declarations[i].name))
            {
              if (parsed & (1 << i))
                {
                  gtk_css_parser_warn_syntax (parser, "Variable \"%s\" defined multiple times", declarations[i].name);
                  /* Unset, just to be sure */
                  parsed &= ~(1 << i);
                  if (declarations[i].clear_func)
                    declarations[i].clear_func (declarations[i].result);
                }

              if (!gtk_css_parser_try_token (parser, GTK_CSS_TOKEN_COLON))
                {
                  gtk_css_parser_error_syntax (parser, "Expected ':' after variable declaration");
                }
              else
                {
                  if (!declarations[i].parse_func (parser, context, declarations[i].result))
                    {
                      /* nothing to do */
                    }
                  else if (!gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF))
                    {
                      gtk_css_parser_error_syntax (parser, "Expected ';' at end of statement");
                      if (declarations[i].clear_func)
                        declarations[i].clear_func (declarations[i].result);
                    }
                  else
                    {
                      parsed |= (1 << i);
                    }
                }
              break;
            }
        }
      if (i == n_declarations)
        {
          if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_IDENT))
            gtk_css_parser_error_syntax (parser, "No variable named \"%s\"",
                                         gtk_css_token_get_string (gtk_css_parser_get_token (parser)));
          else
            gtk_css_parser_error_syntax (parser, "Expected a variable name");
        }

      gtk_css_parser_end_block (parser);
    }

  return parsed;
}

static gboolean
parse_unsigned (GtkCssParser *parser,
                Context      *context,
                gpointer      out)
{
  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNLESS_INTEGER))
    {
      double d;

      if (!gtk_css_parser_consume_number (parser, &d))
        return FALSE;

      *(unsigned *) out = d;
      return TRUE;
    }

  gtk_css_parser_error_value (parser, "Not an allowed value here");
  return FALSE;
}

static gboolean
parse_size (GtkCssParser *parser,
            Context      *context,
            gpointer      out)
{
  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNLESS_INTEGER))
    {
      double d;

      if (!gtk_css_parser_consume_number (parser, &d))
        return FALSE;

      *(gsize *) out = d;
      return TRUE;
    }

  gtk_css_parser_error_value (parser, "Not an allowed value here");
  return FALSE;
}

static gboolean
parse_boolean (GtkCssParser *parser,
               Context      *context,
               gpointer      boolean)
{
  if (gtk_css_parser_try_ident (parser, "true"))
    {
      *(gboolean *) boolean = TRUE;
      return TRUE;
    }
  else if (gtk_css_parser_try_ident (parser, "false"))
    {
      *(gboolean *) boolean = FALSE;
      return TRUE;
    }

  gtk_css_parser_error_syntax (parser, "Boolean value must be \"true\" or \"false\"");
  return FALSE;
}

static gboolean
parse_enum (GtkCssParser *parser,
            GType         type,
            gpointer      out_value)
{
  GEnumClass *class;
  GEnumValue *v;
  char *enum_name;

  enum_name = gtk_css_parser_consume_ident (parser);
  if (enum_name == NULL)
    return FALSE;

  class = g_type_class_ref (type);

  v = g_enum_get_value_by_nick (class, enum_name);
  if (v == NULL)
    {
      gtk_css_parser_error_value (parser, "Unknown value \"%s\" for enum \"%s\"",
                                  enum_name, g_type_name (type));
      g_free (enum_name);
      g_type_class_unref (class);
      return FALSE;
    }

  *(int*)out_value = v->value;

  g_free (enum_name);
  g_type_class_unref (class);

  return TRUE;
}

static gboolean
parse_rect (GtkCssParser    *parser,
            Context         *context,
            gpointer         out_rect)
{
  double numbers[4];

  if (!gtk_css_parser_consume_number (parser, &numbers[0]) ||
      !gtk_css_parser_consume_number (parser, &numbers[1]) ||
      !gtk_css_parser_consume_number (parser, &numbers[2]) ||
      !gtk_css_parser_consume_number (parser, &numbers[3]))
    return FALSE;

  graphene_rect_init (out_rect, numbers[0], numbers[1], numbers[2], numbers[3]);

  return TRUE;
}

static gboolean
parse_vec4 (GtkCssParser    *parser,
            Context         *context,
            gpointer         out_vec4)
{
  double numbers[4];

  if (!gtk_css_parser_consume_number (parser, &numbers[0]) ||
      !gtk_css_parser_consume_number (parser, &numbers[1]) ||
      !gtk_css_parser_consume_number (parser, &numbers[2]) ||
      !gtk_css_parser_consume_number (parser, &numbers[3]))
    return FALSE;

  graphene_vec4_init (out_vec4, numbers[0], numbers[1], numbers[2], numbers[3]);

  return TRUE;
}

static GBytes *
consume_bytes (GtkCssParser *parser)
{
  GtkCssLocation start_location;
  GError *error = NULL;
  char *url, *scheme;
  GBytes *bytes;

  start_location = *gtk_css_parser_get_start_location (parser);
  url = gtk_css_parser_consume_url (parser);
  if (url == NULL)
    return NULL;

  scheme = g_uri_parse_scheme (url);
  if (scheme && g_ascii_strcasecmp (scheme, "data") == 0)
    {
      bytes = gtk_css_data_url_parse (url, NULL, &error);
    }
  else
    {
      GFile *file;

      file = gtk_css_parser_resolve_url (parser, url);
      if (file)
        {
          bytes = g_file_load_bytes (file, NULL, NULL, &error);
          g_object_unref (file);
        }
      else
        {
          g_set_error (&error,
                       GTK_CSS_PARSER_ERROR,
                       GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                       "Failed to resolve URL");
          bytes = NULL;
        }
    }

  g_free (scheme);
  g_free (url);

  if (bytes == NULL)
    {
      gtk_css_parser_emit_error (parser,
                                 &start_location,
                                 gtk_css_parser_get_end_location (parser),
                                 error);
      g_clear_error (&error);
    }

  return bytes;
}

static void
clear_bytes (gpointer inout_bytes)
{
  g_clear_pointer ((GBytes **) inout_bytes, g_bytes_unref);
}

static gboolean
parse_bytes (GtkCssParser *parser,
             Context      *context,
             gpointer      out_data)
{
  GBytes **out_bytes = out_data;
  GBytes *bytes;

  bytes = consume_bytes (parser);
  if (bytes == NULL)
    return FALSE;

  *out_bytes = bytes;

  return TRUE;
}

static gboolean
parse_compressed_bytes (GtkCssParser *parser,
                        Context      *context,
                        gpointer      out_data)
{
  GZlibDecompressor *decompressor;
  GError *error = NULL;
  GBytes *decompressed_bytes, *data_bytes;

  if (!parse_bytes (parser, context, &data_bytes))
    return FALSE;

  decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP);
  decompressed_bytes = g_converter_convert_bytes (G_CONVERTER (decompressor), data_bytes, &error);
  g_object_unref (decompressor);
  g_bytes_unref (data_bytes);
  if (decompressed_bytes == NULL)
    {
      gtk_css_parser_emit_error (parser,
                                 gtk_css_parser_get_start_location (parser),
                                 gtk_css_parser_get_end_location (parser),
                                 error);
      g_clear_error (&error);
      return FALSE;
    }
  
  *(GBytes **) out_data = decompressed_bytes;

  return TRUE;
}

static gboolean
parse_texture_data (GtkCssParser *parser,
                    Context      *context,
                    gpointer      out_data)
{
  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_STRING))
    {
      char *s;
      gsize i, j, len;
      guchar *data;

      s = gtk_css_parser_consume_string (parser);
      if (s == NULL)
        return FALSE;

      len = strlen (s);

      data = g_malloc (len);
      j = 0;
      for (i = 0; i < len; i++)
        {
          int v1, v2;

          if (g_ascii_isspace (s[i]))
            continue;

          v1 = g_ascii_xdigit_value (s[i]);
          if (v1 < 0)
            {
              gtk_css_parser_error_syntax (parser, "Invalid hex character at position %zu", i);
              continue;
            }
          i++;
          v2 = g_ascii_xdigit_value (s[i]);
          if (v2 < 0)
            {
              gtk_css_parser_error_syntax (parser, "Invalid hex character at position %zu", i);
              continue;
            }

          data[j++] = v1 * 16 + v2;
        }

      *(GBytes **) out_data = g_bytes_new_take (data, j);
      return TRUE;
    }

  return parse_compressed_bytes (parser, context, out_data);
}

static gboolean
parse_color_state (GtkCssParser *parser,
                   Context      *context,
                   gpointer      color_state)
{
  GdkColorState *cs = NULL;

  if (gtk_css_parser_try_ident (parser, "srgb"))
    cs = gdk_color_state_get_srgb ();
  else if (gtk_css_parser_try_ident (parser, "srgb-linear"))
    cs = gdk_color_state_get_srgb_linear ();
  else if (gtk_css_parser_try_ident (parser, "rec2100-pq"))
    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_parser_try_ident (parser, "oklab"))
    cs = gdk_color_state_get_oklab ();
  else if (gtk_css_parser_try_ident (parser, "oklch"))
    cs = gdk_color_state_get_oklch ();
  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 = gdk_color_state_ref (cs);
  return TRUE;
}

static gboolean
parse_default_color_state (GtkCssParser *parser,
                           Context      *context,
                           gpointer      out_value)
{
   GdkColorState *color_state;

  if (!parse_color_state (parser, context, &color_state))
    return FALSE;

  if (!GDK_IS_DEFAULT_COLOR_STATE (color_state))
    {
      gtk_css_parser_error_value (parser, "The color state must be a default color state.");
      gdk_color_state_unref (color_state);
      return FALSE;
    }

  *(GdkColorState **) out_value = color_state;
  return TRUE;
}

static void
clear_color_state (gpointer inout_color_state)
{
  GdkColorState **cs = inout_color_state;

  g_clear_pointer (cs, gdk_color_state_unref);
}

static gboolean
parse_dmabuf_fourcc (GtkCssParser *parser,
                     Context      *context,
                     gpointer      data)
{
  guint32 fourcc;

  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_STRING))
    {
      char *fourcc_str = gtk_css_parser_consume_string (parser);

      if (strlen (fourcc_str) != 4)
        {
          gtk_css_parser_error_value (parser, "fourccs must be 4 characters long");
          g_free (fourcc_str);
          return FALSE;
        }

      fourcc = (fourcc_str[0] <<  0) |
               (fourcc_str[1] <<  8) |
               (fourcc_str[2] << 16) |
               (fourcc_str[3] << 24);
      g_free (fourcc_str);
    }
  else if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNLESS_INTEGER))
    {
      double tmp;
      if (!gtk_css_parser_consume_number (parser, &tmp))
        return FALSE;

      fourcc = tmp;
    }
  else
    {
      gtk_css_parser_error_value (parser, "fourccs must be specified as strings or integers");
      return FALSE;
    }

  *(guint32 *) data = fourcc;
  return TRUE;
}

static gboolean
parse_4_unsigned (GtkCssParser *parser,
                  Context      *context,
                  gpointer      data)
{
  unsigned *u = data;

  if (!parse_unsigned (parser, context, &u[0]))
    return FALSE;
  if (!gtk_css_parser_try_token (parser, GTK_CSS_TOKEN_COMMA))
    return TRUE;
  if (!parse_unsigned (parser, context, &u[1]))
    return FALSE;
  if (!gtk_css_parser_try_token (parser, GTK_CSS_TOKEN_COMMA))
    return TRUE;
  if (!parse_unsigned (parser, context, &u[2]))
    return FALSE;
  if (!gtk_css_parser_try_token (parser, GTK_CSS_TOKEN_COMMA))
    return TRUE;
  if (!parse_unsigned (parser, context, &u[3]))
    return FALSE;

  return TRUE;
}

#ifdef HAVE_DMABUF
static void
destroy_fd (gpointer data)
{
  close (GPOINTER_TO_INT (data));
}
#endif

static GdkTexture *
parse_dmabuf_texture (GtkCssParser *parser,
                      Context      *context)
{
  GBytes *bytes = NULL;
  GdkTexture *texture;
  GError *error = NULL;
  GtkCssLocation start_location;
  guint width = 0;
  guint height = 0;
  GdkDmabuf dmabuf = { 0, };
  gboolean premultiplied = TRUE;
  GdkColorState *color_state = NULL;
  unsigned offsets[4] = { 0, }, strides[4] = { 0, };
  const Declaration declarations[] = {
    { "data", parse_texture_data, clear_bytes, &bytes },
    { "width", parse_unsigned, NULL, &width },
    { "height", parse_unsigned , NULL, &height },
    { "fourcc", parse_dmabuf_fourcc, NULL, &dmabuf.fourcc },
    { "premultiplied", parse_boolean, NULL, &premultiplied },
    { "offset", parse_4_unsigned, NULL, offsets },
    { "stride", parse_4_unsigned, NULL, strides },
    { "color-state", parse_color_state, clear_color_state, &color_state }
  };
  guint parse_result;
#ifdef HAVE_DMABUF
  int dmabuf_fd;
#endif

  if (!gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF))
    {
      gtk_css_parser_error_syntax (parser, "Expected '{' for \"dmabuf\"");
      return NULL;
    }

  start_location = *gtk_css_parser_get_start_location (parser);
  gtk_css_parser_end_block_prelude (parser);

  parse_result = parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  /* deduce number of planes from given amount of strides */
  while (dmabuf.n_planes < GDK_DMABUF_MAX_PLANES && strides[dmabuf.n_planes] > 0)
    {
      dmabuf.planes[dmabuf.n_planes].fd = -1;
      dmabuf.planes[dmabuf.n_planes].offset = offsets[dmabuf.n_planes];
      dmabuf.planes[dmabuf.n_planes].stride = strides[dmabuf.n_planes];
      dmabuf.n_planes++;
    }

  if (dmabuf.fourcc == 0)
    {
      g_set_error (&error,
                   GTK_CSS_PARSER_ERROR,
                   GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                   "Cannot create a dmabuf texture without fourcc");
    }
  else if (dmabuf.n_planes == 0)
    {
      g_set_error (&error,
                   GTK_CSS_PARSER_ERROR,
                   GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                   "No stride specified");
    }
  else if (bytes == NULL && !(parse_result & (1 << 0)))
    {
      g_set_error (&error,
                   GTK_CSS_PARSER_ERROR,
                   GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                   "Cannot create a dmabuf texture without data");
    }
  else if (width == 0 && !(parse_result & (1 << 1)))
    {
      g_set_error (&error,
                   GTK_CSS_PARSER_ERROR,
                   GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                   "No width specified");
    }
  else if (height == 0 && !(parse_result & (1 << 2)))
    {
      g_set_error (&error,
                   GTK_CSS_PARSER_ERROR,
                   GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                   "No height specified");
    }
  if (error)
    {
      gtk_css_parser_emit_error (parser,
                                 &start_location,
                                 gtk_css_parser_get_end_location (parser),
                                 error);
      g_clear_pointer (&bytes, g_bytes_unref);
      g_clear_pointer (&color_state, gdk_color_state_unref);
      g_clear_error (&error);
      return NULL;
    }

#ifdef HAVE_DMABUF
  dmabuf_fd = gdk_dmabuf_new_for_bytes (bytes, &error);
  if (dmabuf_fd >= 0)
    {
      GdkDmabufTextureBuilder *builder = gdk_dmabuf_texture_builder_new ();

      dmabuf.planes[0].fd = dmabuf_fd;

      gdk_dmabuf_texture_builder_set_dmabuf (builder, &dmabuf);
      gdk_dmabuf_texture_builder_set_premultiplied (builder, premultiplied);
      gdk_dmabuf_texture_builder_set_width (builder, width);
      gdk_dmabuf_texture_builder_set_height (builder, height);
      gdk_dmabuf_texture_builder_set_color_state (builder, color_state);
      texture = gdk_dmabuf_texture_builder_build (builder, destroy_fd, GINT_TO_POINTER (dmabuf_fd), &error);
      if (texture == NULL)
        close (dmabuf_fd);

      g_object_unref (builder);
    }
  else
    texture = NULL;
  if (error)
    {
      gtk_css_parser_emit_error (parser,
                                 &start_location,
                                 gtk_css_parser_get_end_location (parser),
                                 error);
      g_clear_error (&error);
    }
#else
  gtk_css_parser_warn (parser,
                       GTK_CSS_PARSER_WARNING_UNIMPLEMENTED,
                       &start_location,
                       gtk_css_parser_get_end_location (parser),
                       "No dmabuf support available. Using fallback.");
  texture = NULL;
#endif

  if (texture == NULL)
    {
      GdkMemoryLayout layout;

      if (gdk_memory_layout_init_from_dmabuf (&layout, &dmabuf, premultiplied, width, height) && 
          g_bytes_get_size (bytes) >= layout.size)
        {
          if (color_state == NULL)
            {
              if (gdk_memory_format_get_dmabuf_yuv_fourcc (layout.format) == dmabuf.fourcc)
                color_state = gdk_color_state_ref (gdk_color_state_yuv ());
              else
                color_state = gdk_color_state_ref (gdk_color_state_get_srgb ());
            }
          texture = gdk_memory_texture_new_from_layout (bytes, &layout, color_state, NULL, NULL);
        }
      else
        {
          gtk_css_parser_error (parser,
                                GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                                &start_location,
                                gtk_css_parser_get_end_location (parser),
                                "Cannot create fallback texture for this fourcc");
        }
    }

  g_bytes_unref (bytes);
  g_clear_pointer (&color_state, gdk_color_state_unref);

  return texture;
}

static gboolean
parse_memory_format (GtkCssParser *parser,
                     Context      *context,
                     gpointer      out)
{
  gsize i;

  for (i = 0; i < GDK_MEMORY_N_FORMATS; i++)
    {
      if (gtk_css_parser_try_ident (parser, gdk_memory_format_get_name (i)))
        {
          *(GdkMemoryFormat *) out = i;
          return TRUE;
        }
    }

  return parse_enum (parser, GDK_TYPE_MEMORY_FORMAT, out);
}

static GdkTexture *
parse_memory_texture (GtkCssParser *parser,
                      Context      *context)
{
  GBytes *bytes = NULL;
  GdkTexture *texture;
  GError *error = NULL;
  GtkCssLocation start_location;
  GdkMemoryLayout layout = { 0, };
  GdkColorState *color_state = NULL;
  unsigned offsets[4] = { 0, }, strides[4] = { 0, };
  const Declaration declarations[] = {
    { "data", parse_texture_data, clear_bytes, &bytes },
    { "width", parse_size, NULL, &layout.width },
    { "height", parse_size, NULL, &layout.height },
    { "format", parse_memory_format, NULL, &layout.format },
    { "offset", parse_4_unsigned, NULL, offsets },
    { "stride", parse_4_unsigned, NULL, strides },
    { "color-state", parse_color_state, clear_color_state, &color_state }
  };
  guint parse_result;
  gsize i, n_planes;

  if (!gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF))
    {
      gtk_css_parser_error_syntax (parser, "Expected '{' for \"memory\"");
      return NULL;
    }

  start_location = *gtk_css_parser_get_start_location (parser);
  gtk_css_parser_end_block_prelude (parser);

  parse_result = parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  n_planes = gdk_memory_format_get_n_planes (layout.format);
  for (i = 0; i < n_planes; i++)
    {
      layout.planes[i].offset = offsets[i];
      layout.planes[i].stride = strides[i];
    }

  if (bytes == NULL)
    {
      if (!(parse_result & (1 << 0)))
        gtk_css_parser_error_value (parser, "Cannot create a memory texture without data");
    }
  else
    layout.size = g_bytes_get_size (bytes);

  if (!gdk_memory_layout_is_valid (&layout, &error))
    {
      gtk_css_parser_emit_error (parser,
                                 &start_location,
                                 gtk_css_parser_get_end_location (parser),
                                 error);
      g_clear_error (&error);
      g_clear_pointer (&bytes, g_bytes_unref);
    }

  if (color_state == NULL)
    color_state = gdk_color_state_ref (gdk_color_state_get_srgb ());
  
  if (bytes)
    {
      texture = gdk_memory_texture_new_from_layout (bytes, &layout, color_state, NULL, NULL);
      g_bytes_unref (bytes);
    }
  else
    texture = NULL;

  gdk_color_state_unref (color_state);

  return texture;
}

static gboolean
parse_dxgi_format (GtkCssParser *parser,
                   Context      *context,
                   gpointer      out)
{
  GdkMemoryFormat format;
  guint value;

  if (!parse_unsigned (parser, context, &value))
    return FALSE;

  /* We can ignore premultiplied here, we just need to update the format later */
  if (!gdk_memory_format_find_by_dxgi_format (value, FALSE, &format))
    {
      gtk_css_parser_error_value (parser, "Unhandled DXGI format");
      return FALSE;
    }

  *((GdkMemoryFormat *) out) = format;
  return TRUE;
}

#ifdef GDK_WINDOWING_WIN32
static void
destroy_d3d12_resource (gpointer data)
{
  ID3D12Resource *resource = data;

  ID3D12Resource_Release (resource);
}
#endif

static GdkTexture *
parse_d3d12_texture (GtkCssParser *parser,
                     Context      *context)
{
  GBytes *bytes = NULL;
  GdkTexture *texture;
  GError *error = NULL;
  GtkCssLocation start_location;
  GdkMemoryFormat format = GDK_MEMORY_N_FORMATS;
  guint width = 0;
  guint height = 0;
  gboolean premultiplied = FALSE;
  GdkColorState *color_state = NULL;
  GdkMemoryLayout layout;
#ifdef GDK_WINDOWING_WIN32
  ID3D12Resource *resource;
  GdkD3D12TextureBuilder *builder;
#endif
  const Declaration declarations[] = {
    { "data", parse_texture_data, clear_bytes, &bytes },
    { "width", parse_unsigned, NULL, &width },
    { "height", parse_unsigned , NULL, &height },
    { "format", parse_dxgi_format, NULL, &format },
    { "premultiplied", parse_boolean, NULL, &premultiplied },
    { "color-state", parse_color_state, clear_color_state, &color_state }
  };
  guint parse_result;

  if (!gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF))
    {
      gtk_css_parser_error_syntax (parser, "Expected '{' for \"d3d12\"");
      return NULL;
    }

  start_location = *gtk_css_parser_get_start_location (parser);
  gtk_css_parser_end_block_prelude (parser);

  parse_result = parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  if (format == GDK_MEMORY_N_FORMATS)
    {
      if (!(parse_result & (1 << 3)))
        g_set_error (&error,
                    GTK_CSS_PARSER_ERROR,
                    GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                    "Cannot create a dmabuf texture without data");
      g_clear_pointer (&bytes, g_bytes_unref);
    }
  else if (bytes == NULL && !(parse_result & (1 << 0)))
    {
      g_set_error (&error,
                   GTK_CSS_PARSER_ERROR,
                   GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                   "Cannot create a dmabuf texture without data");
    }
  else if (width == 0)
    {
      if (!(parse_result & (1 << 1)))
        g_set_error (&error,
                     GTK_CSS_PARSER_ERROR,
                     GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                     "No width specified");
      g_clear_pointer (&bytes, g_bytes_unref);
    }
  else if (height == 0)
    {
      if (!(parse_result & (1 << 2)))
        g_set_error (&error,
                     GTK_CSS_PARSER_ERROR,
                     GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                     "No height specified");
      g_clear_pointer (&bytes, g_bytes_unref);
    }
  else
    {
      gdk_memory_layout_init (&layout, format, width, height, gdk_memory_format_alignment (format));
      if (layout.size > g_bytes_get_size (bytes))
        {
          g_set_error (&error,
                      GTK_CSS_PARSER_ERROR,
                      GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                      "Not enough data for texture. Expected %zu bytes, got %zu",
                      layout.size, g_bytes_get_size (bytes));
        }
    }
  if (error || bytes == NULL || width == 0 || height == 0)
    {
      if (error)
        {
          gtk_css_parser_emit_error (parser,
                                    &start_location,
                                    gtk_css_parser_get_end_location (parser),
                                    error);
          g_clear_error (&error);
        }
      g_clear_pointer (&bytes, g_bytes_unref);
      g_clear_pointer (&color_state, gdk_color_state_unref);
      return NULL;
    }

  if (premultiplied)
    format = gdk_memory_format_get_premultiplied (format);
  if (color_state == NULL)
    color_state = gdk_color_state_ref (gdk_color_state_get_srgb ());

#ifdef GDK_WINDOWING_WIN32
  resource = gdk_d3d12_resource_new_from_bytes (g_bytes_get_data (bytes, NULL),
                                                &layout,
                                                &error);
  if (resource == NULL)
    {
      gtk_css_parser_emit_error (parser,
                                &start_location,
                                gtk_css_parser_get_end_location (parser),
                                error);
      g_bytes_unref (bytes);
      gdk_color_state_unref (color_state);
      return FALSE;
    }

  builder = gdk_d3d12_texture_builder_new();
  gdk_d3d12_texture_builder_set_resource (builder, resource);
  gdk_d3d12_texture_builder_set_premultiplied (builder, premultiplied);
  gdk_d3d12_texture_builder_set_color_state (builder, color_state);
  texture = gdk_d3d12_texture_builder_build (builder, destroy_d3d12_resource, resource, &error);
  if (texture)
    {
      g_clear_pointer (&bytes, g_bytes_unref);
      g_clear_pointer (&color_state, gdk_color_state_unref);
      return texture;
    }

  gtk_css_parser_emit_error (parser,
                            &start_location,
                            gtk_css_parser_get_end_location (parser),
                            error);
  g_clear_error (&error);
#else
  gtk_css_parser_warn (parser,
                       GTK_CSS_PARSER_WARNING_UNIMPLEMENTED,
                       &start_location,
                       gtk_css_parser_get_end_location (parser),
                       "No D3D12 support available. Using fallback.");
#endif

  texture = gdk_memory_texture_new_from_layout (bytes, &layout, color_state, NULL, NULL);
  g_bytes_unref (bytes);
  gdk_color_state_unref (color_state);
  return texture;
}

static GdkTexture *
parse_default_texture (GtkCssParser *parser,
                       Context      *context)
{
  GdkTexture *texture;
  GError *error = NULL;
  GtkCssLocation start_location;
  GBytes *bytes;

  start_location = *gtk_css_parser_get_start_location (parser);
  bytes = consume_bytes (parser);
  if (bytes == NULL)
    return NULL;

  texture = gdk_texture_new_from_bytes (bytes, &error);
  g_bytes_unref (bytes);
  if (texture == NULL)
    {
      gtk_css_parser_emit_error (parser,
                                 &start_location,
                                 gtk_css_parser_get_end_location (parser),
                                 error);
      g_clear_error (&error);
      return NULL;
    }

  return texture;
}

static gboolean
parse_texture (GtkCssParser *parser,
               Context      *context,
               gpointer      out_data)
{
  GdkTexture *texture;
  char *texture_name;

  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_STRING))
    {
      texture_name = gtk_css_parser_consume_string (parser);

      if (context->named_textures)
        texture = g_hash_table_lookup (context->named_textures, texture_name);
      else
        texture = NULL;

      if (texture)
        {
          *(GdkTexture **) out_data = g_object_ref (texture);
          g_free (texture_name);
          return TRUE;
        }
      else if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF))
        {
          gtk_css_parser_error_value (parser, "No texture named \"%s\"", texture_name);
          g_free (texture_name);
          return FALSE;
        }

      if (context->named_textures && g_hash_table_lookup (context->named_textures, texture_name))
        {
          gtk_css_parser_error_value (parser, "A texture named \"%s\" already exists.", texture_name);
          g_clear_pointer (&texture_name, g_free);
        }
    }
  else
    texture_name = NULL;

  if (gtk_css_parser_try_ident (parser, "memory"))
    texture = parse_memory_texture (parser, context);
  else if (gtk_css_parser_try_ident (parser, "dmabuf"))
    texture = parse_dmabuf_texture (parser, context);
  else if (gtk_css_parser_try_ident (parser, "d3d12"))
    texture = parse_d3d12_texture (parser, context);
  else
    texture = parse_default_texture (parser, context);

  if (texture == NULL)
    {
      g_free (texture_name);
      return FALSE;
    }
    
  if (texture_name)
    {
      if (context->named_textures == NULL)
        context->named_textures = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                         g_free, g_object_unref);
      g_hash_table_insert (context->named_textures, texture_name, g_object_ref (texture));
    }

  *(GdkTexture **) out_data = texture;
  return TRUE;
}

static void
clear_texture (gpointer inout_texture)
{
  g_clear_object ((GdkTexture **) inout_texture);
}

typedef struct _CairoHookData CairoHookData;
struct _CairoHookData
{
  GtkCssParser *parser;
  graphene_rect_t node_bounds;
  cairo_surface_t *surface;
};

static cairo_surface_t *
csi_hooks_surface_create (void            *closure,
                          cairo_content_t  content,
                          double           width,
                          double           height,
                          long             uid)
{
  CairoHookData *hook = closure;

  if (hook->surface != NULL)
    {
      return cairo_surface_create_similar (hook->surface,
                                           content,
                                           width,
                                           height);
    }

  if (width != hook->node_bounds.size.width || height != hook->node_bounds.size.height)
    {
      gtk_css_parser_error (hook->parser,
                            GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                            gtk_css_parser_get_block_location (hook->parser),
                            gtk_css_parser_get_start_location (hook->parser),
                            "Node size %gx%g does not match script size %gx%g",
                            hook->node_bounds.size.width, hook->node_bounds.size.height,
                            width, height);
    }

  hook->surface = cairo_recording_surface_create (content,
                                                  &(cairo_rectangle_t) {
                                                      hook->node_bounds.origin.x,
                                                      hook->node_bounds.origin.y,
                                                      hook->node_bounds.size.width,
                                                      hook->node_bounds.size.height
                                                  });

  return cairo_surface_reference (hook->surface);
}

static cairo_surface_t *
interpret_cairo_script (GtkCssParser          *parser,
                        const graphene_rect_t *bounds,
                        GBytes                *script)
{
#ifdef HAVE_CAIRO_SCRIPT_INTERPRETER
  cairo_script_interpreter_t *csi;
  CairoHookData hook = { parser, *bounds, NULL };
  cairo_script_interpreter_hooks_t hooks = {
    .closure = &hook,
    .surface_create = csi_hooks_surface_create,
  };

  csi = cairo_script_interpreter_create ();
  cairo_script_interpreter_install_hooks (csi, &hooks);
  cairo_script_interpreter_feed_string (csi, g_bytes_get_data (script, NULL), g_bytes_get_size (script));
  if (hook.surface == NULL)
    {
      gtk_css_parser_error_value (parser, "Cairo script did not create a surface");
    }
  else if (cairo_surface_status (hook.surface) != CAIRO_STATUS_SUCCESS)
    {
      gtk_css_parser_error_value (parser, "Invalid Cairo script: %s", cairo_status_to_string (cairo_surface_status (hook.surface)));
      cairo_script_interpreter_destroy (csi);
      g_clear_pointer (&hook.surface, cairo_surface_destroy);
    }
  if (cairo_script_interpreter_destroy (csi) != CAIRO_STATUS_SUCCESS)
    {
      gtk_css_parser_error_value (parser, "Invalid Cairo script");
      g_clear_pointer (&hook.surface, cairo_surface_destroy);
    }

  return hook.surface;
#else
  gtk_css_parser_warn (parser,
                       GTK_CSS_PARSER_WARNING_UNIMPLEMENTED,
                       gtk_css_parser_get_block_location (parser),
                       gtk_css_parser_get_start_location (parser),
                       "GTK was compiled without script interpreter support. Using fallback pixel data for Cairo node.");
  return NULL;
#endif
}

static gboolean
parse_rounded_rect (GtkCssParser *parser,
                    Context      *context,
                    gpointer      out_rect)
{
  graphene_rect_t r;
  graphene_size_t corners[4];
  double d;
  guint i;

  if (!parse_rect (parser, context, &r))
    return FALSE;

  if (!gtk_css_parser_try_delim (parser, '/'))
    {
      gsk_rounded_rect_init_from_rect (out_rect, &r, 0);
      return TRUE;
    }

  for (i = 0; i < 4; i++)
    {
      if (!gtk_css_parser_has_number (parser))
        break;
      if (!gtk_css_parser_consume_number (parser, &d))
        return FALSE;
      corners[i].width = d;
    }

  if (i == 0)
    {
      gtk_css_parser_error_syntax (parser, "Expected a number");
      return FALSE;
    }

  /* The magic (i - 1) >> 1 below makes it take the correct value
   * according to spec. Feel free to check the 4 cases
   */

  for (; i < 4; i++)
    corners[i].width = corners[(i - 1) >> 1].width;

  if (gtk_css_parser_try_delim (parser, '/'))
    {
      gtk_css_parser_consume_token (parser);

      for (i = 0; i < 4; i++)
        {
          if (!gtk_css_parser_has_number (parser))
            break;
          if (!gtk_css_parser_consume_number (parser, &d))
            return FALSE;
          corners[i].height = d;
        }

      if (i == 0)
        {
          gtk_css_parser_error_syntax (parser, "Expected a number");
          return FALSE;
        }

      for (; i < 4; i++)
        corners[i].height = corners[(i - 1) >> 1].height;
    }
  else
    {
      for (i = 0; i < 4; i++)
        corners[i].height = corners[i].width;
    }

  gsk_rounded_rect_init (out_rect, &r, &corners[0], &corners[1], &corners[2], &corners[3]);

  return TRUE;
}

static gboolean
parse_double (GtkCssParser *parser,
              Context      *context,
              gpointer      out_double)
{
  return gtk_css_parser_consume_number (parser, out_double);
}

static gboolean
parse_positive_double (GtkCssParser *parser,
                       Context      *context,
                       gpointer      out_double)
{
  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNED_NUMBER)
      || gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNED_INTEGER))
    {
      gtk_css_parser_error_syntax (parser, "Expected a positive number");
      return FALSE;
    }

  return gtk_css_parser_consume_number (parser, out_double);
}

static gboolean
parse_strictly_positive_double (GtkCssParser *parser,
                                Context      *context,
                                gpointer      out_double)
{
  double tmp;

  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNED_NUMBER)
      || gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNED_INTEGER))
    {
      gtk_css_parser_error_syntax (parser, "Expected a strictly positive number");
      return FALSE;
    }

  if (!gtk_css_parser_consume_number (parser, &tmp))
    return FALSE;

  if (tmp == 0)
    {
      gtk_css_parser_error_syntax (parser, "Expected a strictly positive number");
      return FALSE;
    }

  *(double *) out_double = tmp;
  return TRUE;
}

static gboolean
parse_point (GtkCssParser *parser,
             Context      *context,
             gpointer      out_point)
{
  double x, y;

  if (!gtk_css_parser_consume_number (parser, &x) ||
      !gtk_css_parser_consume_number (parser, &y))
    return FALSE;

  graphene_point_init (out_point, x, y);

  return TRUE;
}

static gboolean
parse_transform (GtkCssParser *parser,
                 Context      *context,
                 gpointer      out_transform)
{
  GskTransform *transform;

  if (!gsk_transform_parser_parse (parser, &transform))
    {
      gsk_transform_unref (transform);
      return FALSE;
    }

  *(GskTransform **) out_transform = transform;

  return TRUE;
}

static void
clear_transform (gpointer inout_transform)
{
  g_clear_pointer ((GskTransform **) inout_transform, gsk_transform_unref);
}

static gboolean
parse_string (GtkCssParser *parser,
              Context      *context,
              gpointer      out_string)
{
  const GtkCssToken *token;
  char *s;

  token = gtk_css_parser_get_token (parser);
  if (!gtk_css_token_is (token, GTK_CSS_TOKEN_STRING))
    {
      gtk_css_parser_error_syntax (parser, "Expected a string");
      return FALSE;
    }

  s = g_strdup (gtk_css_token_get_string (token));
  gtk_css_parser_consume_token (parser);

  g_free (*(char **) out_string);
  *(char **) out_string = s;

  return TRUE;
}

static void
clear_string (gpointer inout_string)
{
  g_clear_pointer ((char **) inout_string, g_free);
}

typedef struct {
  Context *context;
  GdkColor *color;
} ColorArgData;

static guint
parse_color_arg (GtkCssParser *parser,
                 guint         arg,
                 gpointer      data)
{
  ColorArgData *d = data;
  GdkColorState *color_state;
  float values[4];

  if (!parse_color_state (parser, d->context, &color_state))
    return 0;

  for (int i = 0; i < 3; i++)
    {
      double number;

      if (!gtk_css_parser_consume_number_or_percentage (parser, 01, &number))
        return 0;

      values[i] = number;
    }

  if (gtk_css_parser_try_delim (parser, '/'))
    {
      double number;

      if (!gtk_css_parser_consume_number_or_percentage (parser, 01, &number))
        return 0;

      values[3] = number;
    }
  else
    {
      values[3] = 1;
    }

  gdk_color_init (d->color, color_state, values);
  return 1;
}

static gboolean
parse_color (GtkCssParser *parser,
             Context      *context,
             gpointer      color)
{
  GdkRGBA rgba;

  if (gtk_css_parser_has_function (parser, "color"))
    {
      ColorArgData data = { context, color };

      if (!gtk_css_parser_consume_function (parser, 11, parse_color_arg, &data))
        return FALSE;

      return TRUE;
    }
  else if (gdk_rgba_parser_parse (parser, &rgba))
    {
      gdk_color_init_from_rgba ((GdkColor *) color, &rgba);
      return TRUE;
    }

  return FALSE;
}

static gboolean
parse_stops (GtkCssParser *parser,
             Context      *context,
             gpointer      out_stops)
{
  GArray *stops;
  GskGradientStop stop;

  stops = g_array_new (FALSEFALSEsizeof (GskGradientStop));

  for (;;)
    {
      double dval;

      if (!gtk_css_parser_consume_number (parser, &dval))
        goto error;

      stop.offset = dval;

      if (gtk_css_parser_has_number (parser))
        {
          if (!gtk_css_parser_consume_number (parser, &dval))
            goto error;

          stop.transition_hint = dval;
        }
      else
        stop.transition_hint = 0.5;

      if (!parse_color (parser, context, &stop.color))
        goto error;

      if (stops->len == 0 && stop.offset < 0)
        gtk_css_parser_error_value (parser, "Color stop offset must be >= 0");
      else if (stops->len > 0 && stop.offset < g_array_index (stops, GskGradientStop, stops->len - 1).offset)
        gtk_css_parser_error_value (parser, "Color stop offset must be >= previous value");
      else if (stop.offset > 1)
        gtk_css_parser_error_value (parser, "Color stop offset must be <= 1");
      else
        g_array_append_val (stops, stop);

      if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_COMMA))
        gtk_css_parser_skip (parser);
      else
        break;
  }

  if (stops->len < 2)
    {
      gtk_css_parser_error_value (parser, "At least 2 color stops need to be specified");
      goto error;
    }

  if (*(GArray **) out_stops)
    g_array_free (*(GArray **) out_stops, TRUE);
  *(GArray **) out_stops = stops;

  return TRUE;

error:
  g_array_free (stops, TRUE);
  return FALSE;
}

static void
clear_stops (gpointer inout_stops)
{
  GArray **stops = (GArray **) inout_stops;

  if (*stops)
    {
      for (int i = 0; i < (*stops)->len; i++)
        {
          GskGradientStop *stop = &g_array_index (*stops, GskGradientStop, i);
          gdk_color_finish (&stop->color);
        }

      g_array_free (*stops, TRUE);
      *stops = NULL;
    }
}

static gboolean
parse_float4 (GtkCssParser *parser,
              Context      *context,
              gpointer      out_floats)
{
  float *floats = (float *) out_floats;
  double d[4];
  int i;

  for (i = 0; i < 4 && !gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF); i ++)
    {
      if (!gtk_css_parser_consume_number (parser, &d[i]))
        return FALSE;
    }
  if (i == 0)
    {
      gtk_css_parser_error_syntax (parser, "Expected a color");
      return FALSE;
    }
  for (; i < 4; i++)
    {
      d[i] = d[(i - 1) >> 1];
    }

  for (i = 0; i < 4; i++)
    {
      floats[i] = d[i];
    }

  return TRUE;
}

static gboolean
parse_shadows (GtkCssParser *parser,
               Context      *context,
               gpointer      out_shadows)
{
  GArray *shadows = out_shadows;

  do
    {
      GskShadowEntry shadow;
      GdkColor color = GDK_COLOR_SRGB (0001);
      double dx = 0, dy = 0, radius = 0;

      if (!parse_color (parser, context, &color))
        gtk_css_parser_error_value (parser, "Expected shadow color");

      if (!gtk_css_parser_consume_number (parser, &dx))
        gtk_css_parser_error_value (parser, "Expected shadow x offset");

      if (!gtk_css_parser_consume_number (parser, &dy))
        gtk_css_parser_error_value (parser, "Expected shadow y offset");

      if (gtk_css_parser_has_number (parser))
        {
          if (!gtk_css_parser_consume_number (parser, &radius))
            gtk_css_parser_error_value (parser, "Expected shadow blur radius");
        }

      gdk_color_init_copy (&shadow.color, &color);
      graphene_point_init (&shadow.offset, dx, dy);
      shadow.radius = radius;

      g_array_append_val (shadows, shadow);

      gdk_color_finish (&color);
    }
  while (gtk_css_parser_try_token (parser, GTK_CSS_TOKEN_COMMA));

  return TRUE;
}

static void
clear_shadows (gpointer inout_shadows)
{
  GArray *shadows = inout_shadows;

  for (gsize i = 0; i < shadows->len; i++)
    {
      GskShadowEntry *shadow = &g_array_index (shadows, GskShadowEntry, i);
      gdk_color_finish (&shadow->color);
    }

  g_array_set_size (shadows, 0);
}

static const struct
{
  GskScalingFilter filter;
  const char *name;
} scaling_filters[] = {
  { GSK_SCALING_FILTER_LINEAR, "linear" },
  { GSK_SCALING_FILTER_NEAREST, "nearest" },
  { GSK_SCALING_FILTER_TRILINEAR, "trilinear" },
};

static gboolean
parse_scaling_filter (GtkCssParser *parser,
                      Context      *context,
                      gpointer      out_filter)
{
  for (unsigned int i = 0; i < G_N_ELEMENTS (scaling_filters); i++)
    {
      if (gtk_css_parser_try_ident (parser, scaling_filters[i].name))
        {
          *(GskScalingFilter *) out_filter = scaling_filters[i].filter;
          return TRUE;
        }
    }

  gtk_css_parser_error_syntax (parser, "Not a valid scaling filter.");

  return FALSE;
}

static const struct
{
  GskNoiseType type;
  const char *name;
} noise_types[] = {
  { GSK_NOISE_FRACTAL_NOISE, "fractal-noise" },
  { GSK_NOISE_TURBULENCE, "turbulence" },
};

static const char *
get_noise_type_name (GskNoiseType type)
{
  for (unsigned int i = 0; i < G_N_ELEMENTS (noise_types); i++)
    {
      if (noise_types[i].type == type)
        return noise_types[i].name;
    }

  return NULL;
}

static gboolean
parse_noise_type (GtkCssParser *parser,
                  Context      *context,
                  gpointer      out_type)
{
  guint i;

  for (i = 0; i < G_N_ELEMENTS (noise_types); i++)
    {
      if (gtk_css_parser_try_ident (parser, noise_types[i].name))
        {
          *(GskNoiseType *) out_type = noise_types[i].type;
          return TRUE;
        }
    }

  gtk_css_parser_error_syntax (parser, "Not a valid noise type.");

  return FALSE;
}

static const struct
{
  GskBlendMode mode;
  const char *name;
} blend_modes[] = {
  { GSK_BLEND_MODE_DEFAULT, "normal" },
  { GSK_BLEND_MODE_MULTIPLY, "multiply" },
  { GSK_BLEND_MODE_SCREEN, "screen" },
  { GSK_BLEND_MODE_OVERLAY, "overlay" },
  { GSK_BLEND_MODE_DARKEN, "darken" },
  { GSK_BLEND_MODE_LIGHTEN, "lighten" },
  { GSK_BLEND_MODE_COLOR_DODGE, "color-dodge" },
  { GSK_BLEND_MODE_COLOR_BURN, "color-burn" },
  { GSK_BLEND_MODE_HARD_LIGHT, "hard-light" },
  { GSK_BLEND_MODE_SOFT_LIGHT, "soft-light" },
  { GSK_BLEND_MODE_DIFFERENCE, "difference" },
  { GSK_BLEND_MODE_EXCLUSION, "exclusion" },
  { GSK_BLEND_MODE_COLOR, "color" },
  { GSK_BLEND_MODE_HUE, "hue" },
  { GSK_BLEND_MODE_SATURATION, "saturation" },
  { GSK_BLEND_MODE_LUMINOSITY, "luminosity" }
};

static const char *
get_blend_mode_name (GskBlendMode mode)
{
  for (unsigned int i = 0; i < G_N_ELEMENTS (blend_modes); i++)
    {
      if (blend_modes[i].mode == mode)
        return blend_modes[i].name;
    }

  return NULL;
}

static gboolean
parse_blend_mode (GtkCssParser *parser,
                  Context      *context,
                  gpointer      out_mode)
{
  guint i;

  for (i = 0; i < G_N_ELEMENTS (blend_modes); i++)
    {
      if (gtk_css_parser_try_ident (parser, blend_modes[i].name))
        {
          *(GskBlendMode *) out_mode = blend_modes[i].mode;
          return TRUE;
        }
    }

  gtk_css_parser_error_syntax (parser, "Not a valid blend mode.");

  return FALSE;
}

static const struct
{
  GskMaskMode mode;
  const char *name;
} mask_modes[] = {
  { GSK_MASK_MODE_ALPHA, "alpha" },
  { GSK_MASK_MODE_INVERTED_ALPHA, "inverted-alpha" },
  { GSK_MASK_MODE_LUMINANCE, "luminance" },
  { GSK_MASK_MODE_INVERTED_LUMINANCE, "inverted-luminance" },
};

static const char *
get_mask_mode_name (GskMaskMode mode)
{
  for (unsigned int i = 0; i < G_N_ELEMENTS (mask_modes); i++)
    {
      if (mask_modes[i].mode == mode)
        return mask_modes[i].name;
    }

  return NULL;
}

static gboolean
parse_mask_mode (GtkCssParser *parser,
                 Context      *context,
                 gpointer      out_mode)
{
  guint i;

  for (i = 0; i < G_N_ELEMENTS (mask_modes); i++)
    {
      if (gtk_css_parser_try_ident (parser, mask_modes[i].name))
        {
          *(GskMaskMode *) out_mode = mask_modes[i].mode;
          return TRUE;
        }
    }

  gtk_css_parser_error_syntax (parser, "Not a valid mask mode.");

  return FALSE;
}

static gboolean
parse_porter_duff (GtkCssParser *parser,
                   Context      *context,
                   gpointer      out_rule)
{
  return parse_enum (parser, GSK_TYPE_PORTER_DUFF, out_rule);
}

static gboolean G_GNUC_UNUSED
parse_rect_snap (GtkCssParser *parser,
                 Context      *context,
                 gpointer      out_snap)
{
  GskRectSnap snap;
  GskSnapDirection dir[4];
  gboolean grow_shrink[4] = { FALSE, };
  gsize i;

  for (i = 0; i < 4; i++)
    {
      if (gtk_css_parser_try_ident (parser, "round"))
        dir[i] = GSK_SNAP_ROUND;
      else if (gtk_css_parser_try_ident (parser, "floor"))
        dir[i] = GSK_SNAP_FLOOR;
      else if (gtk_css_parser_try_ident (parser, "ceil"))
        dir[i] = GSK_SNAP_CEIL;
      else if (gtk_css_parser_try_ident (parser, "none"))
        dir[i] = GSK_SNAP_NONE;
      else if (gtk_css_parser_try_ident (parser, "grow"))
        {
          dir[i] = (i == 0 || i == 3) ? GSK_SNAP_FLOOR : GSK_SNAP_CEIL;
          grow_shrink[i] = TRUE;
        }
      else if (gtk_css_parser_try_ident (parser, "shrink"))
        {
          dir[i] = (i == 0 || i == 3) ? GSK_SNAP_CEIL : GSK_SNAP_FLOOR;
          grow_shrink[i] = TRUE;
        }
      else
        break;
    }
  if (i == 0)
    {
      gtk_css_parser_error_value (parser, "Unknown value for snap");
      return FALSE;
    }
  for (; i < 4; i++)
    {
      dir[i] = dir[(i - 1) >> 1];
      grow_shrink[i] = grow_shrink[(i - 1) >> 1];
      if (grow_shrink[(i - 1) >> 1])
        {
          /* It just so happens to work that the
           * inheritance of these values flips
           * ceil<=>floor for grow and shrink
           */

          switch (dir[i])
            {
              case GSK_SNAP_FLOOR:
                dir[i] = GSK_SNAP_CEIL;
                break;
              case GSK_SNAP_CEIL:
                dir[i] = GSK_SNAP_FLOOR;
                break;
              case GSK_SNAP_ROUND:
              case GSK_SNAP_NONE:
              default:
                g_assert_not_reached ();
                break;
            }
        }
    }
  snap = gsk_rect_snap_new (dir[0], dir[1], dir[2], dir[3]);

  *(GskRectSnap *) out_snap = snap;
  return TRUE;
}

static PangoFont *
font_from_string (PangoFontMap *fontmap,
                  const char   *string,
                  gboolean      allow_fallback)
{
  PangoFontDescription *desc;
  PangoContext *ctx;
  PangoFont *font;

  desc = pango_font_description_from_string (string);
  ctx = pango_font_map_create_context (fontmap);
  font = pango_font_map_load_font (fontmap, ctx, desc);
  g_object_unref (ctx);

  if (font && !allow_fallback)
    {
      PangoFontDescription *desc2;
      const char *family, *family2;

      desc2 = pango_font_describe (font);

      family = pango_font_description_get_family (desc);
      family2 = pango_font_description_get_family (desc2);

      if (g_strcmp0 (family, family2) != 0)
        g_clear_object (&font);

      pango_font_description_free (desc2);
    }

  pango_font_description_free (desc);

  return font;
}

#define MIN_ASCII_GLYPH 32
#define MAX_ASCII_GLYPH 127 /* exclusive */
#define N_ASCII_GLYPHS (MAX_ASCII_GLYPH - MIN_ASCII_GLYPH)

static PangoGlyphString *
create_ascii_glyphs (PangoFont *font)
{
  PangoLanguage *language = pango_language_from_string ("en_US"); /* just pick one */
  PangoCoverage *coverage;
  PangoAnalysis not_a_hack = {
    .shape_engine = NULL, /* unused */
    .lang_engine = NULL, /* unused by pango_shape() */
    .font = font,
    .level = 0,
    .gravity = PANGO_GRAVITY_SOUTH,
    .flags = 0,
    .script = PANGO_SCRIPT_COMMON,
    .language = language,
    .extra_attrs = NULL
  };
  PangoGlyphString *result, *glyph_string;
  guint i;

  result = pango_glyph_string_new ();

  coverage = pango_font_get_coverage (font, language);

  pango_glyph_string_set_size (result, N_ASCII_GLYPHS);
  glyph_string = pango_glyph_string_new ();
  for (i = MIN_ASCII_GLYPH; i < MAX_ASCII_GLYPH; i++)
    {
      const char text[2] = { i, 0 };

      if (!pango_coverage_get (coverage, i))
        {
          result->glyphs[i - MIN_ASCII_GLYPH].glyph = PANGO_GLYPH_INVALID_INPUT;
          continue;
        }

      pango_shape_with_flags (text, 1,
                              text, 1,
                              ¬_a_hack,
                              glyph_string,
                              PANGO_SHAPE_NONE);

      if (glyph_string->num_glyphs != 1)
        result->glyphs[i - MIN_ASCII_GLYPH].glyph = PANGO_GLYPH_INVALID_INPUT;
      else
        result->glyphs[i - MIN_ASCII_GLYPH] = glyph_string->glyphs[0];
    }

  g_object_unref (coverage);

  pango_glyph_string_free (glyph_string);

  return result;
}

static void
delete_file (gpointer data)
{
  char *path = data;

  g_remove (path);
  g_free (path);
}

static void
ensure_fontmap (Context *context)
{
  GPtrArray *files;

  if (context->fontmap)
    return;

  context->fontmap = pango_cairo_font_map_new ();

#ifdef HAVE_PANGOFT
  {
    FcConfig *config;

    config = FcConfigCreate ();
    pango_fc_font_map_set_config (PANGO_FC_FONT_MAP (context->fontmap), config);
    FcConfigDestroy (config);
  }
#endif

  files = g_ptr_array_new_with_free_func (delete_file);

  g_object_set_data_full (G_OBJECT (context->fontmap), "font-files",
                          files, (GDestroyNotify) g_ptr_array_unref);
}

static gboolean
add_font_from_file (Context     *context,
                    const char  *path,
                    GError     **error)
{
  ensure_fontmap (context);

  if (pango_font_map_add_font_file (context->fontmap, path, error))
    {
      GPtrArray *files;

      files = (GPtrArray *) g_object_get_data (G_OBJECT (context->fontmap), "font-files");
      g_ptr_array_add (files, g_strdup (path));
      return TRUE;
    }
  else
    {
      g_remove (path);
      return FALSE;
    }
}

static gboolean
add_font_from_bytes (Context  *context,
                     GBytes   *bytes,
                     GError  **error)
{
  GFile *file;
  GIOStream *iostream;
  GOutputStream *ostream;
  gboolean result;

  file = g_file_new_tmp ("gtk4-font-XXXXXX.ttf", (GFileIOStream **) &iostream, error);
  if (!file)
    return FALSE;

  ostream = g_io_stream_get_output_stream (iostream);
  if (g_output_stream_write_bytes (ostream, bytes, NULL, error) == -1)
    {
      g_object_unref (file);
      g_object_unref (iostream);

      return FALSE;
    }

  g_io_stream_close (iostream, NULL, NULL);
  g_object_unref (iostream);

  result = add_font_from_file (context, g_file_peek_path (file), error);

  g_object_unref (file);

  return result;
}

static gboolean
parse_font (GtkCssParser *parser,
            Context      *context,
            gpointer      out_font)
{
  PangoFont *font = NULL;
  char *font_name;

  font_name = gtk_css_parser_consume_string (parser);
  if (font_name == NULL)
    return FALSE;

  if (gtk_css_parser_has_url (parser))
    {
      GBytes *bytes;
      GError *error = NULL;
      GtkCssLocation start_location;

      start_location = *gtk_css_parser_get_start_location (parser);
      bytes = consume_bytes (parser);
      if (bytes != NULL)
        {
          if (add_font_from_bytes (context, bytes, &error))
            {
              font = font_from_string (context->fontmap, font_name, FALSE);
              if (!font)
                {
                  gtk_css_parser_error (parser,
                                        GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                                        &start_location,
                                        gtk_css_parser_get_end_location (parser),
                                        "The given url does not define a font named \"%s\"",
                                        font_name);
                }
            }
          else
            {
              gtk_css_parser_error (parser,
                                    GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE,
                                    &start_location,
                                    gtk_css_parser_get_end_location (parser),
                                    "%s", error->message);
              g_clear_error (&error);
            }

          g_bytes_unref (bytes);
        }
    }
  else
    {
      if (context->fontmap)
        font = font_from_string (context->fontmap, font_name, FALSE);

      if (!font)
        font = font_from_string (pango_cairo_font_map_get_default (), font_name, TRUE);

      if (!font)
        gtk_css_parser_error_value (parser, "The font \"%s\" does not exist", font_name);
    }

  g_free (font_name);

  if (font)
    {
      *((PangoFont**)out_font) = font;
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

static void
clear_font (gpointer inout_font)
{
  g_clear_object ((PangoFont **) inout_font);
}

#define GLYPH_NEEDS_WIDTH ((PangoGlyphUnit) -1)

static gboolean
parse_glyphs (GtkCssParser *parser,
              Context      *context,
              gpointer      out_glyphs)
{
  PangoGlyphString *glyph_string;

  glyph_string = pango_glyph_string_new ();

  do
    {
      PangoGlyphInfo gi = { 0, { 000}, { 1 } };
      double d, d2;
      int i;

      if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_STRING))
        {
          char *s = gtk_css_parser_consume_string (parser);

          for (i = 0; s[i] != 0; i++)
            {
              if (s[i] < MIN_ASCII_GLYPH || s[i] >= MAX_ASCII_GLYPH)
                {
                  gtk_css_parser_error_value (parser, "Unsupported character %d in string", i);
                }
              gi.glyph = PANGO_GLYPH_INVALID_INPUT - MAX_ASCII_GLYPH + s[i];
              gi.geometry.width = GLYPH_NEEDS_WIDTH;
              pango_glyph_string_set_size (glyph_string, glyph_string->num_glyphs + 1);
              glyph_string->glyphs[glyph_string->num_glyphs - 1] = gi;
            }

          g_free (s);
        }
      else
        {
          if (!gtk_css_parser_consume_integer (parser, &i))
            {
              pango_glyph_string_free (glyph_string);
              return FALSE;
            }
          gi.glyph = i;

          if (gtk_css_parser_has_number (parser))
            {
              gtk_css_parser_consume_number (parser, &d);
              gi.geometry.width = (int) (d * PANGO_SCALE);
            }
          else
            {
              gi.geometry.width = GLYPH_NEEDS_WIDTH;
            }

          if (gtk_css_parser_has_number (parser))
            {
              if (!gtk_css_parser_consume_number (parser, &d) ||
                  !gtk_css_parser_consume_number (parser, &d2))
                {
                  pango_glyph_string_free (glyph_string);
                  return FALSE;
                }
              gi.geometry.x_offset = (int) round (d * PANGO_SCALE);
              gi.geometry.y_offset = (int) round (d2 * PANGO_SCALE);

              if (gtk_css_parser_try_ident (parser, "same-cluster"))
                gi.attr.is_cluster_start = 0;
              else
                gi.attr.is_cluster_start = 1;

              if (gtk_css_parser_try_ident (parser, "color"))
                gi.attr.is_color = 1;
              else
                gi.attr.is_color = 0;
            }

          pango_glyph_string_set_size (glyph_string, glyph_string->num_glyphs + 1);
          glyph_string->glyphs[glyph_string->num_glyphs - 1] = gi;
        }
    }
  while (gtk_css_parser_try_token (parser, GTK_CSS_TOKEN_COMMA));

  *((PangoGlyphString **)out_glyphs) = glyph_string;

  return TRUE;
}

static void
clear_glyphs (gpointer inout_glyphs)
{
  g_clear_pointer ((PangoGlyphString **) inout_glyphs, pango_glyph_string_free);
}

static gboolean
parse_node (GtkCssParser *parser, Context *context, gpointer out_node);

static void
clear_node (gpointer inout_node)
{
  g_clear_pointer ((GskRenderNode **) inout_node, gsk_render_node_unref);
}

static GskRenderNode *
parse_container_node (GtkCssParser *parser,
                      Context      *context)
{
  GPtrArray *nodes;
  const GtkCssToken *token;
  GskRenderNode *node;

  nodes = g_ptr_array_new_with_free_func ((GDestroyNotify) gsk_render_node_unref);

  for (token = gtk_css_parser_get_token (parser);
       !gtk_css_token_is (token, GTK_CSS_TOKEN_EOF);
       token = gtk_css_parser_get_token (parser))
    {
      node = NULL;
      /* We don't want a semicolon here, but the parse_node function will figure
       * that out itself and return an error if we encounter one.
       */

      gtk_css_parser_start_semicolon_block (parser, GTK_CSS_TOKEN_OPEN_CURLY);

      if (parse_node (parser, context, &node))
        g_ptr_array_add (nodes, node);

      gtk_css_parser_end_block (parser);
    }

  node = gsk_container_node_new ((GskRenderNode **) nodes->pdata, nodes->len);

  g_ptr_array_unref (nodes);

  return node;
}

static GdkTexture *
create_default_texture (void)
{
  static const guint32 pixels[100] = {
    0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 00000,
    0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 00000,
    0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 00000,
    0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 00000,
    0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 00000,
    000000xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC,
    000000xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC,
    000000xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC,
    000000xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC,
    000000xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC, 0xFFFF00CC };
  GBytes *bytes;
  GdkTexture *texture;

  bytes = g_bytes_new_static ((guchar *) pixels, 400);
  texture = gdk_memory_texture_new (1010, GDK_MEMORY_DEFAULT, bytes, 40);
  g_bytes_unref (bytes);

  return texture;
}

static GskRenderNode *
create_default_render_node_with_bounds (const graphene_rect_t *rect)
{
  return gsk_color_node_new (&GDK_RGBA("FF00CC"), rect);
}

static GskRenderNode *
create_default_render_node (void)
{
  return create_default_render_node_with_bounds (&GRAPHENE_RECT_INIT (005050));
}

static GskPath *
create_default_path (void)
{
  GskPathBuilder *builder;
  guint i;

  builder = gsk_path_builder_new ();

  gsk_path_builder_move_to (builder, 250);
  for (i = 1; i < 5; i++)
    {
      gsk_path_builder_line_to (builder,
                                sin (i * G_PI * 0.8) * 25 + 25,
                                -cos (i * G_PI * 0.8) * 25 + 25);
    }
  gsk_path_builder_close (builder);

  return gsk_path_builder_free_to_path (builder);
}

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_color_state_rule (GtkCssParser *parser,
                        Context      *context)
{
  char *name = NULL;
  GdkColorState *cs = NULL;
  GdkCicp cicp = { 222, 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);
      g_free (name);
      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_hue_interpolation (GtkCssParser *parser,
                         Context      *context,
                         gpointer      out_value)
{
  GskHueInterpolation interpolation;

  if (gtk_css_parser_try_ident (parser, "shorter"))
    interpolation = GSK_HUE_INTERPOLATION_SHORTER;
  else if (gtk_css_parser_try_ident (parser, "longer"))
    interpolation = GSK_HUE_INTERPOLATION_LONGER;
  else if (gtk_css_parser_try_ident (parser, "increasing"))
    interpolation = GSK_HUE_INTERPOLATION_INCREASING;
  else if (gtk_css_parser_try_ident (parser, "decreasing"))
    interpolation = GSK_HUE_INTERPOLATION_DECREASING;
  else
    {
      gtk_css_parser_error_value (parser, "Unknown value for hue interpolation");
      return FALSE;
    }

  *((GskHueInterpolation *)out_value) = interpolation;
  return TRUE;
}

static gboolean
parse_colors4 (GtkCssParser *parser,
               Context      *context,
               gpointer      out_colors)
{
  GdkColor colors[4];
  int i;

  for (i = 0; i < 4 && !gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF); i ++)
    {
      if (!parse_color (parser, context, &colors[i]))
        return FALSE;
    }
  if (i == 0)
    {
      gtk_css_parser_error_syntax (parser, "Expected a color");
      return FALSE;
    }
  for (; i < 4; i++)
    {
      colors[i] = colors[(i - 1) >> 1];
    }

  memcpy (out_colors, colors, sizeof (GdkColor) * 4);

  return TRUE;
}

static GskRenderNode *
parse_color_node (GtkCssParser *parser,
                  Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GdkColor color = GDK_COLOR_SRGB (100.81);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "color", parse_color, NULL, &color },
    { "snap", parse_rect_snap, NULL, &snap },
  };
  GskRenderNode *node;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  node = gsk_color_node_new2 (&color, &bounds, snap);

  gdk_color_finish (&color);

  return node;
}

static gboolean
parse_repeat (GtkCssParser *parser,
              Context      *context,
              gpointer      out_val)
{
  GskRepeat *ret = out_val;

  if (gtk_css_parser_try_ident (parser, "none"))
    *ret = GSK_REPEAT_NONE;
  else if (gtk_css_parser_try_ident (parser, "pad"))
    *ret = GSK_REPEAT_PAD;
  else if (gtk_css_parser_try_ident (parser, "repeat"))
    *ret = GSK_REPEAT_REPEAT;
  else if (gtk_css_parser_try_ident (parser, "reflect"))
    *ret = GSK_REPEAT_REFLECT;
  else
    return FALSE;

  return TRUE;
}

static GskRenderNode *
parse_linear_gradient_node_internal (GtkCssParser *parser,
                                     Context      *context,
                                     gboolean      repeating)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  graphene_point_t start = GRAPHENE_POINT_INIT (00);
  graphene_point_t end = GRAPHENE_POINT_INIT (050);
  GArray *stops = NULL;
  GdkColorState *interpolation = NULL;
  GskHueInterpolation hue_interpolation = GSK_HUE_INTERPOLATION_SHORTER;
  gboolean premultiplied = TRUE;
  GskRepeat repeat = GSK_REPEAT_PAD;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "start", parse_point, NULL, &start },
    { "end", parse_point, NULL, &end },
    { "stops", parse_stops, clear_stops, &stops },
    { "interpolation", parse_color_state, &clear_color_state, &interpolation },
    { "hue-interpolation", parse_hue_interpolation, NULL, &hue_interpolation },
    { "premultiplied", parse_boolean, NULL, &premultiplied },
    { "repeat", parse_repeat, NULL, &repeat },
  };
  GskGradient *gradient;
  GskRenderNode *result;
  gsize n_decls = G_N_ELEMENTS (declarations);

  if (repeating)
    {
      repeat = GSK_REPEAT_REPEAT;
      n_decls = n_decls - 1;
    }

  parse_declarations (parser, context, declarations, n_decls);
  if (stops == NULL)
    {
      GskGradientStop from = {
        .offset = 0.0,
        .transition_hint = 0.5,
        .color = GDK_COLOR_SRGB (0.667101)
      };
      GskGradientStop to = {
        .offset = 1.0,
        .transition_hint = 0.5,
        .color = GDK_COLOR_SRGB (100.81)
      };

      stops = g_array_new (FALSEFALSEsizeof (GskGradientStop));
      g_array_append_val (stops, from);
      g_array_append_val (stops, to);
    }

  if (interpolation == NULL)
    interpolation = GDK_COLOR_STATE_SRGB;

  gradient = gsk_gradient_new ();
  for (unsigned int i = 0; i < stops->len; i++)
    {
      GskGradientStop *stop = &g_array_index (stops, GskGradientStop, i);
      gsk_gradient_add_stop (gradient, stop->offset, stop->transition_hint, &stop->color);
    }

  gsk_gradient_set_interpolation (gradient, interpolation);
  gsk_gradient_set_hue_interpolation (gradient, hue_interpolation);
  gsk_gradient_set_premultiplied (gradient, premultiplied);
  gsk_gradient_set_repeat (gradient, repeat);

  result = gsk_linear_gradient_node_new2 (&bounds, snap, &start, &end, gradient);

  gsk_gradient_free (gradient);
  clear_stops (&stops);
  clear_color_state (&interpolation);

  return result;
}

static GskRenderNode *
parse_linear_gradient_node (GtkCssParser *parser,
                            Context      *context)
{
  return parse_linear_gradient_node_internal (parser, context, FALSE);
}

static GskRenderNode *
parse_repeating_linear_gradient_node (GtkCssParser *parser,
                                      Context      *context)
{
  return parse_linear_gradient_node_internal (parser, context, TRUE);
}

typedef struct
{
  graphene_point_t center;
  double radius;
  gboolean has_center;
  gboolean has_radius;
} ParsedCircle;

static gboolean
parse_circle (GtkCssParser *parser,
              Context      *context,
              gpointer      out_circle)
{
  ParsedCircle *circle = out_circle;
  double num[3];

  if (!gtk_css_parser_consume_number (parser, &num[0]))
    return FALSE;

  if (gtk_css_parser_has_number (parser))
    {
      if (!gtk_css_parser_consume_number (parser, &num[1]) ||
          !gtk_css_parser_consume_number (parser, &num[2]))
        return FALSE;

      circle->center.x = num[0];
      circle->center.y = num[1];
      circle->radius = num[2];
      circle->has_center = TRUE;
      circle->has_radius = TRUE;
    }
  else
    {
      circle->radius = num[0];
      circle->has_center = FALSE;
      circle->has_radius = TRUE;
    }

  return TRUE;
}

typedef struct
{
  double value;
  gboolean has_value;
} ParsedNumber;

static gboolean
parse_positive_number (GtkCssParser *parser,
                       Context      *context,
                       gpointer      out_number)
{
  ParsedNumber *number = out_number;

  if (!parse_positive_double (parser, context, &number->value))
    return FALSE;

  number->has_value = TRUE;
  return TRUE;
}

static GskRenderNode *
parse_radial_gradient_node_internal (GtkCssParser *parser,
                                     Context      *context,
                                     gboolean      repeating)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  graphene_point_t center = GRAPHENE_POINT_INIT (2525);
  double hradius = 25.0;
  double vradius = 25.0;
  GArray *stops = NULL;
  GdkColorState *interpolation = NULL;
  GskHueInterpolation hue_interpolation = GSK_HUE_INTERPOLATION_SHORTER;
  gboolean premultiplied = TRUE;
  ParsedCircle start = {
    .center = { .x = 25, .y = 25 },
    .radius = 0,
    .has_center = FALSE,
    .has_radius = FALSE
  };
  ParsedCircle end = {
    .center = { .x = 25, .y = 25 },
    .radius = 1,
    .has_center = FALSE,
    .has_radius = FALSE
  };
  ParsedNumber aspect_ratio = { .has_value = FALSE };
  GskRepeat repeat = GSK_REPEAT_PAD;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "center", parse_point, NULL, ¢er },
    { "hradius", parse_strictly_positive_double, NULL, &hradius },
    { "vradius", parse_strictly_positive_double, NULL, &vradius },
    { "start", parse_circle, NULL, &start },
    { "end", parse_circle, NULL, &end },
    { "aspect-ratio", parse_positive_number, NULL, &aspect_ratio },
    { "stops", parse_stops, clear_stops, &stops },
    { "interpolation", parse_color_state, &clear_color_state, &interpolation },
    { "hue-interpolation", parse_hue_interpolation, NULL, &hue_interpolation },
    { "premultiplied", parse_boolean, NULL, &premultiplied },
    { "repeat", parse_repeat, NULL, &repeat },
  };
  GskGradient *gradient;
  GskRenderNode *result;
  gsize n_decls = G_N_ELEMENTS (declarations);

  if (repeating)
    {
      repeat = GSK_REPEAT_REPEAT;
      n_decls = n_decls - 1;
    }

  parse_declarations (parser, context, declarations, n_decls);
  if (stops == NULL)
    {
      GskGradientStop from = {
        .offset = 0.0,
        .transition_hint = 0.5,
        .color = GDK_COLOR_SRGB (0.667101)
      };
      GskGradientStop to = {
        .offset = 1.0,
        .transition_hint = 0.5,
        .color = GDK_COLOR_SRGB (100.81)
      };

      stops = g_array_new (FALSEFALSEsizeof (GskGradientStop));
      g_array_append_val (stops, from);
      g_array_append_val (stops, to);
    }

  if (interpolation == NULL)
    interpolation = GDK_COLOR_STATE_SRGB;

  if (!start.has_center)
    {
      start.center = center;
      start.radius *= hradius;
    }

  if (!end.has_center)
    {
      end.center = center;
      end.radius *= hradius;
    }

  if (!aspect_ratio.has_value)
    aspect_ratio.value = hradius / vradius;

  gradient = gsk_gradient_new ();
  for (unsigned int i = 0; i < stops->len; i++)
    {
      GskGradientStop *stop = &g_array_index (stops, GskGradientStop, i);
      gsk_gradient_add_stop (gradient, stop->offset, stop->transition_hint, &stop->color);
    }

  gsk_gradient_set_interpolation (gradient, interpolation);
  gsk_gradient_set_hue_interpolation (gradient, hue_interpolation);
  gsk_gradient_set_premultiplied (gradient, premultiplied);
  gsk_gradient_set_repeat (gradient, repeat);

  result = gsk_radial_gradient_node_new2 (&bounds,
                                          snap,
                                          &start.center, start.radius,
                                          &end.center, end.radius,
                                          aspect_ratio.value,
                                          gradient);

  clear_stops (&stops);
  clear_color_state (&interpolation);
  gsk_gradient_free (gradient);

  return result;
}

static GskRenderNode *
parse_radial_gradient_node (GtkCssParser *parser,
                            Context      *context)
{
  return parse_radial_gradient_node_internal (parser, context, FALSE);
}

static GskRenderNode *
parse_repeating_radial_gradient_node (GtkCssParser *parser,
                                      Context      *context)
{
  return parse_radial_gradient_node_internal (parser, context, TRUE);
}

static GskRenderNode *
parse_conic_gradient_node (GtkCssParser *parser,
                           Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  graphene_point_t center = GRAPHENE_POINT_INIT (2525);
  double rotation = 0.0;
  GArray *stops = NULL;
  GdkColorState *interpolation = NULL;
  GskHueInterpolation hue_interpolation = GSK_HUE_INTERPOLATION_SHORTER;
  gboolean premultiplied = TRUE;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "center", parse_point, NULL, ¢er },
    { "rotation", parse_double, NULL, &rotation },
    { "stops", parse_stops, clear_stops, &stops },
    { "interpolation", parse_color_state, &clear_color_state, &interpolation },
    { "hue-interpolation", parse_hue_interpolation, NULL, &hue_interpolation },
    { "premultiplied", parse_boolean, NULL, &premultiplied },
  };
  GskGradient *gradient;
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (stops == NULL)
    {
      GskGradientStop from = {
        .offset = 0.0,
        .transition_hint = 0.5,
        .color = GDK_COLOR_SRGB (0.667101)
      };
      GskGradientStop to = {
        .offset = 1.0,
        .transition_hint = 0.5,
        .color = GDK_COLOR_SRGB (100.81)
      };

      stops = g_array_new (FALSEFALSEsizeof (GskGradientStop));
      g_array_append_val (stops, from);
      g_array_append_val (stops, to);
    }

  if (interpolation == NULL)
    interpolation = GDK_COLOR_STATE_SRGB;

  gradient = gsk_gradient_new ();
  for (unsigned int i = 0; i < stops->len; i++)
    {
      GskGradientStop *stop = &g_array_index (stops, GskGradientStop, i);
      gsk_gradient_add_stop (gradient, stop->offset, stop->transition_hint, &stop->color);
    }
  gsk_gradient_set_interpolation (gradient, interpolation);
  gsk_gradient_set_hue_interpolation (gradient, hue_interpolation);
  gsk_gradient_set_premultiplied (gradient, premultiplied);
  gsk_gradient_set_repeat (gradient, GSK_REPEAT_PAD);

  result = gsk_conic_gradient_node_new2 (&bounds, snap, ¢er, rotation, gradient);

  gsk_gradient_free (gradient);
  clear_stops (&stops);
  clear_color_state (&interpolation);

  return result;
}

static GskRenderNode *
parse_inset_shadow_node (GtkCssParser *parser,
                         Context      *context)
{
  GskRoundedRect outline = GSK_ROUNDED_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  GdkColor color = GDK_COLOR_SRGB (0001);
  double dx = 1, dy = 1, blur = 0, spread = 0;
  const Declaration declarations[] = {
    { "outline", parse_rounded_rect, NULL, &outline },
    { "snap", parse_rect_snap, NULL, &snap },
    { "color", parse_color, NULL, &color },
    { "dx", parse_double, NULL, &dx },
    { "dy", parse_double, NULL, &dy },
    { "spread", parse_double, NULL, &spread },
    { "blur", parse_positive_double, NULL, &blur }
  };
  GskRenderNode *node;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  node = gsk_inset_shadow_node_new2 (&outline, snap, &color, &GRAPHENE_POINT_INIT (dx, dy), spread, blur);

  gdk_color_finish (&color);

  return node;
}

G_GNUC_BEGIN_IGNORE_DEPRECATIONS
typedef union {
    gint32 i;
    double v[4];
} UniformValue;

typedef struct {
  GskGLShader *shader;
  GskShaderArgsBuilder *args;
} ShaderInfo;

static void
clear_shader_info (gpointer data)
{
  ShaderInfo *info = data;
  g_clear_object (&info->shader);
  g_clear_pointer (&info->args, gsk_shader_args_builder_unref);
}

static gboolean
parse_shader (GtkCssParser *parser,
              Context      *context,
              gpointer      out_shader_info)
{
  ShaderInfo *shader_info = out_shader_info;
  char *sourcecode = NULL;
  GBytes *bytes;
  GskGLShader *shader;

  if (!parse_string (parser, context, &sourcecode))
    {
      gtk_css_parser_error_value (parser, "Not a string");
      return FALSE;
    }

  bytes = g_bytes_new_take (sourcecode, strlen (sourcecode));
  shader = gsk_gl_shader_new_from_bytes (bytes);
  g_bytes_unref (bytes);

  shader_info->shader = shader;

  return TRUE;
}

static gboolean
parse_uniform_value (GtkCssParser *parser,
                     int           idx,
                     ShaderInfo   *shader_info)
{
  switch (gsk_gl_shader_get_uniform_type (shader_info->shader, idx))
    {
    case GSK_GL_UNIFORM_TYPE_FLOAT:
      {
        double f;
        if (!gtk_css_parser_consume_number (parser, &f))
          return FALSE;
        gsk_shader_args_builder_set_float (shader_info->args, idx, f);
      }
      break;

    case GSK_GL_UNIFORM_TYPE_INT:
      {
        int i;
        if (!gtk_css_parser_consume_integer (parser, &i))
          return FALSE;
        gsk_shader_args_builder_set_int (shader_info->args, idx, i);
      }
      break;

    case GSK_GL_UNIFORM_TYPE_UINT:
      {
        int i;
        if (!gtk_css_parser_consume_integer (parser, &i) || i < 0)
          return FALSE;
        gsk_shader_args_builder_set_uint (shader_info->args, idx, i);
      }
      break;

    case GSK_GL_UNIFORM_TYPE_BOOL:
      {
        int i;
        if (!gtk_css_parser_consume_integer (parser, &i) || (i != 0 && i != 1))
          return FALSE;
        gsk_shader_args_builder_set_bool (shader_info->args, idx, i);
      }
      break;

    case GSK_GL_UNIFORM_TYPE_VEC2:
      {
        double f0, f1;
        graphene_vec2_t v;
        if (!gtk_css_parser_consume_number (parser, &f0) ||
            !gtk_css_parser_consume_number (parser, &f1))
          return FALSE;
        graphene_vec2_init (&v, f0, f1);
        gsk_shader_args_builder_set_vec2 (shader_info->args, idx, &v);
      }
      break;

    case GSK_GL_UNIFORM_TYPE_VEC3:
      {
        double f0, f1, f2;
        graphene_vec3_t v;
        if (!gtk_css_parser_consume_number (parser, &f0) ||
            !gtk_css_parser_consume_number (parser, &f1) ||
            !gtk_css_parser_consume_number (parser, &f2))
          return FALSE;
        graphene_vec3_init (&v, f0, f1, f2);
        gsk_shader_args_builder_set_vec3 (shader_info->args, idx, &v);
      }
      break;

    case GSK_GL_UNIFORM_TYPE_VEC4:
      {
        double f0, f1, f2, f3;
        graphene_vec4_t v;
        if (!gtk_css_parser_consume_number (parser, &f0) ||
            !gtk_css_parser_consume_number (parser, &f1) ||
            !gtk_css_parser_consume_number (parser, &f2) ||
            !gtk_css_parser_consume_number (parser, &f3))
          return FALSE;
        graphene_vec4_init (&v, f0, f1, f2, f3);
        gsk_shader_args_builder_set_vec4 (shader_info->args, idx, &v);
      }
      break;

    case GSK_GL_UNIFORM_TYPE_NONE:
    default:
      g_assert_not_reached ();
      break;
    }

  if (idx < gsk_gl_shader_get_n_uniforms (shader_info->shader))
    {
      if (!gtk_css_parser_try_token (parser, GTK_CSS_TOKEN_COMMA))
        return FALSE;
    }

  return TRUE;
}

static gboolean
parse_shader_args (GtkCssParser *parser,
                   Context      *context,
                   gpointer      data)
{
  ShaderInfo *shader_info = data;
  int n_uniforms;
  int i;

  shader_info->args = gsk_shader_args_builder_new (shader_info->shader, NULL);
  n_uniforms = gsk_gl_shader_get_n_uniforms (shader_info->shader);

  for (i = 0; i < n_uniforms; i++)
    {
      if (!parse_uniform_value (parser, i, shader_info))
        return FALSE;
    }

  return TRUE;
}

static const char default_glsl[] =
  "void\n"
  "mainImage(out vec4 fragColor,\n"
  "          in vec2 fragCoord,\n"
  "          in vec2 resolution,\n"
  "          in vec2 uv)\n"
  "{\n"
  "  fragColor = vec4(1.0, 105.0/255.0, 180.0/255.0, 1.0);\n"
  "}";

static GskGLShader *
get_default_glshader (void)
{
  GBytes *bytes;
  GskGLShader *shader;

  bytes = g_bytes_new (default_glsl, strlen (default_glsl) + 1);
  shader = gsk_gl_shader_new_from_bytes (bytes);
  g_bytes_unref (bytes);

  return shader;
}

static GskRenderNode *
parse_glshader_node (GtkCssParser *parser,
                     Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRenderNode *child[4] = { NULL, };
  ShaderInfo shader_info = {
    NULL,
    NULL,
  };
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "sourcecode", parse_shader, NULL, &shader_info },
    { "args", parse_shader_args, clear_shader_info, &shader_info },
    { "child1", parse_node, clear_node, &child[0] },
    { "child2", parse_node, clear_node, &child[1] },
    { "child3", parse_node, clear_node, &child[2] },
    { "child4", parse_node, clear_node, &child[3] },
  };
  GskGLShader *shader;
  GskShaderArgsBuilder *builder;
  GskRenderNode *node;
  GBytes *args = NULL;
  int len, i;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  for (len = 0; len < 4; len++)
    {
      if (child[len] == NULL)
        break;
    }

  if (shader_info.shader)
    shader = shader_info.shader;
  else
    shader = get_default_glshader ();

  if (shader_info.args)
    builder = shader_info.args;
  else
    builder = gsk_shader_args_builder_new (shader, NULL);

  args = gsk_shader_args_builder_free_to_args (builder);

  node = gsk_gl_shader_node_new (shader, &bounds, args, child, len);

  g_bytes_unref (args);
  g_object_unref (shader);

  for (i = 0; i < 4; i++)
    {
      if (child[i])
        gsk_render_node_unref (child[i]);
    }

  return node;
}
G_GNUC_END_IGNORE_DEPRECATIONS

static GskRenderNode *
parse_mask_node (GtkCssParser *parser,
                 Context      *context)
{
  GskRenderNode *source = NULL;
  GskRenderNode *mask = NULL;
  GskMaskMode mode = GSK_MASK_MODE_ALPHA;
  const Declaration declarations[] = {
    { "mode", parse_mask_mode, NULL, &mode },
    { "source", parse_node, clear_node, &source },
    { "mask", parse_node, clear_node, &mask },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS(declarations));
  if (source == NULL)
    source = create_default_render_node ();
  if (mask == NULL)
    mask = gsk_color_node_new (&GDK_RGBA("AAFF00"), &GRAPHENE_RECT_INIT (005050));

  result = gsk_mask_node_new (source, mask, mode);

  gsk_render_node_unref (source);
  gsk_render_node_unref (mask);

  return result;
}

static GskRenderNode *
parse_border_node (GtkCssParser *parser,
                   Context      *context)
{
  GskRoundedRect outline = GSK_ROUNDED_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  GskRectSnap border_snap = GSK_RECT_SNAP_NONE;
  float widths[4] = { 1111 };
  GdkColor colors[4] = {
    GDK_COLOR_SRGB (0001),
    GDK_COLOR_SRGB (0001),
    GDK_COLOR_SRGB (0001),
    GDK_COLOR_SRGB (0001),
  };
  const Declaration declarations[] = {
    { "outline", parse_rounded_rect, NULL, &outline },
    { "snap", parse_rect_snap, NULL, &snap },
    { "widths", parse_float4, NULL, &widths },
    { "border-snap", parse_rect_snap, NULL, &border_snap },
    { "colors", parse_colors4, NULL, &colors },
  };

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  return gsk_border_node_new2 (&outline, snap, widths, border_snap, colors);
}

static GskRenderNode *
parse_texture_node (GtkCssParser *parser,
                    Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GdkTexture *texture = NULL;
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "texture", parse_texture, clear_texture, &texture },
    { "snap", parse_rect_snap, NULL, &snap },
  };
  GskRenderNode *node;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  if (texture == NULL)
    texture = create_default_texture ();

  node = gsk_texture_node_new2 (texture, &bounds, snap);
  g_object_unref (texture);

  return node;
}

static GskRenderNode *
parse_texture_scale_node (GtkCssParser *parser,
                          Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  GdkTexture *texture = NULL;
  GskScalingFilter filter = GSK_SCALING_FILTER_LINEAR;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "texture", parse_texture, clear_texture, &texture },
    { "filter", parse_scaling_filter, NULL, &filter }
  };
  GskRenderNode *node;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  if (texture == NULL)
    texture = create_default_texture ();

  node = gsk_texture_scale_node_new2 (texture, &bounds, snap, filter);
  g_object_unref (texture);

  return node;
}

static GskRenderNode *
parse_cairo_node (GtkCssParser *parser,
                  Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GdkTexture *pixels = NULL;
  cairo_surface_t *surface;
  GBytes *bytes = NULL;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "pixels", parse_texture, clear_texture, &pixels },
    { "script", parse_bytes, clear_bytes, &bytes }
  };
  GskRenderNode *node;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  node = gsk_cairo_node_new (&bounds);

  if (bytes != NULL)
    {
      surface = interpret_cairo_script (parser, &bounds, bytes);
      g_bytes_unref (bytes);
    }
  else
    surface = NULL;

  if (surface != NULL)
    {
      gsk_cairo_node_set_surface (node, surface);
    }
  else if (pixels != NULL)
    {
      cairo_t *cr = gsk_cairo_node_get_draw_context (node);
      surface = gdk_texture_download_surface (pixels, GDK_COLOR_STATE_SRGB);
      cairo_set_source_surface (cr, surface, 00);
      cairo_paint (cr);
      cairo_destroy (cr);
    }
  else
    {
      /* do nothing */
    }

  g_clear_object (&pixels);
  g_clear_pointer (&surface, cairo_surface_destroy);

  return node;
}

static GskRenderNode *
parse_outset_shadow_node (GtkCssParser *parser,
                          Context      *context)
{
  GskRoundedRect outline = GSK_ROUNDED_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  GdkColor color = GDK_COLOR_SRGB (0001);
  double dx = 1, dy = 1, blur = 0, spread = 0;
  const Declaration declarations[] = {
    { "outline", parse_rounded_rect, NULL, &outline },
    { "snap", parse_rect_snap, NULL, &snap },
    { "color", parse_color, NULL, &color },
    { "dx", parse_double, NULL, &dx },
    { "dy", parse_double, NULL, &dy },
    { "spread", parse_double, NULL, &spread },
    { "blur", parse_positive_double, NULL, &blur }
  };
  GskRenderNode *node;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  node = gsk_outset_shadow_node_new2 (&outline, snap, &color, &GRAPHENE_POINT_INIT (dx, dy), spread, blur);

  gdk_color_finish (&color);

  return node;
}

static GskRenderNode *
parse_transform_node (GtkCssParser *parser,
                      Context      *context)
{
  GskRenderNode *child = NULL;
  GskTransform *transform = NULL;
  const Declaration declarations[] = {
    { "transform", parse_transform, clear_transform, &transform },
    { "child", parse_node, clear_node, &child },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  result = gsk_transform_node_new (child, transform);

  gsk_render_node_unref (child);
  gsk_transform_unref (transform);

  return result;
}

static GskRenderNode *
parse_opacity_node (GtkCssParser *parser,
                    Context      *context)
{
  GskRenderNode *child = NULL;
  double opacity = 0.5;
  const Declaration declarations[] = {
    { "opacity", parse_double, NULL, &opacity },
    { "child", parse_node, clear_node, &child },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  result = gsk_opacity_node_new (child, opacity);

  gsk_render_node_unref (child);

  return result;
}

static GskRenderNode *
parse_color_matrix_node (GtkCssParser *parser,
                         Context      *context)
{
  GskRenderNode *child = NULL;
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (0000);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  graphene_matrix_t matrix;
  GskTransform *transform = NULL;
  graphene_vec4_t offset;
  GdkColorState *color_state = GDK_COLOR_STATE_SRGB;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "matrix", parse_transform, clear_transform, &transform },
    { "offset", parse_vec4, NULL, &offset },
    { "child", parse_node, clear_node, &child },
    { "color-state", parse_default_color_state, clear_color_state, &color_state },
  };
  GskRenderNode *result;
  guint parse_result;

  graphene_vec4_init (&offset, 0000);

  parse_result = parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();
  if (!(parse_result & (1 << 0)))
    bounds = child->bounds;

  gsk_transform_to_matrix (transform, &matrix);

  result = gsk_color_matrix_node_new2 (&bounds, snap, child, color_state, &matrix, &offset);

  gsk_transform_unref (transform);
  gsk_render_node_unref (child);
  gdk_color_state_unref (color_state);

  return result;
}

static GskRenderNode *
parse_cross_fade_node (GtkCssParser *parser,
                       Context      *context)
{
  GskRenderNode *start = NULL;
  GskRenderNode *end = NULL;
  double progress = 0.5;
  const Declaration declarations[] = {
    { "progress", parse_double, NULL, &progress },
    { "start", parse_node, clear_node, &start },
    { "end", parse_node, clear_node, &end },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (start == NULL)
    start = gsk_color_node_new (&GDK_RGBA("AAFF00"), &GRAPHENE_RECT_INIT (005050));
  if (end == NULL)
    end = create_default_render_node ();

  result = gsk_cross_fade_node_new (start, end, progress);

  gsk_render_node_unref (start);
  gsk_render_node_unref (end);

  return result;
}

static GskRenderNode *
parse_blend_node (GtkCssParser *parser,
                  Context      *context)
{
  GskRenderNode *bottom = NULL;
  GskRenderNode *top = NULL;
  GskBlendMode mode = GSK_BLEND_MODE_DEFAULT;
  GdkColorState *color_state = GDK_COLOR_STATE_SRGB;
  const Declaration declarations[] = {
    { "mode", parse_blend_mode, NULL, &mode },
    { "bottom", parse_node, clear_node, &bottom },
    { "top", parse_node, clear_node, &top },
    { "color-state", parse_default_color_state, clear_color_state, &color_state },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (bottom == NULL)
    bottom = gsk_color_node_new (&GDK_RGBA("AAFF00"), &GRAPHENE_RECT_INIT (005050));
  if (top == NULL)
    top = create_default_render_node ();

  result = gsk_blend_node_new2 (bottom, top, color_state, mode);

  gsk_render_node_unref (bottom);
  gsk_render_node_unref (top);

  gdk_color_state_unref (color_state);

  return result;
}

static GskRenderNode *
parse_repeat_node (GtkCssParser *parser,
                   Context      *context)
{
  GskRenderNode *child = NULL;
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (0000);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  graphene_rect_t child_bounds = GRAPHENE_RECT_INIT (0000);
  GskRectSnap child_snap = GSK_RECT_SNAP_NONE;
  GskRepeat repeat = GSK_REPEAT_REPEAT;
  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "child-bounds", parse_rect, NULL, &child_bounds },
    { "child-snap", parse_rect_snap, NULL, &child_snap },
    { "repeat", parse_repeat, NULL, &repeat },
  };
  GskRenderNode *result;
  guint parse_result;

  parse_result = parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  if (!(parse_result & (1 << 1)))
    gsk_render_node_get_bounds (child, &bounds);
  if (!(parse_result & (1 << 3)))
    gsk_render_node_get_bounds (child, &child_bounds);

  result = gsk_repeat_node_new2 (&bounds, snap, child, &child_bounds, child_snap, repeat);

  gsk_render_node_unref (child);

  return result;
}

static gboolean
unpack_glyphs (PangoFont        *font,
               PangoGlyphString *glyphs)
{
  PangoGlyphString *ascii = NULL;
  guint i;

  for (i = 0; i < glyphs->num_glyphs; i++)
    {
      PangoGlyphInfo *gi = &glyphs->glyphs[i];

      if (gi->glyph >= PANGO_GLYPH_INVALID_INPUT - MAX_ASCII_GLYPH &&
          gi->glyph < PANGO_GLYPH_INVALID_INPUT)
        {
          PangoGlyph idx = gi->glyph - (PANGO_GLYPH_INVALID_INPUT - MAX_ASCII_GLYPH) - MIN_ASCII_GLYPH;

          if (ascii == NULL)
            {
              ascii = create_ascii_glyphs (font);
              if (ascii == NULL)
                return FALSE;
            }

          if (ascii->glyphs[idx].glyph == PANGO_GLYPH_INVALID_INPUT)
            {
              g_clear_pointer (&ascii, pango_glyph_string_free);
              return FALSE;
            }

          gi->glyph = ascii->glyphs[idx].glyph;
          gi->geometry.width = ascii->glyphs[idx].geometry.width;
        }
      
      if (gi->geometry.width == GLYPH_NEEDS_WIDTH)
        {
          PangoRectangle rect;

          pango_font_get_glyph_extents (font, gi->glyph, NULL, &rect);

          gi->geometry.width = rect.width;
        }
    }

  g_clear_pointer (&ascii, pango_glyph_string_free);

  return TRUE;
}

static gboolean
parse_hint_style (GtkCssParser *parser,
                  Context      *context,
                  gpointer      out)
{
  if (!parse_enum (parser, CAIRO_GOBJECT_TYPE_HINT_STYLE, out))
    return FALSE;

  if (*(cairo_hint_style_t *) out != CAIRO_HINT_STYLE_NONE &&
      *(cairo_hint_style_t *) out != CAIRO_HINT_STYLE_SLIGHT &&
      *(cairo_hint_style_t *) out != CAIRO_HINT_STYLE_FULL)
    {
      gtk_css_parser_error_value (parser, "Unsupported value for enum \"%s\"",
                                  g_type_name (CAIRO_GOBJECT_TYPE_HINT_STYLE));
      return FALSE;
    }

  return TRUE;
}

static gboolean
parse_antialias (GtkCssParser *parser,
                 Context      *context,
                 gpointer      out)
{
  if (!parse_enum (parser, CAIRO_GOBJECT_TYPE_ANTIALIAS, out))
    return FALSE;

  if (*(cairo_antialias_t *) out != CAIRO_ANTIALIAS_NONE &&
      *(cairo_antialias_t *) out != CAIRO_ANTIALIAS_GRAY)
    {
      gtk_css_parser_error_value (parser, "Unsupported value for enum \"%s\"",
                                  g_type_name (CAIRO_GOBJECT_TYPE_ANTIALIAS));
      return FALSE;
    }

  return TRUE;
}

static gboolean
parse_hint_metrics (GtkCssParser *parser,
                    Context      *context,
                    gpointer      out)
{
  if (!parse_enum (parser, CAIRO_GOBJECT_TYPE_HINT_METRICS, out))
    return FALSE;

  if (*(cairo_hint_metrics_t *) out != CAIRO_HINT_METRICS_OFF &&
      *(cairo_hint_metrics_t *) out != CAIRO_HINT_METRICS_ON)
    {
      gtk_css_parser_error_value (parser, "Unsupported value for enum \"%s\"",
                                  g_type_name (CAIRO_GOBJECT_TYPE_HINT_METRICS));
      return FALSE;
    }

  return TRUE;
}

static GskRenderNode *
parse_text_node (GtkCssParser *parser,
                 Context      *context)
{
  PangoFont *font = NULL;
  graphene_point_t offset = GRAPHENE_POINT_INIT (00);
  GdkColor color = GDK_COLOR_SRGB (0001);
  PangoGlyphString *glyphs = NULL;
  cairo_hint_style_t hint_style = CAIRO_HINT_STYLE_SLIGHT;
  cairo_antialias_t antialias = CAIRO_ANTIALIAS_GRAY;
  cairo_hint_metrics_t hint_metrics = CAIRO_HINT_METRICS_OFF;
  PangoFont *hinted;
  const Declaration declarations[] = {
    { "font", parse_font, clear_font, &font },
    { "offset", parse_point, NULL, &offset },
    { "color", parse_color, NULL, &color },
    { "glyphs", parse_glyphs, clear_glyphs, &glyphs },
    { "hint-style", parse_hint_style, NULL, &hint_style },
    { "antialias", parse_antialias, NULL, &antialias },
    { "hint-metrics", parse_hint_metrics, NULL, &hint_metrics },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  if (font == NULL)
    {
      font = font_from_string (pango_cairo_font_map_get_default (), "Sans 15px"TRUE);
      g_assert (font);
    }

  hinted = gsk_reload_font (font, 1.0, hint_metrics, hint_style, antialias);
  g_object_unref (font);
  font = hinted;

  if (!glyphs)
    {
      const char *text = "Hello";
      PangoGlyphInfo gi = { 0, { 000}, { 1 } };
      guint i;

      glyphs = pango_glyph_string_new ();
      pango_glyph_string_set_size (glyphs, strlen (text));
      for (i = 0; i < strlen (text); i++)
        {
          gi.glyph = PANGO_GLYPH_INVALID_INPUT - MAX_ASCII_GLYPH + text[i];
          gi.geometry.width = GLYPH_NEEDS_WIDTH;
          glyphs->glyphs[i] = gi;
        }
    }

  if (!unpack_glyphs (font, glyphs))
    {
      gtk_css_parser_error_value (parser, "Given font cannot decode the glyph text");
      result = NULL;
    }
  else
    {
      result = gsk_text_node_new2 (font, glyphs, &color, &offset);
      if (result == NULL)
        {
          gtk_css_parser_error_value (parser, "Glyphs result in empty text");
        }
    }

  g_object_unref (font);
  pango_glyph_string_free (glyphs);

  /* return anything, whatever, just not NULL */
  if (result == NULL)
    result = create_default_render_node ();

  gdk_color_finish (&color);

  return result;
}

static GskRenderNode *
parse_blur_node (GtkCssParser *parser,
                 Context      *context)
{
  GskRenderNode *child = NULL;
  double blur_radius = 1.0;
  const Declaration declarations[] = {
    { "blur", parse_positive_double, NULL, &blur_radius },
    { "child", parse_node, clear_node, &child },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  result = gsk_blur_node_new (child, blur_radius);

  gsk_render_node_unref (child);

  return result;
}

static GskRenderNode *
parse_clip_node (GtkCssParser *parser,
                 Context      *context)
{
  GskRoundedRect clip = GSK_ROUNDED_RECT_INIT (005050);
  GskRenderNode *child = NULL;
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  const Declaration declarations[] = {
    { "clip", parse_rounded_rect, NULL, &clip },
    { "child", parse_node, clear_node, &child },
    { "snap", parse_rect_snap, NULL, &snap },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  if (gsk_rounded_rect_is_rectilinear (&clip))
    result = gsk_clip_node_new2 (child, &clip.bounds, snap);
  else
    result = gsk_rounded_clip_node_new (child, &clip);

  gsk_render_node_unref (child);

  return result;
}

static GskRenderNode *
parse_rounded_clip_node (GtkCssParser *parser,
                         Context      *context)
{
  GskRoundedRect clip = GSK_ROUNDED_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  GskRenderNode *child = NULL;
  const Declaration declarations[] = {
    { "clip", parse_rounded_rect, NULL, &clip },
    { "child", parse_node, clear_node, &child },
    { "snap", parse_rect_snap, NULL, &snap },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  result = gsk_rounded_clip_node_new2 (child, &clip, snap);

  gsk_render_node_unref (child);

  return result;
}

static gboolean
parse_path (GtkCssParser *parser,
            Context      *context,
            gpointer      out_path)
{
  GskPath *path;
  char *str = NULL;

  if (!parse_string (parser, context, &str))
    return FALSE;

  path = gsk_path_parse (str);
  g_free (str);

  if (path == NULL)
    {
      gtk_css_parser_error_value (parser, "Invalid path");
      return FALSE;
    }

  *((GskPath **) out_path) = path;

  return TRUE;
}

static void
clear_path (gpointer inout_path)
{
  g_clear_pointer ((GskPath **) inout_path, gsk_path_unref);
}

static gboolean
parse_dash (GtkCssParser *parser,
            Context      *context,
            gpointer      out_dash)
{
  GArray *dash;
  double d;

  /* because CSS does this, too */
  if (gtk_css_parser_try_ident (parser, "none"))
    {
      *((GArray **) out_dash) = NULL;
      return TRUE;
    }

  dash = g_array_new (FALSEFALSEsizeof (float));
  while (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNLESS_NUMBER) ||
         gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_SIGNLESS_INTEGER))
    {
      if (!gtk_css_parser_consume_number (parser, &d))
        {
          g_array_free (dash, TRUE);
          return FALSE;
        }

      g_array_append_vals (dash, (float[1]) { d }, 1);
    }

  if (dash->len == 0)
    {
      gtk_css_parser_error_syntax (parser, "Empty dash array");
      g_array_free (dash, TRUE);
      return FALSE;
    }

  *((GArray **) out_dash) = dash;

  return TRUE;
}

static void
clear_dash (gpointer inout_array)
{
  g_clear_pointer ((GArray **) inout_array, g_array_unref);
}

static gboolean
parse_fill_rule (GtkCssParser *parser,
                 Context      *context,
                 gpointer      out_rule)
{
  return parse_enum (parser, GSK_TYPE_FILL_RULE, out_rule);
}

static GskRenderNode *
parse_fill_node (GtkCssParser *parser,
                 Context      *context)
{
  GskRenderNode *child = NULL;
  GskPath *path = NULL;
  int rule = GSK_FILL_RULE_WINDING;
  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
    { "path", parse_path, clear_path, &path },
    { "fill-rule", parse_fill_rule, NULL, &rule },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (path == NULL)
    path = create_default_path ();
  if (child == NULL)
    {
      graphene_rect_t bounds;
      if (gsk_path_get_bounds (path, &bounds))
        child = create_default_render_node_with_bounds (&bounds);
      else
        child = gsk_container_node_new (NULL, 0);
    }

  result = gsk_fill_node_new (child, path, rule);

  gsk_path_unref (path);

  gsk_render_node_unref (child);

  return result;
}

static gboolean
parse_line_cap (GtkCssParser *parser,
                Context      *context,
                gpointer      out)
{
  return parse_enum (parser, GSK_TYPE_LINE_CAP, out);
}

static gboolean
parse_line_join (GtkCssParser *parser,
                 Context      *context,
                 gpointer      out)
{
  return parse_enum (parser, GSK_TYPE_LINE_JOIN, out);
}

static GskRenderNode *
parse_stroke_node (GtkCssParser *parser,
                   Context      *context)
{
  GskRenderNode *child = NULL;
  GskPath *path = NULL;
  double line_width = 1.0;
  int line_cap = GSK_LINE_CAP_BUTT;
  int line_join = GSK_LINE_JOIN_MITER;
  double miter_limit = 4.0;
  GArray *dash = NULL;
  double dash_offset = 0.0;
  GskStroke *stroke;

  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
    { "path", parse_path, clear_path, &path },
    { "line-width", parse_positive_double, NULL, &line_width },
    { "line-cap", parse_line_cap, NULL, &line_cap },
    { "line-join", parse_line_join, NULL, &line_join },
    { "miter-limit", parse_positive_double, NULL, &miter_limit },
    { "dash", parse_dash, clear_dash, &dash },
    { "dash-offset", parse_double, NULL, &dash_offset}
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (path == NULL)
    path = create_default_path ();

  stroke = gsk_stroke_new (line_width);
  gsk_stroke_set_line_cap (stroke, line_cap);
  gsk_stroke_set_line_join (stroke, line_join);
  gsk_stroke_set_miter_limit (stroke, miter_limit);
  if (dash)
    {
      gsk_stroke_set_dash (stroke, (float *) dash->data, dash->len);
      g_array_free (dash, TRUE);
    }
  gsk_stroke_set_dash_offset (stroke, dash_offset);

  if (child == NULL)
    {
      graphene_rect_t bounds;
      gsk_path_get_stroke_bounds (path, stroke, &bounds);
      child = create_default_render_node_with_bounds (&bounds);
    }

  result = gsk_stroke_node_new (child, path, stroke);

  gsk_path_unref (path);
  gsk_stroke_free (stroke);
  gsk_render_node_unref (child);

  return result;
}

static GskRenderNode *
parse_shadow_node (GtkCssParser *parser,
                   Context      *context)
{
  GskRenderNode *child = NULL;
  GArray *shadows = g_array_new (FALSETRUEsizeof (GskShadowEntry));
  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
    { "shadows", parse_shadows, clear_shadows, shadows },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  if (shadows->len == 0)
    {
      GskShadowEntry default_shadow = { GDK_COLOR_SRGB (0001), GRAPHENE_POINT_INIT (11), 0 };
      g_array_append_val (shadows, default_shadow);
    }

  result = gsk_shadow_node_new2 (child, (GskShadowEntry *)shadows->data, shadows->len);

  clear_shadows (shadows);
  g_array_free (shadows, TRUE);
  gsk_render_node_unref (child);

  return result;
}

static GskRenderNode *
parse_debug_node (GtkCssParser *parser,
                  Context      *context)
{
  char *message = NULL;
  GskRenderNode *child = NULL;
  const Declaration declarations[] = {
    { "message", parse_string, clear_string, &message},
    { "child", parse_node, clear_node, &child },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  result = gsk_debug_node_new (child, message);

  gsk_render_node_unref (child);

  return result;
}

static GskRenderNode *
parse_subsurface_node (GtkCssParser *parser,
                       Context      *context)
{
  GskRenderNode *child = NULL;
  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  result = gsk_subsurface_node_new (child, NULL);

  gsk_render_node_unref (child);

  return result;
}

static gboolean
parse_component_transfer (GtkCssParser *parser,
                          Context      *context,
                          gpointer      out_transfer)
{
  return gsk_component_transfer_parser_parse (parser, out_transfer);
}

static void
clear_component_transfer (gpointer inout_transfer)
{
  g_clear_pointer ((GskComponentTransfer **) inout_transfer, gsk_component_transfer_free);
}

static GskRenderNode *
parse_component_transfer_node (GtkCssParser *parser,
                               Context      *context)
{
  GskRenderNode *child = NULL;
  GskComponentTransfer *red = NULL;
  GskComponentTransfer *green = NULL;
  GskComponentTransfer *blue = NULL;
  GskComponentTransfer *alpha = NULL;
  GdkColorState *color_state = GDK_COLOR_STATE_SRGB;
  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
    { "red", parse_component_transfer, clear_component_transfer, &red },
    { "green", parse_component_transfer, clear_component_transfer, &green },
    { "blue", parse_component_transfer, clear_component_transfer, &blue },
    { "alpha", parse_component_transfer, clear_component_transfer, &alpha },
    { "color-state", parse_default_color_state, clear_color_state, &color_state },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();
  if (red == NULL)
    red = gsk_component_transfer_new_identity ();
  if (green == NULL)
    green = gsk_component_transfer_new_identity ();
  if (blue == NULL)
    blue = gsk_component_transfer_new_identity ();
  if (alpha == NULL)
    alpha = gsk_component_transfer_new_identity ();

  result = gsk_component_transfer_node_new2 (child, color_state, red, green, blue, alpha);

  gsk_component_transfer_free (red);
  gsk_component_transfer_free (green);
  gsk_component_transfer_free (blue);
  gsk_component_transfer_free (alpha);

  gdk_color_state_unref (color_state);

  gsk_render_node_unref (child);

  return result;
}

static GskRenderNode *
parse_copy_node (GtkCssParser *parser,
                 Context      *context)
{
  GskRenderNode *child = NULL;
  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  result = gsk_copy_node_new (child);

  gsk_render_node_unref (child);

  return result;
}

static GskRenderNode *
parse_paste_node (GtkCssParser *parser,
                  Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  gsize depth = 0;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "depth", parse_size, NULL, &depth},
    { "snap", parse_rect_snap, NULL, &snap },
  };
  GskRenderNode *node;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  node = gsk_paste_node_new2 (&bounds, snap, depth);

  return node;
}

static GskRenderNode *
parse_composite_node (GtkCssParser *parser,
                      Context      *context)
{
  GskRenderNode *child = NULL;
  GskRenderNode *mask = NULL;
  GskPorterDuff porter_duff = GSK_PORTER_DUFF_SOURCE_OVER_DEST; 
  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
    { "mask", parse_node, clear_node, &mask },
    { "operator", parse_porter_duff, NULL, &porter_duff },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();
  if (mask == NULL)
    mask = gsk_color_node_new (&GDK_RGBA("FFFFFF"), &GRAPHENE_RECT_INIT (005050));

  result = gsk_composite_node_new (child, mask, porter_duff);

  gsk_render_node_unref (child);
  gsk_render_node_unref (mask);

  return result;
}

static struct {
  const char *name;
  GskIsolation value;
} isolation_flags[] = {
  { "background", GSK_ISOLATION_BACKGROUND },
  { "copy-paste", GSK_ISOLATION_COPY_PASTE }
};

static gboolean
parse_isolation_flags (GtkCssParser *parser,
                       GskIsolation *out_result)
{
  GskIsolation result = 0;
  guint i;

  do
    {
      for (i = 0; i < G_N_ELEMENTS (isolation_flags); i++)
        {
          if (gtk_css_parser_try_ident (parser, isolation_flags[i].name))
            {
              if (result & isolation_flags[i].value)
                {
                  gtk_css_parser_error_value (parser, "Duplicate value");
                  return FALSE;
                }
              result |= isolation_flags[i].value;
              break;
            }
        }
    }
  while (i < G_N_ELEMENTS (isolation_flags));

  if (result == 0)
    {
      gtk_css_parser_error_value (parser, "Expected an isolation feature");
      return FALSE;
    }

  *out_result = result;
  return TRUE;
}

static gboolean
parse_isolation (GtkCssParser *parser,
                 Context      *context,
                 gpointer      out)
{
  GskIsolation isolation;

  if (gtk_css_parser_try_ident (parser, "none"))
    {
      isolation = GSK_ISOLATION_NONE;
    }
  else if (gtk_css_parser_try_ident (parser, "all"))
    {
      isolation = GSK_ISOLATION_ALL;
    }
  else if (gtk_css_parser_try_ident (parser, "not"))
    {
      if (!parse_isolation_flags (parser, &isolation))
        return FALSE;
      isolation = GSK_ISOLATION_ALL & ~isolation;
    }
  else
    {
      if (!parse_isolation_flags (parser, &isolation))
        return FALSE;
    }

  *(GskIsolation *) out = isolation;
  return TRUE;
}

static GskRenderNode *
parse_isolation_node (GtkCssParser *parser,
                      Context      *context)
{
  GskRenderNode *child = NULL;
  GskIsolation features = GSK_ISOLATION_ALL;
  const Declaration declarations[] = {
    { "child", parse_node, clear_node, &child },
    { "isolations", parse_isolation, NULL, &features },
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();

  result = gsk_isolation_node_new (child, features);

  gsk_render_node_unref (child);

  return result;
}

static gboolean
parse_scale (GtkCssParser *parser,
             Context      *context,
             gpointer      out)
{
  double d1, d2;

  if (!gtk_css_parser_consume_number (parser, &d1))
    return FALSE;

  if (gtk_css_parser_has_number (parser))
    {
      if (!gtk_css_parser_consume_number (parser, &d2))
        return FALSE;
    }
  else
    {
      d2 = d1;
    }

  *(graphene_size_t *) out = GRAPHENE_SIZE_INIT (d1, d2);
  return TRUE;
}

static gboolean
parse_four_floats (GtkCssParser *parser,
                   Context      *context,
                   gpointer      out)
{
  for (unsigned int i = 0; i < 4; i++)
    {
      double n;

      if (!gtk_css_parser_consume_number (parser, &n))
        return FALSE;

      ((float *) out)[i] = n;
    }

  return TRUE;
}

static const char *channel_names[] = { "red""green""blue""alpha" };

static gboolean
parse_channels (GtkCssParser *parser,
                Context      *context,
                gpointer      out)
{
  guint tmp[2];
  GdkColorChannel *channels = out;
  gsize i, j;

  for (i = 0; i < G_N_ELEMENTS (tmp); i++)
    {
      for (j = 0; j < G_N_ELEMENTS (channel_names); j++)
        {
          if (gtk_css_parser_try_ident (parser, channel_names[j]))
            {
              tmp[i] = j;
              break;
            }
        }
      if (j == G_N_ELEMENTS (channel_names))
        {
          gtk_css_parser_error_value (parser, "Not a valid channel name");
          return FALSE;
        }
    }

  for (i = 0; i < G_N_ELEMENTS (tmp); i++)
    channels[i] = tmp[i];

  return TRUE;
}

static GskRenderNode *
parse_displacement_node (GtkCssParser *parser,
                         Context      *context)
{
  GskRenderNode *child = NULL, *displacement = NULL;
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  GdkColorChannel channels[2] = { GDK_COLOR_CHANNEL_RED, GDK_COLOR_CHANNEL_GREEN };
  graphene_size_t max = { 55 };
  graphene_size_t scale = { 1010 };
  /* using size_t because parse function */
  graphene_size_t offset = { 0.50.5 };
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "child", parse_node, clear_node, &child },
    { "displacement", parse_node, clear_node, &displacement },
    { "max", parse_scale, NULL, &max },
    { "scale", parse_scale, NULL, &scale },
    { "offset", parse_scale, NULL, &offset },
    { "channels", parse_channels, NULL, channels }
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (child == NULL)
    child = create_default_render_node ();
  if (displacement == NULL)
    displacement = create_default_render_node ();

  result = gsk_displacement_node_new (&bounds,
                                      snap,
                                      child,
                                      displacement,
                                      channels,
                                      &max,
                                      &scale,
                                      &GRAPHENE_POINT_INIT (offset.width, offset.height));

  gsk_render_node_unref (child);
  gsk_render_node_unref (displacement);

  return result;
}

static GskRenderNode *
parse_arithmetic_node (GtkCssParser *parser,
                       Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  GskRenderNode *first = NULL;
  GskRenderNode *second = NULL;
  GdkColorState *color_state = GDK_COLOR_STATE_SRGB;
  float k[4] = { 0000 };
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "first", parse_node, clear_node, &first },
    { "second", parse_node, clear_node, &second },
    { "k", parse_four_floats, NULL, k },
    { "color-state", parse_default_color_state, clear_color_state, &color_state }
  };
  GskRenderNode *result;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));
  if (first == NULL)
    first = gsk_color_node_new (&GDK_RGBA("AAFF00"), &GRAPHENE_RECT_INIT (005050));
  if (second == NULL)
    second = create_default_render_node ();

  result = gsk_arithmetic_node_new (&bounds, snap, first, second, color_state, k);

  gsk_render_node_unref (first);
  gsk_render_node_unref (second);
  gdk_color_state_unref (color_state);

  return result;
}

static GskRenderNode *
parse_turbulence_node (GtkCssParser *parser,
                       Context      *context)
{
  graphene_rect_t bounds = GRAPHENE_RECT_INIT (005050);
  GskRectSnap snap = GSK_RECT_SNAP_NONE;
  graphene_size_t base_frequency = { 00 };
  size_t num_octaves = 1;
  unsigned int seed = 0;
  GskNoiseType noise_type = GSK_NOISE_TURBULENCE;
  gboolean stitch_tiles = FALSE;
  GdkColorState *color_state = GDK_COLOR_STATE_SRGB;
  const Declaration declarations[] = {
    { "bounds", parse_rect, NULL, &bounds },
    { "snap", parse_rect_snap, NULL, &snap },
    { "base-frequency", parse_scale, NULL, &base_frequency },
    { "num-octaves", parse_size, NULL, &num_octaves },
    { "seed", parse_unsigned, NULL, &seed },
    { "noise-type", parse_noise_type, NULL, &noise_type },
    { "stitch-tiles", parse_boolean, NULL, &stitch_tiles },
    { "color-state", parse_default_color_state, clear_color_state, &color_state }
  };
  GskRenderNode *node;

  parse_declarations (parser, context, declarations, G_N_ELEMENTS (declarations));

  node = gsk_turbulence_node_new (&bounds, snap, color_state,
                                  &base_frequency,
                                  num_octaves, seed, noise_type, stitch_tiles);

  gdk_color_state_unref (color_state);

  return node;
}

static gboolean
parse_node (GtkCssParser *parser,
            Context      *context,
            gpointer      out_node)
{
  static struct {
    const char *name;
    GskRenderNode * (* func) (GtkCssParser *, Context *);
  } node_parsers[] = {
    { "blend", parse_blend_node },
    { "blur", parse_blur_node },
    { "border", parse_border_node },
    { "cairo", parse_cairo_node },
    { "clip", parse_clip_node },
    { "color", parse_color_node },
    { "color-matrix", parse_color_matrix_node },
    { "container", parse_container_node },
    { "cross-fade", parse_cross_fade_node },
    { "debug", parse_debug_node },
    { "inset-shadow", parse_inset_shadow_node },
    { "linear-gradient", parse_linear_gradient_node },
    { "radial-gradient", parse_radial_gradient_node },
    { "conic-gradient", parse_conic_gradient_node },
    { "opacity", parse_opacity_node },
    { "outset-shadow", parse_outset_shadow_node },
    { "repeat", parse_repeat_node },
    { "repeating-linear-gradient", parse_repeating_linear_gradient_node },
    { "repeating-radial-gradient", parse_repeating_radial_gradient_node },
    { "rounded-clip", parse_rounded_clip_node },
    { "fill", parse_fill_node },
    { "stroke", parse_stroke_node },
    { "shadow", parse_shadow_node },
    { "text", parse_text_node },
    { "texture", parse_texture_node },
    { "texture-scale", parse_texture_scale_node },
    { "transform", parse_transform_node },
    { "glshader", parse_glshader_node },
    { "mask", parse_mask_node },
    { "subsurface", parse_subsurface_node },
    { "component-transfer", parse_component_transfer_node },
    { "copy", parse_copy_node },
    { "paste", parse_paste_node },
    { "composite", parse_composite_node },
    { "isolation", parse_isolation_node },
    { "displacement", parse_displacement_node },
    { "arithmetic", parse_arithmetic_node },
    { "turbulence", parse_turbulence_node },
  };
  GskRenderNode **node_p = out_node;
  guint i;

  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_STRING))
    {
      GskRenderNode *node;
      char *node_name;

      node_name = gtk_css_parser_consume_string (parser);

      if (context->named_nodes)
        node = g_hash_table_lookup (context->named_nodes, node_name);
      else
        node = NULL;

      if (node)
        {
          *node_p = gsk_render_node_ref (node);
          g_free (node_name);
          return TRUE;
        }
      else
        {
          gtk_css_parser_error_value (parser, "No node named \"%s\"", node_name);
          g_free (node_name);
          return FALSE;
        }
    }

  for (i = 0; i < G_N_ELEMENTS (node_parsers); i++)
    {
      if (gtk_css_parser_try_ident (parser, node_parsers[i].name))
        {
          GskRenderNode *node;
          GtkCssLocation node_name_start_location, node_name_end_location;
          char *node_name;

          if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_STRING))
            {
              node_name_start_location = *gtk_css_parser_get_start_location (parser);
              node_name_end_location = *gtk_css_parser_get_end_location (parser);
              node_name = gtk_css_parser_consume_string (parser);
            }
          else
            node_name = NULL;

          if (!gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF))
            {
              gtk_css_parser_error_syntax (parser, "Expected '{' after node name");
              return FALSE;
            }

          gtk_css_parser_end_block_prelude (parser);
          node = node_parsers[i].func (parser, context);
          if (node)
            {
              if (!gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_EOF))
                gtk_css_parser_error_syntax (parser, "Expected '}' at end of node definition");
              g_clear_pointer (node_p, gsk_render_node_unref);

              if (node_name)
                {
                  if (context->named_nodes == NULL)
                    context->named_nodes = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                                  g_free, (GDestroyNotify) gsk_render_node_unref);
                  if (g_hash_table_lookup (context->named_nodes, node_name))
                    {
                      gtk_css_parser_error (parser,
                                            GTK_CSS_PARSER_ERROR_FAILED,
                                            &node_name_start_location,
                                            &node_name_end_location,
                                            "A node named \"%s\" already exists.", node_name);
                    }
                  else
                    {
                      g_hash_table_insert (context->named_nodes, g_strdup (node_name), gsk_render_node_ref (node));
                    }
                }

              *node_p = node;
            }

          g_free (node_name);

          return node != NULL;
        }
    }

  if (gtk_css_parser_has_token (parser, GTK_CSS_TOKEN_IDENT))
    gtk_css_parser_error_value (parser, "\"%s\" is not a valid node name",
                                gtk_css_token_get_string (gtk_css_parser_get_token (parser)));
  else
    gtk_css_parser_error_syntax (parser, "Expected a node name");

  return FALSE;
}

static void
gsk_render_node_parser_error (GtkCssParser         *parser,
                              const GtkCssLocation *start,
                              const GtkCssLocation *end,
                              const GError         *error,
                              gpointer              user_data)
{
  struct {
    GskParseErrorFunc error_func;
    gpointer user_data;
  } *error_func_pair = user_data;

  if (error_func_pair->error_func)
    error_func_pair->error_func ((const GskParseLocation *)start,
                                 (const GskParseLocation *)end,
                                 error,
                                 error_func_pair->user_data);
}

GskRenderNode *
gsk_render_node_deserialize_from_bytes (GBytes            *bytes,
                                        GskParseErrorFunc  error_func,
                                        gpointer           user_data)
{
  GskRenderNode *root = NULL;
  GtkCssParser *parser;
  Context context;
  struct {
    GskParseErrorFunc error_func;
    gpointer user_data;
  } error_func_pair = { error_func, user_data };

  parser = gtk_css_parser_new_for_bytes (bytes, NULL, gsk_render_node_parser_error,
                                         &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)
    {
      GskRenderNode *child = gsk_container_node_get_child (root, 0);

      gsk_render_node_ref (child);
      gsk_render_node_unref (root);
      root = child;
    }

  context_finish (&context);
  gtk_css_parser_unref (parser);

  return root;
}



typedef struct
{
  int indentation_level;
  GString *str;
  GHashTable *named_nodes;
  gsize named_node_counter;
  GHashTable *named_textures;
  gsize named_texture_counter;
  GHashTable *named_color_states;
  gsize named_color_state_counter;
  GHashTable *fonts;
} Printer;

static void
printer_init_check_texture (Printer    *printer,
                            GdkTexture *texture)
{
  gpointer name;

  if (!g_hash_table_lookup_extended (printer->named_textures, texture, NULL, &name))
    g_hash_table_insert (printer->named_textures, texture, NULL);
  else if (name == NULL)
    g_hash_table_insert (printer->named_textures, texture, g_strdup (""));
}

typedef struct {
  hb_face_t *face;
  hb_subset_input_t *input;
  gboolean serialized;
} FontInfo;

static void
font_info_free (gpointer data)
{
  FontInfo *info = (FontInfo *) data;

  hb_face_destroy (info->face);
  if (info->input)
    hb_subset_input_destroy (info->input);
  g_free (info);
}

static guint
font_info_hash (gconstpointer v)
{
  const FontInfo *info = (const FontInfo *) v;
  hb_blob_t *blob;
  const char *data;
  unsigned int length;
  guint ret;

  blob = hb_face_reference_blob (info->face);

  data = hb_blob_get_data (blob, &length);
  ret = *(guint *) data;

  hb_blob_destroy (blob);

  return ret;
}

static gboolean
font_info_equal (gconstpointer v0,
                 gconstpointer v1)
{
  const FontInfo *info0 = (const FontInfo *) v0;
  const FontInfo *info1 = (const FontInfo *) v1;
  hb_blob_t *b0, *b1;
  const char *data0, *data1;
  unsigned int length0, length1;
  gboolean ret;

  b0 = hb_face_reference_blob (info0->face);
  b1 = hb_face_reference_blob (info1->face);

  data0 = hb_blob_get_data (b0, &length0);
  data1 = hb_blob_get_data (b1, &length1);

  ret = length0 == length1 &&
        memcmp (data0, data1, length0) == 0;

  hb_blob_destroy (b0);
  hb_blob_destroy (b1);

  return ret;
}

static void
printer_init_collect_font_info (Printer       *printer,
                                GskRenderNode *node)
{
  PangoFont *font;
  FontInfo lookup;
  FontInfo *info;

  font = gsk_text_node_get_font (node);

  lookup.face = hb_font_get_face (pango_font_get_hb_font (font));
  info = (FontInfo *) g_hash_table_lookup (printer->fonts, &lookup);
  if (!info)
    {
      info = g_new0 (FontInfo, 1);

      info->face = hb_face_reference (hb_font_get_face (pango_font_get_hb_font (font)));
      if (!g_object_get_data (G_OBJECT (pango_font_get_font_map (font)), "font-files"))
        {
          if (g_strcmp0 (g_getenv ("GSK_SUBSET_FONTS"), "1") == 0)
            {
              info->input = hb_subset_input_create_or_fail ();
              hb_subset_input_set_flags (info->input, HB_SUBSET_FLAGS_RETAIN_GIDS);
            }
          else
            info->serialized = TRUE/* Don't subset (or serialize) system fonts */
        }

      g_hash_table_add (printer->fonts, info);
    }

  if (info->input)
    {
      const PangoGlyphInfo *glyphs;
      guint n_glyphs;

      glyphs = gsk_text_node_get_glyphs (node, &n_glyphs);

      for (guint i = 0; i < n_glyphs; i++)
        hb_set_add (hb_subset_input_glyph_set (info->input), glyphs[i].glyph);
    }
}

static void
printer_init_duplicates_for_node (Printer       *printer,
                                  GskRenderNode *node)
{
  gpointer name;

  if (g_hash_table_lookup_extended (printer->named_nodes, node, NULL, &name))
    {
      /* We've handled this node before */

      if (name == NULL)
        g_hash_table_insert (printer->named_nodes, node, g_strdup (""));
      return;
    }

  g_hash_table_insert (printer->named_nodes, node, NULL);

  switch (gsk_render_node_get_node_type (node))
    {
    case GSK_TEXT_NODE:
      printer_init_collect_font_info (printer, node);
      break;

    case GSK_TEXTURE_NODE:
      printer_init_check_texture (printer, gsk_texture_node_get_texture (node));
      break;

    case GSK_TEXTURE_SCALE_NODE:
      printer_init_check_texture (printer, gsk_texture_scale_node_get_texture (node));
      break;

    case GSK_COLOR_NODE:
    case GSK_BORDER_NODE:
    case GSK_INSET_SHADOW_NODE:
    case GSK_OUTSET_SHADOW_NODE:
    case GSK_LINEAR_GRADIENT_NODE:
    case GSK_REPEATING_LINEAR_GRADIENT_NODE:
    case GSK_RADIAL_GRADIENT_NODE:
    case GSK_REPEATING_RADIAL_GRADIENT_NODE:
    case GSK_CONIC_GRADIENT_NODE:
    case GSK_CAIRO_NODE:
    case GSK_PASTE_NODE:
    case GSK_TRANSFORM_NODE:
    case GSK_OPACITY_NODE:
    case GSK_COLOR_MATRIX_NODE:
    case GSK_BLUR_NODE:
    case GSK_REPEAT_NODE:
    case GSK_CLIP_NODE:
    case GSK_ROUNDED_CLIP_NODE:
    case GSK_SHADOW_NODE:
    case GSK_DEBUG_NODE:
    case GSK_FILL_NODE:
    case GSK_STROKE_NODE:
    case GSK_BLEND_NODE:
    case GSK_MASK_NODE:
    case GSK_CROSS_FADE_NODE:
    case GSK_GL_SHADER_NODE:
    case GSK_CONTAINER_NODE:
    case GSK_SUBSURFACE_NODE:
    case GSK_COMPONENT_TRANSFER_NODE:
    case GSK_COPY_NODE:
    case GSK_COMPOSITE_NODE:
    case GSK_ISOLATION_NODE:
    case GSK_DISPLACEMENT_NODE:
    case GSK_ARITHMETIC_NODE:
    case GSK_TURBULENCE_NODE:
      {
        GskRenderNode **children;
        gsize i, n_children;

        children = gsk_render_node_get_children (node, &n_children);
        for (i = 0; i < n_children; i++)
          {
            printer_init_duplicates_for_node (printer, children[i]);
          }
      }
      break;

    default:
    case GSK_NOT_A_RENDER_NODE:
      g_assert_not_reached ();
      break;

    }
}

static void
printer_init (Printer       *self,
              GskRenderNode *node)
{
  self->indentation_level = 0;
  self->str = g_string_new (NULL);
  self->named_nodes = g_hash_table_new_full (NULL, NULL, NULL, g_free);
  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 (font_info_hash, font_info_equal, font_info_free, NULL);

  printer_init_duplicates_for_node (self, node);
}

static void
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);
}

#define IDENT_LEVEL 2 /* Spaces per level */
static void
_indent (Printer *self)
{
  if (self->indentation_level > 0)
    g_string_append_printf (self->str, "%*s", self->indentation_level * IDENT_LEVEL, " ");
}
#undef IDENT_LEVEL

static void
start_node (Printer    *self,
            const char *node_type,
            const char *node_name)
{
  g_string_append_printf (self->str, "%s ", node_type);
  if (node_name)
    {
      gtk_css_print_string (self->str, node_name, FALSE);
      g_string_append_c (self->str, ' ');
    }
  g_string_append_printf (self->str, "{\n");
  self->indentation_level ++;
}

static void
end_node (Printer *self)
{
  self->indentation_level --;
  _indent (self);
  g_string_append (self->str, "}\n");
}

static void
string_append_double (GString *string,
                      double   d)
{
  char buf[G_ASCII_DTOSTR_BUF_SIZE];

  g_ascii_formatd (buf, G_ASCII_DTOSTR_BUF_SIZE, "%g", d);
  g_string_append (string, buf);
}


static void
append_rect (GString               *str,
             const graphene_rect_t *r)
{
  string_append_double (str, r->origin.x);
  g_string_append_c (str, ' ');
  string_append_double (str, r->origin.y);
  g_string_append_c (str, ' ');
  string_append_double (str, r->size.width);
  g_string_append_c (str, ' ');
  string_append_double (str, r->size.height);
}

static void
append_rounded_rect (GString              *str,
                     const GskRoundedRect *r)
{
  append_rect (str, &r->bounds);

  if (!gsk_rounded_rect_is_rectilinear (r))
    {
      gboolean all_the_same = TRUE;
      gboolean all_square = TRUE;
      float w = r->corner[0].width;
      float h = r->corner[0].height;
      int i;

      for (i = 1; i < 4; i ++)
        {
          if (r->corner[i].width != w ||
              r->corner[i].height != h)
            all_the_same = FALSE;

          if (r->corner[i].width != r->corner[i].height)
            all_square = FALSE;

        }

      g_string_append (str, " / ");

      if (all_the_same)
        {
          string_append_double (str, w);
        }
      else if (all_square)
        {
          string_append_double (str, r->corner[0].width);
          g_string_append_c (str, ' ');
          string_append_double (str, r->corner[1].width);
          g_string_append_c (str, ' ');
          string_append_double (str, r->corner[2].width);
          g_string_append_c (str, ' ');
          string_append_double (str, r->corner[3].width);
        }
      else
        {
          for (i = 0; i < 4; i ++)
            {
              string_append_double (str, r->corner[i].width);
              g_string_append_c (str, ' ');
            }

          g_string_append (str, "/ ");

          for (i = 0; i < 3; i ++)
            {
              string_append_double (str, r->corner[i].height);
              g_string_append_c (str, ' ');
            }

          string_append_double (str, r->corner[3].height);
        }
    }
}

static void
append_point (GString                *str,
              const graphene_point_t *p)
{
  string_append_double (str, p->x);
  g_string_append_c (str, ' ');
  string_append_double (str, p->y);
}

static void
append_vec4 (GString               *str,
             const graphene_vec4_t *v)
{
  string_append_double (str, graphene_vec4_get_x (v));
  g_string_append_c (str, ' ');
  string_append_double (str, graphene_vec4_get_y (v));
  g_string_append_c (str, ' ');
  string_append_double (str, graphene_vec4_get_z (v));
  g_string_append_c (str, ' ');
  string_append_double (str, graphene_vec4_get_w (v));
}

static void
append_float_param (Printer    *p,
                    const char *param_name,
                    float       value,
                    float       default_value)
{
  /* Don't approximate-compare here, better be too verbose */
  if (value == default_value)
    return;

  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  string_append_double (p->str, value);
  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_size_param (Printer    *p,
                   const char *param_name,
                   gsize       value)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: %" G_GSIZE_FORMAT ";\n", param_name, value);
}

static void
append_boolean_param (Printer    *p,
                      const char *param_name,
                      gboolean    value)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: %s;\n", param_name, value ? "true" : "false");
}

static void
print_color_state (Printer       *p,
                   GdkColorState *color_state)
{
  if (GDK_IS_DEFAULT_COLOR_STATE (color_state) ||
      GDK_IS_BUILTIN_COLOR_STATE (color_state))
    {
      g_string_append (p->str, gdk_color_state_get_name (color_state));
    }
  else
    {
      gpointer name;

      if (!g_hash_table_lookup_extended (p->named_color_states, color_state, NULL, &name))
        {
          name = g_strdup_printf ("cicp%zu", ++p->named_color_state_counter);
          g_hash_table_insert (p->named_color_states, color_state, name);
        }
      g_assert (name != NULL);
      g_string_append_c (p->str, '"');
      g_string_append (p->str, name);
      g_string_append_c (p->str, '"');
    }
}

static void
print_color (Printer        *p,
             const GdkColor *color)
{
  if (gdk_color_state_equal (color->color_state, GDK_COLOR_STATE_SRGB) &&
      round (CLAMP (color->red, 01) * 255) == color->red * 255 &&
      round (CLAMP (color->green, 01) * 255) == color->green * 255 &&
      round (CLAMP (color->blue, 01) * 255) == color->blue * 255)
    {
      gdk_rgba_print ((const GdkRGBA *) color->values, p->str);
    }
  else
    {
      g_string_append (p->str, "color(");
      print_color_state (p, color->color_state);
      g_string_append_c (p->str, ' ');
      string_append_double (p->str, color->r);
      g_string_append_c (p->str, ' ');
      string_append_double (p->str, color->g);
      g_string_append_c (p->str, ' ');
      string_append_double (p->str, color->b);
      if (color->a < 1)
        {
          g_string_append (p->str, " / ");
          string_append_double (p->str, color->a);
        }
      g_string_append_c (p->str, ')');
    }
}

static void
append_color_param (Printer        *p,
                    const char     *param_name,
                    const GdkColor *color)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  print_color (p, color);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_rect_param (Printer               *p,
                   const char            *param_name,
                   const graphene_rect_t *value)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  append_rect (p->str, value);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_rounded_rect_param (Printer              *p,
                           const char           *param_name,
                           const GskRoundedRect *value)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  append_rounded_rect (p->str, value);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_point_param (Printer                *p,
                    const char             *param_name,
                    const graphene_point_t *value)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  append_point (p->str, value);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_circle_param (Printer                *p,
                     const char             *param_name,
                     const graphene_point_t *center,
                     float                   radius)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  append_point (p->str, center);
  g_string_append_c (p->str, ' ');
  string_append_double (p->str, radius);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_repeat_param (Printer    *p,
                     const char *param_name,
                     GskRepeat   value)
{
  const char *names[] = { "none""pad""repeat""reflect" };

  _indent (p);
  g_string_append_printf (p->str, "%s: %s;\n", param_name, names[value]);
}

static void
append_string_param (Printer    *p,
                     const char *param_name,
                     const char *value)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  gtk_css_print_string (p->str, value, TRUE);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static const char *
enum_to_nick (GType type,
              int   value)
{
  GEnumClass *class;
  GEnumValue *v;

  class = g_type_class_ref (type);
  v = g_enum_get_value (class, value);
  g_type_class_unref (class);

  return v->value_nick;
}

static void
append_enum_param (Printer    *p,
                   const char *param_name,
                   GType       type,
                   int         value)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  g_string_append (p->str, enum_to_nick (type, value));
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_noise_param (Printer      *p,
                    const char   *param_name,
                    GskNoiseType  noise_type)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  g_string_append (p->str, get_noise_type_name (noise_type));
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static gboolean
snap_direction_equal (GskRectSnap  snap,
                      guint        first_id,
                      guint        second_id,
                      gboolean    *grow_shrink)
{
  GskSnapDirection first = gsk_rect_snap_get_direction (snap, first_id);
  GskSnapDirection second = gsk_rect_snap_get_direction (snap, second_id);

  if (first == second)
    {
      *grow_shrink = FALSE;
      return TRUE;
    }

  if ((first == GSK_SNAP_CEIL && second == GSK_SNAP_FLOOR) ||
      (first == GSK_SNAP_FLOOR && second == GSK_SNAP_CEIL))
    {
      *grow_shrink = TRUE;
      return TRUE;
    }

  return FALSE;
}

static void G_GNUC_UNUSED
append_snap_param (Printer     *p,
                   const char  *param_name,
                   GskRectSnap  snap)
{
  static const char *names[] = {
    [GSK_SNAP_NONE] = "none",
    [GSK_SNAP_FLOOR] = "floor",
    [GSK_SNAP_CEIL] = "ceil",
    [GSK_SNAP_ROUND] = "round",
  };
  gboolean grow_shrink[4] = { FALSE, };
  guint i, n;

  if (snap == GSK_RECT_SNAP_NONE)
    return;

  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);

  if (snap_direction_equal (snap, 13, &grow_shrink[1]))
    {
      if (snap_direction_equal (snap, 02, &grow_shrink[0]))
        {
          gboolean grow_shrink_check;

          if (snap_direction_equal (snap, 01, &grow_shrink_check) &&
              grow_shrink_check == grow_shrink[0] &&
              grow_shrink_check == grow_shrink[1])
            n = 1;
          else
            n = 2;
        }
      else
        n = 3;
    }
  else
    n = 4;

  for (i = 0; i < n; i++)
    {
      if (i > 0)
        g_string_append_c (p->str, ' ');

      if (grow_shrink[i])
        {
          if ((gsk_rect_snap_get_direction (snap, i) == GSK_SNAP_CEIL) ^
              (i == 0 || i == 3))
            g_string_append (p->str, "grow");
          else
            g_string_append (p->str, "shrink");
        }
      else
        {
          g_string_append (p->str, names[gsk_rect_snap_get_direction (snap, i)]);
        }
    }
  g_string_append (p->str, ";\n");
}

static void
append_vec4_param (Printer               *p,
                   const char            *param_name,
                   const graphene_vec4_t *value)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  append_vec4 (p->str, value);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_matrix_param (Printer                 *p,
                     const char              *param_name,
                     const graphene_matrix_t *value)
{
  GskTransform *transform = NULL;

  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);

  transform = gsk_transform_matrix (transform, value);
  gsk_transform_print (transform,p->str);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');

  gsk_transform_unref (transform);
}

static void
append_transform_param (Printer      *p,
                        const char   *param_name,
                        GskTransform *transform)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  gsk_transform_print (transform, p->str);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_memory_format_param (Printer         *p,
                            const char      *param_name,
                            GdkMemoryFormat  format)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: %s;\n", param_name, gdk_memory_format_get_name (format));
}

static void render_node_print (Printer       *p,
                               GskRenderNode *node);

static void
append_node_param (Printer       *p,
                   const char    *param_name,
                   GskRenderNode *node)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  render_node_print (p, node);
}

static void
append_stops_param (Printer           *p,
                    const char        *param_name,
                    const GskGradient *gradient)
{
  gsize i;

  _indent (p);
  g_string_append (p->str, param_name);
  g_string_append (p->str, ": ");

  for (i = 0; i < gsk_gradient_get_n_stops (gradient); i ++)
    {
      if (i > 0)
        g_string_append (p->str, ", ");

      string_append_double (p->str, gsk_gradient_get_stop_offset (gradient, i));

      if (i > 0 && gsk_gradient_get_stop_transition_hint (gradient, i) != 0.5)
        {
          g_string_append_c (p->str, ' ');
          string_append_double (p->str, gsk_gradient_get_stop_transition_hint (gradient, i));
        }

      g_string_append_c (p->str, ' ');
      print_color (p, gsk_gradient_get_stop_color (gradient, i));
    }
  g_string_append (p->str, ";\n");
}

static void
append_color_state_param (Printer       *p,
                          const char    *param_name,
                          GdkColorState *color_state,
                          GdkColorState *default_value)
{
  if (gdk_color_state_equal (color_state, default_value))
    return;

  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  print_color_state (p, color_state);
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static cairo_status_t
cairo_write_array (void                *closure,
                   const unsigned char *data,
                   unsigned int         length)
{
  g_byte_array_append (closure, data, length);

  return CAIRO_STATUS_SUCCESS;
}

static void
cairo_destroy_array (gpointer array)
{
  g_byte_array_free (array, TRUE);
}

static void
append_escaping_newlines (GString    *str,
                          const char *string)
{
  gsize len;

  do {
    len = strcspn (string, "\n");
    g_string_append_len (str, string, len);
    string += len;
    if (*string)
      {
        g_string_append (str, "\\\n");
        string++;
      }
  } while (*string);
}

/* like g_base64 encode, but breaks lines
 * in CSS-compatible way
 */

static char *
base64_encode_with_linebreaks (const guchar *data,
                               gsize         len)
{
  gsize max;
  char *out;
  int state = 0;
  int save = 0;
  gsize outlen;

  g_return_val_if_fail (data != NULL || len == 0, NULL);

  /* We can use a smaller limit here, since we know the saved state is 0,
     +1 is needed for trailing \0, also check for unlikely integer overflow */

  g_return_val_if_fail (len < ((G_MAXSIZE - 1) / 4 - 1) * 3, NULL);

  /* The glib docs say:
   *
   * The output buffer must be large enough to fit all the data that will
   * be written to it. Due to the way base64 encodes you will need
   * at least: (@len / 3 + 1) * 4 + 4 bytes (+ 4 may be needed in case of
   * non-zero state). If you enable line-breaking you will need at least:
   * ((@len / 3 + 1) * 4 + 4) / 76 + 1 bytes of extra space.
   */

  max = (len / 3 + 1) * 4;
  max += ((len / 3 + 1) * 4 + 4) / 76 + 1;
  /* and the null byte */
  max += 1;

  out = g_malloc (max);

  outlen = g_base64_encode_step (data, len, TRUE, out, &state, &save);
  outlen += g_base64_encode_close (TRUE, out + outlen, &state, &save);
  out[outlen] = '\0';

  return out;
}

static void
append_bytes_url (Printer    *p,
                  GBytes     *bytes,
                  const char *mime_type)
{
  char *b64;

  g_string_append_printf (p->str, "url(\"data:%s;base64,\\\n", mime_type ? mime_type : "");
  b64 = base64_encode_with_linebreaks (g_bytes_get_data (bytes, NULL),
                                       g_bytes_get_size (bytes));
  append_escaping_newlines (p->str, b64);
  g_free (b64);
  g_string_append (p->str, "\")");
}

static void
append_bytes_param (Printer    *p,
                    const char *param_name,
                    GBytes     *bytes,
                    const char *mime_type)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  append_bytes_url (p, bytes, mime_type);
  g_string_append (p->str, ";\n");
}

static char *
bytes_to_hex_string (GBytes *bytes)
{
  const char *values = "0123456789ABCDEF";
  char *s;
  gsize i, size;
  const guchar *data;

  data = g_bytes_get_data (bytes, &size);
  g_assert (size < G_MAXSIZE / 2 - 1);
  s = g_new (char2 * size + 1);

  for (i = 0; i < size; i++)
    {
      s[2 * i + 0] = values[data[i] / 16];
      s[2 * i + 1] = values[data[i] % 16];
    }

  s[2 * size] = 0;

  return s;
}

static void
append_compressed_bytes_param (Printer    *p,
                               const char *param_name,
                               GBytes     *bytes)
{
  GZlibCompressor *compressor;
  GBytes *compressed_bytes;

  compressor = g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP, 9);
#if GLIB_CHECK_VERSION (2850)
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
  g_zlib_compressor_set_os (compressor, 3);
G_GNUC_END_IGNORE_DEPRECATIONS
#endif

  compressed_bytes = g_converter_convert_bytes (G_CONVERTER (compressor), bytes, NULL);
  g_assert (compressed_bytes != NULL);

#if !GLIB_CHECK_VERSION (2850)
  /* fallback for above */
  ((guchar *) g_bytes_get_data (compressed_bytes, NULL))[9] = 3;
#endif

  /* rough estimates for the length of each string */
  if (2 * g_bytes_get_size (bytes) <= 4 * g_bytes_get_size (compressed_bytes) / 3 + 25)
    {
      char *bytes_str = bytes_to_hex_string (bytes);
      append_string_param (p, param_name, bytes_str);
      g_free (bytes_str);
    }
  else
    {
      append_bytes_param (p, param_name, compressed_bytes, "application/gzip");
    }

  g_object_unref (compressor);
  g_bytes_unref (compressed_bytes);
}

static void
append_memory_texture (Printer    *p,
                       GdkTexture *texture)
{
  const GdkMemoryLayout *layout;
  gsize i;

  layout = gdk_memory_texture_get_layout (GDK_MEMORY_TEXTURE (texture));

  g_string_append_printf (p->str, "memory {\n");
  p->indentation_level ++;

  append_memory_format_param (p, "format", layout->format);
  append_unsigned_param (p, "width", layout->width);
  append_unsigned_param (p, "height", layout->height);

  if (gdk_memory_format_get_n_planes (layout->format) > 0 ||
      layout->planes[0].offset != 0)
    {
      _indent (p);
      g_string_append (p->str, "offset: ");
      for (i = 0; i < gdk_memory_format_get_n_planes (layout->format); i++)
        {
          if (i > 0)
            g_string_append_printf (p->str, ", ");
          g_string_append_printf (p->str, "%zu", layout->planes[i].offset);
        }
      g_string_append (p->str, ";\n");
    }
  _indent (p);
  g_string_append (p->str, "stride: ");
  for (i = 0; i < gdk_memory_format_get_n_planes (layout->format); i++)
    {
      if (i > 0)
        g_string_append_printf (p->str, ", ");
      g_string_append_printf (p->str, "%zu", layout->planes[i].stride);
    }
  g_string_append (p->str, ";\n");

  append_color_state_param (p, "color-state", gdk_texture_get_color_state (texture), GDK_COLOR_STATE_SRGB);
  append_compressed_bytes_param (p, "data", gdk_memory_texture_get_bytes (GDK_MEMORY_TEXTURE (texture)));

  p->indentation_level --;
  _indent (p);
  g_string_append_printf (p->str, "}\n");
}

static void
append_dmabuf_texture (Printer    *p,
                       GdkTexture *texture)
{
  GdkMemoryLayout layout;
  GBytes *bytes;
  gsize i, n_planes;
  char fourcc_str[5];
  GdkColorState *default_color_state;
  const GdkDmabuf *dmabuf;

  dmabuf = gdk_dmabuf_texture_get_dmabuf (GDK_DMABUF_TEXTURE (texture));

  gdk_memory_layout_init (&layout,
                          gdk_texture_get_format (texture),
                          gdk_texture_get_width (texture),
                          gdk_texture_get_height (texture),
                          1);

  bytes = gdk_texture_download_bytes (texture, &layout);

  n_planes = gdk_memory_format_get_n_planes (layout.format);
  default_color_state = GDK_COLOR_STATE_SRGB;

  g_string_append_printf (p->str, "dmabuf {\n");
  p->indentation_level ++;

  if (dmabuf->fourcc == gdk_memory_format_get_dmabuf_rgb_fourcc (layout.format))
    default_color_state = GDK_COLOR_STATE_SRGB;
  else if (dmabuf->fourcc == gdk_memory_format_get_dmabuf_yuv_fourcc (layout.format))
    default_color_state = GDK_COLOR_STATE_YUV;
  else
    {
      g_assert_not_reached ();
    }

  fourcc_str[0] = dmabuf->fourcc >> 0;
  fourcc_str[1] = dmabuf->fourcc >> 8;
  fourcc_str[2] = dmabuf->fourcc >> 16;
  fourcc_str[3] = dmabuf->fourcc >> 24;
  fourcc_str[4] = 0;
  if (g_ascii_isalnum (fourcc_str[0]) &&
      g_ascii_isalnum (fourcc_str[1]) &&
      g_ascii_isalnum (fourcc_str[2]) &&
      g_ascii_isalnum (fourcc_str[3]))
    {
      append_string_param (p, "fourcc", fourcc_str);
    }
  else
    {
      append_unsigned_param (p, "fourcc", dmabuf->fourcc);
    }
  if (dmabuf->modifier != DRM_FORMAT_MOD_LINEAR)
    {
      _indent (p);
      g_string_append_printf (p->str, "/* modifier: %llu (0x%0llX) */\n",
                              (unsigned long long) dmabuf->modifier, (unsigned long long) dmabuf->modifier);
    }
  append_unsigned_param (p, "width", layout.width);
  append_unsigned_param (p, "height", layout.height);
  if (gdk_memory_format_alpha (gdk_texture_get_format (texture)) == GDK_MEMORY_ALPHA_STRAIGHT)
    append_boolean_param (p, "premultiplied"FALSE);

  if (n_planes > 0 || layout.planes[0].offset != 0)
    {
      _indent (p);
      g_string_append (p->str, "offset: ");
      for (i = 0; i < n_planes; i++)
        {
          if (i > 0)
            g_string_append_printf (p->str, ", ");
          g_string_append_printf (p->str, "%zu", layout.planes[i].offset);
        }
      g_string_append (p->str, ";\n");
    }
  for (i = 0; i < n_planes; i++)
    {
      _indent (p);
      g_string_append (p->str, "stride: ");
      for (i = 0; i < n_planes; i++)
        {
          if (i > 0)
            g_string_append_printf (p->str, ", ");
          g_string_append_printf (p->str, "%zu", layout.planes[i].stride);
        }
      g_string_append (p->str, ";\n");
    }

  append_color_state_param (p, "color-state", gdk_texture_get_color_state (texture), default_color_state);
  append_compressed_bytes_param (p, "data", bytes);

  p->indentation_level --;
  _indent (p);
  g_string_append_printf (p->str, "}\n");

  g_bytes_unref (bytes);
}

#ifdef GDK_WINDOWING_WIN32
static void
append_d3d12_texture (Printer    *p,
                      GdkTexture *texture)
{
  GdkMemoryLayout layout;
  GBytes *bytes;
  D3D12_RESOURCE_DESC desc;

  gdk_memory_layout_init (&layout,
                          gdk_texture_get_format (texture),
                          gdk_texture_get_width (texture),
                          gdk_texture_get_height (texture),
                          gdk_memory_format_alignment (gdk_texture_get_format (texture)));

  bytes = gdk_texture_download_bytes (texture, &layout);

  g_string_append_printf (p->str, "d3d12 {\n");
  p->indentation_level ++;

  ID3D12Resource_GetDesc (gdk_d3d12_texture_get_resource (GDK_D3D12_TEXTURE (texture)), &desc);
  append_unsigned_param (p, "format", desc.Format);
  append_unsigned_param (p, "width", layout.width);
  append_unsigned_param (p, "height", layout.height);
  if (gdk_memory_format_alpha (gdk_texture_get_format (texture)) == GDK_MEMORY_ALPHA_STRAIGHT)
    append_boolean_param (p, "premultiplied"FALSE);

  append_color_state_param (p, "color-state", gdk_texture_get_color_state (texture), GDK_COLOR_STATE_SRGB);
  append_compressed_bytes_param (p, "data", bytes);

  p->indentation_level --;
  _indent (p);
  g_string_append_printf (p->str, "}\n");

  g_bytes_unref (bytes);
}
#endif

static void
append_texture_param (Printer    *p,
                      const char *param_name,
                      GdkTexture *texture)
{
  GBytes *bytes;
  const char *texture_name;

  _indent (p);

  g_string_append_printf (p->str, "%s: ", param_name);

  texture_name = g_hash_table_lookup (p->named_textures, texture);
  if (texture_name == NULL)
    {
      /* nothing to do here, texture is unique */
    }
  else if (texture_name[0])
    {
      /* texture has been named already */
      gtk_css_print_string (p->str, texture_name, TRUE);
      g_string_append (p->str, ";\n");
      return;
    }
  else
    {
      /* texture needs a name */
      char *new_name = g_strdup_printf ("texture%zu", ++p->named_texture_counter);
      gtk_css_print_string (p->str, new_name, TRUE);
      g_string_append_c (p->str, ' ');
      g_hash_table_insert (p->named_textures, texture, new_name);
    }

  if (GDK_IS_MEMORY_TEXTURE (texture))
    {
      append_memory_texture (p, texture);
    }
  else if (GDK_IS_DMABUF_TEXTURE (texture))
    {
      append_dmabuf_texture (p, texture);
    }
#ifdef GDK_WINDOWING_WIN32
  else if (GDK_IS_D3D12_TEXTURE (texture))
    {
      append_d3d12_texture (p, texture);
    }
#endif
  else
    {
      switch (gdk_texture_get_depth (texture))
        {
        case GDK_MEMORY_U8:
          bytes = gdk_texture_save_to_png_bytes (texture);
          append_bytes_url (p, bytes, "image/png");
          g_bytes_unref (bytes);
          g_string_append (p->str, ";\n");
          break;

        case GDK_MEMORY_FLOAT16:
        case GDK_MEMORY_FLOAT32:
          bytes = gdk_texture_save_to_tiff_bytes (texture);
          append_bytes_url (p, bytes, "image/tiff");
          g_bytes_unref (bytes);
          g_string_append (p->str, ";\n");
          break;

        case GDK_MEMORY_NONE:
        case GDK_N_DEPTHS:
        default:
          g_assert_not_reached ();
        }
    }
}

static void
gsk_text_node_serialize_font (GskRenderNode *node,
                              Printer       *p)
{
  PangoFont *font = gsk_text_node_get_font (node);
  PangoFontDescription *desc;
  char *s;
  FontInfo lookup;
  FontInfo *info;
  hb_face_t *face;
  hb_blob_t *blob;
  const char *data;
  guint length;
  GBytes *bytes;

  desc = pango_font_describe_with_absolute_size (font);
  s = pango_font_description_to_string (desc);
  g_string_append_printf (p->str, "\"%s\"", s);
  g_free (s);
  pango_font_description_free (desc);

  lookup.face = hb_font_get_face (pango_font_get_hb_font (font));
  info = g_hash_table_lookup (p->fonts, &lookup);
  if (info->serialized)
    return;

  if (info->input)
    face = hb_subset_or_fail (info->face, info->input);
  else
    face = hb_face_reference (info->face);

  if (face == NULL)
    return;

  blob = hb_face_reference_blob (face);
  data = hb_blob_get_data (blob, &length);
  bytes = g_bytes_new_static (data, length);

  g_string_append (p->str, " ");
  append_bytes_url (p, bytes, "font/ttf");

  g_bytes_unref (bytes);
  hb_blob_destroy (blob);
  hb_face_destroy (face);

  info->serialized = TRUE;
}

static void
gsk_text_node_serialize_font_options (GskRenderNode *node,
                                      Printer       *p)
{
  PangoFont *font = gsk_text_node_get_font (node);
  cairo_scaled_font_t *sf = pango_cairo_font_get_scaled_font (PANGO_CAIRO_FONT (font));
  cairo_font_options_t *options;
  cairo_hint_style_t hint_style;
  cairo_antialias_t antialias;
  cairo_hint_metrics_t hint_metrics;

  options = cairo_font_options_create ();
  cairo_scaled_font_get_font_options (sf, options);
  hint_style = cairo_font_options_get_hint_style (options);
  antialias = cairo_font_options_get_antialias (options);
  hint_metrics = cairo_font_options_get_hint_metrics (options);
  cairo_font_options_destroy (options);

  /* medium and full are identical in the absence of subpixel modes */
  if (hint_style == CAIRO_HINT_STYLE_MEDIUM)
    hint_style = CAIRO_HINT_STYLE_FULL;

  if (hint_style == CAIRO_HINT_STYLE_NONE ||
      hint_style == CAIRO_HINT_STYLE_FULL)
    append_enum_param (p, "hint-style", CAIRO_GOBJECT_TYPE_HINT_STYLE, hint_style);

  /* CAIRO_ANTIALIAS_NONE is the only value we ever emit here, since gray is the default,
   * and we don't accept any other values.
   */

  if (antialias == CAIRO_ANTIALIAS_NONE)
    append_enum_param (p, "antialias", CAIRO_GOBJECT_TYPE_ANTIALIAS, antialias);

  /* CAIRO_HINT_METRICS_ON is the only value we ever emit here, since off is the default,
   * and we don't accept any other values.
   */

  if (hint_metrics == CAIRO_HINT_METRICS_ON)
    append_enum_param (p, "hint-metrics", CAIRO_GOBJECT_TYPE_HINT_METRICS, CAIRO_HINT_METRICS_ON);
}

void
gsk_text_node_serialize_glyphs (GskRenderNode *node,
                                GString       *p)
{
  const guint n_glyphs = gsk_text_node_get_num_glyphs (node);
  const PangoGlyphInfo *glyphs = gsk_text_node_get_glyphs (node, NULL);
  PangoFont *font = gsk_text_node_get_font (node);
  GString *str;
  guint i, j;
  PangoGlyphString *ascii;

  ascii = create_ascii_glyphs (font);
  str = g_string_new ("");

  for (i = 0; i < n_glyphs; i++)
    {
      if (ascii)
        {
          for (j = 0; j < ascii->num_glyphs; j++)
            {
              if (glyphs[i].glyph == ascii->glyphs[j].glyph &&
                  glyphs[i].geometry.width == ascii->glyphs[j].geometry.width &&
                  glyphs[i].geometry.x_offset == 0 &&
                  glyphs[i].geometry.y_offset == 0 &&
                  glyphs[i].attr.is_cluster_start &&
                  !glyphs[i].attr.is_color)
                {
                  switch (j + MIN_ASCII_GLYPH)
                    {
                      case '\\':
                        g_string_append (str, "\\\\");
                        break;
                      case '"':
                        g_string_append (str, "\\\"");
                        break;
                      default:
                        g_string_append_c (str, j + MIN_ASCII_GLYPH);
                        break;
                    }
                  break;
                }
            }
          if (j != ascii->num_glyphs)
            continue;
        }

      if (str->len)
        {
          g_string_append_printf (p, "\"%s\", ", str->str);
          g_string_set_size (str, 0);
        }

      g_string_append_printf (p, "%u ", glyphs[i].glyph);
      string_append_double (p, (double) glyphs[i].geometry.width / PANGO_SCALE);
      if (!glyphs[i].attr.is_cluster_start ||
          glyphs[i].attr.is_color ||
          glyphs[i].geometry.x_offset != 0 ||
          glyphs[i].geometry.y_offset != 0)
        {
          g_string_append (p, " ");
          string_append_double (p, (double) glyphs[i].geometry.x_offset / (double) PANGO_SCALE);
          g_string_append (p, " ");
          string_append_double (p, (double) glyphs[i].geometry.y_offset / (double) PANGO_SCALE);
          if (!glyphs[i].attr.is_cluster_start)
            g_string_append (p, " same-cluster");
          if (glyphs[i].attr.is_color)
            g_string_append (p, " color");
        }

      if (i + 1 < n_glyphs)
        g_string_append (p, ", ");
    }

  if (str->len)
    g_string_append_printf (p, "\"%s\"", str->str);

  g_string_free (str, TRUE);
  if (ascii)
    pango_glyph_string_free (ascii);
}

static void
append_path_param (Printer    *p,
                   const char *param_name,
                   GskPath    *path)
{
  char *str, *s;

  _indent (p);
  g_string_append (p->str, "path: \"\\\n");
  str = gsk_path_to_string (path);
  /* Put each command on a new line */
  for (s = str; *s; s++)
    {
      if (*s == ' ' &&
          (s[1] == 'M' || s[1] == 'C' || s[1] == 'Z' || s[1] == 'L'))
        *s = '\n';
    }
  append_escaping_newlines (p->str, str);
  g_string_append (p->str, "\";\n");
  g_free (str);
}

static void
append_dash_param (Printer     *p,
                   const char  *param_name,
                   const float *dash,
                   gsize        n_dash)
{
  _indent (p);
  g_string_append (p->str, "dash: ");

  if (n_dash == 0)
    {
      g_string_append (p->str, "none");
    }
  else
    {
      gsize i;

      string_append_double (p->str, dash[0]);
      for (i = 1; i < n_dash; i++)
        {
          g_string_append_c (p->str, ' ');
          string_append_double (p->str, dash[i]);
        }
    }

  g_string_append (p->str, ";\n");
}

static const char *
hue_interpolation_to_string (GskHueInterpolation value)
{
  const char *name[] = { "shorter""longer""increasing""decreasing" };

  return name[value];
}

static void
append_hue_interpolation_param (Printer             *p,
                                const char          *param_name,
                                GskHueInterpolation  value,
                                GskHueInterpolation  default_value)
{
  if (value == default_value)
    return;

  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  g_string_append (p->str, hue_interpolation_to_string (value));
  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_component_transfer_param (Printer                    *p,
                                 const char                 *param_name,
                                 const GskComponentTransfer *transfer)
{
  if (transfer->kind == GSK_COMPONENT_TRANSFER_IDENTITY)
    return;

  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  gsk_component_transfer_print (transfer, p->str);
  g_string_append (p->str, ";\n");
}

static void
append_isolation_param (Printer      *p,
                        const char   *param_name,
                        GskIsolation  isolation)
{
  _indent (p);
  g_string_append_printf (p->str, "%s:", param_name);

  if (isolation == GSK_ISOLATION_NONE)
    {
      g_string_append (p->str, " none");
    }
  else if (isolation == GSK_ISOLATION_ALL)
    {
      g_string_append (p->str, " all");
    }
  else
    {
      gsize i;

      if (gtk_popcount (GSK_ISOLATION_ALL & ~isolation) < gtk_popcount (isolation))
        {
          g_string_append (p->str, " not");
          isolation = GSK_ISOLATION_ALL & ~isolation;
        }

      for (i = 0; i < G_N_ELEMENTS (isolation_flags); i++)
        {
          if (isolation & isolation_flags[i].value)
            {
              g_string_append_c (p->str, ' ');
              g_string_append (p->str, isolation_flags[i].name);
            }
        }
    }

  g_string_append_c (p->str, ';');
  g_string_append_c (p->str, '\n');
}

static void
append_two_float_param (Printer    *p,
                        const char *param_name,
                        float       value1,
                        float       value2)
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  string_append_double (p->str, value1);
  if (value1 != value2)
    {
      g_string_append_c (p->str, ' ');
      string_append_double (p->str, value2);
    }
  g_string_append (p->str, ";\n");
}

static void
append_four_float_param (Printer     *p,
                         const char  *param_name,
                         const float  values[4])
{
  _indent (p);
  g_string_append_printf (p->str, "%s: ", param_name);
  string_append_double (p->str, values[0]);
  g_string_append_c (p->str, ' ');
  string_append_double (p->str, values[1]);
  g_string_append_c (p->str, ' ');
  string_append_double (p->str, values[2]);
  g_string_append_c (p->str, ' ');
  string_append_double (p->str, values[3]);
  g_string_append (p->str, ";\n");
}

static void
append_channels_param (Printer         *p,
                       const char      *param_name,
                       GdkColorChannel  channel1,
                       GdkColorChannel  channel2)
{
  _indent (p);
  g_string_append_printf (p->str,
                          "%s: %s %s;\n",
                          param_name,
                          channel_names[channel1],
                          channel_names[channel2]);
}

static void
render_node_print (Printer       *p,
                   GskRenderNode *node)
{
  const char *node_name;

  node_name = g_hash_table_lookup (p->named_nodes, node);
  if (node_name == NULL)
    {
      /* nothing to do here, node is unique */
    }
  else if (node_name[0])
    {
      /* node has been named already */
      gtk_css_print_string (p->str, node_name, TRUE);
      g_string_append (p->str, ";\n");
      return;
    }
  else
    {
      /* node needs a name */
      char *new_name = g_strdup_printf ("node%zu", ++p->named_node_counter);
      g_hash_table_insert (p->named_nodes, node, new_name);
      node_name = new_name;
    }

  switch (gsk_render_node_get_node_type (node))
    {
    case GSK_CONTAINER_NODE:
      {
        guint i;

        start_node (p, "container", node_name);
        for (i = 0; i < gsk_container_node_get_n_children (node); i ++)
          {
            GskRenderNode *child = gsk_container_node_get_child (node, i);

            /* Only in container nodes do we want nodes to be indented. */
            _indent (p);
            render_node_print (p, child);
          }
        end_node (p);
      }
      break;

    case GSK_COLOR_NODE:
      {
        start_node (p, "color", node_name);
        append_rect_param (p, "bounds", &node->bounds);
        append_color_param (p, "color", gsk_color_node_get_gdk_color (node));
        append_snap_param (p, "snap", gsk_color_node_get_snap (node));
        end_node (p);
      }
      break;

    case GSK_CROSS_FADE_NODE:
      {
        start_node (p, "cross-fade", node_name);

        append_float_param (p, "progress", gsk_cross_fade_node_get_progress (node), 0.5f);
        append_node_param (p, "start", gsk_cross_fade_node_get_start_child (node));
        append_node_param (p, "end", gsk_cross_fade_node_get_end_child (node));

        end_node (p);
      }
      break;

    case GSK_REPEATING_LINEAR_GRADIENT_NODE:
    case GSK_LINEAR_GRADIENT_NODE:
      {
        const GskGradient *gradient;

        start_node (p, "linear-gradient", node_name);

        gradient = gsk_gradient_node_get_gradient (node);
        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_linear_gradient_node_get_snap (node));
        append_point_param (p, "start", gsk_linear_gradient_node_get_start (node));
        append_point_param (p, "end", gsk_linear_gradient_node_get_end (node));
        append_stops_param (p, "stops", gradient);

        if (gsk_gradient_get_repeat (gradient) != GSK_REPEAT_PAD)
          append_repeat_param (p, "repeat", gsk_gradient_get_repeat (gradient));

        append_color_state_param (p, "interpolation",
                                  gsk_gradient_get_interpolation (gradient),
                                  GDK_COLOR_STATE_SRGB);
        append_hue_interpolation_param (p, "hue-interpolation",
                                        gsk_gradient_get_hue_interpolation (gradient),
                                        GSK_HUE_INTERPOLATION_SHORTER);
        if (!gsk_gradient_get_premultiplied (gradient))
          append_boolean_param (p, "premultiplied"FALSE);

        end_node (p);
      }
      break;

    case GSK_REPEATING_RADIAL_GRADIENT_NODE:
    case GSK_RADIAL_GRADIENT_NODE:
      {
        const GskGradient *gradient;

        start_node (p, "radial-gradient", node_name);

        gradient = gsk_gradient_node_get_gradient (node);

        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_radial_gradient_node_get_snap (node));
        append_circle_param (p, "start",
                             gsk_radial_gradient_node_get_start_center (node),
                             gsk_radial_gradient_node_get_start_radius (node));
        append_circle_param (p, "end",
                             gsk_radial_gradient_node_get_end_center (node),
                             gsk_radial_gradient_node_get_end_radius (node));
        append_float_param (p, "aspect-ratio", gsk_radial_gradient_node_get_aspect_ratio (node), 1);

        append_stops_param (p, "stops", gradient);

        if (gsk_gradient_get_repeat (gradient) != GSK_REPEAT_PAD)
          append_repeat_param (p, "repeat", gsk_gradient_get_repeat (gradient));

        append_color_state_param (p, "interpolation",
                                  gsk_gradient_get_interpolation (gradient),
                                  GDK_COLOR_STATE_SRGB);
        append_hue_interpolation_param (p, "hue-interpolation",
                                        gsk_gradient_get_hue_interpolation (gradient),
                                        GSK_HUE_INTERPOLATION_SHORTER);
        if (!gsk_gradient_get_premultiplied (gradient))
          append_boolean_param (p, "premultiplied"FALSE);

        end_node (p);
      }
      break;

    case GSK_CONIC_GRADIENT_NODE:
      {
        const GskGradient *gradient;

        start_node (p, "conic-gradient", node_name);

        gradient = gsk_gradient_node_get_gradient (node);
        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_conic_gradient_node_get_snap (node));
        append_point_param (p, "center", gsk_conic_gradient_node_get_center (node));
        append_float_param (p, "rotation", gsk_conic_gradient_node_get_rotation (node), 0.0f);

        append_stops_param (p, "stops", gradient);

        append_color_state_param (p, "interpolation",
                                  gsk_gradient_get_interpolation (gradient),
                                  GDK_COLOR_STATE_SRGB);
        append_hue_interpolation_param (p, "hue-interpolation",
                                        gsk_gradient_get_hue_interpolation (gradient),
                                        GSK_HUE_INTERPOLATION_SHORTER);
        if (!gsk_gradient_get_premultiplied (gradient))
          append_boolean_param (p, "premultiplied"FALSE);

        end_node (p);
      }
      break;

    case GSK_OPACITY_NODE:
      {
        start_node (p, "opacity", node_name);

        append_float_param (p, "opacity", gsk_opacity_node_get_opacity (node), 0.5f);
        append_node_param (p, "child", gsk_opacity_node_get_child (node));

        end_node (p);
      }
      break;

    case GSK_OUTSET_SHADOW_NODE:
      {
        start_node (p, "outset-shadow", node_name);

        append_float_param (p, "blur", gsk_outset_shadow_node_get_blur_radius (node), 0.0f);
        if (!gdk_color_equal (gsk_outset_shadow_node_get_gdk_color (node), &GDK_COLOR_SRGB (0001)))
          append_color_param (p, "color", gsk_outset_shadow_node_get_gdk_color (node));
        append_float_param (p, "dx", gsk_outset_shadow_node_get_dx (node), 1.0f);
        append_float_param (p, "dy", gsk_outset_shadow_node_get_dy (node), 1.0f);
        append_rounded_rect_param (p, "outline", gsk_outset_shadow_node_get_outline (node));
        append_snap_param (p, "snap", gsk_outset_shadow_node_get_snap (node));
        append_float_param (p, "spread", gsk_outset_shadow_node_get_spread (node), 0.0f);

        end_node (p);
      }
      break;

    case GSK_CLIP_NODE:
      {
        start_node (p, "clip", node_name);

        append_rect_param (p, "clip", gsk_clip_node_get_clip (node));
        append_node_param (p, "child", gsk_clip_node_get_child (node));
        append_snap_param (p, "snap", gsk_clip_node_get_snap (node));

        end_node (p);
      }
      break;

    case GSK_ROUNDED_CLIP_NODE:
      {
        start_node (p, "rounded-clip", node_name);

        append_rounded_rect_param (p, "clip", gsk_rounded_clip_node_get_clip (node));
        append_node_param (p, "child", gsk_rounded_clip_node_get_child (node));
        append_snap_param (p, "snap", gsk_rounded_clip_node_get_snap (node));

        end_node (p);
      }
      break;

    case GSK_FILL_NODE:
      {
        start_node (p, "fill", node_name);

        append_node_param (p, "child", gsk_fill_node_get_child (node));
        append_path_param (p, "path", gsk_fill_node_get_path (node));
        append_enum_param (p, "fill-rule", GSK_TYPE_FILL_RULE, gsk_fill_node_get_fill_rule (node));

        end_node (p);
      }
      break;

    case GSK_STROKE_NODE:
      {
        const GskStroke *stroke;
        const float *dash;
        gsize n_dash;

        start_node (p, "stroke", node_name);

        append_node_param (p, "child", gsk_stroke_node_get_child (node));
        append_path_param (p, "path", gsk_stroke_node_get_path (node));

        stroke = gsk_stroke_node_get_stroke (node);
        append_float_param (p, "line-width", gsk_stroke_get_line_width (stroke), 0.0f);
        append_enum_param (p, "line-cap", GSK_TYPE_LINE_CAP, gsk_stroke_get_line_cap (stroke));
        append_enum_param (p, "line-join", GSK_TYPE_LINE_JOIN, gsk_stroke_get_line_join (stroke));
        append_float_param (p, "miter-limit", gsk_stroke_get_miter_limit (stroke), 4.0f);
        dash = gsk_stroke_get_dash (stroke, &n_dash);
        if (dash)
          append_dash_param (p, "dash", dash, n_dash);
        append_float_param (p, "dash-offset", gsk_stroke_get_dash_offset (stroke), 0.0f);

        end_node (p);
      }
      break;

    case GSK_TRANSFORM_NODE:
      {
        GskTransform *transform = gsk_transform_node_get_transform (node);
        start_node (p, "transform", node_name);

        if (gsk_transform_get_category (transform) != GSK_TRANSFORM_CATEGORY_IDENTITY)
          append_transform_param (p, "transform", transform);
        append_node_param (p, "child", gsk_transform_node_get_child (node));

        end_node (p);
      }
      break;

    case GSK_COLOR_MATRIX_NODE:
      {
        start_node (p, "color-matrix", node_name);

        if (!graphene_matrix_is_identity (gsk_color_matrix_node_get_color_matrix (node)))
          append_matrix_param (p, "matrix", gsk_color_matrix_node_get_color_matrix (node));
        if (!graphene_vec4_equal (gsk_color_matrix_node_get_color_offset (node), graphene_vec4_zero ()))
          append_vec4_param (p, "offset", gsk_color_matrix_node_get_color_offset (node));
        append_node_param (p, "child", gsk_color_matrix_node_get_child (node));
        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_color_matrix_node_get_snap (node));
        append_color_state_param (p, "color-state", gsk_color_matrix_node_get_color_state (node), GDK_COLOR_STATE_SRGB);

        end_node (p);
      }
      break;

    case GSK_BORDER_NODE:
      {
        const GdkColor *colors = gsk_border_node_get_gdk_colors (node);
        const float *widths = gsk_border_node_get_widths (node);
        guint i, n;
        start_node (p, "border", node_name);

        if (!gdk_color_equal (&colors[3], &colors[1]))
          n = 4;
        else if (!gdk_color_equal (&colors[2], &colors[0]))
          n = 3;
        else if (!gdk_color_equal (&colors[1], &colors[0]))
          n = 2;
        else if (!gdk_color_equal (&colors[0], (&(GdkColor) { .color_state = GDK_COLOR_STATE_SRGB, .values = { 0001 } })))
          n = 1;
        else
          n = 0;

        if (n > 0)
          {
            _indent (p);
            g_string_append (p->str, "colors: ");
            for (i = 0; i < n; i++)
              {
                if (i > 0)
                  g_string_append_c (p->str, ' ');
                print_color (p, &colors[i]);
              }
            g_string_append (p->str, ";\n");
          }

        append_rounded_rect_param (p, "outline", gsk_border_node_get_outline (node));
        append_snap_param (p, "snap", gsk_border_node_get_snap (node));

        if (widths[3] != widths[1])
          n = 4;
        else if (widths[2] != widths[0])
          n = 3;
        else if (widths[1] != widths[0])
          n = 2;
        else if (widths[0] != 1.0)
          n = 1;
        else
          n = 0;

        if (n > 0)
          {
            _indent (p);
            g_string_append (p->str, "widths: ");
            for (i = 0; i < n; i++)
              {
                if (i > 0)
                  g_string_append_c (p->str, ' ');
                string_append_double (p->str, widths[i]);
              }
            g_string_append (p->str, ";\n");
          }
        append_snap_param (p, "border-snap", gsk_border_node_get_snap (node));

        end_node (p);
      }
      break;

    case GSK_SHADOW_NODE:
      {
        const guint n_shadows = gsk_shadow_node_get_n_shadows (node);
        int i;

        start_node (p, "shadow", node_name);

        _indent (p);
        g_string_append (p->str, "shadows: ");
        for (i = 0; i < n_shadows; i ++)
          {
            const GskShadowEntry *s = gsk_shadow_node_get_shadow_entry (node, i);

            if (i > 0)
              g_string_append (p->str, ", ");

            print_color (p, &s->color);
            g_string_append_c (p->str, ' ');
            string_append_double (p->str, s->offset.x);
            g_string_append_c (p->str, ' ');
            string_append_double (p->str, s->offset.y);
            if (s->radius > 0)
              {
                g_string_append_c (p->str, ' ');
                string_append_double (p->str, s->radius);
              }
          }

        g_string_append_c (p->str, ';');
        g_string_append_c (p->str, '\n');
        append_node_param (p, "child", gsk_shadow_node_get_child (node));

        end_node (p);
      }
      break;

    case GSK_INSET_SHADOW_NODE:
      {
        start_node (p, "inset-shadow", node_name);

        append_float_param (p, "blur", gsk_inset_shadow_node_get_blur_radius (node), 0.0f);
        if (!gdk_color_equal (gsk_inset_shadow_node_get_gdk_color (node), &GDK_COLOR_SRGB (0001)))
          append_color_param (p, "color", gsk_inset_shadow_node_get_gdk_color (node));
        append_float_param (p, "dx", gsk_inset_shadow_node_get_dx (node), 1.0f);
        append_float_param (p, "dy", gsk_inset_shadow_node_get_dy (node), 1.0f);
        append_rounded_rect_param (p, "outline", gsk_inset_shadow_node_get_outline (node));
        append_snap_param (p, "snap", gsk_inset_shadow_node_get_snap (node));
        append_float_param (p, "spread", gsk_inset_shadow_node_get_spread (node), 0.0f);

        end_node (p);
      }
      break;

    case GSK_TEXTURE_NODE:
      {
        start_node (p, "texture", node_name);

        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_texture_node_get_snap (node));
        append_texture_param (p, "texture", gsk_texture_node_get_texture (node));

        end_node (p);
      }
      break;

    case GSK_TEXTURE_SCALE_NODE:
      {
        GskScalingFilter filter = gsk_texture_scale_node_get_filter (node);

        start_node (p, "texture-scale", node_name);
        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_texture_scale_node_get_snap (node));

        if (filter != GSK_SCALING_FILTER_LINEAR)
          {
            _indent (p);
            for (unsigned int i = 0; i < G_N_ELEMENTS (scaling_filters); i++)
              {
                if (scaling_filters[i].filter == filter)
                  {
                    g_string_append_printf (p->str, "filter: %s;\n", scaling_filters[i].name);
                    break;
                  }
              }
          }

        append_texture_param (p, "texture", gsk_texture_scale_node_get_texture (node));
        end_node (p);
      }
      break;

    case GSK_TEXT_NODE:
      {
        const graphene_point_t *offset = gsk_text_node_get_offset (node);
        const GdkColor *color = gsk_text_node_get_gdk_color (node);

        start_node (p, "text", node_name);

        if (!gdk_color_equal (color, &GDK_COLOR_SRGB (0001)))
          append_color_param (p, "color", color);

        _indent (p);
        g_string_append (p->str, "font: ");
        gsk_text_node_serialize_font (node, p);
        g_string_append (p->str, ";\n");

        _indent (p);
        g_string_append (p->str, "glyphs: ");
        gsk_text_node_serialize_glyphs (node, p->str);
        g_string_append (p->str, ";\n");

        if (!graphene_point_equal (offset, graphene_point_zero ()))
          append_point_param (p, "offset", offset);

        gsk_text_node_serialize_font_options (node, p);

        end_node (p);
      }
      break;

    case GSK_DEBUG_NODE:
      {
        const char *message = gsk_debug_node_get_message (node);

        start_node (p, "debug", node_name);

        /* TODO: We potentially need to escape certain characters in the message */
        if (message)
          append_string_param (p, "message", message);
        append_node_param (p, "child", gsk_debug_node_get_child (node));

        end_node (p);
      }
      break;

    case GSK_BLUR_NODE:
      {
        start_node (p, "blur", node_name);

        append_float_param (p, "blur", gsk_blur_node_get_radius (node), 1.0f);
        append_node_param (p, "child", gsk_blur_node_get_child (node));

        end_node (p);
      }
      break;

    case GSK_GL_SHADER_NODE:
      {
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        GskGLShader *shader = gsk_gl_shader_node_get_shader (node);
        GBytes *args = gsk_gl_shader_node_get_args (node);

        start_node (p, "glshader", node_name);

        append_rect_param (p, "bounds", &node->bounds);

        GBytes *bytes = gsk_gl_shader_get_source (shader);
        /* Ensure we are zero-terminated */
        char *sourcecode = g_strndup (g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes));
        append_string_param (p, "sourcecode", sourcecode);
        g_free (sourcecode);

        if (gsk_gl_shader_get_n_uniforms (shader) > 0)
          {
            GString *data = g_string_new ("");

            for (guint i = 0; i < gsk_gl_shader_get_n_uniforms (shader); i++)
              {
                if (i > 0)
                  g_string_append (data, ", ");

                switch (gsk_gl_shader_get_uniform_type (shader, i))
                  {
                  case GSK_GL_UNIFORM_TYPE_NONE:
                  default:
                    g_assert_not_reached ();
                    break;

                  case GSK_GL_UNIFORM_TYPE_FLOAT:
                    {
                      float value = gsk_gl_shader_get_arg_float (shader, args, i);
                      string_append_double (data, value);
                    }
                    break;

                  case GSK_GL_UNIFORM_TYPE_INT:
                    {
                      gint32 value = gsk_gl_shader_get_arg_int (shader, args, i);
                      g_string_append_printf (data, "%d", value);
                    }
                    break;

                  case GSK_GL_UNIFORM_TYPE_UINT:
                    {
                      guint32 value = gsk_gl_shader_get_arg_uint (shader, args, i);
                      g_string_append_printf (data, "%u", value);
                    }
                    break;

                  case GSK_GL_UNIFORM_TYPE_BOOL:
                    {
                      gboolean value = gsk_gl_shader_get_arg_bool (shader, args, i);
                      g_string_append_printf (data, "%d", value);
                    }
                    break;

                  case GSK_GL_UNIFORM_TYPE_VEC2:
                    {
                      graphene_vec2_t value;
                      gsk_gl_shader_get_arg_vec2 (shader, args, i,
                                                  &value);
                      string_append_double (data, graphene_vec2_get_x (&value));
                      g_string_append (data, " ");
                      string_append_double (data, graphene_vec2_get_y (&value));
                    }
                    break;

                  case GSK_GL_UNIFORM_TYPE_VEC3:
                    {
                      graphene_vec3_t value;
                      gsk_gl_shader_get_arg_vec3 (shader, args, i,
                                                  &value);
                      string_append_double (data, graphene_vec3_get_x (&value));
                      g_string_append (data, " ");
                      string_append_double (data, graphene_vec3_get_y (&value));
                      g_string_append (data, " ");
                      string_append_double (data, graphene_vec3_get_z (&value));
                    }
                    break;

                  case GSK_GL_UNIFORM_TYPE_VEC4:
                    {
                      graphene_vec4_t value;
                      gsk_gl_shader_get_arg_vec4 (shader, args, i,
                                                  &value);
                      string_append_double (data, graphene_vec4_get_x (&value));
                      g_string_append (data, " ");
                      string_append_double (data, graphene_vec4_get_y (&value));
                      g_string_append (data, " ");
                      string_append_double (data, graphene_vec4_get_z (&value));
                      g_string_append (data, " ");
                      string_append_double (data, graphene_vec4_get_w (&value));
                    }
                    break;
                  }
              }
            _indent (p);
            g_string_append_printf (p->str, "args: %s;\n", data->str);
            g_string_free (data, TRUE);
          }

        for (guint i = 0; i < gsk_gl_shader_node_get_n_children (node); i ++)
          {
            GskRenderNode *child = gsk_gl_shader_node_get_child (node, i);
            char *name;

            name = g_strdup_printf ("child%d", i + 1);
            append_node_param (p, name, child);
            g_free (name);
          }

        end_node (p);
G_GNUC_END_IGNORE_DEPRECATIONS
      }
      break;

    case GSK_REPEAT_NODE:
      {
        GskRenderNode *child = gsk_repeat_node_get_child (node);
        GskRepeat repeat = gsk_repeat_node_get_repeat (node);

        start_node (p, "repeat", node_name);

        if (!graphene_rect_equal (&node->bounds, &child->bounds))
          append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_repeat_node_get_snap (node));
        append_rect_param (p, "child-bounds", gsk_repeat_node_get_child_bounds (node));
        append_snap_param (p, "child-snap", gsk_repeat_node_get_child_snap (node));
        append_node_param (p, "child", gsk_repeat_node_get_child (node));
        if (repeat != GSK_REPEAT_REPEAT)
          append_repeat_param (p, "repeat", repeat);

        end_node (p);
      }
      break;

    case GSK_BLEND_NODE:
      {
        GskBlendMode mode = gsk_blend_node_get_blend_mode (node);

        start_node (p, "blend", node_name);

        if (mode != GSK_BLEND_MODE_DEFAULT)
          {
            _indent (p);
            g_string_append_printf (p->str, "mode: %s;\n", get_blend_mode_name (mode));
          }
        append_node_param (p, "bottom", gsk_blend_node_get_bottom_child (node));
        append_node_param (p, "top", gsk_blend_node_get_top_child (node));
        append_color_state_param (p, "color-state", gsk_blend_node_get_color_state (node), GDK_COLOR_STATE_SRGB);

        end_node (p);
      }
      break;

    case GSK_MASK_NODE:
      {
        GskMaskMode mode = gsk_mask_node_get_mask_mode (node);

        start_node (p, "mask", node_name);

        if (mode != GSK_MASK_MODE_ALPHA)
          {
            _indent (p);
            g_string_append_printf (p->str, "mode: %s;\n", get_mask_mode_name (mode));
          }
        append_node_param (p, "source", gsk_mask_node_get_source (node));
        append_node_param (p, "mask", gsk_mask_node_get_mask (node));

        end_node (p);
      }
      break;

    case GSK_NOT_A_RENDER_NODE:
      g_assert_not_reached ();
      break;

    case GSK_CAIRO_NODE:
      {
        cairo_surface_t *surface = gsk_cairo_node_get_surface (node);
        GByteArray *array;
        GBytes *bytes;

        start_node (p, "cairo", node_name);
        append_rect_param (p, "bounds", &node->bounds);

        if (surface != NULL)
          {
            array = g_byte_array_new ();
#if CAIRO_HAS_PNG_FUNCTIONS
            cairo_surface_write_to_png_stream (surface, cairo_write_array, array);
#endif
            bytes = g_byte_array_free_to_bytes (array);

            append_bytes_param (p, "pixels", bytes, "image/png");

            g_bytes_unref (bytes);

#ifdef CAIRO_HAS_SCRIPT_SURFACE
            if (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_RECORDING)
              {
                static const cairo_user_data_key_t cairo_is_stupid_key = { 0, };
                cairo_device_t *script;

                array = g_byte_array_new ();
                script = cairo_script_create_for_stream (cairo_write_array, array);

                if (cairo_script_from_recording_surface (script, surface) == CAIRO_STATUS_SUCCESS)
                  {
                    g_byte_array_ref (array); /* Cairo... see below */
                    bytes = g_byte_array_free_to_bytes (array);
                    append_bytes_param (p, "script", bytes, NULL);
                    g_bytes_unref (bytes);
                  }

                /* because Cairo is stupid and writes to the device after we finished it,
                 * we can't just
                g_byte_array_free (array, TRUE);
                 * but have to
                 */

                g_byte_array_set_size (array, 0);
                cairo_device_set_user_data (script, &cairo_is_stupid_key, array, cairo_destroy_array);
                cairo_device_destroy (script);
              }
#endif
          }

        end_node (p);
      }
      break;

    case GSK_SUBSURFACE_NODE:
      {
        start_node (p, "subsurface", node_name);

        append_node_param (p, "child", gsk_subsurface_node_get_child (node));

        end_node (p);
      }
      break;

    case GSK_COMPONENT_TRANSFER_NODE:
      {
        start_node (p, "component-transfer", node_name);

        append_node_param (p, "child", gsk_component_transfer_node_get_child (node));
        append_component_transfer_param (p, "red", gsk_component_transfer_node_get_transfer (node, 0));
        append_component_transfer_param (p, "green", gsk_component_transfer_node_get_transfer (node, 1));
        append_component_transfer_param (p, "blue", gsk_component_transfer_node_get_transfer (node, 2));
        append_component_transfer_param (p, "alpha", gsk_component_transfer_node_get_transfer (node, 3));
        append_color_state_param (p, "color-state", gsk_component_transfer_node_get_color_state (node), GDK_COLOR_STATE_SRGB);

        end_node (p);
      }
      break;

    case GSK_COPY_NODE:
      {
        start_node (p, "copy", node_name);
        append_node_param (p, "child", gsk_debug_node_get_child (node));
        end_node (p);
      }
      break;

    case GSK_PASTE_NODE:
      {
        start_node (p, "paste", node_name);
        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_paste_node_get_snap (node));
        if (gsk_paste_node_get_depth (node) != 0)
          append_size_param (p, "depth", gsk_paste_node_get_depth (node));
        end_node (p);
      }
      break;

    case GSK_COMPOSITE_NODE:
      start_node (p, "composite", node_name);
      append_node_param (p, "child", gsk_composite_node_get_child (node));
      append_node_param (p, "mask", gsk_composite_node_get_mask (node));
      append_enum_param (p, "operator", GSK_TYPE_PORTER_DUFF, gsk_composite_node_get_operator (node));
      end_node (p);
      break;

    case GSK_ISOLATION_NODE:
      start_node (p, "isolation", node_name);
      append_node_param (p, "child", gsk_isolation_node_get_child (node));
      if (gsk_isolation_node_get_isolations (node) != GSK_ISOLATION_ALL)
        append_isolation_param (p, "isolations", gsk_isolation_node_get_isolations (node));
      end_node (p);
      break;

    case GSK_DISPLACEMENT_NODE:
      {
        const graphene_size_t *max, *scale;
        const graphene_point_t *offset;
        const GdkColorChannel *channels = gsk_displacement_node_get_channels (node);
        max = gsk_displacement_node_get_max (node);
        scale = gsk_displacement_node_get_scale (node);
        offset = gsk_displacement_node_get_offset (node);
        start_node (p, "displacement", node_name);
        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_displacement_node_get_snap (node));
        append_node_param (p, "child", gsk_displacement_node_get_child (node));
        append_node_param (p, "displacement", gsk_displacement_node_get_displacement (node));
        append_two_float_param (p, "max", max->width, max->height);
        append_two_float_param (p, "scale", scale->width, scale->height);
        append_two_float_param (p, "offset", offset->x, offset->y);
        append_channels_param (p, "channels", channels[0], channels[1]);
        end_node (p);
      }
      break;

    case GSK_ARITHMETIC_NODE:
      {
        start_node (p, "arithmetic", node_name);

        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_arithmetic_node_get_snap (node));
        append_four_float_param (p, "k", gsk_arithmetic_node_get_factors (node));
        append_node_param (p, "first", gsk_arithmetic_node_get_first_child (node));
        append_node_param (p, "second", gsk_arithmetic_node_get_second_child (node));
        append_color_state_param (p, "color-state", gsk_arithmetic_node_get_color_state (node), GDK_COLOR_STATE_SRGB);

        end_node (p);
      }
      break;

    case GSK_TURBULENCE_NODE:
      {
        const graphene_size_t *freq = gsk_turbulence_node_get_base_frequency (node);

        start_node (p, "turbulence", node_name);

        append_rect_param (p, "bounds", &node->bounds);
        append_snap_param (p, "snap", gsk_turbulence_node_get_snap (node));
        append_color_state_param (p, "color-state", gsk_turbulence_node_get_color_state (node), GDK_COLOR_STATE_SRGB);
        append_two_float_param (p, "base-frequency", freq->width, freq->height);
        if (gsk_turbulence_node_get_num_octaves (node) != 1)
          append_size_param (p, "num-octaves", gsk_turbulence_node_get_num_octaves (node));
        if (gsk_turbulence_node_get_seed (node) != 0)
          append_unsigned_param (p, "seed", gsk_turbulence_node_get_seed (node));
        if (gsk_turbulence_node_get_noise_type (node) != GSK_NOISE_TURBULENCE)
          append_noise_param (p, "noise-type", gsk_turbulence_node_get_noise_type (node));
        if (gsk_turbulence_node_get_stitch_tiles (node))
          append_boolean_param (p, "stitch-tiles"TRUE);

        end_node (p);
      }
      break;

    default:
      g_error ("Unhandled node: %s", g_type_name_from_instance ((GTypeInstance *) node));
      break;
    }
}

static void
serialize_color_state (GString       *str,
                       GdkColorState *color_state,
                       const char    *name)
{
  const GdkCicp *cicp = gdk_color_state_get_cicp (color_state);

  g_string_append_printf (str, "@cicp \"%s\" {\n", name);
  g_string_append_printf (str, "  primaries: %u;\n", cicp->color_primaries);
  g_string_append_printf (str, "  transfer: %u;\n", cicp->transfer_function);
  g_string_append_printf (str, "  matrix: %u;\n", cicp->matrix_coefficients);
  if (cicp->range != GDK_CICP_RANGE_FULL)
    g_string_append_printf (str, "  range: %s;\n", enum_to_nick (GDK_TYPE_CICP_RANGE, cicp->range));
  g_string_append (str, "}\n");
}

/**
 * gsk_render_node_serialize:
 * @node: a `GskRenderNode`
 *
 * Serializes the @node for later deserialization via
 * gsk_render_node_deserialize(). No guarantees are made about the format
 * used other than that the same version of GTK will be able to deserialize
 * the result of a call to gsk_render_node_serialize() and
 * gsk_render_node_deserialize() will correctly reject files it cannot open
 * that were created with previous versions of GTK.
 *
 * The intended use of this functions is testing, benchmarking and debugging.
 * The format is not meant as a permanent storage format.
 *
 * Returns: a `GBytes` representing the node.
 **/

GBytes *
gsk_render_node_serialize (GskRenderNode *node)
{
  Printer p;
  GHashTableIter iter;
  GString *str;
  GHashTable *table;
  GPtrArray *keys;
  const char *name;
  GdkColorState *cs;

  printer_init (&p, node);

  if (gsk_render_node_get_node_type (node) == GSK_CONTAINER_NODE)
    {
      guint i;

      for (i = 0; i < gsk_container_node_get_n_children (node); i ++)
        {
          GskRenderNode *child = gsk_container_node_get_child (node, i);

          render_node_print (&p, child);
        }
    }
  else
    {
      render_node_print (&p, node);
    }

  str = g_string_new (NULL);

  table = g_hash_table_new (g_str_hash, g_str_equal);
  g_hash_table_iter_init (&iter, p.named_color_states);
  while (g_hash_table_iter_next (&iter, (gpointer *)&cs, (gpointer *)&name))
    g_hash_table_insert (table, (gpointer) name, (gpointer) cs);

  keys = g_hash_table_get_keys_as_ptr_array (table);
  g_ptr_array_sort_values (keys, (GCompareFunc) strcmp);
  for (unsigned int i = 0; i < keys->len; i++)
    {
      name = g_ptr_array_index (keys, i);
      cs = g_hash_table_lookup (table, name);
      serialize_color_state (str, cs, name);
    }
  g_ptr_array_unref (keys);
  g_hash_table_unref (table);

  g_string_append_len (str, p.str->str, p.str->len);

  printer_clear (&p);

  return g_string_free_to_bytes (str);
}

Messung V0.5 in Prozent
C=98 H=95 G=96

¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.167Angebot  (Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-07-02) ¤

*Eine klare Vorstellung vom Zielzustand






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik