diff mupdf-source/source/xps/xps-path.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/xps/xps-path.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1093 @@
+// Copyright (C) 2004-2024 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 "xps-imp.h"
+
+#include <math.h>
+#include <string.h>
+#include <stdlib.h>
+
+static char *
+xps_parse_float_array(fz_context *ctx, xps_document *doc, char *s, int num, int *obtained, float *x)
+{
+	int k = 0;
+
+	if (s == NULL || *s == 0)
+	{
+		if (obtained)
+			*obtained = k;
+		return NULL;
+	}
+
+	while (*s)
+	{
+		while (*s == 0x0d || *s == '\t' || *s == ' ' || *s == 0x0a)
+			s++;
+		x[k] = fz_strtof(s, &s);
+		while (*s == 0x0d || *s == '\t' || *s == ' ' || *s == 0x0a)
+			s++;
+		if (*s == ',')
+			s++;
+		if (++k == num)
+			break;
+	}
+	if (obtained)
+		*obtained = k;
+	return s;
+}
+
+char *
+xps_parse_point(fz_context *ctx, xps_document *doc, char *s_in, float *x, float *y)
+{
+	char *s_out = s_in;
+	float xy[2];
+	int obtained = 0;
+
+	s_out = xps_parse_float_array(ctx, doc, s_out, 2, &obtained, &xy[0]);
+	if (obtained >= 2)
+	{
+		*x = xy[0];
+		*y = xy[1];
+	}
+	return s_out;
+}
+
+/* Draw an arc segment transformed by the matrix, we approximate with straight
+ * line segments. We cannot use the fz_arc function because they only draw
+ * circular arcs, we need to transform the line to make them elliptical but
+ * without transforming the line width.
+ *
+ * We are guaranteed that on entry the point is at the point that would be
+ * calculated by th0, and on exit, a point is generated for us at th0.
+ */
+static void
+xps_draw_arc_segment(fz_context *ctx, xps_document *doc, 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);
+		}
+	}
+}
+
+/* Given two vectors find the angle between them. */
+static float
+angle_between(fz_point u, 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);
+}
+
+/*
+	Some explanation of the parameters here is warranted. See:
+
+	http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
+
+	Add an arc segment to path, that describes a section of an elliptical
+	arc from the current point of path to (point_x,point_y), such that:
+
+	The arc segment is taken from an elliptical arc of semi major radius
+	size_x, semi minor radius size_y, where the semi major axis of the
+	ellipse is rotated by rotation_angle.
+
+	If is_large_arc, then the arc segment is selected to be > 180 degrees.
+
+	If is_clockwise, then the arc sweeps clockwise.
+*/
+static void
+xps_draw_arc(fz_context *ctx, xps_document *doc, 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);
+	xps_draw_arc_segment(ctx, doc, path, mtx, th1, th1 + dth, is_clockwise);
+
+	fz_lineto(ctx, path, point_x, point_y);
+}
+
+fz_path *
+xps_parse_abbreviated_geometry(fz_context *ctx, xps_document *doc, char *geom, int *fill_rule)
+{
+	fz_path *path;
+	char **args = NULL;
+	char **pargs;
+	char *s = geom;
+	fz_point pt;
+	int i, n;
+	int cmd, old;
+	float x1, y1, x2, y2, x3, y3;
+	float smooth_x, smooth_y; /* saved cubic bezier control point for smooth curves */
+	int reset_smooth;
+
+	fz_var(args);
+
+	path = fz_new_path(ctx);
+
+	fz_try(ctx)
+	{
+		args = fz_malloc_array(ctx, strlen(geom) + 1, char*);
+		pargs = args;
+
+		while (*s)
+		{
+			if ((*s >= 'A' && *s <= 'Z') || (*s >= 'a' && *s <= 'z'))
+			{
+				*pargs++ = s++;
+			}
+			else if ((*s >= '0' && *s <= '9') || *s == '.' || *s == '+' || *s == '-' || *s == 'e' || *s == 'E')
+			{
+				*pargs++ = s;
+				while ((*s >= '0' && *s <= '9') || *s == '.' || *s == '+' || *s == '-' || *s == 'e' || *s == 'E')
+					s ++;
+			}
+			else
+			{
+				s++;
+			}
+		}
+
+		*pargs = s;
+
+		n = pargs - args;
+		i = 0;
+
+		old = 0;
+
+		reset_smooth = 1;
+		smooth_x = 0;
+		smooth_y = 0;
+
+		while (i < n)
+		{
+			cmd = args[i][0];
+			if (cmd == '+' || cmd == '.' || cmd == '-' || (cmd >= '0' && cmd <= '9'))
+				cmd = old; /* it's a number, repeat old command */
+			else
+				i ++;
+
+			if (reset_smooth)
+			{
+				smooth_x = 0;
+				smooth_y = 0;
+			}
+
+			reset_smooth = 1;
+
+			switch (cmd)
+			{
+			case 'F':
+				if (i >= n) break;
+				*fill_rule = atoi(args[i]);
+				i ++;
+				break;
+
+			case 'M':
+				if (i + 1 >= n) break;
+				fz_moveto(ctx, path, fz_atof(args[i]), fz_atof(args[i+1]));
+				i += 2;
+				break;
+			case 'm':
+				if (i + 1 >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				fz_moveto(ctx, path, pt.x + fz_atof(args[i]), pt.y + fz_atof(args[i+1]));
+				i += 2;
+				break;
+
+			case 'L':
+				if (i + 1 >= n) break;
+				fz_lineto(ctx, path, fz_atof(args[i]), fz_atof(args[i+1]));
+				i += 2;
+				break;
+			case 'l':
+				if (i + 1 >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, pt.x + fz_atof(args[i]), pt.y + fz_atof(args[i+1]));
+				i += 2;
+				break;
+
+			case 'H':
+				if (i >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, fz_atof(args[i]), pt.y);
+				i += 1;
+				break;
+			case 'h':
+				if (i >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, pt.x + fz_atof(args[i]), pt.y);
+				i += 1;
+				break;
+
+			case 'V':
+				if (i >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, pt.x, fz_atof(args[i]));
+				i += 1;
+				break;
+			case 'v':
+				if (i >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				fz_lineto(ctx, path, pt.x, pt.y + fz_atof(args[i]));
+				i += 1;
+				break;
+
+			case 'C':
+				if (i + 5 >= n) break;
+				x1 = fz_atof(args[i+0]);
+				y1 = fz_atof(args[i+1]);
+				x2 = fz_atof(args[i+2]);
+				y2 = fz_atof(args[i+3]);
+				x3 = fz_atof(args[i+4]);
+				y3 = fz_atof(args[i+5]);
+				fz_curveto(ctx, path, x1, y1, x2, y2, x3, y3);
+				i += 6;
+				reset_smooth = 0;
+				smooth_x = x3 - x2;
+				smooth_y = y3 - y2;
+				break;
+
+			case 'c':
+				if (i + 5 >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				x1 = fz_atof(args[i+0]) + pt.x;
+				y1 = fz_atof(args[i+1]) + pt.y;
+				x2 = fz_atof(args[i+2]) + pt.x;
+				y2 = fz_atof(args[i+3]) + pt.y;
+				x3 = fz_atof(args[i+4]) + pt.x;
+				y3 = fz_atof(args[i+5]) + pt.y;
+				fz_curveto(ctx, path, x1, y1, x2, y2, x3, y3);
+				i += 6;
+				reset_smooth = 0;
+				smooth_x = x3 - x2;
+				smooth_y = y3 - y2;
+				break;
+
+			case 'S':
+				if (i + 3 >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				x1 = fz_atof(args[i+0]);
+				y1 = fz_atof(args[i+1]);
+				x2 = fz_atof(args[i+2]);
+				y2 = fz_atof(args[i+3]);
+				fz_curveto(ctx, path, pt.x + smooth_x, pt.y + smooth_y, x1, y1, x2, y2);
+				i += 4;
+				reset_smooth = 0;
+				smooth_x = x2 - x1;
+				smooth_y = y2 - y1;
+				break;
+
+			case 's':
+				if (i + 3 >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				x1 = fz_atof(args[i+0]) + pt.x;
+				y1 = fz_atof(args[i+1]) + pt.y;
+				x2 = fz_atof(args[i+2]) + pt.x;
+				y2 = fz_atof(args[i+3]) + pt.y;
+				fz_curveto(ctx, path, pt.x + smooth_x, pt.y + smooth_y, x1, y1, x2, y2);
+				i += 4;
+				reset_smooth = 0;
+				smooth_x = x2 - x1;
+				smooth_y = y2 - y1;
+				break;
+
+			case 'Q':
+				if (i + 3 >= n) break;
+				x1 = fz_atof(args[i+0]);
+				y1 = fz_atof(args[i+1]);
+				x2 = fz_atof(args[i+2]);
+				y2 = fz_atof(args[i+3]);
+				fz_quadto(ctx, path, x1, y1, x2, y2);
+				i += 4;
+				break;
+			case 'q':
+				if (i + 3 >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				x1 = fz_atof(args[i+0]) + pt.x;
+				y1 = fz_atof(args[i+1]) + pt.y;
+				x2 = fz_atof(args[i+2]) + pt.x;
+				y2 = fz_atof(args[i+3]) + pt.y;
+				fz_quadto(ctx, path, x1, y1, x2, y2);
+				i += 4;
+				break;
+
+			case 'A':
+				if (i + 6 >= n) break;
+				xps_draw_arc(ctx, doc, path,
+					fz_atof(args[i+0]), fz_atof(args[i+1]), fz_atof(args[i+2]),
+					atoi(args[i+3]), atoi(args[i+4]),
+					fz_atof(args[i+5]), fz_atof(args[i+6]));
+				i += 7;
+				break;
+			case 'a':
+				if (i + 6 >= n) break;
+				pt = fz_currentpoint(ctx, path);
+				xps_draw_arc(ctx, doc, path,
+					fz_atof(args[i+0]), fz_atof(args[i+1]), fz_atof(args[i+2]),
+					atoi(args[i+3]), atoi(args[i+4]),
+					fz_atof(args[i+5]) + pt.x, fz_atof(args[i+6]) + pt.y);
+				i += 7;
+				break;
+
+			case 'Z':
+			case 'z':
+				fz_closepath(ctx, path);
+				break;
+
+			default:
+				fz_warn(ctx, "ignoring invalid command '%c'", cmd);
+				if (old == cmd) /* avoid infinite loop */
+					i++;
+				break;
+			}
+
+			old = cmd;
+		}
+	}
+	fz_always(ctx)
+		fz_free(ctx, args);
+	fz_catch(ctx)
+	{
+		fz_drop_path(ctx, path);
+		fz_rethrow(ctx);
+	}
+
+	return path;
+}
+
+static void
+xps_parse_arc_segment(fz_context *ctx, xps_document *doc, fz_path *path, fz_xml *root, int stroking, int *skipped_stroke)
+{
+	/* ArcSegment pretty much follows the SVG algorithm for converting an
+	 * arc in endpoint representation to an arc in centerpoint
+	 * representation. Once in centerpoint it can be given to the
+	 * graphics library in the form of a postscript arc. */
+
+	float rotation_angle;
+	int is_large_arc, is_clockwise;
+	float point_x, point_y;
+	float size_x, size_y;
+	int is_stroked;
+
+	char *point_att = fz_xml_att(root, "Point");
+	char *size_att = fz_xml_att(root, "Size");
+	char *rotation_angle_att = fz_xml_att(root, "RotationAngle");
+	char *is_large_arc_att = fz_xml_att(root, "IsLargeArc");
+	char *sweep_direction_att = fz_xml_att(root, "SweepDirection");
+	char *is_stroked_att = fz_xml_att(root, "IsStroked");
+
+	if (!point_att || !size_att || !rotation_angle_att || !is_large_arc_att || !sweep_direction_att)
+	{
+		fz_warn(ctx, "ArcSegment element is missing attributes");
+		return;
+	}
+
+	is_stroked = 1;
+	if (is_stroked_att && !strcmp(is_stroked_att, "false"))
+			is_stroked = 0;
+	if (!is_stroked)
+		*skipped_stroke = 1;
+
+	point_x = point_y = 0;
+	size_x = size_y = 0;
+
+	xps_parse_point(ctx, doc, point_att, &point_x, &point_y);
+	xps_parse_point(ctx, doc, size_att, &size_x, &size_y);
+	rotation_angle = fz_atof(rotation_angle_att);
+	is_large_arc = !strcmp(is_large_arc_att, "true");
+	is_clockwise = !strcmp(sweep_direction_att, "Clockwise");
+
+	if (stroking && !is_stroked)
+	{
+		fz_moveto(ctx, path, point_x, point_y);
+		return;
+	}
+
+	xps_draw_arc(ctx, doc, path, size_x, size_y, rotation_angle, is_large_arc, is_clockwise, point_x, point_y);
+}
+
+static void
+xps_parse_poly_quadratic_bezier_segment(fz_context *ctx, xps_document *doc, fz_path *path, fz_xml *root, int stroking, int *skipped_stroke)
+{
+	char *points_att = fz_xml_att(root, "Points");
+	char *is_stroked_att = fz_xml_att(root, "IsStroked");
+	float x[2], y[2];
+	int is_stroked;
+	fz_point pt;
+	char *s;
+	int n;
+
+	if (!points_att)
+	{
+		fz_warn(ctx, "PolyQuadraticBezierSegment element has no points");
+		return;
+	}
+
+	is_stroked = 1;
+	if (is_stroked_att && !strcmp(is_stroked_att, "false"))
+			is_stroked = 0;
+	if (!is_stroked)
+		*skipped_stroke = 1;
+
+	s = points_att;
+	n = 0;
+	while (*s != 0)
+	{
+		while (*s == ' ') s++;
+		x[n] = y[n] = 0;
+		s = xps_parse_point(ctx, doc, s, &x[n], &y[n]);
+		n ++;
+		if (n == 2)
+		{
+			if (stroking && !is_stroked)
+			{
+				fz_moveto(ctx, path, x[1], y[1]);
+			}
+			else
+			{
+				pt = fz_currentpoint(ctx, path);
+				fz_curveto(ctx, path,
+						(pt.x + 2 * x[0]) / 3, (pt.y + 2 * y[0]) / 3,
+						(x[1] + 2 * x[0]) / 3, (y[1] + 2 * y[0]) / 3,
+						x[1], y[1]);
+			}
+			n = 0;
+		}
+	}
+}
+
+static void
+xps_parse_poly_bezier_segment(fz_context *ctx, xps_document *doc, fz_path *path, fz_xml *root, int stroking, int *skipped_stroke)
+{
+	char *points_att = fz_xml_att(root, "Points");
+	char *is_stroked_att = fz_xml_att(root, "IsStroked");
+	float x[3], y[3];
+	int is_stroked;
+	char *s;
+	int n;
+
+	if (!points_att)
+	{
+		fz_warn(ctx, "PolyBezierSegment element has no points");
+		return;
+	}
+
+	is_stroked = 1;
+	if (is_stroked_att && !strcmp(is_stroked_att, "false"))
+			is_stroked = 0;
+	if (!is_stroked)
+		*skipped_stroke = 1;
+
+	s = points_att;
+	n = 0;
+	while (*s != 0)
+	{
+		while (*s == ' ') s++;
+		x[n] = y[n] = 0;
+		s = xps_parse_point(ctx, doc, s, &x[n], &y[n]);
+		n ++;
+		if (n == 3)
+		{
+			if (stroking && !is_stroked)
+				fz_moveto(ctx, path, x[2], y[2]);
+			else
+				fz_curveto(ctx, path, x[0], y[0], x[1], y[1], x[2], y[2]);
+			n = 0;
+		}
+	}
+}
+
+static void
+xps_parse_poly_line_segment(fz_context *ctx, xps_document *doc, fz_path *path, fz_xml *root, int stroking, int *skipped_stroke)
+{
+	char *points_att = fz_xml_att(root, "Points");
+	char *is_stroked_att = fz_xml_att(root, "IsStroked");
+	int is_stroked;
+	float x, y;
+	char *s;
+
+	if (!points_att)
+	{
+		fz_warn(ctx, "PolyLineSegment element has no points");
+		return;
+	}
+
+	is_stroked = 1;
+	if (is_stroked_att && !strcmp(is_stroked_att, "false"))
+			is_stroked = 0;
+	if (!is_stroked)
+		*skipped_stroke = 1;
+
+	s = points_att;
+	while (*s != 0)
+	{
+		while (*s == ' ') s++;
+		x = y = 0;
+		s = xps_parse_point(ctx, doc, s, &x, &y);
+		if (stroking && !is_stroked)
+			fz_moveto(ctx, path, x, y);
+		else
+			fz_lineto(ctx, path, x, y);
+	}
+}
+
+static void
+xps_parse_path_figure(fz_context *ctx, xps_document *doc, fz_path *path, fz_xml *root, int stroking)
+{
+	fz_xml *node;
+
+	char *is_closed_att;
+	char *start_point_att;
+	char *is_filled_att;
+
+	int is_closed = 0;
+	int is_filled = 1;
+	float start_x = 0;
+	float start_y = 0;
+
+	int skipped_stroke = 0;
+
+	is_closed_att = fz_xml_att(root, "IsClosed");
+	start_point_att = fz_xml_att(root, "StartPoint");
+	is_filled_att = fz_xml_att(root, "IsFilled");
+
+	if (is_closed_att)
+		is_closed = !strcmp(is_closed_att, "true");
+	if (is_filled_att)
+		is_filled = !strcmp(is_filled_att, "true");
+	if (start_point_att)
+		xps_parse_point(ctx, doc, start_point_att, &start_x, &start_y);
+
+	if (!stroking && !is_filled) /* not filled, when filling */
+		return;
+
+	fz_moveto(ctx, path, start_x, start_y);
+
+	for (node = fz_xml_down(root); node; node = fz_xml_next(node))
+	{
+		if (fz_xml_is_tag(node, "ArcSegment"))
+			xps_parse_arc_segment(ctx, doc, path, node, stroking, &skipped_stroke);
+		if (fz_xml_is_tag(node, "PolyBezierSegment"))
+			xps_parse_poly_bezier_segment(ctx, doc, path, node, stroking, &skipped_stroke);
+		if (fz_xml_is_tag(node, "PolyLineSegment"))
+			xps_parse_poly_line_segment(ctx, doc, path, node, stroking, &skipped_stroke);
+		if (fz_xml_is_tag(node, "PolyQuadraticBezierSegment"))
+			xps_parse_poly_quadratic_bezier_segment(ctx, doc, path, node, stroking, &skipped_stroke);
+	}
+
+	if (is_closed)
+	{
+		if (stroking && skipped_stroke)
+			fz_lineto(ctx, path, start_x, start_y); /* we've skipped using fz_moveto... */
+		else
+			fz_closepath(ctx, path); /* no skipped segments, safe to closepath properly */
+	}
+}
+
+fz_path *
+xps_parse_path_geometry(fz_context *ctx, xps_document *doc, xps_resource *dict, fz_xml *root, int stroking, int *fill_rule)
+{
+	fz_xml *node;
+
+	char *figures_att;
+	char *fill_rule_att;
+	char *transform_att;
+
+	fz_xml *transform_tag = NULL;
+	fz_xml *figures_tag = NULL; /* only used by resource */
+
+	fz_matrix transform;
+	fz_path *path;
+
+	figures_att = fz_xml_att(root, "Figures");
+	fill_rule_att = fz_xml_att(root, "FillRule");
+	transform_att = fz_xml_att(root, "Transform");
+
+	for (node = fz_xml_down(root); node; node = fz_xml_next(node))
+	{
+		if (fz_xml_is_tag(node, "PathGeometry.Transform"))
+			transform_tag = fz_xml_down(node);
+	}
+
+	xps_resolve_resource_reference(ctx, doc, dict, &transform_att, &transform_tag, NULL);
+	xps_resolve_resource_reference(ctx, doc, dict, &figures_att, &figures_tag, NULL);
+
+	if (fill_rule_att)
+	{
+		if (!strcmp(fill_rule_att, "NonZero"))
+			*fill_rule = 1;
+		if (!strcmp(fill_rule_att, "EvenOdd"))
+			*fill_rule = 0;
+	}
+
+	transform = xps_parse_transform(ctx, doc, transform_att, transform_tag, fz_identity);
+
+	if (figures_att)
+		path = xps_parse_abbreviated_geometry(ctx, doc, figures_att, fill_rule);
+	else
+		path = fz_new_path(ctx);
+
+	fz_try(ctx)
+	{
+		if (figures_tag)
+			xps_parse_path_figure(ctx, doc, path, figures_tag, stroking);
+
+		for (node = fz_xml_down(root); node; node = fz_xml_next(node))
+		{
+			if (fz_xml_is_tag(node, "PathFigure"))
+				xps_parse_path_figure(ctx, doc, path, node, stroking);
+		}
+
+		if (transform_att || transform_tag)
+			fz_transform_path(ctx, path, transform);
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_path(ctx, path);
+		fz_rethrow(ctx);
+	}
+
+	return path;
+}
+
+static int
+xps_parse_line_cap(char *attr)
+{
+	if (attr)
+	{
+		if (!strcmp(attr, "Flat")) return 0;
+		if (!strcmp(attr, "Round")) return 1;
+		if (!strcmp(attr, "Square")) return 2;
+		if (!strcmp(attr, "Triangle")) return 3;
+	}
+	return 0;
+}
+
+void
+xps_clip(fz_context *ctx, xps_document *doc, fz_matrix ctm, xps_resource *dict, char *clip_att, fz_xml *clip_tag)
+{
+	fz_device *dev = doc->dev;
+	fz_path *path;
+	int fill_rule = 0;
+
+	if (clip_att)
+		path = xps_parse_abbreviated_geometry(ctx, doc, clip_att, &fill_rule);
+	else if (clip_tag)
+		path = xps_parse_path_geometry(ctx, doc, dict, clip_tag, 0, &fill_rule);
+	else
+		path = fz_new_path(ctx);
+	fz_try(ctx)
+		fz_clip_path(ctx, dev, path, fill_rule == 0, ctm, fz_infinite_rect);
+	fz_always(ctx)
+		fz_drop_path(ctx, path);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+void
+xps_parse_path(fz_context *ctx, xps_document *doc, fz_matrix ctm, char *base_uri, xps_resource *dict, fz_xml *root)
+{
+	fz_device *dev = doc->dev;
+
+	fz_xml *node;
+
+	char *fill_uri;
+	char *stroke_uri;
+	char *opacity_mask_uri;
+
+	char *transform_att;
+	char *clip_att;
+	char *data_att;
+	char *fill_att;
+	char *stroke_att;
+	char *opacity_att;
+	char *opacity_mask_att;
+
+	fz_xml *transform_tag = NULL;
+	fz_xml *clip_tag = NULL;
+	fz_xml *data_tag = NULL;
+	fz_xml *fill_tag = NULL;
+	fz_xml *stroke_tag = NULL;
+	fz_xml *opacity_mask_tag = NULL;
+
+	char *fill_opacity_att = NULL;
+	char *stroke_opacity_att = NULL;
+
+	char *stroke_dash_array_att;
+	char *stroke_dash_cap_att;
+	char *stroke_dash_offset_att;
+	char *stroke_end_line_cap_att;
+	char *stroke_start_line_cap_att;
+	char *stroke_line_join_att;
+	char *stroke_miter_limit_att;
+	char *stroke_thickness_att;
+
+	fz_stroke_state *stroke = NULL;
+	float samples[FZ_MAX_COLORS];
+	fz_colorspace *colorspace;
+	fz_path *path = NULL;
+	fz_path *stroke_path = NULL;
+	fz_rect area;
+	int fill_rule;
+	int dash_len = 0;
+
+	/*
+	 * Extract attributes and extended attributes.
+	 */
+
+	transform_att = fz_xml_att(root, "RenderTransform");
+	clip_att = fz_xml_att(root, "Clip");
+	data_att = fz_xml_att(root, "Data");
+	fill_att = fz_xml_att(root, "Fill");
+	stroke_att = fz_xml_att(root, "Stroke");
+	opacity_att = fz_xml_att(root, "Opacity");
+	opacity_mask_att = fz_xml_att(root, "OpacityMask");
+
+	stroke_dash_array_att = fz_xml_att(root, "StrokeDashArray");
+	stroke_dash_cap_att = fz_xml_att(root, "StrokeDashCap");
+	stroke_dash_offset_att = fz_xml_att(root, "StrokeDashOffset");
+	stroke_end_line_cap_att = fz_xml_att(root, "StrokeEndLineCap");
+	stroke_start_line_cap_att = fz_xml_att(root, "StrokeStartLineCap");
+	stroke_line_join_att = fz_xml_att(root, "StrokeLineJoin");
+	stroke_miter_limit_att = fz_xml_att(root, "StrokeMiterLimit");
+	stroke_thickness_att = fz_xml_att(root, "StrokeThickness");
+
+	for (node = fz_xml_down(root); node; node = fz_xml_next(node))
+	{
+		if (fz_xml_is_tag(node, "Path.RenderTransform"))
+			transform_tag = fz_xml_down(node);
+		if (fz_xml_is_tag(node, "Path.OpacityMask"))
+			opacity_mask_tag = fz_xml_down(node);
+		if (fz_xml_is_tag(node, "Path.Clip"))
+			clip_tag = fz_xml_down(node);
+		if (fz_xml_is_tag(node, "Path.Fill"))
+			fill_tag = fz_xml_down(node);
+		if (fz_xml_is_tag(node, "Path.Stroke"))
+			stroke_tag = fz_xml_down(node);
+		if (fz_xml_is_tag(node, "Path.Data"))
+			data_tag = fz_xml_down(node);
+	}
+
+	fill_uri = base_uri;
+	stroke_uri = base_uri;
+	opacity_mask_uri = base_uri;
+
+	xps_resolve_resource_reference(ctx, doc, dict, &data_att, &data_tag, NULL);
+	xps_resolve_resource_reference(ctx, doc, dict, &clip_att, &clip_tag, NULL);
+	xps_resolve_resource_reference(ctx, doc, dict, &transform_att, &transform_tag, NULL);
+	xps_resolve_resource_reference(ctx, doc, dict, &fill_att, &fill_tag, &fill_uri);
+	xps_resolve_resource_reference(ctx, doc, dict, &stroke_att, &stroke_tag, &stroke_uri);
+	xps_resolve_resource_reference(ctx, doc, dict, &opacity_mask_att, &opacity_mask_tag, &opacity_mask_uri);
+
+	/*
+	 * Act on the information we have gathered:
+	 */
+
+	if (!data_att && !data_tag)
+		return;
+
+	if (fz_xml_is_tag(fill_tag, "SolidColorBrush"))
+	{
+		fill_opacity_att = fz_xml_att(fill_tag, "Opacity");
+		fill_att = fz_xml_att(fill_tag, "Color");
+		fill_tag = NULL;
+	}
+
+	if (fz_xml_is_tag(stroke_tag, "SolidColorBrush"))
+	{
+		stroke_opacity_att = fz_xml_att(stroke_tag, "Opacity");
+		stroke_att = fz_xml_att(stroke_tag, "Color");
+		stroke_tag = NULL;
+	}
+
+	if (stroke_att || stroke_tag)
+	{
+		if (stroke_dash_array_att)
+		{
+			char *s = stroke_dash_array_att;
+
+			while (*s)
+			{
+				while (*s == ' ')
+					s++;
+				if (*s) /* needed in case of a space before the last quote */
+					dash_len++;
+
+				while (*s && *s != ' ')
+					s++;
+			}
+		}
+		stroke = fz_new_stroke_state_with_dash_len(ctx, dash_len);
+		stroke->start_cap = xps_parse_line_cap(stroke_start_line_cap_att);
+		stroke->dash_cap = xps_parse_line_cap(stroke_dash_cap_att);
+		stroke->end_cap = xps_parse_line_cap(stroke_end_line_cap_att);
+
+		stroke->linejoin = FZ_LINEJOIN_MITER_XPS;
+		if (stroke_line_join_att)
+		{
+			if (!strcmp(stroke_line_join_att, "Miter")) stroke->linejoin = FZ_LINEJOIN_MITER_XPS;
+			if (!strcmp(stroke_line_join_att, "Round")) stroke->linejoin = FZ_LINEJOIN_ROUND;
+			if (!strcmp(stroke_line_join_att, "Bevel")) stroke->linejoin = FZ_LINEJOIN_BEVEL;
+		}
+
+		stroke->miterlimit = 10;
+		if (stroke_miter_limit_att)
+			stroke->miterlimit = fz_atof(stroke_miter_limit_att);
+
+		stroke->linewidth = 1;
+		if (stroke_thickness_att)
+			stroke->linewidth = fz_atof(stroke_thickness_att);
+
+		stroke->dash_phase = 0;
+		stroke->dash_len = 0;
+		if (stroke_dash_array_att)
+		{
+			char *s = stroke_dash_array_att;
+
+			if (stroke_dash_offset_att)
+				stroke->dash_phase = fz_atof(stroke_dash_offset_att) * stroke->linewidth;
+
+			while (*s)
+			{
+				while (*s == ' ')
+					s++;
+				if (*s) /* needed in case of a space before the last quote */
+					stroke->dash_list[stroke->dash_len++] = fz_atof(s) * stroke->linewidth;
+				while (*s && *s != ' ')
+					s++;
+			}
+			if (dash_len > 0)
+			{
+				/* fz_stroke_path doesn't draw non-empty paths with phase length zero */
+				float phase_len = 0;
+				int i;
+				for (i = 0; i < dash_len; i++)
+					phase_len += stroke->dash_list[i];
+				if (phase_len == 0)
+					dash_len = 0;
+			}
+			stroke->dash_len = dash_len;
+		}
+	}
+
+	ctm = xps_parse_transform(ctx, doc, transform_att, transform_tag, ctm);
+
+	if (clip_att || clip_tag)
+		xps_clip(ctx, doc, ctm, dict, clip_att, clip_tag);
+
+	fz_try(ctx)
+	{
+		fill_rule = 0;
+		if (data_att)
+			path = xps_parse_abbreviated_geometry(ctx, doc, data_att, &fill_rule);
+		else if (data_tag)
+		{
+			path = xps_parse_path_geometry(ctx, doc, dict, data_tag, 0, &fill_rule);
+			// /home/sebras/src/jxr/fts_06xx.xps
+			if (stroke_att || stroke_tag)
+				stroke_path = xps_parse_path_geometry(ctx, doc, dict, data_tag, 1, &fill_rule);
+		}
+		if (!stroke_path)
+			stroke_path = path;
+
+		if (stroke_att || stroke_tag)
+		{
+			area = fz_bound_path(ctx, stroke_path, stroke, ctm);
+			if (stroke_path != path && (fill_att || fill_tag)) {
+				fz_rect bounds = fz_bound_path(ctx, path, NULL, ctm);
+				area = fz_union_rect(area, bounds);
+			}
+		}
+		else
+			area = fz_bound_path(ctx, path, NULL, ctm);
+
+		xps_begin_opacity(ctx, doc, ctm, area, opacity_mask_uri, dict, opacity_att, opacity_mask_tag);
+
+		if (fill_att)
+		{
+			xps_parse_color(ctx, doc, base_uri, fill_att, &colorspace, samples);
+			if (fill_opacity_att)
+				samples[0] *= fz_atof(fill_opacity_att);
+			xps_set_color(ctx, doc, colorspace, samples);
+			fz_fill_path(ctx, dev, path, fill_rule == 0, ctm,
+				doc->colorspace, doc->color, doc->alpha, fz_default_color_params);
+		}
+
+		if (fill_tag)
+		{
+			fz_clip_path(ctx, dev, path, fill_rule == 0, ctm, area);
+			xps_parse_brush(ctx, doc, ctm, area, fill_uri, dict, fill_tag);
+			fz_pop_clip(ctx, dev);
+		}
+
+		if (stroke_att)
+		{
+			xps_parse_color(ctx, doc, base_uri, stroke_att, &colorspace, samples);
+			if (stroke_opacity_att)
+				samples[0] *= fz_atof(stroke_opacity_att);
+			xps_set_color(ctx, doc, colorspace, samples);
+			fz_stroke_path(ctx, dev, stroke_path, stroke, ctm,
+				doc->colorspace, doc->color, doc->alpha, fz_default_color_params);
+		}
+
+		if (stroke_tag)
+		{
+			fz_clip_stroke_path(ctx, dev, stroke_path, stroke, ctm, area);
+			xps_parse_brush(ctx, doc, ctm, area, stroke_uri, dict, stroke_tag);
+			fz_pop_clip(ctx, dev);
+		}
+
+		xps_end_opacity(ctx, doc, opacity_mask_uri, dict, opacity_att, opacity_mask_tag);
+	}
+	fz_always(ctx)
+	{
+		if (stroke_path != path)
+			fz_drop_path(ctx, stroke_path);
+		fz_drop_path(ctx, path);
+		fz_drop_stroke_state(ctx, stroke);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	if (clip_att || clip_tag)
+		fz_pop_clip(ctx, dev);
+}