-#include <stdio.h> /* fprintf, printf */
-#include <string.h> /* memcpy */
+#include <string.h> /* memcpy, memset */
#include <cairo.h>
-#include <gdk/gdk.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
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. */
- char* svg_data;
+ 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
/**
* @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.
*
* 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_buffer(char* buffer,
- size_t bytecount,
- 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;
svgtiny_code code;
diagram = svgtiny_create();
if (!diagram) {
g_set_error_literal(error,
- GDK_PIXBUF_ERROR,
- GDK_PIXBUF_ERROR_FAILED,
- "svgtiny_create() failed");
+ 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);
-
- if (code != svgtiny_OK) {
- switch (code) {
+ g_assert((int)width >= 0);
+ g_assert((int)height >= 0);
+
+ /* 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:
g_set_error_literal(error,
G_FILE_ERROR,
g_set_error_literal(error,
GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
- "libdom error in svgtiny_parse()");
+ "invalid XML DOM in svgtiny_parse()");
break;
case svgtiny_NOT_SVG:
g_set_error_literal(error,
GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
- "encountered svgtiny_NOT_SVG in svgtiny_parse()");
+ "missing <svg> element in svgtiny_parse()");
break;
case svgtiny_SVG_ERROR:
g_set_error(error,
GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
- "SVG error in svgtiny_parse() on line %i: %s",
+ "SVG format error in svgtiny_parse() on line %i: %s",
diagram->error_line,
diagram->error_message);
break;
- }
- return NULL;
}
- return diagram;
+ /* All other cases above are failure */
+ return NULL;
}
/**
* 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;
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;
}
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;
}
/* 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;
}
* @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(char* buffer,
- size_t bytecount,
+static GdkPixbuf* gdk_pixbuf_from_svg_buffer(const gchar* buffer,
+ gsize bytecount,
GError** error) {
diagram_t* diagram;
cairo_t* cr = 0;
}
+/**
+ * @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,
SvgTinyContext* context = g_new(SvgTinyContext, 1);
- context->size_func = size_func;
context->prepared_func = prep_func;
context->updated_func = updated_func;
context->user_data = user_data;
- /* YOLO, no error checking */
- context->svg_data = g_malloc(0);
+ 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 buf_size,
GError** error) {
- size_t increment = 0;
SvgTinyContext* context = (SvgTinyContext*)data;
- /* YOLO, no error checking */
+ 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);
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,
}
}
+/**
+ * @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);
}
-/*
-static void emit_size(SvgTinyContext* context, GdkPixbuf* pixbuf) {
- int w = gdk_pixbuf_get_width(pixbuf);
- int h = gdk_pixbuf_get_height(pixbuf);
- if (context->size_func != NULL) {
- (*context->size_func)(&w, &h, 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_size(context, pixbuf);*/
emit_prepared(context, pixbuf);
emit_updated(context, pixbuf);
g_object_unref(pixbuf);
}
else {
g_propagate_error(error, sub_error);
- result = FALSE;
+ return FALSE;
}
+ 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;
}
-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[] = {
}
+/**
+ * @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;
}
pb = gdk_pixbuf_new_from_file(svgpath, &err);
if (!pb) {
- fprintf(stderr,
- "Error %d in gdk_pixbuf_new_from_file: %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;
}