Eine aufbereitete Darstellung der Quelle

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

Benutzer

Quelle  gtksvg.c

  Sprache: C
 

/*
 * Copyright © 2025 Red Hat, Inc
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * 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: Matthias Clasen <mclasen@redhat.com>
 */


#include "config.h"

#include "gtksvgprivate.h"
#include "gtksvgrendererprivate.h"
#include "gtksvgserializeprivate.h"
#include "gtksvgparserprivate.h"

#include "gtkenums.h"
#include "gtksymbolicpaintable.h"
#include "gtktypebuiltins.h"
#include "gtkmarshalers.h"
#include "gtk/css/gtkcssparserprivate.h"
#include "gtk/css/gtkcssdataurlprivate.h"
#include "gtksnapshotprivate.h"
#include "gsk/gskarithmeticnodeprivate.h"
#include "gsk/gskblendnodeprivate.h"
#include "gsk/gskcolormatrixnodeprivate.h"
#include "gsk/gskcolornodeprivate.h"
#include "gsk/gskcomponenttransfernodeprivate.h"
#include "gsk/gskdisplacementnodeprivate.h"
#include "gsk/gskisolationnodeprivate.h"
#include "gsk/gskrepeatnodeprivate.h"
#include "gsk/gskpathprivate.h"
#include "gsk/gskcontourprivate.h"
#include "gsk/gskrectprivate.h"
#include "gsk/gskroundedrectprivate.h"
#include "gtk/gtkcssselectorprivate.h"
#include "gtksvgenumtypes.h"
#include "gtksvgutilsprivate.h"
#include "gtksvgstringutilsprivate.h"
#include "gtksvgvalueprivate.h"
#include "gtksvgkeywordprivate.h"
#include "gtksvgnumberprivate.h"
#include "gtksvgnumbersprivate.h"
#include "gtksvgstringprivate.h"
#include "gtksvgstringlistprivate.h"
#include "gtksvgenumprivate.h"
#include "gtksvgfilterrefprivate.h"
#include "gtksvgtransformprivate.h"
#include "gtksvgcolorprivate.h"
#include "gtksvgpaintprivate.h"
#include "gtksvgfilterfunctionsprivate.h"
#include "gtksvgdasharrayprivate.h"
#include "gtksvgpathprivate.h"
#include "gtksvgpathdataprivate.h"
#include "gtksvgclipprivate.h"
#include "gtksvgmaskprivate.h"
#include "gtksvgviewboxprivate.h"
#include "gtksvgcontentfitprivate.h"
#include "gtksvgorientprivate.h"
#include "gtksvglanguageprivate.h"
#include "gtksvghrefprivate.h"
#include "gtksvgtextdecorationprivate.h"
#include "gtksvgpathutilsprivate.h"
#include "gtksvgcolorutilsprivate.h"
#include "gtksvgpangoutilsprivate.h"
#include "gtksvglocationprivate.h"
#include "gtksvgerrorprivate.h"
#include "gtksvgelementtypeprivate.h"
#include "gtksvgpropertyprivate.h"
#include "gtksvgfiltertypeprivate.h"
#include "gtksvgfilterprivate.h"
#include "gtksvgcolorstopprivate.h"
#include "gtksvgelementinternal.h"
#include "gtksvganimationprivate.h"
#include "gtksvgtimespecprivate.h"
#include "gtksvggpaprivate.h"
#include "gtksvgmediaqueryprivate.h"

#include <glib/gstdio.h>

#include <stdint.h>

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

#include "gdk/gdkrgbaprivate.h"

#ifdef DEBUG
extern int64_t time_base;
#endif

/**
 * GtkSvg:
 *
 * A paintable implementation that renders SVG, with animations.
 *
 * `GtkSvg` objects are created by parsing a subset of SVG,
 * including SVG animations.
 *
 * `GtkSvg` fills or strokes paths with symbolic or fixed colors.
 * It can have multiple states, and paths can be included in a subset
 * of the states. States can have animations, and the transition
 * between different states can also be animated.
 *
 * To show a static SVG image, it is enough to load the
 * the SVG and use it like any other paintable.
 *
 * To play an SVG animation, use [method@Gtk.Svg.set_frame_clock]
 * to connect the paintable to a frame clock, and call
 * [method@Gtk.Svg.play] after loading the SVG. The animation can
 * be paused using [method@Gtk.Svg.pause].
 *
 * To set the current state, use [method@Gtk.Svg.set_state].
 *
 *
 * ## Error handling
 *
 * Loading an SVG into `GtkSvg` will always produce a (possibly empty)
 * paintable. GTK will drop things that it can't handle and try to make
 * sense of the rest.
 *
 * To track errors during parsing or rendering, connect to the
 * [signal@Gtk.Svg::error] signal.
 *
 * For parsing errors in the `GTK_SVG_ERROR` domain, the functions
 * [func@Gtk.SvgError.get_start], [func@Gtk.SvgError.get_end],
 * [func@Gtk.SvgError.get_element] and [func@Gtk.SvgError.get_attribute]
 * can be used to obtain information about where the error occurred.
 *
 *
 * ## The supported subset of SVG
 *
 * The paintable supports much of [SVG 2](https://svgwg.org/svg2-draft/),
 * including [animations](https://svgwg.org/specs/animations/), with some
 * exceptions.
 *
 * Among the graphical elements, `<textPath>` and `<foreignObject>` are
 * not supported.
 *
 * In the `<filter>` element, the following primitives are not supported:
 * feConvolveMatrix, feDiffuseLighting, feMorphology, feSpecularLighting.
 *
 * Support for the `mask` attribute is limited to just a url referring to
 * the `<mask>` element by ID.
 *
 * In animation elements, the parsing of `begin` and `end` attributes is
 * limited, and the `min` and `max` attributes are not supported.
 *
 * The interactive aspects of SVG are supported by [class@Gtk.SvgWidget].
 *
 *
 * ## SVG Extensions
 *
 * The paintable supports a number of [custom attributes](icon-format.html)
 * that offer a convenient way to define states, transitions and animations.
 * For example,
 *
 *     <circle cx='5' cy='5' r='5'
 *             gpa:states='0 1'
 *             gpa:animation-type='automatic'
 *             gpa:animation-direction='segment'
 *             gpa:animation-duration='600ms'/>
 *
 * defines the circle to be shown in states 0 and 1, and animates a segment
 * of the circle.
 *
 *
 * <image src="svg-renderer1.svg">
 *
 * Note that the generated animations are implemented using standard
 * SVG attributes (`visibility`, `stroke-dasharray, `stroke-dashoffset`,
 * `pathLength` and `filter`). Setting these attributes in your SVG
 * is therefore going to interfere with generated animations.
 *
 * To connect general SVG animations to the states of the paintable,
 * use the custom `StateChange(...)` condition in the `begin` and `end`
 * attributes of SVG animation elements. For example,
 *
 *     <animate href='path1'
 *              attributeName='fill'
 *              begin='StateChange(1 2 3, 0)'
 *              dur='300ms'
 *              fill='freeze'
 *              from='black'
 *              to='magenta'/>
 *
 * will make the fill color of path1 transition from black to
 * magenta when the renderer enters state 0 from states 1, 2, or 3.
 *
 * <image src="svg-renderer2.svg">
 *
 * The `StateChange(...)` condition triggers for upcoming state changes
 * as well, to support fade-out transitions. For example,
 *
 *     <animate href='path1'
 *              attributeName='opacity'
 *              begin='StateChange(0, 1 2 3) -300ms'
 *              dur='300ms'
 *              fill='freeze'
 *              from='1'
 *              to='0'/>
 *
 * will start a fade-out of path1 300ms before a transition from state
 * 0 to 1, 2 or 3.
 *
 * States can be specified numerically, or by name. It is also possible
 * to say `not STATE` instead of explictly listing all states that are
 * different from `STATE`.
 *
 * In addition to the `gpa:fill` and `gpa:stroke` attributes, symbolic
 * colors can also be specified as a custom paint server reference,
 * like this: `url(#gpa:warning)`. This works in `fill` and `stroke`
 * attributes, but also when specifying colors in SVG animation
 * attributes like `to` or `values`.
 *
 * Note that the SVG syntax allows for a fallback RGB color to be
 * specified after the url, for compatibility with other SVG consumers:
 *
 *     fill='url(#gpa:warning) orange'
 *
 * GtkSvg also allows to refer to symbolic colors like system colors
 * in CSS, with names like SymbolicForeground, SymbolicSuccess, etc.
 * These can be used whenever a color is required.
 *
 * In contrast to SVG 1.1 and 2.0, we allow the `transform` attribute
 * to be animated with `<animate>`.
 *
 *
 * Since: 4.22
 */


/* {{{ Errors */

static unsigned int error_signal;

void
gtk_svg_emit_error (GtkSvg       *svg,
                    const GError *error)
{
  g_signal_emit (svg, error_signal, 0, error);
}

void
gtk_svg_invalid_attribute (GtkSvg               *self,
                           GMarkupParseContext  *context,
                           const char          **attr_names,
                           const char           *attr_name,
                           const char           *format,
                           ...)
{
  GError *error = NULL;
  GtkSvgLocation start, end;

  if (format)
    {
      va_list args;
      va_start (args, format);
      error = g_error_new_valist (GTK_SVG_ERROR,
                                  GTK_SVG_ERROR_INVALID_ATTRIBUTE,
                                  format, args);
      va_end (args);
    }
  else
    {
      error = g_error_new (GTK_SVG_ERROR,
                           GTK_SVG_ERROR_INVALID_ATTRIBUTE,
                           "Invalid attribute: %s", attr_name);
    }

  gtk_svg_error_set_input (error, "svg");
  gtk_svg_error_set_element (error, g_markup_parse_context_get_element (context));
  gtk_svg_error_set_attribute (error, attr_name);
  gtk_svg_location_init_tag_range (&start, &end, context);

  if (attr_names && attr_name)
    {
      for (unsigned int i = 0; attr_names[i]; i++)
        {
          if (strcmp (attr_names[i], attr_name) == 0)
            {
              gtk_svg_location_init_attr_range (&start, &end, context, i);
              break;
            }
        }
    }

  gtk_svg_error_set_location (error, &start, &end);

  gtk_svg_emit_error (self, error);
  g_error_free (error);
}

void
gtk_svg_invalid_reference (GtkSvg     *self,
                           const char *format,
                           ...)
{
  va_list args;
  GError *error;

  va_start (args, format);
  error = g_error_new_valist (GTK_SVG_ERROR,
                              GTK_SVG_ERROR_INVALID_REFERENCE,
                              format, args);
  va_end (args);

  gtk_svg_emit_error (self, error);
  g_error_free (error);
}

void
gtk_svg_update_error (GtkSvg     *self,
                      const char *format,
                      ...)
{
  va_list args;
  GError *error;

  va_start (args, format);
  error = g_error_new_valist (GTK_SVG_ERROR,
                              GTK_SVG_ERROR_FAILED_UPDATE,
                              format, args);
  va_end (args);

  gtk_svg_emit_error (self, error);
  g_error_free (error);
}

void
gtk_svg_rendering_error (GtkSvg     *self,
                         const char *format,
                         ...)
{
  va_list args;
  GError *error;

  va_start (args, format);
  error = g_error_new_valist (GTK_SVG_ERROR,
                              GTK_SVG_ERROR_FAILED_RENDERING,
                              format, args);
  va_end (args);

  gtk_svg_emit_error (self, error);
  g_error_free (error);
}

/* }}} */
/* {{{ Helpers */

