#include "mupdf/fitz.h"

typedef struct fz_display_node_s fz_display_node;

#define STACK_SIZE 96

typedef enum fz_display_command_e
{
	FZ_CMD_BEGIN_PAGE,
	FZ_CMD_END_PAGE,
	FZ_CMD_FILL_PATH,
	FZ_CMD_STROKE_PATH,
	FZ_CMD_CLIP_PATH,
	FZ_CMD_CLIP_STROKE_PATH,
	FZ_CMD_FILL_TEXT,
	FZ_CMD_STROKE_TEXT,
	FZ_CMD_CLIP_TEXT,
	FZ_CMD_CLIP_STROKE_TEXT,
	FZ_CMD_IGNORE_TEXT,
	FZ_CMD_FILL_SHADE,
	FZ_CMD_FILL_IMAGE,
	FZ_CMD_FILL_IMAGE_MASK,
	FZ_CMD_CLIP_IMAGE_MASK,
	FZ_CMD_POP_CLIP,
	FZ_CMD_BEGIN_MASK,
	FZ_CMD_END_MASK,
	FZ_CMD_BEGIN_GROUP,
	FZ_CMD_END_GROUP,
	FZ_CMD_BEGIN_TILE,
	FZ_CMD_END_TILE
	, FZ_CMD_APPLY_TRANSFER_FUNCTION, /* SumatraPDF: support transfer functions */
} fz_display_command;

struct fz_display_node_s
{
	fz_display_command cmd;
	fz_display_node *next;
	fz_rect rect;
	union {
		fz_path *path;
		fz_text *text;
		fz_shade *shade;
		fz_image *image;
		int blendmode;
		fz_transfer_function *tr; /* SumatraPDF: support transfer functions */
	} item;
	fz_stroke_state *stroke;
	int flag; /* even_odd, accumulate, isolated/knockout... */
	fz_matrix ctm;
	fz_colorspace *colorspace;
	float alpha;
	float color[FZ_MAX_COLORS];
};

struct fz_display_list_s
{
	fz_storable storable;
	fz_display_node *first;
	fz_display_node *last;
	int len;

	int top;
	struct {
		fz_rect *update;
		fz_rect rect;
	} stack[STACK_SIZE];
	int tiled;
};

enum { ISOLATED = 1, KNOCKOUT = 2 };

static fz_display_node *
fz_new_display_node(fz_context *ctx, fz_display_command cmd, const fz_matrix *ctm,
	fz_colorspace *colorspace, float *color, float alpha)
{
	fz_display_node *node;
	int i;

	node = fz_malloc_struct(ctx, fz_display_node);
	node->cmd = cmd;
	node->next = NULL;
	node->rect = fz_empty_rect;
	node->item.path = NULL;
	node->stroke = NULL;
	node->flag = (cmd == FZ_CMD_BEGIN_TILE ? fz_gen_id(ctx) : 0);
	node->ctm = *ctm;
	if (colorspace)
	{
		node->colorspace = fz_keep_colorspace(ctx, colorspace);
		if (color)
		{
			for (i = 0; i < node->colorspace->n; i++)
				node->color[i] = color[i];
		}
	}
	else
	{
		node->colorspace = NULL;
	}
	node->alpha = alpha;

	return node;
}

static void
fz_append_display_node(fz_display_list *list, fz_display_node *node)
{
	switch (node->cmd)
	{
	case FZ_CMD_CLIP_PATH:
	case FZ_CMD_CLIP_STROKE_PATH:
	case FZ_CMD_CLIP_IMAGE_MASK:
		if (list->top < STACK_SIZE)
		{
			list->stack[list->top].update = &node->rect;
			list->stack[list->top].rect = fz_empty_rect;
		}
		list->top++;
		break;
	case FZ_CMD_CLIP_TEXT:
		/* don't reset the clip rect for accumulated text */
		if (node->flag == 2)
			break;
		/* fallthrough */
	case FZ_CMD_END_MASK:
	case FZ_CMD_CLIP_STROKE_TEXT:
		if (list->top < STACK_SIZE)
		{
			list->stack[list->top].update = NULL;
			list->stack[list->top].rect = fz_empty_rect;
		}
		list->top++;
		break;
	case FZ_CMD_BEGIN_TILE:
		list->tiled++;
		if (list->top > 0 && list->top <= STACK_SIZE)
		{
			list->stack[list->top-1].rect = fz_infinite_rect;
		}
		break;
	case FZ_CMD_END_TILE:
		list->tiled--;
		break;
	case FZ_CMD_END_GROUP:
		break;
	case FZ_CMD_POP_CLIP:
		if (list->top > STACK_SIZE)
		{
			list->top--;
			node->rect = fz_infinite_rect;
		}
		else if (list->top > 0)
		{
			fz_rect *update;
			list->top--;
			update = list->stack[list->top].update;
			if (list->tiled == 0)
			{
				if (update)
				{
					fz_intersect_rect(update, &list->stack[list->top].rect);
					node->rect = *update;
				}
				else
					node->rect = list->stack[list->top].rect;
			}
			else
				node->rect = fz_infinite_rect;
		}
		/* fallthrough */
	default:
		if (list->top > 0 && list->tiled == 0 && list->top <= STACK_SIZE)
			fz_union_rect(&list->stack[list->top-1].rect, &node->rect);
		break;
	}
	if (!list->first)
	{
		list->first = node;
		list->last = node;
	}
	else
	{
		list->last->next = node;
		list->last = node;
	}
	list->len++;
}

