diff mupdf-source/source/svg/svg-run.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/source/svg/svg-run.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1823 @@
+// Copyright (C) 2004-2025 Artifex Software, Inc.
+//
+// This file is part of MuPDF.
+//
+// MuPDF is free software: you can redistribute it and/or modify it under the
+// terms of the GNU Affero General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// MuPDF 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 Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
+//
+// Alternative licensing terms are available from the licensor.
+// For commercial licensing, see <https://www.artifex.com/> or contact
+// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
+// CA 94129, USA, for further information.
+
+#include "mupdf/fitz.h"
+#include "svg-imp.h"
+
+#include <string.h>
+#include <math.h>
+
+/* default page size */
+#define DEF_WIDTH 612
+#define DEF_HEIGHT 792
+#define DEF_FONTSIZE 12
+
+#define MAX_USE_DEPTH 100
+
+typedef struct svg_state
+{
+	fz_matrix transform;
+	fz_stroke_state *stroke;
+	int use_depth;
+
+	float viewport_w, viewport_h;
+	float viewbox_w, viewbox_h, viewbox_size;
+	float fontsize;
+
+	float opacity;
+
+	int fill_rule;
+	int fill_is_set;
+	float fill_color[3];
+	float fill_opacity;
+
+	int stroke_is_set;
+	float stroke_color[3];
+	float stroke_opacity;
+
+	const char *font_family;
+	int is_bold;
+	int is_italic;
+	int text_anchor;
+} svg_state;
+
+static void svg_parse_common(fz_context *ctx, svg_document *doc, fz_xml *node, svg_state *state);
+static void svg_run_element(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, const svg_state *state);
+
+void svg_begin_state(fz_context *ctx, svg_state *child, const svg_state *parent)
+{
+	memcpy(child, parent, sizeof(svg_state));
+	child->stroke = fz_clone_stroke_state(ctx, parent->stroke);
+}
+
+void svg_end_state(fz_context *ctx, svg_state *child)
+{
+	fz_drop_stroke_state(ctx, child->stroke);
+}
+
+static void svg_fill(fz_context *ctx, fz_device *dev, svg_document *doc, fz_path *path, svg_state *state)
+{
+	float opacity = state->opacity * state->fill_opacity;
+	if (path)
+		fz_fill_path(ctx, dev, path, state->fill_rule, state->transform, fz_device_rgb(ctx), state->fill_color, opacity, fz_default_color_params);
+}
+
+static void svg_stroke(fz_context *ctx, fz_device *dev, svg_document *doc, fz_path *path, svg_state *state)
+{
+	float opacity = state->opacity * state->stroke_opacity;
+	if (path)
+		fz_stroke_path(ctx, dev, path, state->stroke, state->transform, fz_device_rgb(ctx), state->stroke_color, opacity, fz_default_color_params);
+}
+
+static void svg_draw_path(fz_context *ctx, fz_device *dev, svg_document *doc, fz_path *path, svg_state *state)
+{
+	if (state->fill_is_set)
+		svg_fill(ctx, dev, doc, path, state);
+	if (state->stroke_is_set)
+		svg_stroke(ctx, dev, doc, path, state);
+}
+
+/*
+	We use the MAGIC number 0.551915 as a bezier subdivision to approximate
+	a quarter circle arc. The reasons for this can be found here:
+	http://mechanicalexpressions.com/explore/geometric-modeling/circle-spline-approximation.pdf
+*/
+static const float MAGIC_CIRCLE = 0.551915f;
+
+static void approx_circle(fz_context *ctx, fz_path *path, float cx, float cy, float rx, float ry)
+{
+	float mx = rx * MAGIC_CIRCLE;
+	float my = ry * MAGIC_CIRCLE;
+	fz_moveto(ctx, path, cx, cy+ry);
+	fz_curveto(ctx, path, cx + mx, cy + ry, cx + rx, cy + my, cx + rx, cy);
+	fz_curveto(ctx, path, cx + rx, cy - my, cx + mx, cy - ry, cx, cy - ry);
+	fz_curveto(ctx, path, cx - mx, cy - ry, cx - rx, cy - my, cx - rx, cy);
+	fz_curveto(ctx, path, cx - rx, cy + my, cx - mx, cy + ry, cx, cy + ry);
+	fz_closepath(ctx, path);
+}
+
+static void
+svg_run_rect(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *node, const svg_state *inherit_state)
+{
+	svg_state local_state;
+
+	char *x_att = fz_xml_att(node, "x");
+	char *y_att = fz_xml_att(node, "y");
+	char *w_att = fz_xml_att(node, "width");
+	char *h_att = fz_xml_att(node, "height");
+	char *rx_att = fz_xml_att(node, "rx");
+	char *ry_att = fz_xml_att(node, "ry");
+
+	float x = 0;
+	float y = 0;
+	float w = 0;
+	float h = 0;
+	float rx = 0;
+	float ry = 0;
+
+	fz_path *path = NULL;
+
+	fz_var(path);
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+		svg_parse_common(ctx, doc, node, &local_state);
+
+		if (x_att) x = svg_parse_length(x_att, local_state.viewbox_w, local_state.fontsize);
+		if (y_att) y = svg_parse_length(y_att, local_state.viewbox_h, local_state.fontsize);
+		if (w_att) w = svg_parse_length(w_att, local_state.viewbox_w, local_state.fontsize);
+		if (h_att) h = svg_parse_length(h_att, local_state.viewbox_h, local_state.fontsize);
+		if (rx_att) rx = svg_parse_length(rx_att, local_state.viewbox_w, local_state.fontsize);
+		if (ry_att) ry = svg_parse_length(ry_att, local_state.viewbox_h, local_state.fontsize);
+
+		if (rx_att && !ry_att)
+			ry = rx;
+		if (ry_att && !rx_att)
+			rx = ry;
+		if (rx > w * 0.5f)
+			rx = w * 0.5f;
+		if (ry > h * 0.5f)
+			ry = h * 0.5f;
+
+		if (w <= 0 || h <= 0)
+			return;
+
+		path = fz_new_path(ctx);
+		if (rx == 0 || ry == 0)
+		{
+			fz_moveto(ctx, path, x, y);
+			fz_lineto(ctx, path, x + w, y);
+			fz_lineto(ctx, path, x + w, y + h);
+			fz_lineto(ctx, path, x, y + h);
+		}
+		else
+		{
+			float rxs = rx * MAGIC_CIRCLE;
+			float rys = rx * MAGIC_CIRCLE;
+			fz_moveto(ctx, path, x + w - rx, y);
+			fz_curveto(ctx, path, x + w - rxs, y, x + w, y + rys, x + w, y + ry);
+			fz_lineto(ctx, path, x + w, y + h - ry);
+			fz_curveto(ctx, path, x + w, y + h - rys, x + w - rxs, y + h, x + w - rx, y + h);
+			fz_lineto(ctx, path, x + rx, y + h);
+			fz_curveto(ctx, path, x + rxs, y + h, x, y + h - rys, x, y + h - rx);
+			fz_lineto(ctx, path, x, y + rx);
+			fz_curveto(ctx, path, x, y + rxs, x + rxs, y, x + rx, y);
+		}
+		fz_closepath(ctx, path);
+
+		svg_draw_path(ctx, dev, doc, path, &local_state);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_path(ctx, path);
+		svg_end_state(ctx, &local_state);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+}
+
+static void
+svg_run_circle(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *node, const svg_state *inherit_state)
+{
+	svg_state local_state;
+
+	char *cx_att = fz_xml_att(node, "cx");
+	char *cy_att = fz_xml_att(node, "cy");
+	char *r_att = fz_xml_att(node, "r");
+
+	float cx = 0;
+	float cy = 0;
+	float r = 0;
+	fz_path *path = NULL;
+
+	fz_var(path);
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+		svg_parse_common(ctx, doc, node, &local_state);
+
+		if (cx_att) cx = svg_parse_length(cx_att, local_state.viewbox_w, local_state.fontsize);
+		if (cy_att) cy = svg_parse_length(cy_att, local_state.viewbox_h, local_state.fontsize);
+		if (r_att) r = svg_parse_length(r_att, local_state.viewbox_size, 12);
+
+		if (r > 0)
+		{
+			path = fz_new_path(ctx);
+			approx_circle(ctx, path, cx, cy, r, r);
+			svg_draw_path(ctx, dev, doc, path, &local_state);
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_path(ctx, path);
+		svg_end_state(ctx, &local_state);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+}
+
+static void
+svg_run_ellipse(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *node, const svg_state *inherit_state)
+{
+	svg_state local_state;
+
+	char *cx_att = fz_xml_att(node, "cx");
+	char *cy_att = fz_xml_att(node, "cy");
+	char *rx_att = fz_xml_att(node, "rx");
+	char *ry_att = fz_xml_att(node, "ry");
+
+	float cx = 0;
+	float cy = 0;
+	float rx = 0;
+	float ry = 0;
+
+	fz_path *path = NULL;
+
+	fz_var(path);
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+		svg_parse_common(ctx, doc, node, &local_state);
+
+		if (cx_att) cx = svg_parse_length(cx_att, local_state.viewbox_w, local_state.fontsize);
+		if (cy_att) cy = svg_parse_length(cy_att, local_state.viewbox_h, local_state.fontsize);
+		if (rx_att) rx = svg_parse_length(rx_att, local_state.viewbox_w, local_state.fontsize);
+		if (ry_att) ry = svg_parse_length(ry_att, local_state.viewbox_h, local_state.fontsize);
+
+		if (rx > 0 && ry > 0)
+		{
+			path = fz_new_path(ctx);
+			approx_circle(ctx, path, cx, cy, rx, ry);
+			svg_draw_path(ctx, dev, doc, path, &local_state);
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_path(ctx, path);
+		svg_end_state(ctx, &local_state);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+svg_run_line(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *node, const svg_state *inherit_state)
+{
+	svg_state local_state;
+	fz_path *path = NULL;
+
+	char *x1_att = fz_xml_att(node, "x1");
+	char *y1_att = fz_xml_att(node, "y1");
+	char *x2_att = fz_xml_att(node, "x2");
+	char *y2_att = fz_xml_att(node, "y2");
+
+	float x1 = 0;
+	float y1 = 0;
+	float x2 = 0;
+	float y2 = 0;
+
+	fz_var(path);
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+		svg_parse_common(ctx, doc, node, &local_state);
+
+		if (x1_att) x1 = svg_parse_length(x1_att, local_state.viewbox_w, local_state.fontsize);
+		if (y1_att) y1 = svg_parse_length(y1_att, local_state.viewbox_h, local_state.fontsize);
+		if (x2_att) x2 = svg_parse_length(x2_att, local_state.viewbox_w, local_state.fontsize);
+		if (y2_att) y2 = svg_parse_length(y2_att, local_state.viewbox_h, local_state.fontsize);
+
+		if (local_state.stroke_is_set)
+		{
+			path = fz_new_path(ctx);
+			fz_moveto(ctx, path, x1, y1);
+			fz_lineto(ctx, path, x2, y2);
+			svg_stroke(ctx, dev, doc, path, &local_state);
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_path(ctx, path);
+		svg_end_state(ctx, &local_state);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static fz_path *
+svg_parse_polygon_imp(fz_context *ctx, svg_document *doc, fz_xml *node, int doclose)
+{
+	fz_path *path;
+
+	const char *str = fz_xml_att(node, "points");
+	float number;
+	float args[2];
+	int nargs;
+	int isfirst;
+
+	if (!str)
+		return NULL;
+
+	isfirst = 1;
+	nargs = 0;
+
+	path = fz_new_path(ctx);
+	fz_try(ctx)
+	{
+		while (*str)
+		{
+			while (svg_is_whitespace_or_comma(*str))
+				str ++;
+
+			if (svg_is_digit(*str))
+			{
+				str = svg_lex_number(&number, str);
+				args[nargs++] = number;
+			}
+
+			if (nargs == 2)
+			{
+				if (isfirst)
+				{
+					fz_moveto(ctx, path, args[0], args[1]);
+					isfirst = 0;
+				}
+				else
+				{
+					fz_lineto(ctx, path, args[0], args[1]);
+				}
+				nargs = 0;
+			}
+		}
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_path(ctx, path);
+		fz_rethrow(ctx);
+	}
+
+	return path;
+}
+
+static void
+svg_run_polyline(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *node, const svg_state *inherit_state)
+{
+	svg_state local_state;
+	fz_path *path = NULL;
+
+	fz_var(path);
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+		svg_parse_common(ctx, doc, node, &local_state);
+
+		if (local_state.stroke_is_set)
+		{
+			path = svg_parse_polygon_imp(ctx, doc, node, 0);
+			svg_stroke(ctx, dev, doc, path, &local_state);
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_path(ctx, path);
+		svg_end_state(ctx, &local_state);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+svg_run_polygon(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *node, const svg_state *inherit_state)
+{
+	svg_state local_state;
+	fz_path *path = NULL;
+
+	fz_var(path);
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+		svg_parse_common(ctx, doc, node, &local_state);
+
+		path = svg_parse_polygon_imp(ctx, doc, node, 1);
+			svg_draw_path(ctx, dev, doc, path, &local_state);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_path(ctx, path);
+		svg_end_state(ctx, &local_state);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+svg_add_arc_segment(fz_context *ctx, fz_path *path, fz_matrix mtx, float th0, float th1, int iscw)
+{
+	float t, d;
+	fz_point p;
+
+	while (th1 < th0)
+		th1 += FZ_PI * 2;
+
+	d = FZ_PI / 180; /* 1-degree precision */
+
+	if (iscw)
+	{
+		for (t = th0 + d; t < th1 - d/2; t += d)
+		{
+			p = fz_transform_point_xy(cosf(t), sinf(t), mtx);
+			fz_lineto(ctx, path, p.x, p.y);
+		}
+	}
+	else
+	{
+		th0 += FZ_PI * 2;
+		for (t = th0 - d; t > th1 + d/2; t -= d)
+		{
+			p = fz_transform_point_xy(cosf(t), sinf(t), mtx);
+			fz_lineto(ctx, path, p.x, p.y);
+		}
+	}
+}
+
+static float
+angle_between(const fz_point u, const fz_point v)
+{
+	float det = u.x * v.y - u.y * v.x;
+	float sign = (det < 0 ? -1 : 1);
+	float magu = u.x * u.x + u.y * u.y;
+	float magv = v.x * v.x + v.y * v.y;
+	float udotv = u.x * v.x + u.y * v.y;
+	float t = udotv / (magu * magv);
+	/* guard against rounding errors when near |1| (where acos will return NaN) */
+	if (t < -1) t = -1;
+	if (t > 1) t = 1;
+	return sign * acosf(t);
+}
+
+static void
+svg_add_arc(fz_context *ctx, fz_path *path,
+	float size_x, float size_y, float rotation_angle,
+	int is_large_arc, int is_clockwise,
+	float point_x, float point_y)
+{
+	fz_matrix rotmat, revmat;
+	fz_matrix mtx;
+	fz_point pt;
+	float rx, ry;
+	float x1, y1, x2, y2;
+	float x1t, y1t;
+	float cxt, cyt, cx, cy;
+	float t1, t2, t3;
+	float sign;
+	float th1, dth;
+
+	pt = fz_currentpoint(ctx, path);
+	x1 = pt.x;
+	y1 = pt.y;
+	x2 = point_x;
+	y2 = point_y;
+	rx = size_x;
+	ry = size_y;
+
+	if (is_clockwise != is_large_arc)
+		sign = 1;
+	else
+		sign = -1;
+
+	rotmat = fz_rotate(rotation_angle);
+	revmat = fz_rotate(-rotation_angle);
+
+	/* http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes */
+	/* Conversion from endpoint to center parameterization */
+
+	/* F.6.6.1 -- ensure radii are positive and non-zero */
+	rx = fabsf(rx);
+	ry = fabsf(ry);
+	if (rx < 0.001f || ry < 0.001f || (x1 == x2 && y1 == y2))
+	{
+		fz_lineto(ctx, path, x2, y2);
+		return;
+	}
+
+	/* F.6.5.1 */
+	pt.x = (x1 - x2) / 2;
+	pt.y = (y1 - y2) / 2;
+	pt = fz_transform_vector(pt, revmat);
+	x1t = pt.x;
+	y1t = pt.y;
+
+	/* F.6.6.2 -- ensure radii are large enough */
+	t1 = (x1t * x1t) / (rx * rx) + (y1t * y1t) / (ry * ry);
+	if (t1 > 1)
+	{
+		rx = rx * sqrtf(t1);
+		ry = ry * sqrtf(t1);
+	}
+
+	/* F.6.5.2 */
+	t1 = (rx * rx * ry * ry) - (rx * rx * y1t * y1t) - (ry * ry * x1t * x1t);
+	t2 = (rx * rx * y1t * y1t) + (ry * ry * x1t * x1t);
+	t3 = t1 / t2;
+	/* guard against rounding errors; sqrt of negative numbers is bad for your health */
+	if (t3 < 0) t3 = 0;
+	t3 = sqrtf(t3);
+
+	cxt = sign * t3 * (rx * y1t) / ry;
+	cyt = sign * t3 * -(ry * x1t) / rx;
+
+	/* F.6.5.3 */
+	pt.x = cxt;
+	pt.y = cyt;
+	pt = fz_transform_vector(pt, rotmat);
+	cx = pt.x + (x1 + x2) / 2;
+	cy = pt.y + (y1 + y2) / 2;
+
+	/* F.6.5.4 */
+	{
+		fz_point coord1, coord2, coord3, coord4;
+		coord1.x = 1;
+		coord1.y = 0;
+		coord2.x = (x1t - cxt) / rx;
+		coord2.y = (y1t - cyt) / ry;
+		coord3.x = (x1t - cxt) / rx;
+		coord3.y = (y1t - cyt) / ry;
+		coord4.x = (-x1t - cxt) / rx;
+		coord4.y = (-y1t - cyt) / ry;
+		th1 = angle_between(coord1, coord2);
+		dth = angle_between(coord3, coord4);
+		if (dth < 0 && !is_clockwise)
+			dth += ((FZ_PI / 180) * 360);
+		if (dth > 0 && is_clockwise)
+			dth -= ((FZ_PI / 180) * 360);
+	}
+
+	mtx = fz_pre_scale(fz_pre_rotate(fz_translate(cx, cy), rotation_angle), rx, ry);
+	svg_add_arc_segment(ctx, path, mtx, th1, th1 + dth, is_clockwise);
+
+	fz_lineto(ctx, path, point_x, point_y);
+}
+
+static void
+svg_parse_path_data(fz_context *ctx, fz_path *path, const char *str)
+{
+	fz_point p;
+	float x1, y1, x2, y2;
+
+	int cmd;
+	float number;
+	float args[7];
+	int nargs;
+
+	/* saved control point for smooth curves */
+	int reset_smooth = 1;
+	float smooth_x = 0.0f;
+	float smooth_y = 0.0f;
+
+	cmd = 0;
+	nargs = 0;
+
+	fz_moveto(ctx, path, 0.0f, 0.0f); /* for the case of opening 'm' */
+
+	while (*str)
+	{
+		while (svg_is_whitespace_or_comma(*str))
+			str ++;
+
+		/* arcto flag arguments are 1-character 0 or 1 */
+		if ((cmd == 'a' || cmd == 'A') && (nargs == 3 || nargs == 4) && (*str == '0' || *str == '1'))
+		{
+			args[nargs++] = *str++ - '0';
+		}
+		else if (svg_is_digit(*str))
+		{
+			str = svg_lex_number(&number, str);
+			if (nargs == nelem(args))
+			{
+				fz_warn(ctx, "stack overflow in path data");
+				return;
+			}
+			args[nargs++] = number;
+		}
+		else if (svg_is_alpha(*str))
+		{
+			if (nargs != 0)
+			{
+				fz_warn(ctx, "syntax error in path data (wrong number of parameters to '%c')", cmd);
+				return;
+			}
+			cmd = *str++;
+		}
+		else if (*str == 0)
+		{
+			return;
+		}
+		else
+		{
+			fz_warn(ctx, "syntax error in path data: '%c'", *str);
+			return;
+		}
+
+		if (reset_smooth)
+		{
+			smooth_x = 0.0f;
+			smooth_y = 0.0f;
+		}
+
+		reset_smooth = 1;
+
+		switch (cmd)
+		{
+		case 'M':
+			if (nargs == 2)
+			{
+				fz_moveto(ctx, path, args[0], args[1]);
+				nargs = 0;
+				cmd = 'L'; /* implicit lineto after */
+			}
+			break;
+
+		case 'm':
+			if (nargs == 2)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_moveto(ctx, path, p.x + args[0], p.y + args[1]);
+				nargs = 0;
+				cmd = 'l'; /* implicit lineto after */
+			}
+			break;
+
+		case 'Z':
+		case 'z':
+			if (nargs == 0)
+			{
+				fz_closepath(ctx, path);
+			}
+			break;
+
+		case 'L':
+			if (nargs == 2)
+			{
+				fz_lineto(ctx, path, args[0], args[1]);
+				nargs = 0;
+			}
+			break;
+
+		case 'l':
+			if (nargs == 2)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, p.x + args[0], p.y + args[1]);
+				nargs = 0;
+			}
+			break;
+
+		case 'H':
+			if (nargs == 1)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, args[0], p.y);
+				nargs = 0;
+			}
+			break;
+
+		case 'h':
+			if (nargs == 1)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, p.x + args[0], p.y);
+				nargs = 0;
+			}
+			break;
+
+		case 'V':
+			if (nargs == 1)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, p.x, args[0]);
+				nargs = 0;
+			}
+			break;
+
+		case 'v':
+			if (nargs == 1)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, p.x, p.y + args[0]);
+				nargs = 0;
+			}
+			break;
+
+		case 'C':
+			reset_smooth = 0;
+			if (nargs == 6)
+			{
+				fz_curveto(ctx, path, args[0], args[1], args[2], args[3], args[4], args[5]);
+				smooth_x = args[4] - args[2];
+				smooth_y = args[5] - args[3];
+				nargs = 0;
+			}
+			break;
+
+		case 'c':
+			reset_smooth = 0;
+			if (nargs == 6)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_curveto(ctx, path,
+					p.x + args[0], p.y + args[1],
+					p.x + args[2], p.y + args[3],
+					p.x + args[4], p.y + args[5]);
+				smooth_x = args[4] - args[2];
+				smooth_y = args[5] - args[3];
+				nargs = 0;
+			}
+			break;
+
+		case 'S':
+			reset_smooth = 0;
+			if (nargs == 4)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_curveto(ctx, path,
+					p.x + smooth_x, p.y + smooth_y,
+					args[0], args[1],
+					args[2], args[3]);
+				smooth_x = args[2] - args[0];
+				smooth_y = args[3] - args[1];
+				nargs = 0;
+			}
+			break;
+
+		case 's':
+			reset_smooth = 0;
+			if (nargs == 4)
+			{
+				p = fz_currentpoint(ctx, path);
+				fz_curveto(ctx, path,
+					p.x + smooth_x, p.y + smooth_y,
+					p.x + args[0], p.y + args[1],
+					p.x + args[2], p.y + args[3]);
+				smooth_x = args[2] - args[0];
+				smooth_y = args[3] - args[1];
+				nargs = 0;
+			}
+			break;
+
+		case 'Q':
+			reset_smooth = 0;
+			if (nargs == 4)
+			{
+				p = fz_currentpoint(ctx, path);
+				x1 = args[0];
+				y1 = args[1];
+				x2 = args[2];
+				y2 = args[3];
+				fz_curveto(ctx, path,
+					(p.x + 2 * x1) / 3, (p.y + 2 * y1) / 3,
+					(x2 + 2 * x1) / 3, (y2 + 2 * y1) / 3,
+					x2, y2);
+				smooth_x = x2 - x1;
+				smooth_y = y2 - y1;
+				nargs = 0;
+			}
+			break;
+
+		case 'q':
+			reset_smooth = 0;
+			if (nargs == 4)
+			{
+				p = fz_currentpoint(ctx, path);
+				x1 = args[0] + p.x;
+				y1 = args[1] + p.y;
+				x2 = args[2] + p.x;
+				y2 = args[3] + p.y;
+				fz_curveto(ctx, path,
+					(p.x + 2 * x1) / 3, (p.y + 2 * y1) / 3,
+					(x2 + 2 * x1) / 3, (y2 + 2 * y1) / 3,
+					x2, y2);
+				smooth_x = x2 - x1;
+				smooth_y = y2 - y1;
+				nargs = 0;
+			}
+			break;
+
+		case 'T':
+			reset_smooth = 0;
+			if (nargs == 2)
+			{
+				p = fz_currentpoint(ctx, path);
+				x1 = p.x + smooth_x;
+				y1 = p.y + smooth_y;
+				x2 = args[0];
+				y2 = args[1];
+				fz_curveto(ctx, path,
+					(p.x + 2 * x1) / 3, (p.y + 2 * y1) / 3,
+					(x2 + 2 * x1) / 3, (y2 + 2 * y1) / 3,
+					x2, y2);
+				smooth_x = x2 - x1;
+				smooth_y = y2 - y1;
+				nargs = 0;
+			}
+			break;
+
+		case 't':
+			reset_smooth = 0;
+			if (nargs == 2)
+			{
+				p = fz_currentpoint(ctx, path);
+				x1 = p.x + smooth_x;
+				y1 = p.y + smooth_y;
+				x2 = args[0] + p.x;
+				y2 = args[1] + p.y;
+				fz_curveto(ctx, path,
+					(p.x + 2 * x1) / 3, (p.y + 2 * y1) / 3,
+					(x2 + 2 * x1) / 3, (y2 + 2 * y1) / 3,
+					x2, y2);
+				smooth_x = x2 - x1;
+				smooth_y = y2 - y1;
+				nargs = 0;
+			}
+			break;
+
+		case 'A':
+			if (nargs == 7)
+			{
+				svg_add_arc(ctx, path, args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
+				nargs = 0;
+			}
+			break;
+		case 'a':
+			if (nargs == 7)
+			{
+				p = fz_currentpoint(ctx, path);
+				svg_add_arc(ctx, path, args[0], args[1], args[2], args[3], args[4], args[5] + p.x, args[6] + p.y);
+				nargs = 0;
+			}
+			break;
+
+		case 0:
+			if (nargs != 0)
+			{
+				fz_warn(ctx, "path data must begin with a command");
+				return;
+			}
+			break;
+
+		default:
+			fz_warn(ctx, "unrecognized command in path data: '%c'", cmd);
+			return;
+		}
+	}
+}
+
+static void
+svg_run_path(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *node, const svg_state *inherit_state)
+{
+	svg_state local_state;
+	fz_path *path = NULL;
+
+	const char *d_att = fz_xml_att(node, "d");
+	/* unused: char *path_length_att = fz_xml_att(node, "pathLength"); */
+
+	fz_var(path);
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+		svg_parse_common(ctx, doc, node, &local_state);
+
+		if (d_att)
+		{
+			path = fz_new_path(ctx);
+			svg_parse_path_data(ctx, path, d_att);
+			svg_draw_path(ctx, dev, doc, path, &local_state);
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_path(ctx, path);
+		svg_end_state(ctx, &local_state);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+/* svg, symbol, image, foreignObject establish new viewports */
+static void
+svg_parse_viewport(fz_context *ctx, svg_document *doc, fz_xml *node, svg_state *state)
+{
+	char *w_att = fz_xml_att(node, "width");
+	char *h_att = fz_xml_att(node, "height");
+
+	if (w_att)
+		state->viewport_w = svg_parse_length(w_att, state->viewbox_w, state->fontsize);
+	if (h_att)
+		state->viewport_h = svg_parse_length(h_att, state->viewbox_h, state->fontsize);
+
+}
+
+static void
+svg_lex_viewbox(const char *s, float *x, float *y, float *w, float *h)
+{
+	*x = *y = *w = *h = 0;
+	while (svg_is_whitespace_or_comma(*s)) ++s;
+	if (svg_is_digit(*s)) s = svg_lex_number(x, s);
+	while (svg_is_whitespace_or_comma(*s)) ++s;
+	if (svg_is_digit(*s)) s = svg_lex_number(y, s);
+	while (svg_is_whitespace_or_comma(*s)) ++s;
+	if (svg_is_digit(*s)) s = svg_lex_number(w, s);
+	while (svg_is_whitespace_or_comma(*s)) ++s;
+	if (svg_is_digit(*s)) s = svg_lex_number(h, s);
+}
+
+static int
+svg_parse_preserve_aspect_ratio(const char *att, int *x, int *y)
+{
+	*x = *y = 1;
+	if (strstr(att, "none")) return 0;
+	if (strstr(att, "xMin")) *x = 0;
+	if (strstr(att, "xMid")) *x = 1;
+	if (strstr(att, "xMax")) *x = 2;
+	if (strstr(att, "YMin")) *y = 0;
+	if (strstr(att, "YMid")) *y = 1;
+	if (strstr(att, "YMax")) *y = 2;
+	return 1;
+}
+
+/* svg, symbol, image, foreignObject plus marker, pattern, view can use viewBox to set the transform */
+static void
+svg_parse_viewbox(fz_context *ctx, svg_document *doc, fz_xml *node, svg_state *state)
+{
+	char *viewbox_att = fz_xml_att(node, "viewBox");
+	char *preserve_att = fz_xml_att(node, "preserveAspectRatio");
+	if (viewbox_att)
+	{
+		/* scale and translate to fit [minx miny minx+w miny+h] to [0 0 viewport.w viewport.h] */
+		float min_x, min_y, box_w, box_h, sx, sy;
+		int align_x=1, align_y=1, preserve=1;
+		float pad_x=0, pad_y=0;
+
+		svg_lex_viewbox(viewbox_att, &min_x, &min_y, &box_w, &box_h);
+		sx = state->viewport_w / box_w;
+		sy = state->viewport_h / box_h;
+
+		if (preserve_att)
+			preserve = svg_parse_preserve_aspect_ratio(preserve_att, &align_x, &align_y);
+		if (preserve)
+		{
+			sx = sy = fz_min(sx, sy);
+			if (align_x == 1) pad_x = (box_w * sx - state->viewport_w) / 2;
+			if (align_x == 2) pad_x = (box_w * sx - state->viewport_w);
+			if (align_y == 1) pad_y = (box_h * sy - state->viewport_h) / 2;
+			if (align_y == 2) pad_y = (box_h * sy - state->viewport_h);
+			state->transform = fz_concat(fz_translate(-pad_x, -pad_y), state->transform);
+		}
+		state->transform = fz_concat(fz_scale(sx, sy), state->transform);
+		state->transform = fz_concat(fz_translate(-min_x, -min_y), state->transform);
+		state->viewbox_w = box_w;
+		state->viewbox_h = box_h;
+		state->viewbox_size = sqrtf(box_w*box_w + box_h*box_h) / sqrtf(2);
+	}
+}
+
+static const char *linecap_table[] = { "butt", "round", "square" };
+static const char *linejoin_table[] = { "miter", "round", "bevel" };
+
+/* parse transform and presentation attributes */
+static void
+svg_parse_common(fz_context *ctx, svg_document *doc, fz_xml *node, svg_state *state)
+{
+	fz_stroke_state *stroke = state->stroke;
+
+	char *transform_att = fz_xml_att(node, "transform");
+
+	char *font_size_att = fz_xml_att(node, "font-size");
+
+	char *style_att = fz_xml_att(node, "style");
+
+	// TODO: clip, clip-path, clip-rule
+
+	char *opacity_att = fz_xml_att(node, "opacity");
+
+	char *fill_att = fz_xml_att(node, "fill");
+	char *fill_rule_att = fz_xml_att(node, "fill-rule");
+	char *fill_opacity_att = fz_xml_att(node, "fill-opacity");
+
+	char *stroke_att = fz_xml_att(node, "stroke");
+	char *stroke_opacity_att = fz_xml_att(node, "stroke-opacity");
+	char *stroke_width_att = fz_xml_att(node, "stroke-width");
+	char *stroke_linecap_att = fz_xml_att(node, "stroke-linecap");
+	char *stroke_linejoin_att = fz_xml_att(node, "stroke-linejoin");
+	char *stroke_miterlimit_att = fz_xml_att(node, "stroke-miterlimit");
+	// TODO: stroke-dasharray, stroke-dashoffset
+
+	// TODO: marker, marker-start, marker-mid, marker-end
+
+	// TODO: overflow
+	// TODO: mask
+
+	/* Dirty hack scans of CSS style */
+	if (style_att)
+	{
+		svg_parse_color_from_style(ctx, doc, style_att,
+			&state->fill_is_set, state->fill_color,
+			&state->stroke_is_set, state->stroke_color);
+	}
+
+	if (transform_att)
+	{
+		state->transform = svg_parse_transform(ctx, doc, transform_att, state->transform);
+	}
+
+	if (font_size_att)
+	{
+		state->fontsize = svg_parse_length(font_size_att, state->fontsize, state->fontsize);
+	}
+	else
+	{
+		state->fontsize = svg_parse_number_from_style(ctx, doc, style_att, "font-size", state->fontsize);
+	}
+
+	if (opacity_att)
+	{
+		state->opacity = svg_parse_number(opacity_att, 0, 1, state->opacity);
+	}
+
+	if (fill_att)
+	{
+		if (!strcmp(fill_att, "none"))
+		{
+			state->fill_is_set = 0;
+		}
+		else
+		{
+			state->fill_is_set = 1;
+			svg_parse_color(ctx, doc, fill_att, state->fill_color);
+		}
+	}
+
+	if (fill_opacity_att)
+		state->fill_opacity = svg_parse_number(fill_opacity_att, 0, 1, state->fill_opacity);
+
+	if (fill_rule_att)
+	{
+		if (!strcmp(fill_rule_att, "nonzero"))
+			state->fill_rule = 0;
+		if (!strcmp(fill_rule_att, "evenodd"))
+			state->fill_rule = 1;
+	}
+
+	if (stroke_att)
+	{
+		if (!strcmp(stroke_att, "none"))
+		{
+			state->stroke_is_set = 0;
+		}
+		else
+		{
+			state->stroke_is_set = 1;
+			svg_parse_color(ctx, doc, stroke_att, state->stroke_color);
+		}
+	}
+
+	if (stroke_opacity_att)
+		state->stroke_opacity = svg_parse_number(stroke_opacity_att, 0, 1, state->stroke_opacity);
+
+	if (stroke_width_att)
+	{
+		if (!strcmp(stroke_width_att, "inherit"))
+			;
+		else
+			stroke->linewidth = svg_parse_length(stroke_width_att, state->viewbox_size, state->fontsize);
+	}
+	else
+	{
+		stroke->linewidth = svg_parse_number_from_style(ctx, doc, style_att, "stroke-width", state->stroke->linewidth);
+	}
+
+	if (stroke_linecap_att)
+	{
+		if (!strcmp(stroke_linecap_att, "butt"))
+			stroke->start_cap = FZ_LINECAP_BUTT;
+		if (!strcmp(stroke_linecap_att, "round"))
+			stroke->start_cap = FZ_LINECAP_ROUND;
+		if (!strcmp(stroke_linecap_att, "square"))
+			stroke->start_cap = FZ_LINECAP_SQUARE;
+	}
+	else
+	{
+		stroke->start_cap = svg_parse_enum_from_style(ctx, doc, style_att, "stroke-linecap",
+			nelem(linecap_table), linecap_table, FZ_LINECAP_BUTT);
+	}
+
+	stroke->dash_cap = stroke->start_cap;
+	stroke->end_cap = stroke->start_cap;
+
+	if (stroke_linejoin_att)
+	{
+		if (!strcmp(stroke_linejoin_att, "miter"))
+			stroke->linejoin = FZ_LINEJOIN_MITER;
+		if (!strcmp(stroke_linejoin_att, "round"))
+			stroke->linejoin = FZ_LINEJOIN_ROUND;
+		if (!strcmp(stroke_linejoin_att, "bevel"))
+			stroke->linejoin = FZ_LINEJOIN_BEVEL;
+	}
+	else
+	{
+		stroke->linejoin = svg_parse_enum_from_style(ctx, doc, style_att, "stroke-linejoin",
+			nelem(linejoin_table), linejoin_table, FZ_LINEJOIN_MITER);
+	}
+
+	if (stroke_miterlimit_att)
+	{
+		if (!strcmp(stroke_miterlimit_att, "inherit"))
+			;
+		else
+			stroke->miterlimit = svg_parse_length(stroke_miterlimit_att, state->viewbox_size, state->fontsize);
+	}
+	else
+	{
+		stroke->miterlimit = svg_parse_number_from_style(ctx, doc, style_att, "stroke-miterlimit", state->stroke->miterlimit);
+	}
+}
+
+static void
+svg_parse_font_attributes(fz_context *ctx, svg_document *doc, fz_xml *node, svg_state *state, char *buf, int buf_size)
+{
+	char *style_att = fz_xml_att(node, "style");
+	char *font_family_att = fz_xml_att(node, "font-family");
+	char *font_weight_att = fz_xml_att(node, "font-weight");
+	char *font_style_att = fz_xml_att(node, "font-style");
+	char *text_anchor_att = fz_xml_att(node, "text-anchor");
+
+	if (font_family_att)
+		fz_strlcpy(buf, font_family_att, buf_size);
+	else
+		svg_parse_string_from_style(ctx, doc, style_att, "font-family", buf, buf_size, state->font_family);
+	state->font_family = buf;
+
+	if (font_weight_att)
+	{
+		state->is_bold = atoi(font_weight_att) > 400;
+		if (!strcmp(font_weight_att, "bold")) state->is_bold = 1;
+		if (!strcmp(font_weight_att, "bolder")) state->is_bold = 1;
+	}
+	else
+	{
+		static const char *is_bold_table[] = {
+			"normal", "100", "200", "300", "400", "bold", "bolder", "500", "600", "700", "800", "900"
+		};
+		state->is_bold = svg_parse_enum_from_style(ctx, doc, style_att, "font-weight",
+			nelem(is_bold_table), is_bold_table, state->is_bold ? 5 : 0) >= 5;
+	}
+
+	if (font_style_att)
+	{
+		state->is_italic = 0;
+		if (!strcmp(font_style_att, "italic")) state->is_italic = 1;
+		if (!strcmp(font_style_att, "oblique")) state->is_italic = 1;
+	}
+	else
+	{
+		static const char *is_italic_table[] = {
+			"normal", "italic", "oblique"
+		};
+		state->is_italic = svg_parse_enum_from_style(ctx, doc, style_att, "font-style",
+			nelem(is_italic_table), is_italic_table, state->is_italic) >= 1;
+	}
+
+	if (text_anchor_att)
+	{
+		state->text_anchor = 0;
+		if (!strcmp(text_anchor_att, "middle")) state->text_anchor = 1;
+		if (!strcmp(text_anchor_att, "end")) state->text_anchor = 2;
+	}
+	else
+	{
+		static const char *text_anchor_table[] = {
+			"start", "middle", "end"
+		};
+		state->text_anchor = svg_parse_enum_from_style(ctx, doc, style_att, "text-anchor",
+			nelem(text_anchor_table), text_anchor_table, state->text_anchor);
+	}
+}
+
+static void
+svg_run_svg(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, const svg_state *inherit_state)
+{
+	svg_state local_state;
+	fz_xml *node;
+
+	char *w_att = fz_xml_att(root, "width");
+	char *h_att = fz_xml_att(root, "height");
+	char *viewbox_att = fz_xml_att(root, "viewBox");
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+
+		/* get default viewport from viewBox if width and/or height is missing */
+		if (viewbox_att && (!w_att || !h_att))
+		{
+			float x, y;
+			svg_lex_viewbox(viewbox_att, &x, &y, &local_state.viewbox_w, &local_state.viewbox_h);
+			if (!w_att) local_state.viewport_w = local_state.viewbox_w;
+			if (!h_att) local_state.viewport_h = local_state.viewbox_h;
+		}
+
+		svg_parse_viewport(ctx, doc, root, &local_state);
+		svg_parse_viewbox(ctx, doc, root, &local_state);
+		svg_parse_common(ctx, doc, root, &local_state);
+
+		for (node = fz_xml_down(root); node; node = fz_xml_next(node))
+			svg_run_element(ctx, dev, doc, node, &local_state);
+	}
+	fz_always(ctx)
+		svg_end_state(ctx, &local_state);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+svg_run_g(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, const svg_state *inherit_state)
+{
+	svg_state local_state;
+	fz_xml *node;
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+		svg_parse_common(ctx, doc, root, &local_state);
+
+		for (node = fz_xml_down(root); node; node = fz_xml_next(node))
+			svg_run_element(ctx, dev, doc, node, &local_state);
+	}
+	fz_always(ctx)
+		svg_end_state(ctx, &local_state);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+svg_run_use_symbol(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *use, fz_xml *symbol, const svg_state *inherit_state)
+{
+	svg_state local_state;
+	fz_xml *node;
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+
+		svg_parse_viewport(ctx, doc, use, &local_state);
+		svg_parse_viewbox(ctx, doc, use, &local_state);
+
+		for (node = fz_xml_down(symbol); node; node = fz_xml_next(node))
+			svg_run_element(ctx, dev, doc, node, &local_state);
+	}
+	fz_always(ctx)
+		svg_end_state(ctx, &local_state);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static int
+is_use_cycle(fz_xml *use, fz_xml *symbol)
+{
+	/* If "use" is a direct child of "symbol", we have a recursive symbol/use definition! */
+	while (use)
+	{
+		if (use == symbol)
+			return 1;
+		use = fz_xml_up(use);
+	}
+	return 0;
+}
+
+static void
+svg_run_use(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, const svg_state *inherit_state)
+{
+	svg_state local_state;
+
+	char *href_att = fz_xml_att_alt(root, "xlink:href", "href");
+	char *x_att = fz_xml_att(root, "x");
+	char *y_att = fz_xml_att(root, "y");
+	fz_xml *linked = NULL;
+
+	float x = 0;
+	float y = 0;
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+
+		if (++local_state.use_depth > MAX_USE_DEPTH)
+		{
+			fz_warn(ctx, "svg: too much recursion");
+			break;
+		}
+
+		svg_parse_common(ctx, doc, root, &local_state);
+		if (x_att) x = svg_parse_length(x_att, local_state.viewbox_w, local_state.fontsize);
+		if (y_att) y = svg_parse_length(y_att, local_state.viewbox_h, local_state.fontsize);
+
+		local_state.transform = fz_concat(fz_translate(x, y), local_state.transform);
+
+		if (href_att && href_att[0] == '#')
+		{
+			linked = fz_tree_lookup(ctx, doc->idmap, href_att + 1);
+			if (linked)
+			{
+				if (is_use_cycle(root, linked))
+					fz_warn(ctx, "svg: cyclic <use> reference");
+
+				if (fz_xml_is_tag(linked, "symbol"))
+					svg_run_use_symbol(ctx, dev, doc, root, linked, &local_state);
+				else
+					svg_run_element(ctx, dev, doc, linked, &local_state);
+			}
+			else
+			{
+				fz_warn(ctx, "svg: cannot find linked symbol");
+			}
+		}
+		else
+		{
+			fz_warn(ctx, "svg: cannot find linked symbol");
+		}
+
+	}
+	fz_always(ctx)
+		svg_end_state(ctx, &local_state);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+svg_run_image(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, const svg_state *inherit_state)
+{
+	svg_state local_state;
+	float x=0, y=0, w=0, h=0;
+	const char *data;
+
+	static const char *jpeg_uri = "data:image/jpeg;base64,";
+	static const char *png_uri = "data:image/png;base64,";
+
+	char *href_att = fz_xml_att_alt(root, "xlink:href", "href");
+	char *x_att = fz_xml_att(root, "x");
+	char *y_att = fz_xml_att(root, "y");
+	char *w_att = fz_xml_att(root, "width");
+	char *h_att = fz_xml_att(root, "height");
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+
+		svg_parse_common(ctx, doc, root, &local_state);
+		if (x_att) x = svg_parse_length(x_att, local_state.viewbox_w, local_state.fontsize);
+		if (y_att) y = svg_parse_length(y_att, local_state.viewbox_h, local_state.fontsize);
+		if (w_att) w = svg_parse_length(w_att, local_state.viewbox_w, local_state.fontsize);
+		if (h_att) h = svg_parse_length(h_att, local_state.viewbox_h, local_state.fontsize);
+
+		if (w <= 0 || h <= 0)
+			break; // out of try-catch
+
+		if (!href_att)
+			break; // out of try-catch
+
+		local_state.transform = fz_concat(fz_translate(x, y), local_state.transform);
+		local_state.transform = fz_concat(fz_scale(w, h), local_state.transform);
+
+		if (!strncmp(href_att, jpeg_uri, strlen(jpeg_uri)))
+			data = href_att + strlen(jpeg_uri);
+		else if (!strncmp(href_att, png_uri, strlen(png_uri)))
+			data = href_att + strlen(png_uri);
+		else
+			data = NULL;
+		if (data)
+		{
+			fz_image *img = NULL;
+			fz_buffer *buf;
+
+			fz_var(img);
+
+			buf = fz_new_buffer_from_base64(ctx, data, 0);
+			fz_try(ctx)
+			{
+				fz_matrix orient;
+				img = fz_new_image_from_buffer(ctx, buf);
+				orient = fz_image_orientation_matrix(ctx, img);
+				local_state.transform = fz_concat(orient, local_state.transform);
+				fz_fill_image(ctx, dev, img, local_state.transform, 1, fz_default_color_params);
+			}
+			fz_always(ctx)
+			{
+				fz_drop_buffer(ctx, buf);
+				fz_drop_image(ctx, img);
+			}
+			fz_catch(ctx)
+			{
+				fz_rethrow_if(ctx, FZ_ERROR_SYSTEM);
+				fz_report_error(ctx);
+				fz_warn(ctx, "svg: ignoring embedded image '%s'", href_att);
+			}
+		}
+		else if (doc->zip)
+		{
+			char path[2048];
+			fz_buffer *buf = NULL;
+			fz_image *img = NULL;
+
+			fz_var(buf);
+			fz_var(img);
+
+			fz_strlcpy(path, doc->base_uri, sizeof path);
+			fz_strlcat(path, "/", sizeof path);
+			fz_strlcat(path, href_att, sizeof path);
+			fz_urldecode(path);
+
+			fz_try(ctx)
+			{
+				fz_matrix orient;
+				buf = fz_read_archive_entry(ctx, doc->zip, path);
+				img = fz_new_image_from_buffer(ctx, buf);
+				orient = fz_image_orientation_matrix(ctx, img);
+				local_state.transform = fz_concat(orient, local_state.transform);
+				fz_fill_image(ctx, dev, img, local_state.transform, 1, fz_default_color_params);
+			}
+			fz_always(ctx)
+			{
+				fz_drop_buffer(ctx, buf);
+				fz_drop_image(ctx, img);
+			}
+			fz_catch(ctx)
+			{
+				fz_rethrow_if(ctx, FZ_ERROR_SYSTEM);
+				fz_report_error(ctx);
+				fz_warn(ctx, "svg: ignoring external image '%s'", href_att);
+			}
+		}
+		else
+		{
+			fz_warn(ctx, "svg: ignoring external image '%s'", href_att);
+		}
+
+	}
+	fz_always(ctx)
+		svg_end_state(ctx, &local_state);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static fz_font *
+svg_load_font(fz_context *ctx, const svg_state *state)
+{
+	int bold = state->is_bold;
+	int italic = state->is_italic;
+	int mono = 0;
+	int serif = 1;
+
+	/* scan font-family property for common fallback names */
+
+	if (!mono && strstr(state->font_family, "monospace")) mono = 1;
+	if (!mono && strstr(state->font_family, "Courier")) mono = 1;
+
+	if (serif && strstr(state->font_family, "sans-serif")) serif = 0;
+	if (serif && strstr(state->font_family, "Arial")) serif = 0;
+	if (serif && strstr(state->font_family, "Helvetica")) serif = 0;
+
+	if (mono) {
+		if (bold) {
+			if (italic) return fz_new_base14_font(ctx, "Courier-BoldOblique");
+			else return fz_new_base14_font(ctx, "Courier-Bold");
+		} else {
+			if (italic) return fz_new_base14_font(ctx, "Courier-Oblique");
+			else return fz_new_base14_font(ctx, "Courier");
+		}
+	} else if (serif) {
+		if (bold) {
+			if (italic) return fz_new_base14_font(ctx, "Times-BoldItalic");
+			else return fz_new_base14_font(ctx, "Times-Bold");
+		} else {
+			if (italic) return fz_new_base14_font(ctx, "Times-Italic");
+			else return fz_new_base14_font(ctx, "Times-Roman");
+		}
+	} else {
+		if (bold) {
+			if (italic) return fz_new_base14_font(ctx, "Helvetica-BoldOblique");
+			else return fz_new_base14_font(ctx, "Helvetica-Bold");
+		} else {
+			if (italic) return fz_new_base14_font(ctx, "Helvetica-Oblique");
+			else return fz_new_base14_font(ctx, "Helvetica");
+		}
+	}
+}
+
+static fz_matrix
+svg_run_text_string(fz_context *ctx, fz_device *dev, fz_matrix trm, const char *s, const svg_state *state)
+{
+	fz_font *font = NULL;
+	fz_text *text = NULL;
+
+	fz_var(font);
+	fz_var(text);
+
+	fz_try(ctx)
+	{
+		font = svg_load_font(ctx, state);
+		text = fz_new_text(ctx);
+
+		if (state->text_anchor > 0)
+		{
+			fz_matrix adv = fz_measure_string(ctx, font, trm, s, 0, 0, FZ_BIDI_LTR, FZ_LANG_UNSET);
+			if (state->text_anchor == 1)
+				trm.e -= (adv.e - trm.e) / 2;
+			else if (state->text_anchor == 2)
+				trm.e -= (adv.e - trm.e);
+		}
+
+		trm = fz_show_string(ctx, text, font, trm, s, 0, 0, FZ_BIDI_LTR, FZ_LANG_UNSET);
+
+		if (state->fill_is_set)
+			fz_fill_text(ctx, dev, text,
+				state->transform,
+				fz_device_rgb(ctx), state->fill_color,
+				state->opacity,
+				fz_default_color_params);
+		if (state->stroke_is_set)
+			fz_stroke_text(ctx, dev, text,
+				state->stroke,
+				state->transform,
+				fz_device_rgb(ctx), state->stroke_color,
+				state->opacity,
+				fz_default_color_params);
+		if (!state->fill_is_set && !state->stroke_is_set)
+			fz_ignore_text(ctx, dev, text, state->transform);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_text(ctx, text);
+		fz_drop_font(ctx, font);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+
+	return trm;
+}
+
+static void
+svg_collapse_whitespace(char *start, int is_first, int is_last)
+{
+	int c, last_c = (is_first ? ' ' : 0);
+	char *s, *p;
+	s = p = start;
+	while ((c = *s++) != 0)
+	{
+		if (c == '\n' || c == '\r')
+			continue;
+		if (c == '\t')
+			c = ' ';
+		if (c == ' ' && last_c == ' ')
+			continue;
+		*p++ = last_c = c;
+	}
+	if (is_last && p > start && p[-1] == ' ')
+		--p;
+	*p = 0;
+}
+
+static fz_matrix
+svg_run_text(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, const svg_state *inherit_state,
+	float x, float y, int is_first, int is_last)
+{
+	svg_state local_state;
+	char font_family[100];
+	fz_xml *node;
+	fz_matrix trm;
+	int cif, cil;
+	char *text;
+
+	char *x_att = fz_xml_att(root, "x");
+	char *y_att = fz_xml_att(root, "y");
+	char *dx_att = fz_xml_att(root, "dx");
+	char *dy_att = fz_xml_att(root, "dy");
+
+	fz_try(ctx)
+	{
+		svg_begin_state(ctx, &local_state, inherit_state);
+
+		svg_parse_common(ctx, doc, root, &local_state);
+		svg_parse_font_attributes(ctx, doc, root, &local_state, font_family, sizeof font_family);
+
+		trm = fz_scale(local_state.fontsize, -local_state.fontsize);
+		trm.e = x;
+		trm.f = y;
+
+		if (x_att) trm.e = svg_parse_length(x_att, local_state.viewbox_w, local_state.fontsize);
+		if (y_att) trm.f = svg_parse_length(y_att, local_state.viewbox_h, local_state.fontsize);
+
+		if (dx_att) trm.e += svg_parse_length(dx_att, local_state.viewbox_w, local_state.fontsize);
+		if (dy_att) trm.f += svg_parse_length(dy_att, local_state.viewbox_h, local_state.fontsize);
+
+		cif = is_first;
+		for (node = fz_xml_down(root); node; node = fz_xml_next(node))
+		{
+			cil = is_last && !fz_xml_next(node);
+			text = fz_xml_text(node);
+			if (text)
+			{
+				svg_collapse_whitespace(text, cif, cil);
+				trm = svg_run_text_string(ctx, dev, trm, text, &local_state);
+			}
+			else if (fz_xml_is_tag(node, "tspan"))
+				trm = svg_run_text(ctx, dev, doc, node, &local_state, trm.e, trm.f, cif, cil);
+			else if (fz_xml_is_tag(node, "textPath"))
+				trm = svg_run_text(ctx, dev, doc, node, &local_state, trm.e, trm.f, cif, cil);
+			cif = 0;
+		}
+	}
+	fz_always(ctx)
+		svg_end_state(ctx, &local_state);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return trm;
+}
+
+static void
+svg_run_element(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, const svg_state *state)
+{
+	if (fz_xml_is_tag(root, "svg"))
+		svg_run_svg(ctx, dev, doc, root, state);
+
+	else if (fz_xml_is_tag(root, "g"))
+		svg_run_g(ctx, dev, doc, root, state);
+
+	else if (fz_xml_is_tag(root, "title"))
+		;
+	else if (fz_xml_is_tag(root, "desc"))
+		;
+
+	else if (fz_xml_is_tag(root, "defs"))
+		;
+	else if (fz_xml_is_tag(root, "symbol"))
+		;
+
+	else if (fz_xml_is_tag(root, "use"))
+		svg_run_use(ctx, dev, doc, root, state);
+
+	else if (fz_xml_is_tag(root, "path"))
+		svg_run_path(ctx, dev, doc, root, state);
+	else if (fz_xml_is_tag(root, "rect"))
+		svg_run_rect(ctx, dev, doc, root, state);
+	else if (fz_xml_is_tag(root, "circle"))
+		svg_run_circle(ctx, dev, doc, root, state);
+	else if (fz_xml_is_tag(root, "ellipse"))
+		svg_run_ellipse(ctx, dev, doc, root, state);
+	else if (fz_xml_is_tag(root, "line"))
+		svg_run_line(ctx, dev, doc, root, state);
+	else if (fz_xml_is_tag(root, "polyline"))
+		svg_run_polyline(ctx, dev, doc, root, state);
+	else if (fz_xml_is_tag(root, "polygon"))
+		svg_run_polygon(ctx, dev, doc, root, state);
+
+	else if (fz_xml_is_tag(root, "image"))
+		svg_run_image(ctx, dev, doc, root, state);
+
+	else if (fz_xml_is_tag(root, "text"))
+		svg_run_text(ctx, dev, doc, root, state, 0, 0, 1, 1);
+
+	else
+	{
+		/* ignore unrecognized tags */
+	}
+}
+
+void
+svg_parse_document_bounds(fz_context *ctx, svg_document *doc, fz_xml *root)
+{
+	char *version_att;
+	char *w_att;
+	char *h_att;
+	char *viewbox_att;
+	int version;
+
+	if (!fz_xml_is_tag(root, "svg"))
+		fz_throw(ctx, FZ_ERROR_SYNTAX, "expected svg element (found %s)", fz_xml_tag(root));
+
+	version_att = fz_xml_att(root, "version");
+	w_att = fz_xml_att(root, "width");
+	h_att = fz_xml_att(root, "height");
+	viewbox_att = fz_xml_att(root, "viewBox");
+
+	version = 10;
+	if (version_att)
+		version = fz_atof(version_att) * 10;
+
+	if (version > 12)
+		fz_warn(ctx, "svg document version is newer than we support");
+
+	/* If no width or height attributes, then guess from the viewbox */
+	if (w_att == NULL && h_att == NULL && viewbox_att != NULL)
+	{
+		float min_x, min_y, box_w, box_h;
+		svg_lex_viewbox(viewbox_att, &min_x, &min_y, &box_w, &box_h);
+		doc->width = box_w;
+		doc->height = box_h;
+	}
+	else
+	{
+		doc->width = DEF_WIDTH;
+		if (w_att)
+			doc->width = svg_parse_length(w_att, doc->width, DEF_FONTSIZE);
+
+		doc->height = DEF_HEIGHT;
+		if (h_att)
+			doc->height = svg_parse_length(h_att, doc->height, DEF_FONTSIZE);
+	}
+}
+
+void
+svg_run_document(fz_context *ctx, svg_document *doc, fz_xml *root, fz_device *dev, fz_matrix ctm)
+{
+	svg_state state;
+
+	svg_parse_document_bounds(ctx, doc, root);
+
+	/* Initial graphics state */
+	state.transform = ctm;
+	state.stroke = fz_new_stroke_state(ctx);
+	state.use_depth = 0;
+
+	state.viewport_w = DEF_WIDTH;
+	state.viewport_h = DEF_HEIGHT;
+
+	state.viewbox_w = DEF_WIDTH;
+	state.viewbox_h = DEF_HEIGHT;
+	state.viewbox_size = sqrtf(DEF_WIDTH*DEF_WIDTH + DEF_HEIGHT*DEF_HEIGHT) / sqrtf(2);
+
+	state.fontsize = 12;
+
+	state.opacity = 1;
+
+	state.fill_rule = 0;
+
+	state.fill_is_set = 1;
+	state.fill_color[0] = 0;
+	state.fill_color[1] = 0;
+	state.fill_color[2] = 0;
+	state.fill_opacity = 1;
+
+	state.stroke_is_set = 0;
+	state.stroke_color[0] = 0;
+	state.stroke_color[1] = 0;
+	state.stroke_color[2] = 0;
+	state.stroke_opacity = 1;
+
+	state.font_family = "serif";
+	state.is_bold = 0;
+	state.is_italic = 0;
+	state.text_anchor = 0;
+
+	fz_try(ctx)
+	{
+		svg_run_svg(ctx, dev, doc, root, &state);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_stroke_state(ctx, state.stroke);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+}