]> gitweb.michael.orlitzky.com - libsvgtiny-pixbuf.git/blobdiff - io-svg.c
COPYING: add one to state the "or later" bit
[libsvgtiny-pixbuf.git] / io-svg.c
index 2d509c62b5b38319ea42399c26c86d9b440e2591..8ef9ff90e3c1bfb73dcbd7e3bb919475d9495f4c 100644 (file)
--- a/io-svg.c
+++ b/io-svg.c
@@ -1,18 +1,29 @@
-#include <stdio.h> /* fopen, fprintf, fread, printf */
-#include <stdlib.h> /* malloc */
+#include <string.h> /* memcpy, memset */
 
 #include <cairo.h>
-#include <gdk/gdk.h>
-#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gdk-pixbuf/gdk-pixbuf.h> /* includes glib.h */
+#include <glib/gprintf.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
 #include <svgtiny.h>
 
-/*
- * The width and height of the viewport that we'll render the SVG
+/* This "header" includes extra code that we've copy/pasted from GDK */
+#include "gdk_pixbuf_get_from_surface.h"
+
+/* Declare the two functions that we export. Their names aren't
+ * mentioned in gdk-pixbuf-io.h, but they are required by the
+ * implementation, as you can confirm from the API docs or in
+ * gdk-pixbuf-io.c. The G_MODULE_EXPORT macro is defined in
+ * glib's gmodule.h */
+G_MODULE_EXPORT void fill_vtable(GdkPixbufModule* module);
+G_MODULE_EXPORT void fill_info(GdkPixbufFormat *info);
+
+
+/* The width and height of the viewport that we'll render the SVG
  * into. The final "picture" may not actually be this size; based on
  * the height, width, viewBox, and preserveAspectRatio attributes in
  * the SVG itself, libsvgtiny may scale, stretch, offset, etc. the
- * paths to make them fit nicely into the viewport.
- */
+ * paths to make them fit nicely into the viewport. */
 #define VIEWPORT_WIDTH 512
 #define VIEWPORT_HEIGHT 512
 
@@ -21,17 +32,33 @@ typedef struct svgtiny_diagram diagram_t;
 typedef struct svgtiny_shape shape_t;
 
 