static void
fz_free_display_node(fz_context *ctx, fz_display_node *node)
{
	switch (node->cmd)
	{
	case FZ_CMD_FILL_PATH:
	case FZ_CMD_STROKE_PATH:
	case FZ_CMD_CLIP_PATH:
	case FZ_CMD_CLIP_STROKE_PATH:
		fz_free_path(ctx, node->item.path);
		break;
	case FZ_CMD_FILL_TEXT:
	case FZ_CMD_STROKE_TEXT:
	case FZ_CMD_CLIP_TEXT:
	case FZ_CMD_CLIP_STROKE_TEXT:
	case FZ_CMD_IGNORE_TEXT:
		fz_free_text(ctx, node->item.text);
		break;
	case FZ_CMD_FILL_SHADE:
		fz_drop_shade(ctx, node->item.shade);
		break;
	case FZ_CMD_FILL_IMAGE:
	case FZ_CMD_FILL_IMAGE_MASK:
	case FZ_CMD_CLIP_IMAGE_MASK:
		fz_drop_image(ctx, node->item.image);
		break;
	case FZ_CMD_POP_CLIP:
	case FZ_CMD_BEGIN_MASK:
	case FZ_CMD_END_MASK:
	case FZ_CMD_BEGIN_GROUP:
	case FZ_CMD_END_GROUP:
	case FZ_CMD_BEGIN_TILE:
	case FZ_CMD_END_TILE:
	case FZ_CMD_BEGIN_PAGE:
	case FZ_CMD_END_PAGE:
		break;
	/* SumatraPDF: support transfer functions */
	case FZ_CMD_APPLY_TRANSFER_FUNCTION:
		fz_drop_transfer_function(ctx, node->item.tr);
		break;
	}
	if (node->stroke)
		fz_drop_stroke_state(ctx, node->stroke);
	if (node->colorspace)
		fz_drop_colorspace(ctx, node->colorspace);
	fz_free(ctx, node);
}

static void
fz_list_begin_page(fz_device *dev, const fz_rect *mediabox, const fz_matrix *ctm)
{
	fz_context *ctx = dev->ctx;
	fz_display_node *node = fz_new_display_node(ctx, FZ_CMD_BEGIN_PAGE, ctm, NULL, NULL, 0);
	node->rect = *mediabox;
	fz_transform_rect(&node->rect, ctm);
	fz_append_display_node(dev->user, node);
}

static void
fz_list_end_page(fz_device *dev)
{
	fz_context *ctx = dev->ctx;
	fz_display_node *node = fz_new_display_node(ctx, FZ_CMD_END_PAGE, &fz_identity, NULL, NULL, 0);
	fz_append_display_node(dev->user, node);
}