static float
ease (double *params,
      double  progress)
{
  static const double epsilon = 0.00001;
  double tmin, t, tmax;
  double x1, y1, x2, y2;

  x1 = params[0];
  y1 = params[1];
  x2 = params[2];
  y2 = params[3];

  if (progress <= 0)
    return 0;
  if (progress >= 1)
    return 1;

  tmin = 0.0;
  tmax = 1.0;
  t = progress;

  while (tmin < tmax)
    {
      double sample;

      sample = (((1 + 3 * x1 - 3 * x2) * t
                +    -6 * x1 + 3 * x2) * t
                +     3 * x1         ) * t;
      if (fabs (sample - progress) < epsilon)
        break;

      if (progress > sample)
        tmin = t;
      else
        tmax = t;
      t = (tmax + tmin) * .5;
    }

  return (((1 + 3 * y1 - 3 * y2) * t
          +    -6 * y1 + 3 * y2) * t
          +     3 * y1         ) * t;
}

static gboolean
strv_unique (GStrv strv)
{
  if (strv)
    for (unsigned int i = 0; strv[i]; i++)
      for (unsigned int j = i + 1; strv[j]; j++)
        if (strcmp (strv[i], strv[j]) == 0)
          return FALSE;

  return TRUE;
}

static int64_t
get_current_time (GtkSvg *self)
{
  if (self->clock && self->playing && self->load_time != INDEFINITE)
    return MAX (self->current_time, gdk_frame_clock_get_frame_time (self->clock));
  return self->current_time;
}

/* }}} */
/* {{{ Image loading */

static GdkTexture *
load_texture (const char  *string,
              gboolean     allow_external,
              gboolean    *cache_this,
              GError     **error)
{
  GdkTexture *texture = NULL;

  *cache_this = TRUE;

  if (g_str_has_prefix (string, "data:"))
    {
      GBytes *bytes;

      bytes = gtk_css_data_url_parse (string, NULL, error);

      if (bytes == NULL)
        return NULL;

      *cache_this = FALSE;
      texture = gdk_texture_new_from_bytes (bytes, error);

      g_bytes_unref (bytes);
    }
  else if (g_str_has_prefix (string, "resource:"))
    {
      texture = gdk_texture_new_from_resource (string + strlen ("resource:"));
    }
  else if (allow_external && g_str_has_prefix (string, "file:"))
    {
      GFile *file = g_file_new_for_uri (string);
      texture = gdk_texture_new_from_file (file, error);
      g_object_unref (file);
    }
  else if (allow_external)
    {
      texture = gdk_texture_new_from_filename (string, error);
    }
  else
    {
      g_set_error (error, GTK_SVG_ERROR, GTK_SVG_ERROR_FEATURE_DISABLED,
                   "Not loading texture from %s: External resources disabled", string);
    }

  return texture;
}

GdkTexture *
get_texture (GtkSvg      *svg,
             const char  *string,
             GError     **error)
{
  GdkTexture *texture;

  texture = g_hash_table_lookup (svg->images, string);
  if (!texture)
    {
      gboolean cache_this;

      texture = load_texture (string,
                              (svg->features & GTK_SVG_EXTERNAL_RESOURCES) != 0,
                              &cache_this,
                              error);
      if (texture && cache_this)
        g_hash_table_insert (svg->images, g_strdup (string), g_object_ref (texture));
    }

  return texture;
}

/* }}} */
/* {{{ Font handling */

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

  g_remove (path);
  g_free (path);
}

void
ensure_fontmap (GtkSvg *svg)
{
  if (svg->fontmap)
    return;

  svg->fontmap = pango_cairo_font_map_new ();

  if ((svg->features & GTK_SVG_SYSTEM_RESOURCES) == 0)
    {
#ifdef HAVE_PANGOFT
      /* This isolates us from the system fonts */
      FcConfig *config;

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

  svg->font_files = g_ptr_array_new_with_free_func (delete_file);
}

static gboolean
add_font_from_file (GtkSvg      *svg,
                    const char  *path,
                    GError     **error)
{
  ensure_fontmap (svg);

  if (!pango_font_map_add_font_file (svg->fontmap, path, error))
    return FALSE;

  g_ptr_array_add (svg->font_files, g_strdup (path));
  return TRUE;
}

static gboolean
add_font_from_bytes (GtkSvg      *svg,
                     GBytes      *bytes,
                     GError     **error)
{
  GFile *file;
  GIOStream *iostream;
  GOutputStream *ostream;

  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);

  if (!add_font_from_file (svg, g_file_peek_path (file), error))
    {
      g_file_delete (file, NULL, NULL);
      g_object_unref (file);

      return FALSE;
    }

  g_object_unref (file);
  return TRUE;
}

gboolean
add_font_from_url (GtkSvg               *svg,
                   GMarkupParseContext  *context,
                   const char          **attr_names,
                   const char           *attr_name,
                   const char           *url)
{
  char *scheme;
  GBytes *bytes;
  char *mimetype = NULL;
  GError *error = NULL;

  scheme = g_uri_parse_scheme (url);
  if (!scheme || g_ascii_strcasecmp (scheme, "data") != 0)
    {
      char *start = g_utf8_make_valid (url, 20);
      gtk_svg_invalid_attribute (svg, context, attr_names, attr_name,
                                 "Unsupported uri scheme for font: %s…", start);
      g_free (start);
      return FALSE;
    }

  g_free (scheme);

  bytes = gtk_css_data_url_parse (url, &mimetype, &error);
  if (!bytes)
    {
      gtk_svg_invalid_attribute (svg, context, attr_names, attr_name,
                                 "Can't parse font data: %s", error->message);
      g_error_free (error);
      g_free (mimetype);
      return FALSE;
    }

  if (strcmp (mimetype, "font/ttf") != 0)
    {
      gtk_svg_invalid_attribute (svg, context, attr_names, attr_name,
                                 "Unsupported mime type for font data: %s", mimetype);
      g_bytes_unref (bytes);
      g_free (mimetype);
      return FALSE;
    }

  g_free (mimetype);

  if (!add_font_from_bytes (svg, bytes, &error))
    {
      g_bytes_unref (bytes);
      gtk_svg_invalid_attribute (svg, context, attr_names, attr_name,
                                 "Failed to add font: %s", error->message);
      g_error_free (error);
      return FALSE;
    }

  g_bytes_unref (bytes);
  return TRUE;
}

/* }}} */
/* {{{ Animation */

static int64_t
get_last_start (SvgAnimation *a)
{
  if (a->status == ANIMATION_STATUS_DONE)
    return a->previous.begin;
  else
    return a->current.begin;
}

static int
compare_anim (gconstpointer a,
              gconstpointer b)
{
  SvgAnimation *a1 = (SvgAnimation *) a;
  SvgAnimation *a2 = (SvgAnimation *) b;
  int64_t start1, start2;

  /* It doesn't matter, but we sort by attributes to get
   * a predictable order
   */

  if (a1->attr < a2->attr)
    return -1;
  else if (a1->attr > a2->attr)
    return 1;

  /* The situation with animateTransform vs. animateMotion
   * is special: they don't add to each other, and
   * the accumulate animateMotion transform is always
   * applied after the accumulate animateTransform.
   */

  if (a1->attr == SVG_PROPERTY_TRANSFORM)
    {
      if (a1->type == ANIMATION_TYPE_MOTION && a2->type != ANIMATION_TYPE_MOTION)
        return 1;
      else if (a1->type != ANIMATION_TYPE_MOTION && a2->type == ANIMATION_TYPE_MOTION)
        return -1;
    }

  /* SvgAnimation priorities:
   * - started later > started earlier
   * - later in document > earlier in document
   *
   * Sort higher priority animations to the end
   * so their effect overrides lower priority ones.
   */

  start1 = get_last_start (a1);
  start2 = get_last_start (a2);

  if (start1 < start2)
    return -1;
  else if (start1 > start2)
    return 1;

  if (a1->line < a2->line)
    return -1;
  else if (a1->line > a2->line)
    return 1;

  return 0;
}

static void
collect_next_update_for_animation (SvgAnimation  *a,
                                   int64_t        current_time,
                                   GtkSvgRunMode *run_mode,
                                   int64_t       *next_update)
{
  svg_animation_update_run_mode (a, current_time);

#ifdef DEBUG
  if (a->run_mode > *run_mode)
    {
      const char *mode_name[] = { "STOPPED""DISCRETE""CONTINUOUS" };
      dbg_print ("run""%s updates run mode to %s", a->id, mode_name[a->run_mode]);
    }
  if (a->next_invalidate < *next_update)
    {
      dbg_print ("run""%s updates next update to %s", a->id, format_time (a->next_invalidate));
    }
#endif

  *run_mode = MAX (*run_mode, a->run_mode);
  *next_update = MIN (*next_update, a->next_invalidate);

  if (a->state_changed)
    {
      *next_update = current_time;
      a->state_changed = FALSE;
    }
}

static void
collect_next_update_for_shape (SvgElement    *shape,
                               int64_t        current_time,
                               GtkSvgRunMode *run_mode,
                               int64_t       *next_update)
{
  if (shape->animations)
    {
      for (unsigned int i = 0; i < shape->animations->len; i++)
        {
          SvgAnimation *a = g_ptr_array_index (shape->animations, i);
          collect_next_update_for_animation (a, current_time, run_mode, next_update);
        }
    }

  if (shape->shapes)
    {
      for (unsigned int i = 0; i < shape->shapes->len; i++)
        {
          SvgElement *s = g_ptr_array_index (shape->shapes, i);
          collect_next_update_for_shape (s, current_time, run_mode, next_update);
        }
    }
}