+/* Our main data structure. One of these is created when we start
+ * loading an SVG, and it persists while the SVG is being read. It
+ * contains a few boilerplate members, and the svg_data/svg_data_size
+ * fields that we use to keep track of the buffer where the SVG file
+ * lives. */
 typedef struct {
   GdkPixbufModuleUpdatedFunc  updated_func;
   GdkPixbufModulePreparedFunc prepared_func;
-  GdkPixbufModuleSizeFunc     size_func;
   gpointer                    user_data;
+
+  /* The SVG "file" that we're building in memory. */
+  gchar*                      svg_data;
+
+  /* How far into svg_data are we? This should always point to the
+     next empty byte. If (for example) svg_data_size is 2, then
+     svg_data[0] and svg_data[1] are used, but svg_data[2] is free. */
+  size_t svg_data_size;
+
 } SvgTinyContext;
 
 
 /**
  * @brief Render an svgtiny path using cairo.
  *
+ * This was lovingly borrowed from @c examples/svgtiny_display_x11.c
+ * in libsvgtiny itself, and modified to use a transparent background.
+ *
  * @param cr
  *   A pointer to a valid cairo context.
  *
@@ -47,8 +74,8 @@ static void render_path(cairo_t* cr, shape_t* path) {
     switch ((int) path->path[j]) {
     case svgtiny_PATH_MOVE:
       cairo_move_to(cr,
-                   path->path[j + 1],
-                   path->path[j + 2]);
+                    path->path[j + 1],
+                    path->path[j + 2]);
       j += 3;
       break;
     case svgtiny_PATH_CLOSE:
@@ -57,116 +84,124 @@ static void render_path(cairo_t* cr, shape_t* path) {
       break;
     case svgtiny_PATH_LINE:
       cairo_line_to(cr,
-                   path->path[j + 1],
-                   path->path[j + 2]);
+                    path->path[j + 1],
+                    path->path[j + 2]);
       j += 3;
       break;
     case svgtiny_PATH_BEZIER:
       cairo_curve_to(cr,
-                    path->path[j + 1],
-                    path->path[j + 2],
-                    path->path[j + 3],
-                    path->path[j + 4],
-                    path->path[j + 5],
-                    path->path[j + 6]);
+                     path->path[j + 1],
+                     path->path[j + 2],
+                     path->path[j + 3],
+                     path->path[j + 4],
+                     path->path[j + 5],
+                     path->path[j + 6]);
       j += 7;
-      break;
-    default:
-      fprintf(stderr, "error: unmatched case in render_path\n");
-      j += 1;
     }
   }
   if (path->fill != svgtiny_TRANSPARENT) {
     cairo_set_source_rgba(cr,
-                         svgtiny_RED(path->fill) / 255.0,
-                         svgtiny_GREEN(path->fill) / 255.0,
-                         svgtiny_BLUE(path->fill) / 255.0,
-                         1);
+                          svgtiny_RED(path->fill) / 255.0,
+                          svgtiny_GREEN(path->fill) / 255.0,
+                          svgtiny_BLUE(path->fill) / 255.0,
+                          1);
     cairo_fill_preserve(cr);
   }
   if (path->stroke != svgtiny_TRANSPARENT) {
     cairo_set_source_rgba(cr,
-                         svgtiny_RED(path->stroke) / 255.0,
-                         svgtiny_GREEN(path->stroke) / 255.0,
-                         svgtiny_BLUE(path->stroke) / 255.0,
-                         1);
+                          svgtiny_RED(path->stroke) / 255.0,
+                          svgtiny_GREEN(path->stroke) / 255.0,
+                          svgtiny_BLUE(path->stroke) / 255.0,
+                          1);
     cairo_set_line_width(cr, path->stroke_width);
     cairo_stroke_preserve(cr);
   }
 }
 
 /**
- * @brief Parse an SVG file into a diagram_t structure.
+ * @brief Parse a buffer of SVG data into a diagram_t structure.
+ *
+ * @param buffer
+ *   The buffer containing the SVG document.
  *
- * @param fp
- *   A pointer to an open file stream.
+ * @param bytecount
+ *   The number of bytes in @c buffer.
  *
  * @return If successful, a pointer to a @c diagram_t structure is
  *   returned; if not, @c NULL is returned. You are expected to @c
  *   svgtiny_free the result if it is valid.
  */