static void
fz_list_fill_path(fz_device *dev, fz_path *path, int even_odd, const fz_matrix *ctm,
	fz_colorspace *colorspace, float *color, float alpha)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_FILL_PATH, ctm, colorspace, color, alpha);
	fz_try(ctx)
	{
		fz_bound_path(dev->ctx, path, NULL, ctm, &node->rect);
		node->item.path = fz_clone_path(dev->ctx, path);
		node->flag = even_odd;
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_stroke_path(fz_device *dev, fz_path *path, fz_stroke_state *stroke,
	const fz_matrix *ctm, fz_colorspace *colorspace, float *color, float alpha)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_STROKE_PATH, ctm, colorspace, color, alpha);
	fz_try(ctx)
	{
		fz_bound_path(dev->ctx, path, stroke, ctm, &node->rect);
		node->item.path = fz_clone_path(dev->ctx, path);
		node->stroke = fz_keep_stroke_state(dev->ctx, stroke);
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_clip_path(fz_device *dev, fz_path *path, const fz_rect *rect, int even_odd, const fz_matrix *ctm)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_CLIP_PATH, ctm, NULL, NULL, 0);
	fz_try(ctx)
	{
		fz_bound_path(dev->ctx, path, NULL, ctm, &node->rect);
		if (rect)
			fz_intersect_rect(&node->rect, rect);
		node->item.path = fz_clone_path(dev->ctx, path);
		node->flag = even_odd;
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_clip_stroke_path(fz_device *dev, fz_path *path, const fz_rect *rect, fz_stroke_state *stroke, const fz_matrix *ctm)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_CLIP_STROKE_PATH, ctm, NULL, NULL, 0);
	fz_try(ctx)
	{
		fz_bound_path(dev->ctx, path, stroke, ctm, &node->rect);
		if (rect)
			fz_intersect_rect(&node->rect, rect);
		node->item.path = fz_clone_path(dev->ctx, path);
		node->stroke = fz_keep_stroke_state(dev->ctx, stroke);
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_fill_text(fz_device *dev, fz_text *text, const fz_matrix *ctm,
	fz_colorspace *colorspace, float *color, float alpha)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_FILL_TEXT, ctm, colorspace, color, alpha);
	fz_try(ctx)
	{
		fz_bound_text(dev->ctx, text, NULL, ctm, &node->rect);
		node->item.text = fz_clone_text(dev->ctx, text);
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_stroke_text(fz_device *dev, fz_text *text, fz_stroke_state *stroke, const fz_matrix *ctm,
	fz_colorspace *colorspace, float *color, float alpha)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_STROKE_TEXT, ctm, colorspace, color, alpha);
	node->item.text = NULL;
	fz_try(ctx)
	{
		fz_bound_text(dev->ctx, text, stroke, ctm, &node->rect);
		node->item.text = fz_clone_text(dev->ctx, text);
		node->stroke = fz_keep_stroke_state(dev->ctx, stroke);
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_clip_text(fz_device *dev, fz_text *text, const fz_matrix *ctm, int accumulate)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_CLIP_TEXT, ctm, NULL, NULL, 0);
	fz_try(ctx)
	{
		fz_bound_text(dev->ctx, text, NULL, ctm, &node->rect);
		node->item.text = fz_clone_text(dev->ctx, text);
		node->flag = accumulate;
		/* when accumulating, be conservative about culling */
		if (accumulate)
			node->rect = fz_infinite_rect;
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_clip_stroke_text(fz_device *dev, fz_text *text, fz_stroke_state *stroke, const fz_matrix *ctm)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_CLIP_STROKE_TEXT, ctm, NULL, NULL, 0);
	fz_try(ctx)
	{
		fz_bound_text(dev->ctx, text, stroke, ctm, &node->rect);
		node->item.text = fz_clone_text(dev->ctx, text);
		node->stroke = fz_keep_stroke_state(dev->ctx, stroke);
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_ignore_text(fz_device *dev, fz_text *text, const fz_matrix *ctm)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_IGNORE_TEXT, ctm, NULL, NULL, 0);
	fz_try(ctx)
	{
		fz_bound_text(dev->ctx, text, NULL, ctm, &node->rect);
		node->item.text = fz_clone_text(dev->ctx, text);
	}
	fz_catch(ctx)
	{
		fz_free_display_node(ctx, node);
		fz_rethrow(ctx);
	}
	fz_append_display_node(dev->user, node);
}

static void
fz_list_pop_clip(fz_device *dev)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_POP_CLIP, &fz_identity, NULL, NULL, 0);
	fz_append_display_node(dev->user, node);
}

static void
fz_list_fill_shade(fz_device *dev, fz_shade *shade, const fz_matrix *ctm, float alpha)
{
	fz_display_node *node;
	fz_context *ctx = dev->ctx;
	node = fz_new_display_node(ctx, FZ_CMD_FILL_SHADE, ctm, NULL, NULL, alpha);
	fz_bound_shade(ctx, shade, ctm, &node->rect);
	node->item.shade = fz_keep_shade(ctx, shade);
	fz_append_display_node(dev->user, node);
}

static void
fz_list_fill_image(fz_device *dev, fz_image *image, const fz_matrix *ctm, float alpha)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_FILL_IMAGE, ctm, NULL, NULL, alpha);
	node->rect = fz_unit_rect;
	fz_transform_rect(&node->rect, ctm);
	node->item.image = fz_keep_image(dev->ctx, image);
	fz_append_display_node(dev->user, node);
}

