view mupdf-source/source/svg/svg-run.c @ 46:7ee69f120f19 default tip

>>>>> tag v1.26.5+1 for changeset b74429b0f5c4
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 11 Oct 2025 17:17:30 +0200
parents b50eed0cc0ef
children
line wrap: on
line source

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