static void
collect_next_update (GtkSvg *self)
{
  GtkSvgRunMode run_mode = GTK_SVG_RUN_MODE_STOPPED;
  int64_t next_update = INDEFINITE;

  collect_next_update_for_shape (self->content, self->current_time, &run_mode, &next_update);

#ifdef DEBUG
  if (strstr (g_getenv ("SVG_DEBUG") ?: """continuous"))
    run_mode = GTK_SVG_RUN_MODE_CONTINUOUS;
#endif

  self->run_mode = run_mode;
  self->next_update = next_update;

#ifdef DEBUG
  const char *mode_name[] = { "STOPPED""DISCRETE""CONTINUOUS" };
  dbg_print ("run""run mode %s", mode_name[self->run_mode]);
  dbg_print ("run""next update %s", format_time (self->next_update));
#endif
}

static void
invalidate_for_next_update (GtkSvg *self)
{
  if (self->next_update <= self->current_time ||
      self->run_mode == GTK_SVG_RUN_MODE_CONTINUOUS)
    {
      dbg_print ("run""invalidating for update");
      gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
    }
#ifdef DEBUG
  else
    {
      GString *s = g_string_new ("not invalidating (");
      g_string_append_printf (s, "%s", format_time (self->next_update));
      g_string_append_printf (s, " > %s)", format_time (self->current_time));
      dbg_print ("run""%s", s->str);
      g_string_free (s, TRUE);
    }
#endif
}

static void
shape_update_animation_state (SvgElement *shape,
                              int64_t     current_time)
{
  if (shape->animations)
    {
      gboolean any_changed = FALSE;

      for (unsigned int i = 0; i < shape->animations->len; i++)
        {
          SvgAnimation *a = g_ptr_array_index (shape->animations, i);
          svg_animation_update_state (a, current_time);
          any_changed |= a->state_changed;
        }

      if (any_changed)
        g_ptr_array_sort_values (shape->animations, compare_anim);
    }

  if (shape->shapes)
    {
      for (unsigned int i = 0; i < shape->shapes->len; i++)
        {
          SvgElement *s = g_ptr_array_index (shape->shapes, i);
          shape_update_animation_state (s, current_time);
        }
    }
}

static void schedule_next_update (GtkSvg *self);

static void
update_animation_state (GtkSvg *self)
{
  shape_update_animation_state (self->content, self->current_time);

  collect_next_update (self);
  invalidate_for_next_update (self);
  schedule_next_update (self);
}

static void
animations_update_for_pause (SvgElement *shape,
                             int64_t     duration)
{
  if (shape->animations)
    {
      for (unsigned int i = 0; i < shape->animations->len; i++)
        {
          SvgAnimation *a = g_ptr_array_index (shape->animations, i);
          svg_animation_update_for_pause (a, duration);
        }
    }

  if (svg_element_type_is_path (svg_element_get_type (shape)))
    {
      for (unsigned int i = 0; i < shape->shapes->len; i++)
        {
          SvgElement *sh = g_ptr_array_index (shape->shapes, i);
          animations_update_for_pause (sh, duration);
        }
    }
}

/* }}} */
/* {{{ Animated attributes */

/* SvgAnimation works by
 * - updating the current time
 * - walking the shape tree and applying the relevant
 *   animations to produce current values
 * - rendering the next frame based on these values
 *
 * When walking the shape tree, we need to do so in an order
 * that ensures values are computed before they are used by
 * other animations (e.g. a motion animation needs to use the
 * updated path of the shape it uses to compute the next position).
 * This update order is determined at parse time.
 *
 * It is sometimes necessary to compute new values during
 * rendering (e.g. for <use>, where the <use> element replaces
 * the parent of the referred to shape for inheritance purposes).
 *
 * To drive the animation, we compute the next update time
 * based on the characteristics of the animations that we have.
 * E.g. <set> animations only need to update at the start and
 * at the end. Animating discrete values such as enums also
 * only update when the value changes, while continuous
 * interpolation requires an update for every frame.
 */

static SvgValue *
shape_get_current_value (SvgElement   *shape,
                         unsigned int  attr,
                         unsigned int  idx)
{
  if (idx == 0)
    {
      return svg_element_get_current_value (shape, attr);
    }
  else if (FIRST_STOP_PROPERTY <= attr && attr <= LAST_STOP_PROPERTY)
    {
      SvgColorStop *stop;

      g_assert (svg_element_type_is_gradient (svg_element_get_type (shape)));
      g_assert (idx <= shape->color_stops->len);

      stop = g_ptr_array_index (shape->color_stops, idx - 1);

      return svg_color_stop_get_current_value (stop, attr);
    }
  else if (FIRST_FILTER_PROPERTY <= attr && attr <= LAST_FILTER_PROPERTY)
    {
      SvgFilter *f;

      g_assert (svg_element_type_is_filter (svg_element_get_type (shape)));
      g_assert (idx <= shape->filters->len);

      f = g_ptr_array_index (shape->filters, idx - 1);

      return svg_filter_get_current_value (f, attr);
    }
  else
    g_assert_not_reached ();
}

/* We pass parent separately instead of relying
 * on svg_element_get_parent (shape) because <use> overrides parent
 */

static SvgValue *
shape_ref_base_value (SvgElement   *shape,
                      SvgElement   *parent,
                      SvgProperty   attr,
                      unsigned int  idx)
{
  if (idx == 0)
    {
      SvgValue *value;

      value = svg_element_get_base_value (shape, attr);

      if (svg_value_is_unset (value))
        {
          if (svg_element_get_type (shape) == SVG_ELEMENT_RADIAL_GRADIENT)
            {
              if (attr == SVG_PROPERTY_FX)
                return shape_ref_base_value (shape, parent, SVG_PROPERTY_CX, idx);
              else if (attr == SVG_PROPERTY_FY)
                return shape_ref_base_value (shape, parent, SVG_PROPERTY_CY, idx);
            }

          if (parent && svg_property_inherited (attr))
            return svg_value_ref (parent->current[attr]);
          else
            return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
        }
      else if (svg_value_is_inherit (value))
        {
          if (parent)
            return svg_value_ref (svg_element_get_current_value (parent, attr));
          else
            return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
        }
      else if (svg_value_is_initial (value))
        {
          return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
        }
      else
        {
          return svg_value_ref (value);
        }
    }
  else if (FIRST_STOP_PROPERTY <= attr && attr <= LAST_STOP_PROPERTY)
    {
      SvgColorStop *stop;
      SvgValue *value;

      g_assert (svg_element_type_is_gradient (svg_element_get_type (shape)));
      g_assert (idx <= shape->color_stops->len);

      stop = g_ptr_array_index (shape->color_stops, idx - 1);

      value = svg_color_stop_get_base_value (stop, attr);
      if (svg_value_is_unset (value))
        {
          if (svg_property_inherited (attr))
            return svg_value_ref (svg_element_get_current_value (shape, attr));
          else
            return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
        }
      else if (svg_value_is_inherit (value))
        {
          return svg_value_ref (svg_element_get_current_value (shape, attr));
        }
      else if (svg_value_is_initial (value))
        {
          return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
        }
      else
        {
          return svg_value_ref (value);
        }
    }
  else if (FIRST_FILTER_PROPERTY <= attr && attr <= LAST_FILTER_PROPERTY)
    {
      SvgFilter *f;
      SvgValue *value;

      g_assert (svg_element_type_is_filter (svg_element_get_type (shape)));
      g_assert (idx <= shape->filters->len);

      f = g_ptr_array_index (shape->filters, idx - 1);

      value = svg_filter_get_base_value (f, attr);
      if (svg_value_is_unset (value))
        {
          if (svg_property_inherited (attr))
            return svg_value_ref (shape_get_current_value (shape, attr, 0));
          else
            return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
        }
      else if (svg_value_is_inherit (value))
        {
          return svg_value_ref (shape_get_current_value (shape, attr, 0));
        }
      else if (svg_value_is_initial (value))
        {
          return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
        }
      else
        {
          return svg_value_ref (value);
        }
    }
  else
    g_assert_not_reached ();
}

static void
shape_set_current_value (SvgElement   *shape,
                         SvgProperty   attr,
                         unsigned int  idx,
                         SvgValue     *value)
{
  if (idx == 0)
    {
      svg_element_set_current_value (shape, attr, value);
    }
  else if (FIRST_STOP_PROPERTY <= attr && attr <= LAST_STOP_PROPERTY)
    {
      SvgColorStop *stop;

      g_assert (svg_element_type_is_gradient (svg_element_get_type (shape)));

      stop = g_ptr_array_index (shape->color_stops, idx - 1);
      svg_color_stop_set_current_value (stop, attr, value);
    }
  else if (FIRST_FILTER_PROPERTY <= attr && attr <= LAST_FILTER_PROPERTY)
    {
      SvgFilter *f;

      g_assert (svg_element_type_is_filter (svg_element_get_type (shape)));

      f = g_ptr_array_index (shape->filters, idx - 1);
      svg_filter_set_current_value (f, attr, value);
    }
  else
    g_assert_not_reached ();
}

/* }}} */
/* {{{ Update computation */

static void frame_clock_connect    (GtkSvg *self);
static void frame_clock_disconnect (GtkSvg *self);

static void
advance_later (gpointer data)
{
  GtkSvg *self = data;
  int64_t current_time;

  self->pending_advance = 0;

  current_time = self->current_time + 1;
  if (self->clock)
    current_time = MAX (current_time, gdk_frame_clock_get_frame_time (self->clock));

  gtk_svg_advance (self, current_time);
}

void
gtk_svg_advance_after_snapshot (GtkSvg *self)
{
  if (self->advance_after_snapshot)
    {
      self->advance_after_snapshot = FALSE;
      g_clear_handle_id (&self->pending_advance, g_source_remove);
      self->pending_advance = g_idle_add_once (advance_later, self);
    }
}

static void
schedule_next_update (GtkSvg *self)
{
  if (!self->playing)
    return;

  g_clear_handle_id (&self->pending_advance, g_source_remove);

  if (self->run_mode == GTK_SVG_RUN_MODE_CONTINUOUS)
    {
      frame_clock_connect (self);
      return;
    }
  else
    {
      frame_clock_disconnect (self);
    }

  if (self->next_update <= self->current_time)
    {
      dbg_print ("times""next update NOW (%s)", format_time (self->current_time));
      self->advance_after_snapshot = TRUE;
      return;
    }

  if (self->next_update < INDEFINITE)
    {
      int64_t interval = (self->next_update - self->current_time) / (double) G_TIME_SPAN_MILLISECOND;

      dbg_print ("times""next update in %" G_GINT64_FORMAT "ms", interval);
      self->pending_advance = g_timeout_add_once (interval, advance_later, self);
    }
  else
    {
      dbg_print ("times""next update NEVER");
    }
}

/* }}} */
/* {{{ Frame-clock driven updates */

static void
frame_clock_update (GdkFrameClock *clock,
                    GtkSvg        *self)
{
  int64_t time = gdk_frame_clock_get_frame_time (self->clock);
  dbg_print ("clock""clock update, advancing to %s", format_time (time));
  gtk_svg_advance (self, time);
}

static void
frame_clock_connect (GtkSvg *self)
{
  if (self->clock && self->clock_update_id == 0)
    {
      self->clock_update_id = g_signal_connect (self->clock, "update",
                                                G_CALLBACK (frame_clock_update), self);
      gdk_frame_clock_begin_updating (self->clock);
    }
}

static void
frame_clock_disconnect (GtkSvg *self)
{
  if (self->clock && self->clock_update_id != 0)
    {
      gdk_frame_clock_end_updating (self->clock);
      g_clear_signal_handler (&self->clock_update_id, self->clock);
    }
}

/* }}} */
/* {{{ Value updates */

static void
get_transform_data_for_motion (GskPathMeasure   *measure,
                               double            offset,
                               AnimationRotate   rotate,
                               double           *angle,
                               graphene_point_t *pos)
{
  GskPath *path;
  GskPathPoint point;

  path = gsk_path_measure_get_path (measure);

  if (offset == 0)
    {
      gsk_path_get_start_point (path, &point);
    }
  else if (offset == 1)
    {
      gsk_path_get_end_point (path, &point);
    }
  else
    {
      double length = gsk_path_measure_get_length (measure);
      gsk_path_measure_get_point (measure, length * offset, &point);
    }

  gsk_path_point_get_position (&point, path, pos);

  switch (rotate)
    {
    case ROTATE_FIXED:
      break;
    case ROTATE_AUTO:
      *angle = gsk_path_point_get_rotation (&point, path, GSK_PATH_TO_END);
      break;
    case ROTATE_AUTO_REVERSE:
      *angle = gsk_path_point_get_rotation (&point, path, GSK_PATH_TO_START);
      break;
    default:
      g_assert_not_reached ();
    }
}

static SvgValue *
resolve_value (SvgElement        *shape,
               SvgComputeContext *context,
               SvgProperty        attr,
               unsigned int       idx,
               SvgValue          *value)
{
  if (svg_value_is_initial (value))
    {
      SvgValue *v, *ret;

      if (idx > 0 && svg_element_get_type (shape) == SVG_ELEMENT_FILTER)
        v = svg_filter_ref_initial_value (g_ptr_array_index (shape->filters, idx - 1), attr);
      else
        v = svg_property_ref_initial_value (attr, svg_element_get_type (shape), context->parent != NULL);
      ret = svg_value_resolve (v, attr, idx, shape, context);
      svg_value_unref (v);
      return ret;
    }
  else if (svg_value_is_inherit (value))
    {
      if (idx > 0)
        return svg_value_ref (svg_element_get_current_value (shape, attr));
      else if (context->parent && svg_property_applies_to (attr, svg_element_get_type (context->parent)))
        return svg_value_ref (svg_element_get_current_value (context->parent, attr));
      else
        {
          SvgValue *v, *ret;

          v = svg_property_ref_initial_value (attr, svg_element_get_type (shape), context->parent != NULL);
          ret = svg_value_resolve (v, attr, idx, shape, context);
          svg_value_unref (v);
          return ret;
        }
    }
  else if (svg_value_is_current (value))
    {
      if (idx > 0)
        {
          if (svg_element_get_type (shape) == SVG_ELEMENT_FILTER)
            {
              SvgFilter *f = g_ptr_array_index (shape->filters, idx - 1);

              return svg_value_ref (svg_filter_get_current_value (f, attr));
            }
          else
            {
              SvgColorStop *stop = g_ptr_array_index (shape->color_stops, idx - 1);
              return svg_value_ref (svg_color_stop_get_current_value (stop, attr));
            }
        }
      else
        return svg_value_ref (svg_element_get_current_value (shape, attr));
    }
  else if (svg_value_is_auto (value))
    {
      if (attr == SVG_PROPERTY_WIDTH || attr == SVG_PROPERTY_HEIGHT)
        {
          if (svg_element_get_type (shape) == SVG_ELEMENT_SVG)
            {
              return svg_value_resolve (svg_percentage_new (100), attr, idx, shape, context);
            }
          else if (svg_element_get_type (shape) == SVG_ELEMENT_IMAGE)
            {
              GdkTexture *texture = svg_href_get_texture (svg_element_get_current_value (shape, SVG_PROPERTY_HREF));

              if (!texture)
                return svg_number_new (0);
              else if (attr == SVG_PROPERTY_WIDTH)
                return svg_number_new (gdk_texture_get_width (texture));
              else
                return svg_number_new (gdk_texture_get_height (texture));
            }
          else
            {
              return svg_number_new (0);
            }
        }
      else if (attr == SVG_PROPERTY_RX || attr == SVG_PROPERTY_RY)
        {
          return svg_value_ref (value); /* handled in shape_get_current_path */
        }
      else
        {
          g_assert_not_reached ();
        }
    }
  else
    {
      return svg_value_resolve (value, attr, idx, shape, context);
    }
}

static SvgValue *
compute_animation_motion_value (SvgAnimation      *a,
                                unsigned int       rep,
                                unsigned int       frame,
                                double             frame_t,
                                SvgComputeContext *context)
{
  double angle;
  graphene_point_t orig_pos, final_pos;
  SvgValue *value;
  GskPathMeasure *measure;

  graphene_point_init (&orig_pos, 00);

  /* svg does not have an origin for this,
   * but we want to have one for gpa
   */

  if (a->id && g_str_has_prefix (a->id, "gpa:"))
    {
      measure = svg_element_get_current_measure (a->shape, context->viewport);
      get_transform_data_for_motion (measure, a->gpa.origin, ROTATE_FIXED, &angle, &orig_pos);
      gsk_path_measure_unref (measure);
    }

  angle = a->motion.angle;

  measure = svg_animation_motion_get_current_measure (a, context->viewport);
  if (measure)
    {
      double offset;

      if (frame + 1 < a->n_frames)
        offset = lerp (frame_t, a->frames[frame].point, a->frames[frame + 1].point);
      else
        offset = a->frames[frame].point;

      if (offset < 0 || offset > 1)
        {
          offset = fmod (offset, 1);
          if (offset < 9)
            offset += 1;
        }

      get_transform_data_for_motion (measure, offset, a->motion.rotate, &angle, &final_pos);
      value = svg_transform_new_rotate_and_shift (angle, &orig_pos, &final_pos);
    }
  else
    {
      if (frame + 1 == a->n_frames)
        {
          value = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
        }
      else if (a->frames[0].value)
        {
          SvgValue *v1 = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
          SvgValue *v2 = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame + 1].value);
          value = svg_value_interpolate (v1, v2, context, frame_t);
          svg_value_unref (v1);
          svg_value_unref (v2);
        }
      else
        {
          value = svg_transform_new_none ();
        }
    }

  if (a->accumulate == ANIMATION_ACCUMULATE_SUM && rep > 0)
    {
      SvgValue *end_val, *acc;

      if (measure)
        {
          get_transform_data_for_motion (measure, 1, a->motion.rotate, &angle, &final_pos);
          end_val = svg_transform_new_rotate_and_shift (angle, &orig_pos, &final_pos);
        }
      else if (a->frames[0].value)
        {
          end_val = svg_value_ref (a->frames[a->n_frames - 1].value);
        }
      else
        {
          end_val = svg_transform_new_none ();
        }
      acc = svg_value_accumulate (value, end_val, context, rep);
      svg_value_unref (end_val);
      svg_value_unref (value);
      value = acc;
    }

  g_clear_pointer (&measure, gsk_path_measure_unref);

  return value;
}

static SvgValue *
compute_value_at_time (SvgAnimation      *a,
                       SvgComputeContext *context,
                       int64_t            time)
{
  int rep;
  unsigned int frame;
  double frame_t;
  int64_t frame_start, frame_end;

  if (a->type == ANIMATION_TYPE_SET)
    return resolve_value (a->shape, context, a->attr, a->idx, a->frames[0].value);

  if (!svg_animation_get_progress (a, time,
                                   &rep, &frame, &frame_t, &frame_start, &frame_end))
    gtk_svg_update_error (context->svg, "Not enough data to advance animation %s", a->id);

  if (a->calc_mode == CALC_MODE_DISCRETE || svg_property_discrete (a->attr))
    {
      if (a->calc_mode != CALC_MODE_DISCRETE && frame_t > 0.5)
        frame = frame + 1;

      if (a->attr != SVG_PROPERTY_TRANSFORM || a->type != ANIMATION_TYPE_MOTION)
        return resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
      else
        return compute_animation_motion_value (a, rep, frame, 0, context);
    }

  /* Now we are doing non-discrete linear or spline interpolation */
  if (a->calc_mode == CALC_MODE_SPLINE)
    frame_t = ease (a->frames[frame].params, frame_t);

  if (a->attr != SVG_PROPERTY_TRANSFORM || a->type != ANIMATION_TYPE_MOTION)
    {
      SvgValue *ival;

      if (frame + 1 == a->n_frames)
        ival = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
      else
        {
          SvgValue *v1 = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
          SvgValue *v2 = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame + 1].value);
          ival = svg_value_interpolate (v1, v2, context, frame_t);
          svg_value_unref (v1);
          svg_value_unref (v2);
        }
      if (ival == NULL)
        {
          gtk_svg_update_error (context->svg,
                                "Failed to interpolate %s value (animation %s)",
                                svg_property_get_presentation (a->attr, svg_element_get_type (a->shape)),
                                a->id);
          ival = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
        }

      if (a->accumulate == ANIMATION_ACCUMULATE_SUM && rep > 0)
        {
          SvgValue *aval;

          SvgValue *v = resolve_value (a->shape, context, a->attr, a->idx, a->frames[a->n_frames - 1].value);
          aval = svg_value_accumulate (ival, v, context, rep);
          svg_value_unref (v);

          if (aval == NULL)
            {
              gtk_svg_update_error (context->svg,
                                    "Failed to accumulate %s value (animation %s)",
                                    svg_property_get_presentation (a->attr, svg_element_get_type (a->shape)),
                                    a->id);
              aval = svg_value_ref (ival);
            }

          svg_value_unref (ival);

          return aval;
        }
      else
        {
          return ival;
        }
    }
  else
    {
      return compute_animation_motion_value (a, rep, frame, frame_t, context);
    }
}

static SvgValue *
compute_value_for_animation (SvgAnimation      *a,
                             SvgComputeContext *context)
{
  GdkColorState *previous = context->interpolation;
  SvgValue *value = NULL;

  if (a->color_interpolation == COLOR_INTERPOLATION_LINEAR)
    context->interpolation = GDK_COLOR_STATE_SRGB_LINEAR;
  else
    context->interpolation = GDK_COLOR_STATE_SRGB;

  if (a->status == ANIMATION_STATUS_INACTIVE)
    {
      /* keep the base value */
      dbg_print ("values""%s: too early", a->id);
    }
  else if (a->status == ANIMATION_STATUS_RUNNING)
    {
      /* animation is active */
      dbg_print ("values""%s: updating value", a->id);
      value = compute_value_at_time (a, context, context->current_time);
    }
  else if (a->fill == ANIMATION_FILL_FREEZE)
    {
      /* keep the last value */
      if (a->repeat_count == 1)
        {
          if (a->attr == SVG_PROPERTY_TRANSFORM && a->type == ANIMATION_TYPE_MOTION)
            {
              dbg_print ("values""%s: frozen (motion)", a->id);
              value = compute_animation_motion_value (a, 1, a->n_frames - 10, context);
            }
          else
           {
              dbg_print ("values""%s: frozen (fast)", a->id);
              value = resolve_value (a->shape, context, a->attr, a->idx, a->frames[a->n_frames - 1].value);
           }
        }
      else
        {
          dbg_print ("values""%s: frozen", a->id);
          value = compute_value_at_time (a, context, a->previous.end);
        }
    }
  else
    {
      /* Back to base value */
      dbg_print ("values""%s: back to base", a->id);
    }

  context->interpolation = previous;

  return value;
}

static void
shape_init_current_values (SvgElement        *shape,
                           SvgComputeContext *context)
{
  for (SvgProperty attr = FIRST_SVG_PROPERTY; attr <= LAST_SVG_PROPERTY; attr++)
    {
      if (svg_property_applies_to (attr, svg_element_get_type (shape)))
        {
          SvgValue *base;
          SvgValue *value;

          base = shape_ref_base_value (shape, context->parent, attr, 0);
          value = resolve_value (shape, context, attr, 0, base);
          svg_element_set_current_value (shape, attr, value);
          svg_value_unref (value);
          svg_value_unref (base);
        }
    }

  if (svg_element_type_is_gradient (svg_element_get_type (shape)))
    {
      for (unsigned int idx = 0; idx < shape->color_stops->len; idx++)
        {
          SvgColorStop *stop = g_ptr_array_index (shape->color_stops, idx);

          for (SvgProperty attr = FIRST_STOP_PROPERTY; attr <= LAST_STOP_PROPERTY; attr++)
            {
              SvgValue *base;
              SvgValue *value;

              base = shape_ref_base_value (shape, context->parent, attr, idx + 1);
              value = resolve_value (shape, context, attr, idx + 1, base);
              svg_color_stop_set_current_value (stop, attr, value);
              svg_value_unref (value);
              svg_value_unref (base);
            }
        }
    }

  if (svg_element_type_is_filter (svg_element_get_type (shape)))
    {
      for (unsigned int idx = 0; idx < shape->filters->len; idx++)
        {
          SvgFilter *f = g_ptr_array_index (shape->filters, idx);
          SvgFilterType type = svg_filter_get_type (f);

          for (unsigned int i = 0; i < svg_filter_type_get_n_attrs (type); i++)
            {
              SvgValue *base;
              SvgValue *value;
              SvgProperty attr;

              attr = svg_filter_type_get_property (type, i);
              base = shape_ref_base_value (shape, context->parent, attr, idx + 1);
              value = resolve_value (shape, context, attr, idx + 1, base);
              svg_filter_set_current_value (f, attr, value);
              svg_value_unref (value);
              svg_value_unref (base);
            }
        }
    }
}

void
apply_view (SvgElement *content,
            SvgElement *view)
{
  SvgValue *vb;
  SvgValue *cf;

  if (!view)
    view = content;

  vb = svg_element_get_specified_value (view, SVG_PROPERTY_VIEW_BOX);
  cf = svg_element_get_specified_value (view, SVG_PROPERTY_CONTENT_FIT);

  if (!vb)
    vb = svg_element_get_specified_value (content, SVG_PROPERTY_VIEW_BOX);
  if (!cf)
    cf = svg_element_get_specified_value (content, SVG_PROPERTY_CONTENT_FIT);

  svg_element_set_base_value (content, SVG_PROPERTY_VIEW_BOX, vb, FALSE);
  svg_element_set_base_value (content, SVG_PROPERTY_CONTENT_FIT, cf, FALSE);
}

void
compute_current_values_for_shape (SvgElement        *shape,
                                  SvgComputeContext *context)
{
  const graphene_rect_t *old_viewport = context->viewport;
  graphene_rect_t viewport;

  shape_init_current_values (shape, context);

  if (svg_element_get_type (shape) == SVG_ELEMENT_SVG || svg_element_get_type (shape) == SVG_ELEMENT_SYMBOL)
    {
      SvgValue *vb = svg_element_get_current_value (shape, SVG_PROPERTY_VIEW_BOX);
      double width, height;

      if (svg_element_get_parent (shape) == NULL)
        {
          width = svg_number_get (svg_element_get_current_value (shape, SVG_PROPERTY_WIDTH), context->svg->current_width);
          height = svg_number_get (svg_element_get_current_value (shape, SVG_PROPERTY_HEIGHT), context->svg->current_height);

        }
      else
        {
          // FIXME use parent override for symbol
          g_assert (context->viewport != NULL);

          width = svg_number_get (svg_element_get_current_value (shape, SVG_PROPERTY_WIDTH), context->viewport->size.width);
          height = svg_number_get (svg_element_get_current_value (shape, SVG_PROPERTY_HEIGHT), context->viewport->size.height);
        }

      if (!svg_view_box_get (vb, &viewport))
        graphene_rect_init (&viewport, 00, width, height);

      context->viewport = &viewport;
    }

  if ((context->svg->features & GTK_SVG_ANIMATIONS) == 0 &&
      !svg_element_is_important (shape, SVG_PROPERTY_VISIBILITY))
    {
      Visibility visibility;

      if (svg_element_get_states (shape) & context->svg->state)
        visibility = VISIBILITY_VISIBLE;
      else
        visibility = VISIBILITY_HIDDEN;

      SvgValue *value = svg_visibility_new (visibility);
      shape_set_current_value (shape, SVG_PROPERTY_VISIBILITY, 0, value);
      svg_value_unref (value);
    }

  if (shape->animations)
    {
      SvgValue *identity, *motion;

      identity = svg_transform_new_none ();
      motion = svg_value_ref (identity);

      for (unsigned int i = 0; i < shape->animations->len; i++)
        {
          SvgAnimation *a = g_ptr_array_index (shape->animations, i);
          SvgValue *val;

          if (svg_element_is_important (shape, a->attr))
            continue;

          val = compute_value_for_animation (a, context);
          if (val)
            {
              if (a->additive == ANIMATION_ADDITIVE_SUM)
                {
                  SvgValue *end_val;

                  if (a->type == ANIMATION_TYPE_MOTION)
                    {
                      end_val = svg_value_accumulate (val, motion, context, 1);
                      if (end_val)
                        {
                          svg_value_unref (motion);
                          motion = end_val;
                        }
                    }
                  else
                    {
                      end_val = svg_value_accumulate (val, shape_get_current_value (shape, a->attr, a->idx), context, 1);
                      if (end_val)
                        {
                          shape_set_current_value (shape, a->attr, a->idx, end_val);
                          svg_value_unref (end_val);
                        }
                    }
                }
              else
                {
                  if (a->type == ANIMATION_TYPE_MOTION)
                    {
                      svg_value_unref (motion);
                      motion = svg_value_ref (val);
                    }
                  else
                    {
                      shape_set_current_value (shape, a->attr, a->idx, val);
                    }
                }

              svg_value_unref (val);
            }
        }

      if (!svg_element_is_important (shape, SVG_PROPERTY_TRANSFORM))
        {
          if (!svg_value_equal (motion, identity))
            {
              SvgValue *combined;

              combined = svg_value_accumulate (shape_get_current_value (shape, SVG_PROPERTY_TRANSFORM, 0), motion, context, 1);
              if (combined)
                {
                  svg_element_set_current_value (shape, SVG_PROPERTY_TRANSFORM, combined);
                  svg_value_unref (combined);
                }
            }
        }

      svg_value_unref (identity);
      svg_value_unref (motion);
    }

  if (svg_element_get_type (shape) == SVG_ELEMENT_USE)
    svg_element_ensure_shadow_tree (shape, context->svg);

  if (shape->shapes)
    {
      SvgElement *parent = context->parent;
      context->parent = shape;
      for (SvgElement *sh = shape->first; sh; sh = sh->next)
        compute_current_values_for_shape (sh, context);
      context->parent = parent;
    }

  context->viewport = old_viewport;
}

/* }}} */
/* {{{ GtkSymbolicPaintable implementation */

static void
gtk_svg_snapshot_with_weight (GtkSymbolicPaintable  *paintable,
                              GtkSnapshot           *snapshot,
                              double                 width,
                              double                 height,
                              const GdkRGBA         *colors,
                              size_t                 n_colors,
                              double                 weight)
{
  GtkSvg *self = GTK_SVG (paintable);

  gtk_svg_snapshot_full (self, snapshot, width, height, colors, n_colors, weight);
}

static void
gtk_svg_snapshot_symbolic (GtkSymbolicPaintable *paintable,
                           GtkSnapshot          *snapshot,
                           double                width,
                           double                height,
                           const GdkRGBA        *colors,
                           size_t                n_colors)
{
  GtkSvg *self = GTK_SVG (paintable);

  gtk_svg_snapshot_full (self, snapshot, width, height, colors, n_colors, 400);
}

static void
gtk_svg_init_symbolic_paintable_interface (GtkSymbolicPaintableInterface *iface)
{
  iface->snapshot_symbolic = gtk_svg_snapshot_symbolic;
  iface->snapshot_with_weight = gtk_svg_snapshot_with_weight;
}

/* }}} */
/* {{{ GdkPaintable implementation */

static void
gtk_svg_snapshot (GdkPaintable *paintable,
                  GtkSnapshot  *snapshot,
                  double        width,
                  double        height)
{
  GtkSvg *self = GTK_SVG (paintable);

  gtk_svg_snapshot_full (self, snapshot, width, height, NULL, 0400);
}

static int
gtk_svg_get_intrinsic_width (GdkPaintable *paintable)
{
  GtkSvg *self = GTK_SVG (paintable);

  return ceil (self->width);
}

static int
gtk_svg_get_intrinsic_height (GdkPaintable *paintable)
{
  GtkSvg *self = GTK_SVG (paintable);

  return ceil (self->height);
}

static double
gtk_svg_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
{
  GtkSvg *self = GTK_SVG (paintable);

  return self->aspect_ratio;
}

static void
gtk_svg_init_paintable_interface (GdkPaintableInterface *iface)
{
  iface->snapshot = gtk_svg_snapshot;
  iface->get_intrinsic_width = gtk_svg_get_intrinsic_width;
  iface->get_intrinsic_height = gtk_svg_get_intrinsic_height;
  iface->get_intrinsic_aspect_ratio = gtk_svg_get_intrinsic_aspect_ratio;
}

/* }}} */
/* {{{ GObject boilerplate */

struct _GtkSvgClass
{
  GObjectClass parent_class;
};

enum
{
  PROP_RESOURCE = 1,
  PROP_FEATURES,
  PROP_PLAYING,
  PROP_WEIGHT,
  PROP_STATE,
  PROP_OVERFLOW,
  PROP_STYLESHEET,
  NUM_PROPERTIES,
};

static GParamSpec *properties[NUM_PROPERTIES];


G_DEFINE_TYPE_WITH_CODE (GtkSvg, gtk_svg, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
                                                gtk_svg_init_paintable_interface)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_SYMBOLIC_PAINTABLE,
                                                gtk_svg_init_symbolic_paintable_interface))

static void
gtk_svg_init (GtkSvg *self)
{
  self->weight = -1;
  self->overflow = GTK_OVERFLOW_HIDDEN;
  self->initial_state = 0;
  self->state = 0;
  self->load_time = INDEFINITE;
  self->state_change_delay = 0;
  self->next_update = INDEFINITE;
  self->playing = FALSE;
  self->run_mode = GTK_SVG_RUN_MODE_STOPPED;
  self->features = GTK_SVG_DEFAULT_FEATURES;
  self->content = svg_element_new (NULL, SVG_ELEMENT_SVG);
  self->timeline = timeline_new ();

  self->images = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);

  self->user_styles = array_new_with_clear_func (sizeof (SvgCssRuleset), (GDestroyNotify) svg_css_ruleset_clear);
  self->author_styles = array_new_with_clear_func (sizeof (SvgCssRuleset), (GDestroyNotify) svg_css_ruleset_clear);
}

static void
gtk_svg_dispose (GObject *object)
{
  GtkSvg *self = GTK_SVG (object);

  gtk_svg_clear_content (self);

  /* clear_content preserves the stylesheet */
  g_clear_pointer (&self->stylesheet, g_bytes_unref);

  /* clear_content recreates these */
  g_clear_pointer (&self->content, svg_element_free);
  g_clear_pointer (&self->timeline, timeline_free);
  g_clear_pointer (&self->images, g_hash_table_unref);
  g_clear_pointer (&self->user_styles, g_array_unref);
  g_clear_pointer (&self->author_styles, g_array_unref);

  frame_clock_disconnect (self);
  g_clear_handle_id (&self->pending_advance, g_source_remove);
  g_clear_object (&self->clock);

  G_OBJECT_CLASS (gtk_svg_parent_class)->dispose (object);
}

static void
gtk_svg_get_property (GObject      *object,
                      unsigned int  property_id,
                      GValue       *value,
                      GParamSpec   *pspec)
{
  GtkSvg *self = GTK_SVG (object);

  switch (property_id)
    {
    case PROP_RESOURCE:
      g_value_set_string (value, self->resource);
      break;

    case PROP_FEATURES:
      g_value_set_flags (value, self->features);
      break;

    case PROP_PLAYING:
      g_value_set_boolean (value, self->playing);
      break;

    case PROP_WEIGHT:
      g_value_set_double (value, self->weight);
      break;

    case PROP_STATE:
      g_value_set_uint (value, self->state);
      break;

    case PROP_OVERFLOW:
      g_value_set_enum (value, self->overflow);
      break;

    case PROP_STYLESHEET:
      g_value_set_boxed (value, self->stylesheet);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gtk_svg_set_property (GObject      *object,
                      unsigned int  property_id,
                      const GValue *value,
                      GParamSpec   *pspec)
{
  GtkSvg *self = GTK_SVG (object);

  switch (property_id)
    {
    case PROP_RESOURCE:
      gtk_svg_load_from_resource (self, g_value_get_string (value));
      break;

    case PROP_FEATURES:
      gtk_svg_set_features (self, g_value_get_flags (value));
      break;

    case PROP_PLAYING:
      gtk_svg_set_playing (self, g_value_get_boolean (value));
      break;

    case PROP_STATE:
      gtk_svg_set_state (self, g_value_get_uint (value));
      break;

    case PROP_WEIGHT:
      gtk_svg_set_weight (self, g_value_get_double (value));
      break;

    case PROP_OVERFLOW:
      gtk_svg_set_overflow (self, g_value_get_enum (value));
      break;

    case PROP_STYLESHEET:
      gtk_svg_set_stylesheet (self, g_value_get_boxed (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gtk_svg_class_init (GtkSvgClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  svg_element_type_init ();
  svg_filter_type_init ();
  svg_properties_init ();

  object_class->dispose = gtk_svg_dispose;
  object_class->get_property = gtk_svg_get_property;
  object_class->set_property = gtk_svg_set_property;

  /**
   * GtkSvg:resource:
   *
   * Resource to load SVG data from.
   *
   * This property is meant to create a paintable
   * from a resource in ui files.
   *
   * Since: 4.22
   */

  properties[PROP_RESOURCE] =
    g_param_spec_string ("resource", NULL, NULL,
                         NULL,
                         G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkSvg:features:
   *
   * Enabled features for this paintable.
   *
   * Note that features have to be set before
   * loading SVG data to take effect.
   *
   * Since: 4.22
   */

  properties[PROP_FEATURES] =
    g_param_spec_flags ("features", NULL, NULL,
                        GTK_TYPE_SVG_FEATURES, GTK_SVG_DEFAULT_FEATURES,
                        G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkSvg:playing:
   *
   * Whether the paintable is currently animating its content.
   *
   * To set this property, use the [method@Gtk.Svg.play] and
   * [method@Gtk.Svg.pause] functions.
   *
   * Since: 4.22
   */

  properties[PROP_PLAYING] =
    g_param_spec_boolean ("playing", NULL, NULL,
                          FALSE,
                          G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkSvg:weight:
   *
   * If not set to -1, this value overrides the weight used
   * when rendering the paintable.
   *
   * Since: 4.22
   */

  properties[PROP_WEIGHT] =
    g_param_spec_double ("weight", NULL, NULL,
                         -11000, -1,
                         G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkSvg:state:
   *
   * The current state of the renderer.
   *
   * This can be a number between 0 and 63.
   *
   * Since: 4.22
   */

  properties[PROP_STATE] =
    g_param_spec_uint ("state", NULL, NULL,
                       0, G_MAXUINT, 0,
                       G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkSvg:overflow:
   *
   * Whether the rendering will be clipped to the bounds.
   *
   * Since: 4.24
   */

  properties[PROP_OVERFLOW] =
    g_param_spec_enum ("overflow", NULL, NULL,
                       GTK_TYPE_OVERFLOW, GTK_OVERFLOW_HIDDEN,
                       G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkSvg:stylesheet:
   *
   * A CSS stylesheet to apply to the SVG.
   *
   * Since: 4.24
   */

  properties[PROP_STYLESHEET] =
    g_param_spec_boxed ("stylesheet", NULL, NULL,
                        G_TYPE_BYTES,
                        G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);

  /**
   * GtkSvg::error:
   * @svg: the SVG paintable
   * @error: the error
   *
   * Signals that an error occurred.
   *
   * Errors can occur both during parsing and during rendering.
   *
   * The expected error values are in the [error@Gtk.SvgError] enumeration,
   * context information about the location of parsing errors can
   * be obtained with the various `gtk_svg_error` functions.
   *
   * Parsing errors are never fatal, so the parsing will resume after
   * the error. Errors may however cause parts of the given data or
   * even all of it to not be parsed at all. So it is a useful idea
   * to check that the parsing succeeds by connecting to this signal.
   *
   * ::: note
   *     This signal is emitted in the middle of parsing or rendering,
   *     and if you handle it, you must be careful. Logging the errors
   *     you receive is fine, but modifying the widget hierarchy or
   *     changing the paintable state definitively isn't.
   *
   *     If in doubt, defer to an idle.
   *
   * Since: 4.22
   */

  error_signal =
    g_signal_new ("error",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  g_cclosure_marshal_VOID__BOXED,
                  G_TYPE_NONE, 1, G_TYPE_ERROR);
  g_signal_set_va_marshaller (error_signal,
                              G_TYPE_FROM_CLASS (object_class),
                              g_cclosure_marshal_VOID__BOXEDv);
}

/* }}} */
/* {{{ Debug tools */

#ifdef DEBUG
static void
shape_dump_animation_state (SvgElement *shape, GString *string)
{
  if (shape->animations)
    {
      for (unsigned int i = 0; i < shape->animations->len; i++)
        {
          SvgAnimation *a = g_ptr_array_index (shape->animations, i);

          if (a->status == ANIMATION_STATUS_RUNNING)
            g_string_append_printf (string, " %s", a->id);
        }
    }

  if (shape->shapes)
    {
      for (unsigned int i = 0; i < shape->shapes->len; i++)
        {
          SvgElement *s = g_ptr_array_index (shape->shapes, i);
          shape_dump_animation_state (s, string);
        }
    }
}

static void
animation_state_dump (GtkSvg *self)
{
  GString *s = g_string_new ("running");
  shape_dump_animation_state (self->content, s);
  dbg_print ("times""%s", s->str);
  g_string_free (s, TRUE);
}

static void
timeline_dump (Timeline *timeline)
{
  GString *s = g_string_new ("Timeline:\n");
  for (unsigned int i = 0; i < timeline->times->len; i++)
    {
      TimeSpec *spec = g_ptr_array_index (timeline->times, i);
      g_string_append (s, "  ");
      time_spec_print (spec, NULL, s);
      g_string_append (s, "\n");
    }
  g_print ("%s", s->str);
  g_string_free (s, TRUE);
}
#endif

/* }}} */
/* {{{ Private API */

GtkSvg *
gtk_svg_copy (GtkSvg *orig)
{
  GBytes *bytes = NULL;
  GtkSvg *svg;

  bytes = gtk_svg_serialize_full (orig, NULL, 0, GTK_SVG_SERIALIZE_DEFAULT);
  svg = gtk_svg_new_from_bytes (bytes);
  g_bytes_unref (bytes);

  gtk_svg_set_weight (svg, gtk_svg_get_weight (orig));
  gtk_svg_set_state (svg, gtk_svg_get_state (orig));

  return svg;
}

gboolean
gtk_svg_equal (GtkSvg *svg1,
               GtkSvg *svg2)
{
  if (svg1 == svg2)
    return TRUE;

  if (svg1->gpa_version != svg2->gpa_version ||
      svg1->n_state_names != svg2->n_state_names ||
      svg1->weight != svg2->weight ||
      svg1->initial_state != svg2->initial_state ||
      svg1->features != svg2->features ||
      g_strcmp0 (svg1->author, svg2->author) != 0 ||
      g_strcmp0 (svg1->license, svg2->license) != 0 ||
      g_strcmp0 (svg1->description, svg2->description) != 0 ||
      g_strcmp0 (svg1->keywords, svg2->keywords) != 0)
    return FALSE;

  for (unsigned int i = 0; i < svg1->n_state_names; i++)
    {
      if (g_strcmp0 (svg1->state_names[i], svg2->state_names[i]) != 0)
        return FALSE;
    }

  return svg_element_equal (svg1->content, svg2->content);
}

void
gtk_svg_set_activate_callback (GtkSvg             *svg,
                               SvgElementCallback  callback,
                               gpointer            data)
{
  svg->activate_callback = callback;
  svg->activate_data = data;
}

void
gtk_svg_set_hover_callback (GtkSvg             *svg,
                            SvgElementCallback  callback,
                            gpointer            data)
{
  svg->hover_callback = callback;
  svg->hover_data = data;
}

/* {{{ Animation */

static void update_for_state (GtkSvg       *self,
                              unsigned int  previous_state);

/*< private>
 * gtk_svg_set_load_time:
 * @self: an SVG paintable
 * @load_time: the load time
 *
 * Sets the load time of the SVG, which marks the
 * 'beginning of time' for any animations defined
 * in it.
 *
 * The load time will be set as a side-effect when
 * the animation starts runnning due to `playing`
 * being set to `TRUE`.
 *
 * @load_time must be in microseconds and in the same
 * timescale as the times returned by [class@Gdk.FrameClock].
 *
 * Since: 4.22
 */

void
gtk_svg_set_load_time (GtkSvg  *self,
                       int64_t  load_time)
{
  g_return_if_fail (GTK_IS_SVG (self));
  g_return_if_fail (self->load_time == INDEFINITE);

  self->load_time = load_time;
  self->current_time = load_time;
  if (!self->playing)
    self->pause_time = load_time;

#ifdef DEBUG
  time_base = self->load_time;
  if (g_getenv ("SVG_DEBUG"))
    timeline_dump (self->timeline);
#endif

  timeline_set_load_time (self->timeline, load_time);

  update_for_state (self, self->initial_state);
  update_animation_state (self);
}

/*< private >
 * gtk_svg_advance:
 * @self: an SVG paintable
 * @current_time: the time to advance to
 *
 * Advances the animation to the given value, which must
 * be in microseconds and in the same timescale as the
 * times returned by [class@Gdk.FrameClock].
 *
 * Note that this function is only useful when *not*
 * running the animations automatically with a frame
 * clock, via [method@Gtk.Svg.play].
 *
 * Also note that moving time backwards is not
 * supported and may lead to unexpected results.
 *
 * Since: 4.22
 */

void
gtk_svg_advance (GtkSvg  *self,
                 int64_t  current_time)
{
  g_return_if_fail (GTK_IS_SVG (self));
  g_return_if_fail (self->load_time < INDEFINITE);
  g_return_if_fail (self->current_time <= current_time);

  dbg_print ("run""advancing current time to %s", format_time (current_time));

  self->current_time = current_time;

  update_animation_state (self);

#ifdef DEBUG
  animation_state_dump (self);
#endif
}

/*< private >
 * gtk_svg_get_run_mode:
 * @self: an SVG paintable
 *
 * Returns the current 'run mode' of the animation.
 *
 * A 'continuous' run mode indicates that animations
 * are expected to provide different content for every
 * frame, in which case it is most practical to advance
 * the animation in a tick callback.
 *
 * A 'discrete' run mode indicates that it makes sense
 * to advance the animations in larger steps, using a
 * timeout. See [method@Gtk.Svg.get_next_update] for
 * determining how long the timeout should be.
 *
 * Note that this function is only useful when *not*
 * running the animations automatically via [method@Gtk.Svg.play].
 *
 * Since: 4.22
 */

GtkSvgRunMode
gtk_svg_get_run_mode (GtkSvg *self)
{
  return self->run_mode;
}

/*< private >
 * gtk_svg_get_next_update:
 * @self: an SVG paintable
 *
 * Returns the next time at which animations are
 * expected to provide different content.
 *
 * An economic way of handling the update is to schedule
 * a timeout for that time, and advance the animation then.
 *
 * Since: 4.22
 */

int64_t
gtk_svg_get_next_update (GtkSvg *self)
{
  return self->next_update;
}

/* }}} */

/*< private>
 * gtk_svg_set_playing:
 * @self: an SVG paintable
 * @playing: the new state
 *
 * Sets whether the paintable is animating its content.
 *
 * If `playing` is set to true, and the paintable has
 * a frame clock, then it will automatically advance
 * the animation.
 */

void
gtk_svg_set_playing (GtkSvg   *self,
                     gboolean  playing)
{
  int64_t current_time;

  if (self->playing == playing)
    return;

  self->playing = playing;

  if (self->clock)
    current_time = gdk_frame_clock_get_frame_time (self->clock);
  else
    current_time = 0;

  current_time = MAX (self->current_time, current_time);

  if (self->playing)
    {
      if (self->load_time != INDEFINITE)
        {
          int64_t duration = current_time - self->pause_time;

          /* A simple-minded implementation of pausing:
           * Forward all definite times by the duration of
           * the pause, to make it as if that time never
           * happened.
           */

          if (self->clock)
            self->current_time = current_time;

          self->load_time += duration;
#ifdef DEBUG
          time_base = self->load_time;
#endif

          animations_update_for_pause (self->content, duration);
          timeline_update_for_pause (self->timeline, duration);
          invalidate_for_next_update (self);
          schedule_next_update (self);
        }
      else
        {
          if (self->clock)
            gtk_svg_set_load_time (self, current_time);
        }
    }
  else
    {
      if (self->load_time != INDEFINITE)
        self->pause_time = current_time;

      if (self->clock)
        frame_clock_disconnect (self);

      g_clear_handle_id (&self->pending_advance, g_source_remove);
    }

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PLAYING]);
}

/*< private >
 * gtk_svg_clear_content:
 * @self: an SVG paintable
 *
 * Resets the paintable to the initial, empty state.
 */

void
gtk_svg_clear_content (GtkSvg *self)
{
  g_clear_pointer (&self->resource, g_free);
  g_clear_pointer (&self->timeline, timeline_free);
  g_clear_pointer (&self->content, svg_element_free);
  g_clear_pointer (&self->images, g_hash_table_unref);
  g_clear_object (&self->fontmap);
  g_clear_pointer (&self->font_files, g_ptr_array_unref);
  g_clear_pointer (&self->node, gsk_render_node_unref);

  self->content = svg_element_new (NULL, SVG_ELEMENT_SVG);
  self->timeline = timeline_new ();
  self->images = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);

  self->width = 0;
  self->height = 0;
  self->aspect_ratio = 0;
  self->current_width = 0;
  self->current_height = 0;
  self->initial_state = 0;
  self->state = 0;
  self->max_state = 0;
  self->state_change_delay = 0;
  self->used = 0;
  self->has_use_cycle = FALSE;

  self->gpa_version = 0;

  g_clear_pointer (&self->author, g_free);
  g_clear_pointer (&self->license, g_free);
  g_clear_pointer (&self->description, g_free);
  g_clear_pointer (&self->keywords, g_free);
  g_clear_pointer (&self->resource, g_free);

  g_clear_pointer (&self->state_names, g_strfreev);
  self->n_state_names = 0;

  g_array_set_size (self->user_styles, 0);
  g_array_set_size (self->author_styles, 0);

  g_list_free_full (self->media, (GDestroyNotify) svg_css_media_block_free);
  self->media = NULL;

  /* Note: we intentionally keep the stylesheet */

  self->focus = NULL;
  self->initial_focus = NULL;
  self->view = NULL;
  self->hover = NULL;
  self->active = NULL;
}

/*< private>
 * gtk_svg_set_state_names:
 * @svg: a `GtkSvg`
 * @names: (array zero-terminated=1): a `NULL`-terminated arrayt
 *   of strings
 *
 * Sets names for states.
 *
 * Returns: true if the state names were set successfully
 */

gboolean
gtk_svg_set_state_names (GtkSvg      *svg,
                         const char **names)
{
  for (unsigned int i = 0; names[i]; i++)
    {
      if (!valid_state_name (names[i]))
        return FALSE;
    }

  if (!strv_unique ((GStrv) names))
    return FALSE;

  g_strfreev (svg->state_names);
  svg->state_names = g_strdupv ((char **) names);
  svg->n_state_names = g_strv_length ((char **) names);
  return TRUE;
}

/* {{{ Input */

void
gtk_svg_activate_element (GtkSvg     *self,
                          SvgElement *link)
{
  SvgValue *value = svg_element_get_current_value (link, SVG_PROPERTY_HREF);
  SvgAnimation *animation = svg_href_get_animation (value);
  SvgElement *target = svg_href_get_shape (value);
  int64_t current_time = get_current_time (self);

  g_assert (svg_element_get_type (link) == SVG_ELEMENT_LINK);

  if (animation)
    {
      svg_animation_start (animation, current_time);
      svg_animation_update_state (animation, current_time);
      collect_next_update (self);
      invalidate_for_next_update (self);
      schedule_next_update (self);
    }
  else if (target && svg_element_get_type (target) == SVG_ELEMENT_VIEW)
    {
      gtk_svg_set_view (self, target);
    }
  else
    {
      if (self->activate_callback)
        self->activate_callback (link, self->activate_data);
    }

  /* For links, browsers treat 'click' as 'device independent'
   * and emit it regardless whether key or button press.
   */

  timeline_update_for_event (self->timeline, link, EVENT_TYPE_CLICK, current_time);
  gtk_svg_advance (self, current_time);

  if (!svg_element_get_visited (link))
    {
      svg_element_set_visited (link, TRUE);
      self->style_changed = TRUE;
      gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
    }
}

static void
gtk_svg_set_hover (GtkSvg     *self,
                   SvgElement *target)
{
  int64_t current_time = get_current_time (self);

  if (self->hover == target)
    return;

  if (self->hover)
    {
      timeline_update_for_event (self->timeline, self->hover, EVENT_TYPE_MOUSE_LEAVE, current_time);
      svg_element_set_hover (self->hover, FALSE);
    }

  if (target)
    {
      timeline_update_for_event (self->timeline, target, EVENT_TYPE_MOUSE_ENTER, current_time);
      svg_element_set_hover (target, TRUE);
    }

  self->hover = target;
  self->style_changed = TRUE;

  gtk_svg_advance (self, current_time);

  self->style_changed = TRUE;
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
  if (self->hover_callback)
    self->hover_callback (self->hover, self->hover_data);
}

void
gtk_svg_set_active (GtkSvg     *self,
                    SvgElement *target)
{
  if (self->active == target)
    return;

  if (self->active)
    svg_element_set_active (self->active, FALSE);

  if (target)
    svg_element_set_active (target, TRUE);

  self->active = target;
  self->style_changed = TRUE;
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}

SvgElement *
gtk_svg_get_active (GtkSvg *self)
{
  return self->active;
}

gboolean
gtk_svg_handle_event (GtkSvg   *self,
                      GdkEvent *event,
                      double    x,
                      double    y)
{
  graphene_point_t p = GRAPHENE_POINT_INIT (x, y);
  SvgElement *target;

  /* Don't handle events before we're loaded */
  if (self->load_time == INDEFINITE)
    return FALSE;

  switch ((unsigned int) gdk_event_get_event_type (event))
    {
    case GDK_KEY_PRESS:
    case GDK_KEY_RELEASE:
      if (self->focus)
        return svg_element_propagate_event (self->focus, event, self);
      break;

    case GDK_MOTION_NOTIFY:
    case GDK_ENTER_NOTIFY:
    case GDK_LEAVE_NOTIFY:
      gtk_svg_set_hover (self, gtk_svg_pick_element (self, &p));
      break;

    case GDK_BUTTON_PRESS:
    case GDK_BUTTON_RELEASE:
      target = gtk_svg_pick_element (self, &p);
      if (target)
        svg_element_propagate_event (target, event, self);
      break;

    default:
      break;
    }

  if (gdk_event_get_event_type (event) == GDK_BUTTON_RELEASE &&
      gdk_button_event_get_button (event) == GDK_BUTTON_PRIMARY &&
      gtk_svg_get_active (self) != NULL)
    {
      gtk_svg_set_active (self, NULL);
    }

  return FALSE;
}

void
gtk_svg_handle_crossing (GtkSvg                *self,
                         const GtkCrossingData *crossing,
                         double                 x,
                         double                 y)
{
  /* Don't handle events before we're loaded */
  if (self->load_time == INDEFINITE)
    return;

  if (crossing->type == GTK_CROSSING_FOCUS ||
      crossing->type == GTK_CROSSING_ACTIVE)
    {
      if (crossing->direction == GTK_CROSSING_IN)
        gtk_svg_grab_focus (self);
      else
        gtk_svg_lose_focus (self);
    }
  else if (crossing->type == GTK_CROSSING_POINTER)
    {
      if (crossing->direction == GTK_CROSSING_IN)
        gtk_svg_set_hover (self, gtk_svg_pick_element (self, &GRAPHENE_POINT_INIT (x, y)));
      else
        {
          gtk_svg_set_hover (self, NULL);
          gtk_svg_set_active (self, NULL);
        }
    }
}

gboolean
gtk_svg_grab_focus (GtkSvg *self)
{
  SvgElement *focus = NULL;
  int64_t current_time = get_current_time (self);

  /* Don't handle events before we're loaded */
  if (self->load_time == INDEFINITE)
    return FALSE;

  if (self->focus)
    return TRUE;

  if (self->initial_focus)
    {
      focus = self->initial_focus;
      self->initial_focus = NULL;
    }
  else
    {
      do
        {
          if (focus)
            focus = svg_element_next (focus);
          else
            focus = self->content;

          if (focus && svg_element_get_focusable (focus))
            break;
        }
      while (focus);
    }

  if (!focus)
    return FALSE;

  svg_element_set_focus (focus, TRUE);

  timeline_update_for_event (self->timeline, focus, EVENT_TYPE_FOCUS, current_time);
  gtk_svg_advance (self, current_time);

  self->focus = focus;

  self->style_changed = TRUE;
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));

  return TRUE;
}

gboolean
gtk_svg_lose_focus (GtkSvg *self)
{
  int64_t current_time = get_current_time (self);

  /* Don't handle events before we're loaded */
  if (self->load_time == INDEFINITE)
    return FALSE;

  if (!self->focus)
    return TRUE;

  svg_element_set_focus (self->focus, FALSE);

  timeline_update_for_event (self->timeline, self->focus, EVENT_TYPE_BLUR, current_time);
  gtk_svg_advance (self, current_time);

  self->focus = NULL;

  self->style_changed = TRUE;
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));

  return TRUE;
}

gboolean
gtk_svg_move_focus (GtkSvg           *self,
                    GtkDirectionType  direction)
{
  SvgElement *focus = NULL;
  int64_t current_time;

  /* Don't handle events before we're loaded */
  if (self->load_time == INDEFINITE)
    return FALSE;

  if (direction == GTK_DIR_TAB_FORWARD)
    {
      SvgElement *next = self->focus;

      do
        {
          if (next)
            next = svg_element_next (next);
          else
            next = svg_element_first (self->content);

          if (next == NULL || svg_element_get_focusable (next))
            {
              focus = next;
              break;
            }
        }
      while (next);
    }
  else if (direction == GTK_DIR_TAB_BACKWARD)
    {
      SvgElement *next = self->focus;

      do
        {
          if (next)
            next = svg_element_previous (next);
          else
            next = svg_element_last (self->content);

          if (next == NULL || svg_element_get_focusable (next))
            {
              focus = next;
              break;
            }
        } while (next);
    }
  else
    {
      focus = self->focus;
    }

  if (self->focus == focus)
    return FALSE;

  current_time = get_current_time (self);

  if (self->focus)
    {
      timeline_update_for_event (self->timeline, self->focus, EVENT_TYPE_BLUR, current_time);
      svg_element_set_focus (self->focus, FALSE);
    }

  if (focus)
    {
      timeline_update_for_event (self->timeline, focus, EVENT_TYPE_FOCUS, current_time);
      svg_element_set_focus (focus, TRUE);
    }

  gtk_svg_advance (self, current_time);

  self->focus = focus;
  self->style_changed = TRUE;
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));

  return self->focus != NULL;
}

/* }}} */
/* {{{ Views */

void
gtk_svg_set_view (GtkSvg     *self,
                  SvgElement *view)
{
  g_assert (view == NULL || view->type == SVG_ELEMENT_VIEW);

  if (self->view == view)
    return;

  self->view = view;
  self->view_changed = TRUE;

  determine_size (self);
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}

/* }}} */
/* }}} */
/* {{{ Public API */
/* {{{ Constructors */

/**
 * gtk_svg_new:
 *
 * Creates a new, empty SVG paintable.
 *
 * Returns: the paintable
 *
 * Since: 4.22
 */

GtkSvg *
gtk_svg_new (void)
{
  return g_object_new (GTK_TYPE_SVG, NULL);
}

/**
 * gtk_svg_new_from_bytes:
 * @bytes: the data
 *
 * Parses the SVG data in @bytes and creates a paintable.
 *
 * Returns: the paintable
 *
 * Since: 4.22
 */

GtkSvg *
gtk_svg_new_from_bytes (GBytes *bytes)
{
  GtkSvg *self = g_object_new (GTK_TYPE_SVG, NULL);

  gtk_svg_init_from_bytes (self, bytes);

  return self;
}

/**
 * gtk_svg_new_from_resource:
 * @path: the resource path
 *
 * Parses the SVG data in the resource and creates a paintable.
 *
 * Returns: the paintable
 *
 * Since: 4.22
 */

GtkSvg *
gtk_svg_new_from_resource (const char *path)
{
  GtkSvg *self = g_object_new (GTK_TYPE_SVG, NULL);

  gtk_svg_init_from_resource (self, path);

  return self;
}

/* }}} */
/* {{{ Loading */

/**
 * gtk_svg_load_from_bytes:
 * @self: an SVG paintable
 * @bytes: the data to load
 *
 * Loads SVG content into an existing SVG paintable.
 *
 * To track errors while loading SVG content,
 * connect to the [signal@Gtk.Svg::error] signal.
 *
 * This clears any previously loaded content.
 *
 * Since: 4.22
 */

void
gtk_svg_load_from_bytes (GtkSvg *self,
                         GBytes *bytes)
{
  g_return_if_fail (GTK_IS_SVG (self));
  g_return_if_fail (bytes != NULL);

  gtk_svg_clear_content (self);

  gtk_svg_init_from_bytes (self, bytes);

  gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}

/**
 * gtk_svg_load_from_resource:
 * @self: an SVG paintable
 * @path: the resource path
 *
 * Loads SVG content into an existing SVG paintable.
 *
 * To track errors while loading SVG content,
 * connect to the [signal@Gtk.Svg::error] signal.
 *
 * This clears any previously loaded content.
 *
 * Since: 4.22
 */

void
gtk_svg_load_from_resource (GtkSvg     *self,
                            const char *path)
{
  g_return_if_fail (GTK_IS_SVG (self));
  g_return_if_fail (path != NULL);

  gtk_svg_clear_content (self);
  self->resource = g_strdup (path);
  gtk_svg_init_from_resource (self, path);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_RESOURCE]);
}

/* }}} */
/* {{{ Setters and getters */

/**
 * gtk_svg_set_weight:
 * @self: an SVG paintable
 * @weight: the font weight, as a value between -1 and 1000
 *
 * Sets the weight that is used when rendering.
 *
 * The weight affects the effective linewidth when stroking
 * paths.
 *
 * The default value of -1 means to use the font weight
 * from CSS.
 *
 * Since: 4.22
 */

void
gtk_svg_set_weight (GtkSvg *self,
                    double  weight)
{
  g_return_if_fail (GTK_IS_SVG (self));
  g_return_if_fail (-1 <= weight && weight <= 1000);

  if (self->weight == weight)
    return;

  self->weight = weight;

  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WEIGHT]);
}

/**
 * gtk_svg_get_weight:
 * @self: an SVG paintable
 *
 * Gets the value of the weight property.
 *
 * Returns: the weight
 *
 * Since: 4.22
 */

double
gtk_svg_get_weight (GtkSvg *self)
{
  g_return_val_if_fail (GTK_IS_SVG (self), -1);

  return self->weight;
}

static void
update_for_state (GtkSvg       *self,
                  unsigned int  previous_state)
{
  int64_t current_time;

  if ((self->features & GTK_SVG_ANIMATIONS) == 0 || !self->playing)
    {
      if (self->gpa_version > 0)
        {
          apply_state (self, self->state);
          gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
        }
    }

  current_time = get_current_time (self);
  timeline_update_for_state (self->timeline,
                             previous_state, self->state,
                             current_time + self->state_change_delay);
   gtk_svg_advance (self, current_time);

#ifdef DEBUG
   animation_state_dump (self);
#endif
}

/**
 * gtk_svg_set_state:
 * @self: an SVG paintable
 * @state: the state to set, as a value between 0 and 63
 *
 * Sets the state of the paintable.
 *
 * If the paintable is currently playing, the state change
 * will apply transitions that are defined in the SVG. If
 * the paintable is not playing, the state change will take
 * effect instantaneously.
 *
 * Since: 4.22
 */

void
gtk_svg_set_state (GtkSvg       *self,
                   unsigned int  state)
{
  unsigned int previous_state;

  g_return_if_fail (GTK_IS_SVG (self));
  g_return_if_fail (state <= 63);

  if (self->state == state)
    return;

  previous_state = self->state;
  self->state = state;

  dbg_print ("state""renderer state %u -> %u", previous_state, state);

  if ((self->features & GTK_SVG_EXTENSIONS) == 0)
    goto done;

  /* Don't jiggle things while we're still loading */
  if (self->load_time != INDEFINITE)
    update_for_state (self, previous_state);

done:
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STATE]);
}

/**
 * gtk_svg_get_state:
 * @self: an SVG paintable
 *
 * Gets the current state of the paintable.
 *
 * Returns: the state
 *
 * Since: 4.22
 */

unsigned int
gtk_svg_get_state (GtkSvg *self)
{
  g_return_val_if_fail (GTK_IS_SVG (self), 0);

  return self->state;
}

/**
 * gtk_svg_get_state_names:
 * @self: an SVG paintable
 * @length: (out) (nullable): return location for the number
 *   of strings that are returned
 *
 * Returns a `NULL`-terminated array of
 * state names, if available.
 *
 * Note that the returned array and the strings
 * contained in it will only be valid until the
 * `GtkSvg` is cleared or reloaded, so if you
 * want to keep it around, you should make a copy.
 *
 * Returns: (nullable) (transfer none): the state names
 *
 * Since: 4.22
 */

const char **
gtk_svg_get_state_names (GtkSvg       *self,
                         unsigned int *length)
{
  g_return_val_if_fail (GTK_IS_SVG (self), NULL);

  if (length)
    *length = self->n_state_names;

  return (const char **) self->state_names;
}

/**
 * gtk_svg_set_features:
 * @self: an SVG paintable
 * @features: features to enable
 *
 * Enables or disables features of the SVG paintable.
 *
 * By default, all features are enabled.
 *
 * Note that this call only has an effect before the
 * SVG is loaded.
 *
 * Since: 4.22
 */

void
gtk_svg_set_features (GtkSvg         *self,
                      GtkSvgFeatures  features)
{
  g_return_if_fail (GTK_IS_SVG (self));

  if (self->features == features)
    return;

  self->features = features;

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FEATURES]);
}

/**
 * gtk_svg_get_features:
 * @self: an SVG paintable
 *
 * Returns the currently enabled features.
 *
 * Returns: the enabled features
 *
 * Since: 4.22
 */

GtkSvgFeatures
gtk_svg_get_features (GtkSvg *self)
{
  g_return_val_if_fail (GTK_IS_SVG (self), GTK_SVG_DEFAULT_FEATURES);

  return self->features;
}

/**
 * gtk_svg_set_overflow:
 * @self: an SVG paintable
 * @overflow: the new overflow value
 *
 * Sets whether the rendering will be clipped
 * to the bounds.
 *
 * Clipping is expected for [iface@Gdk.Paintable]
 * semantics, so this property should not be
 * changed when using a `GtkSvg` as a paintable.
 *
 * Since: 4.24
 */

void
gtk_svg_set_overflow (GtkSvg      *self,
                      GtkOverflow  overflow)
{
  g_return_if_fail (GTK_IS_SVG (self));

  if (self->overflow == overflow)
    return;

  self->overflow = overflow;
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_OVERFLOW]);
}

/**
 * gtk_svg_get_overflow:
 * @self: an SVG paintable
 *
 * Gets the current overflow value.
 *
 * Returns: the current overflow value
 *
 * Since: 4.24
 */

GtkOverflow
gtk_svg_get_overflow (GtkSvg *self)
{
  g_return_val_if_fail (GTK_IS_SVG (self), GTK_OVERFLOW_HIDDEN);

  return self->overflow;
}

/**
 * gtk_svg_set_stylesheet:
 * @self: an SVG paintable
 * @bytes: (nullable): CSS data
 *
 * Sets a CSS user stylesheet to use.
 *
 * Note that styles are applied at load time,
 * so this function must be called before
 * loading SVG.
 *
 * Since: 4.24
 */

void
gtk_svg_set_stylesheet (GtkSvg *self,
                        GBytes *bytes)
{
  g_return_if_fail (GTK_IS_SVG (self));

  g_clear_pointer (&self->stylesheet, g_bytes_unref);
  if (bytes)
    self->stylesheet = g_bytes_ref (bytes);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STYLESHEET]);
}

/**
 * gtk_svg_get_stylesheet:
 * @self: an SVG paintable
 *
 * Gets the CSS user stylesheet.
 *
 * Returns: (nullable): a `GBytes` with the CSS data
 *
 * Since: 4.24
 */

GBytes *
gtk_svg_get_stylesheet (GtkSvg *self)
{
  g_return_val_if_fail (GTK_IS_SVG (self), NULL);

  return self->stylesheet;
}

static void
setting_changed (GtkSvg *self)
{
  gtk_svg_update_media (self);
  self->style_changed = TRUE;
}

/**
 * gtk_svg_set_settings:
 * @self: an SVG paintable
 * @settings: (nullable): the settings object
 *
 * Sets a settings object holding settinsg used when
 * rendering the SVG.
 *
 * Since: 4.24
 */

void
gtk_svg_set_settings (GtkSvg      *self,
                      GtkSettings *settings)
{
  g_return_if_fail (GTK_IS_SVG (self));

  if (self->settings == settings)
    return;

  if (self->settings)
    g_signal_handlers_disconnect_by_func (self->settings, setting_changed, self);

  self->settings = settings;

  if (settings)
    {
      g_signal_connect_swapped (self->settings, "notify::gtk-interface-color-scheme",
                                G_CALLBACK (setting_changed), self);
      g_signal_connect_swapped (self->settings, "notify::gtk-interface-contrast",
                                G_CALLBACK (setting_changed), self);
      g_signal_connect_swapped (self->settings, "notify::gtk-interface-reduced-motion",
                                G_CALLBACK (setting_changed), self);
    }

  setting_changed (self);
}

/* }}} */
/* {{{ Animation */

/**
 * gtk_svg_set_frame_clock:
 * @self: an SVG paintable
 * @clock: the frame clock
 *
 * Sets a frame clock.
 *
 * Without a frame clock, GtkSvg will not advance animations.
 *
 * Since: 4.22
 */

void
gtk_svg_set_frame_clock (GtkSvg        *self,
                         GdkFrameClock *clock)
{
  g_return_if_fail (GTK_IS_SVG (self));

  gboolean was_connected;

  if (self->clock == clock)
    return;

  was_connected = self->clock_update_id != 0;

  frame_clock_disconnect (self);

  g_set_object (&self->clock, clock);

  if (self->clock && self->playing)
    {
      if (self->load_time == INDEFINITE)
        gtk_svg_set_load_time (self, gdk_frame_clock_get_frame_time (clock));
      else
        gtk_svg_advance (self, gdk_frame_clock_get_frame_time (clock));
    }

  /* FIXME should account for the difference between old and
   * new clock even when paused
   */


  if (was_connected)
    frame_clock_connect (self);
}

/**
 * gtk_svg_play:
 * @self: an SVG paintable
 *
 * Start playing animations and state transitions.
 *
 * SvgAnimations can be paused and started repeatedly.
 *
 * Since: 4.22
 */

void
gtk_svg_play (GtkSvg *self)
{
  g_return_if_fail (GTK_IS_SVG (self));

  gtk_svg_set_playing (self, TRUE);
}

/**
 * gtk_svg_pause:
 * @self: an SVG paintable
 *
 * Stop any playing animations and state transitions.
 *
 * SvgAnimations can be paused and started repeatedly.
 *
 * Since: 4.22
 */

void
gtk_svg_pause (GtkSvg *self)
{
  g_return_if_fail (GTK_IS_SVG (self));

  gtk_svg_set_playing (self, FALSE);
}

/* }}} */
/* }}} */

/* vim:set foldmethod=marker: */

Messung V0.5 in Prozent
C=98 H=94 G=95

¤ Dauer der Verarbeitung: 0.80 Sekunden  (vorverarbeitet am  2026-07-03) ¤

*© Formatika GbR, Deutschland






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