/* Copyright (C) 2001-2019 Artifex Software, Inc.
   All Rights Reserved.

   This software is provided AS-IS with no warranty, either express or
   implied.

   This software is distributed under license and may not be copied,
   modified or distributed except as expressly authorized under the terms
   of the license contained in the file LICENSE in this distribution.

   Refer to licensing information at http://www.artifex.com or contact
   Artifex Software, Inc.,  1305 Grant Avenue - Suite 200, Novato,
   CA 94945, U.S.A., +1(415)492-9861, for further information.
*/

/* XPS interpreter - path (vector drawing) support */

#include "ghostxps.h"

char *
xps_get_real_params(char *s, int num, float *x)
{
    int k = 0;

    if (s != NULL && *s != 0) {
        while (*s)
        {
            while (*s == 0x0d || *s == '\t' || *s == ' ' || *s == 0x0a)
                s++;
            x[k] = (float)strtod(s, &s);
            while (*s == 0x0d || *s == '\t' || *s == ' ' || *s == 0x0a)
                s++;
            if (*s == ',')
                s++;
            if (++k == num)
                break;
        }
        return s;
    } else
        return NULL;
}

char *
xps_get_point(char *s_in, float *x, float *y)
{
    char *s_out = s_in;
    float xy[2];

    s_out = xps_get_real_params(s_out, 2, xy);
    *x = xy[0];
    *y = xy[1];
    return s_out;
}

void
xps_clip(xps_context_t *ctx)
{
    if (ctx->fill_rule == 0)
        gs_eoclip(ctx->pgs);
    else
        gs_clip(ctx->pgs);
    gs_newpath(ctx->pgs);
}

void
xps_fill(xps_context_t *ctx)
{
    if (gs_currentopacityalpha(ctx->pgs) < 0.001)
        gs_newpath(ctx->pgs);
    else if (ctx->fill_rule == 0) {
        if (gs_eofill(ctx->pgs) == gs_error_Remap_Color){
            xps_high_level_pattern(ctx);
            gs_eofill(ctx->pgs);
        }
    }
    else {
        if (gs_fill(ctx->pgs) == gs_error_Remap_Color){
            xps_high_level_pattern(ctx);
            gs_fill(ctx->pgs);
        }
    }
}

/* Draw an arc segment transformed by the matrix, we approximate with straight
 * line segments. We cannot use the gs_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 inline void
xps_draw_arc_segment(xps_context_t *ctx, gs_matrix *mtx, float th0, float th1, int iscw)
{
    float t, d;
    gs_point p;

    while (th1 < th0)
        th1 += (float)(M_PI * 2.0);

    d = (float)(1 * (M_PI / 180.0)); /* 1-degree precision */

    if (iscw)
    {
        for (t = th0 + d; t < th1 - d/2; t += d)
        {
            gs_point_transform(cos(t), sin(t), mtx, &p);
            gs_lineto(ctx->pgs, p.x, p.y);
        }
    }
    else
    {
        th0 += (float)(M_PI * 2);
        for (t = th0 - d; t > th1 + d/2; t -= d)
        {
            gs_point_transform(cos(t), sin(t), mtx, &p);
            gs_lineto(ctx->pgs, p.x, p.y);
        }
    }
}

/* Given two vectors find the angle between them. */
static inline double
angle_between(const gs_point u, const gs_point v)
{
    double det = u.x * v.y - u.y * v.x;
    double sign = (det < 0 ? -1.0 : 1.0);
    double magu = u.x * u.x + u.y * u.y;
    double magv = v.x * v.x + v.y * v.y;
    double udotv = u.x * v.x + u.y * v.y;
    double t = udotv / (magu * magv);
    /* guard against rounding errors when near |1| (where acos will return NaN) */
    if (t < -1.0) t = -1.0;
    if (t > 1.0) t = 1.0;
    return sign * acos(t);
}