static void
fz_list_fill_image_mask(fz_device *dev, fz_image *image, const fz_matrix *ctm,
	fz_colorspace *colorspace, float *color, float alpha)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_FILL_IMAGE_MASK, ctm, colorspace, color, alpha);
	node->rect = fz_unit_rect;
	fz_transform_rect(&node->rect, ctm);
	node->item.image = fz_keep_image(dev->ctx, image);
	fz_append_display_node(dev->user, node);
}

static void
fz_list_clip_image_mask(fz_device *dev, fz_image *image, const fz_rect *rect, const fz_matrix *ctm)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_CLIP_IMAGE_MASK, ctm, NULL, NULL, 0);
	node->rect = fz_unit_rect;
	fz_transform_rect(&node->rect, ctm);
	if (rect)
		fz_intersect_rect(&node->rect, rect);
	node->item.image = fz_keep_image(dev->ctx, image);
	fz_append_display_node(dev->user, node);
}

static void
fz_list_begin_mask(fz_device *dev, const fz_rect *rect, int luminosity, fz_colorspace *colorspace, float *color)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_BEGIN_MASK, &fz_identity, colorspace, color, 0);
	node->rect = *rect;
	node->flag = luminosity;
	fz_append_display_node(dev->user, node);
}

static void
fz_list_end_mask(fz_device *dev)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_END_MASK, &fz_identity, NULL, NULL, 0);
	fz_append_display_node(dev->user, node);
}

static void
fz_list_begin_group(fz_device *dev, const fz_rect *rect, int isolated, int knockout, int blendmode, float alpha)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_BEGIN_GROUP, &fz_identity, NULL, NULL, alpha);
	node->rect = *rect;
	node->item.blendmode = blendmode;
	node->flag |= isolated ? ISOLATED : 0;
	node->flag |= knockout ? KNOCKOUT : 0;
	fz_append_display_node(dev->user, node);
}

static void
fz_list_end_group(fz_device *dev)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_END_GROUP, &fz_identity, NULL, NULL, 0);
	fz_append_display_node(dev->user, node);
}

static int
fz_list_begin_tile(fz_device *dev, const fz_rect *area, const fz_rect *view, float xstep, float ystep, const fz_matrix *ctm, int id)
{
	/* We ignore id here, as we will pass on our own id */
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_BEGIN_TILE, ctm, NULL, NULL, 0);
	node->rect = *area;
	node->color[0] = xstep;
	node->color[1] = ystep;
	node->color[2] = view->x0;
	node->color[3] = view->y0;
	node->color[4] = view->x1;
	node->color[5] = view->y1;
	fz_append_display_node(dev->user, node);
	return 0;
}

static void
fz_list_end_tile(fz_device *dev)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_END_TILE, &fz_identity, NULL, NULL, 0);
	fz_append_display_node(dev->user, node);
}

/* SumatraPDF: support transfer functions */
static void
fz_list_apply_transfer_function(fz_device *dev, fz_transfer_function *tr, int for_mask)
{
	fz_display_node *node;
	node = fz_new_display_node(dev->ctx, FZ_CMD_APPLY_TRANSFER_FUNCTION, &fz_identity, NULL, NULL, 0);
	node->item.tr = fz_keep_transfer_function(dev->ctx, tr);
	node->flag = for_mask;
	node->rect = fz_infinite_rect;
	fz_append_display_node(dev->user, node);
}

fz_device *
fz_new_list_device(fz_context *ctx, fz_display_list *list)
{
	fz_device *dev = fz_new_device(ctx, list);

	dev->begin_page = fz_list_begin_page;
	dev->end_page = fz_list_end_page;

	dev->fill_path = fz_list_fill_path;
	dev->stroke_path = fz_list_stroke_path;
	dev->clip_path = fz_list_clip_path;
	dev->clip_stroke_path = fz_list_clip_stroke_path;

	dev->fill_text = fz_list_fill_text;
	dev->stroke_text = fz_list_stroke_text;
	dev->clip_text = fz_list_clip_text;
	dev->clip_stroke_text = fz_list_clip_stroke_text;
	dev->ignore_text = fz_list_ignore_text;

	dev->fill_shade = fz_list_fill_shade;
	dev->fill_image = fz_list_fill_image;
	dev->fill_image_mask = fz_list_fill_image_mask;
	dev->clip_image_mask = fz_list_clip_image_mask;

	dev->pop_clip = fz_list_pop_clip;

	dev->begin_mask = fz_list_begin_mask;
	dev->end_mask = fz_list_end_mask;
	dev->begin_group = fz_list_begin_group;
	dev->end_group = fz_list_end_group;

	dev->begin_tile = fz_list_begin_tile;
	dev->end_tile = fz_list_end_tile;

	/* SumatraPDF: support transfer functions */
	dev->apply_transfer_function = fz_list_apply_transfer_function;

	return dev;
}