-static diagram_t* svgtiny_diagram_from_file(FILE* fp, int width, int height) {
+static diagram_t* svgtiny_diagram_from_buffer(const gchar* buffer,
+                                              gsize bytecount,
+                                              guint width,
+                                              guint height,
+                                              GError** error) {
   diagram_t* diagram;
-
-  size_t bytecount;
-  char* buffer;
-  size_t bytesread;
   svgtiny_code code;
 
-  /* Find the size of the file stream */
-  fseek(fp, 0L, SEEK_END);
-  bytecount = ftell(fp);
-  rewind(fp);
-
-  buffer = malloc(bytecount);
-  if (!buffer) {
-    fprintf(stderr, "Unable to allocate %zd bytes\n", bytecount);
-    return NULL;
-  }
-
-  bytesread = fread(buffer, 1, bytecount, fp);
-  if (bytesread != bytecount) {
-    fprintf(stderr, "Read only %zd of %zd bytes from stream\n",
-           bytesread,
-           bytecount);
-  }
-  fclose(fp);
-
   diagram = svgtiny_create();
   if (!diagram) {
-    fprintf(stderr, "svgtiny_create() failed\n");
+    g_set_error_literal(error,
+                        G_FILE_ERROR,
+                        G_FILE_ERROR_NOMEM,
+                        "out of memory in svgtiny_create()");
     return NULL;
   }
 
-  code = svgtiny_parse(diagram, buffer, bytecount, "", width, height);
-  free(buffer);
+  g_assert((int)width >= 0);
+  g_assert((int)height >= 0);
 
-  if (code != svgtiny_OK) {
-    fprintf(stderr, "svgtiny_parse failed with ");
-    switch (code) {
+  /* There's a thread-safety issue in libwapcaplet that can cause
+   * svgtiny_parse() to crash if you load lots of SVGs at once:
+   *
+   *   https://bugs.netsurf-browser.org/mantis/view.php?id=2857
+   *
+   * Putting a lock around svgtiny_parse() is a pretty simple solution
+   * and looks like it does the trick.
+  */
+  static GMutex mutex;
+  g_mutex_lock(&mutex);
+  code = svgtiny_parse(diagram,
+                       buffer,
+                       bytecount, "",
+                       (int)width,
+                       (int)height);
+  g_mutex_unlock (&mutex);
+
+  switch(code) {
+    case svgtiny_OK:
+      /* The one success case. */
+      return diagram;
     case svgtiny_OUT_OF_MEMORY:
-      fprintf(stderr, "svgtiny_OUT_OF_MEMORY");
+      g_set_error_literal(error,
+                          G_FILE_ERROR,
+                          G_FILE_ERROR_NOMEM,
+                          "out of memory in svgtiny_parse()");
       break;
     case svgtiny_LIBDOM_ERROR:
-      fprintf(stderr, "svgtiny_LIBDOM_ERROR");
+      g_set_error_literal(error,
+                          GDK_PIXBUF_ERROR,
+                          GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                          "invalid XML DOM in svgtiny_parse()");
       break;
     case svgtiny_NOT_SVG:
-      fprintf(stderr, "svgtiny_NOT_SVG");
+      g_set_error_literal(error,
+                          GDK_PIXBUF_ERROR,
+                          GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                          "missing <svg> element in svgtiny_parse()");
       break;
     case svgtiny_SVG_ERROR:
-      fprintf(stderr, "svgtiny_SVG_ERROR: line %i: %s",
-             diagram->error_line,
-             diagram->error_message);
+      g_set_error(error,
+                  GDK_PIXBUF_ERROR,
+                  GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                  "SVG format error in svgtiny_parse() on line %i: %s",
+                  diagram->error_line,
+                  diagram->error_message);
       break;
-    default:
-      fprintf(stderr, "unknown svgtiny_code %i", code);
-      break;
-    }
-    fprintf(stderr, "\n");
-    return NULL;
   }
 
-  return diagram;
+  /* All other cases above are failure */
+  return NULL;
 }
 
 /**
@@ -179,21 +214,21 @@ static diagram_t* svgtiny_diagram_from_file(FILE* fp, int width, int height) {
  *   is returned; if not, @c NULL is returned. You are expected to @c
  *   cairo_destroy the result if it is valid.
  */
-static cairo_t* cairo_context_from_diagram(diagram_t* diagram) {
+static cairo_t* cairo_context_from_diagram(const diagram_t* diagram) {
   cairo_t* cr;
   cairo_surface_t* surface;
   cairo_status_t crs;
   unsigned int i;
 
   surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
-                                      diagram->width,
-                                      diagram->height);
+                                       diagram->width,
+                                       diagram->height);
 
   crs = cairo_surface_status(surface);
   if (crs != CAIRO_STATUS_SUCCESS) {
-    fprintf(stderr,
-           "cairo_image_surface_create failed: %s\n",
-           cairo_status_to_string(crs));
+    g_fprintf(stderr,
+              "cairo_image_surface_create failed: %s\n",
+              cairo_status_to_string(crs));
     cairo_surface_destroy(surface);
     return NULL;
   }
@@ -206,9 +241,9 @@ static cairo_t* cairo_context_from_diagram(diagram_t* diagram) {
   cairo_surface_destroy(surface);
 
   if (crs != CAIRO_STATUS_SUCCESS) {
-    fprintf(stderr,
-           "cairo_create failed: %s\n",
-           cairo_status_to_string(crs));
+    g_fprintf(stderr,
+              "cairo_create failed: %s\n",
+              cairo_status_to_string(crs));
     cairo_destroy(cr);
     return NULL;
   }
@@ -227,17 +262,17 @@ static cairo_t* cairo_context_from_diagram(diagram_t* diagram) {
     /* If this shape is text... */
     if (diagram->shape[i].text) {
       /* Figure out what color to use from the R/G/B components of the
-        shape's stroke. */
+         shape's stroke. */
       cairo_set_source_rgba(cr,
-                           svgtiny_RED(diagram->shape[i].stroke) / 255.0,
-                           svgtiny_GREEN(diagram->shape[i].stroke) / 255.0,
-                           svgtiny_BLUE(diagram->shape[i].stroke) / 255.0,
-                           1);
+                            svgtiny_RED(diagram->shape[i].stroke) / 255.0,
+                            svgtiny_GREEN(diagram->shape[i].stroke) / 255.0,
+                            svgtiny_BLUE(diagram->shape[i].stroke) / 255.0,
+                            1);
       /* Then move to the actual position of the text within the
-        shape... */
+         shape... */
       cairo_move_to(cr,
-                   diagram->shape[i].text_x,
-                   diagram->shape[i].text_y);
+                    diagram->shape[i].text_x,
+                    diagram->shape[i].text_y);
 
       /* and draw it. */
       cairo_show_text(cr, diagram->shape[i].text);
@@ -248,9 +283,9 @@ static cairo_t* cairo_context_from_diagram(diagram_t* diagram) {
   /* Check the status again just for good measure? */
   crs = cairo_status(cr);
   if (crs != CAIRO_STATUS_SUCCESS) {
-    fprintf(stderr,
-           "cairo error: %s\n",
-           cairo_status_to_string(crs));
+    g_fprintf(stderr,
+              "cairo error: %s\n",
+              cairo_status_to_string(crs));
     cairo_destroy(cr);
     return NULL;
   }
@@ -258,17 +293,36 @@ static cairo_t* cairo_context_from_diagram(diagram_t* diagram) {
   return cr;
 }
 
-static GdkPixbuf* gdk_pixbuf_from_svg_file_stream(FILE *fp, GError** error) {
+/**
+ * @brief Create a GdkPixbuf from a buffer of SVG data.
+ *
+ * @param buffer
+ *   The buffer containing the SVG document.
+ *
+ * @param bytecount
+ *   The number of bytes in @c buffer.
+ *
+ * @param error
+ *   The address of a @c GError pointer that we use to return errors.
+ *
+ * @return If successful, a valid pointer to a @c GdkPixbuf is
+ *   returned; if not, @c NULL is returned and @c error is populated.
+ */
+static GdkPixbuf* gdk_pixbuf_from_svg_buffer(const gchar* buffer,
+                                             gsize bytecount,
+                                             GError** error) {
   diagram_t* diagram;
   cairo_t* cr = 0;
   GdkPixbuf* pb;
+  GError* sub_error = NULL;
 
-  diagram = svgtiny_diagram_from_file(fp, VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
+  diagram = svgtiny_diagram_from_buffer(buffer,
+                                        bytecount,
+                                        VIEWPORT_WIDTH,
+                                        VIEWPORT_HEIGHT,
+                                        &sub_error);
   if (!diagram) {
-    g_set_error_literal(error,
-                       GDK_PIXBUF_ERROR,
-                       GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
-                       "Could not parse SVG diagram from file");
+    g_propagate_error(error, sub_error);
     return NULL;
   }
 
@@ -276,92 +330,355 @@ static GdkPixbuf* gdk_pixbuf_from_svg_file_stream(FILE *fp, GError** error) {
   if (!cr) {
     svgtiny_free(diagram);
     g_set_error_literal(error,
-                       GDK_PIXBUF_ERROR,
-                       GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
-                       "Could not create Cairo surface from SVG diagram");
+                        GDK_PIXBUF_ERROR,
+                        GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                        "could not create Cairo surface from SVG diagram");
     return NULL;
   }
 
 
-  /* We're using the viewport width and height and not the diagram
-   * width/height for the image. The diagram can be of a different
-   * size and aspect ratio than the viewport, and our main use case is
-   * for icons that are generally square and reasonably sized. If the
-   * diagram is "small," then we want to scale it up until it fits
-   * nicely in the viewport before rendering it. That's as opposed to
-   * rendering the image small, and letting GDK scale it up. Of course
-   * this reasoning makes the assumption that the viewport is usually
-   * larger than the diagram.
-  */
+  /* I've gone back and forth on this about five times: we use the
+   * diagram width and height, and not the viewport width and height.
+   * This can ultimately render an image that's larger than the
+   * viewport size, but I think GDK will resize the final pixbuf
+   * anyway. More importantly, rendering small icons at a larger
+   * (viewport) size seems to make the whole thing go ape-shit.
+   * So for now I'm back in the diagram camp.
+   */
   pb = gdk_pixbuf_get_from_surface(cairo_get_target(cr),
-                                  0,
-                                  0,
-                                  VIEWPORT_WIDTH,
-                                  VIEWPORT_HEIGHT);
+                                   0,
+                                   0,
+                                   diagram->width,
+                                   diagram->height);
 
 
   if (!pb) {
     g_set_error_literal(error,
-                       GDK_PIXBUF_ERROR,
-                       GDK_PIXBUF_ERROR_FAILED,
-                       "Failed to obtain a GdkPixbuf from Cairo surface");
+                        GDK_PIXBUF_ERROR,
+                        GDK_PIXBUF_ERROR_FAILED,
+                        "failed to obtain a GdkPixbuf from Cairo surface");
   }
 
   return pb;
 }
 
 
+/**
+ * @brief Our implementation of GdkPixbufModuleBeginLoadFunc, as
+ *   defined in gdk-pixbuf-io.h.
+ */
 static gpointer gdk_pixbuf_begin_load(GdkPixbufModuleSizeFunc size_func,
-                                     GdkPixbufModulePreparedFunc prep_func,
-                                     GdkPixbufModuleUpdatedFunc updated_func,
-                                     gpointer user_data,
-                                     GError **error)
-{
-        SvgTinyContext* context    = g_new0(SvgTinyContext, 1);
-
-        context->size_func     = size_func;
-        context->prepared_func = prep_func;
-        context->updated_func  = updated_func;
-        context->user_data     = user_data;
-
-        return context;
+                                      GdkPixbufModulePreparedFunc prep_func,
+                                      GdkPixbufModuleUpdatedFunc updated_func,
+                                      gpointer user_data,
+                                      GError **error) {
+
+  SvgTinyContext* context = g_new(SvgTinyContext, 1);
+
+  context->prepared_func  = prep_func;
+  context->updated_func   = updated_func;
+  context->user_data      = user_data;
+
+  context->svg_data       = NULL;
+  context->svg_data_size  = 0;
+
+  return context;
 }
 
+
+/**
+ * @brief Our implementation of GdkPixbufModuleIncrementLoadFunc, as
+ *   defined in gdk-pixbuf-io.h.
+ */
 static gboolean gdk_pixbuf_load_increment(gpointer data,
-                                         const guchar* buf,
-                                         guint size,
-                                         GError** error) {
+                                          const guchar* buf,
+                                          guint buf_size,
+                                          GError** error) {
   SvgTinyContext* context = (SvgTinyContext*)data;
-  /*
-  if (!rsvg_handle_write (context->handle, buf, size, error)) {
-    rsvg_propagate_error (error, _("Error writing"), ERROR_WRITING);
-    return FALSE;
+
+  if (buf_size == 0) {
+    return TRUE;
   }
-  */
+
+  context->svg_data = g_realloc(context->svg_data,
+                                context->svg_data_size + buf_size);
+  memcpy(context->svg_data + context->svg_data_size, buf, buf_size);
+  context->svg_data_size += buf_size;
+
   return TRUE;
 }
 
+/**
+ * @brief Convenience function to execute the "updated" callback
+ *   stored in our @c context.
+ */
 static void emit_updated(SvgTinyContext* context, GdkPixbuf* pixbuf) {
   if (context->updated_func != NULL) {
     (*context->updated_func)(pixbuf,
-                            0,
-                            0,
-                            gdk_pixbuf_get_width(pixbuf),
-                            gdk_pixbuf_get_height(pixbuf),
-                            context->user_data);
+                             0,
+                             0,
+                             gdk_pixbuf_get_width(pixbuf),
+                             gdk_pixbuf_get_height(pixbuf),
+                             context->user_data);
   }
 }
 
+/**
+ * @brief Convenience function to execute the "prepared" callback
+ *   stored in our @c context.
+ */
 static void emit_prepared(SvgTinyContext* context, GdkPixbuf* pixbuf) {
   if (context->prepared_func != NULL) {
     (*context->prepared_func)(pixbuf, NULL, context->user_data);
   }
 }
 
+
+
+/**
+ * @brief Replace one GTK <xi:include> element by its data.
+ *
+ * @param node
+ *   A pointer to an <xi:include> element node.
+ *
+ * @return TRUE if we replaced the node, and FALSE otherwise.
+ *
+ */
+static gboolean process_one_xinclude(xmlNode* node) {
+  xmlChar* href;
+
+  href = xmlGetProp(node, BAD_CAST "href");
+  if (href == NULL) {
+    /* We only process XIncludes with base64 data hrefs */
+    return FALSE;
+  }
+
+  if (xmlStrncmp(href, BAD_CAST "data:text/xml;base64,", 21)) {
+    /* We only process XIncludes with base64 data hrefs */
+    return FALSE;
+  }
+
+  xmlChar* hrefdata = href+21;
+
+  /* Verify that hrefdata is base64-encoded (and that it's safe to
+     cast to a signed gchar pointer). I'm assuming that everyone is
+     using the RFC 4648 encoding? */
+  for (unsigned int i=0; i < xmlStrlen(hrefdata); i++) {
+    if (hrefdata[i] > 'z') {
+      return FALSE;
+    }
+    if (hrefdata[i] < '0' && hrefdata[i] != '+' && hrefdata[i] != '/') {
+      return FALSE;
+    }
+  }
+
+  /* WARNING: the xmlChar and guchar types here are compatible, but
+     the decoded data is not necessarily NULL-terminated, while all of
+     the libxml2 functions that operate on a xmlChar pointer assume
+     that they are. */
+  gsize decoded_size;
+  xmlChar* decoded = g_base64_decode((const gchar*)hrefdata, &decoded_size);
+
+  /* This cast is safe because signed and unsigned chars are the same size,
+     and xmlReadMemory is going to treat the data as binary anyway. */
+  xmlDoc* xinc_doc = xmlReadMemory((const char*)decoded,
+                                   decoded_size,
+                                   "xinclude.xml",
+                                   NULL,
+                                   0);
+  g_free(decoded);
+
+  if (xinc_doc == NULL) {
+    return FALSE;
+  }
+
+  xmlNode* xinc_root = xmlDocGetRootElement(xinc_doc);
+  if (xinc_root == NULL || xmlStrcmp(xinc_root->name, BAD_CAST "svg")) {
+    return FALSE;
+  }
+
+  /* Replace the original xinclude "node" with the children of this
+     "svg" node. Do the order of the nodes in an SVG matter? I don't
+     know, but we go to a little bit of extra trouble here to ensure
+     that we put the replacement in the right place, i.e. after its
+     previous sibling (if there is one). */
+
+  xmlNode* p = xmlPreviousElementSibling(node);
+  xmlNode* cur_node;
+
+  /* If there is no previous sibling element, do one AddChild()
+     first. Then we're back to the case of a previous sibling. */
+  if (p) {
+    cur_node = xmlFirstElementChild(xinc_root);
+  }
+  else {
+    p = node->parent;
+    cur_node = xmlFirstElementChild(xinc_root);
+    if (cur_node) {
+      /* Without the xmlCopyNode, I get segfaults, and I don't care to
+        investigate why. */
+      p = xmlAddChild(p, xmlCopyNode(cur_node,1));
+      xmlReconciliateNs(p->doc, p);
+
+      cur_node = xmlNextElementSibling(cur_node);
+    }
+  }
+
+  g_assert(p != NULL); /* xmlAddChild didn't fail */
+
+  xmlUnlinkNode(node);
+  xmlFreeNode(node);
+
+  while (cur_node) {
+    p = xmlAddNextSibling(p, xmlCopyNode(cur_node,1));
+    xmlReconciliateNs(p->doc, p);
+    cur_node = xmlNextElementSibling(cur_node);
+  }
+
+  xmlFreeDoc(xinc_doc);
+
+  return TRUE;
+}
+
+/**
+ * @brief Replace all GTK <xi:include> elements in a tree by their data.
+ *
+ * @param node
+ *   A node pointer, to the root of the tree.
+ *
+ * @return TRUE if we replaced any <xi:include> element nodes, and
+ *   FALSE otherwise.
+ *
+ */
+static gboolean process_child_xincludes(xmlNode* a_node) {
+  gboolean result = FALSE;
+  xmlNode* cur_node = a_node;
+  xmlNode* next_node;
+
+  g_assert(cur_node == NULL || cur_node->type == XML_ELEMENT_NODE);
+
+  while (cur_node) {
+    if (!xmlStrcmp(cur_node->name, BAD_CAST "include")) {
+      /* process_one_xinclude() clobbers this node, so we need
+         to get its successor before calling that function. */
+      next_node = xmlNextElementSibling(cur_node);
+      if (process_one_xinclude(cur_node)) {
+        result = TRUE;
+      }
+      cur_node = next_node;
+      continue;
+    }
+
+    if (process_child_xincludes(xmlFirstElementChild(cur_node))) {
+      result = TRUE;
+    }
+    cur_node = xmlNextElementSibling(cur_node);
+  }
+
+  return result;
+}
+
+
+/**
+ * @brief Process GTK <xi:include> elements in an SVG buffer.
+ *
+ * GTK is very cute. Its gtk-encode-symbolic-svg tool wraps your SVG
+ * in its own boilerplate, but then rather than including your SVG
+ * data verbatim, it includes it via a sketchy XInclude that looks
+ * like,
+ *
+ *   <xi:include href="data:text/xml;base64,PD94bWwgd..."/>
+ *
+ * Librsvg knows how to parse that, but libxml2 doesn't (the latter
+ * can handle an XInclude, just not a base64-encoded data reference).
+ * Fortunately, you can read the source to gtk-encode-symbolic-svg,
+ * and just see what the format of that line will be. Here we're going
+ * to parse out the base64 data, decode it, strip out its opening
+ * <xml> and <svg> tags, and then replace the original <xi:include>
+ * element with the result.
+ *
+ * @param buffer
+ *   A buffer containing SVG file data.
+ *
+ * @param buf_size
+ *   The size of @c buffer (which may not be NULL-terminated).
+ *
+ * @param new_size
+ *   A pointer to the size of the new buffer, valid only if the
+ *   return value is non-NULL.
+ *
+ * @return A pointer to a buffer where the <xi:include> has been
+ * processed. If no replacements were made, the result will be @c
+ * NULL; otherwise, you are expected to @c free it when you are done.
+ */
+static gchar* process_gtk_symbolic_svg_xinclude(const gchar* buffer,
+                                                gsize buf_size,
+                                                gsize* new_size) {
+
+  xmlDoc* doc = xmlReadMemory(buffer,buf_size,"symbolic.xml",NULL,0);
+  if (doc == NULL) {
+    return NULL;
+  }
+
+  xmlNode* root_element = xmlDocGetRootElement(doc);
+  if (root_element == NULL) {
+    return NULL;
+  }
+
+  gchar* result = NULL;
+  if (process_child_xincludes(root_element)) {
+    /* If we actually replaced something, we need to return the new
+       document in a buffer. */
+    xmlChar *xmlbuf;
+    int xmlbuf_size;
+    xmlDocDumpFormatMemory(doc, &xmlbuf, &xmlbuf_size, 1);
+    /* We're going to free() this later on with g_free() instead of
+       xmlFree(), so the two "byte" types had better be the same
+       size. */
+    g_assert(sizeof(xmlChar) == sizeof(gchar));
+    *new_size = (gsize)xmlbuf_size;
+    result = (gchar*)xmlbuf;
+  }
+
+  xmlFreeDoc(doc);
+  xmlCleanupParser();
+  return result;
+}
+
+
+/**
+ * @brief Our implementation of GdkPixbufModuleStopLoadFunc, as
+ *   defined in gdk-pixbuf-io.h.
+ */
 static gboolean gdk_pixbuf_stop_load(gpointer data, GError **error) {
   SvgTinyContext* context = (SvgTinyContext*)data;
   GdkPixbuf* pixbuf = NULL;
-  gboolean result = TRUE;
+  GError* sub_error = NULL;
+
+  g_assert(context != NULL);
+  if (context->svg_data == NULL || context->svg_data_size == 0) {
+    /* Is it possible to begin/stop with no increments in between?
+     * I sure don't know. Let's play it safe. */
+    return FALSE;
+  }
+
+  /* If we're inside of gtk-encode-symbolic-svg right now, we need to
+     process the insane librsvg-specific XInclude directive it hands
+     us before proceeding. */
+  gsize newsize;
+  gchar* newdata = process_gtk_symbolic_svg_xinclude(context->svg_data,
+                                                     context->svg_data_size,
+                                                     &newsize);
+  if (newdata != NULL) {
+    g_free(context->svg_data);
+    context->svg_data = newdata;
+    context->svg_data_size = newsize;
+  }
+
+  /* OK, we've got an SVG with no XIncludes now. */
+  pixbuf = gdk_pixbuf_from_svg_buffer(context->svg_data,
+                                      context->svg_data_size,
+                                      &sub_error);
 
   if (pixbuf != NULL) {
     emit_prepared(context, pixbuf);
@@ -369,23 +686,30 @@ static gboolean gdk_pixbuf_stop_load(gpointer data, GError **error) {
     g_object_unref(pixbuf);
   }
   else {
-    result = FALSE;
+    g_propagate_error(error, sub_error);
+    return FALSE;
   }
-  g_free (context);
+  g_free(context->svg_data);
+  g_free(context);
 
-  return result;
+  return TRUE;
 }
 
 
-G_MODULE_EXPORT void fill_vtable(GdkPixbufModule* module);
+/**
+ * @brief Our implementation of GdkPixbufModuleFillVtableFunc, as
+ *   defined in gdk-pixbuf-io.h.
+ */
 void fill_vtable(GdkPixbufModule* module) {
   module->begin_load = gdk_pixbuf_begin_load;
   module->load_increment = gdk_pixbuf_load_increment;
   module->stop_load = gdk_pixbuf_stop_load;
-  module->load = gdk_pixbuf_from_svg_file_stream;
 }
 
-G_MODULE_EXPORT void fill_info(GdkPixbufFormat *info);
+/**
+ * @brief Our implementation of GdkPixbufModuleFillInfoFunc, as
+ *   defined in gdk-pixbuf-io.h.
+ */
 void fill_info(GdkPixbufFormat* info) {
   /* Borrowed from librsvg-2.40.21 */
   static const GdkPixbufModulePattern signature[] = {
@@ -413,44 +737,40 @@ void fill_info(GdkPixbufFormat* info) {
   };
 
   info->name        = "svg";
-  info->signature   = (GdkPixbufModulePattern*) signature;
+  info->signature   = (GdkPixbufModulePattern*)signature;
   info->description = "Scalable Vector Graphics";
-  info->mime_types  = (gchar**) mime_types;
-  info->extensions  = (gchar**) extensions;
+  info->mime_types  = (gchar**)mime_types;
+  info->extensions  = (gchar**)extensions;
   info->flags       = GDK_PIXBUF_FORMAT_SCALABLE;
   info->license     = "AGPL3";
 }
 
 
+/**
+ * @brief Entry point of the svg2png test program.
+ */
 int main(int argc, char** argv) {
   char* svgpath;
   char* pngpath;
-  FILE* fp;
   GError* err = NULL;
   GdkPixbuf* pb;
 
   /* Parse arguments, and maybe print usage */
   if (argc < 3) {
-    printf("Usage: %s INPUT OUTPUT\n", argv[0]);
-    printf("Convert an SVG file (INPUT) to a PNG file (OUTPUT)\n");
+    g_printf("Usage: %s INPUT OUTPUT\n", argv[0]);
+    g_printf("Convert an SVG file (INPUT) to a PNG file (OUTPUT)\n");
     return 2;
   }
 
   svgpath = argv[1];
   pngpath = argv[2];
 
-  fp = fopen(svgpath, "rb");
-  if (!fp) {
-    perror(svgpath);
-    return 1;
-  }
-
-  pb = gdk_pixbuf_from_svg_file_stream(fp, &err);
+  pb = gdk_pixbuf_new_from_file(svgpath, &err);
   if (!pb) {
-    fprintf(stderr,
-           "Error %d in gdk_pixbuf_from_svg_file_stream: %s\n",
-           err->code,
-           err->message);
+    g_fprintf(stderr,
+              "Error %d in gdk_pixbuf_new_from_file: %s\n",
+              err->code,
+              err->message);
     g_error_free(err);
     return 1;
   }