static void
xps_draw_arc(xps_context_t *ctx,
        float size_x, float size_y, float rotation_angle,
        int is_large_arc, int is_clockwise,
        float point_x, float point_y)
{
    gs_matrix rotmat, revmat;
    gs_matrix mtx;
    gs_point pt;
    double rx, ry;
    double x1, y1, x2, y2;
    double x1t, y1t;
    double cxt, cyt, cx, cy;
    double t1, t2, t3;
    double sign;
    double th1, dth;

    gs_currentpoint(ctx->pgs, &pt);
    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;

    gs_make_rotation(rotation_angle, &rotmat);
    gs_make_rotation(-rotation_angle, &revmat);

    /* 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.001 || ry < 0.001 || (x1 == x2 && y1 == y2))
    {
        gs_lineto(ctx->pgs, x2, y2);
        return;
    }

    /* F.6.5.1 */
    gs_distance_transform((x1 - x2) / 2.0, (y1 - y2) / 2.0, &revmat, &pt);
    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.0)
    {
        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.0) t3 = 0.0;
    t3 = sqrtf(t3);

    cxt = sign * t3 * (rx * y1t) / ry;
    cyt = sign * t3 * -(ry * x1t) / rx;

    /* F.6.5.3 */
    gs_distance_transform(cxt, cyt, &rotmat, &pt);
    cx = pt.x + (x1 + x2) / 2;
    cy = pt.y + (y1 + y2) / 2;

    /* F.6.5.4 */
    {
        gs_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 += (degrees_to_radians * 360);
        if (dth > 0 && is_clockwise)
            dth -= (degrees_to_radians * 360);
    }

    gs_make_identity(&mtx);
    gs_matrix_translate(&mtx, cx, cy, &mtx);
    gs_matrix_rotate(&mtx, rotation_angle, &mtx);
    gs_matrix_scale(&mtx, rx, ry, &mtx);
    xps_draw_arc_segment(ctx, &mtx, th1, th1 + dth, is_clockwise);

    gs_lineto(ctx->pgs, point_x, point_y);
}

/*
 * Parse an abbreviated geometry string, and call
 * ghostscript moveto/lineto/curveto functions to
 * build up a path.
 */