static void
fz_free_display_list(fz_context *ctx, fz_storable *list_)
{
	fz_display_list *list = (fz_display_list *)list_;
	fz_display_node *node;

	if (list == NULL)
		return;
	node = list->first;
	while (node)
	{
		fz_display_node *next = node->next;
		fz_free_display_node(ctx, node);
		node = next;
	}
	fz_free(ctx, list);
}

fz_display_list *
fz_new_display_list(fz_context *ctx)
{
	fz_display_list *list = fz_malloc_struct(ctx, fz_display_list);
	FZ_INIT_STORABLE(list, 1, fz_free_display_list);
	list->first = NULL;
	list->last = NULL;
	list->len = 0;
	list->top = 0;
	list->tiled = 0;
	return list;
}

fz_display_list *
fz_keep_display_list(fz_context *ctx, fz_display_list *list)
{
	return (fz_display_list *)fz_keep_storable(ctx, &list->storable);
}

void
fz_drop_display_list(fz_context *ctx, fz_display_list *list)
{
	fz_drop_storable(ctx, &list->storable);
}

static fz_display_node *
skip_to_end_tile(fz_display_node *node, int *progress)
{
	fz_display_node *next;
	int depth = 1;

	/* Skip through until we find the matching end_tile. Note that
	 * (somewhat nastily) we return the PREVIOUS node to this to help
	 * the calling routine. */
	do
	{
		next = node->next;
		if (next == NULL)
			break;
		if (next->cmd == FZ_CMD_BEGIN_TILE)
			depth++;
		else if (next->cmd == FZ_CMD_END_TILE)
		{
			depth--;
			if (depth == 0)
				return node;
		}
		(*progress)++;
		node = next;
	}
	while (1);

	return NULL;
}