void
xps_parse_abbreviated_geometry(xps_context_t *ctx, char *geom)
{
    char **args;
    char **pargs;
    char *s = geom;
    gs_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;

    args = xps_alloc(ctx, sizeof(char*) * (strlen(geom) + 1));
    if (!args) {
        gs_throw(gs_error_VMerror, "out of memory: args.\n");
        return;
    }
    pargs = args;

    //dmprintf1(ctx->memory, "new path (%.70s)\n", geom);
    gs_newpath(ctx->pgs);

    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.0;
    smooth_y = 0.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.0;
            smooth_y = 0.0;
        }

        reset_smooth = 1;

        switch (cmd)
        {
        case 'F':
            if (i + 1 <= n)
            {
                ctx->fill_rule = atoi(args[i]);
                i++;
            }
            break;

        case 'M':
            if (i + 2 <= n)
            {
                gs_moveto(ctx->pgs, atof(args[i]), atof(args[i+1]));
                //dmprintf2(ctx->memory, "moveto %g %g\n", atof(args[i]), atof(args[i+1]));
                i += 2;
            }
            break;
        case 'm':
            if (i + 2 <= n)
            {
                gs_rmoveto(ctx->pgs, atof(args[i]), atof(args[i+1]));
                //dmprintf2(ctx->memory, "rmoveto %g %g\n", atof(args[i]), atof(args[i+1]));
                i += 2;
            }
            break;

        case 'L':
            if (i + 2 <= n)
            {
                gs_lineto(ctx->pgs, atof(args[i]), atof(args[i+1]));
                //dmprintf2(ctx->memory, "lineto %g %g\n", atof(args[i]), atof(args[i+1]));
                i += 2;
            }
            break;
        case 'l':
            if (i + 2 <= n)
            {
                gs_rlineto(ctx->pgs, atof(args[i]), atof(args[i+1]));
                //dmprintf2(ctx->memory, "rlineto %g %g\n", atof(args[i]), atof(args[i+1]));
                i += 2;
            }
            break;

        case 'H':
            if (i + 1 <= n)
            {
                gs_currentpoint(ctx->pgs, &pt);
                gs_lineto(ctx->pgs, atof(args[i]), pt.y);
                //dmprintf1(ctx->memory, "hlineto %g\n", atof(args[i]));
                i += 1;
            }
            break;
        case 'h':
            if (i + 1 <= n)
            {
                gs_rlineto(ctx->pgs, atof(args[i]), 0.0);
                //dmprintf1(ctx->memory, "rhlineto %g\n", atof(args[i]));
                i += 1;
            }
            break;

        case 'V':
            if (i + 1 <= n)
            {
                gs_currentpoint(ctx->pgs, &pt);
                gs_lineto(ctx->pgs, pt.x, atof(args[i]));
                //dmprintf1(ctx->memory, "vlineto %g\n", atof(args[i]));
                i += 1;
            }
            break;
        case 'v':
            if (i + 1 <= n)
            {
                gs_rlineto(ctx->pgs, 0.0, atof(args[i]));
                //dmprintf1(ctx->memory, "rvlineto %g\n", atof(args[i]));
                i += 1;
            }
            break;

        case 'C':
            if (i + 6 <= n)
            {
                x1 = atof(args[i+0]);
                y1 = atof(args[i+1]);
                x2 = atof(args[i+2]);
                y2 = atof(args[i+3]);
                x3 = atof(args[i+4]);
                y3 = atof(args[i+5]);
                gs_curveto(ctx->pgs, x1, y1, x2, y2, x3, y3);
                i += 6;
                reset_smooth = 0;
                smooth_x = x3 - x2;
                smooth_y = y3 - y2;
            }
            break;

        case 'c':
            if (i + 6 <= n)
            {
                gs_currentpoint(ctx->pgs, &pt);
                x1 = atof(args[i+0]) + pt.x;
                y1 = atof(args[i+1]) + pt.y;
                x2 = atof(args[i+2]) + pt.x;
                y2 = atof(args[i+3]) + pt.y;
                x3 = atof(args[i+4]) + pt.x;
                y3 = atof(args[i+5]) + pt.y;
                gs_curveto(ctx->pgs, x1, y1, x2, y2, x3, y3);
                i += 6;
                reset_smooth = 0;
                smooth_x = x3 - x2;
                smooth_y = y3 - y2;
            }
            break;

        case 'S':
            if (i + 4 <= n)
            {
                gs_currentpoint(ctx->pgs, &pt);
                x1 = atof(args[i+0]);
                y1 = atof(args[i+1]);
                x2 = atof(args[i+2]);
                y2 = atof(args[i+3]);
                //dmprintf2(ctx->memory, "smooth %g %g\n", smooth_x, smooth_y);
                gs_curveto(ctx->pgs, 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 + 4 <= n)
            {
                gs_currentpoint(ctx->pgs, &pt);
                x1 = atof(args[i+0]) + pt.x;
                y1 = atof(args[i+1]) + pt.y;
                x2 = atof(args[i+2]) + pt.x;
                y2 = atof(args[i+3]) + pt.y;
                //dmprintf2(ctx->memory, "smooth %g %g\n", smooth_x, smooth_y);
                gs_curveto(ctx->pgs, 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 + 4 <= n)
            {
                gs_currentpoint(ctx->pgs, &pt);
                x1 = atof(args[i+0]);
                y1 = atof(args[i+1]);
                x2 = atof(args[i+2]);
                y2 = atof(args[i+3]);
                //dmprintf4(ctx->memory, "conicto %g %g %g %g\n", x1, y1, x2, y2);
                gs_curveto(ctx->pgs,
                        (pt.x + 2 * x1) / 3, (pt.y + 2 * y1) / 3,
                        (x2 + 2 * x1) / 3, (y2 + 2 * y1) / 3,
                        x2, y2);
                i += 4;
            }
            break;
        case 'q':
            if (i + 4 <= n)
            {
                gs_currentpoint(ctx->pgs, &pt);
                x1 = atof(args[i+0]) + pt.x;
                y1 = atof(args[i+1]) + pt.y;
                x2 = atof(args[i+2]) + pt.x;
                y2 = atof(args[i+3]) + pt.y;
                //dmprintf4(ctx->memory, "conicto %g %g %g %g\n", x1, y1, x2, y2);
                gs_curveto(ctx->pgs,
                        (pt.x + 2 * x1) / 3, (pt.y + 2 * y1) / 3,
                        (x2 + 2 * x1) / 3, (y2 + 2 * y1) / 3,
                        x2, y2);
                i += 4;
            }
            break;

        case 'A':
            if (i + 7 <= n)
            {
                xps_draw_arc(ctx,
                        atof(args[i+0]), atof(args[i+1]), atof(args[i+2]),
                        atoi(args[i+3]), atoi(args[i+4]),
                        atof(args[i+5]), atof(args[i+6]));
                i += 7;
            }
            break;
        case 'a':
            if (i + 7 <= n)
            {
                gs_currentpoint(ctx->pgs, &pt);
                xps_draw_arc(ctx,
                        atof(args[i+0]), atof(args[i+1]), atof(args[i+2]),
                        atoi(args[i+3]), atoi(args[i+4]),
                        atof(args[i+5]) + pt.x, atof(args[i+6]) + pt.y);
                i += 7;
            }
            break;

        case 'Z':
        case 'z':
            gs_closepath(ctx->pgs);
            //dmputs(ctx->memory, "closepath\n");
            break;

        default:
            /* eek */
            if (old == cmd) /* avoid infinite loop */
                i++;
            break;
        }

        old = cmd;
    }

    xps_free(ctx, args);
}