void
fz_run_display_list(fz_display_list *list, fz_device *dev, const fz_matrix *top_ctm, const fz_rect *scissor, fz_cookie *cookie)
{
	fz_display_node *node;
	fz_matrix ctm;
	int clipped = 0;
	int tiled = 0;
	int progress = 0;
	fz_context *ctx = dev->ctx;

	if (!scissor)
		scissor = &fz_infinite_rect;

	if (cookie)
	{
		cookie->progress_max = list->len;
		cookie->progress = 0;
	}

	for (node = list->first; node; node = node->next)
	{
		int empty;

		fz_rect node_rect = node->rect;
		fz_transform_rect(&node_rect, top_ctm);

		/* Check the cookie for aborting */
		if (cookie)
		{
			if (cookie->abort)
				break;
			cookie->progress = progress++;
		}

		/* cull objects to draw using a quick visibility test */

		if (tiled ||
			node->cmd == FZ_CMD_BEGIN_TILE || node->cmd == FZ_CMD_END_TILE ||
			node->cmd == FZ_CMD_BEGIN_PAGE || node->cmd == FZ_CMD_END_PAGE)
		{
			empty = 0;
		}
		else
		{
			fz_rect rect = node_rect;
			fz_intersect_rect(&rect, scissor);
			empty = fz_is_empty_rect(&rect);
		}

		if (clipped || empty)
		{
			switch (node->cmd)
			{
			case FZ_CMD_CLIP_PATH:
			case FZ_CMD_CLIP_STROKE_PATH:
			case FZ_CMD_CLIP_STROKE_TEXT:
			case FZ_CMD_CLIP_IMAGE_MASK:
			case FZ_CMD_BEGIN_MASK:
			case FZ_CMD_BEGIN_GROUP:
				clipped++;
				continue;
			case FZ_CMD_CLIP_TEXT:
				/* Accumulated text has no extra pops */
				if (node->flag != 2)
					clipped++;
				continue;
			case FZ_CMD_POP_CLIP:
			case FZ_CMD_END_GROUP:
				if (!clipped)
					goto visible;
				clipped--;
				continue;
			case FZ_CMD_END_MASK:
				if (!clipped)
					goto visible;
				continue;
			default:
				continue;
			}
		}

visible:
		fz_concat(&ctm, &node->ctm, top_ctm);

		fz_try(ctx)
		{
			switch (node->cmd)
			{
			case FZ_CMD_BEGIN_PAGE:
				fz_begin_page(dev, &node_rect, &ctm);
				break;
			case FZ_CMD_END_PAGE:
				fz_end_page(dev);
				break;
			case FZ_CMD_FILL_PATH:
				fz_fill_path(dev, node->item.path, node->flag, &ctm,
					node->colorspace, node->color, node->alpha);
				break;
			case FZ_CMD_STROKE_PATH:
				fz_stroke_path(dev, node->item.path, node->stroke, &ctm,
					node->colorspace, node->color, node->alpha);
				break;
			case FZ_CMD_CLIP_PATH:
				fz_clip_path(dev, node->item.path, &node_rect, node->flag, &ctm);
				break;
			case FZ_CMD_CLIP_STROKE_PATH:
				fz_clip_stroke_path(dev, node->item.path, &node_rect, node->stroke, &ctm);
				break;
			case FZ_CMD_FILL_TEXT:
				fz_fill_text(dev, node->item.text, &ctm,
					node->colorspace, node->color, node->alpha);
				break;
			case FZ_CMD_STROKE_TEXT:
				fz_stroke_text(dev, node->item.text, node->stroke, &ctm,
					node->colorspace, node->color, node->alpha);
				break;
			case FZ_CMD_CLIP_TEXT:
				fz_clip_text(dev, node->item.text, &ctm, node->flag);
				break;
			case FZ_CMD_CLIP_STROKE_TEXT:
				fz_clip_stroke_text(dev, node->item.text, node->stroke, &ctm);
				break;
			case FZ_CMD_IGNORE_TEXT:
				fz_ignore_text(dev, node->item.text, &ctm);
				break;
			case FZ_CMD_FILL_SHADE:
				if ((dev->hints & FZ_IGNORE_SHADE) == 0)
					fz_fill_shade(dev, node->item.shade, &ctm, node->alpha);
				break;
			case FZ_CMD_FILL_IMAGE:
				if ((dev->hints & FZ_IGNORE_IMAGE) == 0)
					fz_fill_image(dev, node->item.image, &ctm, node->alpha);
				break;
			case FZ_CMD_FILL_IMAGE_MASK:
				if ((dev->hints & FZ_IGNORE_IMAGE) == 0)
					fz_fill_image_mask(dev, node->item.image, &ctm,
						node->colorspace, node->color, node->alpha);
				break;
			case FZ_CMD_CLIP_IMAGE_MASK:
				if ((dev->hints & FZ_IGNORE_IMAGE) == 0)
					fz_clip_image_mask(dev, node->item.image, &node_rect, &ctm);
				break;
			case FZ_CMD_POP_CLIP:
				fz_pop_clip(dev);
				break;
			case FZ_CMD_BEGIN_MASK:
				fz_begin_mask(dev, &node_rect, node->flag, node->colorspace, node->color);
				break;
			case FZ_CMD_END_MASK:
				fz_end_mask(dev);
				break;
			case FZ_CMD_BEGIN_GROUP:
				fz_begin_group(dev, &node_rect,
					(node->flag & ISOLATED) != 0, (node->flag & KNOCKOUT) != 0,
					node->item.blendmode, node->alpha);
				break;
			case FZ_CMD_END_GROUP:
				fz_end_group(dev);
				break;
			case FZ_CMD_BEGIN_TILE:
			{
				int cached;
				fz_rect tile_rect;
				tiled++;
				tile_rect.x0 = node->color[2];
				tile_rect.y0 = node->color[3];
				tile_rect.x1 = node->color[4];
				tile_rect.y1 = node->color[5];
				cached = fz_begin_tile_id(dev, &node->rect, &tile_rect, node->color[0], node->color[1], &ctm, node->flag);
				if (cached)
					node = skip_to_end_tile(node, &progress);
				break;
			}
			case FZ_CMD_END_TILE:
				tiled--;
				fz_end_tile(dev);
				break;
			/* SumatraPDF: support transfer functions */
			case FZ_CMD_APPLY_TRANSFER_FUNCTION:
				fz_apply_transfer_function(dev, node->item.tr, node->flag);
				break;
			}
		}
		fz_catch(ctx)
		{
			/* Swallow the error */
			if (cookie)
				cookie->errors++;
			fz_warn(ctx, "Ignoring error during interpretation");
		}
	}
}