static void
xps_parse_arc_segment(xps_context_t *ctx, xps_item_t *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 = xps_att(root, "Point");
    char *size_att = xps_att(root, "Size");
    char *rotation_angle_att = xps_att(root, "RotationAngle");
    char *is_large_arc_att = xps_att(root, "IsLargeArc");
    char *sweep_direction_att = xps_att(root, "SweepDirection");
    char *is_stroked_att = xps_att(root, "IsStroked");

    if (!point_att || !size_att || !rotation_angle_att || !is_large_arc_att || !sweep_direction_att)
    {
        gs_warn("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;

    xps_get_point(point_att, &point_x, &point_y);
    xps_get_point(size_att, &size_x, &size_y);
    rotation_angle = atof(rotation_angle_att);
    is_large_arc = !strcmp(is_large_arc_att, "true");
    is_clockwise = !strcmp(sweep_direction_att, "Clockwise");

    if (stroking && !is_stroked)
    {
        gs_moveto(ctx->pgs, point_x, point_y);
        return;
    }

    xps_draw_arc(ctx, size_x, size_y, rotation_angle, is_large_arc, is_clockwise, point_x, point_y);
}

static void
xps_parse_poly_quadratic_bezier_segment(xps_context_t *ctx, xps_item_t *root, int stroking, int *skipped_stroke)
{
    char *points_att = xps_att(root, "Points");
    char *is_stroked_att = xps_att(root, "IsStroked");
    float x[2], y[2];
    int is_stroked;
    gs_point pt;
    char *s;
    int n;

    if (!points_att)
    {
        gs_warn("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++;
        s = xps_get_point(s, &x[n], &y[n]);
        n ++;
        if (n == 2)
        {
            if (stroking && !is_stroked)
            {
                gs_moveto(ctx->pgs, x[1], y[1]);
            }
            else
            {
                gs_currentpoint(ctx->pgs, &pt);
                gs_curveto(ctx->pgs,
                        (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(xps_context_t *ctx, xps_item_t *root, int stroking, int *skipped_stroke)
{
    char *points_att = xps_att(root, "Points");
    char *is_stroked_att = xps_att(root, "IsStroked");
    float x[3], y[3];
    int is_stroked;
    char *s;
    int n;

    if (!points_att)
    {
        gs_warn("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++;
        s = xps_get_point(s, &x[n], &y[n]);
        n ++;
        if (n == 3)
        {
            if (stroking && !is_stroked)
                gs_moveto(ctx->pgs, x[2], y[2]);
            else
                gs_curveto(ctx->pgs, x[0], y[0], x[1], y[1], x[2], y[2]);
            n = 0;
        }
    }
}

static void
xps_parse_poly_line_segment(xps_context_t *ctx, xps_item_t *root, int stroking, int *skipped_stroke)
{
    char *points_att = xps_att(root, "Points");
    char *is_stroked_att = xps_att(root, "IsStroked");
    int is_stroked;
    float xy[2];
    char *s;

    if (!points_att)
    {
        gs_warn("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)
    {
        s = xps_get_real_params(s, 2, &xy[0]);
        if (stroking && !is_stroked)
            gs_moveto(ctx->pgs, xy[0], xy[1]);
        else
            gs_lineto(ctx->pgs, xy[0], xy[1]);
    }
}

static void
xps_parse_path_figure(xps_context_t *ctx, xps_item_t *root, int stroking)
{
    xps_item_t *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.0;
    float start_y = 0.0;

    int skipped_stroke = 0;

    is_closed_att = xps_att(root, "IsClosed");
    start_point_att = xps_att(root, "StartPoint");
    is_filled_att = xps_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_get_point(start_point_att, &start_x, &start_y);

    if (!stroking && !is_filled) /* not filled, when filling */
        return;

    gs_moveto(ctx->pgs, start_x, start_y);

    for (node = xps_down(root); node; node = xps_next(node))
    {
        if (!strcmp(xps_tag(node), "ArcSegment"))
            xps_parse_arc_segment(ctx, node, stroking, &skipped_stroke);
        if (!strcmp(xps_tag(node), "PolyBezierSegment"))
            xps_parse_poly_bezier_segment(ctx, node, stroking, &skipped_stroke);
        if (!strcmp(xps_tag(node), "PolyLineSegment"))
            xps_parse_poly_line_segment(ctx, node, stroking, &skipped_stroke);
        if (!strcmp(xps_tag(node), "PolyQuadraticBezierSegment"))
            xps_parse_poly_quadratic_bezier_segment(ctx, node, stroking, &skipped_stroke);
    }

    if (is_closed)
    {
        if (stroking && skipped_stroke)
            gs_lineto(ctx->pgs, start_x, start_y); /* we've skipped using gs_moveto... */
        else
            gs_closepath(ctx->pgs); /* no skipped segments, safe to closepath properly */
    }
}

void
xps_parse_path_geometry(xps_context_t *ctx, xps_resource_t *dict, xps_item_t *root, int stroking)
{
    xps_item_t *node;

    char *figures_att;
    char *fill_rule_att;
    char *transform_att;

    xps_item_t *transform_tag = NULL;
    xps_item_t *figures_tag = NULL; /* only used by resource */

    gs_matrix transform;
    gs_matrix saved_transform;

    gs_newpath(ctx->pgs);

    figures_att = xps_att(root, "Figures");
    fill_rule_att = xps_att(root, "FillRule");
    transform_att = xps_att(root, "Transform");

    for (node = xps_down(root); node; node = xps_next(node))
    {
        if (!strcmp(xps_tag(node), "PathGeometry.Transform"))
            transform_tag = xps_down(node);
    }

    xps_resolve_resource_reference(ctx, dict, &transform_att, &transform_tag, NULL);
    xps_resolve_resource_reference(ctx, dict, &figures_att, &figures_tag, NULL);

    if (fill_rule_att)
    {
        if (!strcmp(fill_rule_att, "NonZero"))
            ctx->fill_rule = 1;
        if (!strcmp(fill_rule_att, "EvenOdd"))
            ctx->fill_rule = 0;
    }

    gs_make_identity(&transform);
    if (transform_att || transform_tag)
    {
        if (transform_att)
            xps_parse_render_transform(ctx, transform_att, &transform);
        if (transform_tag)
            xps_parse_matrix_transform(ctx, transform_tag, &transform);
    }

    gs_currentmatrix(ctx->pgs, &saved_transform);
    gs_concat(ctx->pgs, &transform);

    if (figures_att)
    {
        xps_parse_abbreviated_geometry(ctx, figures_att);
    }

    if (figures_tag)
    {
        xps_parse_path_figure(ctx, figures_tag, stroking);
    }

    for (node = xps_down(root); node; node = xps_next(node))
    {
        if (!strcmp(xps_tag(node), "PathFigure"))
            xps_parse_path_figure(ctx, node, stroking);
    }

    gs_setmatrix(ctx->pgs, &saved_transform);
}

static int
xps_parse_line_cap(char *attr)
{
    if (attr)
    {
        if (!strcmp(attr, "Flat")) return gs_cap_butt;
        if (!strcmp(attr, "Square")) return gs_cap_square;
        if (!strcmp(attr, "Round")) return gs_cap_round;
        if (!strcmp(attr, "Triangle")) return gs_cap_triangle;
    }
    return gs_cap_butt;
}

/*
 * Parse an XPS <Path> element, and call relevant ghostscript
 * functions for drawing and/or clipping the child elements.
 */

int
xps_parse_path(xps_context_t *ctx, char *base_uri, xps_resource_t *dict, xps_item_t *root)
{
    xps_item_t *node;
    int code;

    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;

    xps_item_t *transform_tag = NULL;
    xps_item_t *clip_tag = NULL;
    xps_item_t *data_tag = NULL;
    xps_item_t *fill_tag = NULL;
    xps_item_t *stroke_tag = NULL;
    xps_item_t *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;

    gs_line_join linejoin;
    float linewidth;
    float miterlimit;
    float samples[XPS_MAX_COLORS];

    bool opacity_pushed = false;
    bool uses_stroke = false;

    gs_gsave(ctx->pgs);

    ctx->fill_rule = 0;

    /*
     * Extract attributes and extended attributes.
     */

    transform_att = xps_att(root, "RenderTransform");
    clip_att = xps_att(root, "Clip");
    data_att = xps_att(root, "Data");
    fill_att = xps_att(root, "Fill");
    stroke_att = xps_att(root, "Stroke");
    opacity_att = xps_att(root, "Opacity");
    opacity_mask_att = xps_att(root, "OpacityMask");

    stroke_dash_array_att = xps_att(root, "StrokeDashArray");
    stroke_dash_cap_att = xps_att(root, "StrokeDashCap");
    stroke_dash_offset_att = xps_att(root, "StrokeDashOffset");
    stroke_end_line_cap_att = xps_att(root, "StrokeEndLineCap");
    stroke_start_line_cap_att = xps_att(root, "StrokeStartLineCap");
    stroke_line_join_att = xps_att(root, "StrokeLineJoin");
    stroke_miter_limit_att = xps_att(root, "StrokeMiterLimit");
    stroke_thickness_att = xps_att(root, "StrokeThickness");

    for (node = xps_down(root); node; node = xps_next(node))
    {
        if (!strcmp(xps_tag(node), "Path.RenderTransform"))
            transform_tag = xps_down(node);

        if (!strcmp(xps_tag(node), "Path.OpacityMask"))
            opacity_mask_tag = xps_down(node);

        if (!strcmp(xps_tag(node), "Path.Clip"))
            clip_tag = xps_down(node);

        if (!strcmp(xps_tag(node), "Path.Fill"))
            fill_tag = xps_down(node);

        if (!strcmp(xps_tag(node), "Path.Stroke"))
            stroke_tag = xps_down(node);

        if (!strcmp(xps_tag(node), "Path.Data"))
            data_tag = xps_down(node);
    }

    fill_uri = base_uri;
    stroke_uri = base_uri;
    opacity_mask_uri = base_uri;

    xps_resolve_resource_reference(ctx, dict, &data_att, &data_tag, NULL);
    xps_resolve_resource_reference(ctx, dict, &clip_att, &clip_tag, NULL);
    xps_resolve_resource_reference(ctx, dict, &transform_att, &transform_tag, NULL);
    xps_resolve_resource_reference(ctx, dict, &fill_att, &fill_tag, &fill_uri);
    xps_resolve_resource_reference(ctx, dict, &stroke_att, &stroke_tag, &stroke_uri);
    xps_resolve_resource_reference(ctx, dict, &opacity_mask_att, &opacity_mask_tag, &opacity_mask_uri);

    /*
     * Act on the information we have gathered:
     */

    if (fill_tag && !strcmp(xps_tag(fill_tag), "SolidColorBrush"))
    {
        fill_opacity_att = xps_att(fill_tag, "Opacity");
        fill_att = xps_att(fill_tag, "Color");
        fill_tag = NULL;
    }

    if (stroke_tag && !strcmp(xps_tag(stroke_tag), "SolidColorBrush"))
    {
        stroke_opacity_att = xps_att(stroke_tag, "Opacity");
        stroke_att = xps_att(stroke_tag, "Color");
        stroke_tag = NULL;
    }

    gs_setlinestartcap(ctx->pgs, xps_parse_line_cap(stroke_start_line_cap_att));
    gs_setlineendcap(ctx->pgs, xps_parse_line_cap(stroke_end_line_cap_att));
    gs_setlinedashcap(ctx->pgs, xps_parse_line_cap(stroke_dash_cap_att));

    linejoin = gs_join_miter;
    if (stroke_line_join_att)
    {
        if (!strcmp(stroke_line_join_att, "Miter")) linejoin = gs_join_miter;
        if (!strcmp(stroke_line_join_att, "Bevel")) linejoin = gs_join_bevel;
        if (!strcmp(stroke_line_join_att, "Round")) linejoin = gs_join_round;
    }
    gs_setlinejoin(ctx->pgs, linejoin);

    miterlimit = 10.0;
    if (stroke_miter_limit_att)
        miterlimit = atof(stroke_miter_limit_att);
    gs_setmiterlimit(ctx->pgs, miterlimit);

    linewidth = 1.0;
    if (stroke_thickness_att)
        linewidth = atof(stroke_thickness_att);
    gs_setlinewidth(ctx->pgs, linewidth);

    if (stroke_dash_array_att)
    {
        char *s = stroke_dash_array_att;
        float dash_array[100];
        float dash_offset = 0.0;
        int dash_count = 0;

        if (stroke_dash_offset_att)
            dash_offset = atof(stroke_dash_offset_att) * linewidth;

        while (*s)
        {
            while (*s == ' ')
                s++;
            if (*s) /* needed in case of a space before the last quote */
                dash_array[dash_count++] = atof(s) * linewidth;
            while (*s && *s != ' ')
                s++;
        }

        if (dash_count > 0)
        {
            float phase_len = 0;
            int i;
            for (i = 0; i < dash_count; ++i)
                phase_len += dash_array[i];
            if (phase_len == 0)
                dash_count = 0;
        }

        gs_setdash(ctx->pgs, dash_array, dash_count, dash_offset);
    }
    else
    {
        gs_setdash(ctx->pgs, NULL, 0, 0.0);
    }

    if (transform_att || transform_tag)
    {
        gs_matrix transform;

        if (transform_att)
            xps_parse_render_transform(ctx, transform_att, &transform);
        if (transform_tag)
            xps_parse_matrix_transform(ctx, transform_tag, &transform);

        gs_concat(ctx->pgs, &transform);
    }

    if (clip_att || clip_tag)
    {
        if (clip_att)
            xps_parse_abbreviated_geometry(ctx, clip_att);
        if (clip_tag)
            xps_parse_path_geometry(ctx, dict, clip_tag, 0);
        xps_clip(ctx);
    }

#if 0 // XXX
    if (opacity_att || opacity_mask_tag)
    {
        /* clip the bounds with the actual path */
        if (data_att)
            xps_parse_abbreviated_geometry(ctx, data_att);
        if (data_tag)
            xps_parse_path_geometry(ctx, dict, data_tag, 0);
        xps_update_bounds(ctx, &saved_bounds_opacity);
        gs_newpath(ctx->pgs);
    }
#endif
    /* xps_begin_opacity put into the fill_att, etc loops so that we can
       push groups of smaller sizes.  This makes it necessary to add a pushed
       flag so that we don't do multiple pushes if we have a fill and stroke
       attribute for the same group. */
    if (stroke_att || stroke_tag) {
        uses_stroke = true;
    }
    if (fill_att)
    {
        gs_color_space *colorspace;

        if (data_att)
            xps_parse_abbreviated_geometry(ctx, data_att);
        if (data_tag)
            xps_parse_path_geometry(ctx, dict, data_tag, 0);

        code = xps_begin_opacity(ctx, opacity_mask_uri, dict, opacity_att, opacity_mask_tag, true, uses_stroke);
        if (code)
        {
            gs_grestore(ctx->pgs);
            return gs_rethrow(code, "cannot create transparency group");
        }

        /* Color must be set *after* we begin opacity */
        xps_parse_color(ctx, base_uri, fill_att, &colorspace, samples);
        if (fill_opacity_att)
            samples[0] *= atof(fill_opacity_att);
        xps_set_color(ctx, colorspace, samples);
        rc_decrement(colorspace, "xps_parse_path");

        opacity_pushed = true;
        xps_fill(ctx);
    }

    if (fill_tag)
    {
        if (data_att)
            xps_parse_abbreviated_geometry(ctx, data_att);
        if (data_tag)
            xps_parse_path_geometry(ctx, dict, data_tag, 0);

        if (!opacity_pushed) {
            code = xps_begin_opacity(ctx, opacity_mask_uri, dict, opacity_att, opacity_mask_tag, true, uses_stroke);
            if (code)
            {
                gs_grestore(ctx->pgs);
                return gs_rethrow(code, "cannot create transparency group");
            }
            opacity_pushed = true;
        }

        code = xps_parse_brush(ctx, fill_uri, dict, fill_tag);
        if (code < 0)
        {
            xps_end_opacity(ctx, opacity_mask_uri, dict, opacity_att, opacity_mask_tag);
            gs_grestore(ctx->pgs);
            return gs_rethrow(code, "cannot parse fill brush");
        }
    }

    if (stroke_att)
    {
        gs_color_space *colorspace;

        if (data_att)
            xps_parse_abbreviated_geometry(ctx, data_att);
        if (data_tag)
            xps_parse_path_geometry(ctx, dict, data_tag, 1);

        if (!opacity_pushed) {
            code = xps_begin_opacity(ctx, opacity_mask_uri, dict, opacity_att, opacity_mask_tag, true, true);
            if (code)
            {
                gs_grestore(ctx->pgs);
                return gs_rethrow(code, "cannot create transparency group");
            }
            opacity_pushed = true;
        }

        /* Color must be set *after* the group is pushed */
        xps_parse_color(ctx, base_uri, stroke_att, &colorspace, samples);
        if (stroke_opacity_att)
            samples[0] *= atof(stroke_opacity_att);
        xps_set_color(ctx, colorspace, samples);
        rc_decrement(colorspace, "xps_parse_path");

        gs_stroke(ctx->pgs);
    }

    if (stroke_tag)
    {
        if (data_att)
            xps_parse_abbreviated_geometry(ctx, data_att);
        if (data_tag)
            xps_parse_path_geometry(ctx, dict, data_tag, 1);

        if (!opacity_pushed) {
            code = xps_begin_opacity(ctx, opacity_mask_uri, dict, opacity_att, opacity_mask_tag, true, true);
            if (code)
            {
                gs_grestore(ctx->pgs);
                return gs_rethrow(code, "cannot create transparency group");
            }
            opacity_pushed = true;
        }

        ctx->fill_rule = 1; /* over-ride fill rule when converting outline to stroked */
        gs_strokepath2(ctx->pgs);

        code = xps_parse_brush(ctx, stroke_uri, dict, stroke_tag);
        if (code < 0)
        {
            xps_end_opacity(ctx, opacity_mask_uri, dict, opacity_att, opacity_mask_tag);
            gs_grestore(ctx->pgs);
            return gs_rethrow(code, "cannot parse stroke brush");
        }
    }

    xps_end_opacity(ctx, opacity_mask_uri, dict, opacity_att, opacity_mask_tag);
    gs_grestore(ctx->pgs);
    return 0;
}
